chromium/ui/file_manager/file_manager/widgets/xf_password_dialog.ts

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


import type {CrButtonElement} from 'chrome://resources/ash/common/cr_elements/cr_button/cr_button.js';
import type {CrDialogElement} from 'chrome://resources/ash/common/cr_elements/cr_dialog/cr_dialog.js';
import type {CrInputElement} from 'chrome://resources/ash/common/cr_elements/cr_input/cr_input.js';
import {loadTimeData} from 'chrome://resources/ash/common/load_time_data.m.js';

import {AsyncQueue} from '../common/js/async_util.js';

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


/**
 * The custom element tag name.
 */
export const TAG_NAME = 'xf-password-dialog';

/**
 * Exception thrown when user cancels the password dialog box.
 */
export const USER_CANCELLED: Error = new Error('Cancelled by user');

/**
 * Dialog to request user to enter password. Uses the askForPassword() which
 * resolves with either the password or rejected with USER_CANCELLED.
 */
export class XfPasswordDialog extends HTMLElement {
  /**
   * Mutex used to serialize modal dialogs and error notifications.
   */
  private mutex_: AsyncQueue = new AsyncQueue();

  /**
   * Controls whether the user is validating the password (Unlock button or
   * Enter key) or cancelling the dialog (Cancel button or Escape key).
   */
  private success_: boolean = false;

  /**
   * Return input password using the resolve method of a Promise.
   */
  private resolve_: ((password: string) => void)|null = null;

  /**
   * Return password prompt error using the reject method of a Promise.
   */
  private reject_: ((error: Error) => void)|null = null;

  /**
   * Password dialog.
   * TODO(crbug.com/40858292): This type should be CrDialogElement, and
   * an import of that type from cr_dialog.js should be added to this file.
   */
  private dialog_: CrDialogElement;

  /**
   * Input field for password.
   */
  private input_: CrInputElement;

  constructor() {
    super();

    const template = document.createElement('template');
    template.innerHTML = getTemplate() as unknown as string;
    const fragment = template.content.cloneNode(true);
    this.attachShadow({mode: 'open'}).appendChild(fragment);

    this.dialog_ = this.shadowRoot!.querySelector('#password-dialog')!;
    this.dialog_.consumeKeydownEvent = true;
    this.input_ = this.shadowRoot!.querySelector('#input')!;
    this.input_.errorMessage =
        loadTimeData.getString('PASSWORD_DIALOG_INVALID');
  }

  /**
   * Called when this element is attached to the DOM.
   */
  connectedCallback() {
    const cancelButton =
        this.shadowRoot!.querySelector<CrButtonElement>('#cancel')!;
    cancelButton.onclick = () => this.cancel_();

    const unlockButton =
        this.shadowRoot!.querySelector<CrButtonElement>('#unlock')!;
    unlockButton.onclick = () => this.unlock_();

    this.dialog_.addEventListener('close', () => this.onClose_());
  }

  /**
   * Asks the user for a password to open the given file.
   * @param filename Name of the file to open.
   * @param password Previously entered password. If not null, it
   *     indicates that an invalid password was previously tried.
   * @return Password provided by the user. The returned
   *     promise is rejected with USER_CANCELLED if the user
   *     presses Cancel.
   */
  async askForPassword(filename: string, password: string|null = null):
      Promise<string> {
    const mutexUnlock = await this.mutex_.lock();
    try {
      return await new Promise((resolve, reject) => {
        this.success_ = false;
        this.resolve_ = resolve;
        this.reject_ = reject;
        if (password !== null) {
          this.input_.value = password;
          // An invalid password has previously been entered for this file.
          // Display an 'invalid password' error message.
          this.input_.invalid = true;
        } else {
          this.input_.invalid = false;
        }
        this.showModal_(filename);
        this.input_.inputElement.select();
      });
    } finally {
      mutexUnlock();
    }
  }

  /**
   * Shows the password prompt represented by |filename|.
   * @param filename
   */
  private showModal_(filename: string) {
    this.dialog_.querySelector<HTMLElement>('#name')!.innerText = filename;
    this.dialog_.showModal();
  }

  /**
   * Triggers a 'Cancelled by user' error.
   */
  private cancel_() {
    this.dialog_.close();
  }

  /**
   * Sends user input password.
   */
  private unlock_() {
    this.dialog_.close();
    this.success_ = true;
  }

  /**
   * Resolves the promise when the dialog is closed.
   * This can be triggered by the buttons, Esc key or anything that closes the
   * dialog.
   */
  private onClose_() {
    if (this.success_) {
      this.resolve_!(this.input_.value);
    } else {
      this.reject_!(USER_CANCELLED);
    }
    this.input_.value = '';
  }
}

declare global {
  interface HTMLElementTagNameMap {
    [TAG_NAME]: XfPasswordDialog;
  }
}

customElements.define(TAG_NAME, XfPasswordDialog);