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