Loosen up heuristics for detecting account creation forms.
[chromium-blink-merge.git] / remoting / webapp / client_screen.js
blob37f507d65afd616992f74efdc10ec1c73e3b8ec1
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.
5 /**
6 * @fileoverview
7 * Functions related to the 'client screen' for Chromoting.
8 */
10 'use strict';
12 /** @suppress {duplicate} */
13 var remoting = remoting || {};
15 /**
16 * @type {remoting.ClientSession} The client session object, set once the
17 * access code has been successfully verified.
19 remoting.clientSession = null;
21 /**
22 * @type {string} The normalized access code.
24 remoting.accessCode = '';
26 /**
27 * @type {string} The host's JID, returned by the server.
29 remoting.hostJid = '';
31 /**
32 * @type {string} For Me2Me connections, the id of the current host.
34 remoting.hostId = '';
36 /**
37 * @type {boolean} For Me2Me connections. Set to true if connection
38 * must be retried on failure.
40 remoting.retryIfOffline = false;
42 /**
43 * @type {string} The host's public key, returned by the server.
45 remoting.hostPublicKey = '';
47 /**
48 * @type {XMLHttpRequest} The XHR object corresponding to the current
49 * support-hosts request, if there is one outstanding.
50 * @private
52 remoting.supportHostsXhr_ = null;
54 /**
55 * @enum {string}
57 remoting.ConnectionType = {
58 It2Me: 'It2Me',
59 Me2Me: 'Me2Me'
62 /**
63 * @type {remoting.ConnectionType?}
65 remoting.currentConnectionType = null;
67 /**
68 * Entry point for the 'connect' functionality. This function defers to the
69 * WCS loader to call it back with an access token.
71 remoting.connectIt2Me = function() {
72 remoting.currentConnectionType = remoting.ConnectionType.It2Me;
73 remoting.WcsLoader.load(connectIt2MeWithAccessToken_,
74 remoting.showErrorMessage);
77 /**
78 * Cancel an incomplete connect operation.
80 * @return {void} Nothing.
82 remoting.cancelConnect = function() {
83 if (remoting.supportHostsXhr_) {
84 remoting.supportHostsXhr_.abort();
85 remoting.supportHostsXhr_ = null;
87 if (remoting.clientSession) {
88 remoting.clientSession.removePlugin();
89 remoting.clientSession = null;
91 if (remoting.currentConnectionType == remoting.ConnectionType.Me2Me) {
92 remoting.initDaemonUi();
93 } else {
94 remoting.setMode(remoting.AppMode.HOME);
95 document.getElementById('access-code-entry').value = '';
99 /**
100 * Toggle the scale-to-fit feature for the current client session.
102 * @return {void} Nothing.
104 remoting.toggleScaleToFit = function() {
105 remoting.clientSession.setScaleToFit(!remoting.clientSession.getScaleToFit());
109 * Update the remoting client layout in response to a resize event.
111 * @return {void} Nothing.
113 remoting.onResize = function() {
114 if (remoting.clientSession)
115 remoting.clientSession.onResize();
119 * Handle changes in the visibility of the window, for example by pausing video.
121 * @return {void} Nothing.
123 remoting.onVisibilityChanged = function() {
124 if (remoting.clientSession)
125 remoting.clientSession.pauseVideo(document.webkitHidden);
129 * Disconnect the remoting client.
131 * @return {void} Nothing.
133 remoting.disconnect = function() {
134 if (remoting.clientSession) {
135 remoting.clientSession.disconnect();
136 remoting.clientSession = null;
137 console.log('Disconnected.');
138 if (remoting.currentConnectionType == remoting.ConnectionType.It2Me) {
139 remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED_IT2ME);
140 } else {
141 remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED_ME2ME);
147 * Sends a Ctrl-Alt-Del sequence to the remoting client.
149 * @return {void} Nothing.
151 remoting.sendCtrlAltDel = function() {
152 if (remoting.clientSession) {
153 console.log('Sending Ctrl-Alt-Del.');
154 remoting.clientSession.sendCtrlAltDel();
159 * Sends a Print Screen keypress to the remoting client.
161 * @return {void} Nothing.
163 remoting.sendPrintScreen = function() {
164 if (remoting.clientSession) {
165 console.log('Sending Print Screen.');
166 remoting.clientSession.sendPrintScreen();
171 * If WCS was successfully loaded, proceed with the connection, otherwise
172 * report an error.
174 * @param {string?} token The OAuth2 access token, or null if an error occurred.
175 * @return {void} Nothing.
177 function connectIt2MeWithAccessToken_(token) {
178 if (token) {
179 var accessCode = document.getElementById('access-code-entry').value;
180 remoting.accessCode = normalizeAccessCode_(accessCode);
181 // At present, only 12-digit access codes are supported, of which the first
182 // 7 characters are the supportId.
183 var kSupportIdLen = 7;
184 var kHostSecretLen = 5;
185 var kAccessCodeLen = kSupportIdLen + kHostSecretLen;
186 if (remoting.accessCode.length != kAccessCodeLen) {
187 console.error('Bad access code length');
188 showConnectError_(remoting.Error.INVALID_ACCESS_CODE);
189 } else {
190 var supportId = remoting.accessCode.substring(0, kSupportIdLen);
191 remoting.setMode(remoting.AppMode.CLIENT_CONNECTING);
192 resolveSupportId(supportId, token);
194 } else {
195 showConnectError_(remoting.Error.AUTHENTICATION_FAILED);
200 * Callback function called when the state of the client plugin changes. The
201 * current state is available via the |state| member variable.
203 * @param {number} oldState The previous state of the plugin.
204 * @param {number} newState The current state of the plugin.
206 // TODO(jamiewalch): Make this pass both the current and old states to avoid
207 // race conditions.
208 function onClientStateChange_(oldState, newState) {
209 if (!remoting.clientSession) {
210 // If the connection has been cancelled, then we no longer have a reference
211 // to the session object and should ignore any state changes.
212 return;
215 // Clear the PIN on successful connection, or on error if we're not going to
216 // automatically retry.
217 var clearPin = false;
219 if (newState == remoting.ClientSession.State.CREATED) {
220 console.log('Created plugin');
222 } else if (newState == remoting.ClientSession.State.BAD_PLUGIN_VERSION) {
223 showConnectError_(remoting.Error.BAD_PLUGIN_VERSION);
225 } else if (newState == remoting.ClientSession.State.CONNECTING) {
226 console.log('Connecting as ' + remoting.oauth2.getCachedEmail());
228 } else if (newState == remoting.ClientSession.State.INITIALIZING) {
229 console.log('Initializing connection');
231 } else if (newState == remoting.ClientSession.State.CONNECTED) {
232 if (remoting.clientSession) {
233 clearPin = true;
234 setConnectionInterruptedButtonsText_();
235 remoting.setMode(remoting.AppMode.IN_SESSION);
236 remoting.toolbar.center();
237 remoting.toolbar.preview();
238 remoting.clipboard.startSession();
239 updateStatistics_();
242 } else if (newState == remoting.ClientSession.State.CLOSED) {
243 if (oldState == remoting.ClientSession.State.CONNECTED) {
244 remoting.clientSession.removePlugin();
245 remoting.clientSession = null;
246 console.log('Connection closed by host');
247 if (remoting.currentConnectionType == remoting.ConnectionType.It2Me) {
248 remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED_IT2ME);
249 } else {
250 remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED_ME2ME);
252 } else {
253 // The transition from CONNECTING to CLOSED state may happen
254 // only with older client plugins. Current version should go the
255 // FAILED state when connection fails.
256 showConnectError_(remoting.Error.INVALID_ACCESS_CODE);
259 } else if (newState == remoting.ClientSession.State.FAILED) {
260 console.error('Client plugin reported connection failed: ' +
261 remoting.clientSession.error);
262 clearPin = true;
263 if (remoting.clientSession.error ==
264 remoting.ClientSession.ConnectionError.HOST_IS_OFFLINE) {
265 clearPin = false;
266 retryConnectOrReportOffline_();
267 } else if (remoting.clientSession.error ==
268 remoting.ClientSession.ConnectionError.SESSION_REJECTED) {
269 showConnectError_(remoting.Error.INVALID_ACCESS_CODE);
270 } else if (remoting.clientSession.error ==
271 remoting.ClientSession.ConnectionError.INCOMPATIBLE_PROTOCOL) {
272 showConnectError_(remoting.Error.INCOMPATIBLE_PROTOCOL);
273 } else if (remoting.clientSession.error ==
274 remoting.ClientSession.ConnectionError.NETWORK_FAILURE) {
275 showConnectError_(remoting.Error.NETWORK_FAILURE);
276 } else if (remoting.clientSession.error ==
277 remoting.ClientSession.ConnectionError.HOST_OVERLOAD) {
278 showConnectError_(remoting.Error.HOST_OVERLOAD);
279 } else {
280 showConnectError_(remoting.Error.UNEXPECTED);
283 if (clearPin) {
284 document.getElementById('pin-entry').value = '';
287 } else {
288 console.error('Unexpected client plugin state: ' + newState);
289 // This should only happen if the web-app and client plugin get out of
290 // sync, and even then the version check should allow compatibility.
291 showConnectError_(remoting.Error.MISSING_PLUGIN);
296 * If we have a hostId to retry, try refreshing it and connecting again. If not,
297 * then show the 'host offline' error message.
299 * @return {void} Nothing.
301 function retryConnectOrReportOffline_() {
302 if (remoting.clientSession) {
303 remoting.clientSession.removePlugin();
304 remoting.clientSession = null;
306 if (remoting.hostId && remoting.retryIfOffline) {
307 console.warn('Connection failed. Retrying.');
308 /** @param {boolean} success True if the refresh was successful. */
309 var onDone = function(success) {
310 if (success) {
311 remoting.retryIfOffline = false;
312 remoting.connectMe2MeWithPin();
313 } else {
314 showConnectError_(remoting.Error.HOST_IS_OFFLINE);
317 remoting.hostList.refresh(onDone);
318 } else {
319 console.error('Connection failed. Not retrying.');
320 showConnectError_(remoting.Error.HOST_IS_OFFLINE);
325 * Create the client session object and initiate the connection.
327 * @return {void} Nothing.
329 function startSession_() {
330 console.log('Starting session...');
331 var accessCode = document.getElementById('access-code-entry');
332 accessCode.value = ''; // The code has been validated and won't work again.
333 remoting.clientSession =
334 new remoting.ClientSession(
335 remoting.hostJid, remoting.hostPublicKey,
336 remoting.accessCode, 'spake2_plain', '',
337 /** @type {string} */ (remoting.oauth2.getCachedEmail()),
338 remoting.ClientSession.Mode.IT2ME,
339 onClientStateChange_);
340 /** @param {string?} token The auth token. */
341 var createPluginAndConnect = function(token) {
342 if (token) {
343 remoting.clientSession.createPluginAndConnect(
344 document.getElementById('session-mode'),
345 token);
346 } else {
347 showConnectError_(remoting.Error.AUTHENTICATION_FAILED);
350 remoting.oauth2.callWithToken(createPluginAndConnect,
351 remoting.showErrorMessage);
355 * Show a client-side error message.
357 * @param {remoting.Error} errorTag The error to be localized and
358 * displayed.
359 * @return {void} Nothing.
361 function showConnectError_(errorTag) {
362 console.error('Connection failed: ' + errorTag);
363 var errorDiv = document.getElementById('connect-error-message');
364 l10n.localizeElementFromTag(errorDiv, /** @type {string} */ (errorTag));
365 remoting.accessCode = '';
366 if (remoting.clientSession) {
367 remoting.clientSession.disconnect();
368 remoting.clientSession = null;
370 if (remoting.currentConnectionType == remoting.ConnectionType.It2Me) {
371 remoting.setMode(remoting.AppMode.CLIENT_CONNECT_FAILED_IT2ME);
372 } else {
373 remoting.setMode(remoting.AppMode.CLIENT_CONNECT_FAILED_ME2ME);
378 * Set the text on the buttons shown under the error message so that they are
379 * easy to understand in the case where a successful connection failed, as
380 * opposed to the case where a connection never succeeded.
382 function setConnectionInterruptedButtonsText_() {
383 var button1 = document.getElementById('client-reconnect-button');
384 l10n.localizeElementFromTag(button1, /*i18n-content*/'RECONNECT');
385 button1.removeAttribute('autofocus');
386 var button2 = document.getElementById('client-finished-me2me-button');
387 l10n.localizeElementFromTag(button2, /*i18n-content*/'OK');
388 button2.setAttribute('autofocus', 'autofocus');
392 * Parse the response from the server to a request to resolve a support id.
394 * @param {XMLHttpRequest} xhr The XMLHttpRequest object.
395 * @return {void} Nothing.
397 function parseServerResponse_(xhr) {
398 remoting.supportHostsXhr_ = null;
399 console.log('parseServerResponse: xhr =', xhr);
400 if (xhr.status == 200) {
401 var host = /** @type {{data: {jabberId: string, publicKey: string}}} */
402 jsonParseSafe(xhr.responseText);
403 if (host && host.data && host.data.jabberId && host.data.publicKey) {
404 remoting.hostJid = host.data.jabberId;
405 remoting.hostPublicKey = host.data.publicKey;
406 var split = remoting.hostJid.split('/');
407 document.getElementById('connected-to').innerText = split[0];
408 startSession_();
409 return;
410 } else {
411 console.error('Invalid "support-hosts" response from server.');
414 var errorMsg = remoting.Error.UNEXPECTED;
415 if (xhr.status == 404) {
416 errorMsg = remoting.Error.INVALID_ACCESS_CODE;
417 } else if (xhr.status == 0) {
418 errorMsg = remoting.Error.NO_RESPONSE;
419 } else if (xhr.status == 503) {
420 errorMsg = remoting.Error.SERVICE_UNAVAILABLE;
421 } else {
422 console.error('The server responded: ' + xhr.responseText);
424 showConnectError_(errorMsg);
428 * Normalize the access code entered by the user.
430 * @param {string} accessCode The access code, as entered by the user.
431 * @return {string} The normalized form of the code (whitespace removed).
433 function normalizeAccessCode_(accessCode) {
434 // Trim whitespace.
435 // TODO(sergeyu): Do we need to do any other normalization here?
436 return accessCode.replace(/\s/g, '');
440 * Initiate a request to the server to resolve a support ID.
442 * @param {string} supportId The canonicalized support ID.
443 * @param {string} token The OAuth access token.
445 function resolveSupportId(supportId, token) {
446 var headers = {
447 'Authorization': 'OAuth ' + token
450 remoting.supportHostsXhr_ = remoting.xhr.get(
451 'https://www.googleapis.com/chromoting/v1/support-hosts/' +
452 encodeURIComponent(supportId),
453 parseServerResponse_,
455 headers);
459 * Timer callback to update the statistics panel.
461 function updateStatistics_() {
462 if (!remoting.clientSession ||
463 remoting.clientSession.state != remoting.ClientSession.State.CONNECTED) {
464 return;
466 var perfstats = remoting.clientSession.getPerfStats();
467 remoting.stats.update(perfstats);
468 remoting.clientSession.logStatistics(perfstats);
469 // Update the stats once per second.
470 window.setTimeout(updateStatistics_, 1000);
474 * Shows PIN entry screen.
476 * @param {string} hostId The unique id of the host.
477 * @param {boolean} retryIfOffline If true and the host can't be contacted,
478 * refresh the host list and try again. This allows bookmarked hosts to
479 * work even if they reregister with Talk and get a different Jid.
480 * @return {void} Nothing.
482 remoting.connectMe2Me = function(hostId, retryIfOffline) {
483 remoting.currentConnectionType = remoting.ConnectionType.Me2Me;
484 remoting.hostId = hostId;
485 remoting.retryIfOffline = retryIfOffline;
487 var host = remoting.hostList.getHostForId(remoting.hostId);
488 // If we're re-loading a tab for a host that has since been unregistered
489 // then the hostId may no longer resolve.
490 if (!host) {
491 showConnectError_(remoting.Error.HOST_IS_OFFLINE);
492 return;
494 var message = document.getElementById('pin-message');
495 l10n.localizeElement(message, host.hostName);
496 remoting.setMode(remoting.AppMode.CLIENT_PIN_PROMPT);
500 * Start a connection to the specified host, using the cached details
501 * and the PIN entered by the user.
503 * @return {void} Nothing.
505 remoting.connectMe2MeWithPin = function() {
506 console.log('Connecting to host...');
507 remoting.setMode(remoting.AppMode.CLIENT_CONNECTING);
509 var host = remoting.hostList.getHostForId(remoting.hostId);
510 // If the user clicked on a cached host that has since been removed then we
511 // won't find the hostId. If the user clicked on the entry for the local host
512 // immediately after having enabled it then we won't know it's JID or public
513 // key until the host heartbeats and we pull a fresh host list.
514 if (!host || !host.jabberId || !host.publicKey) {
515 retryConnectOrReportOffline_();
516 return;
518 remoting.hostJid = host.jabberId;
519 remoting.hostPublicKey = host.publicKey;
520 document.getElementById('connected-to').innerText = host.hostName;
521 document.title = chrome.i18n.getMessage('PRODUCT_NAME') + ': ' +
522 host.hostName;
524 remoting.WcsLoader.load(connectMe2MeWithAccessToken_,
525 remoting.showErrorMessage);
529 * Continue making the connection to a host, once WCS has initialized.
531 * @param {string?} token The OAuth2 access token, or null if an error occurred.
532 * @return {void} Nothing.
534 function connectMe2MeWithAccessToken_(token) {
535 if (token) {
536 /** @type {string} */
537 var pin = document.getElementById('pin-entry').value;
539 remoting.clientSession =
540 new remoting.ClientSession(
541 remoting.hostJid, remoting.hostPublicKey,
542 pin, 'spake2_hmac,spake2_plain', remoting.hostId,
543 /** @type {string} */ (remoting.oauth2.getCachedEmail()),
544 remoting.ClientSession.Mode.ME2ME, onClientStateChange_);
545 // Don't log errors for cached JIDs.
546 remoting.clientSession.logErrors(!remoting.retryIfOffline);
547 remoting.clientSession.createPluginAndConnect(
548 document.getElementById('session-mode'),
549 token);
550 } else {
551 showConnectError_(remoting.Error.AUTHENTICATION_FAILED);