/* 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 */ "use strict"; const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; const AUTOFILL_BUNDLE_URI = "chrome://formautofill/locale/formautofill.properties"; const REGIONS_BUNDLE_URI = "chrome://global/locale/regionNames.properties"; Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://formautofill/FormAutofillUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "profileStorage", "resource://formautofill/ProfileStorage.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "MasterPassword", "resource://formautofill/MasterPassword.jsm"); class EditDialog { constructor(subStorageName, elements, record) { this._storageInitPromise = profileStorage.initialize(); this._subStorageName = subStorageName; this._elements = elements; this._record = record; this.localizeDocument(); window.addEventListener("DOMContentLoaded", this, {once: true}); } async init() { if (this._record) { await this.loadInitialValues(this._record); } this.attachEventListeners(); // For testing only: loadInitialValues for credit card is an async method, and tests // need to wait until the values have been filled before editing the fields. window.dispatchEvent(new CustomEvent("FormReady")); } uninit() { this.detachEventListeners(); this._elements = null; } /** * Fill the form with a record object. * @param {object} record */ loadInitialValues(record) { for (let field in record) { let input = document.getElementById(field); if (input) { input.value = record[field]; } } } /** * Get inputs from the form. * @returns {object} */ buildFormObject() { return Array.from(document.forms[0].elements).reduce((obj, input) => { if (input.value) { obj[input.id] = input.value; } return obj; }, {}); } /** * Get storage and ensure it has been initialized. * @returns {object} */ async getStorage() { await this._storageInitPromise; return profileStorage[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) { storage.update(guid, record); } else { storage.add(record); } } /** * Handle events * * @param {DOMEvent} event */ handleEvent(event) { switch (event.type) { case "DOMContentLoaded": { this.init(); break; } case "unload": { this.uninit(); break; } case "click": { this.handleClick(event); break; } case "input": { this.handleInput(event); break; } case "keypress": { this.handleKeyPress(event); 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 * * @param {DOMEvent} event */ handleInput(event) { // Toggle disabled attribute on the save button based on // whether the form is filled or empty. if (Object.keys(this.buildFormObject()).length == 0) { this._elements.save.setAttribute("disabled", true); } else { this._elements.save.removeAttribute("disabled"); } } /** * Handle key press events * * @param {DOMEvent} event */ handleKeyPress(event) { if (event.keyCode == KeyEvent.DOM_VK_ESCAPE) { window.close(); } } /** * Attach event listener */ attachEventListeners() { window.addEventListener("keypress", this); this._elements.controlsContainer.addEventListener("click", this); document.addEventListener("input", this); } /** * Remove event listener */ detachEventListeners() { window.removeEventListener("keypress", this); this._elements.controlsContainer.removeEventListener("click", this); document.removeEventListener("input", this); } } class EditAddress extends EditDialog { constructor(elements, record) { super("addresses", elements, record); this.formatForm(record && record.country); } /** * 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) { // TODO: Use fmt to show/hide and order fields (Bug 1383687) const {addressLevel1Label, postalCodeLabel} = FormAutofillUtils.getFormFormat(country); this._elements.addressLevel1Label.dataset.localization = addressLevel1Label; this._elements.postalCodeLabel.dataset.localization = postalCodeLabel; FormAutofillUtils.localizeMarkup(AUTOFILL_BUNDLE_URI, document); } localizeDocument() { if (this._record) { this._elements.title.dataset.localization = "editAddressTitle"; } FormAutofillUtils.localizeMarkup(REGIONS_BUNDLE_URI, this._elements.country); } async handleSubmit() { await this.saveRecord(this.buildFormObject(), this._record ? this._record.guid : null); window.close(); } } class EditCreditCard extends EditDialog { constructor(elements, record) { super("creditCards", elements, record); this.generateYears(); } generateYears() { const count = 11; const currentYear = new Date().getFullYear(); const ccExpYear = this._record && this._record["cc-exp-year"]; 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)); } } localizeDocument() { if (this._record) { this._elements.title.dataset.localization = "editCreditCardTitle"; } FormAutofillUtils.localizeMarkup(AUTOFILL_BUNDLE_URI, document); } /** * Decrypt cc-number first and fill the form. * @param {object} creditCard */ async loadInitialValues(creditCard) { let decryptedCC = await MasterPassword.decrypt(creditCard["cc-number-encrypted"]); super.loadInitialValues(Object.assign({}, creditCard, {"cc-number": decryptedCC})); } async handleSubmit() { let creditCard = this.buildFormObject(); // Show error on the cc-number field if it's empty or invalid if (!FormAutofillUtils.isCCNumber(creditCard["cc-number"])) { this._elements.ccNumber.setCustomValidity(true); return; } let storage = await this.getStorage(); await storage.normalizeCCNumberFields(creditCard); await this.saveRecord(creditCard, this._record ? this._record.guid : null); window.close(); } 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); } }