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