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