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