tor-browser

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

autofillEditForms.mjs (8232B)


      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 /* eslint-disable mozilla/balanced-listeners */ // Not relevant since the document gets unloaded.
      6 
      7 const lazy = {};
      8 ChromeUtils.defineESModuleGetters(lazy, {
      9  FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs",
     10 });
     11 
     12 class EditAutofillForm {
     13  constructor(elements) {
     14    this._elements = elements;
     15  }
     16 
     17  /**
     18   * Fill the form with a record object.
     19   *
     20   * @param  {object} [record = {}]
     21   */
     22  loadRecord(record = {}) {
     23    for (let field of this._elements.form.elements) {
     24      let value = record[field.id];
     25      value = typeof value == "undefined" ? "" : value;
     26 
     27      if (record.guid) {
     28        field.value = value;
     29      } else if (field.localName == "select") {
     30        this.setDefaultSelectedOptionByValue(field, value);
     31      } else {
     32        // Use .defaultValue instead of .value to avoid setting the `dirty` flag
     33        // which triggers form validation UI.
     34        field.defaultValue = value;
     35      }
     36    }
     37    if (!record.guid) {
     38      // Reset the dirty value flag and validity state.
     39      this._elements.form.reset();
     40    } else {
     41      for (let field of this._elements.form.elements) {
     42        this.updatePopulatedState(field);
     43        this.updateCustomValidity(field);
     44      }
     45    }
     46  }
     47 
     48  setDefaultSelectedOptionByValue(select, value) {
     49    for (let option of select.options) {
     50      option.defaultSelected = option.value == value;
     51    }
     52  }
     53 
     54  /**
     55   * Get a record from the form suitable for a save/update in storage.
     56   *
     57   * @returns {object}
     58   */
     59  buildFormObject() {
     60    let initialObject = {};
     61    if (this.hasMailingAddressFields) {
     62      // Start with an empty string for each mailing-address field so that any
     63      // fields hidden for the current country are blanked in the return value.
     64      initialObject = {
     65        "street-address": "",
     66        "address-level3": "",
     67        "address-level2": "",
     68        "address-level1": "",
     69        "postal-code": "",
     70      };
     71    }
     72 
     73    return Array.from(this._elements.form.elements).reduce((obj, input) => {
     74      if (!input.disabled) {
     75        obj[input.id] = input.value;
     76      }
     77      return obj;
     78    }, initialObject);
     79  }
     80 
     81  /**
     82   * Handle events
     83   *
     84   * @param  {DOMEvent} event
     85   */
     86  handleEvent(event) {
     87    switch (event.type) {
     88      case "change": {
     89        this.handleChange(event);
     90        break;
     91      }
     92      case "input": {
     93        this.handleInput(event);
     94        break;
     95      }
     96    }
     97  }
     98 
     99  /**
    100   * Handle change events
    101   *
    102   * @param  {DOMEvent} event
    103   */
    104  handleChange(event) {
    105    this.updatePopulatedState(event.target);
    106  }
    107 
    108  /**
    109   * Handle input events
    110   */
    111  handleInput(_e) {}
    112 
    113  /**
    114   * Attach event listener
    115   */
    116  attachEventListeners() {
    117    this._elements.form.addEventListener("input", this);
    118  }
    119 
    120  /**
    121   * Set the field-populated attribute if the field has a value.
    122   *
    123   * @param {DOMElement} field The field that will be checked for a value.
    124   */
    125  updatePopulatedState(field) {
    126    let span = field.parentNode.querySelector(".label-text");
    127    if (!span) {
    128      return;
    129    }
    130    span.toggleAttribute("field-populated", !!field.value.trim());
    131  }
    132 
    133  /**
    134   * Run custom validity routines specific to the field and type of form.
    135   *
    136   * @param {DOMElement} _field The field that will be validated.
    137   */
    138  updateCustomValidity(_field) {}
    139 }
    140 
    141 export class EditCreditCard extends EditAutofillForm {
    142  /**
    143   * @param {HTMLElement[]} elements
    144   * @param {object} record with a decrypted cc-number
    145   * @param {object} addresses in an object with guid keys for the billing address picker.
    146   */
    147  constructor(elements, record, addresses) {
    148    super(elements);
    149 
    150    this._addresses = addresses;
    151    Object.assign(this._elements, {
    152      ccNumber: this._elements.form.querySelector("#cc-number"),
    153      invalidCardNumberStringElement: this._elements.form.querySelector(
    154        "#invalidCardNumberString"
    155      ),
    156      month: this._elements.form.querySelector("#cc-exp-month"),
    157      year: this._elements.form.querySelector("#cc-exp-year"),
    158      billingAddress: this._elements.form.querySelector("#billingAddressGUID"),
    159      billingAddressRow:
    160        this._elements.form.querySelector(".billingAddressRow"),
    161    });
    162 
    163    this.attachEventListeners();
    164    this.loadRecord(record, addresses);
    165  }
    166 
    167  loadRecord(record, addresses, preserveFieldValues) {
    168    // _record must be updated before generateYears and generateBillingAddressOptions are called.
    169    this._record = record;
    170    this._addresses = addresses;
    171    this.generateBillingAddressOptions(preserveFieldValues);
    172    if (!preserveFieldValues) {
    173      // Re-generating the months will reset the selected option.
    174      this.generateMonths();
    175      // Re-generating the years will reset the selected option.
    176      this.generateYears();
    177      super.loadRecord(record);
    178    }
    179  }
    180 
    181  generateMonths() {
    182    const count = 12;
    183 
    184    // Clear the list
    185    this._elements.month.textContent = "";
    186 
    187    // Empty month option
    188    this._elements.month.appendChild(new Option());
    189 
    190    // Populate month list. Format: "month number - month name"
    191    let dateFormat = new Intl.DateTimeFormat(navigator.language, {
    192      month: "long",
    193    }).format;
    194    for (let i = 0; i < count; i++) {
    195      let monthNumber = (i + 1).toString();
    196      let monthName = dateFormat(new Date(1970, i));
    197      let option = new Option();
    198      option.value = monthNumber;
    199      // XXX: Bug 1446164 - Localize this string.
    200      option.textContent = `${monthNumber.padStart(2, "0")} - ${monthName}`;
    201      this._elements.month.appendChild(option);
    202    }
    203  }
    204 
    205  generateYears() {
    206    const count = 11;
    207    const currentYear = new Date().getFullYear();
    208    const ccExpYear = this._record && this._record["cc-exp-year"];
    209 
    210    // Clear the list
    211    this._elements.year.textContent = "";
    212 
    213    // Provide an empty year option
    214    this._elements.year.appendChild(new Option());
    215 
    216    if (ccExpYear && ccExpYear < currentYear) {
    217      this._elements.year.appendChild(new Option(ccExpYear));
    218    }
    219 
    220    for (let i = 0; i < count; i++) {
    221      let year = currentYear + i;
    222      let option = new Option(year);
    223      this._elements.year.appendChild(option);
    224    }
    225 
    226    if (ccExpYear && ccExpYear > currentYear + count) {
    227      this._elements.year.appendChild(new Option(ccExpYear));
    228    }
    229  }
    230 
    231  generateBillingAddressOptions(preserveFieldValues) {
    232    let billingAddressGUID;
    233    if (preserveFieldValues && this._elements.billingAddress.value) {
    234      billingAddressGUID = this._elements.billingAddress.value;
    235    } else if (this._record) {
    236      billingAddressGUID = this._record.billingAddressGUID;
    237    }
    238 
    239    this._elements.billingAddress.textContent = "";
    240 
    241    this._elements.billingAddress.appendChild(new Option("", ""));
    242 
    243    let hasAddresses = false;
    244    for (let [guid, address] of Object.entries(this._addresses)) {
    245      hasAddresses = true;
    246      let selected = guid == billingAddressGUID;
    247      let option = new Option(
    248        lazy.FormAutofillUtils.getAddressLabel(address),
    249        guid,
    250        selected,
    251        selected
    252      );
    253      this._elements.billingAddress.appendChild(option);
    254    }
    255 
    256    this._elements.billingAddressRow.hidden = !hasAddresses;
    257  }
    258 
    259  attachEventListeners() {
    260    this._elements.form.addEventListener("change", this);
    261    super.attachEventListeners();
    262  }
    263 
    264  handleInput(event) {
    265    // Clear the error message if cc-number is valid
    266    if (
    267      event.target == this._elements.ccNumber &&
    268      lazy.FormAutofillUtils.isCCNumber(this._elements.ccNumber.value)
    269    ) {
    270      this._elements.ccNumber.setCustomValidity("");
    271    }
    272    super.handleInput(event);
    273  }
    274 
    275  updateCustomValidity(field) {
    276    super.updateCustomValidity(field);
    277 
    278    // Mark the cc-number field as invalid if the number is empty or invalid.
    279    if (
    280      field == this._elements.ccNumber &&
    281      !lazy.FormAutofillUtils.isCCNumber(field.value)
    282    ) {
    283      let invalidCardNumberString =
    284        this._elements.invalidCardNumberStringElement.textContent;
    285      field.setCustomValidity(invalidCardNumberString || " ");
    286    }
    287  }
    288 }