chromium/chrome/browser/resources/chromeos/password_change/confirm_password_change.js

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview 'confirm-password-change' is a dialog so that the user can
 * either confirm their old password, or confirm their new password (twice),
 * or both, as part of an in-session password change.
 * The dialog shows a spinner while it tries to change the password. This
 * spinner is also shown immediately in the case we are trying to change the
 * password using scraped data, and if this fails the spinner is hidden and
 * the main confirm dialog is shown.
 */

// TODO(crbug.com/40613129): Add logic to show only some of the passwords
// fields if some of the passwords were successfully scraped.

import 'chrome://confirm-password-change/strings.m.js';
import 'chrome://resources/ash/common/cr_elements/cr_button/cr_button.js';
import 'chrome://resources/ash/common/cr_elements/cr_input/cr_input.js';
import 'chrome://resources/ash/common/cr_elements/cr_shared_vars.css.js';
import 'chrome://resources/polymer/v3_0/paper-spinner/paper-spinner-lite.js';

import {sendWithPromise} from 'chrome://resources/ash/common/cr.m.js';
import {I18nBehavior, I18nBehaviorInterface} from 'chrome://resources/ash/common/i18n_behavior.js';
import {loadTimeData} from 'chrome://resources/ash/common/load_time_data.m.js';
import {WebUIListenerBehavior} from 'chrome://resources/ash/common/web_ui_listener_behavior.js';
import {CrDialogElement} from 'chrome://resources/cr_elements/cr_dialog/cr_dialog.js';
import {mixinBehaviors, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {getTemplate} from './confirm_password_change.html.js';

/** @enum{number} */
const ValidationErrorType = {
  NO_ERROR: 0,
  MISSING_OLD_PASSWORD: 1,
  MISSING_NEW_PASSWORD: 2,
  MISSING_CONFIRM_NEW_PASSWORD: 3,
  PASSWORDS_DO_NOT_MATCH: 4,
  INCORRECT_OLD_PASSWORD: 5,
};

/**
 * @constructor
 * @extends {PolymerElement}
 * @implements {I18nBehaviorInterface}
 */
const ConfirmPasswordChangeElementBase =
    mixinBehaviors([I18nBehavior, WebUIListenerBehavior], PolymerElement);

/**
 * @typedef {{
 *   dialog: CrDialogElement,
 * }}
 */
ConfirmPasswordChangeElementBase.$;

/** @polymer */
class ConfirmPasswordChangeElement extends ConfirmPasswordChangeElementBase {
  static get is() {
    return 'confirm-password-change';
  }

  static get template() {
    return getTemplate();
  }

  static get properties() {
    return {
      /** @private {boolean} */
      showSpinner_:
          {type: Boolean, value: true, observer: 'onShowSpinnerChanged_'},

      /** @private {boolean} */
      showOldPasswordPrompt_: {type: Boolean, value: true},

      /** @private {string} */
      oldPassword_: {type: String, value: ''},

      /** @private {boolean} */
      showNewPasswordPrompt_: {type: Boolean, value: true},

      /** @private {string} */
      newPassword_: {type: String, value: ''},

      /** @private {string} */
      confirmNewPassword_: {type: String, value: ''},

      /** @private {!ValidationErrorType} */
      currentValidationError_: {
        type: Number,
        value: ValidationErrorType.NO_ERROR,
        observer: 'onErrorChanged_',
      },

      /** @private {string} */
      promptString_: {
        type: String,
        computed:
            'getPromptString_(showOldPasswordPrompt_, showNewPasswordPrompt_)',
      },

      /** @private {string} */
      errorString_:
          {type: String, computed: 'getErrorString_(currentValidationError_)'},

    };
  }

  static get observers() {
    return [
      'onShowPromptChanged_(showOldPasswordPrompt_, showNewPasswordPrompt_)',

    ];
  }


  /** @override */
  connectedCallback() {
    super.connectedCallback();

    this.addWebUIListener('incorrect-old-password', () => {
      this.onIncorrectOldPassword_();
    });

    this.getInitialState_();
  }

  /** @private */
  getInitialState_() {
    sendWithPromise('getInitialState').then((result) => {
      this.showOldPasswordPrompt_ = result.showOldPasswordPrompt;
      this.showNewPasswordPrompt_ = result.showNewPasswordPrompt;
      this.showSpinner_ = result.showSpinner;
    });
  }


  /** @private */
  onShowSpinnerChanged_() {
    // Dialog is on top, spinner is underneath, so showing dialog hides spinner.
    if (this.showSpinner_) {
      this.$.dialog.close();
    } else {
      this.$.dialog.showModal();
    }
  }

  /** @private */
  onShowPromptChanged_() {
    const suffix = (this.showOldPasswordPrompt_ ? 'Old' : '') +
        (this.showNewPasswordPrompt_ ? 'New' : '');
    const width = loadTimeData.getInteger('width' + suffix);
    const height = loadTimeData.getInteger('height' + suffix);

    window.resizeTo(width, height);
  }

  /** @private */
  onErrorChanged_() {
    if (this.currentValidationError_ !== ValidationErrorType.NO_ERROR) {
      this.showSpinner_ = false;
    }
  }

  /** @private */
  onSaveTap_() {
    this.currentValidationError_ = this.findFirstError_();
    if (this.currentValidationError_ === ValidationErrorType.NO_ERROR) {
      chrome.send('changePassword', [this.oldPassword_, this.newPassword_]);
      this.showSpinner_ = true;
    }
  }

  /** @private */
  onIncorrectOldPassword_() {
    if (this.showOldPasswordPrompt_) {
      // User manually typed in the incorrect old password. Show the user an
      // incorrect password error and hide the spinner so they can try again.
      this.currentValidationError_ = ValidationErrorType.INCORRECT_OLD_PASSWORD;
    } else {
      // Until now we weren't showing the old password prompt, since we had
      // scraped the old password. But the password we scraped seems to be the
      // wrong one. So, start again, but this time ask for the old password too.
      this.showOldPasswordPrompt_ = true;
      this.currentValidationError_ = ValidationErrorType.MISSING_OLD_PASSWORD;
    }
  }

  /**
   * @return {!ValidationErrorType}
   * @private
   */
  findFirstError_() {
    if (this.showOldPasswordPrompt_) {
      if (!this.oldPassword_) {
        return ValidationErrorType.MISSING_OLD_PASSWORD;
      }
    }
    if (this.showNewPasswordPrompt_) {
      if (!this.newPassword_) {
        return ValidationErrorType.MISSING_NEW_PASSWORD;
      }
      if (!this.confirmNewPassword_) {
        return ValidationErrorType.MISSING_CONFIRM_NEW_PASSWORD;
      }
      if (this.newPassword_ !== this.confirmNewPassword_) {
        return ValidationErrorType.PASSWORDS_DO_NOT_MATCH;
      }
    }
    return ValidationErrorType.NO_ERROR;
  }

  /**
   * @return {boolean}
   * @private
   */
  invalidOldPassword_() {
    const err = this.currentValidationError_;
    return err === ValidationErrorType.MISSING_OLD_PASSWORD ||
        err === ValidationErrorType.INCORRECT_OLD_PASSWORD;
  }

  /**
   * @return {boolean}
   * @private
   */
  invalidNewPassword_() {
    return this.currentValidationError_ ===
        ValidationErrorType.MISSING_NEW_PASSWORD;
  }

  /**
   * @return {boolean}
   * @private
   */
  invalidConfirmNewPassword_() {
    const err = this.currentValidationError_;
    return err === ValidationErrorType.MISSING_CONFIRM_NEW_PASSWORD ||
        err === ValidationErrorType.PASSWORDS_DO_NOT_MATCH;
  }

  /**
   * @return {string}
   * @private
   */
  getPromptString_() {
    if (this.showOldPasswordPrompt_ && this.showNewPasswordPrompt_) {
      return this.i18n('bothPasswordsPrompt');
    }
    if (this.showOldPasswordPrompt_) {
      return this.i18n('oldPasswordPrompt');
    }
    if (this.showNewPasswordPrompt_) {
      return this.i18n('newPasswordPrompt');
    }
    return '';
  }

  /**
   * @return {string}
   * @private
   */
  getErrorString_() {
    switch (this.currentValidationError_) {
      case ValidationErrorType.INCORRECT_OLD_PASSWORD:
        return this.i18n('incorrectPassword');
      case ValidationErrorType.PASSWORDS_DO_NOT_MATCH:
        return this.i18n('matchError');
      default:
        return '';
    }
  }
}

customElements.define(
    ConfirmPasswordChangeElement.is, ConfirmPasswordChangeElement);