- add sources.
[platform/framework/web/crosswalk.git] / src / remoting / webapp / remoting.js
1 // Copyright (c) 2012 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 'use strict';
6
7 /** @suppress {duplicate} */
8 var remoting = remoting || {};
9
10 /** @type {remoting.HostSession} */ remoting.hostSession = null;
11
12 /**
13  * @type {boolean} True if this is a v2 app; false if it is a legacy app.
14  */
15 remoting.isAppsV2 = false;
16
17 /**
18  * Show the authorization consent UI and register a one-shot event handler to
19  * continue the authorization process.
20  *
21  * @param {function():void} authContinue Callback to invoke when the user
22  *     clicks "Continue".
23  */
24 function consentRequired_(authContinue) {
25   /** @type {HTMLElement} */
26   var dialog = document.getElementById('auth-dialog');
27   /** @type {HTMLElement} */
28   var button = document.getElementById('auth-button');
29   var consentGranted = function(event) {
30     dialog.hidden = true;
31     button.removeEventListener('click', consentGranted, false);
32     authContinue();
33   };
34   dialog.hidden = false;
35   button.addEventListener('click', consentGranted, false);
36 }
37
38 /**
39  * Entry point for app initialization.
40  */
41 remoting.init = function() {
42   // Determine whether or not this is a V2 web-app. In order to keep the apps
43   // v2 patch as small as possible, all JS changes needed for apps v2 are done
44   // at run-time. Only the manifest is patched.
45   var manifest = chrome.runtime.getManifest();
46   if (manifest && manifest.app && manifest.app.background) {
47     remoting.isAppsV2 = true;
48     var htmlNode = /** @type {HTMLElement} */ (document.body.parentNode);
49     htmlNode.classList.add('apps-v2');
50   }
51
52   if (!remoting.isAppsV2) {
53     migrateLocalToChromeStorage_();
54   }
55
56   remoting.logExtensionInfo_();
57   l10n.localize();
58
59   // Create global objects.
60   remoting.settings = new remoting.Settings();
61   if (remoting.isAppsV2) {
62     remoting.identity = new remoting.Identity(consentRequired_);
63   } else {
64     remoting.oauth2 = new remoting.OAuth2();
65     if (!remoting.oauth2.isAuthenticated()) {
66       document.getElementById('auth-dialog').hidden = false;
67     }
68     remoting.identity = remoting.oauth2;
69   }
70   remoting.stats = new remoting.ConnectionStats(
71       document.getElementById('statistics'));
72   remoting.formatIq = new remoting.FormatIq();
73   remoting.hostList = new remoting.HostList(
74       document.getElementById('host-list'),
75       document.getElementById('host-list-empty'),
76       document.getElementById('host-list-error-message'),
77       document.getElementById('host-list-refresh-failed-button'),
78       document.getElementById('host-list-loading-indicator'));
79   remoting.toolbar = new remoting.Toolbar(
80       document.getElementById('session-toolbar'));
81   remoting.clipboard = new remoting.Clipboard();
82   var sandbox = /** @type {HTMLIFrameElement} */
83       document.getElementById('wcs-sandbox');
84   remoting.wcsSandbox = new remoting.WcsSandboxContainer(sandbox.contentWindow);
85
86   /** @param {remoting.Error} error */
87   var onGetEmailError = function(error) {
88     // No need to show the error message for NOT_AUTHENTICATED
89     // because we will show "auth-dialog".
90     if (error != remoting.Error.NOT_AUTHENTICATED) {
91       remoting.showErrorMessage(error);
92     }
93   }
94   remoting.identity.getEmail(remoting.onEmail, onGetEmailError);
95
96   remoting.showOrHideIT2MeUi();
97   remoting.showOrHideMe2MeUi();
98
99   // The plugin's onFocus handler sends a paste command to |window|, because
100   // it can't send one to the plugin element itself.
101   window.addEventListener('paste', pluginGotPaste_, false);
102   window.addEventListener('copy', pluginGotCopy_, false);
103
104   remoting.initModalDialogs();
105
106   if (isHostModeSupported_()) {
107     var noShare = document.getElementById('chrome-os-no-share');
108     noShare.parentNode.removeChild(noShare);
109   } else {
110     var button = document.getElementById('share-button');
111     button.disabled = true;
112   }
113
114   var onLoad = function() {
115     // Parse URL parameters.
116     var urlParams = getUrlParameters_();
117     if ('mode' in urlParams) {
118       if (urlParams['mode'] == 'me2me') {
119         var hostId = urlParams['hostId'];
120         remoting.connectMe2Me(hostId);
121         return;
122       }
123     }
124     // No valid URL parameters, start up normally.
125     remoting.initHomeScreenUi();
126   }
127   remoting.hostList.load(onLoad);
128
129   // For Apps v1, check the tab type to warn the user if they are not getting
130   // the best keyboard experience.
131   if (!remoting.isAppsV2 && navigator.platform.indexOf('Mac') == -1) {
132     /** @param {boolean} isWindowed */
133     var onIsWindowed = function(isWindowed) {
134       if (!isWindowed) {
135         document.getElementById('startup-mode-box-me2me').hidden = false;
136         document.getElementById('startup-mode-box-it2me').hidden = false;
137       }
138     };
139     isWindowed_(onIsWindowed);
140   }
141 };
142
143 /**
144  * Display the user's email address and allow access to the rest of the app,
145  * including parsing URL parameters.
146  *
147  * @param {string} email The user's email address.
148  * @return {void} Nothing.
149  */
150 remoting.onEmail = function(email) {
151   document.getElementById('current-email').innerText = email;
152   document.getElementById('get-started-it2me').disabled = false;
153   document.getElementById('get-started-me2me').disabled = false;
154 };
155
156 /**
157  * Returns whether or not IT2Me is supported via the host NPAPI plugin.
158  *
159  * @return {boolean}
160  */
161 function isIT2MeSupported_() {
162   var container = document.getElementById('host-plugin-container');
163   /** @type {remoting.HostPlugin} */
164   var plugin = remoting.HostSession.createPlugin();
165   container.appendChild(plugin);
166   var result = plugin.hasOwnProperty('REQUESTED_ACCESS_CODE');
167   container.removeChild(plugin);
168   return result;
169 }
170
171 /**
172  * initHomeScreenUi is called if the app is not starting up in session mode,
173  * and also if the user cancels pin entry or the connection in session mode.
174  */
175 remoting.initHomeScreenUi = function() {
176   remoting.hostController = new remoting.HostController();
177   document.getElementById('share-button').disabled = !isIT2MeSupported_();
178   remoting.setMode(remoting.AppMode.HOME);
179   remoting.hostSetupDialog =
180       new remoting.HostSetupDialog(remoting.hostController);
181   var dialog = document.getElementById('paired-clients-list');
182   var message = document.getElementById('paired-client-manager-message');
183   var deleteAll = document.getElementById('delete-all-paired-clients');
184   var close = document.getElementById('close-paired-client-manager-dialog');
185   var working = document.getElementById('paired-client-manager-dialog-working');
186   var error = document.getElementById('paired-client-manager-dialog-error');
187   var noPairedClients = document.getElementById('no-paired-clients');
188   remoting.pairedClientManager =
189       new remoting.PairedClientManager(remoting.hostController, dialog, message,
190                                        deleteAll, close, noPairedClients,
191                                        working, error);
192   // Display the cached host list, then asynchronously update and re-display it.
193   remoting.updateLocalHostState();
194   remoting.hostList.refresh(remoting.updateLocalHostState);
195   remoting.butterBar = new remoting.ButterBar();
196 };
197
198 /**
199  * Fetches local host state and updates the DOM accordingly.
200  */
201 remoting.updateLocalHostState = function() {
202   /**
203    * @param {string?} hostId Host id.
204    */
205   var onHostId = function(hostId) {
206     remoting.hostController.getLocalHostState(onHostState.bind(null, hostId));
207   };
208
209   /**
210    * @param {string?} hostId Host id.
211    * @param {remoting.HostController.State} state Host state.
212    */
213   var onHostState = function(hostId, state) {
214     remoting.hostList.setLocalHostStateAndId(state, hostId);
215     remoting.hostList.display();
216   };
217
218   /**
219    * @param {boolean} response True if the feature is present.
220    */
221   var onHasFeatureResponse = function(response) {
222     /**
223      * @param {remoting.Error} error
224      */
225     var onError = function(error) {
226       console.error('Failed to get pairing status: ' + error);
227       remoting.pairedClientManager.setPairedClients([]);
228     };
229
230     if (response) {
231       remoting.hostController.getPairedClients(
232           remoting.pairedClientManager.setPairedClients.bind(
233               remoting.pairedClientManager),
234           onError);
235     } else {
236       console.log('Pairing registry not supported by host.');
237       remoting.pairedClientManager.setPairedClients([]);
238     }
239   };
240
241   remoting.hostController.hasFeature(
242       remoting.HostController.Feature.PAIRING_REGISTRY, onHasFeatureResponse);
243   remoting.hostController.getLocalHostId(onHostId);
244 };
245
246 /**
247  * Log information about the current extension.
248  * The extension manifest is parsed to extract this info.
249  */
250 remoting.logExtensionInfo_ = function() {
251   var v2OrLegacy = remoting.isAppsV2 ? " (v2)" : " (legacy)";
252   var manifest = chrome.runtime.getManifest();
253   if (manifest && manifest.version) {
254     var name = chrome.i18n.getMessage('PRODUCT_NAME');
255     console.log(name + ' version: ' + manifest.version + v2OrLegacy);
256   } else {
257     console.error('Failed to get product version. Corrupt manifest?');
258   }
259 };
260
261 /**
262  * If an IT2Me client or host is active then prompt the user before closing.
263  * If a Me2Me client is active then don't bother, since closing the window is
264  * the more intuitive way to end a Me2Me session, and re-connecting is easy.
265  */
266 remoting.promptClose = function() {
267   if (!remoting.clientSession ||
268       remoting.clientSession.getMode() == remoting.ClientSession.Mode.ME2ME) {
269     return null;
270   }
271   switch (remoting.currentMode) {
272     case remoting.AppMode.CLIENT_CONNECTING:
273     case remoting.AppMode.HOST_WAITING_FOR_CODE:
274     case remoting.AppMode.HOST_WAITING_FOR_CONNECTION:
275     case remoting.AppMode.HOST_SHARED:
276     case remoting.AppMode.IN_SESSION:
277       return chrome.i18n.getMessage(/*i18n-content*/'CLOSE_PROMPT');
278     default:
279       return null;
280   }
281 };
282
283 /**
284  * Sign the user out of Chromoting by clearing (and revoking, if possible) the
285  * OAuth refresh token.
286  *
287  * Also clear all local storage, to avoid leaking information.
288  */
289 remoting.signOut = function() {
290   remoting.oauth2.clear();
291   chrome.storage.local.clear();
292   remoting.setMode(remoting.AppMode.HOME);
293   document.getElementById('auth-dialog').hidden = false;
294 };
295
296 /**
297  * Returns whether the app is running on ChromeOS.
298  *
299  * @return {boolean} True if the app is running on ChromeOS.
300  */
301 remoting.runningOnChromeOS = function() {
302   return !!navigator.userAgent.match(/\bCrOS\b/);
303 }
304
305 /**
306  * Callback function called when the browser window gets a paste operation.
307  *
308  * @param {Event} eventUncast
309  * @return {void} Nothing.
310  */
311 function pluginGotPaste_(eventUncast) {
312   var event = /** @type {remoting.ClipboardEvent} */ eventUncast;
313   if (event && event.clipboardData) {
314     remoting.clipboard.toHost(event.clipboardData);
315   }
316 }
317
318 /**
319  * Callback function called when the browser window gets a copy operation.
320  *
321  * @param {Event} eventUncast
322  * @return {void} Nothing.
323  */
324 function pluginGotCopy_(eventUncast) {
325   var event = /** @type {remoting.ClipboardEvent} */ eventUncast;
326   if (event && event.clipboardData) {
327     if (remoting.clipboard.toOs(event.clipboardData)) {
328       // The default action may overwrite items that we added to clipboardData.
329       event.preventDefault();
330     }
331   }
332 }
333
334 /**
335  * Returns whether Host mode is supported on this platform.
336  *
337  * @return {boolean} True if Host mode is supported.
338  */
339 function isHostModeSupported_() {
340   // Currently, sharing on Chromebooks is not supported.
341   return !remoting.runningOnChromeOS();
342 }
343
344 /**
345  * @return {Object.<string, string>} The URL parameters.
346  */
347 function getUrlParameters_() {
348   var result = {};
349   var parts = window.location.search.substring(1).split('&');
350   for (var i = 0; i < parts.length; i++) {
351     var pair = parts[i].split('=');
352     result[pair[0]] = decodeURIComponent(pair[1]);
353   }
354   return result;
355 }
356
357 /**
358  * @param {string} jsonString A JSON-encoded string.
359  * @return {*} The decoded object, or undefined if the string cannot be parsed.
360  */
361 function jsonParseSafe(jsonString) {
362   try {
363     return JSON.parse(jsonString);
364   } catch (err) {
365     return undefined;
366   }
367 }
368
369 /**
370  * Return the current time as a formatted string suitable for logging.
371  *
372  * @return {string} The current time, formatted as [mmdd/hhmmss.xyz]
373  */
374 remoting.timestamp = function() {
375   /**
376    * @param {number} num A number.
377    * @param {number} len The required length of the answer.
378    * @return {string} The number, formatted as a string of the specified length
379    *     by prepending zeroes as necessary.
380    */
381   var pad = function(num, len) {
382     var result = num.toString();
383     if (result.length < len) {
384       result = new Array(len - result.length + 1).join('0') + result;
385     }
386     return result;
387   };
388   var now = new Date();
389   var timestamp = pad(now.getMonth() + 1, 2) + pad(now.getDate(), 2) + '/' +
390       pad(now.getHours(), 2) + pad(now.getMinutes(), 2) +
391       pad(now.getSeconds(), 2) + '.' + pad(now.getMilliseconds(), 3);
392   return '[' + timestamp + ']';
393 };
394
395 /**
396  * Show an error message, optionally including a short-cut for signing in to
397  * Chromoting again.
398  *
399  * @param {remoting.Error} error
400  * @return {void} Nothing.
401  */
402 remoting.showErrorMessage = function(error) {
403   l10n.localizeElementFromTag(
404       document.getElementById('token-refresh-error-message'),
405       error);
406   var auth_failed = (error == remoting.Error.AUTHENTICATION_FAILED);
407   document.getElementById('token-refresh-auth-failed').hidden = !auth_failed;
408   document.getElementById('token-refresh-other-error').hidden = auth_failed;
409   remoting.setMode(remoting.AppMode.TOKEN_REFRESH_FAILED);
410 };
411
412 /**
413  * Determine whether or not the app is running in a window.
414  * @param {function(boolean):void} callback Callback to receive whether or not
415  *     the current tab is running in windowed mode.
416  */
417 function isWindowed_(callback) {
418   /** @param {chrome.Window} win The current window. */
419   var windowCallback = function(win) {
420     callback(win.type == 'popup');
421   };
422   /** @param {chrome.Tab} tab The current tab. */
423   var tabCallback = function(tab) {
424     if (tab.pinned) {
425       callback(false);
426     } else {
427       chrome.windows.get(tab.windowId, null, windowCallback);
428     }
429   };
430   if (chrome.tabs) {
431     chrome.tabs.getCurrent(tabCallback);
432   } else {
433     console.error('chome.tabs is not available.');
434   }
435 }
436
437 /**
438  * Migrate settings in window.localStorage to chrome.storage.local so that
439  * users of older web-apps that used the former do not lose their settings.
440  */
441 function migrateLocalToChromeStorage_() {
442   // The OAuth2 class still uses window.localStorage, so don't migrate any of
443   // those settings.
444   var oauthSettings = [
445       'oauth2-refresh-token',
446       'oauth2-refresh-token-revokable',
447       'oauth2-access-token',
448       'oauth2-xsrf-token',
449       'remoting-email'
450   ];
451   for (var setting in window.localStorage) {
452     if (oauthSettings.indexOf(setting) == -1) {
453       var copy = {}
454       copy[setting] = window.localStorage.getItem(setting);
455       chrome.storage.local.set(copy);
456       window.localStorage.removeItem(setting);
457     }
458   }
459 }
460
461 /**
462  * Generate a nonce, to be used as an xsrf protection token.
463  *
464  * @return {string} A URL-Safe Base64-encoded 128-bit random value. */
465 remoting.generateXsrfToken = function() {
466   var random = new Uint8Array(16);
467   window.crypto.getRandomValues(random);
468   var base64Token = window.btoa(String.fromCharCode.apply(null, random));
469   return base64Token.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
470 };