Add Feedback link to the app.
[chromium-blink-merge.git] / remoting / webapp / remoting.js
blob9e15be88217d567a77a8b535852a9b6fc3172735
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.
5 'use strict';
7 /** @suppress {duplicate} */
8 var remoting = remoting || {};
10 /** @type {remoting.HostSession} */ remoting.hostSession = null;
12 /**
13  * @type {boolean} True if this is a v2 app; false if it is a legacy app.
14  */
15 remoting.isAppsV2 = false;
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);
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   }
52   if (!remoting.isAppsV2) {
53     migrateLocalToChromeStorage_();
54   }
56   console.log(remoting.getExtensionInfo());
57   l10n.localize();
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);
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);
96   remoting.showOrHideIT2MeUi();
97   remoting.showOrHideMe2MeUi();
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);
104   remoting.initModalDialogs();
105   remoting.initFeedback(document.getElementById('send-feedback'));
107   if (isHostModeSupported_()) {
108     var noShare = document.getElementById('chrome-os-no-share');
109     noShare.parentNode.removeChild(noShare);
110   } else {
111     var button = document.getElementById('share-button');
112     button.disabled = true;
113   }
115   var onLoad = function() {
116     // Parse URL parameters.
117     var urlParams = getUrlParameters_();
118     if ('mode' in urlParams) {
119       if (urlParams['mode'] == 'me2me') {
120         var hostId = urlParams['hostId'];
121         remoting.connectMe2Me(hostId);
122         return;
123       }
124     }
125     // No valid URL parameters, start up normally.
126     remoting.initHomeScreenUi();
127   }
128   remoting.hostList.load(onLoad);
130   // For Apps v1, check the tab type to warn the user if they are not getting
131   // the best keyboard experience.
132   if (!remoting.isAppsV2 && navigator.platform.indexOf('Mac') == -1) {
133     /** @param {boolean} isWindowed */
134     var onIsWindowed = function(isWindowed) {
135       if (!isWindowed) {
136         document.getElementById('startup-mode-box-me2me').hidden = false;
137         document.getElementById('startup-mode-box-it2me').hidden = false;
138       }
139     };
140     isWindowed_(onIsWindowed);
141   }
145  * Returns whether or not IT2Me is supported via the host NPAPI plugin.
147  * @return {boolean}
148  */
149 function isIT2MeSupported_() {
150   // Currently, IT2Me on Chromebooks is not supported.
151   return !remoting.runningOnChromeOS();
155  * Create an instance of the NPAPI plugin.
156  * @param {Element} container The element to add the plugin to.
157  * @return {remoting.HostPlugin} The new plugin instance or null if it failed to
158  *     load.
159  */
160 remoting.createNpapiPlugin = function(container) {
161   var plugin = document.createElement('embed');
162   plugin.type = remoting.settings.PLUGIN_MIMETYPE;
163   // Hiding the plugin means it doesn't load, so make it size zero instead.
164   plugin.width = 0;
165   plugin.height = 0;
166   container.appendChild(plugin);
168   // Verify if the plugin was loaded successfully.
169   if (!plugin.hasOwnProperty('REQUESTED_ACCESS_CODE')) {
170     container.removeChild(plugin);
171     return null;
172   }
174   return /** @type {remoting.HostPlugin} */ (plugin);
178  * Returns true if the current platform is fully supported. It's only used when
179  * we detect that host native messaging components are not installed. In that
180  * case the result of this function determines if the webapp should show the
181  * controls that allow to install and enable Me2Me host.
183  * @return {boolean}
184  */
185 remoting.isMe2MeInstallable = function() {
186   /** @type {string} */
187   var platform = navigator.platform;
188   // Chromoting host is not installable on ChromeOS and any linux distro other
189   // than Ubuntu.
190   return platform == 'Win32' || platform == 'MacIntel';
194  * Display the user's email address and allow access to the rest of the app,
195  * including parsing URL parameters.
197  * @param {string} email The user's email address.
198  * @return {void} Nothing.
199  */
200 remoting.onEmail = function(email) {
201   document.getElementById('current-email').innerText = email;
202   document.getElementById('get-started-it2me').disabled = false;
203   document.getElementById('get-started-me2me').disabled = false;
207  * initHomeScreenUi is called if the app is not starting up in session mode,
208  * and also if the user cancels pin entry or the connection in session mode.
209  */
210 remoting.initHomeScreenUi = function() {
211   remoting.hostController = new remoting.HostController();
212   document.getElementById('share-button').disabled = !isIT2MeSupported_();
213   remoting.setMode(remoting.AppMode.HOME);
214   remoting.hostSetupDialog =
215       new remoting.HostSetupDialog(remoting.hostController);
216   var dialog = document.getElementById('paired-clients-list');
217   var message = document.getElementById('paired-client-manager-message');
218   var deleteAll = document.getElementById('delete-all-paired-clients');
219   var close = document.getElementById('close-paired-client-manager-dialog');
220   var working = document.getElementById('paired-client-manager-dialog-working');
221   var error = document.getElementById('paired-client-manager-dialog-error');
222   var noPairedClients = document.getElementById('no-paired-clients');
223   remoting.pairedClientManager =
224       new remoting.PairedClientManager(remoting.hostController, dialog, message,
225                                        deleteAll, close, noPairedClients,
226                                        working, error);
227   // Display the cached host list, then asynchronously update and re-display it.
228   remoting.updateLocalHostState();
229   remoting.hostList.refresh(remoting.updateLocalHostState);
230   remoting.butterBar = new remoting.ButterBar();
234  * Fetches local host state and updates the DOM accordingly.
235  */
236 remoting.updateLocalHostState = function() {
237   /**
238    * @param {remoting.HostController.State} state Host state.
239    */
240   var onHostState = function(state) {
241     if (state == remoting.HostController.State.STARTED) {
242       remoting.hostController.getLocalHostId(onHostId.bind(null, state));
243     } else {
244       onHostId(state, null);
245     }
246   };
248   /**
249    * @param {remoting.HostController.State} state Host state.
250    * @param {string?} hostId Host id.
251    */
252   var onHostId = function(state, hostId) {
253     remoting.hostList.setLocalHostStateAndId(state, hostId);
254     remoting.hostList.display();
255   };
257   /**
258    * @param {boolean} response True if the feature is present.
259    */
260   var onHasFeatureResponse = function(response) {
261     /**
262      * @param {remoting.Error} error
263      */
264     var onError = function(error) {
265       console.error('Failed to get pairing status: ' + error);
266       remoting.pairedClientManager.setPairedClients([]);
267     };
269     if (response) {
270       remoting.hostController.getPairedClients(
271           remoting.pairedClientManager.setPairedClients.bind(
272               remoting.pairedClientManager),
273           onError);
274     } else {
275       console.log('Pairing registry not supported by host.');
276       remoting.pairedClientManager.setPairedClients([]);
277     }
278   };
280   remoting.hostController.hasFeature(
281       remoting.HostController.Feature.PAIRING_REGISTRY, onHasFeatureResponse);
282   remoting.hostController.getLocalHostState(onHostState);
286  * @return {string} Information about the current extension.
287  */
288 remoting.getExtensionInfo = function() {
289   var v2OrLegacy = remoting.isAppsV2 ? " (v2)" : " (legacy)";
290   var manifest = chrome.runtime.getManifest();
291   if (manifest && manifest.version) {
292     var name = chrome.i18n.getMessage('PRODUCT_NAME');
293     return name + ' version: ' + manifest.version + v2OrLegacy;
294   } else {
295     return 'Failed to get product version. Corrupt manifest?';
296   }
300  * If an IT2Me client or host is active then prompt the user before closing.
301  * If a Me2Me client is active then don't bother, since closing the window is
302  * the more intuitive way to end a Me2Me session, and re-connecting is easy.
303  */
304 remoting.promptClose = function() {
305   if (!remoting.clientSession ||
306       remoting.clientSession.getMode() == remoting.ClientSession.Mode.ME2ME) {
307     return null;
308   }
309   switch (remoting.currentMode) {
310     case remoting.AppMode.CLIENT_CONNECTING:
311     case remoting.AppMode.HOST_WAITING_FOR_CODE:
312     case remoting.AppMode.HOST_WAITING_FOR_CONNECTION:
313     case remoting.AppMode.HOST_SHARED:
314     case remoting.AppMode.IN_SESSION:
315       return chrome.i18n.getMessage(/*i18n-content*/'CLOSE_PROMPT');
316     default:
317       return null;
318   }
322  * Sign the user out of Chromoting by clearing (and revoking, if possible) the
323  * OAuth refresh token.
325  * Also clear all local storage, to avoid leaking information.
326  */
327 remoting.signOut = function() {
328   remoting.oauth2.clear();
329   chrome.storage.local.clear();
330   remoting.setMode(remoting.AppMode.HOME);
331   document.getElementById('auth-dialog').hidden = false;
335  * Returns whether the app is running on ChromeOS.
337  * @return {boolean} True if the app is running on ChromeOS.
338  */
339 remoting.runningOnChromeOS = function() {
340   return !!navigator.userAgent.match(/\bCrOS\b/);
344  * Callback function called when the browser window gets a paste operation.
346  * @param {Event} eventUncast
347  * @return {void} Nothing.
348  */
349 function pluginGotPaste_(eventUncast) {
350   var event = /** @type {remoting.ClipboardEvent} */ eventUncast;
351   if (event && event.clipboardData) {
352     remoting.clipboard.toHost(event.clipboardData);
353   }
357  * Callback function called when the browser window gets a copy operation.
359  * @param {Event} eventUncast
360  * @return {void} Nothing.
361  */
362 function pluginGotCopy_(eventUncast) {
363   var event = /** @type {remoting.ClipboardEvent} */ eventUncast;
364   if (event && event.clipboardData) {
365     if (remoting.clipboard.toOs(event.clipboardData)) {
366       // The default action may overwrite items that we added to clipboardData.
367       event.preventDefault();
368     }
369   }
373  * Returns whether Host mode is supported on this platform.
375  * @return {boolean} True if Host mode is supported.
376  */
377 function isHostModeSupported_() {
378   // Currently, sharing on Chromebooks is not supported.
379   return !remoting.runningOnChromeOS();
383  * @return {Object.<string, string>} The URL parameters.
384  */
385 function getUrlParameters_() {
386   var result = {};
387   var parts = window.location.search.substring(1).split('&');
388   for (var i = 0; i < parts.length; i++) {
389     var pair = parts[i].split('=');
390     result[pair[0]] = decodeURIComponent(pair[1]);
391   }
392   return result;
396  * @param {string} jsonString A JSON-encoded string.
397  * @return {*} The decoded object, or undefined if the string cannot be parsed.
398  */
399 function jsonParseSafe(jsonString) {
400   try {
401     return JSON.parse(jsonString);
402   } catch (err) {
403     return undefined;
404   }
408  * Return the current time as a formatted string suitable for logging.
410  * @return {string} The current time, formatted as [mmdd/hhmmss.xyz]
411  */
412 remoting.timestamp = function() {
413   /**
414    * @param {number} num A number.
415    * @param {number} len The required length of the answer.
416    * @return {string} The number, formatted as a string of the specified length
417    *     by prepending zeroes as necessary.
418    */
419   var pad = function(num, len) {
420     var result = num.toString();
421     if (result.length < len) {
422       result = new Array(len - result.length + 1).join('0') + result;
423     }
424     return result;
425   };
426   var now = new Date();
427   var timestamp = pad(now.getMonth() + 1, 2) + pad(now.getDate(), 2) + '/' +
428       pad(now.getHours(), 2) + pad(now.getMinutes(), 2) +
429       pad(now.getSeconds(), 2) + '.' + pad(now.getMilliseconds(), 3);
430   return '[' + timestamp + ']';
434  * Show an error message, optionally including a short-cut for signing in to
435  * Chromoting again.
437  * @param {remoting.Error} error
438  * @return {void} Nothing.
439  */
440 remoting.showErrorMessage = function(error) {
441   l10n.localizeElementFromTag(
442       document.getElementById('token-refresh-error-message'),
443       error);
444   var auth_failed = (error == remoting.Error.AUTHENTICATION_FAILED);
445   document.getElementById('token-refresh-auth-failed').hidden = !auth_failed;
446   document.getElementById('token-refresh-other-error').hidden = auth_failed;
447   remoting.setMode(remoting.AppMode.TOKEN_REFRESH_FAILED);
451  * Determine whether or not the app is running in a window.
452  * @param {function(boolean):void} callback Callback to receive whether or not
453  *     the current tab is running in windowed mode.
454  */
455 function isWindowed_(callback) {
456   /** @param {chrome.Window} win The current window. */
457   var windowCallback = function(win) {
458     callback(win.type == 'popup');
459   };
460   /** @param {chrome.Tab} tab The current tab. */
461   var tabCallback = function(tab) {
462     if (tab.pinned) {
463       callback(false);
464     } else {
465       chrome.windows.get(tab.windowId, null, windowCallback);
466     }
467   };
468   if (chrome.tabs) {
469     chrome.tabs.getCurrent(tabCallback);
470   } else {
471     console.error('chome.tabs is not available.');
472   }
476  * Migrate settings in window.localStorage to chrome.storage.local so that
477  * users of older web-apps that used the former do not lose their settings.
478  */
479 function migrateLocalToChromeStorage_() {
480   // The OAuth2 class still uses window.localStorage, so don't migrate any of
481   // those settings.
482   var oauthSettings = [
483       'oauth2-refresh-token',
484       'oauth2-refresh-token-revokable',
485       'oauth2-access-token',
486       'oauth2-xsrf-token',
487       'remoting-email'
488   ];
489   for (var setting in window.localStorage) {
490     if (oauthSettings.indexOf(setting) == -1) {
491       var copy = {}
492       copy[setting] = window.localStorage.getItem(setting);
493       chrome.storage.local.set(copy);
494       window.localStorage.removeItem(setting);
495     }
496   }
500  * Generate a nonce, to be used as an xsrf protection token.
502  * @return {string} A URL-Safe Base64-encoded 128-bit random value. */
503 remoting.generateXsrfToken = function() {
504   var random = new Uint8Array(16);
505   window.crypto.getRandomValues(random);
506   var base64Token = window.btoa(String.fromCharCode.apply(null, random));
507   return base64Token.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');