From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- .../formautofill/content/autofillEditForms.js | 642 +++++++++++++++++++++ .../formautofill/content/customElements.js | 401 +++++++++++++ .../formautofill/content/editAddress.xhtml | 118 ++++ .../formautofill/content/editCreditCard.xhtml | 122 ++++ .../extensions/formautofill/content/editDialog.js | 235 ++++++++ .../formautofill/content/formautofill.css | 58 ++ .../formautofill/content/formfill-anchor.svg | 8 + .../content/icon-credit-card-generic.svg | 8 + .../formautofill/content/icon-credit-card.svg | 8 + .../formautofill/content/manageAddresses.xhtml | 54 ++ .../formautofill/content/manageCreditCards.xhtml | 55 ++ .../formautofill/content/manageDialog.css | 130 +++++ .../formautofill/content/manageDialog.js | 451 +++++++++++++++ .../content/third-party/cc-logo-amex.png | Bin 0 -> 1306 bytes .../content/third-party/cc-logo-amex@2x.png | Bin 0 -> 2311 bytes .../content/third-party/cc-logo-cartebancaire.png | Bin 0 -> 1240 bytes .../third-party/cc-logo-cartebancaire@2x.png | Bin 0 -> 3111 bytes .../content/third-party/cc-logo-diners.svg | 1 + .../content/third-party/cc-logo-discover.png | Bin 0 -> 1117 bytes .../content/third-party/cc-logo-discover@2x.png | Bin 0 -> 2471 bytes .../content/third-party/cc-logo-jcb.svg | 1 + .../content/third-party/cc-logo-mastercard.svg | 1 + .../content/third-party/cc-logo-mir.svg | 1 + .../content/third-party/cc-logo-unionpay.svg | 1 + .../content/third-party/cc-logo-visa.svg | 1 + 25 files changed, 2296 insertions(+) create mode 100644 browser/extensions/formautofill/content/autofillEditForms.js create mode 100644 browser/extensions/formautofill/content/customElements.js create mode 100644 browser/extensions/formautofill/content/editAddress.xhtml create mode 100644 browser/extensions/formautofill/content/editCreditCard.xhtml create mode 100644 browser/extensions/formautofill/content/editDialog.js create mode 100644 browser/extensions/formautofill/content/formautofill.css create mode 100644 browser/extensions/formautofill/content/formfill-anchor.svg create mode 100644 browser/extensions/formautofill/content/icon-credit-card-generic.svg create mode 100644 browser/extensions/formautofill/content/icon-credit-card.svg create mode 100644 browser/extensions/formautofill/content/manageAddresses.xhtml create mode 100644 browser/extensions/formautofill/content/manageCreditCards.xhtml create mode 100644 browser/extensions/formautofill/content/manageDialog.css create mode 100644 browser/extensions/formautofill/content/manageDialog.js create mode 100644 browser/extensions/formautofill/content/third-party/cc-logo-amex.png create mode 100644 browser/extensions/formautofill/content/third-party/cc-logo-amex@2x.png create mode 100644 browser/extensions/formautofill/content/third-party/cc-logo-cartebancaire.png create mode 100644 browser/extensions/formautofill/content/third-party/cc-logo-cartebancaire@2x.png create mode 100644 browser/extensions/formautofill/content/third-party/cc-logo-diners.svg create mode 100644 browser/extensions/formautofill/content/third-party/cc-logo-discover.png create mode 100644 browser/extensions/formautofill/content/third-party/cc-logo-discover@2x.png create mode 100644 browser/extensions/formautofill/content/third-party/cc-logo-jcb.svg create mode 100644 browser/extensions/formautofill/content/third-party/cc-logo-mastercard.svg create mode 100644 browser/extensions/formautofill/content/third-party/cc-logo-mir.svg create mode 100644 browser/extensions/formautofill/content/third-party/cc-logo-unionpay.svg create mode 100644 browser/extensions/formautofill/content/third-party/cc-logo-visa.svg (limited to 'browser/extensions/formautofill/content') diff --git a/browser/extensions/formautofill/content/autofillEditForms.js b/browser/extensions/formautofill/content/autofillEditForms.js new file mode 100644 index 0000000000..fea7a90a0a --- /dev/null +++ b/browser/extensions/formautofill/content/autofillEditForms.js @@ -0,0 +1,642 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* exported EditAddress, EditCreditCard */ +/* eslint-disable mozilla/balanced-listeners */ // Not relevant since the document gets unloaded. + +"use strict"; + +const { FormAutofill } = ChromeUtils.importESModule( + "resource://autofill/FormAutofill.sys.mjs" +); +const { FormAutofillUtils } = ChromeUtils.importESModule( + "resource://gre/modules/shared/FormAutofillUtils.sys.mjs" +); + +class EditAutofillForm { + constructor(elements) { + this._elements = elements; + } + + /** + * Fill the form with a record object. + * + * @param {object} [record = {}] + */ + loadRecord(record = {}) { + for (let field of this._elements.form.elements) { + let value = record[field.id]; + value = typeof value == "undefined" ? "" : value; + + if (record.guid) { + field.value = value; + } else if (field.localName == "select") { + this.setDefaultSelectedOptionByValue(field, value); + } else { + // Use .defaultValue instead of .value to avoid setting the `dirty` flag + // which triggers form validation UI. + field.defaultValue = value; + } + } + if (!record.guid) { + // Reset the dirty value flag and validity state. + this._elements.form.reset(); + } else { + for (let field of this._elements.form.elements) { + this.updatePopulatedState(field); + this.updateCustomValidity(field); + } + } + } + + setDefaultSelectedOptionByValue(select, value) { + for (let option of select.options) { + option.defaultSelected = option.value == value; + } + } + + /** + * Get a record from the form suitable for a save/update in storage. + * + * @returns {object} + */ + buildFormObject() { + let initialObject = {}; + if (this.hasMailingAddressFields) { + // Start with an empty string for each mailing-address field so that any + // fields hidden for the current country are blanked in the return value. + initialObject = { + "street-address": "", + "address-level3": "", + "address-level2": "", + "address-level1": "", + "postal-code": "", + }; + } + + return Array.from(this._elements.form.elements).reduce((obj, input) => { + if (!input.disabled) { + obj[input.id] = input.value; + } + return obj; + }, initialObject); + } + + /** + * Handle events + * + * @param {DOMEvent} event + */ + handleEvent(event) { + switch (event.type) { + case "change": { + this.handleChange(event); + break; + } + case "input": { + this.handleInput(event); + break; + } + } + } + + /** + * Handle change events + * + * @param {DOMEvent} event + */ + handleChange(event) { + this.updatePopulatedState(event.target); + } + + /** + * Handle input events + * + * @param {DOMEvent} event + */ + handleInput(event) {} + + /** + * Attach event listener + */ + attachEventListeners() { + this._elements.form.addEventListener("input", this); + } + + /** + * Set the field-populated attribute if the field has a value. + * + * @param {DOMElement} field The field that will be checked for a value. + */ + updatePopulatedState(field) { + let span = field.parentNode.querySelector(".label-text"); + if (!span) { + return; + } + span.toggleAttribute("field-populated", !!field.value.trim()); + } + + /** + * Run custom validity routines specific to the field and type of form. + * + * @param {DOMElement} field The field that will be validated. + */ + updateCustomValidity(field) {} +} + +class EditAddress extends EditAutofillForm { + /** + * @param {HTMLElement[]} elements + * @param {object} record + * @param {object} config + * @param {boolean} [config.noValidate=undefined] Whether to validate the form + */ + constructor(elements, record, config) { + super(elements); + + Object.assign(this, config); + let { form } = this._elements; + Object.assign(this._elements, { + addressLevel3Label: form.querySelector( + "#address-level3-container > .label-text" + ), + addressLevel2Label: form.querySelector( + "#address-level2-container > .label-text" + ), + addressLevel1Label: form.querySelector( + "#address-level1-container > .label-text" + ), + postalCodeLabel: form.querySelector( + "#postal-code-container > .label-text" + ), + country: form.querySelector("#country"), + }); + + this.populateCountries(); + // Need to populate the countries before trying to set the initial country. + // Also need to use this._record so it has the default country selected. + this.loadRecord(record); + this.attachEventListeners(); + + form.noValidate = !!config.noValidate; + } + + loadRecord(record) { + this._record = record; + if (!record) { + record = { + country: FormAutofill.DEFAULT_REGION, + }; + } + + let { addressLevel1Options } = FormAutofillUtils.getFormFormat( + record.country + ); + this.populateAddressLevel1(addressLevel1Options, record.country); + + super.loadRecord(record); + this.loadAddressLevel1(record["address-level1"], record.country); + this.formatForm(record.country); + } + + get hasMailingAddressFields() { + let { addressFields } = this._elements.form.dataset; + return ( + !addressFields || + addressFields.trim().split(/\s+/).includes("mailing-address") + ); + } + + /** + * `mailing-address` is a special attribute token to indicate mailing fields + country. + * + * @param {object[]} mailingFieldsOrder - `fieldsOrder` from `getFormFormat` + * @param {string} addressFields - white-space-separated string of requested address fields to show + * @returns {object[]} in the same structure as `mailingFieldsOrder` but including non-mail fields + */ + static computeVisibleFields(mailingFieldsOrder, addressFields) { + if (addressFields) { + let requestedFieldClasses = addressFields.trim().split(/\s+/); + let fieldClasses = []; + if (requestedFieldClasses.includes("mailing-address")) { + fieldClasses = fieldClasses.concat(mailingFieldsOrder); + // `country` isn't part of the `mailingFieldsOrder` so add it when filling a mailing-address + requestedFieldClasses.splice( + requestedFieldClasses.indexOf("mailing-address"), + 1, + "country" + ); + } + + for (let fieldClassName of requestedFieldClasses) { + fieldClasses.push({ + fieldId: fieldClassName, + newLine: fieldClassName == "name", + }); + } + return fieldClasses; + } + + // This is the default which is shown in the management interface and includes all fields. + return mailingFieldsOrder.concat([ + { + fieldId: "country", + }, + { + fieldId: "tel", + }, + { + fieldId: "email", + newLine: true, + }, + ]); + } + + /** + * Format the form based on country. The address-level1 and postal-code labels + * should be specific to the given country. + * + * @param {string} country + */ + formatForm(country) { + const { + addressLevel3L10nId, + addressLevel2L10nId, + addressLevel1L10nId, + addressLevel1Options, + postalCodeL10nId, + fieldsOrder: mailingFieldsOrder, + postalCodePattern, + countryRequiredFields, + } = FormAutofillUtils.getFormFormat(country); + + document.l10n.setAttributes( + this._elements.addressLevel3Label, + addressLevel3L10nId + ); + document.l10n.setAttributes( + this._elements.addressLevel2Label, + addressLevel2L10nId + ); + document.l10n.setAttributes( + this._elements.addressLevel1Label, + addressLevel1L10nId + ); + document.l10n.setAttributes( + this._elements.postalCodeLabel, + postalCodeL10nId + ); + let addressFields = this._elements.form.dataset.addressFields; + let extraRequiredFields = this._elements.form.dataset.extraRequiredFields; + let fieldClasses = EditAddress.computeVisibleFields( + mailingFieldsOrder, + addressFields + ); + let requiredFields = new Set(countryRequiredFields); + if (extraRequiredFields) { + for (let extraRequiredField of extraRequiredFields.trim().split(/\s+/)) { + requiredFields.add(extraRequiredField); + } + } + this.arrangeFields(fieldClasses, requiredFields); + this.updatePostalCodeValidation(postalCodePattern); + this.populateAddressLevel1(addressLevel1Options, country); + } + + /** + * Update address field visibility and order based on libaddressinput data. + * + * @param {object[]} fieldsOrder array of objects with `fieldId` and optional `newLine` properties + * @param {Set} requiredFields Set of `fieldId` strings that mark which fields are required + */ + arrangeFields(fieldsOrder, requiredFields) { + /** + * @see FormAutofillStorage.VALID_ADDRESS_FIELDS + */ + let fields = [ + // `name` is a wrapper for the 3 name fields. + "name", + "organization", + "street-address", + "address-level3", + "address-level2", + "address-level1", + "postal-code", + "country", + "tel", + "email", + ]; + let inputs = []; + for (let i = 0; i < fieldsOrder.length; i++) { + let { fieldId, newLine } = fieldsOrder[i]; + + let container = this._elements.form.querySelector( + `#${fieldId}-container` + ); + let containerInputs = [ + ...container.querySelectorAll("input, textarea, select"), + ]; + containerInputs.forEach(function (input) { + input.disabled = false; + // libaddressinput doesn't list 'country' or 'name' as required. + input.required = + fieldId == "country" || + fieldId == "name" || + requiredFields.has(fieldId); + }); + inputs.push(...containerInputs); + container.style.display = "flex"; + container.style.order = i; + container.style.pageBreakAfter = newLine ? "always" : "auto"; + // Remove the field from the list of fields + fields.splice(fields.indexOf(fieldId), 1); + } + for (let i = 0; i < inputs.length; i++) { + // Assign tabIndex starting from 1 + inputs[i].tabIndex = i + 1; + } + // Hide the remaining fields + for (let field of fields) { + let container = this._elements.form.querySelector(`#${field}-container`); + container.style.display = "none"; + for (let input of [ + ...container.querySelectorAll("input, textarea, select"), + ]) { + input.disabled = true; + } + } + } + + updatePostalCodeValidation(postalCodePattern) { + let postalCodeInput = this._elements.form.querySelector("#postal-code"); + if (postalCodePattern && postalCodeInput.style.display != "none") { + postalCodeInput.setAttribute("pattern", postalCodePattern); + } else { + postalCodeInput.removeAttribute("pattern"); + } + } + + /** + * Set the address-level1 value on the form field (input or select, whichever is present). + * + * @param {string} addressLevel1Value Value of the address-level1 from the autofill record + * @param {string} country The corresponding country + */ + loadAddressLevel1(addressLevel1Value, country) { + let field = this._elements.form.querySelector("#address-level1"); + + if (field.localName == "input") { + field.value = addressLevel1Value || ""; + return; + } + + let matchedSelectOption = FormAutofillUtils.findAddressSelectOption( + field, + { + country, + "address-level1": addressLevel1Value, + }, + "address-level1" + ); + if (matchedSelectOption && !matchedSelectOption.selected) { + field.value = matchedSelectOption.value; + field.dispatchEvent(new Event("input", { bubbles: true })); + field.dispatchEvent(new Event("change", { bubbles: true })); + } else if (addressLevel1Value) { + // If the option wasn't found, insert an option at the beginning of + // the select that matches the stored value. + field.insertBefore( + new Option(addressLevel1Value, addressLevel1Value, true, true), + field.firstChild + ); + } + } + + /** + * Replace the text input for address-level1 with a select dropdown if + * a fixed set of names exists. Otherwise show a text input. + * + * @param {Map?} options Map of options with regionCode -> name mappings + * @param {string} country The corresponding country + */ + populateAddressLevel1(options, country) { + let field = this._elements.form.querySelector("#address-level1"); + + if (field.dataset.country == country) { + return; + } + + if (!options) { + if (field.localName == "input") { + return; + } + + let input = document.createElement("input"); + input.setAttribute("type", "text"); + input.id = "address-level1"; + input.required = field.required; + input.disabled = field.disabled; + input.tabIndex = field.tabIndex; + field.replaceWith(input); + return; + } + + if (field.localName == "input") { + let select = document.createElement("select"); + select.id = "address-level1"; + select.required = field.required; + select.disabled = field.disabled; + select.tabIndex = field.tabIndex; + field.replaceWith(select); + field = select; + } + + field.textContent = ""; + field.dataset.country = country; + let fragment = document.createDocumentFragment(); + fragment.appendChild(new Option(undefined, undefined, true, true)); + for (let [regionCode, regionName] of options) { + let option = new Option(regionName, regionCode); + fragment.appendChild(option); + } + field.appendChild(fragment); + } + + populateCountries() { + let fragment = document.createDocumentFragment(); + // Sort countries by their visible names. + let countries = [...FormAutofill.countries.entries()].sort((e1, e2) => + e1[1].localeCompare(e2[1]) + ); + for (let [country] of countries) { + const countryName = Services.intl.getRegionDisplayNames(undefined, [ + country.toLowerCase(), + ]); + const option = new Option(countryName, country); + fragment.appendChild(option); + } + this._elements.country.appendChild(fragment); + } + + handleChange(event) { + if (event.target == this._elements.country) { + this.formatForm(event.target.value); + } + super.handleChange(event); + } + + attachEventListeners() { + this._elements.form.addEventListener("change", this); + super.attachEventListeners(); + } +} + +class EditCreditCard extends EditAutofillForm { + /** + * @param {HTMLElement[]} elements + * @param {object} record with a decrypted cc-number + * @param {object} addresses in an object with guid keys for the billing address picker. + */ + constructor(elements, record, addresses) { + super(elements); + + this._addresses = addresses; + Object.assign(this._elements, { + ccNumber: this._elements.form.querySelector("#cc-number"), + invalidCardNumberStringElement: this._elements.form.querySelector( + "#invalidCardNumberString" + ), + month: this._elements.form.querySelector("#cc-exp-month"), + year: this._elements.form.querySelector("#cc-exp-year"), + billingAddress: this._elements.form.querySelector("#billingAddressGUID"), + billingAddressRow: + this._elements.form.querySelector(".billingAddressRow"), + }); + + this.attachEventListeners(); + this.loadRecord(record, addresses); + } + + loadRecord(record, addresses, preserveFieldValues) { + // _record must be updated before generateYears and generateBillingAddressOptions are called. + this._record = record; + this._addresses = addresses; + this.generateBillingAddressOptions(preserveFieldValues); + if (!preserveFieldValues) { + // Re-generating the months will reset the selected option. + this.generateMonths(); + // Re-generating the years will reset the selected option. + this.generateYears(); + super.loadRecord(record); + } + } + + generateMonths() { + const count = 12; + + // Clear the list + this._elements.month.textContent = ""; + + // Empty month option + this._elements.month.appendChild(new Option()); + + // Populate month list. Format: "month number - month name" + let dateFormat = new Intl.DateTimeFormat(navigator.language, { + month: "long", + }).format; + for (let i = 0; i < count; i++) { + let monthNumber = (i + 1).toString(); + let monthName = dateFormat(new Date(1970, i)); + let option = new Option(); + option.value = monthNumber; + // XXX: Bug 1446164 - Localize this string. + option.textContent = `${monthNumber.padStart(2, "0")} - ${monthName}`; + this._elements.month.appendChild(option); + } + } + + generateYears() { + const count = 11; + const currentYear = new Date().getFullYear(); + const ccExpYear = this._record && this._record["cc-exp-year"]; + + // Clear the list + this._elements.year.textContent = ""; + + // Provide an empty year option + this._elements.year.appendChild(new Option()); + + if (ccExpYear && ccExpYear < currentYear) { + this._elements.year.appendChild(new Option(ccExpYear)); + } + + for (let i = 0; i < count; i++) { + let year = currentYear + i; + let option = new Option(year); + this._elements.year.appendChild(option); + } + + if (ccExpYear && ccExpYear > currentYear + count) { + this._elements.year.appendChild(new Option(ccExpYear)); + } + } + + generateBillingAddressOptions(preserveFieldValues) { + let billingAddressGUID; + if (preserveFieldValues && this._elements.billingAddress.value) { + billingAddressGUID = this._elements.billingAddress.value; + } else if (this._record) { + billingAddressGUID = this._record.billingAddressGUID; + } + + this._elements.billingAddress.textContent = ""; + + this._elements.billingAddress.appendChild(new Option("", "")); + + let hasAddresses = false; + for (let [guid, address] of Object.entries(this._addresses)) { + hasAddresses = true; + let selected = guid == billingAddressGUID; + let option = new Option( + FormAutofillUtils.getAddressLabel(address), + guid, + selected, + selected + ); + this._elements.billingAddress.appendChild(option); + } + + this._elements.billingAddressRow.hidden = !hasAddresses; + } + + attachEventListeners() { + this._elements.form.addEventListener("change", this); + super.attachEventListeners(); + } + + handleInput(event) { + // Clear the error message if cc-number is valid + if ( + event.target == this._elements.ccNumber && + FormAutofillUtils.isCCNumber(this._elements.ccNumber.value) + ) { + this._elements.ccNumber.setCustomValidity(""); + } + super.handleInput(event); + } + + updateCustomValidity(field) { + super.updateCustomValidity(field); + + // Mark the cc-number field as invalid if the number is empty or invalid. + if ( + field == this._elements.ccNumber && + !FormAutofillUtils.isCCNumber(field.value) + ) { + let invalidCardNumberString = + this._elements.invalidCardNumberStringElement.textContent; + field.setCustomValidity(invalidCardNumberString || " "); + } + } +} diff --git a/browser/extensions/formautofill/content/customElements.js b/browser/extensions/formautofill/content/customElements.js new file mode 100644 index 0000000000..0f1260634d --- /dev/null +++ b/browser/extensions/formautofill/content/customElements.js @@ -0,0 +1,401 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file is loaded into the browser window scope. +/* eslint-env mozilla/browser-window */ +/* eslint-disable mozilla/balanced-listeners */ // Not relevant since the document gets unloaded. + +"use strict"; + +// Wrap in a block to prevent leaking to window scope. +(() => { + function sendMessageToBrowser(msgName, data) { + let { AutoCompleteParent } = ChromeUtils.importESModule( + "resource://gre/actors/AutoCompleteParent.sys.mjs" + ); + + let actor = AutoCompleteParent.getCurrentActor(); + if (!actor) { + return; + } + + actor.manager.getActor("FormAutofill").sendAsyncMessage(msgName, data); + } + + class MozAutocompleteProfileListitemBase extends MozElements.MozRichlistitem { + constructor() { + super(); + + /** + * For form autofill, we want to unify the selection no matter by + * keyboard navigation or mouseover in order not to confuse user which + * profile preview is being shown. This field is set to true to indicate + * that selectedIndex of popup should be changed while mouseover item + */ + this.selectedByMouseOver = true; + } + + get _stringBundle() { + if (!this.__stringBundle) { + this.__stringBundle = Services.strings.createBundle( + "chrome://formautofill/locale/formautofill.properties" + ); + } + return this.__stringBundle; + } + + _cleanup() { + this.removeAttribute("formautofillattached"); + if (this._itemBox) { + this._itemBox.removeAttribute("size"); + } + } + + _onOverflow() {} + + _onUnderflow() {} + + handleOverUnderflow() {} + + _adjustAutofillItemLayout() { + let outerBoxRect = this.parentNode.getBoundingClientRect(); + + // Make item fit in popup as XUL box could not constrain + // item's width + this._itemBox.style.width = outerBoxRect.width + "px"; + // Use two-lines layout when width is smaller than 150px or + // 185px if an image precedes the label. + let oneLineMinRequiredWidth = this.getAttribute("ac-image") ? 185 : 150; + + if (outerBoxRect.width <= oneLineMinRequiredWidth) { + this._itemBox.setAttribute("size", "small"); + } else { + this._itemBox.removeAttribute("size"); + } + } + } + + MozElements.MozAutocompleteProfileListitem = class MozAutocompleteProfileListitem extends ( + MozAutocompleteProfileListitemBase + ) { + static get markup() { + return ` +
+
+ +
+
+ +
+
+ `; + } + + connectedCallback() { + if (this.delayConnectedCallback()) { + return; + } + + this.textContent = ""; + + this.appendChild(this.constructor.fragment); + + this._itemBox = this.querySelector(".autofill-item-box"); + this._label = this.querySelector(".profile-label"); + this._comment = this.querySelector(".profile-comment"); + + this.initializeAttributeInheritance(); + this._adjustAcItem(); + } + + static get inheritedAttributes() { + return { + ".autofill-item-box": "ac-image", + }; + } + + set selected(val) { + if (val) { + this.setAttribute("selected", "true"); + } else { + this.removeAttribute("selected"); + } + + sendMessageToBrowser("FormAutofill:PreviewProfile"); + } + + get selected() { + return this.getAttribute("selected") == "true"; + } + + _adjustAcItem() { + this._adjustAutofillItemLayout(); + this.setAttribute("formautofillattached", "true"); + this._itemBox.style.setProperty( + "--primary-icon", + `url(${this.getAttribute("ac-image")})` + ); + + let { primary, secondary, ariaLabel } = JSON.parse( + this.getAttribute("ac-value") + ); + + this._label.textContent = primary.toString().replaceAll("*", "•"); + this._comment.textContent = secondary.toString().replaceAll("*", "•"); + if (ariaLabel) { + this.setAttribute("aria-label", ariaLabel); + } + } + }; + + customElements.define( + "autocomplete-profile-listitem", + MozElements.MozAutocompleteProfileListitem, + { extends: "richlistitem" } + ); + + class MozAutocompleteProfileListitemFooter extends MozAutocompleteProfileListitemBase { + static get markup() { + return ` + + `; + } + + constructor() { + super(); + + this.addEventListener("click", event => { + if (event.button != 0) { + return; + } + + if (this._warningTextBox.contains(event.originalTarget)) { + return; + } + + window.openPreferences("privacy-form-autofill"); + }); + } + + connectedCallback() { + if (this.delayConnectedCallback()) { + return; + } + + this.textContent = ""; + this.appendChild(this.constructor.fragment); + + this._itemBox = this.querySelector(".autofill-footer"); + this._optionButton = this.querySelector(".autofill-button"); + this._warningTextBox = this.querySelector(".autofill-warning"); + + /** + * A handler for updating warning message once selectedIndex has been changed. + * + * There're three different states of warning message: + * 1. None of addresses were selected: We show all the categories intersection of fields in the + * form and fields in the results. + * 2. An address was selested: Show the additional categories that will also be filled. + * 3. An address was selected, but the focused category is the same as the only one category: Only show + * the exact category that we're going to fill in. + * + * @private + * @param {object} data + * Message data + * @param {string[]} data.categories + * The categories of all the fields contained in the selected address. + */ + this.updateWarningNote = data => { + let categories = + data && data.categories ? data.categories : this._allFieldCategories; + // If the length of categories is 1, that means all the fillable fields are in the same + // category. We will change the way to inform user according to this flag. When the value + // is true, we show "Also autofills ...", otherwise, show "Autofills ..." only. + let hasExtraCategories = categories.length > 1; + // Show the categories in certain order to conform with the spec. + let orderedCategoryList = [ + { id: "address", l10nId: "category.address" }, + { id: "name", l10nId: "category.name" }, + { id: "organization", l10nId: "category.organization2" }, + { id: "tel", l10nId: "category.tel" }, + { id: "email", l10nId: "category.email" }, + ]; + let showCategories = hasExtraCategories + ? orderedCategoryList.filter( + category => + categories.includes(category.id) && + category.id != this._focusedCategory + ) + : [ + orderedCategoryList.find( + category => category.id == this._focusedCategory + ), + ]; + + let separator = + this._stringBundle.GetStringFromName("fieldNameSeparator"); + let warningTextTmplKey = hasExtraCategories + ? "phishingWarningMessage" + : "phishingWarningMessage2"; + let categoriesText = showCategories + .map(category => + this._stringBundle.GetStringFromName(category.l10nId) + ) + .join(separator); + + this._warningTextBox.textContent = + this._stringBundle.formatStringFromName(warningTextTmplKey, [ + categoriesText, + ]); + this.parentNode.parentNode.adjustHeight(); + }; + + this._adjustAcItem(); + } + + _onCollapse() { + if (this.showWarningText) { + let { FormAutofillParent } = ChromeUtils.importESModule( + "resource://autofill/FormAutofillParent.sys.mjs" + ); + FormAutofillParent.removeMessageObserver(this); + } + this._itemBox.removeAttribute("no-warning"); + } + + _adjustAcItem() { + this._adjustAutofillItemLayout(); + this.setAttribute("formautofillattached", "true"); + + let value = JSON.parse(this.getAttribute("ac-value")); + + this._allFieldCategories = value.categories; + this._focusedCategory = value.focusedCategory; + this.showWarningText = this._allFieldCategories && this._focusedCategory; + + if (this.showWarningText) { + let { FormAutofillParent } = ChromeUtils.importESModule( + "resource://autofill/FormAutofillParent.sys.mjs" + ); + FormAutofillParent.addMessageObserver(this); + this.updateWarningNote(); + } else { + this._itemBox.setAttribute("no-warning", "true"); + } + + // After focusing a field that was previously filled with cc information, + // the "ac-image" is falsely set for the listitem-footer. For now it helps us + // to distinguish between address and cc footer. In the future this false attribute + // setting should be fixed and the "ac-image" check replaced by a different method. + const buttonTextBundleKey = !this.getAttribute("ac-image") + ? "autocompleteManageAddresses" + : "autocompleteManageCreditCards"; + const buttonText = + this._stringBundle.GetStringFromName(buttonTextBundleKey); + this._optionButton.textContent = buttonText; + } + } + + customElements.define( + "autocomplete-profile-listitem-footer", + MozAutocompleteProfileListitemFooter, + { extends: "richlistitem" } + ); + + class MozAutocompleteCreditcardInsecureField extends MozAutocompleteProfileListitemBase { + static get markup() { + return ` +
+ `; + } + + connectedCallback() { + if (this.delayConnectedCallback()) { + return; + } + this.textContent = ""; + this.appendChild(this.constructor.fragment); + + this._itemBox = this.querySelector(".autofill-insecure-item"); + + this._adjustAcItem(); + } + + set selected(val) { + // This item is unselectable since we see this item as a pure message. + } + + get selected() { + return this.getAttribute("selected") == "true"; + } + + _adjustAcItem() { + this._adjustAutofillItemLayout(); + this.setAttribute("formautofillattached", "true"); + + let value = this.getAttribute("ac-value"); + this._itemBox.textContent = value; + } + } + + customElements.define( + "autocomplete-creditcard-insecure-field", + MozAutocompleteCreditcardInsecureField, + { extends: "richlistitem" } + ); + + class MozAutocompleteProfileListitemClearButton extends MozAutocompleteProfileListitemBase { + static get markup() { + return ` + + `; + } + + constructor() { + super(); + + this.addEventListener("click", event => { + if (event.button != 0) { + return; + } + + sendMessageToBrowser("FormAutofill:ClearForm"); + }); + } + + connectedCallback() { + if (this.delayConnectedCallback()) { + return; + } + + this.textContent = ""; + this.appendChild(this.constructor.fragment); + + this._itemBox = this.querySelector(".autofill-item-box"); + this._clearBtn = this.querySelector(".autofill-button"); + + this._adjustAcItem(); + } + + _adjustAcItem() { + this._adjustAutofillItemLayout(); + this.setAttribute("formautofillattached", "true"); + + let clearFormBtnLabel = + this._stringBundle.GetStringFromName("clearFormBtnLabel2"); + this._clearBtn.textContent = clearFormBtnLabel; + } + } + + customElements.define( + "autocomplete-profile-listitem-clear-button", + MozAutocompleteProfileListitemClearButton, + { extends: "richlistitem" } + ); +})(); diff --git a/browser/extensions/formautofill/content/editAddress.xhtml b/browser/extensions/formautofill/content/editAddress.xhtml new file mode 100644 index 0000000000..47ae4a2a3b --- /dev/null +++ b/browser/extensions/formautofill/content/editAddress.xhtml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + +
+ + + +