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 {boolean} True if this is a v2 app; false if it is a legacy app.
15 remoting.isAppsV2 = false;
18 * Show the authorization consent UI and register a one-shot event handler to
19 * continue the authorization process.
21 * @param {function():void} authContinue Callback to invoke when the user
24 function 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) {
31 button.removeEventListener('click', consentGranted, false);
34 dialog.hidden = false;
35 button.addEventListener('click', consentGranted, false);
39 * Entry point for app initialization.
41 remoting.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');
52 if (!remoting.isAppsV2) {
53 migrateLocalToChromeStorage_();
56 console.log(remoting.getExtensionInfo());
59 // Create global objects.
60 remoting.settings = new remoting.Settings();
61 if (remoting.isAppsV2) {
62 remoting.identity = new remoting.Identity(consentRequired_);
63 remoting.fullscreen = new remoting.FullscreenAppsV2();
65 remoting.oauth2 = new remoting.OAuth2();
66 if (!remoting.oauth2.isAuthenticated()) {
67 document.getElementById('auth-dialog').hidden = false;
69 remoting.identity = remoting.oauth2;
70 remoting.fullscreen = new remoting.FullscreenAppsV1();
72 remoting.stats = new remoting.ConnectionStats(
73 document.getElementById('statistics'));
74 remoting.formatIq = new remoting.FormatIq();
75 remoting.hostList = new remoting.HostList(
76 document.getElementById('host-list'),
77 document.getElementById('host-list-empty'),
78 document.getElementById('host-list-error-message'),
79 document.getElementById('host-list-refresh-failed-button'),
80 document.getElementById('host-list-loading-indicator'));
81 remoting.toolbar = new remoting.Toolbar(
82 document.getElementById('session-toolbar'));
83 remoting.clipboard = new remoting.Clipboard();
84 var sandbox = /** @type {HTMLIFrameElement} */
85 document.getElementById('wcs-sandbox');
86 remoting.wcsSandbox = new remoting.WcsSandboxContainer(sandbox.contentWindow);
87 var menuFeedback = new remoting.Feedback(
88 document.getElementById('help-feedback-main'),
89 document.getElementById('help-main'),
90 document.getElementById('send-feedback-main'));
91 var toolbarFeedback = new remoting.Feedback(
92 document.getElementById('help-feedback-toolbar'),
93 document.getElementById('help-toolbar'),
94 document.getElementById('send-feedback-toolbar'));
96 /** @param {remoting.Error} error */
97 var onGetEmailError = function(error) {
98 // No need to show the error message for NOT_AUTHENTICATED
99 // because we will show "auth-dialog".
100 if (error != remoting.Error.NOT_AUTHENTICATED) {
101 remoting.showErrorMessage(error);
104 remoting.identity.getEmail(remoting.onEmail, onGetEmailError);
106 remoting.showOrHideIT2MeUi();
107 remoting.showOrHideMe2MeUi();
109 // The plugin's onFocus handler sends a paste command to |window|, because
110 // it can't send one to the plugin element itself.
111 window.addEventListener('paste', pluginGotPaste_, false);
112 window.addEventListener('copy', pluginGotCopy_, false);
114 remoting.initModalDialogs();
116 if (isHostModeSupported_()) {
117 var noShare = document.getElementById('chrome-os-no-share');
118 noShare.parentNode.removeChild(noShare);
120 var button = document.getElementById('share-button');
121 button.disabled = true;
124 var onLoad = function() {
125 // Parse URL parameters.
126 var urlParams = getUrlParameters_();
127 if ('mode' in urlParams) {
128 if (urlParams['mode'] == 'me2me') {
129 var hostId = urlParams['hostId'];
130 remoting.connectMe2Me(hostId);
134 // No valid URL parameters, start up normally.
135 remoting.initHomeScreenUi();
137 remoting.hostList.load(onLoad);
139 // For Apps v1, check the tab type to warn the user if they are not getting
140 // the best keyboard experience.
141 if (!remoting.isAppsV2 && navigator.platform.indexOf('Mac') == -1) {
142 /** @param {boolean} isWindowed */
143 var onIsWindowed = function(isWindowed) {
145 document.getElementById('startup-mode-box-me2me').hidden = false;
146 document.getElementById('startup-mode-box-it2me').hidden = false;
149 isWindowed_(onIsWindowed);
154 * Returns whether or not IT2Me is supported via the host NPAPI plugin.
158 function isIT2MeSupported_() {
159 // Currently, IT2Me on Chromebooks is not supported.
160 return !remoting.runningOnChromeOS();
164 * Create an instance of the NPAPI plugin.
165 * @param {Element} container The element to add the plugin to.
166 * @return {remoting.HostPlugin} The new plugin instance or null if it failed to
169 remoting.createNpapiPlugin = function(container) {
170 var plugin = document.createElement('embed');
171 plugin.type = remoting.settings.PLUGIN_MIMETYPE;
172 // Hiding the plugin means it doesn't load, so make it size zero instead.
175 container.appendChild(plugin);
177 // Verify if the plugin was loaded successfully.
178 if (!plugin.hasOwnProperty('REQUESTED_ACCESS_CODE')) {
179 container.removeChild(plugin);
183 return /** @type {remoting.HostPlugin} */ (plugin);
187 * Returns true if the current platform is fully supported. It's only used when
188 * we detect that host native messaging components are not installed. In that
189 * case the result of this function determines if the webapp should show the
190 * controls that allow to install and enable Me2Me host.
194 remoting.isMe2MeInstallable = function() {
195 /** @type {string} */
196 var platform = navigator.platform;
197 // The chromoting host is currently not installable on ChromeOS.
198 // For Linux, we have a install package for Ubuntu but not other distros.
199 // Since we cannot tell from javascript alone the Linux distro the client is
200 // on, we don't show the daemon-control UI for Linux unless the host is
202 return platform == 'Win32' || platform == 'MacIntel';
206 * Display the user's email address and allow access to the rest of the app,
207 * including parsing URL parameters.
209 * @param {string} email The user's email address.
210 * @return {void} Nothing.
212 remoting.onEmail = function(email) {
213 document.getElementById('current-email').innerText = email;
214 document.getElementById('get-started-it2me').disabled = false;
215 document.getElementById('get-started-me2me').disabled = false;
219 * initHomeScreenUi is called if the app is not starting up in session mode,
220 * and also if the user cancels pin entry or the connection in session mode.
222 remoting.initHomeScreenUi = function() {
223 remoting.hostController = new remoting.HostController();
224 document.getElementById('share-button').disabled = !isIT2MeSupported_();
225 remoting.setMode(remoting.AppMode.HOME);
226 remoting.hostSetupDialog =
227 new remoting.HostSetupDialog(remoting.hostController);
228 var dialog = document.getElementById('paired-clients-list');
229 var message = document.getElementById('paired-client-manager-message');
230 var deleteAll = document.getElementById('delete-all-paired-clients');
231 var close = document.getElementById('close-paired-client-manager-dialog');
232 var working = document.getElementById('paired-client-manager-dialog-working');
233 var error = document.getElementById('paired-client-manager-dialog-error');
234 var noPairedClients = document.getElementById('no-paired-clients');
235 remoting.pairedClientManager =
236 new remoting.PairedClientManager(remoting.hostController, dialog, message,
237 deleteAll, close, noPairedClients,
239 // Display the cached host list, then asynchronously update and re-display it.
240 remoting.updateLocalHostState();
241 remoting.hostList.refresh(remoting.updateLocalHostState);
242 remoting.butterBar = new remoting.ButterBar();
246 * Fetches local host state and updates the DOM accordingly.
248 remoting.updateLocalHostState = function() {
250 * @param {remoting.HostController.State} state Host state.
252 var onHostState = function(state) {
253 if (state == remoting.HostController.State.STARTED) {
254 remoting.hostController.getLocalHostId(onHostId.bind(null, state));
256 onHostId(state, null);
261 * @param {remoting.HostController.State} state Host state.
262 * @param {string?} hostId Host id.
264 var onHostId = function(state, hostId) {
265 remoting.hostList.setLocalHostStateAndId(state, hostId);
266 remoting.hostList.display();
270 * @param {boolean} response True if the feature is present.
272 var onHasFeatureResponse = function(response) {
274 * @param {remoting.Error} error
276 var onError = function(error) {
277 console.error('Failed to get pairing status: ' + error);
278 remoting.pairedClientManager.setPairedClients([]);
282 remoting.hostController.getPairedClients(
283 remoting.pairedClientManager.setPairedClients.bind(
284 remoting.pairedClientManager),
287 console.log('Pairing registry not supported by host.');
288 remoting.pairedClientManager.setPairedClients([]);
292 remoting.hostController.hasFeature(
293 remoting.HostController.Feature.PAIRING_REGISTRY, onHasFeatureResponse);
294 remoting.hostController.getLocalHostState(onHostState);
298 * @return {string} Information about the current extension.
300 remoting.getExtensionInfo = function() {
301 var v2OrLegacy = remoting.isAppsV2 ? " (v2)" : " (legacy)";
302 var manifest = chrome.runtime.getManifest();
303 if (manifest && manifest.version) {
304 var name = chrome.i18n.getMessage('PRODUCT_NAME');
305 return name + ' version: ' + manifest.version + v2OrLegacy;
307 return 'Failed to get product version. Corrupt manifest?';
312 * If an IT2Me client or host is active then prompt the user before closing.
313 * If a Me2Me client is active then don't bother, since closing the window is
314 * the more intuitive way to end a Me2Me session, and re-connecting is easy.
316 remoting.promptClose = function() {
317 if (!remoting.clientSession ||
318 remoting.clientSession.getMode() == remoting.ClientSession.Mode.ME2ME) {
321 switch (remoting.currentMode) {
322 case remoting.AppMode.CLIENT_CONNECTING:
323 case remoting.AppMode.HOST_WAITING_FOR_CODE:
324 case remoting.AppMode.HOST_WAITING_FOR_CONNECTION:
325 case remoting.AppMode.HOST_SHARED:
326 case remoting.AppMode.IN_SESSION:
327 return chrome.i18n.getMessage(/*i18n-content*/'CLOSE_PROMPT');
334 * Sign the user out of Chromoting by clearing (and revoking, if possible) the
335 * OAuth refresh token.
337 * Also clear all local storage, to avoid leaking information.
339 remoting.signOut = function() {
340 remoting.oauth2.clear();
341 chrome.storage.local.clear();
342 remoting.setMode(remoting.AppMode.HOME);
343 document.getElementById('auth-dialog').hidden = false;
347 * Returns whether the app is running on ChromeOS.
349 * @return {boolean} True if the app is running on ChromeOS.
351 remoting.runningOnChromeOS = function() {
352 return !!navigator.userAgent.match(/\bCrOS\b/);
356 * Callback function called when the browser window gets a paste operation.
358 * @param {Event} eventUncast
359 * @return {void} Nothing.
361 function pluginGotPaste_(eventUncast) {
362 var event = /** @type {remoting.ClipboardEvent} */ eventUncast;
363 if (event && event.clipboardData) {
364 remoting.clipboard.toHost(event.clipboardData);
369 * Callback function called when the browser window gets a copy operation.
371 * @param {Event} eventUncast
372 * @return {void} Nothing.
374 function pluginGotCopy_(eventUncast) {
375 var event = /** @type {remoting.ClipboardEvent} */ eventUncast;
376 if (event && event.clipboardData) {
377 if (remoting.clipboard.toOs(event.clipboardData)) {
378 // The default action may overwrite items that we added to clipboardData.
379 event.preventDefault();
385 * Returns whether Host mode is supported on this platform.
387 * @return {boolean} True if Host mode is supported.
389 function isHostModeSupported_() {
390 // Currently, sharing on Chromebooks is not supported.
391 return !remoting.runningOnChromeOS();
395 * @return {Object.<string, string>} The URL parameters.
397 function getUrlParameters_() {
399 var parts = window.location.search.substring(1).split('&');
400 for (var i = 0; i < parts.length; i++) {
401 var pair = parts[i].split('=');
402 result[pair[0]] = decodeURIComponent(pair[1]);
408 * @param {string} jsonString A JSON-encoded string.
409 * @return {*} The decoded object, or undefined if the string cannot be parsed.
411 function jsonParseSafe(jsonString) {
413 return JSON.parse(jsonString);
420 * Return the current time as a formatted string suitable for logging.
422 * @return {string} The current time, formatted as [mmdd/hhmmss.xyz]
424 remoting.timestamp = function() {
426 * @param {number} num A number.
427 * @param {number} len The required length of the answer.
428 * @return {string} The number, formatted as a string of the specified length
429 * by prepending zeroes as necessary.
431 var pad = function(num, len) {
432 var result = num.toString();
433 if (result.length < len) {
434 result = new Array(len - result.length + 1).join('0') + result;
438 var now = new Date();
439 var timestamp = pad(now.getMonth() + 1, 2) + pad(now.getDate(), 2) + '/' +
440 pad(now.getHours(), 2) + pad(now.getMinutes(), 2) +
441 pad(now.getSeconds(), 2) + '.' + pad(now.getMilliseconds(), 3);
442 return '[' + timestamp + ']';
446 * Show an error message, optionally including a short-cut for signing in to
449 * @param {remoting.Error} error
450 * @return {void} Nothing.
452 remoting.showErrorMessage = function(error) {
453 l10n.localizeElementFromTag(
454 document.getElementById('token-refresh-error-message'),
456 var auth_failed = (error == remoting.Error.AUTHENTICATION_FAILED);
457 document.getElementById('token-refresh-auth-failed').hidden = !auth_failed;
458 document.getElementById('token-refresh-other-error').hidden = auth_failed;
459 remoting.setMode(remoting.AppMode.TOKEN_REFRESH_FAILED);
463 * Determine whether or not the app is running in a window.
464 * @param {function(boolean):void} callback Callback to receive whether or not
465 * the current tab is running in windowed mode.
467 function isWindowed_(callback) {
468 /** @param {chrome.Window} win The current window. */
469 var windowCallback = function(win) {
470 callback(win.type == 'popup');
472 /** @param {chrome.Tab} tab The current tab. */
473 var tabCallback = function(tab) {
477 chrome.windows.get(tab.windowId, null, windowCallback);
481 chrome.tabs.getCurrent(tabCallback);
483 console.error('chome.tabs is not available.');
488 * Migrate settings in window.localStorage to chrome.storage.local so that
489 * users of older web-apps that used the former do not lose their settings.
491 function migrateLocalToChromeStorage_() {
492 // The OAuth2 class still uses window.localStorage, so don't migrate any of
494 var oauthSettings = [
495 'oauth2-refresh-token',
496 'oauth2-refresh-token-revokable',
497 'oauth2-access-token',
501 for (var setting in window.localStorage) {
502 if (oauthSettings.indexOf(setting) == -1) {
504 copy[setting] = window.localStorage.getItem(setting);
505 chrome.storage.local.set(copy);
506 window.localStorage.removeItem(setting);
512 * Generate a nonce, to be used as an xsrf protection token.
514 * @return {string} A URL-Safe Base64-encoded 128-bit random value. */
515 remoting.generateXsrfToken = function() {
516 var random = new Uint8Array(16);
517 window.crypto.getRandomValues(random);
518 var base64Token = window.btoa(String.fromCharCode.apply(null, random));
519 return base64Token.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');