remoting.js revision 0529e5d033099cbfc42635f6f6183833b09dff6e
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  console.log(remoting.getExtensionInfo());
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  var menuFeedback = new remoting.Feedback(
86      document.getElementById('help-feedback-main'),
87      document.getElementById('help-main'),
88      document.getElementById('send-feedback-main'));
89  var toolbarFeedback = new remoting.Feedback(
90      document.getElementById('help-feedback-toolbar'),
91      document.getElementById('help-toolbar'),
92      document.getElementById('send-feedback-toolbar'));
93
94  /** @param {remoting.Error} error */
95  var onGetEmailError = function(error) {
96    // No need to show the error message for NOT_AUTHENTICATED
97    // because we will show "auth-dialog".
98    if (error != remoting.Error.NOT_AUTHENTICATED) {
99      remoting.showErrorMessage(error);
100    }
101  }
102  remoting.identity.getEmail(remoting.onEmail, onGetEmailError);
103
104  remoting.showOrHideIT2MeUi();
105  remoting.showOrHideMe2MeUi();
106
107  // The plugin's onFocus handler sends a paste command to |window|, because
108  // it can't send one to the plugin element itself.
109  window.addEventListener('paste', pluginGotPaste_, false);
110  window.addEventListener('copy', pluginGotCopy_, false);
111
112  remoting.initModalDialogs();
113
114  if (isHostModeSupported_()) {
115    var noShare = document.getElementById('chrome-os-no-share');
116    noShare.parentNode.removeChild(noShare);
117  } else {
118    var button = document.getElementById('share-button');
119    button.disabled = true;
120  }
121
122  var onLoad = function() {
123    // Parse URL parameters.
124    var urlParams = getUrlParameters_();
125    if ('mode' in urlParams) {
126      if (urlParams['mode'] == 'me2me') {
127        var hostId = urlParams['hostId'];
128        remoting.connectMe2Me(hostId);
129        return;
130      }
131    }
132    // No valid URL parameters, start up normally.
133    remoting.initHomeScreenUi();
134  }
135  remoting.hostList.load(onLoad);
136
137  // For Apps v1, check the tab type to warn the user if they are not getting
138  // the best keyboard experience.
139  if (!remoting.isAppsV2 && navigator.platform.indexOf('Mac') == -1) {
140    /** @param {boolean} isWindowed */
141    var onIsWindowed = function(isWindowed) {
142      if (!isWindowed) {
143        document.getElementById('startup-mode-box-me2me').hidden = false;
144        document.getElementById('startup-mode-box-it2me').hidden = false;
145      }
146    };
147    isWindowed_(onIsWindowed);
148  }
149};
150
151/**
152 * Returns whether or not IT2Me is supported via the host NPAPI plugin.
153 *
154 * @return {boolean}
155 */
156function isIT2MeSupported_() {
157  // Currently, IT2Me on Chromebooks is not supported.
158  return !remoting.runningOnChromeOS();
159}
160
161/**
162 * Create an instance of the NPAPI plugin.
163 * @param {Element} container The element to add the plugin to.
164 * @return {remoting.HostPlugin} The new plugin instance or null if it failed to
165 *     load.
166 */
167remoting.createNpapiPlugin = function(container) {
168  var plugin = document.createElement('embed');
169  plugin.type = remoting.settings.PLUGIN_MIMETYPE;
170  // Hiding the plugin means it doesn't load, so make it size zero instead.
171  plugin.width = 0;
172  plugin.height = 0;
173  container.appendChild(plugin);
174
175  // Verify if the plugin was loaded successfully.
176  if (!plugin.hasOwnProperty('REQUESTED_ACCESS_CODE')) {
177    container.removeChild(plugin);
178    return null;
179  }
180
181  return /** @type {remoting.HostPlugin} */ (plugin);
182};
183
184/**
185 * Returns true if the current platform is fully supported. It's only used when
186 * we detect that host native messaging components are not installed. In that
187 * case the result of this function determines if the webapp should show the
188 * controls that allow to install and enable Me2Me host.
189 *
190 * @return {boolean}
191 */
192remoting.isMe2MeInstallable = function() {
193  /** @type {string} */
194  var platform = navigator.platform;
195  // The chromoting host is currently not installable on ChromeOS.
196  // For Linux, we have a install package for Ubuntu but not other distros.
197  // Since we cannot tell from javascript alone the Linux distro the client is
198  // on, we don't show the daemon-control UI for Linux unless the host is
199  // installed.
200  return platform == 'Win32' || platform == 'MacIntel';
201}
202
203/**
204 * Display the user's email address and allow access to the rest of the app,
205 * including parsing URL parameters.
206 *
207 * @param {string} email The user's email address.
208 * @return {void} Nothing.
209 */
210remoting.onEmail = function(email) {
211  document.getElementById('current-email').innerText = email;
212  document.getElementById('get-started-it2me').disabled = false;
213  document.getElementById('get-started-me2me').disabled = false;
214};
215
216/**
217 * initHomeScreenUi is called if the app is not starting up in session mode,
218 * and also if the user cancels pin entry or the connection in session mode.
219 */
220remoting.initHomeScreenUi = function() {
221  remoting.hostController = new remoting.HostController();
222  document.getElementById('share-button').disabled = !isIT2MeSupported_();
223  remoting.setMode(remoting.AppMode.HOME);
224  remoting.hostSetupDialog =
225      new remoting.HostSetupDialog(remoting.hostController);
226  var dialog = document.getElementById('paired-clients-list');
227  var message = document.getElementById('paired-client-manager-message');
228  var deleteAll = document.getElementById('delete-all-paired-clients');
229  var close = document.getElementById('close-paired-client-manager-dialog');
230  var working = document.getElementById('paired-client-manager-dialog-working');
231  var error = document.getElementById('paired-client-manager-dialog-error');
232  var noPairedClients = document.getElementById('no-paired-clients');
233  remoting.pairedClientManager =
234      new remoting.PairedClientManager(remoting.hostController, dialog, message,
235                                       deleteAll, close, noPairedClients,
236                                       working, error);
237  // Display the cached host list, then asynchronously update and re-display it.
238  remoting.updateLocalHostState();
239  remoting.hostList.refresh(remoting.updateLocalHostState);
240  remoting.butterBar = new remoting.ButterBar();
241};
242
243/**
244 * Fetches local host state and updates the DOM accordingly.
245 */
246remoting.updateLocalHostState = function() {
247  /**
248   * @param {remoting.HostController.State} state Host state.
249   */
250  var onHostState = function(state) {
251    if (state == remoting.HostController.State.STARTED) {
252      remoting.hostController.getLocalHostId(onHostId.bind(null, state));
253    } else {
254      onHostId(state, null);
255    }
256  };
257
258  /**
259   * @param {remoting.HostController.State} state Host state.
260   * @param {string?} hostId Host id.
261   */
262  var onHostId = function(state, hostId) {
263    remoting.hostList.setLocalHostStateAndId(state, hostId);
264    remoting.hostList.display();
265  };
266
267  /**
268   * @param {boolean} response True if the feature is present.
269   */
270  var onHasFeatureResponse = function(response) {
271    /**
272     * @param {remoting.Error} error
273     */
274    var onError = function(error) {
275      console.error('Failed to get pairing status: ' + error);
276      remoting.pairedClientManager.setPairedClients([]);
277    };
278
279    if (response) {
280      remoting.hostController.getPairedClients(
281          remoting.pairedClientManager.setPairedClients.bind(
282              remoting.pairedClientManager),
283          onError);
284    } else {
285      console.log('Pairing registry not supported by host.');
286      remoting.pairedClientManager.setPairedClients([]);
287    }
288  };
289
290  remoting.hostController.hasFeature(
291      remoting.HostController.Feature.PAIRING_REGISTRY, onHasFeatureResponse);
292  remoting.hostController.getLocalHostState(onHostState);
293};
294
295/**
296 * @return {string} Information about the current extension.
297 */
298remoting.getExtensionInfo = function() {
299  var v2OrLegacy = remoting.isAppsV2 ? " (v2)" : " (legacy)";
300  var manifest = chrome.runtime.getManifest();
301  if (manifest && manifest.version) {
302    var name = chrome.i18n.getMessage('PRODUCT_NAME');
303    return name + ' version: ' + manifest.version + v2OrLegacy;
304  } else {
305    return 'Failed to get product version. Corrupt manifest?';
306  }
307};
308
309/**
310 * If an IT2Me client or host is active then prompt the user before closing.
311 * If a Me2Me client is active then don't bother, since closing the window is
312 * the more intuitive way to end a Me2Me session, and re-connecting is easy.
313 */
314remoting.promptClose = function() {
315  if (!remoting.clientSession ||
316      remoting.clientSession.getMode() == remoting.ClientSession.Mode.ME2ME) {
317    return null;
318  }
319  switch (remoting.currentMode) {
320    case remoting.AppMode.CLIENT_CONNECTING:
321    case remoting.AppMode.HOST_WAITING_FOR_CODE:
322    case remoting.AppMode.HOST_WAITING_FOR_CONNECTION:
323    case remoting.AppMode.HOST_SHARED:
324    case remoting.AppMode.IN_SESSION:
325      return chrome.i18n.getMessage(/*i18n-content*/'CLOSE_PROMPT');
326    default:
327      return null;
328  }
329};
330
331/**
332 * Sign the user out of Chromoting by clearing (and revoking, if possible) the
333 * OAuth refresh token.
334 *
335 * Also clear all local storage, to avoid leaking information.
336 */
337remoting.signOut = function() {
338  remoting.oauth2.clear();
339  chrome.storage.local.clear();
340  remoting.setMode(remoting.AppMode.HOME);
341  document.getElementById('auth-dialog').hidden = false;
342};
343
344/**
345 * Returns whether the app is running on ChromeOS.
346 *
347 * @return {boolean} True if the app is running on ChromeOS.
348 */
349remoting.runningOnChromeOS = function() {
350  return !!navigator.userAgent.match(/\bCrOS\b/);
351}
352
353/**
354 * Callback function called when the browser window gets a paste operation.
355 *
356 * @param {Event} eventUncast
357 * @return {void} Nothing.
358 */
359function pluginGotPaste_(eventUncast) {
360  var event = /** @type {remoting.ClipboardEvent} */ eventUncast;
361  if (event && event.clipboardData) {
362    remoting.clipboard.toHost(event.clipboardData);
363  }
364}
365
366/**
367 * Callback function called when the browser window gets a copy operation.
368 *
369 * @param {Event} eventUncast
370 * @return {void} Nothing.
371 */
372function pluginGotCopy_(eventUncast) {
373  var event = /** @type {remoting.ClipboardEvent} */ eventUncast;
374  if (event && event.clipboardData) {
375    if (remoting.clipboard.toOs(event.clipboardData)) {
376      // The default action may overwrite items that we added to clipboardData.
377      event.preventDefault();
378    }
379  }
380}
381
382/**
383 * Returns whether Host mode is supported on this platform.
384 *
385 * @return {boolean} True if Host mode is supported.
386 */
387function isHostModeSupported_() {
388  // Currently, sharing on Chromebooks is not supported.
389  return !remoting.runningOnChromeOS();
390}
391
392/**
393 * @return {Object.<string, string>} The URL parameters.
394 */
395function getUrlParameters_() {
396  var result = {};
397  var parts = window.location.search.substring(1).split('&');
398  for (var i = 0; i < parts.length; i++) {
399    var pair = parts[i].split('=');
400    result[pair[0]] = decodeURIComponent(pair[1]);
401  }
402  return result;
403}
404
405/**
406 * @param {string} jsonString A JSON-encoded string.
407 * @return {*} The decoded object, or undefined if the string cannot be parsed.
408 */
409function jsonParseSafe(jsonString) {
410  try {
411    return JSON.parse(jsonString);
412  } catch (err) {
413    return undefined;
414  }
415}
416
417/**
418 * Return the current time as a formatted string suitable for logging.
419 *
420 * @return {string} The current time, formatted as [mmdd/hhmmss.xyz]
421 */
422remoting.timestamp = function() {
423  /**
424   * @param {number} num A number.
425   * @param {number} len The required length of the answer.
426   * @return {string} The number, formatted as a string of the specified length
427   *     by prepending zeroes as necessary.
428   */
429  var pad = function(num, len) {
430    var result = num.toString();
431    if (result.length < len) {
432      result = new Array(len - result.length + 1).join('0') + result;
433    }
434    return result;
435  };
436  var now = new Date();
437  var timestamp = pad(now.getMonth() + 1, 2) + pad(now.getDate(), 2) + '/' +
438      pad(now.getHours(), 2) + pad(now.getMinutes(), 2) +
439      pad(now.getSeconds(), 2) + '.' + pad(now.getMilliseconds(), 3);
440  return '[' + timestamp + ']';
441};
442
443/**
444 * Show an error message, optionally including a short-cut for signing in to
445 * Chromoting again.
446 *
447 * @param {remoting.Error} error
448 * @return {void} Nothing.
449 */
450remoting.showErrorMessage = function(error) {
451  l10n.localizeElementFromTag(
452      document.getElementById('token-refresh-error-message'),
453      error);
454  var auth_failed = (error == remoting.Error.AUTHENTICATION_FAILED);
455  document.getElementById('token-refresh-auth-failed').hidden = !auth_failed;
456  document.getElementById('token-refresh-other-error').hidden = auth_failed;
457  remoting.setMode(remoting.AppMode.TOKEN_REFRESH_FAILED);
458};
459
460/**
461 * Determine whether or not the app is running in a window.
462 * @param {function(boolean):void} callback Callback to receive whether or not
463 *     the current tab is running in windowed mode.
464 */
465function isWindowed_(callback) {
466  /** @param {chrome.Window} win The current window. */
467  var windowCallback = function(win) {
468    callback(win.type == 'popup');
469  };
470  /** @param {chrome.Tab} tab The current tab. */
471  var tabCallback = function(tab) {
472    if (tab.pinned) {
473      callback(false);
474    } else {
475      chrome.windows.get(tab.windowId, null, windowCallback);
476    }
477  };
478  if (chrome.tabs) {
479    chrome.tabs.getCurrent(tabCallback);
480  } else {
481    console.error('chome.tabs is not available.');
482  }
483}
484
485/**
486 * Migrate settings in window.localStorage to chrome.storage.local so that
487 * users of older web-apps that used the former do not lose their settings.
488 */
489function migrateLocalToChromeStorage_() {
490  // The OAuth2 class still uses window.localStorage, so don't migrate any of
491  // those settings.
492  var oauthSettings = [
493      'oauth2-refresh-token',
494      'oauth2-refresh-token-revokable',
495      'oauth2-access-token',
496      'oauth2-xsrf-token',
497      'remoting-email'
498  ];
499  for (var setting in window.localStorage) {
500    if (oauthSettings.indexOf(setting) == -1) {
501      var copy = {}
502      copy[setting] = window.localStorage.getItem(setting);
503      chrome.storage.local.set(copy);
504      window.localStorage.removeItem(setting);
505    }
506  }
507}
508
509/**
510 * Generate a nonce, to be used as an xsrf protection token.
511 *
512 * @return {string} A URL-Safe Base64-encoded 128-bit random value. */
513remoting.generateXsrfToken = function() {
514  var random = new Uint8Array(16);
515  window.crypto.getRandomValues(random);
516  var base64Token = window.btoa(String.fromCharCode.apply(null, random));
517  return base64Token.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
518};
519