tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

password-validation-inputs.mjs (8048B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 import { html } from "chrome://global/content/vendor/lit.all.mjs";
      6 import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
      7 
      8 // eslint-disable-next-line import/no-unassigned-import
      9 import "chrome://browser/content/backup/password-rules-tooltip.mjs";
     10 
     11 /**
     12 * The widget for enabling password protection if the backup is not yet
     13 * encrypted.
     14 */
     15 export default class PasswordValidationInputs extends MozLitElement {
     16  static properties = {
     17    _hasEmail: { type: Boolean, state: true },
     18    _passwordsMatch: { type: Boolean, state: true },
     19    _passwordsValid: { type: Boolean, state: true },
     20    _tooShort: { type: Boolean, state: true },
     21    createPasswordLabelL10nId: {
     22      type: String,
     23      reflect: true,
     24      attribute: "create-password-label-l10n-id",
     25    },
     26    embeddedFxBackupOptIn: {
     27      type: Boolean,
     28      reflect: true,
     29      attribute: "embedded-fx-backup-opt-in",
     30    },
     31  };
     32 
     33  static get queries() {
     34    return {
     35      formEl: "#password-inputs-form",
     36      inputNewPasswordEl: "#new-password-input",
     37      inputRepeatPasswordEl: "#repeat-password-input",
     38      passwordRulesEl: "#password-rules",
     39      repeatPasswordErrorEl: "#repeat-password-error",
     40    };
     41  }
     42 
     43  constructor() {
     44    super();
     45    this._tooShort = true;
     46    this._hasEmail = false;
     47    this._passwordsMatch = false;
     48    this._passwordsValid = false;
     49  }
     50 
     51  connectedCallback() {
     52    super.connectedCallback();
     53    this._onKeydown = e => {
     54      if (e.key === "Escape" && this.passwordRulesEl.open) {
     55        this.passwordRulesEl.hide();
     56        e.stopPropagation();
     57        e.preventDefault();
     58      }
     59    };
     60    document.addEventListener("keydown", this._onKeydown, true);
     61  }
     62  disconnectedCallback() {
     63    document.removeEventListener("keydown", this._onKeydown, true);
     64    super.disconnectedCallback();
     65  }
     66 
     67  setInputValidity(input, isValid, describedById = null) {
     68    input.setAttribute("aria-invalid", isValid ? "false" : "true");
     69    if (describedById) {
     70      input.setAttribute("aria-describedby", describedById);
     71    } else {
     72      input.removeAttribute("aria-describedby");
     73    }
     74  }
     75 
     76  reset() {
     77    this.formEl?.reset();
     78    if (this.inputNewPasswordEl) {
     79      this.inputNewPasswordEl.revealPassword = false;
     80      this.setInputValidity(this.inputNewPasswordEl, true);
     81    }
     82    if (this.inputRepeatPasswordEl) {
     83      this.inputRepeatPasswordEl.revealPassword = false;
     84      this.setInputValidity(this.inputRepeatPasswordEl, true);
     85    }
     86    this._hasEmail = false;
     87    this._tooShort = true;
     88    this._passwordsMatch = false;
     89    this._passwordsValid = false;
     90    this.passwordRulesEl.hide();
     91  }
     92 
     93  handleFocusNewPassword() {
     94    this.passwordRulesEl.show();
     95  }
     96 
     97  handleBlurNewPassword(event) {
     98    if (event.target.checkValidity()) {
     99      this.passwordRulesEl.hide();
    100    }
    101  }
    102 
    103  handleChangeNewPassword() {
    104    this.updatePasswordValidity();
    105  }
    106 
    107  handleChangeRepeatPassword() {
    108    this.updatePasswordValidity();
    109  }
    110 
    111  updatePasswordValidity() {
    112    const emailRegex = /^[\w!#$%&'*+/=?^`{|}~.-]+@[A-Z0-9-]+\.[A-Z0-9.-]+$/i;
    113    const l10n = new Localization(["browser/backupSettings.ftl"], true);
    114 
    115    this._hasEmail = emailRegex.test(this.inputNewPasswordEl.value);
    116    if (this._hasEmail) {
    117      const invalid_password_email_l10n_message = l10n.formatValueSync(
    118        "password-validity-has-email"
    119      );
    120 
    121      this.inputNewPasswordEl.setCustomValidity(
    122        invalid_password_email_l10n_message
    123      );
    124    } else {
    125      this.inputNewPasswordEl.setCustomValidity("");
    126    }
    127 
    128    const newPassValidity = this.inputNewPasswordEl.validity;
    129    this._tooShort = newPassValidity?.valueMissing || newPassValidity?.tooShort;
    130 
    131    const newInvalid = !newPassValidity?.valid;
    132    this.setInputValidity(
    133      this.inputNewPasswordEl,
    134      !newInvalid,
    135      "password-rules-tooltip"
    136    );
    137 
    138    this._passwordsMatch =
    139      this.inputNewPasswordEl.value == this.inputRepeatPasswordEl.value;
    140 
    141    if (!this._passwordsMatch) {
    142      this.inputRepeatPasswordEl.setCustomValidity(
    143        l10n.formatValueSync("password-validity-do-not-match")
    144      );
    145      this.setInputValidity(
    146        this.inputRepeatPasswordEl,
    147        false,
    148        "repeat-password-error"
    149      );
    150      document.l10n.setAttributes(
    151        this.repeatPasswordErrorEl,
    152        "password-validity-do-not-match"
    153      );
    154    } else {
    155      this.inputRepeatPasswordEl.setCustomValidity("");
    156      this.setInputValidity(this.inputRepeatPasswordEl, true);
    157    }
    158 
    159    const repeatPassValidity = this.inputRepeatPasswordEl.validity;
    160    this._passwordsValid =
    161      newPassValidity?.valid &&
    162      repeatPassValidity?.valid &&
    163      this._passwordsMatch;
    164  }
    165 
    166  /**
    167   * Dispatches a custom event whenever validity changes.
    168   *
    169   * @param {Map<string, any>} changedProperties a Map of recently changed properties and their new values
    170   */
    171  updated(changedProperties) {
    172    if (!changedProperties.has("_passwordsValid")) {
    173      return;
    174    }
    175 
    176    if (this._passwordsValid) {
    177      this.dispatchEvent(
    178        new CustomEvent("ValidPasswordsDetected", {
    179          bubbles: true,
    180          composed: true,
    181          detail: {
    182            password: this.inputNewPasswordEl.value,
    183          },
    184        })
    185      );
    186    } else {
    187      this.dispatchEvent(
    188        new CustomEvent("InvalidPasswordsDetected", {
    189          bubbles: true,
    190          composed: true,
    191        })
    192      );
    193    }
    194  }
    195 
    196  contentTemplate() {
    197    return html`
    198      <div id="password-inputs-wrapper" aria-live="polite">
    199        <form id="password-inputs-form">
    200          <!--TODO: (bug 1909983) change first input field label for the "change-password" dialog-->
    201          <label id="new-password-label" for="new-password-input">
    202            <div id="new-password-label-wrapper-span-input">
    203              <span
    204                id="new-password-span"
    205                data-l10n-id=${this.createPasswordLabelL10nId ||
    206                "enable-backup-encryption-create-password-label"}
    207              ></span>
    208              <input
    209                type="password"
    210                id="new-password-input"
    211                minlength="8"
    212                required
    213                aria-describedby="password-rules-tooltip"
    214                @input=${this.handleChangeNewPassword}
    215                @blur=${this.handleBlurNewPassword}
    216                @mouseenter=${this.handleFocusNewPassword}
    217                @focus=${this.handleFocusNewPassword}
    218              />
    219              <!--TODO: (bug 1909984) improve how we read out the first input field for screen readers-->
    220            </div>
    221          </label>
    222          <!--TODO: (bug 1909984) look into how the tooltip vs dialog behaves when pressing the ESC key-->
    223          <password-rules-tooltip
    224            id="password-rules"
    225            role="tooltip"
    226            .hasEmail=${this._hasEmail}
    227            .tooShort=${this._tooShort}
    228            ?embedded-fx-backup-opt-in=${this.embeddedFxBackupOptIn}
    229          ></password-rules-tooltip>
    230          <label id="repeat-password-label" for="repeat-password-input">
    231            <span
    232              id="repeat-password-span"
    233              data-l10n-id="enable-backup-encryption-repeat-password-label"
    234            ></span>
    235            <input
    236              type="password"
    237              id="repeat-password-input"
    238              required
    239              @input=${this.handleChangeRepeatPassword}
    240            />
    241            <span
    242              id="repeat-password-error"
    243              role="alert"
    244              class="field-error"
    245            ></span>
    246          </label>
    247        </form>
    248      </div>
    249    `;
    250  }
    251 
    252  render() {
    253    return html`
    254      <link
    255        rel="stylesheet"
    256        href="chrome://browser/content/backup/password-validation-inputs.css"
    257      />
    258      ${this.contentTemplate()}
    259    `;
    260  }
    261 }
    262 
    263 customElements.define("password-validation-inputs", PasswordValidationInputs);