• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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