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