// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; /** @suppress {duplicate} */ var remoting = remoting || {}; /** @type {remoting.HostSession} */ remoting.hostSession = null; /** * @type {boolean} True if this is a v2 app; false if it is a legacy app. */ remoting.isAppsV2 = false; /** * @type {base.EventSource} An event source object for handling global events. * This is an interim hack. Eventually, we should move functionalities * away from the remoting namespace and into smaller objects. */ remoting.testEvents; /** * Show the authorization consent UI and register a one-shot event handler to * continue the authorization process. * * @param {function():void} authContinue Callback to invoke when the user * clicks "Continue". */ function consentRequired_(authContinue) { /** @type {HTMLElement} */ var dialog = document.getElementById('auth-dialog'); /** @type {HTMLElement} */ var button = document.getElementById('auth-button'); var consentGranted = function(event) { dialog.hidden = true; button.removeEventListener('click', consentGranted, false); authContinue(); }; dialog.hidden = false; button.addEventListener('click', consentGranted, false); } /** * Entry point for app initialization. */ remoting.init = function() { // Determine whether or not this is a V2 web-app. In order to keep the apps // v2 patch as small as possible, all JS changes needed for apps v2 are done // at run-time. Only the manifest is patched. var manifest = chrome.runtime.getManifest(); if (manifest && manifest.app && manifest.app.background) { remoting.isAppsV2 = true; var htmlNode = /** @type {HTMLElement} */ (document.body.parentNode); htmlNode.classList.add('apps-v2'); } if (!remoting.isAppsV2) { migrateLocalToChromeStorage_(); } console.log(remoting.getExtensionInfo()); l10n.localize(); // Create global objects. remoting.settings = new remoting.Settings(); if (remoting.isAppsV2) { remoting.identity = new remoting.Identity(consentRequired_); remoting.fullscreen = new remoting.FullscreenAppsV2(); remoting.windowFrame = new remoting.WindowFrame( document.getElementById('title-bar')); } else { remoting.oauth2 = new remoting.OAuth2(); if (!remoting.oauth2.isAuthenticated()) { document.getElementById('auth-dialog').hidden = false; } remoting.identity = remoting.oauth2; remoting.fullscreen = new remoting.FullscreenAppsV1(); } remoting.stats = new remoting.ConnectionStats( document.getElementById('statistics')); remoting.formatIq = new remoting.FormatIq(); remoting.hostList = new remoting.HostList( document.getElementById('host-list'), document.getElementById('host-list-empty'), document.getElementById('host-list-error-message'), document.getElementById('host-list-refresh-failed-button'), document.getElementById('host-list-loading-indicator')); remoting.toolbar = new remoting.Toolbar( document.getElementById('session-toolbar')); remoting.clipboard = new remoting.Clipboard(); var sandbox = /** @type {HTMLIFrameElement} */ document.getElementById('wcs-sandbox'); remoting.wcsSandbox = new remoting.WcsSandboxContainer(sandbox.contentWindow); var menuFeedback = new remoting.Feedback( document.getElementById('help-feedback-main'), document.getElementById('help-main'), document.getElementById('send-feedback-main')); var toolbarFeedback = new remoting.Feedback( document.getElementById('help-feedback-toolbar'), document.getElementById('help-toolbar'), document.getElementById('send-feedback-toolbar')); /** @param {remoting.Error} error */ var onGetEmailError = function(error) { // No need to show the error message for NOT_AUTHENTICATED // because we will show "auth-dialog". if (error != remoting.Error.NOT_AUTHENTICATED) { remoting.showErrorMessage(error); } } remoting.identity.getEmail(remoting.onEmail, onGetEmailError); remoting.showOrHideIT2MeUi(); remoting.showOrHideMe2MeUi(); // The plugin's onFocus handler sends a paste command to |window|, because // it can't send one to the plugin element itself. window.addEventListener('paste', pluginGotPaste_, false); window.addEventListener('copy', pluginGotCopy_, false); remoting.initModalDialogs(); if (isHostModeSupported_()) { var noShare = document.getElementById('chrome-os-no-share'); noShare.parentNode.removeChild(noShare); } else { var button = document.getElementById('share-button'); button.disabled = true; } var onLoad = function() { // Parse URL parameters. var urlParams = getUrlParameters_(); if ('mode' in urlParams) { if (urlParams['mode'] == 'me2me') { var hostId = urlParams['hostId']; remoting.connectMe2Me(hostId); return; } } // No valid URL parameters, start up normally. remoting.initHomeScreenUi(); } remoting.hostList.load(onLoad); // For Apps v1, check the tab type to warn the user if they are not getting // the best keyboard experience. if (!remoting.isAppsV2 && navigator.platform.indexOf('Mac') == -1) { /** @param {boolean} isWindowed */ var onIsWindowed = function(isWindowed) { if (!isWindowed) { document.getElementById('startup-mode-box-me2me').hidden = false; document.getElementById('startup-mode-box-it2me').hidden = false; } }; isWindowed_(onIsWindowed); } remoting.testEvents = new base.EventSource(); /** @enum {string} */ remoting.testEvents.Names = { uiModeChanged: 'uiModeChanged' }; remoting.testEvents.defineEvents(base.values(remoting.testEvents.Names)); }; /** * Returns whether or not IT2Me is supported via the host NPAPI plugin. * * @return {boolean} */ function isIT2MeSupported_() { // Currently, IT2Me on Chromebooks is not supported. return !remoting.runningOnChromeOS(); } /** * Returns true if the current platform is fully supported. It's only used when * we detect that host native messaging components are not installed. In that * case the result of this function determines if the webapp should show the * controls that allow to install and enable Me2Me host. * * @return {boolean} */ remoting.isMe2MeInstallable = function() { /** @type {string} */ var platform = navigator.platform; // The chromoting host is currently not installable on ChromeOS. // For Linux, we have a install package for Ubuntu but not other distros. // Since we cannot tell from javascript alone the Linux distro the client is // on, we don't show the daemon-control UI for Linux unless the host is // installed. return platform == 'Win32' || platform == 'MacIntel'; } /** * Display the user's email address and allow access to the rest of the app, * including parsing URL parameters. * * @param {string} email The user's email address. * @return {void} Nothing. */ remoting.onEmail = function(email) { document.getElementById('current-email').innerText = email; document.getElementById('get-started-it2me').disabled = false; document.getElementById('get-started-me2me').disabled = false; }; /** * initHomeScreenUi is called if the app is not starting up in session mode, * and also if the user cancels pin entry or the connection in session mode. */ remoting.initHomeScreenUi = function() { remoting.hostController = new remoting.HostController(); document.getElementById('share-button').disabled = !isIT2MeSupported_(); remoting.setMode(remoting.AppMode.HOME); remoting.hostSetupDialog = new remoting.HostSetupDialog(remoting.hostController); var dialog = document.getElementById('paired-clients-list'); var message = document.getElementById('paired-client-manager-message'); var deleteAll = document.getElementById('delete-all-paired-clients'); var close = document.getElementById('close-paired-client-manager-dialog'); var working = document.getElementById('paired-client-manager-dialog-working'); var error = document.getElementById('paired-client-manager-dialog-error'); var noPairedClients = document.getElementById('no-paired-clients'); remoting.pairedClientManager = new remoting.PairedClientManager(remoting.hostController, dialog, message, deleteAll, close, noPairedClients, working, error); // Display the cached host list, then asynchronously update and re-display it. remoting.updateLocalHostState(); remoting.hostList.refresh(remoting.updateLocalHostState); remoting.butterBar = new remoting.ButterBar(); }; /** * Fetches local host state and updates the DOM accordingly. */ remoting.updateLocalHostState = function() { /** * @param {remoting.HostController.State} state Host state. */ var onHostState = function(state) { if (state == remoting.HostController.State.STARTED) { remoting.hostController.getLocalHostId(onHostId.bind(null, state)); } else { onHostId(state, null); } }; /** * @param {remoting.HostController.State} state Host state. * @param {string?} hostId Host id. */ var onHostId = function(state, hostId) { remoting.hostList.setLocalHostStateAndId(state, hostId); remoting.hostList.display(); }; /** * @param {boolean} response True if the feature is present. */ var onHasFeatureResponse = function(response) { /** * @param {remoting.Error} error */ var onError = function(error) { console.error('Failed to get pairing status: ' + error); remoting.pairedClientManager.setPairedClients([]); }; if (response) { remoting.hostController.getPairedClients( remoting.pairedClientManager.setPairedClients.bind( remoting.pairedClientManager), onError); } else { console.log('Pairing registry not supported by host.'); remoting.pairedClientManager.setPairedClients([]); } }; remoting.hostController.hasFeature( remoting.HostController.Feature.PAIRING_REGISTRY, onHasFeatureResponse); remoting.hostController.getLocalHostState(onHostState); }; /** * @return {string} Information about the current extension. */ remoting.getExtensionInfo = function() { var v2OrLegacy = remoting.isAppsV2 ? " (v2)" : " (legacy)"; var manifest = chrome.runtime.getManifest(); if (manifest && manifest.version) { var name = chrome.i18n.getMessage('PRODUCT_NAME'); return name + ' version: ' + manifest.version + v2OrLegacy; } else { return 'Failed to get product version. Corrupt manifest?'; } }; /** * Returns Chrome version. * @return {string?} */ remoting.getChromeVersion = function() { var match = new RegExp('Chrome/([0-9.]*)').exec(navigator.userAgent); if (match && (match.length >= 2)) { return match[1]; } return null; }; /** * If an IT2Me client or host is active then prompt the user before closing. * If a Me2Me client is active then don't bother, since closing the window is * the more intuitive way to end a Me2Me session, and re-connecting is easy. */ remoting.promptClose = function() { if (!remoting.clientSession || remoting.clientSession.getMode() == remoting.ClientSession.Mode.ME2ME) { return null; } switch (remoting.currentMode) { case remoting.AppMode.CLIENT_CONNECTING: case remoting.AppMode.HOST_WAITING_FOR_CODE: case remoting.AppMode.HOST_WAITING_FOR_CONNECTION: case remoting.AppMode.HOST_SHARED: case remoting.AppMode.IN_SESSION: return chrome.i18n.getMessage(/*i18n-content*/'CLOSE_PROMPT'); default: return null; } }; /** * Sign the user out of Chromoting by clearing (and revoking, if possible) the * OAuth refresh token. * * Also clear all local storage, to avoid leaking information. */ remoting.signOut = function() { remoting.oauth2.clear(); chrome.storage.local.clear(); remoting.setMode(remoting.AppMode.HOME); document.getElementById('auth-dialog').hidden = false; }; /** * Returns whether the app is running on ChromeOS. * * @return {boolean} True if the app is running on ChromeOS. */ remoting.runningOnChromeOS = function() { return !!navigator.userAgent.match(/\bCrOS\b/); } /** * Callback function called when the browser window gets a paste operation. * * @param {Event} eventUncast * @return {void} Nothing. */ function pluginGotPaste_(eventUncast) { var event = /** @type {remoting.ClipboardEvent} */ eventUncast; if (event && event.clipboardData) { remoting.clipboard.toHost(event.clipboardData); } } /** * Callback function called when the browser window gets a copy operation. * * @param {Event} eventUncast * @return {void} Nothing. */ function pluginGotCopy_(eventUncast) { var event = /** @type {remoting.ClipboardEvent} */ eventUncast; if (event && event.clipboardData) { if (remoting.clipboard.toOs(event.clipboardData)) { // The default action may overwrite items that we added to clipboardData. event.preventDefault(); } } } /** * Returns whether Host mode is supported on this platform. * * @return {boolean} True if Host mode is supported. */ function isHostModeSupported_() { // Currently, sharing on Chromebooks is not supported. return !remoting.runningOnChromeOS(); } /** * @return {Object.<string, string>} The URL parameters. */ function getUrlParameters_() { var result = {}; var parts = window.location.search.substring(1).split('&'); for (var i = 0; i < parts.length; i++) { var pair = parts[i].split('='); result[pair[0]] = decodeURIComponent(pair[1]); } return result; } /** * @param {string} jsonString A JSON-encoded string. * @return {*} The decoded object, or undefined if the string cannot be parsed. */ function jsonParseSafe(jsonString) { try { return JSON.parse(jsonString); } catch (err) { return undefined; } } /** * Return the current time as a formatted string suitable for logging. * * @return {string} The current time, formatted as [mmdd/hhmmss.xyz] */ remoting.timestamp = function() { /** * @param {number} num A number. * @param {number} len The required length of the answer. * @return {string} The number, formatted as a string of the specified length * by prepending zeroes as necessary. */ var pad = function(num, len) { var result = num.toString(); if (result.length < len) { result = new Array(len - result.length + 1).join('0') + result; } return result; }; var now = new Date(); var timestamp = pad(now.getMonth() + 1, 2) + pad(now.getDate(), 2) + '/' + pad(now.getHours(), 2) + pad(now.getMinutes(), 2) + pad(now.getSeconds(), 2) + '.' + pad(now.getMilliseconds(), 3); return '[' + timestamp + ']'; }; /** * Show an error message, optionally including a short-cut for signing in to * Chromoting again. * * @param {remoting.Error} error * @return {void} Nothing. */ remoting.showErrorMessage = function(error) { l10n.localizeElementFromTag( document.getElementById('token-refresh-error-message'), error); var auth_failed = (error == remoting.Error.AUTHENTICATION_FAILED); document.getElementById('token-refresh-auth-failed').hidden = !auth_failed; document.getElementById('token-refresh-other-error').hidden = auth_failed; remoting.setMode(remoting.AppMode.TOKEN_REFRESH_FAILED); }; /** * Determine whether or not the app is running in a window. * @param {function(boolean):void} callback Callback to receive whether or not * the current tab is running in windowed mode. */ function isWindowed_(callback) { /** @param {chrome.Window} win The current window. */ var windowCallback = function(win) { callback(win.type == 'popup'); }; /** @param {chrome.Tab} tab The current tab. */ var tabCallback = function(tab) { if (tab.pinned) { callback(false); } else { chrome.windows.get(tab.windowId, null, windowCallback); } }; if (chrome.tabs) { chrome.tabs.getCurrent(tabCallback); } else { console.error('chome.tabs is not available.'); } } /** * Migrate settings in window.localStorage to chrome.storage.local so that * users of older web-apps that used the former do not lose their settings. */ function migrateLocalToChromeStorage_() { // The OAuth2 class still uses window.localStorage, so don't migrate any of // those settings. var oauthSettings = [ 'oauth2-refresh-token', 'oauth2-refresh-token-revokable', 'oauth2-access-token', 'oauth2-xsrf-token', 'remoting-email' ]; for (var setting in window.localStorage) { if (oauthSettings.indexOf(setting) == -1) { var copy = {} copy[setting] = window.localStorage.getItem(setting); chrome.storage.local.set(copy); window.localStorage.removeItem(setting); } } } /** * Generate a nonce, to be used as an xsrf protection token. * * @return {string} A URL-Safe Base64-encoded 128-bit random value. */ remoting.generateXsrfToken = function() { var random = new Uint8Array(16); window.crypto.getRandomValues(random); var base64Token = window.btoa(String.fromCharCode.apply(null, random)); return base64Token.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); };