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.
7 /** @suppress {duplicate} */
8 var remoting = remoting || {};
10 /** @type {remoting.HostSession} */ remoting.hostSession = null;
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.
20 * Show the authorization consent UI and register a one-shot event handler to
21 * continue the authorization process.
23 * @param {function():void} authContinue Callback to invoke when the user
26 function 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) {
33 button.removeEventListener('click', consentGranted, false);
36 dialog.hidden = false;
37 button.addEventListener('click', consentGranted, false);
41 * Entry point for app initialization.
43 remoting.init = function() {
44 if (base.isAppsV2()) {
45 var htmlNode = /** @type {HTMLElement} */ (document.body.parentNode);
46 htmlNode.classList.add('apps-v2');
48 migrateLocalToChromeStorage_();
51 console.log(remoting.getExtensionInfo());
54 // Create global objects.
55 remoting.settings = new remoting.Settings();
56 if (base.isAppsV2()) {
57 remoting.identity = new remoting.Identity(consentRequired_);
58 remoting.fullscreen = new remoting.FullscreenAppsV2();
59 remoting.windowFrame = new remoting.WindowFrame(
60 document.getElementById('title-bar'));
62 remoting.oauth2 = new remoting.OAuth2();
63 if (!remoting.oauth2.isAuthenticated()) {
64 document.getElementById('auth-dialog').hidden = false;
66 remoting.identity = remoting.oauth2;
67 remoting.fullscreen = new remoting.FullscreenAppsV1();
69 remoting.stats = new remoting.ConnectionStats(
70 document.getElementById('statistics'));
71 remoting.formatIq = new remoting.FormatIq();
72 remoting.hostList = new remoting.HostList(
73 document.getElementById('host-list'),
74 document.getElementById('host-list-empty'),
75 document.getElementById('host-list-error-message'),
76 document.getElementById('host-list-refresh-failed-button'),
77 document.getElementById('host-list-loading-indicator'));
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 var homeFeedback = new remoting.MenuButton(
85 document.getElementById('help-feedback-main'));
86 var toolbarFeedback = new remoting.MenuButton(
87 document.getElementById('help-feedback-toolbar'));
88 remoting.manageHelpAndFeedback(
89 document.getElementById('title-bar'));
90 remoting.manageHelpAndFeedback(
91 document.getElementById('help-feedback-toolbar'));
92 remoting.manageHelpAndFeedback(
93 document.getElementById('help-feedback-main'));
95 /** @param {remoting.Error} error */
96 var onGetEmailError = function(error) {
97 // No need to show the error message for NOT_AUTHENTICATED
98 // because we will show "auth-dialog".
99 if (error != remoting.Error.NOT_AUTHENTICATED) {
100 remoting.showErrorMessage(error);
103 remoting.identity.getEmail(remoting.onEmail, onGetEmailError);
105 remoting.showOrHideIT2MeUi();
106 remoting.showOrHideMe2MeUi();
108 // The plugin's onFocus handler sends a paste command to |window|, because
109 // it can't send one to the plugin element itself.
110 window.addEventListener('paste', pluginGotPaste_, false);
111 window.addEventListener('copy', pluginGotCopy_, false);
113 remoting.initModalDialogs();
115 if (isHostModeSupported_()) {
116 var noShare = document.getElementById('chrome-os-no-share');
117 noShare.parentNode.removeChild(noShare);
119 var button = document.getElementById('share-button');
120 button.disabled = true;
124 * @return {Promise} A promise that resolves to the id of the current
125 * containing tab/window.
127 var getCurrentId = function () {
128 if (base.isAppsV2()) {
129 return Promise.resolve(chrome.app.window.current().id);
133 * @param {function(*=):void} resolve
134 * @param {function(*=):void} reject
136 return new Promise(function(resolve, reject) {
137 /** @param {chrome.Tab} tab */
138 chrome.tabs.getCurrent(function(tab){
140 resolve(String(tab.id));
142 reject('Cannot retrieve the current tab.');
147 var onLoad = function() {
148 // Parse URL parameters.
149 var urlParams = getUrlParameters_();
150 if ('mode' in urlParams) {
151 if (urlParams['mode'] === 'me2me') {
152 var hostId = urlParams['hostId'];
153 remoting.connectMe2Me(hostId);
155 } else if (urlParams['mode'] === 'hangout') {
157 getCurrentId().then(function(id) {
158 /** @type {string} */
159 var accessCode = urlParams['accessCode'];
160 remoting.ensureSessionConnector_();
161 remoting.setMode(remoting.AppMode.CLIENT_CONNECTING);
162 remoting.connector.connectIT2Me(accessCode);
164 document.body.classList.add('hangout-remote-desktop');
165 var senderId = /** @type {string} */ String(id);
166 var hangoutSession = new remoting.HangoutSession(senderId);
167 hangoutSession.init();
172 // No valid URL parameters, start up normally.
173 remoting.initHomeScreenUi();
175 remoting.hostList.load(onLoad);
177 // For Apps v1, check the tab type to warn the user if they are not getting
178 // the best keyboard experience.
179 if (!base.isAppsV2() && !remoting.platformIsMac()) {
180 /** @param {boolean} isWindowed */
181 var onIsWindowed = function(isWindowed) {
183 document.getElementById('startup-mode-box-me2me').hidden = false;
184 document.getElementById('startup-mode-box-it2me').hidden = false;
187 isWindowed_(onIsWindowed);
190 remoting.testEvents = new base.EventSource();
192 /** @enum {string} */
193 remoting.testEvents.Names = {
194 uiModeChanged: 'uiModeChanged'
196 remoting.testEvents.defineEvents(base.values(remoting.testEvents.Names));
198 remoting.ClientPlugin.preload();
202 * Returns whether or not IT2Me is supported via the host NPAPI plugin.
206 function isIT2MeSupported_() {
207 // Currently, IT2Me on Chromebooks is not supported.
208 return !remoting.runningOnChromeOS();
212 * Returns true if the current platform is fully supported. It's only used when
213 * we detect that host native messaging components are not installed. In that
214 * case the result of this function determines if the webapp should show the
215 * controls that allow to install and enable Me2Me host.
219 remoting.isMe2MeInstallable = function() {
220 // The chromoting host is currently not installable on ChromeOS.
221 // For Linux, we have a install package for Ubuntu but not other distros.
222 // Since we cannot tell from javascript alone the Linux distro the client is
223 // on, we don't show the daemon-control UI for Linux unless the host is
225 return remoting.platformIsWindows() || remoting.platformIsMac();
229 * Display the user's email address and allow access to the rest of the app,
230 * including parsing URL parameters.
232 * @param {string} email The user's email address.
233 * @return {void} Nothing.
235 remoting.onEmail = function(email) {
236 document.getElementById('current-email').innerText = email;
237 document.getElementById('get-started-it2me').disabled = false;
238 document.getElementById('get-started-me2me').disabled = false;
242 * initHomeScreenUi is called if the app is not starting up in session mode,
243 * and also if the user cancels pin entry or the connection in session mode.
245 remoting.initHomeScreenUi = function() {
246 remoting.hostController = new remoting.HostController();
247 document.getElementById('share-button').disabled = !isIT2MeSupported_();
248 remoting.setMode(remoting.AppMode.HOME);
249 remoting.hostSetupDialog =
250 new remoting.HostSetupDialog(remoting.hostController);
251 var dialog = document.getElementById('paired-clients-list');
252 var message = document.getElementById('paired-client-manager-message');
253 var deleteAll = document.getElementById('delete-all-paired-clients');
254 var close = document.getElementById('close-paired-client-manager-dialog');
255 var working = document.getElementById('paired-client-manager-dialog-working');
256 var error = document.getElementById('paired-client-manager-dialog-error');
257 var noPairedClients = document.getElementById('no-paired-clients');
258 remoting.pairedClientManager =
259 new remoting.PairedClientManager(remoting.hostController, dialog, message,
260 deleteAll, close, noPairedClients,
262 // Display the cached host list, then asynchronously update and re-display it.
263 remoting.updateLocalHostState();
264 remoting.hostList.refresh(remoting.updateLocalHostState);
265 remoting.butterBar = new remoting.ButterBar();
269 * Fetches local host state and updates the DOM accordingly.
271 remoting.updateLocalHostState = function() {
273 * @param {remoting.HostController.State} state Host state.
275 var onHostState = function(state) {
276 if (state == remoting.HostController.State.STARTED) {
277 remoting.hostController.getLocalHostId(onHostId.bind(null, state));
279 onHostId(state, null);
284 * @param {remoting.HostController.State} state Host state.
285 * @param {string?} hostId Host id.
287 var onHostId = function(state, hostId) {
288 remoting.hostList.setLocalHostStateAndId(state, hostId);
289 remoting.hostList.display();
293 * @param {boolean} response True if the feature is present.
295 var onHasFeatureResponse = function(response) {
297 * @param {remoting.Error} error
299 var onError = function(error) {
300 console.error('Failed to get pairing status: ' + error);
301 remoting.pairedClientManager.setPairedClients([]);
305 remoting.hostController.getPairedClients(
306 remoting.pairedClientManager.setPairedClients.bind(
307 remoting.pairedClientManager),
310 console.log('Pairing registry not supported by host.');
311 remoting.pairedClientManager.setPairedClients([]);
315 remoting.hostController.hasFeature(
316 remoting.HostController.Feature.PAIRING_REGISTRY, onHasFeatureResponse);
317 remoting.hostController.getLocalHostState(onHostState);
321 * @return {string} Information about the current extension.
323 remoting.getExtensionInfo = function() {
324 var v2OrLegacy = base.isAppsV2() ? " (v2)" : " (legacy)";
325 var manifest = chrome.runtime.getManifest();
326 if (manifest && manifest.version) {
327 var name = chrome.i18n.getMessage('PRODUCT_NAME');
328 return name + ' version: ' + manifest.version + v2OrLegacy;
330 return 'Failed to get product version. Corrupt manifest?';
335 * Returns Chrome version.
338 remoting.getChromeVersion = function() {
339 var match = new RegExp('Chrome/([0-9.]*)').exec(navigator.userAgent);
340 if (match && (match.length >= 2)) {
347 * If an IT2Me client or host is active then prompt the user before closing.
348 * If a Me2Me client is active then don't bother, since closing the window is
349 * the more intuitive way to end a Me2Me session, and re-connecting is easy.
351 remoting.promptClose = function() {
352 if (!remoting.clientSession ||
353 remoting.clientSession.getMode() == remoting.ClientSession.Mode.ME2ME) {
356 switch (remoting.currentMode) {
357 case remoting.AppMode.CLIENT_CONNECTING:
358 case remoting.AppMode.HOST_WAITING_FOR_CODE:
359 case remoting.AppMode.HOST_WAITING_FOR_CONNECTION:
360 case remoting.AppMode.HOST_SHARED:
361 case remoting.AppMode.IN_SESSION:
362 return chrome.i18n.getMessage(/*i18n-content*/'CLOSE_PROMPT');
369 * Sign the user out of Chromoting by clearing (and revoking, if possible) the
370 * OAuth refresh token.
372 * Also clear all local storage, to avoid leaking information.
374 remoting.signOut = function() {
375 remoting.oauth2.clear();
376 chrome.storage.local.clear();
377 remoting.setMode(remoting.AppMode.HOME);
378 document.getElementById('auth-dialog').hidden = false;
382 * Returns whether the app is running on ChromeOS.
384 * @return {boolean} True if the app is running on ChromeOS.
386 remoting.runningOnChromeOS = function() {
387 return !!navigator.userAgent.match(/\bCrOS\b/);
391 * Callback function called when the browser window gets a paste operation.
393 * @param {Event} eventUncast
394 * @return {void} Nothing.
396 function pluginGotPaste_(eventUncast) {
397 var event = /** @type {remoting.ClipboardEvent} */ eventUncast;
398 if (event && event.clipboardData) {
399 remoting.clipboard.toHost(event.clipboardData);
404 * Callback function called when the browser window gets a copy operation.
406 * @param {Event} eventUncast
407 * @return {void} Nothing.
409 function pluginGotCopy_(eventUncast) {
410 var event = /** @type {remoting.ClipboardEvent} */ eventUncast;
411 if (event && event.clipboardData) {
412 if (remoting.clipboard.toOs(event.clipboardData)) {
413 // The default action may overwrite items that we added to clipboardData.
414 event.preventDefault();
420 * Returns whether Host mode is supported on this platform.
422 * @return {boolean} True if Host mode is supported.
424 function isHostModeSupported_() {
425 // Currently, sharing on Chromebooks is not supported.
426 return !remoting.runningOnChromeOS();
430 * @return {Object.<string, string>} The URL parameters.
432 function getUrlParameters_() {
434 var parts = window.location.search.substring(1).split('&');
435 for (var i = 0; i < parts.length; i++) {
436 var pair = parts[i].split('=');
437 result[pair[0]] = decodeURIComponent(pair[1]);
443 * @param {string} jsonString A JSON-encoded string.
444 * @return {*} The decoded object, or undefined if the string cannot be parsed.
446 function jsonParseSafe(jsonString) {
448 return JSON.parse(jsonString);
455 * Return the current time as a formatted string suitable for logging.
457 * @return {string} The current time, formatted as [mmdd/hhmmss.xyz]
459 remoting.timestamp = function() {
461 * @param {number} num A number.
462 * @param {number} len The required length of the answer.
463 * @return {string} The number, formatted as a string of the specified length
464 * by prepending zeroes as necessary.
466 var pad = function(num, len) {
467 var result = num.toString();
468 if (result.length < len) {
469 result = new Array(len - result.length + 1).join('0') + result;
473 var now = new Date();
474 var timestamp = pad(now.getMonth() + 1, 2) + pad(now.getDate(), 2) + '/' +
475 pad(now.getHours(), 2) + pad(now.getMinutes(), 2) +
476 pad(now.getSeconds(), 2) + '.' + pad(now.getMilliseconds(), 3);
477 return '[' + timestamp + ']';
481 * Show an error message, optionally including a short-cut for signing in to
484 * @param {remoting.Error} error
485 * @return {void} Nothing.
487 remoting.showErrorMessage = function(error) {
488 l10n.localizeElementFromTag(
489 document.getElementById('token-refresh-error-message'),
491 var auth_failed = (error == remoting.Error.AUTHENTICATION_FAILED);
492 document.getElementById('token-refresh-auth-failed').hidden = !auth_failed;
493 document.getElementById('token-refresh-other-error').hidden = auth_failed;
494 remoting.setMode(remoting.AppMode.TOKEN_REFRESH_FAILED);
498 * Determine whether or not the app is running in a window.
499 * @param {function(boolean):void} callback Callback to receive whether or not
500 * the current tab is running in windowed mode.
502 function isWindowed_(callback) {
503 /** @param {chrome.Window} win The current window. */
504 var windowCallback = function(win) {
505 callback(win.type == 'popup');
507 /** @param {chrome.Tab} tab The current tab. */
508 var tabCallback = function(tab) {
512 chrome.windows.get(tab.windowId, null, windowCallback);
516 chrome.tabs.getCurrent(tabCallback);
518 console.error('chome.tabs is not available.');
523 * Migrate settings in window.localStorage to chrome.storage.local so that
524 * users of older web-apps that used the former do not lose their settings.
526 function migrateLocalToChromeStorage_() {
527 // The OAuth2 class still uses window.localStorage, so don't migrate any of
529 var oauthSettings = [
530 'oauth2-refresh-token',
531 'oauth2-refresh-token-revokable',
532 'oauth2-access-token',
536 for (var setting in window.localStorage) {
537 if (oauthSettings.indexOf(setting) == -1) {
539 copy[setting] = window.localStorage.getItem(setting);
540 chrome.storage.local.set(copy);
541 window.localStorage.removeItem(setting);
547 * Generate a nonce, to be used as an xsrf protection token.
549 * @return {string} A URL-Safe Base64-encoded 128-bit random value. */
550 remoting.generateXsrfToken = function() {
551 var random = new Uint8Array(16);
552 window.crypto.getRandomValues(random);
553 var base64Token = window.btoa(String.fromCharCode.apply(null, random));
554 return base64Token.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
558 * Tests whether we are running on Mac.
560 * @return {boolean} True if the platform is Mac.
562 remoting.platformIsMac = function() {
563 return navigator.platform.indexOf('Mac') != -1;
567 * Tests whether we are running on Windows.
569 * @return {boolean} True if the platform is Windows.
571 remoting.platformIsWindows = function() {
572 return (navigator.platform.indexOf('Win32') != -1) ||
573 (navigator.platform.indexOf('Win64') != -1);
577 * Tests whether we are running on Linux.
579 * @return {boolean} True if the platform is Linux.
581 remoting.platformIsLinux = function() {
582 return (navigator.platform.indexOf('Linux') != -1) &&
583 !remoting.platformIsChromeOS();
587 * Tests whether we are running on ChromeOS.
589 * @return {boolean} True if the platform is ChromeOS.
591 remoting.platformIsChromeOS = function() {
592 return navigator.userAgent.match(/\bCrOS\b/) != null;