Home | History | Annotate | Download | only in webapp
      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} */
      8 var remoting = remoting || {};
      9 
     10 /**
     11  * @param {Array.<remoting.HostSetupFlow.State>} sequence Sequence of
     12  *     steps for the flow.
     13  * @constructor
     14  */
     15 remoting.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} */
     24 remoting.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. */
     51 remoting.HostSetupFlow.prototype.getState = function() {
     52   return this.state_;
     53 };
     54 
     55 remoting.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  */
     71 remoting.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  */
     99 remoting.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  */
    155 remoting.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  */
    180 remoting.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  */
    235 remoting.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  */
    248 remoting.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  */
    258 remoting.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  */
    267 remoting.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  */
    281 remoting.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  */
    344 remoting.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  */
    383 remoting.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 
    418 remoting.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  */
    455 remoting.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  */
    494 remoting.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 */
    506 remoting.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 */
    532 remoting.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  */
    545 remoting.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} */
    559 remoting.hostSetupDialog = null;
    560