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 {base.EventSource} An event source object for handling global events. 14 * This is an interim hack. Eventually, we should move functionalities 15 * away from the remoting namespace and into smaller objects. 16 */ 17remoting.testEvents; 18 19/** 20 * Show the authorization consent UI and register a one-shot event handler to 21 * continue the authorization process. 22 * 23 * @param {function():void} authContinue Callback to invoke when the user 24 * clicks "Continue". 25 */ 26function consentRequired_(authContinue) { 27 /** @type {HTMLElement} */ 28 var dialog = document.getElementById('auth-dialog'); 29 /** @type {HTMLElement} */ 30 var button = document.getElementById('auth-button'); 31 var consentGranted = function(event) { 32 dialog.hidden = true; 33 button.removeEventListener('click', consentGranted, false); 34 authContinue(); 35 }; 36 dialog.hidden = false; 37 button.addEventListener('click', consentGranted, false); 38} 39 40/** 41 * Entry point for app initialization. 42 */ 43remoting.init = function() { 44 if (base.isAppsV2()) { 45 var htmlNode = /** @type {HTMLElement} */ (document.body.parentNode); 46 htmlNode.classList.add('apps-v2'); 47 } else { 48 migrateLocalToChromeStorage_(); 49 } 50 51 console.log(remoting.getExtensionInfo()); 52 l10n.localize(); 53 54 // Create global objects. 55 remoting.ClientPlugin.factory = new remoting.DefaultClientPluginFactory(); 56 remoting.SessionConnector.factory = 57 new remoting.DefaultSessionConnectorFactory(); 58 remoting.settings = new remoting.Settings(); 59 if (base.isAppsV2()) { 60 remoting.identity = new remoting.Identity(consentRequired_); 61 remoting.fullscreen = new remoting.FullscreenAppsV2(); 62 remoting.windowFrame = new remoting.WindowFrame( 63 document.getElementById('title-bar')); 64 remoting.optionsMenu = remoting.windowFrame.createOptionsMenu(); 65 } else { 66 remoting.oauth2 = new remoting.OAuth2(); 67 if (!remoting.oauth2.isAuthenticated()) { 68 document.getElementById('auth-dialog').hidden = false; 69 } 70 remoting.identity = remoting.oauth2; 71 remoting.fullscreen = new remoting.FullscreenAppsV1(); 72 remoting.toolbar = new remoting.Toolbar( 73 document.getElementById('session-toolbar')); 74 remoting.optionsMenu = remoting.toolbar.createOptionsMenu(); 75 } 76 remoting.stats = new remoting.ConnectionStats( 77 document.getElementById('statistics')); 78 remoting.formatIq = new remoting.FormatIq(); 79 remoting.hostList = new remoting.HostList( 80 document.getElementById('host-list'), 81 document.getElementById('host-list-empty'), 82 document.getElementById('host-list-error-message'), 83 document.getElementById('host-list-refresh-failed-button'), 84 document.getElementById('host-list-loading-indicator')); 85 remoting.clipboard = new remoting.Clipboard(); 86 var sandbox = /** @type {HTMLIFrameElement} */ 87 document.getElementById('wcs-sandbox'); 88 remoting.wcsSandbox = new remoting.WcsSandboxContainer(sandbox.contentWindow); 89 var homeFeedback = new remoting.MenuButton( 90 document.getElementById('help-feedback-main')); 91 var toolbarFeedback = new remoting.MenuButton( 92 document.getElementById('help-feedback-toolbar')); 93 remoting.manageHelpAndFeedback( 94 document.getElementById('title-bar')); 95 remoting.manageHelpAndFeedback( 96 document.getElementById('help-feedback-toolbar')); 97 remoting.manageHelpAndFeedback( 98 document.getElementById('help-feedback-main')); 99 100 /** @param {remoting.Error} error */ 101 var onGetEmailError = function(error) { 102 // No need to show the error message for NOT_AUTHENTICATED 103 // because we will show "auth-dialog". 104 if (error != remoting.Error.NOT_AUTHENTICATED) { 105 remoting.showErrorMessage(error); 106 } 107 } 108 remoting.identity.getEmail(remoting.onEmail, onGetEmailError); 109 110 remoting.showOrHideIT2MeUi(); 111 remoting.showOrHideMe2MeUi(); 112 113 // The plugin's onFocus handler sends a paste command to |window|, because 114 // it can't send one to the plugin element itself. 115 window.addEventListener('paste', pluginGotPaste_, false); 116 window.addEventListener('copy', pluginGotCopy_, false); 117 118 remoting.initModalDialogs(); 119 120 if (isHostModeSupported_()) { 121 var noShare = document.getElementById('chrome-os-no-share'); 122 noShare.parentNode.removeChild(noShare); 123 } else { 124 var button = document.getElementById('share-button'); 125 button.disabled = true; 126 } 127 128 /** 129 * @return {Promise} A promise that resolves to the id of the current 130 * containing tab/window. 131 */ 132 var getCurrentId = function () { 133 if (base.isAppsV2()) { 134 return Promise.resolve(chrome.app.window.current().id); 135 } 136 137 /** 138 * @param {function(*=):void} resolve 139 * @param {function(*=):void} reject 140 */ 141 return new Promise(function(resolve, reject) { 142 /** @param {chrome.Tab} tab */ 143 chrome.tabs.getCurrent(function(tab){ 144 if (tab) { 145 resolve(String(tab.id)); 146 } 147 reject('Cannot retrieve the current tab.'); 148 }); 149 }); 150 }; 151 152 var onLoad = function() { 153 // Parse URL parameters. 154 var urlParams = getUrlParameters_(); 155 if ('mode' in urlParams) { 156 if (urlParams['mode'] === 'me2me') { 157 var hostId = urlParams['hostId']; 158 remoting.connectMe2Me(hostId); 159 return; 160 } else if (urlParams['mode'] === 'hangout') { 161 /** @param {*} id */ 162 getCurrentId().then(function(id) { 163 /** @type {string} */ 164 var accessCode = urlParams['accessCode']; 165 remoting.ensureSessionConnector_(); 166 remoting.setMode(remoting.AppMode.CLIENT_CONNECTING); 167 remoting.connector.connectIT2Me(accessCode); 168 169 document.body.classList.add('hangout-remote-desktop'); 170 var senderId = /** @type {string} */ String(id); 171 var hangoutSession = new remoting.HangoutSession(senderId); 172 hangoutSession.init(); 173 }); 174 return; 175 } 176 } 177 // No valid URL parameters, start up normally. 178 remoting.initHomeScreenUi(); 179 } 180 remoting.hostList.load(onLoad); 181 182 // For Apps v1, check the tab type to warn the user if they are not getting 183 // the best keyboard experience. 184 if (!base.isAppsV2() && !remoting.platformIsMac()) { 185 /** @param {boolean} isWindowed */ 186 var onIsWindowed = function(isWindowed) { 187 if (!isWindowed) { 188 document.getElementById('startup-mode-box-me2me').hidden = false; 189 document.getElementById('startup-mode-box-it2me').hidden = false; 190 } 191 }; 192 isWindowed_(onIsWindowed); 193 } 194 195 remoting.testEvents = new base.EventSource(); 196 197 /** @enum {string} */ 198 remoting.testEvents.Names = { 199 uiModeChanged: 'uiModeChanged' 200 }; 201 remoting.testEvents.defineEvents(base.values(remoting.testEvents.Names)); 202 203 remoting.ClientPlugin.factory.preloadPlugin(); 204}; 205 206/** 207 * Returns whether or not IT2Me is supported via the host NPAPI plugin. 208 * 209 * @return {boolean} 210 */ 211function isIT2MeSupported_() { 212 // Currently, IT2Me on Chromebooks is not supported. 213 return !remoting.runningOnChromeOS(); 214} 215 216/** 217 * Returns true if the current platform is fully supported. It's only used when 218 * we detect that host native messaging components are not installed. In that 219 * case the result of this function determines if the webapp should show the 220 * controls that allow to install and enable Me2Me host. 221 * 222 * @return {boolean} 223 */ 224remoting.isMe2MeInstallable = function() { 225 // The chromoting host is currently not installable on ChromeOS. 226 // For Linux, we have a install package for Ubuntu but not other distros. 227 // Since we cannot tell from javascript alone the Linux distro the client is 228 // on, we don't show the daemon-control UI for Linux unless the host is 229 // installed. 230 return remoting.platformIsWindows() || remoting.platformIsMac(); 231} 232 233/** 234 * Display the user's email address and allow access to the rest of the app, 235 * including parsing URL parameters. 236 * 237 * @param {string} email The user's email address. 238 * @return {void} Nothing. 239 */ 240remoting.onEmail = function(email) { 241 document.getElementById('current-email').innerText = email; 242 document.getElementById('get-started-it2me').disabled = false; 243 document.getElementById('get-started-me2me').disabled = false; 244}; 245 246/** 247 * initHomeScreenUi is called if the app is not starting up in session mode, 248 * and also if the user cancels pin entry or the connection in session mode. 249 */ 250remoting.initHomeScreenUi = function() { 251 remoting.hostController = new remoting.HostController(); 252 document.getElementById('share-button').disabled = !isIT2MeSupported_(); 253 remoting.setMode(remoting.AppMode.HOME); 254 remoting.hostSetupDialog = 255 new remoting.HostSetupDialog(remoting.hostController); 256 var dialog = document.getElementById('paired-clients-list'); 257 var message = document.getElementById('paired-client-manager-message'); 258 var deleteAll = document.getElementById('delete-all-paired-clients'); 259 var close = document.getElementById('close-paired-client-manager-dialog'); 260 var working = document.getElementById('paired-client-manager-dialog-working'); 261 var error = document.getElementById('paired-client-manager-dialog-error'); 262 var noPairedClients = document.getElementById('no-paired-clients'); 263 remoting.pairedClientManager = 264 new remoting.PairedClientManager(remoting.hostController, dialog, message, 265 deleteAll, close, noPairedClients, 266 working, error); 267 // Display the cached host list, then asynchronously update and re-display it. 268 remoting.updateLocalHostState(); 269 remoting.hostList.refresh(remoting.updateLocalHostState); 270 remoting.butterBar = new remoting.ButterBar(); 271}; 272 273/** 274 * Fetches local host state and updates the DOM accordingly. 275 */ 276remoting.updateLocalHostState = function() { 277 /** 278 * @param {remoting.HostController.State} state Host state. 279 */ 280 var onHostState = function(state) { 281 if (state == remoting.HostController.State.STARTED) { 282 remoting.hostController.getLocalHostId(onHostId.bind(null, state)); 283 } else { 284 onHostId(state, null); 285 } 286 }; 287 288 /** 289 * @param {remoting.HostController.State} state Host state. 290 * @param {string?} hostId Host id. 291 */ 292 var onHostId = function(state, hostId) { 293 remoting.hostList.setLocalHostStateAndId(state, hostId); 294 remoting.hostList.display(); 295 }; 296 297 /** 298 * @param {boolean} response True if the feature is present. 299 */ 300 var onHasFeatureResponse = function(response) { 301 /** 302 * @param {remoting.Error} error 303 */ 304 var onError = function(error) { 305 console.error('Failed to get pairing status: ' + error); 306 remoting.pairedClientManager.setPairedClients([]); 307 }; 308 309 if (response) { 310 remoting.hostController.getPairedClients( 311 remoting.pairedClientManager.setPairedClients.bind( 312 remoting.pairedClientManager), 313 onError); 314 } else { 315 console.log('Pairing registry not supported by host.'); 316 remoting.pairedClientManager.setPairedClients([]); 317 } 318 }; 319 320 remoting.hostController.hasFeature( 321 remoting.HostController.Feature.PAIRING_REGISTRY, onHasFeatureResponse); 322 remoting.hostController.getLocalHostState(onHostState); 323}; 324 325/** 326 * @return {string} Information about the current extension. 327 */ 328remoting.getExtensionInfo = function() { 329 var v2OrLegacy = base.isAppsV2() ? " (v2)" : " (legacy)"; 330 var manifest = chrome.runtime.getManifest(); 331 if (manifest && manifest.version) { 332 var name = chrome.i18n.getMessage('PRODUCT_NAME'); 333 return name + ' version: ' + manifest.version + v2OrLegacy; 334 } else { 335 return 'Failed to get product version. Corrupt manifest?'; 336 } 337}; 338 339/** 340 * Returns Chrome version. 341 * @return {string?} 342 */ 343remoting.getChromeVersion = function() { 344 var match = new RegExp('Chrome/([0-9.]*)').exec(navigator.userAgent); 345 if (match && (match.length >= 2)) { 346 return match[1]; 347 } 348 return null; 349}; 350 351/** 352 * If an IT2Me client or host is active then prompt the user before closing. 353 * If a Me2Me client is active then don't bother, since closing the window is 354 * the more intuitive way to end a Me2Me session, and re-connecting is easy. 355 */ 356remoting.promptClose = function() { 357 if (!remoting.clientSession || 358 remoting.clientSession.getMode() == remoting.ClientSession.Mode.ME2ME) { 359 return null; 360 } 361 switch (remoting.currentMode) { 362 case remoting.AppMode.CLIENT_CONNECTING: 363 case remoting.AppMode.HOST_WAITING_FOR_CODE: 364 case remoting.AppMode.HOST_WAITING_FOR_CONNECTION: 365 case remoting.AppMode.HOST_SHARED: 366 case remoting.AppMode.IN_SESSION: 367 return chrome.i18n.getMessage(/*i18n-content*/'CLOSE_PROMPT'); 368 default: 369 return null; 370 } 371}; 372 373/** 374 * Sign the user out of Chromoting by clearing (and revoking, if possible) the 375 * OAuth refresh token. 376 * 377 * Also clear all local storage, to avoid leaking information. 378 */ 379remoting.signOut = function() { 380 remoting.oauth2.clear(); 381 chrome.storage.local.clear(); 382 remoting.setMode(remoting.AppMode.HOME); 383 document.getElementById('auth-dialog').hidden = false; 384}; 385 386/** 387 * Returns whether the app is running on ChromeOS. 388 * 389 * @return {boolean} True if the app is running on ChromeOS. 390 */ 391remoting.runningOnChromeOS = function() { 392 return !!navigator.userAgent.match(/\bCrOS\b/); 393} 394 395/** 396 * Callback function called when the browser window gets a paste operation. 397 * 398 * @param {Event} eventUncast 399 * @return {void} Nothing. 400 */ 401function pluginGotPaste_(eventUncast) { 402 var event = /** @type {remoting.ClipboardEvent} */ eventUncast; 403 if (event && event.clipboardData) { 404 remoting.clipboard.toHost(event.clipboardData); 405 } 406} 407 408/** 409 * Callback function called when the browser window gets a copy operation. 410 * 411 * @param {Event} eventUncast 412 * @return {void} Nothing. 413 */ 414function pluginGotCopy_(eventUncast) { 415 var event = /** @type {remoting.ClipboardEvent} */ eventUncast; 416 if (event && event.clipboardData) { 417 if (remoting.clipboard.toOs(event.clipboardData)) { 418 // The default action may overwrite items that we added to clipboardData. 419 event.preventDefault(); 420 } 421 } 422} 423 424/** 425 * Returns whether Host mode is supported on this platform. 426 * 427 * @return {boolean} True if Host mode is supported. 428 */ 429function isHostModeSupported_() { 430 // Currently, sharing on Chromebooks is not supported. 431 return !remoting.runningOnChromeOS(); 432} 433 434/** 435 * @return {Object.<string, string>} The URL parameters. 436 */ 437function getUrlParameters_() { 438 var result = {}; 439 var parts = window.location.search.substring(1).split('&'); 440 for (var i = 0; i < parts.length; i++) { 441 var pair = parts[i].split('='); 442 result[pair[0]] = decodeURIComponent(pair[1]); 443 } 444 return result; 445} 446 447/** 448 * @param {string} jsonString A JSON-encoded string. 449 * @return {*} The decoded object, or undefined if the string cannot be parsed. 450 */ 451function jsonParseSafe(jsonString) { 452 try { 453 return JSON.parse(jsonString); 454 } catch (err) { 455 return undefined; 456 } 457} 458 459/** 460 * Return the current time as a formatted string suitable for logging. 461 * 462 * @return {string} The current time, formatted as [mmdd/hhmmss.xyz] 463 */ 464remoting.timestamp = function() { 465 /** 466 * @param {number} num A number. 467 * @param {number} len The required length of the answer. 468 * @return {string} The number, formatted as a string of the specified length 469 * by prepending zeroes as necessary. 470 */ 471 var pad = function(num, len) { 472 var result = num.toString(); 473 if (result.length < len) { 474 result = new Array(len - result.length + 1).join('0') + result; 475 } 476 return result; 477 }; 478 var now = new Date(); 479 var timestamp = pad(now.getMonth() + 1, 2) + pad(now.getDate(), 2) + '/' + 480 pad(now.getHours(), 2) + pad(now.getMinutes(), 2) + 481 pad(now.getSeconds(), 2) + '.' + pad(now.getMilliseconds(), 3); 482 return '[' + timestamp + ']'; 483}; 484 485/** 486 * Show an error message, optionally including a short-cut for signing in to 487 * Chromoting again. 488 * 489 * @param {remoting.Error} error 490 * @return {void} Nothing. 491 */ 492remoting.showErrorMessage = function(error) { 493 l10n.localizeElementFromTag( 494 document.getElementById('token-refresh-error-message'), 495 error); 496 var auth_failed = (error == remoting.Error.AUTHENTICATION_FAILED); 497 document.getElementById('token-refresh-auth-failed').hidden = !auth_failed; 498 document.getElementById('token-refresh-other-error').hidden = auth_failed; 499 remoting.setMode(remoting.AppMode.TOKEN_REFRESH_FAILED); 500}; 501 502/** 503 * Determine whether or not the app is running in a window. 504 * @param {function(boolean):void} callback Callback to receive whether or not 505 * the current tab is running in windowed mode. 506 */ 507function isWindowed_(callback) { 508 /** @param {chrome.Window} win The current window. */ 509 var windowCallback = function(win) { 510 callback(win.type == 'popup'); 511 }; 512 /** @param {chrome.Tab} tab The current tab. */ 513 var tabCallback = function(tab) { 514 if (tab.pinned) { 515 callback(false); 516 } else { 517 chrome.windows.get(tab.windowId, null, windowCallback); 518 } 519 }; 520 if (chrome.tabs) { 521 chrome.tabs.getCurrent(tabCallback); 522 } else { 523 console.error('chome.tabs is not available.'); 524 } 525} 526 527/** 528 * Migrate settings in window.localStorage to chrome.storage.local so that 529 * users of older web-apps that used the former do not lose their settings. 530 */ 531function migrateLocalToChromeStorage_() { 532 // The OAuth2 class still uses window.localStorage, so don't migrate any of 533 // those settings. 534 var oauthSettings = [ 535 'oauth2-refresh-token', 536 'oauth2-refresh-token-revokable', 537 'oauth2-access-token', 538 'oauth2-xsrf-token', 539 'remoting-email' 540 ]; 541 for (var setting in window.localStorage) { 542 if (oauthSettings.indexOf(setting) == -1) { 543 var copy = {} 544 copy[setting] = window.localStorage.getItem(setting); 545 chrome.storage.local.set(copy); 546 window.localStorage.removeItem(setting); 547 } 548 } 549} 550 551/** 552 * Generate a nonce, to be used as an xsrf protection token. 553 * 554 * @return {string} A URL-Safe Base64-encoded 128-bit random value. */ 555remoting.generateXsrfToken = function() { 556 var random = new Uint8Array(16); 557 window.crypto.getRandomValues(random); 558 var base64Token = window.btoa(String.fromCharCode.apply(null, random)); 559 return base64Token.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); 560}; 561 562/** 563 * Tests whether we are running on Mac. 564 * 565 * @return {boolean} True if the platform is Mac. 566 */ 567remoting.platformIsMac = function() { 568 return navigator.platform.indexOf('Mac') != -1; 569} 570 571/** 572 * Tests whether we are running on Windows. 573 * 574 * @return {boolean} True if the platform is Windows. 575 */ 576remoting.platformIsWindows = function() { 577 return (navigator.platform.indexOf('Win32') != -1) || 578 (navigator.platform.indexOf('Win64') != -1); 579} 580 581/** 582 * Tests whether we are running on Linux. 583 * 584 * @return {boolean} True if the platform is Linux. 585 */ 586remoting.platformIsLinux = function() { 587 return (navigator.platform.indexOf('Linux') != -1) && 588 !remoting.platformIsChromeOS(); 589} 590 591/** 592 * Tests whether we are running on ChromeOS. 593 * 594 * @return {boolean} True if the platform is ChromeOS. 595 */ 596remoting.platformIsChromeOS = function() { 597 return navigator.userAgent.match(/\bCrOS\b/) != null; 598} 599