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