Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / remoting / webapp / base / js / xmpp_stream_parser.js
blob81ad5bf7f112aa506d3422110101eda6abd8adbe
1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 'use strict';
7 /** @suppress {duplicate} */
8 var remoting = remoting || {};
10 /**
11  * XmppStreamParser is used to parse XMPP stream. Data is fed to the parser
12  * using appendData() method and it calls |onStanzaCallback| and
13  * |onErrorCallback| specified using setCallbacks().
14  *
15  * @constructor
16  */
17 remoting.XmppStreamParser = function() {
18   /** @type {function(Element):void} @private */
19   this.onStanzaCallback_ = function(stanza) {};
20   /** @type {function(string):void} @private */
21   this.onErrorCallback_ = function(error) {};
23   /**
24    * Buffer containing the data that has been received but haven't been parsed.
25    * @private
26    */
27   this.data_ = new ArrayBuffer(0);
29   /**
30    * Current depth in the XML stream.
31    * @private
32    */
33   this.depth_ = 0;
35   /**
36    * Set to true after error.
37    * @private
38    */
39   this.error_ = false;
41   /**
42    * The <stream> opening tag received at the beginning of the stream.
43    * @private
44    */
45   this.startTag_ = '';
47   /**
48    * Closing tag matching |startTag_|.
49    * @private
50    */
51   this.startTagEnd_ = '';
53   /**
54    * String containing current incomplete stanza.
55    * @private
56    */
57   this.currentStanza_ = '';
60 /**
61  * Sets callbacks to be called on incoming stanzas and on error.
62  *
63  * @param {function(Element):void} onStanzaCallback
64  * @param {function(string):void} onErrorCallback
65  */
66 remoting.XmppStreamParser.prototype.setCallbacks =
67     function(onStanzaCallback, onErrorCallback) {
68   this.onStanzaCallback_ = onStanzaCallback;
69   this.onErrorCallback_ = onErrorCallback;
72 /** @param {ArrayBuffer} data */
73 remoting.XmppStreamParser.prototype.appendData = function(data) {
74   console.assert(!this.error_, 'appendData() called in error state.');
76   if (this.data_.byteLength > 0) {
77     // Concatenate two buffers.
78     var newData = new Uint8Array(this.data_.byteLength + data.byteLength);
79     newData.set(new Uint8Array(this.data_), 0);
80     newData.set(new Uint8Array(data), this.data_.byteLength);
81     this.data_ = newData.buffer;
82   } else {
83     this.data_ = data;
84   }
86   // Check if the newly appended data completes XML tag or a piece of text by
87   // looking for '<' and '>' char codes. This has to be done before converting
88   // data to string because the input may not contain complete UTF-8 sequence.
89   var tagStartCode = '<'.charCodeAt(0);
90   var tagEndCode = '>'.charCodeAt(0);
91   var spaceCode = ' '.charCodeAt(0);
92   var tryAgain = true;
93   while (this.data_.byteLength > 0 && tryAgain && !this.error_) {
94     tryAgain = false;
96     // If we are not currently in a middle of a stanza then skip spaces (server
97     // may send spaces periodically as heartbeats) and make sure that the first
98     // character starts XML tag.
99     if (this.depth_ <= 1) {
100       var view = new DataView(this.data_);
101       var firstChar = view.getUint8(0);
102       if (firstChar == spaceCode) {
103         tryAgain = true;
104         this.data_ = this.data_.slice(1);
105         continue;
106       } else if (firstChar != tagStartCode) {
107         var dataAsText = '';
108         try {
109           dataAsText = base.decodeUtf8(this.data_);
110         } catch (exception) {
111           dataAsText = 'charCode = ' + firstChar;
112         }
113         this.processError_('Received unexpected text data: ' + dataAsText);
114         return;
115       }
116     }
118     // Iterate over characters in the buffer to find complete tags.
119     var view = new DataView(this.data_);
120     for (var i = 0; i < view.byteLength; ++i) {
121       var currentChar = view.getUint8(i);
122       if (currentChar == tagStartCode) {
123         if (i > 0) {
124           var text = this.extractStringFromBuffer_(i);
125           if (text == null)
126             return;
127           this.processText_(text);
128           tryAgain = true;
129           break;
130         }
131       } else if (currentChar == tagEndCode) {
132         var tag = this.extractStringFromBuffer_(i + 1);
133         if (tag == null)
134           return;
135         if (tag.charAt(0) != '<') {
136           this.processError_('Received \'>\' without \'<\': ' + tag);
137           return;
138         }
139         this.processTag_(tag);
140         tryAgain = true;
141         break;
142       }
143     }
144   }
148  * @param {string} text
149  * @private
150  */
151 remoting.XmppStreamParser.prototype.processText_ = function(text) {
152   // Tokenization code in appendData() shouldn't allow text tokens in between
153   // stanzas.
154   console.assert(this.depth_ > 1, 'Bad depth: ' + this.depth_ + '.');
155   this.currentStanza_ += text;
159  * @param {string} tag
160  * @private
161  */
162 remoting.XmppStreamParser.prototype.processTag_ = function(tag) {
163   console.assert(tag.charAt(0) == '<' && tag.charAt(tag.length - 1) == '>',
164                  'Malformed tag: ' + tag);
166   this.currentStanza_ += tag;
168   var openTag = tag.charAt(1) != '/';
169   if (openTag) {
170     ++this.depth_;
171     if (this.depth_ == 1) {
172       this.startTag_ = this.currentStanza_;
173       this.currentStanza_ = '';
175       // Create end tag matching the start.
176       var tagName =
177           this.startTag_.substr(1, this.startTag_.length - 2).split(' ', 1)[0];
178       this.startTagEnd_ = '</' + tagName + '>';
180       // Try parsing start together with the end
181       var parsed = this.parseTag_(this.startTag_ + this.startTagEnd_);
182       if (!parsed) {
183         this.processError_('Failed to parse start tag: ' + this.startTag_);
184         return;
185       }
186     }
187   }
189   var closingTag =
190       (tag.charAt(1) == '/') || (tag.charAt(tag.length - 2) == '/');
191   if (closingTag) {
192     // The first start tag is not expected to be closed.
193     if (this.depth_ <= 1) {
194       this.processError_('Unexpected closing tag: ' + tag)
195       return;
196     }
197     --this.depth_;
198     if (this.depth_ == 1) {
199       this.processCompleteStanza_();
200       this.currentStanza_ = '';
201     }
202   }
206  * @private
207  */
208 remoting.XmppStreamParser.prototype.processCompleteStanza_ = function() {
209   var stanza = this.startTag_ + this.currentStanza_ + this.startTagEnd_;
210   var parsed = this.parseTag_(stanza);
211   if (!parsed) {
212     this.processError_('Failed to parse stanza: ' + this.currentStanza_);
213     return;
214   }
215   this.onStanzaCallback_(parsed.firstElementChild);
219  * @param {string} text
220  * @private
221  */
222 remoting.XmppStreamParser.prototype.processError_ = function(text) {
223   this.onErrorCallback_(text);
224   this.error_ = true;
228  * Helper to extract and decode |bytes| bytes from |data_|. Returns NULL in case
229  * the buffer contains invalidUTF-8.
231  * @param {number} bytes Specifies how many bytes should be extracted.
232  * @returns {string?}
233  * @private
234  */
235 remoting.XmppStreamParser.prototype.extractStringFromBuffer_ = function(bytes) {
236   var result = '';
237   try {
238     result = base.decodeUtf8(this.data_.slice(0, bytes));
239   } catch (exception) {
240     this.processError_('Received invalid UTF-8 data.');
241     result = null;
242   }
243   this.data_ = this.data_.slice(bytes);
244   return result;
248  * @param {string} text
249  * @return {Element}
250  * @private
251  */
252 remoting.XmppStreamParser.prototype.parseTag_ = function(text) {
253   /** @type {Document} */
254   var result = new DOMParser().parseFromString(text, 'text/xml');
255   if (result.querySelector('parsererror') != null)
256     return null;
257   return result.firstElementChild;