dbfdg PK !<)..chrome.manifestlocale formautofill en-US en-US/locale/en-US/ PK ! ensureCssLoaded(window); FormAutofillStatus.init(); ChromeUtils.registerWindowActor("FormAutofill", { parent: { esModuleURI: "resource://autofill/FormAutofillParent.sys.mjs", }, child: { esModuleURI: "resource://autofill/FormAutofillChild.sys.mjs", events: { focusin: {}, "form-submission-detected": { createActor: false }, }, }, allFrames: true, }); } onShutdown(isAppShutdown) { if (isAppShutdown) { return; } resProto.setSubstitution(RESOURCE_HOST, null); this.chromeHandle.destruct(); this.chromeHandle = null; if (this.autofillManifest) { Components.manager.removeBootstrappedManifestLocation( this.autofillManifest ); } ChromeUtils.unregisterWindowActor("FormAutofill"); AutoCompleteParent.removePopupStateListener(ensureCssLoaded); FormAutofillParent.removeMessageObserver(this); for (let win of Services.wm.getEnumerator("navigator:browser")) { let cachedStyleSheets = CACHED_STYLESHEETS.get(win); if (!cachedStyleSheets) { continue; } while (cachedStyleSheets.length !== 0) { cachedStyleSheets.pop().remove(); } } } }; PK !<ՊTT background.js/* 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/. */ /* eslint-env webextensions */ "use strict"; browser.runtime.onUpdateAvailable.addListener(_details => { // By listening to but ignoring this event, any updates will // be delayed until the next browser restart. // Note that if we ever wanted to change this, we should make // sure we manually invalidate the startup cache using the // startupcache-invalidate notification. }); PK !< $chrome/content/addressFormLayout.mjs/* 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/. */ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { FormAutofill: "resource://autofill/FormAutofill.sys.mjs", FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs", }); // Defines template descriptors for generating elements in convertLayoutToUI. const fieldTemplates = { commonAttributes(item) { return { id: item.fieldId, name: item.fieldId, required: item.required, value: item.value ?? "", }; }, input(item) { return { tag: "input", type: item.type ?? "text", ...this.commonAttributes(item), }; }, textarea(item) { return { tag: "textarea", ...this.commonAttributes(item), }; }, select(item) { return { tag: "select", children: item.options.map(({ value, text }) => ({ tag: "option", selected: value === item.value, value, text, })), ...this.commonAttributes(item), }; }, }; /** * Creates an HTML element with specified attributes and children. * * @param {string} tag - Tag name for the element to create. * @param {object} options - Options object containing attributes and children. * @param {object} options.attributes - Element's Attributes/Props (id, class, etc.) * @param {Array} options.children - Element's children (array of objects with tag and options). * @returns {HTMLElement} The newly created element. */ const createElement = (tag, { children = [], ...attributes }) => { const element = document.createElement(tag); for (let [attributeName, attributeValue] of Object.entries(attributes)) { if (attributeName in element) { element[attributeName] = attributeValue; } else { element.setAttribute(attributeName, attributeValue); } } for (let { tag: childTag, ...childRest } of children) { element.appendChild(createElement(childTag, childRest)); } return element; }; /** * Generator that creates UI elements from `fields` object, using localization from `l10nStrings`. * * @param {Array} fields - Array of objects as returned from `FormAutofillUtils.getFormLayout`. * @param {object} l10nStrings - Key-value pairs for field label localization. * @yields {HTMLElement} - A localized label element with constructed from a field. */ function* convertLayoutToUI(fields, l10nStrings) { for (const item of fields) { // eslint-disable-next-line no-nested-ternary const fieldTag = item.options ? "select" : item.multiline ? "textarea" : "input"; const fieldUI = { label: { id: `${item.fieldId}-container`, class: `container ${item.newLine ? "new-line" : ""}`, }, field: fieldTemplates[fieldTag](item), span: { class: "label-text", textContent: l10nStrings[item.l10nId] ?? "", }, }; const label = createElement("label", fieldUI.label); const { tag, ...rest } = fieldUI.field; const field = createElement(tag, rest); label.appendChild(field); const span = createElement("span", fieldUI.span); label.appendChild(span); yield label; } } /** * Retrieves the current form data from the current form element on the page. * * @returns {object} An object containing key-value pairs of form data. */ export const getCurrentFormData = () => { const formElement = document.querySelector("form"); const formData = new FormData(formElement); return Object.fromEntries(formData.entries()); }; /** * Checks if the form can be submitted based on the number of non-empty values. * TODO(Bug 1891734): Add address validation. Right now we don't do any validation. (2 fields mimics the old behaviour ). * * @returns {boolean} True if the form can be submitted */ export const canSubmitForm = () => { const formData = getCurrentFormData(); const validValues = Object.values(formData).filter(Boolean); return validValues.length >= 2; }; /** * Generates a form layout based on record data and localization strings. * * @param {HTMLFormElement} formElement - Target form element. * @param {object} record - Address record, includes at least country code defaulted to FormAutofill.DEFAULT_REGION. * @param {object} l10nStrings - Localization strings map. */ export const createFormLayoutFromRecord = ( formElement, record = { country: lazy.FormAutofill.DEFAULT_REGION }, l10nStrings = {} ) => { // Always clear select values because they are not persisted between countries. // For example from US with state NY, we don't want the address-level1 to be NY // when changing to another country that doesn't have state options const selects = formElement.querySelectorAll("select:not(#country)"); for (const select of selects) { select.value = ""; } // Get old data to persist before clearing form const formData = getCurrentFormData(); record = { ...record, ...formData, }; formElement.innerHTML = ""; const fields = lazy.FormAutofillUtils.getFormLayout(record); const layoutGenerator = convertLayoutToUI(fields, l10nStrings); for (const fieldElement of layoutGenerator) { formElement.appendChild(fieldElement); } document.querySelector("#country").addEventListener( "change", ev => // Allow some time for the user to type // before we set the new country and re-render setTimeout(() => { record.country = ev.target.value; createFormLayoutFromRecord(formElement, record, l10nStrings); }, 300), { once: true } ); // Used to notify tests that the form has been updated and is ready window.dispatchEvent(new CustomEvent("FormReadyForTests")); }; PK !<|( ( $chrome/content/autofillEditForms.mjs/* 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/. */ /* eslint-disable mozilla/balanced-listeners */ // Not relevant since the document gets unloaded. const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { FormAutofillUtils: "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 */ handleInput(_e) {} /** * 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) {} } export 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( lazy.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 && lazy.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 && !lazy.FormAutofillUtils.isCCNumber(field.value) ) { let invalidCardNumberString = this._elements.invalidCardNumberStringElement.textContent; field.setCustomValidity(invalidCardNumberString || " "); } } } PK !
PK !
PK !<2naachrome/content/editDialog.mjs/* 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/. */ /* eslint-disable mozilla/balanced-listeners */ // Not relevant since the document gets unloaded. import { getCurrentFormData, canSubmitForm, } from "chrome://formautofill/content/addressFormLayout.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { AutofillTelemetry: "resource://gre/modules/shared/AutofillTelemetry.sys.mjs", formAutofillStorage: "resource://autofill/FormAutofillStorage.sys.mjs", }); class AutofillEditDialog { constructor(subStorageName, elements, record) { this._storageInitPromise = lazy.formAutofillStorage.initialize(); this._subStorageName = subStorageName; this._elements = elements; this._record = record; this.localizeDocument(); window.addEventListener("load", this, { once: true }); } async init() { this.updateSaveButtonState(); this.attachEventListeners(); // For testing only: signal to tests that the dialog is ready for testing. // This is likely no longer needed since retrieving from storage is fully // handled in manageDialog.js now. window.dispatchEvent(new CustomEvent("FormReadyForTests")); } /** * Get storage and ensure it has been initialized. * * @returns {object} */ async getStorage() { await this._storageInitPromise; return lazy.formAutofillStorage[this._subStorageName]; } /** * Asks FormAutofillParent to save or update an record. * * @param {object} record * @param {string} guid [optional] */ async saveRecord(record, guid) { let storage = await this.getStorage(); if (guid) { await storage.update(guid, record); } else { await storage.add(record); } } /** * Handle events * * @param {DOMEvent} event */ handleEvent(event) { switch (event.type) { case "load": { this.init(); break; } case "click": { this.handleClick(event); break; } case "input": { this.handleInput(event); break; } case "keypress": { this.handleKeyPress(event); break; } case "contextmenu": { if ( !HTMLInputElement.isInstance(event.target) && !HTMLTextAreaElement.isInstance(event.target) ) { event.preventDefault(); } break; } } } /** * Handle click events * * @param {DOMEvent} event */ handleClick(event) { if (event.target == this._elements.cancel) { window.close(); } if (event.target == this._elements.save) { this.handleSubmit(); } } /** * Handle input events */ handleInput(_e) { this.updateSaveButtonState(); } /** * Handle key press events * * @param {DOMEvent} event */ handleKeyPress(event) { if (event.keyCode == KeyEvent.DOM_VK_ESCAPE) { window.close(); } } updateSaveButtonState() { // Toggle disabled attribute on the save button based on // whether the form is filled or empty. if (!Object.keys(this._elements.fieldContainer.buildFormObject()).length) { this._elements.save.setAttribute("disabled", true); } else { this._elements.save.removeAttribute("disabled"); } } /** * Attach event listener */ attachEventListeners() { window.addEventListener("keypress", this); window.addEventListener("contextmenu", this); this._elements.save.addEventListener("click", this); this._elements.cancel.addEventListener("click", this); document.addEventListener("input", this); } // An interface to be inherited. localizeDocument() {} recordFormSubmit() { let method = this._record?.guid ? "edit" : "add"; lazy.AutofillTelemetry.recordManageEvent(this.telemetryType, method); } } export class EditAddressDialog extends AutofillEditDialog { telemetryType = lazy.AutofillTelemetry.ADDRESS; constructor(elements, record) { super("addresses", elements, record); if (record) { lazy.AutofillTelemetry.recordManageEvent( this.telemetryType, "show_entry" ); } } localizeDocument() { if (this._record?.guid) { document.l10n.setAttributes( this._elements.title, "autofill-edit-address-title" ); } } updateSaveButtonState() { // Toggle disabled attribute on the save button based on // whether the form is filled or empty. if (!canSubmitForm()) { this._elements.save.setAttribute("disabled", true); } else { this._elements.save.removeAttribute("disabled"); } } async handleSubmit() { await this.saveRecord( getCurrentFormData(), this._record ? this._record.guid : null ); this.recordFormSubmit(); window.close(); } } export class EditCreditCardDialog extends AutofillEditDialog { telemetryType = lazy.AutofillTelemetry.CREDIT_CARD; constructor(elements, record) { elements.fieldContainer._elements.billingAddress.disabled = true; super("creditCards", elements, record); elements.fieldContainer._elements.ccNumber.addEventListener( "blur", this._onCCNumberFieldBlur.bind(this) ); if (record) { lazy.AutofillTelemetry.recordManageEvent( this.telemetryType, "show_entry" ); } } _onCCNumberFieldBlur() { let elem = this._elements.fieldContainer._elements.ccNumber; this._elements.fieldContainer.updateCustomValidity(elem); } localizeDocument() { if (this._record?.guid) { document.l10n.setAttributes( this._elements.title, "autofill-edit-card-title2" ); } } async handleSubmit() { let creditCard = this._elements.fieldContainer.buildFormObject(); if (!this._elements.fieldContainer._elements.form.reportValidity()) { return; } try { await this.saveRecord( creditCard, this._record ? this._record.guid : null ); this.recordFormSubmit(); window.close(); } catch (ex) { console.error(ex); } } } PK !<:--chrome/content/formautofill.css/* 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/. */ #PopupAutoComplete { &[resultstyles~="autofill"] { min-width: 150px !important; } > richlistbox > richlistitem { &[originaltype="autofill"] { display: block; margin: 0; padding: 0; height: auto; min-height: auto; /* Treat @collpased="true" as display: none similar to how it is for XUL elements. * https://developer.mozilla.org/en-US/docs/Web/CSS/visibility#Values */ &[collapsed="true"] { display: none; } } &[disabled="true"] { opacity: 0.5; } } } /* Form Autofill Doorhanger */ #autofill-address-notification popupnotificationcontent > .desc-message-box, #autofill-credit-card-notification popupnotificationcontent > .desc-message-box { margin-block-end: 12px; } #autofill-credit-card-notification popupnotificationcontent > .desc-message-box > image { -moz-context-properties: fill; fill: currentColor; width: auto; height: auto; list-style-image: url(chrome://formautofill/content/icon-credit-card-generic.svg); } #autofill-address-notification popupnotificationcontent > .desc-message-box > description, #autofill-address-notification popupnotificationcontent > .desc-message-box > additional-description, #autofill-credit-card-notification popupnotificationcontent > .desc-message-box > description { font-style: italic; margin-inline-start: 4px; } PK ! PK ! PK ! PK !<_ $chrome/content/manageAddresses.xhtml