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/** 11 * @param {Array.<remoting.HostSetupFlow.State>} sequence Sequence of 12 * steps for the flow. 13 * @constructor 14 */ 15remoting.HostSetupFlow = function(sequence) { 16 this.sequence_ = sequence; 17 this.currentStep_ = 0; 18 this.state_ = sequence[0]; 19 this.pin = ''; 20 this.consent = false; 21}; 22 23/** @enum {number} */ 24remoting.HostSetupFlow.State = { 25 NONE: 0, 26 27 // Dialog states. 28 ASK_PIN: 1, 29 30 // Prompts the user to install the host package. 31 INSTALL_HOST: 2, 32 33 // Processing states. 34 STARTING_HOST: 3, 35 UPDATING_PIN: 4, 36 STOPPING_HOST: 5, 37 38 // Done states. 39 HOST_STARTED: 6, 40 UPDATED_PIN: 7, 41 HOST_STOPPED: 8, 42 43 // Failure states. 44 REGISTRATION_FAILED: 9, 45 START_HOST_FAILED: 10, 46 UPDATE_PIN_FAILED: 11, 47 STOP_HOST_FAILED: 12 48}; 49 50/** @return {remoting.HostSetupFlow.State} Current state of the flow. */ 51remoting.HostSetupFlow.prototype.getState = function() { 52 return this.state_; 53}; 54 55remoting.HostSetupFlow.prototype.switchToNextStep = function() { 56 if (this.state_ == remoting.HostSetupFlow.State.NONE) { 57 return; 58 } 59 60 if (this.currentStep_ < this.sequence_.length - 1) { 61 this.currentStep_ += 1; 62 this.state_ = this.sequence_[this.currentStep_]; 63 } else { 64 this.state_ = remoting.HostSetupFlow.State.NONE; 65 } 66}; 67 68/** 69 * @param {remoting.Error} error 70 */ 71remoting.HostSetupFlow.prototype.switchToErrorState = function(error) { 72 if (error == remoting.Error.CANCELLED) { 73 // Stop the setup flow if user rejected one of the actions. 74 this.state_ = remoting.HostSetupFlow.State.NONE; 75 } else { 76 // Current step failed, so switch to corresponding error state. 77 if (this.state_ == remoting.HostSetupFlow.State.STARTING_HOST) { 78 if (error == remoting.Error.REGISTRATION_FAILED) { 79 this.state_ = remoting.HostSetupFlow.State.REGISTRATION_FAILED; 80 } else { 81 this.state_ = remoting.HostSetupFlow.State.START_HOST_FAILED; 82 } 83 } else if (this.state_ == remoting.HostSetupFlow.State.UPDATING_PIN) { 84 this.state_ = remoting.HostSetupFlow.State.UPDATE_PIN_FAILED; 85 } else if (this.state_ == remoting.HostSetupFlow.State.STOPPING_HOST) { 86 this.state_ = remoting.HostSetupFlow.State.STOP_HOST_FAILED; 87 } else { 88 // TODO(sergeyu): Add other error states and use them here. 89 this.state_ = remoting.HostSetupFlow.State.START_HOST_FAILED; 90 } 91 } 92}; 93 94/** 95 * @param {remoting.HostController} hostController The HostController 96 * responsible for the host daemon. 97 * @constructor 98 */ 99remoting.HostSetupDialog = function(hostController) { 100 this.hostController_ = hostController; 101 this.pinEntry_ = document.getElementById('daemon-pin-entry'); 102 this.pinConfirm_ = document.getElementById('daemon-pin-confirm'); 103 this.pinErrorDiv_ = document.getElementById('daemon-pin-error-div'); 104 this.pinErrorMessage_ = document.getElementById('daemon-pin-error-message'); 105 106 /** @type {remoting.HostSetupFlow} */ 107 this.flow_ = new remoting.HostSetupFlow([remoting.HostSetupFlow.State.NONE]); 108 109 /** @type {remoting.HostSetupDialog} */ 110 var that = this; 111 /** @param {Event} event The event. */ 112 var onPinSubmit = function(event) { 113 event.preventDefault(); 114 that.onPinSubmit_(); 115 }; 116 var onPinConfirmFocus = function() { 117 that.validatePin_(); 118 }; 119 120 var form = document.getElementById('ask-pin-form'); 121 form.addEventListener('submit', onPinSubmit, false); 122 /** @param {Event} event The event. */ 123 var onDaemonPinEntryKeyPress = function(event) { 124 if (event.which == 13) { 125 document.getElementById('daemon-pin-confirm').focus(); 126 event.preventDefault(); 127 } 128 }; 129 /** @param {Event} event A keypress event. */ 130 var noDigitsInPin = function(event) { 131 if (event.which == 13) { 132 return; // Otherwise the "submit" action can't be triggered by Enter. 133 } 134 if ((event.which >= 48) && (event.which <= 57)) { 135 return; 136 } 137 event.preventDefault(); 138 }; 139 this.pinEntry_.addEventListener('keypress', onDaemonPinEntryKeyPress, false); 140 this.pinEntry_.addEventListener('keypress', noDigitsInPin, false); 141 this.pinConfirm_.addEventListener('focus', onPinConfirmFocus, false); 142 this.pinConfirm_.addEventListener('keypress', noDigitsInPin, false); 143 144 this.usageStats_ = document.getElementById('usagestats-consent'); 145 this.usageStatsCheckbox_ = /** @type {HTMLInputElement} */ 146 document.getElementById('usagestats-consent-checkbox'); 147}; 148 149/** 150 * Show the dialog in order to get a PIN prior to starting the daemon. When the 151 * user clicks OK, the dialog shows a spinner until the daemon has started. 152 * 153 * @return {void} Nothing. 154 */ 155remoting.HostSetupDialog.prototype.showForStart = function() { 156 /** @type {remoting.HostSetupDialog} */ 157 var that = this; 158 159 /** 160 * @param {remoting.HostController.State} state 161 */ 162 var onState = function(state) { 163 // Although we don't need an access token in order to start the host, 164 // using callWithToken here ensures consistent error handling in the 165 // case where the refresh token is invalid. 166 remoting.identity.callWithToken( 167 that.showForStartWithToken_.bind(that, state), 168 remoting.showErrorMessage); 169 }; 170 171 this.hostController_.getLocalHostState(onState); 172}; 173 174/** 175 * @param {remoting.HostController.State} state The current state of the local 176 * host. 177 * @param {string} token The OAuth2 token. 178 * @private 179 */ 180remoting.HostSetupDialog.prototype.showForStartWithToken_ = 181 function(state, token) { 182 /** @type {remoting.HostSetupDialog} */ 183 var that = this; 184 185 /** 186 * @param {boolean} supported True if crash dump reporting is supported by 187 * the host. 188 * @param {boolean} allowed True if crash dump reporting is allowed. 189 * @param {boolean} set_by_policy True if crash dump reporting is controlled 190 * by policy. 191 */ 192 function onGetConsent(supported, allowed, set_by_policy) { 193 that.usageStats_.hidden = !supported; 194 that.usageStatsCheckbox_.checked = allowed; 195 that.usageStatsCheckbox_.disabled = set_by_policy; 196 } 197 198 /** @param {remoting.Error} error */ 199 function onError(error) { 200 console.error('Error getting consent status: ' + error); 201 } 202 203 this.usageStats_.hidden = false; 204 this.usageStatsCheckbox_.checked = false; 205 206 // Prevent user from ticking the box until the current consent status is 207 // known. 208 this.usageStatsCheckbox_.disabled = true; 209 210 this.hostController_.getConsent(onGetConsent, onError); 211 212 var flow = [ 213 remoting.HostSetupFlow.State.INSTALL_HOST, 214 remoting.HostSetupFlow.State.ASK_PIN, 215 remoting.HostSetupFlow.State.STARTING_HOST, 216 remoting.HostSetupFlow.State.HOST_STARTED]; 217 218 var installed = 219 state != remoting.HostController.State.NOT_INSTALLED && 220 state != remoting.HostController.State.INSTALLING; 221 222 // Skip the installation step when the host is already installed. 223 if (installed) { 224 flow.shift(); 225 } 226 227 this.startNewFlow_(flow); 228}; 229 230/** 231 * Show the dialog in order to change the PIN associated with a running daemon. 232 * 233 * @return {void} Nothing. 234 */ 235remoting.HostSetupDialog.prototype.showForPin = function() { 236 this.usageStats_.hidden = true; 237 this.startNewFlow_( 238 [remoting.HostSetupFlow.State.ASK_PIN, 239 remoting.HostSetupFlow.State.UPDATING_PIN, 240 remoting.HostSetupFlow.State.UPDATED_PIN]); 241}; 242 243/** 244 * Show the dialog in order to stop the daemon. 245 * 246 * @return {void} Nothing. 247 */ 248remoting.HostSetupDialog.prototype.showForStop = function() { 249 // TODO(sergeyu): Add another step to unregister the host, crubg.com/121146 . 250 this.startNewFlow_( 251 [remoting.HostSetupFlow.State.STOPPING_HOST, 252 remoting.HostSetupFlow.State.HOST_STOPPED]); 253}; 254 255/** 256 * @return {void} Nothing. 257 */ 258remoting.HostSetupDialog.prototype.hide = function() { 259 remoting.setMode(remoting.AppMode.HOME); 260}; 261 262/** 263 * Starts new flow with the specified sequence of steps. 264 * @param {Array.<remoting.HostSetupFlow.State>} sequence Sequence of steps. 265 * @private 266 */ 267remoting.HostSetupDialog.prototype.startNewFlow_ = function(sequence) { 268 this.flow_ = new remoting.HostSetupFlow(sequence); 269 this.pinEntry_.value = ''; 270 this.pinConfirm_.value = ''; 271 this.pinErrorDiv_.hidden = true; 272 this.updateState_(); 273}; 274 275/** 276 * Updates current UI mode according to the current state of the setup 277 * flow and start the action corresponding to the current step (if 278 * any). 279 * @private 280 */ 281remoting.HostSetupDialog.prototype.updateState_ = function() { 282 remoting.updateLocalHostState(); 283 284 /** @param {string} tag1 285 * @param {string=} opt_tag2 */ 286 function showDoneMessage(tag1, opt_tag2) { 287 var messageDiv = document.getElementById('host-setup-done-message'); 288 l10n.localizeElementFromTag(messageDiv, tag1); 289 messageDiv = document.getElementById('host-setup-done-message-2'); 290 if (opt_tag2) { 291 l10n.localizeElementFromTag(messageDiv, opt_tag2); 292 } else { 293 messageDiv.innerText = ''; 294 } 295 remoting.setMode(remoting.AppMode.HOST_SETUP_DONE); 296 } 297 /** @param {string} tag */ 298 function showErrorMessage(tag) { 299 var errorDiv = document.getElementById('host-setup-error-message'); 300 l10n.localizeElementFromTag(errorDiv, tag); 301 remoting.setMode(remoting.AppMode.HOST_SETUP_ERROR); 302 } 303 304 var state = this.flow_.getState(); 305 if (state == remoting.HostSetupFlow.State.NONE) { 306 this.hide(); 307 } else if (state == remoting.HostSetupFlow.State.ASK_PIN) { 308 remoting.setMode(remoting.AppMode.HOST_SETUP_ASK_PIN); 309 } else if (state == remoting.HostSetupFlow.State.INSTALL_HOST) { 310 this.installHost_(); 311 } else if (state == remoting.HostSetupFlow.State.STARTING_HOST) { 312 remoting.showSetupProcessingMessage(/*i18n-content*/'HOST_SETUP_STARTING'); 313 this.startHost_(); 314 } else if (state == remoting.HostSetupFlow.State.UPDATING_PIN) { 315 remoting.showSetupProcessingMessage( 316 /*i18n-content*/'HOST_SETUP_UPDATING_PIN'); 317 this.updatePin_(); 318 } else if (state == remoting.HostSetupFlow.State.STOPPING_HOST) { 319 remoting.showSetupProcessingMessage(/*i18n-content*/'HOST_SETUP_STOPPING'); 320 this.stopHost_(); 321 } else if (state == remoting.HostSetupFlow.State.HOST_STARTED) { 322 // TODO(jamiewalch): Only display the second string if the computer's power 323 // management settings indicate that it's necessary. 324 showDoneMessage(/*i18n-content*/'HOST_SETUP_STARTED', 325 /*i18n-content*/'HOST_SETUP_STARTED_DISABLE_SLEEP'); 326 } else if (state == remoting.HostSetupFlow.State.UPDATED_PIN) { 327 showDoneMessage(/*i18n-content*/'HOST_SETUP_UPDATED_PIN'); 328 } else if (state == remoting.HostSetupFlow.State.HOST_STOPPED) { 329 showDoneMessage(/*i18n-content*/'HOST_SETUP_STOPPED'); 330 } else if (state == remoting.HostSetupFlow.State.REGISTRATION_FAILED) { 331 showErrorMessage(/*i18n-content*/'ERROR_HOST_REGISTRATION_FAILED'); 332 } else if (state == remoting.HostSetupFlow.State.START_HOST_FAILED) { 333 showErrorMessage(/*i18n-content*/'HOST_SETUP_HOST_FAILED'); 334 } else if (state == remoting.HostSetupFlow.State.UPDATE_PIN_FAILED) { 335 showErrorMessage(/*i18n-content*/'HOST_SETUP_UPDATE_PIN_FAILED'); 336 } else if (state == remoting.HostSetupFlow.State.STOP_HOST_FAILED) { 337 showErrorMessage(/*i18n-content*/'HOST_SETUP_STOP_FAILED'); 338 } 339}; 340 341/** 342 * Shows the prompt that asks the user to install the host. 343 */ 344remoting.HostSetupDialog.prototype.installHost_ = function() { 345 /** @type {remoting.HostSetupDialog} */ 346 var that = this; 347 /** @type {remoting.HostSetupFlow} */ 348 var flow = this.flow_; 349 350 /** @param {remoting.Error} error */ 351 var onError = function(error) { 352 flow.switchToErrorState(error); 353 that.updateState_(); 354 }; 355 356 var onDone = function() { 357 that.hostController_.getLocalHostState(onHostState); 358 }; 359 360 /** @param {remoting.HostController.State} state */ 361 var onHostState = function(state) { 362 var installed = 363 state != remoting.HostController.State.NOT_INSTALLED && 364 state != remoting.HostController.State.INSTALLING; 365 366 if (installed) { 367 that.flow_.switchToNextStep(); 368 that.updateState_(); 369 } else { 370 // Prompt the user again if the host is not installed. 371 hostInstallDialog.tryAgain(); 372 } 373 }; 374 375 /** @type {remoting.HostInstallDialog} */ 376 var hostInstallDialog = new remoting.HostInstallDialog(); 377 hostInstallDialog.show(onDone, onError); 378} 379 380/** 381 * Registers and starts the host. 382 */ 383remoting.HostSetupDialog.prototype.startHost_ = function() { 384 /** @type {remoting.HostSetupDialog} */ 385 var that = this; 386 /** @type {remoting.HostSetupFlow} */ 387 var flow = this.flow_; 388 389 /** @return {boolean} */ 390 function isFlowActive() { 391 if (flow !== that.flow_ || 392 flow.getState() != remoting.HostSetupFlow.State.STARTING_HOST) { 393 console.error('Host setup was interrupted when starting the host'); 394 return false; 395 } 396 return true; 397 } 398 399 function onHostStarted() { 400 if (isFlowActive()) { 401 flow.switchToNextStep(); 402 that.updateState_(); 403 } 404 } 405 406 /** @param {remoting.Error} error */ 407 function onError(error) { 408 if (isFlowActive()) { 409 flow.switchToErrorState(error); 410 that.updateState_(); 411 } 412 } 413 414 this.hostController_.start(this.flow_.pin, this.flow_.consent, onHostStarted, 415 onError); 416}; 417 418remoting.HostSetupDialog.prototype.updatePin_ = function() { 419 /** @type {remoting.HostSetupDialog} */ 420 var that = this; 421 /** @type {remoting.HostSetupFlow} */ 422 var flow = this.flow_; 423 424 /** @return {boolean} */ 425 function isFlowActive() { 426 if (flow !== that.flow_ || 427 flow.getState() != remoting.HostSetupFlow.State.UPDATING_PIN) { 428 console.error('Host setup was interrupted when updating PIN'); 429 return false; 430 } 431 return true; 432 } 433 434 function onPinUpdated() { 435 if (isFlowActive()) { 436 flow.switchToNextStep(); 437 that.updateState_(); 438 } 439 } 440 441 /** @param {remoting.Error} error */ 442 function onError(error) { 443 if (isFlowActive()) { 444 flow.switchToErrorState(error); 445 that.updateState_(); 446 } 447 } 448 449 this.hostController_.updatePin(flow.pin, onPinUpdated, onError); 450}; 451 452/** 453 * Stops the host. 454 */ 455remoting.HostSetupDialog.prototype.stopHost_ = function() { 456 /** @type {remoting.HostSetupDialog} */ 457 var that = this; 458 /** @type {remoting.HostSetupFlow} */ 459 var flow = this.flow_; 460 461 /** @return {boolean} */ 462 function isFlowActive() { 463 if (flow !== that.flow_ || 464 flow.getState() != remoting.HostSetupFlow.State.STOPPING_HOST) { 465 console.error('Host setup was interrupted when stopping the host'); 466 return false; 467 } 468 return true; 469 } 470 471 function onHostStopped() { 472 if (isFlowActive()) { 473 flow.switchToNextStep(); 474 that.updateState_(); 475 } 476 } 477 478 /** @param {remoting.Error} error */ 479 function onError(error) { 480 if (isFlowActive()) { 481 flow.switchToErrorState(error); 482 that.updateState_(); 483 } 484 } 485 486 this.hostController_.stop(onHostStopped, onError); 487}; 488 489/** 490 * Validates the PIN and shows an error message if it's invalid. 491 * @return {boolean} true if the PIN is valid, false otherwise. 492 * @private 493 */ 494remoting.HostSetupDialog.prototype.validatePin_ = function() { 495 var pin = this.pinEntry_.value; 496 var pinIsValid = remoting.HostSetupDialog.validPin_(pin); 497 if (!pinIsValid) { 498 l10n.localizeElementFromTag( 499 this.pinErrorMessage_, /*i18n-content*/'INVALID_PIN'); 500 } 501 this.pinErrorDiv_.hidden = pinIsValid; 502 return pinIsValid; 503}; 504 505/** @private */ 506remoting.HostSetupDialog.prototype.onPinSubmit_ = function() { 507 if (this.flow_.getState() != remoting.HostSetupFlow.State.ASK_PIN) { 508 console.error('PIN submitted in an invalid state', this.flow_.getState()); 509 return; 510 } 511 var pin1 = this.pinEntry_.value; 512 var pin2 = this.pinConfirm_.value; 513 if (pin1 != pin2) { 514 l10n.localizeElementFromTag( 515 this.pinErrorMessage_, /*i18n-content*/'PINS_NOT_EQUAL'); 516 this.pinErrorDiv_.hidden = false; 517 this.prepareForPinEntry_(); 518 return; 519 } 520 if (!this.validatePin_()) { 521 this.prepareForPinEntry_(); 522 return; 523 } 524 this.flow_.pin = pin1; 525 this.flow_.consent = !this.usageStats_.hidden && 526 this.usageStatsCheckbox_.checked; 527 this.flow_.switchToNextStep(); 528 this.updateState_(); 529}; 530 531/** @private */ 532remoting.HostSetupDialog.prototype.prepareForPinEntry_ = function() { 533 this.pinEntry_.value = ''; 534 this.pinConfirm_.value = ''; 535 this.pinEntry_.focus(); 536}; 537 538/** 539 * Returns whether a PIN is valid. 540 * 541 * @private 542 * @param {string} pin A PIN. 543 * @return {boolean} Whether the PIN is valid. 544 */ 545remoting.HostSetupDialog.validPin_ = function(pin) { 546 if (pin.length < 6) { 547 return false; 548 } 549 for (var i = 0; i < pin.length; i++) { 550 var c = pin.charAt(i); 551 if ((c < '0') || (c > '9')) { 552 return false; 553 } 554 } 555 return true; 556}; 557 558/** @type {remoting.HostSetupDialog} */ 559remoting.hostSetupDialog = null; 560