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