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