tor-browser

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

manageDialog.mjs (12349B)


      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 const { AppConstants } = ChromeUtils.importESModule(
      6  "resource://gre/modules/AppConstants.sys.mjs"
      7 );
      8 const { FormAutofill } = ChromeUtils.importESModule(
      9  "resource://autofill/FormAutofill.sys.mjs"
     10 );
     11 const { AutofillTelemetry } = ChromeUtils.importESModule(
     12  "resource://gre/modules/shared/AutofillTelemetry.sys.mjs"
     13 );
     14 
     15 const lazy = {};
     16 ChromeUtils.defineESModuleGetters(lazy, {
     17  CreditCard: "resource://gre/modules/CreditCard.sys.mjs",
     18  FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs",
     19  formAutofillStorage: "resource://autofill/FormAutofillStorage.sys.mjs",
     20  FormAutofillPreferences:
     21    "resource://autofill/FormAutofillPreferences.sys.mjs",
     22 });
     23 
     24 ChromeUtils.defineLazyGetter(lazy, "log", () =>
     25  FormAutofill.defineLogGetter(lazy, "manageAddresses")
     26 );
     27 
     28 ChromeUtils.defineLazyGetter(
     29  lazy,
     30  "l10n",
     31  () => new Localization(["	browser/preferences/formAutofill.ftl"], true)
     32 );
     33 
     34 class ManageRecords {
     35  constructor(subStorageName, elements) {
     36    this._storageInitPromise = lazy.formAutofillStorage.initialize();
     37    this._subStorageName = subStorageName;
     38    this._elements = elements;
     39    this._newRequest = false;
     40    this._isLoadingRecords = false;
     41    this.prefWin = window.opener;
     42    window.addEventListener("load", this, { once: true });
     43  }
     44 
     45  async init() {
     46    await this.loadRecords();
     47    this.attachEventListeners();
     48    // For testing only: Notify when the dialog is ready for interaction
     49    window.dispatchEvent(new CustomEvent("FormReadyForTests"));
     50  }
     51 
     52  uninit() {
     53    lazy.log.debug("uninit");
     54    this.detachEventListeners();
     55    this._elements = null;
     56  }
     57 
     58  /**
     59   * Get the selected options on the addresses element.
     60   *
     61   * @returns {Array<DOMElement>}
     62   */
     63  get _selectedOptions() {
     64    return Array.from(this._elements.records.selectedOptions);
     65  }
     66 
     67  /**
     68   * Get storage and ensure it has been initialized.
     69   *
     70   * @returns {object}
     71   */
     72  async getStorage() {
     73    await this._storageInitPromise;
     74    return lazy.formAutofillStorage[this._subStorageName];
     75  }
     76 
     77  /**
     78   * Load records and render them. This function is a wrapper for _loadRecords
     79   * to ensure any reentrant will be handled well.
     80   */
     81  async loadRecords() {
     82    // This function can be early returned when there is any reentrant happends.
     83    // "_newRequest" needs to be set to ensure all changes will be applied.
     84    if (this._isLoadingRecords) {
     85      this._newRequest = true;
     86      return;
     87    }
     88    this._isLoadingRecords = true;
     89 
     90    await this._loadRecords();
     91 
     92    // _loadRecords should be invoked again if there is any multiple entrant
     93    // during running _loadRecords(). This step ensures that the latest request
     94    // still is applied.
     95    while (this._newRequest) {
     96      this._newRequest = false;
     97      await this._loadRecords();
     98    }
     99    this._isLoadingRecords = false;
    100 
    101    // For testing only: Notify when records are loaded
    102    this._elements.records.dispatchEvent(new CustomEvent("RecordsLoaded"));
    103  }
    104 
    105  async _loadRecords() {
    106    let storage = await this.getStorage();
    107    let records = await storage.getAll();
    108    // Sort by last used time starting with most recent
    109    records.sort((a, b) => {
    110      let aLastUsed = a.timeLastUsed || a.timeLastModified;
    111      let bLastUsed = b.timeLastUsed || b.timeLastModified;
    112      return bLastUsed - aLastUsed;
    113    });
    114    await this.renderRecordElements(records);
    115    this.updateButtonsStates(this._selectedOptions.length);
    116  }
    117 
    118  /**
    119   * Render the records onto the page while maintaining selected options if
    120   * they still exist.
    121   *
    122   * @param  {Array<object>} records
    123   */
    124  async renderRecordElements(records) {
    125    let selectedGuids = this._selectedOptions.map(option => option.value);
    126    this.clearRecordElements();
    127    for (let record of records) {
    128      let { id, args, raw } = await this.getLabelInfo(record);
    129      let option = new Option(
    130        raw ?? "",
    131        record.guid,
    132        false,
    133        selectedGuids.includes(record.guid)
    134      );
    135      if (id) {
    136        document.l10n.setAttributes(option, id, args);
    137      }
    138 
    139      option.record = record;
    140      this._elements.records.appendChild(option);
    141    }
    142  }
    143 
    144  /**
    145   * Remove all existing record elements.
    146   */
    147  clearRecordElements() {
    148    const parentElement = this._elements.records;
    149    while (parentElement.lastChild) {
    150      parentElement.removeChild(parentElement.lastChild);
    151    }
    152  }
    153 
    154  /**
    155   * Remove records by selected options.
    156   *
    157   * @param  {Array<DOMElement>} options
    158   */
    159  async removeRecords(options) {
    160    let storage = await this.getStorage();
    161    // Pause listening to storage change event to avoid triggering `loadRecords`
    162    // when removing records
    163    Services.obs.removeObserver(this, "formautofill-storage-changed");
    164 
    165    for (let option of options) {
    166      storage.remove(option.value);
    167      option.remove();
    168    }
    169    this.updateButtonsStates(this._selectedOptions);
    170 
    171    // Resume listening to storage change event
    172    Services.obs.addObserver(this, "formautofill-storage-changed");
    173    // For testing only: notify record(s) has been removed
    174    this._elements.records.dispatchEvent(new CustomEvent("RecordsRemoved"));
    175 
    176    for (let i = 0; i < options.length; i++) {
    177      AutofillTelemetry.recordManageEvent(this.telemetryType, "delete");
    178    }
    179  }
    180 
    181  /**
    182   * Enable/disable the Edit and Remove buttons based on number of selected
    183   * options.
    184   *
    185   * @param  {number} selectedCount
    186   */
    187  updateButtonsStates(selectedCount) {
    188    lazy.log.debug("updateButtonsStates:", selectedCount);
    189    if (selectedCount == 0) {
    190      this._elements.edit.setAttribute("disabled", "disabled");
    191      this._elements.remove.setAttribute("disabled", "disabled");
    192    } else if (selectedCount == 1) {
    193      this._elements.edit.removeAttribute("disabled");
    194      this._elements.remove.removeAttribute("disabled");
    195    } else if (selectedCount > 1) {
    196      this._elements.edit.setAttribute("disabled", "disabled");
    197      this._elements.remove.removeAttribute("disabled");
    198    }
    199    this._elements.add.disabled = !Services.prefs.getBoolPref(
    200      `extensions.formautofill.${this._subStorageName}.enabled`
    201    );
    202  }
    203 
    204  /**
    205   * Handle events
    206   *
    207   * @param  {DOMEvent} event
    208   */
    209  handleEvent(event) {
    210    switch (event.type) {
    211      case "load": {
    212        this.init();
    213        break;
    214      }
    215      case "click": {
    216        this.handleClick(event);
    217        break;
    218      }
    219      case "change": {
    220        this.updateButtonsStates(this._selectedOptions.length);
    221        break;
    222      }
    223      case "unload": {
    224        this.uninit();
    225        break;
    226      }
    227      case "keypress": {
    228        this.handleKeyPress(event);
    229        break;
    230      }
    231      case "contextmenu": {
    232        event.preventDefault();
    233        break;
    234      }
    235    }
    236  }
    237 
    238  /**
    239   * Handle click events
    240   *
    241   * @param  {DOMEvent} event
    242   */
    243  handleClick(event) {
    244    if (event.target == this._elements.remove) {
    245      this.removeRecords(this._selectedOptions);
    246    } else if (event.target == this._elements.add) {
    247      this.openEditDialog();
    248    } else if (
    249      event.target == this._elements.edit ||
    250      (event.target.parentNode == this._elements.records && event.detail > 1)
    251    ) {
    252      this.openEditDialog(this._selectedOptions[0].record);
    253    }
    254  }
    255 
    256  /**
    257   * Handle key press events
    258   *
    259   * @param  {DOMEvent} event
    260   */
    261  handleKeyPress(event) {
    262    if (event.keyCode == KeyEvent.DOM_VK_ESCAPE) {
    263      window.close();
    264    }
    265    if (event.keyCode == KeyEvent.DOM_VK_DELETE) {
    266      this.removeRecords(this._selectedOptions);
    267    }
    268  }
    269 
    270  observe(_subject, topic, _data) {
    271    switch (topic) {
    272      case "formautofill-storage-changed": {
    273        this.loadRecords();
    274      }
    275    }
    276  }
    277 
    278  /**
    279   * Attach event listener
    280   */
    281  attachEventListeners() {
    282    window.addEventListener("unload", this, { once: true });
    283    window.addEventListener("keypress", this);
    284    window.addEventListener("contextmenu", this);
    285    this._elements.records.addEventListener("change", this);
    286    this._elements.records.addEventListener("click", this);
    287    this._elements.controlsContainer.addEventListener("click", this);
    288    Services.obs.addObserver(this, "formautofill-storage-changed");
    289  }
    290 
    291  /**
    292   * Remove event listener
    293   */
    294  detachEventListeners() {
    295    window.removeEventListener("keypress", this);
    296    window.removeEventListener("contextmenu", this);
    297    this._elements.records.removeEventListener("change", this);
    298    this._elements.records.removeEventListener("click", this);
    299    this._elements.controlsContainer.removeEventListener("click", this);
    300    Services.obs.removeObserver(this, "formautofill-storage-changed");
    301  }
    302 }
    303 
    304 export class ManageAddresses extends ManageRecords {
    305  telemetryType = AutofillTelemetry.ADDRESS;
    306 
    307  constructor(elements) {
    308    super("addresses", elements);
    309    elements.add.setAttribute(
    310      "search-l10n-ids",
    311      lazy.FormAutofillUtils.EDIT_ADDRESS_L10N_IDS.join(",")
    312    );
    313    AutofillTelemetry.recordManageEvent(this.telemetryType, "show");
    314  }
    315 
    316  static getAddressL10nStrings() {
    317    const l10nIds = [
    318      ...lazy.FormAutofillUtils.MANAGE_ADDRESSES_L10N_IDS,
    319      ...lazy.FormAutofillUtils.EDIT_ADDRESS_L10N_IDS,
    320    ];
    321 
    322    return l10nIds.reduce(
    323      (acc, id) => ({
    324        ...acc,
    325        [id]: lazy.l10n.formatValueSync(id),
    326      }),
    327      {}
    328    );
    329  }
    330 
    331  /**
    332   * Open the edit address dialog to create/edit an address.
    333   *
    334   * @param  {object} address [optional]
    335   */
    336  openEditDialog(address) {
    337    return lazy.FormAutofillPreferences.openEditAddressDialog(
    338      address,
    339      this.prefWin
    340    );
    341  }
    342 
    343  getLabelInfo(address) {
    344    return { raw: lazy.FormAutofillUtils.getAddressLabel(address) };
    345  }
    346 }
    347 
    348 export class ManageCreditCards extends ManageRecords {
    349  telemetryType = AutofillTelemetry.CREDIT_CARD;
    350 
    351  constructor(elements) {
    352    super("creditCards", elements);
    353    elements.add.setAttribute(
    354      "search-l10n-ids",
    355      lazy.FormAutofillUtils.EDIT_CREDITCARD_L10N_IDS.join(",")
    356    );
    357 
    358    this._isDecrypted = false;
    359    AutofillTelemetry.recordManageEvent(this.telemetryType, "show");
    360  }
    361 
    362  /**
    363   * Open the edit address dialog to create/edit a credit card.
    364   *
    365   * @param  {object} creditCard [optional]
    366   */
    367  async openEditDialog(creditCard) {
    368    return lazy.FormAutofillPreferences.openEditCreditCardDialog(
    369      creditCard,
    370      this.prefWin
    371    );
    372  }
    373 
    374  /**
    375   * Get credit card display label. It should display masked numbers and the
    376   * cardholder's name, separated by a comma.
    377   *
    378   * @param {object} creditCard
    379   * @returns {Promise<string>}
    380   */
    381  async getLabelInfo(creditCard) {
    382    // The card type is displayed visually using an image. For a11y, we need
    383    // to expose it as text. We do this using aria-label. However,
    384    // aria-label overrides the text content, so we must include that also.
    385    // Since the text content is generated by Fluent, aria-label must be
    386    // generated by Fluent also.
    387    const type = creditCard["cc-type"];
    388    const typeL10nId = lazy.CreditCard.getNetworkL10nId(type);
    389    const typeName = typeL10nId
    390      ? await document.l10n.formatValue(typeL10nId)
    391      : (type ?? ""); // Unknown card type
    392    return lazy.CreditCard.getLabelInfo({
    393      name: creditCard["cc-name"],
    394      number: creditCard["cc-number"],
    395      month: creditCard["cc-exp-month"],
    396      year: creditCard["cc-exp-year"],
    397      type: typeName,
    398    });
    399  }
    400 
    401  async renderRecordElements(records) {
    402    // Revert back to encrypted form when re-rendering happens
    403    this._isDecrypted = false;
    404    // Display third-party card icons when possible
    405    this._elements.records.classList.toggle(
    406      "branded",
    407      AppConstants.MOZILLA_OFFICIAL
    408    );
    409    await super.renderRecordElements(records);
    410 
    411    let options = this._elements.records.options;
    412    for (let option of options) {
    413      let record = option.record;
    414      if (record && record["cc-type"]) {
    415        option.setAttribute("cc-type", record["cc-type"]);
    416      } else {
    417        option.removeAttribute("cc-type");
    418      }
    419    }
    420  }
    421 
    422  updateButtonsStates(selectedCount) {
    423    super.updateButtonsStates(selectedCount);
    424  }
    425 
    426  handleClick(event) {
    427    super.handleClick(event);
    428  }
    429 }