tor-browser

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

restore-from-backup.mjs (18938B)


      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 {
      6  html,
      7  ifDefined,
      8  styleMap,
      9 } from "chrome://global/content/vendor/lit.all.mjs";
     10 import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
     11 import { ERRORS } from "chrome://browser/content/backup/backup-constants.mjs";
     12 import { getErrorL10nId } from "chrome://browser/content/backup/backup-errors.mjs";
     13 
     14 // eslint-disable-next-line import/no-unassigned-import
     15 import "chrome://global/content/elements/moz-message-bar.mjs";
     16 
     17 /**
     18 * The widget for allowing users to select and restore from a
     19 * a backup file.
     20 */
     21 export default class RestoreFromBackup extends MozLitElement {
     22  #placeholderFileIconURL = "chrome://global/skin/icons/page-portrait.svg";
     23  /**
     24   * When the user clicks the button to choose a backup file to restore, we send
     25   * a message to the `BackupService` process asking it to read that file.
     26   * When we do this, we set this property to be a promise, which we resolve
     27   * when the file reading is complete.
     28   */
     29  #backupFileReadPromise = null;
     30 
     31  /**
     32   * Resolves when BackupUIParent sends state for the first time.
     33   */
     34  get initializedPromise() {
     35    return this.#initializedResolvers.promise;
     36  }
     37  #initializedResolvers = Promise.withResolvers();
     38 
     39  static properties = {
     40    _fileIconURL: { type: String },
     41    aboutWelcomeEmbedded: { type: Boolean },
     42    backupServiceState: { type: Object },
     43  };
     44 
     45  static get queries() {
     46    return {
     47      filePicker: "#backup-filepicker-input",
     48      passwordInput: "#backup-password-input",
     49      cancelButtonEl: "#restore-from-backup-cancel-button",
     50      confirmButtonEl: "#restore-from-backup-confirm-button",
     51      chooseButtonEl: "#backup-filepicker-button",
     52      errorMessageEl: "#restore-from-backup-error",
     53    };
     54  }
     55 
     56  get isIncorrectPassword() {
     57    return this.backupServiceState?.recoveryErrorCode === ERRORS.UNAUTHORIZED;
     58  }
     59 
     60  constructor() {
     61    super();
     62    this._fileIconURL = "";
     63    // Set the default state
     64    this.backupServiceState = {
     65      backupDirPath: "",
     66      backupFileToRestore: null,
     67      backupFileInfo: null,
     68      defaultParent: {
     69        fileName: "",
     70        path: "",
     71        iconURL: "",
     72      },
     73      encryptionEnabled: false,
     74      scheduledBackupsEnabled: false,
     75      lastBackupDate: null,
     76      lastBackupFileName: "",
     77      supportBaseLink: "https://support.mozilla.org/",
     78      backupInProgress: false,
     79      recoveryInProgress: false,
     80      recoveryErrorCode: ERRORS.NONE,
     81    };
     82  }
     83 
     84  /**
     85   * Dispatches the BackupUI:InitWidget custom event upon being attached to the
     86   * DOM, which registers with BackupUIChild for BackupService state updates.
     87   */
     88  connectedCallback() {
     89    super.connectedCallback();
     90    this.dispatchEvent(
     91      new CustomEvent("BackupUI:InitWidget", { bubbles: true })
     92    );
     93 
     94    // If we have a backup file, but not the associated info, fetch the info
     95    this.maybeGetBackupFileInfo();
     96 
     97    this.addEventListener("BackupUI:SelectNewFilepickerPath", this);
     98    this.addEventListener("BackupUI:StateWasUpdated", this);
     99 
    100    // Resize the textarea when the window is resized
    101    if (this.aboutWelcomeEmbedded) {
    102      this._handleWindowResize = () => this.resizeTextarea();
    103      window.addEventListener("resize", this._handleWindowResize);
    104    }
    105  }
    106 
    107  maybeGetBackupFileInfo() {
    108    if (
    109      this.backupServiceState?.backupFileToRestore &&
    110      !this.backupServiceState?.backupFileInfo
    111    ) {
    112      this.getBackupFileInfo();
    113    }
    114  }
    115 
    116  disconnectedCallback() {
    117    super.disconnectedCallback();
    118    if (this._handleWindowResize) {
    119      window.removeEventListener("resize", this._handleWindowResize);
    120      this._handleWindowResize = null;
    121    }
    122  }
    123 
    124  updated(changedProperties) {
    125    super.updated(changedProperties);
    126 
    127    // Resize the textarea. This only runs once on initial render,
    128    // and once each time one of our reactive properties is changed.
    129    if (this.aboutWelcomeEmbedded) {
    130      this.resizeTextarea();
    131    }
    132 
    133    if (changedProperties.has("backupServiceState")) {
    134      // If we got a recovery error, recoveryInProgress should be false
    135      const inProgress =
    136        this.backupServiceState.recoveryInProgress &&
    137        !this.backupServiceState.recoveryErrorCode;
    138 
    139      this.dispatchEvent(
    140        new CustomEvent("BackupUI:RecoveryProgress", {
    141          bubbles: true,
    142          composed: true,
    143          detail: { recoveryInProgress: inProgress },
    144        })
    145      );
    146 
    147      // It's possible that backupFileToRestore got updated and we need to
    148      // refetch the fileInfo
    149      this.maybeGetBackupFileInfo();
    150    }
    151  }
    152 
    153  handleEvent(event) {
    154    if (event.type == "BackupUI:SelectNewFilepickerPath") {
    155      let { path, iconURL } = event.detail;
    156      this._fileIconURL = iconURL;
    157 
    158      this.#backupFileReadPromise = Promise.withResolvers();
    159      this.#backupFileReadPromise.promise.then(() => {
    160        const payload = {
    161          location: this.backupServiceState?.backupFileCoarseLocation,
    162          valid: this.backupServiceState?.recoveryErrorCode == ERRORS.NONE,
    163        };
    164        if (payload.valid) {
    165          payload.backup_timestamp = new Date(
    166            this.backupServiceState?.backupFileInfo?.date || 0
    167          ).getTime();
    168          payload.restore_id = this.backupServiceState?.restoreID;
    169          payload.encryption =
    170            this.backupServiceState?.backupFileInfo?.isEncrypted;
    171          payload.app_name = this.backupServiceState?.backupFileInfo?.appName;
    172          payload.version = this.backupServiceState?.backupFileInfo?.appVersion;
    173          payload.build_id = this.backupServiceState?.backupFileInfo?.buildID;
    174          payload.os_name = this.backupServiceState?.backupFileInfo?.osName;
    175          payload.os_version =
    176            this.backupServiceState?.backupFileInfo?.osVersion;
    177          payload.telemetry_enabled =
    178            this.backupServiceState?.backupFileInfo?.healthTelemetryEnabled;
    179        }
    180        Glean.browserBackup.restoreFileChosen.record(payload);
    181        Services.obs.notifyObservers(null, "browser-backup-glean-sent");
    182      });
    183 
    184      this.getBackupFileInfo(path);
    185    } else if (event.type == "BackupUI:StateWasUpdated") {
    186      this.#initializedResolvers.resolve();
    187      if (this.#backupFileReadPromise) {
    188        this.#backupFileReadPromise.resolve();
    189        this.#backupFileReadPromise = null;
    190      }
    191    }
    192  }
    193 
    194  handleChooseBackupFile() {
    195    this.dispatchEvent(
    196      new CustomEvent("BackupUI:ShowFilepicker", {
    197        bubbles: true,
    198        composed: true,
    199        detail: {
    200          win: window.browsingContext,
    201          filter: "filterHTML",
    202          existingBackupPath: this.backupServiceState?.backupFileToRestore,
    203        },
    204      })
    205    );
    206  }
    207 
    208  getBackupFileInfo(pathToFile = null) {
    209    let backupFile = pathToFile || this.backupServiceState?.backupFileToRestore;
    210    if (!backupFile) {
    211      return;
    212    }
    213    this.dispatchEvent(
    214      new CustomEvent("BackupUI:GetBackupFileInfo", {
    215        bubbles: true,
    216        composed: true,
    217        detail: {
    218          backupFile,
    219        },
    220      })
    221    );
    222  }
    223 
    224  handleCancel() {
    225    this.dispatchEvent(
    226      new CustomEvent("dialogCancel", {
    227        bubbles: true,
    228        composed: true,
    229      })
    230    );
    231  }
    232 
    233  handleConfirm() {
    234    let backupFile = this.backupServiceState?.backupFileToRestore;
    235    if (!backupFile || this.backupServiceState?.recoveryInProgress) {
    236      return;
    237    }
    238    let backupPassword = this.passwordInput?.value;
    239    this.dispatchEvent(
    240      new CustomEvent("BackupUI:RestoreFromBackupFile", {
    241        bubbles: true,
    242        composed: true,
    243        detail: {
    244          backupFile,
    245          backupPassword,
    246        },
    247      })
    248    );
    249  }
    250 
    251  handleTextareaResize() {
    252    this.resizeTextarea();
    253  }
    254 
    255  /**
    256   * Resizes the textarea to adjust to the size of the content within
    257   */
    258  resizeTextarea() {
    259    const target = this.filePicker;
    260    if (!target) {
    261      return;
    262    }
    263 
    264    const hasValue = target.value && !!target.value.trim().length;
    265 
    266    target.style.height = "auto";
    267    if (hasValue) {
    268      target.style.height = target.scrollHeight + "px";
    269    }
    270  }
    271 
    272  /**
    273   * Constructs a support URL with UTM parameters for use
    274   * when embedded in about:welcome
    275   *
    276   * @param {string} supportPage - The support page slug
    277   * @returns {string} The full support URL including UTM params
    278   */
    279 
    280  getSupportURLWithUTM(supportPage) {
    281    let supportURL = new URL(
    282      supportPage,
    283      this.backupServiceState.supportBaseLink
    284    );
    285    supportURL.searchParams.set("utm_medium", "firefox-desktop");
    286    supportURL.searchParams.set("utm_source", "npo");
    287    supportURL.searchParams.set("utm_campaign", "fx-backup-restore");
    288    supportURL.searchParams.set("utm_content", "restore-error");
    289    return supportURL.href;
    290  }
    291 
    292  /**
    293   * Returns a support link anchor element, either with UTM params for use in
    294   * about:welcome, or falling back to moz-support-link otherwise
    295   *
    296   * @param {object} options - Link configuration options
    297   * @param {string} options.id - The element id
    298   * @param {string} options.l10nId - The fluent l10n id
    299   * @param {string} options.l10nName - The fluent l10n name
    300   * @param {string} options.supportPage - The support page slug
    301   * @returns {TemplateResult} The link template
    302   */
    303 
    304  getSupportLinkAnchor({
    305    id,
    306    l10nId,
    307    l10nName,
    308    supportPage = "firefox-backup",
    309  }) {
    310    if (this.aboutWelcomeEmbedded) {
    311      return html`<a
    312        id=${id}
    313        target="_blank"
    314        href=${this.getSupportURLWithUTM(supportPage)}
    315        data-l10n-id=${ifDefined(l10nId)}
    316        data-l10n-name=${ifDefined(l10nName)}
    317        dir="auto"
    318        rel="noopener noreferrer"
    319      ></a>`;
    320    }
    321 
    322    return html`<a
    323      id=${id}
    324      slot="support-link"
    325      is="moz-support-link"
    326      support-page=${supportPage}
    327      data-l10n-id=${ifDefined(l10nId)}
    328      data-l10n-name=${ifDefined(l10nName)}
    329      dir="auto"
    330    ></a>`;
    331  }
    332 
    333  applyContentCustomizations() {
    334    if (this.aboutWelcomeEmbedded) {
    335      this.style.setProperty(
    336        "--label-font-weight",
    337        "var(--font-weight-semibold)"
    338      );
    339    }
    340  }
    341 
    342  renderBackupFileInfo(backupFileInfo) {
    343    return html`<p
    344      id="restore-from-backup-backup-found-info"
    345      data-l10n-id="backup-file-creation-date-and-device"
    346      data-l10n-args=${JSON.stringify({
    347        machineName: backupFileInfo.deviceName ?? "",
    348        date: backupFileInfo.date ? new Date(backupFileInfo.date).getTime() : 0,
    349      })}
    350    ></p>`;
    351  }
    352 
    353  renderBackupFileStatus() {
    354    const { backupFileInfo, recoveryErrorCode } = this.backupServiceState || {};
    355 
    356    // We have errors and are embedded in about:welcome
    357    if (
    358      recoveryErrorCode &&
    359      !this.isIncorrectPassword &&
    360      this.aboutWelcomeEmbedded
    361    ) {
    362      return this.genericFileErrorTemplate();
    363    }
    364 
    365    // No backup file selected
    366    if (!backupFileInfo) {
    367      return this.getSupportLinkAnchor({
    368        id: "restore-from-backup-no-backup-file-link",
    369        l10nId: "restore-from-backup-no-backup-file-link",
    370      });
    371    }
    372 
    373    // Backup file found and no error
    374    return this.renderBackupFileInfo(backupFileInfo);
    375  }
    376 
    377  controlsTemplate() {
    378    let iconURL = this.#placeholderFileIconURL;
    379    if (
    380      this.backupServiceState?.backupFileToRestore &&
    381      !this.aboutWelcomeEmbedded
    382    ) {
    383      iconURL = this._fileIconURL || this.#placeholderFileIconURL;
    384    }
    385    return html`
    386      <fieldset id="backup-restore-controls">
    387        <fieldset id="backup-filepicker-controls">
    388          <label
    389            id="backup-filepicker-label"
    390            for="backup-filepicker-input"
    391            data-l10n-id="restore-from-backup-filepicker-label"
    392          ></label>
    393          <div id="backup-filepicker">
    394            ${this.inputTemplate(iconURL)}
    395            <moz-button
    396              id="backup-filepicker-button"
    397              @click=${this.handleChooseBackupFile}
    398              data-l10n-id="restore-from-backup-file-choose-button"
    399              aria-controls="backup-filepicker-input"
    400            ></moz-button>
    401          </div>
    402 
    403          ${this.renderBackupFileStatus()}
    404        </fieldset>
    405 
    406        <fieldset id="password-entry-controls">
    407          ${this.backupServiceState?.backupFileInfo?.isEncrypted
    408            ? this.passwordEntryTemplate()
    409            : null}
    410        </fieldset>
    411      </fieldset>
    412    `;
    413  }
    414 
    415  inputTemplate(iconURL) {
    416    const styles = styleMap(
    417      iconURL ? { backgroundImage: `url(${iconURL})` } : {}
    418    );
    419    const backupFileName = this.backupServiceState?.backupFileToRestore || "";
    420 
    421    // Determine the ID of the element that will be rendered by renderBackupFileStatus()
    422    // to reference with aria-describedby
    423    let describedBy = "";
    424    const { backupFileInfo, recoveryErrorCode } = this.backupServiceState || {};
    425 
    426    if (this.aboutWelcomeEmbedded) {
    427      if (recoveryErrorCode && !this.isIncorrectPassword) {
    428        describedBy = "backup-generic-file-error";
    429      } else if (!backupFileInfo) {
    430        describedBy = "restore-from-backup-no-backup-file-link";
    431      } else {
    432        describedBy = "restore-from-backup-backup-found-info";
    433      }
    434    }
    435 
    436    if (this.aboutWelcomeEmbedded) {
    437      return html`
    438        <textarea
    439          id="backup-filepicker-input"
    440          rows="1"
    441          readonly
    442          .value=${backupFileName}
    443          style=${styles}
    444          @input=${this.handleTextareaResize}
    445          aria-describedby=${describedBy}
    446          data-l10n-id="restore-from-backup-filepicker-input"
    447        ></textarea>
    448      `;
    449    }
    450 
    451    return html`
    452      <input
    453        id="backup-filepicker-input"
    454        type="text"
    455        readonly
    456        .value=${backupFileName}
    457        style=${styles}
    458        data-l10n-id="restore-from-backup-filepicker-input"
    459      />
    460    `;
    461  }
    462 
    463  passwordEntryTemplate() {
    464    const isInvalid = this.isIncorrectPassword;
    465    const describedBy = isInvalid
    466      ? "backup-password-error"
    467      : "backup-password-description";
    468 
    469    return html` <fieldset id="backup-password">
    470      <label id="backup-password-label" for="backup-password-input">
    471        <span
    472          id="backup-password-span"
    473          data-l10n-id="restore-from-backup-password-label"
    474        ></span>
    475        <input
    476          type="password"
    477          id="backup-password-input"
    478          aria-invalid=${String(isInvalid)}
    479          aria-describedby=${describedBy}
    480        />
    481      </label>
    482      ${isInvalid
    483        ? html`
    484            <span
    485              id="backup-password-error"
    486              class="field-error"
    487              data-l10n-id="backup-service-error-incorrect-password"
    488            >
    489              ${this.getSupportLinkAnchor({
    490                id: "backup-incorrect-password-support-link",
    491                l10nName: "incorrect-password-support-link",
    492              })}
    493            </span>
    494          `
    495        : html`<label
    496            id="backup-password-description"
    497            data-l10n-id="restore-from-backup-password-description"
    498          ></label> `}
    499    </fieldset>`;
    500  }
    501 
    502  contentTemplate() {
    503    let buttonL10nId = !this.backupServiceState?.recoveryInProgress
    504      ? "restore-from-backup-confirm-button"
    505      : "restore-from-backup-restoring-button";
    506 
    507    return html`
    508      <div
    509        id="restore-from-backup-wrapper"
    510        aria-labelledby="restore-from-backup-header"
    511        aria-describedby="restore-from-backup-description"
    512      >
    513        ${this.aboutWelcomeEmbedded ? null : this.headerTemplate()}
    514        <main id="restore-from-backup-content">
    515          ${!this.aboutWelcomeEmbedded &&
    516          this.backupServiceState?.recoveryErrorCode
    517            ? this.errorTemplate()
    518            : null}
    519          ${!this.aboutWelcomeEmbedded &&
    520          this.backupServiceState?.backupFileInfo
    521            ? this.descriptionTemplate()
    522            : null}
    523          ${this.controlsTemplate()}
    524        </main>
    525 
    526        <moz-button-group id="restore-from-backup-button-group">
    527          ${this.aboutWelcomeEmbedded ? null : this.cancelButtonTemplate()}
    528          <moz-button
    529            id="restore-from-backup-confirm-button"
    530            @click=${this.handleConfirm}
    531            type="primary"
    532            data-l10n-id=${buttonL10nId}
    533            ?disabled=${!this.backupServiceState?.backupFileToRestore ||
    534            this.backupServiceState?.recoveryInProgress}
    535          ></moz-button>
    536        </moz-button-group>
    537      </div>
    538    `;
    539  }
    540 
    541  headerTemplate() {
    542    return html`
    543      <h1
    544        id="restore-from-backup-header"
    545        class="heading-medium"
    546        data-l10n-id="restore-from-backup-header"
    547      ></h1>
    548    `;
    549  }
    550 
    551  cancelButtonTemplate() {
    552    return html`
    553      <moz-button
    554        id="restore-from-backup-cancel-button"
    555        @click=${this.handleCancel}
    556        data-l10n-id="restore-from-backup-cancel-button"
    557      ></moz-button>
    558    `;
    559  }
    560 
    561  descriptionTemplate() {
    562    let { date } = this.backupServiceState?.backupFileInfo || {};
    563    let dateTime = date && new Date(date).getTime();
    564    return html`
    565      <moz-message-bar
    566        id="restore-from-backup-description"
    567        type="info"
    568        data-l10n-id="restore-from-backup-description-with-metadata"
    569        data-l10n-args=${JSON.stringify({
    570          date: dateTime,
    571        })}
    572      >
    573        <a
    574          id="restore-from-backup-learn-more-link"
    575          slot="support-link"
    576          is="moz-support-link"
    577          support-page="firefox-backup"
    578          data-l10n-id="restore-from-backup-support-link"
    579        ></a>
    580      </moz-message-bar>
    581    `;
    582  }
    583 
    584  errorTemplate() {
    585    // We handle incorrect password errors in the password input
    586    if (this.isIncorrectPassword) {
    587      return null;
    588    }
    589 
    590    return html`
    591      <moz-message-bar
    592        id="restore-from-backup-error"
    593        type="error"
    594        data-l10n-id=${getErrorL10nId(
    595          this.backupServiceState?.recoveryErrorCode
    596        )}
    597      >
    598      </moz-message-bar>
    599    `;
    600  }
    601 
    602  genericFileErrorTemplate() {
    603    // We handle incorrect password errors in the password input
    604    if (this.isIncorrectPassword) {
    605      return null;
    606    }
    607 
    608    return html`
    609      <span
    610        id="backup-generic-file-error"
    611        class="field-error"
    612        data-l10n-id="backup-file-restore-file-validation-error"
    613      >
    614        <a
    615          id="backup-generic-error-link"
    616          target="_blank"
    617          slot="support-link"
    618          data-l10n-name="restore-problems"
    619          href=${this.getSupportURLWithUTM("firefox-backup")}
    620          rel="noopener noreferrer"
    621        ></a>
    622      </span>
    623    `;
    624  }
    625 
    626  render() {
    627    this.applyContentCustomizations();
    628    return html`
    629      <link
    630        rel="stylesheet"
    631        href="chrome://browser/content/backup/restore-from-backup.css"
    632      />
    633      ${this.contentTemplate()}
    634    `;
    635  }
    636 }
    637 
    638 customElements.define("restore-from-backup", RestoreFromBackup);