1/* Copyright 2013 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.
4 */
5
6/**
7 * @fileoverview
8 * The application side of the application/sandbox WCS interface, used by the
9 * application to exchange messages with the sandbox.
10 */
11
12'use strict';
13
14/** @suppress {duplicate} */
15var remoting = remoting || {};
16
17/**
18 * @param {Window} sandbox The Javascript Window object representing the
19 *     sandboxed WCS driver.
20 * @constructor
21 */
22remoting.WcsSandboxContainer = function(sandbox) {
23  /** @private */
24  this.sandbox_ = sandbox;
25  /** @type {?function(string):void}
26    * @private */
27  this.onConnected_ = null;
28  /** @type {function(remoting.Error):void}
29    * @private */
30  this.onError_ = function(error) {};
31  /** @type {?function(string):void}
32    * @private */
33  this.onIq_ = null;
34  /** @type {Object.<number, XMLHttpRequest>}
35    * @private */
36  this.pendingXhrs_ = {};
37  /** @private */
38  this.localJid_ = '';
39
40  /** @private */
41  this.accessTokenRefreshTimerStarted_ = false;
42
43  window.addEventListener('message', this.onMessage_.bind(this), false);
44
45  if (base.isAppsV2()) {
46    var message = {
47      'command': 'proxyXhrs'
48    };
49    this.sandbox_.postMessage(message, '*');
50  }
51};
52
53/**
54 * @param {function(string):void} onConnected Callback to be called when WCS is
55 *     connected. May be called synchronously if WCS is already connected.
56 * @param {function(remoting.Error):void} onError called in case of an error.
57 * @return {void} Nothing.
58 */
59remoting.WcsSandboxContainer.prototype.connect = function(
60    onConnected, onError) {
61  this.onError_ = onError;
62  this.ensureAccessTokenRefreshTimer_();
63  if (this.localJid_) {
64    onConnected(this.localJid_);
65  } else {
66    this.onConnected_ = onConnected;
67  }
68};
69
70/**
71 * @param {?function(string):void} onIq Callback invoked when an IQ stanza is
72 *     received.
73 * @return {void} Nothing.
74 */
75remoting.WcsSandboxContainer.prototype.setOnIq = function(onIq) {
76  this.onIq_ = onIq;
77};
78
79/**
80 * Refreshes access token and starts a timer to update it periodically.
81 *
82 * @private
83 */
84remoting.WcsSandboxContainer.prototype.ensureAccessTokenRefreshTimer_ =
85    function() {
86  if (this.accessTokenRefreshTimerStarted_) {
87    return;
88  }
89
90  this.refreshAccessToken_();
91  setInterval(this.refreshAccessToken_.bind(this), 60 * 1000);
92  this.accessTokenRefreshTimerStarted_ = true;
93}
94
95/**
96 * @private
97 * @return {void} Nothing.
98 */
99remoting.WcsSandboxContainer.prototype.refreshAccessToken_ = function() {
100  remoting.identity.callWithToken(
101      this.setAccessToken_.bind(this), this.onError_);
102};
103
104/**
105 * @private
106 * @param {string} token The access token.
107 * @return {void}
108 */
109remoting.WcsSandboxContainer.prototype.setAccessToken_ = function(token) {
110  var message = {
111    'command': 'setAccessToken',
112    'token': token
113  };
114  this.sandbox_.postMessage(message, '*');
115};
116
117/**
118 * @param {string} stanza The IQ stanza to send.
119 * @return {void}
120 */
121remoting.WcsSandboxContainer.prototype.sendIq = function(stanza) {
122  var message = {
123    'command': 'sendIq',
124    'stanza': stanza
125  };
126  this.sandbox_.postMessage(message, '*');
127};
128
129/**
130 * Event handler to process messages from the sandbox.
131 *
132 * @param {Event} event
133 */
134remoting.WcsSandboxContainer.prototype.onMessage_ = function(event) {
135  switch (event.data['command']) {
136
137    case 'onLocalJid':
138      /** @type {string} */
139      var localJid = event.data['localJid'];
140      if (localJid === undefined) {
141        console.error('onReady: missing localJid');
142        break;
143      }
144      this.localJid_ = localJid;
145      if (this.onConnected_) {
146        var callback = this.onConnected_;
147        this.onConnected_ = null;
148        callback(localJid);
149      }
150      break;
151
152    case 'onError':
153      /** @type {remoting.Error} */
154      var error = event.data['error'];
155      if (error === undefined) {
156        console.error('onError: missing error code');
157        break;
158      }
159      this.onError_(error);
160      break;
161
162    case 'onIq':
163      /** @type {string} */
164      var stanza = event.data['stanza'];
165      if (stanza === undefined) {
166        console.error('onIq: missing IQ stanza');
167        break;
168      }
169      if (this.onIq_) {
170        this.onIq_(stanza);
171      }
172      break;
173
174    case 'sendXhr':
175      /** @type {number} */
176      var id = event.data['id'];
177      if (id === undefined) {
178        console.error('sendXhr: missing id');
179        break;
180      }
181      /** @type {Object} */
182      var parameters = event.data['parameters'];
183      if (parameters === undefined) {
184        console.error('sendXhr: missing parameters');
185        break;
186      }
187      /** @type {string} */
188      var method = parameters['method'];
189      if (method === undefined) {
190        console.error('sendXhr: missing method');
191        break;
192      }
193      /** @type {string} */
194      var url = parameters['url'];
195      if (url === undefined) {
196        console.error('sendXhr: missing url');
197        break;
198      }
199      /** @type {string} */
200      var data = parameters['data'];
201      if (data === undefined) {
202        console.error('sendXhr: missing data');
203        break;
204      }
205      /** @type {string|undefined}*/
206      var user = parameters['user'];
207      /** @type {string|undefined}*/
208      var password = parameters['password'];
209      var xhr = new XMLHttpRequest;
210      this.pendingXhrs_[id] = xhr;
211      xhr.open(method, url, true, user, password);
212      /** @type {Object} */
213      var headers = parameters['headers'];
214      if (headers) {
215        for (var header in headers) {
216          xhr.setRequestHeader(header, headers[header]);
217        }
218      }
219      xhr.onreadystatechange = this.onReadyStateChange_.bind(this, id);
220      xhr.send(data);
221      break;
222
223    case 'abortXhr':
224      var id = event.data['id'];
225      if (id === undefined) {
226        console.error('abortXhr: missing id');
227        break;
228      }
229      var xhr = this.pendingXhrs_[id]
230      if (!xhr) {
231        // It's possible for an abort and a reply to cross each other on the
232        // IPC channel. In that case, we silently ignore the abort.
233        break;
234      }
235      xhr.abort();
236      break;
237
238    default:
239      console.error('Unexpected message:', event.data['command'], event.data);
240  }
241};
242
243/**
244 * Return a "copy" of an XHR object suitable for postMessage. Specifically,
245 * remove all non-serializable members such as functions.
246 *
247 * @param {XMLHttpRequest} xhr The XHR to serialize.
248 * @return {Object} A serializable version of the input.
249 */
250function sanitizeXhr_(xhr) {
251  /** @type {Object} */
252  var result = {
253    readyState: xhr.readyState,
254    response: xhr.response,
255    responseText: xhr.responseText,
256    responseType: xhr.responseType,
257    responseXML: xhr.responseXML,
258    status: xhr.status,
259    statusText: xhr.statusText,
260    withCredentials: xhr.withCredentials
261  };
262  return result;
263}
264
265/**
266 * @param {number} id The unique ID of the XHR for which the state has changed.
267 * @private
268 */
269remoting.WcsSandboxContainer.prototype.onReadyStateChange_ = function(id) {
270  var xhr = this.pendingXhrs_[id];
271  if (!xhr) {
272    // XHRs are only removed when they have completed, in which case no
273    // further callbacks should be received.
274    console.error('Unexpected callback for xhr', id);
275    return;
276  }
277  var message = {
278    'command': 'xhrStateChange',
279    'id': id,
280    'xhr': sanitizeXhr_(xhr)
281  };
282  this.sandbox_.postMessage(message, '*');
283  if (xhr.readyState == 4) {
284    delete this.pendingXhrs_[id];
285  }
286}
287
288/** @type {remoting.WcsSandboxContainer} */
289remoting.wcsSandbox = null;
290