/* 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/. */ /* * Implements a service used to access storage and communicate with content. * * A "fields" array is used to communicate with FormAutofillContent. Each item * represents a single input field in the content page as well as its * @autocomplete properties. The schema is as below. Please refer to * FormAutofillContent.js for more details. * * [ * { * section, * addressType, * contactType, * fieldName, * value, * index * }, * { * // ... * } * ] */ /* exported FormAutofillParent */ "use strict"; this.EXPORTED_SYMBOLS = ["FormAutofillParent"]; const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://formautofill/FormAutofillUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "FormAutofillPreferences", "resource://formautofill/FormAutofillPreferences.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "FormAutofillDoorhanger", "resource://formautofill/FormAutofillDoorhanger.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "MasterPassword", "resource://formautofill/MasterPassword.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow", "resource:///modules/RecentWindow.jsm"); this.log = null; FormAutofillUtils.defineLazyLogGetter(this, this.EXPORTED_SYMBOLS[0]); const { ENABLED_AUTOFILL_ADDRESSES_PREF, ENABLED_AUTOFILL_CREDITCARDS_PREF, CREDITCARDS_COLLECTION_NAME, } = FormAutofillUtils; function FormAutofillParent() { // Lazily load the storage JSM to avoid disk I/O until absolutely needed. // Once storage is loaded we need to update saved field names and inform content processes. XPCOMUtils.defineLazyGetter(this, "profileStorage", () => { let {profileStorage} = Cu.import("resource://formautofill/ProfileStorage.jsm", {}); log.debug("Loading profileStorage"); profileStorage.initialize().then(() => { // Update the saved field names to compute the status and update child processes. this._updateSavedFieldNames(); }); return profileStorage; }); } FormAutofillParent.prototype = { QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]), /** * Cache of the Form Autofill status (considering preferences and storage). */ _active: null, /** * Initializes ProfileStorage and registers the message handler. */ async init() { Services.obs.addObserver(this, "sync-pane-loaded"); Services.ppmm.addMessageListener("FormAutofill:InitStorage", this); Services.ppmm.addMessageListener("FormAutofill:GetRecords", this); Services.ppmm.addMessageListener("FormAutofill:SaveAddress", this); Services.ppmm.addMessageListener("FormAutofill:RemoveAddresses", this); Services.ppmm.addMessageListener("FormAutofill:OpenPreferences", this); Services.mm.addMessageListener("FormAutofill:OnFormSubmit", this); // Observing the pref and storage changes Services.prefs.addObserver(ENABLED_AUTOFILL_ADDRESSES_PREF, this); Services.obs.addObserver(this, "formautofill-storage-changed"); // Only listen to credit card related messages if it is available if (FormAutofillUtils.isAutofillCreditCardsAvailable) { Services.ppmm.addMessageListener("FormAutofill:SaveCreditCard", this); Services.ppmm.addMessageListener("FormAutofill:RemoveCreditCards", this); Services.ppmm.addMessageListener("FormAutofill:GetDecryptedString", this); Services.prefs.addObserver(ENABLED_AUTOFILL_CREDITCARDS_PREF, this); } }, observe(subject, topic, data) { log.debug("observe:", topic, "with data:", data); switch (topic) { case "sync-pane-loaded": { let formAutofillPreferences = new FormAutofillPreferences(); let document = subject.document; let prefGroup = formAutofillPreferences.init(document); let parentNode = document.getElementById("passwordsGroup"); let insertBeforeNode = document.getElementById("masterPasswordRow"); parentNode.insertBefore(prefGroup, insertBeforeNode); break; } case "nsPref:changed": { // Observe pref changes and update _active cache if status is changed. this._updateStatus(); break; } case "formautofill-storage-changed": { // Early exit if only metadata is changed if (data == "notifyUsed") { break; } this._updateSavedFieldNames(); break; } default: { throw new Error(`FormAutofillParent: Unexpected topic observed: ${topic}`); } } }, /** * Broadcast the status to frames when the form autofill status changes. */ _onStatusChanged() { log.debug("_onStatusChanged: Status changed to", this._active); Services.ppmm.broadcastAsyncMessage("FormAutofill:enabledStatus", this._active); // Sync process data autofillEnabled to make sure the value up to date // no matter when the new content process is initialized. Services.ppmm.initialProcessData.autofillEnabled = this._active; }, /** * Query preference and storage status to determine the overall status of the * form autofill feature. * * @returns {boolean} whether form autofill is active (enabled and has data) */ _computeStatus() { const savedFieldNames = Services.ppmm.initialProcessData.autofillSavedFieldNames; return (Services.prefs.getBoolPref(ENABLED_AUTOFILL_ADDRESSES_PREF) || Services.prefs.getBoolPref(ENABLED_AUTOFILL_CREDITCARDS_PREF)) && savedFieldNames && savedFieldNames.size > 0; }, /** * Update the status and trigger _onStatusChanged, if necessary. */ _updateStatus() { let wasActive = this._active; this._active = this._computeStatus(); if (this._active !== wasActive) { this._onStatusChanged(); } }, /** * Handles the message coming from FormAutofillContent. * * @param {string} message.name The name of the message. * @param {object} message.data The data of the message. * @param {nsIFrameMessageManager} message.target Caller's message manager. */ async receiveMessage({name, data, target}) { switch (name) { case "FormAutofill:InitStorage": { this.profileStorage.initialize(); break; } case "FormAutofill:GetRecords": { this._getRecords(data, target); break; } case "FormAutofill:SaveAddress": { if (data.guid) { this.profileStorage.addresses.update(data.guid, data.address); } else { this.profileStorage.addresses.add(data.address); } break; } case "FormAutofill:SaveCreditCard": { await this.profileStorage.creditCards.normalizeCCNumberFields(data.creditcard); this.profileStorage.creditCards.add(data.creditcard); break; } case "FormAutofill:RemoveAddresses": { data.guids.forEach(guid => this.profileStorage.addresses.remove(guid)); break; } case "FormAutofill:RemoveCreditCards": { data.guids.forEach(guid => this.profileStorage.creditCards.remove(guid)); break; } case "FormAutofill:OnFormSubmit": { this._onFormSubmit(data, target); break; } case "FormAutofill:OpenPreferences": { const win = RecentWindow.getMostRecentBrowserWindow(); win.openPreferences("panePrivacy", {origin: "autofillFooter"}); break; } case "FormAutofill:GetDecryptedString": { let {cipherText, reauth} = data; let string; try { string = await MasterPassword.decrypt(cipherText, reauth); } catch (e) { if (e.result != Cr.NS_ERROR_ABORT) { throw e; } log.warn("User canceled master password entry"); } target.sendAsyncMessage("FormAutofill:DecryptedString", string); break; } } }, /** * Uninitializes FormAutofillParent. This is for testing only. * * @private */ _uninit() { this.profileStorage._saveImmediately(); Services.ppmm.removeMessageListener("FormAutofill:InitStorage", this); Services.ppmm.removeMessageListener("FormAutofill:GetRecords", this); Services.ppmm.removeMessageListener("FormAutofill:SaveAddress", this); Services.ppmm.removeMessageListener("FormAutofill:RemoveAddresses", this); Services.obs.removeObserver(this, "sync-pane-loaded"); Services.prefs.removeObserver(ENABLED_AUTOFILL_ADDRESSES_PREF, this); if (FormAutofillUtils.isAutofillCreditCardsAvailable) { Services.ppmm.removeMessageListener("FormAutofill:SaveCreditCard", this); Services.ppmm.removeMessageListener("FormAutofill:RemoveCreditCards", this); Services.ppmm.removeMessageListener("FormAutofill:GetDecryptedString", this); Services.prefs.removeObserver(ENABLED_AUTOFILL_CREDITCARDS_PREF, this); } }, /** * Get the records from profile store and return results back to content * process. It will decrypt the credit card number and append * "cc-number-decrypted" to each record if MasterPassword isn't set. * * @private * @param {string} data.collectionName * The name used to specify which collection to retrieve records. * @param {string} data.searchString * The typed string for filtering out the matched records. * @param {string} data.info * The input autocomplete property's information. * @param {nsIFrameMessageManager} target * Content's message manager. */ async _getRecords({collectionName, searchString, info}, target) { let collection = this.profileStorage[collectionName]; if (!collection) { target.sendAsyncMessage("FormAutofill:Records", []); return; } let recordsInCollection = collection.getAll(); if (!info || !info.fieldName || !recordsInCollection.length) { target.sendAsyncMessage("FormAutofill:Records", recordsInCollection); return; } let isCCAndMPEnabled = collectionName == CREDITCARDS_COLLECTION_NAME && MasterPassword.isEnabled; // We don't filter "cc-number" when MasterPassword is set. if (isCCAndMPEnabled && info.fieldName == "cc-number") { recordsInCollection = recordsInCollection.filter(record => !!record["cc-number"]); target.sendAsyncMessage("FormAutofill:Records", recordsInCollection); return; } let records = []; let lcSearchString = searchString.toLowerCase(); for (let record of recordsInCollection) { let fieldValue = record[info.fieldName]; if (!fieldValue) { continue; } // Cache the decrypted "cc-number" in each record for content to preview // when MasterPassword isn't set. if (!isCCAndMPEnabled && record["cc-number-encrypted"]) { record["cc-number-decrypted"] = await MasterPassword.decrypt(record["cc-number-encrypted"]); } // Filter "cc-number" based on the decrypted one. if (info.fieldName == "cc-number") { fieldValue = record["cc-number-decrypted"]; } if (!lcSearchString || String(fieldValue).toLowerCase().startsWith(lcSearchString)) { records.push(record); } } target.sendAsyncMessage("FormAutofill:Records", records); }, _updateSavedFieldNames() { log.debug("_updateSavedFieldNames"); if (!Services.ppmm.initialProcessData.autofillSavedFieldNames) { Services.ppmm.initialProcessData.autofillSavedFieldNames = new Set(); } else { Services.ppmm.initialProcessData.autofillSavedFieldNames.clear(); } ["addresses", "creditCards"].forEach(c => { this.profileStorage[c].getAll().forEach((record) => { Object.keys(record).forEach((fieldName) => { if (!record[fieldName]) { return; } Services.ppmm.initialProcessData.autofillSavedFieldNames.add(fieldName); }); }); }); // Remove the internal guid and metadata fields. this.profileStorage.INTERNAL_FIELDS.forEach((fieldName) => { Services.ppmm.initialProcessData.autofillSavedFieldNames.delete(fieldName); }); Services.ppmm.broadcastAsyncMessage("FormAutofill:savedFieldNames", Services.ppmm.initialProcessData.autofillSavedFieldNames); this._updateStatus(); }, _onAddressSubmit(address, target, timeStartedFillingMS) { if (address.guid) { // Avoid updating the fields that users don't modify. let originalAddress = this.profileStorage.addresses.get(address.guid); for (let field in address.record) { if (address.untouchedFields.includes(field) && originalAddress[field]) { address.record[field] = originalAddress[field]; } } if (!this.profileStorage.addresses.mergeIfPossible(address.guid, address.record)) { this._recordFormFillingTime("address", "autofill-update", timeStartedFillingMS); FormAutofillDoorhanger.show(target, "update").then((state) => { let changedGUIDs = this.profileStorage.addresses.mergeToStorage(address.record); switch (state) { case "create": if (!changedGUIDs.length) { changedGUIDs.push(this.profileStorage.addresses.add(address.record)); } break; case "update": if (!changedGUIDs.length) { this.profileStorage.addresses.update(address.guid, address.record, true); changedGUIDs.push(address.guid); } else { this.profileStorage.addresses.remove(address.guid); } break; } changedGUIDs.forEach(guid => this.profileStorage.addresses.notifyUsed(guid)); }); // Address should be updated Services.telemetry.scalarAdd("formautofill.addresses.fill_type_autofill_update", 1); return; } this._recordFormFillingTime("address", "autofill", timeStartedFillingMS); this.profileStorage.addresses.notifyUsed(address.guid); // Address is merged successfully Services.telemetry.scalarAdd("formautofill.addresses.fill_type_autofill", 1); } else { let changedGUIDs = this.profileStorage.addresses.mergeToStorage(address.record); if (!changedGUIDs.length) { changedGUIDs.push(this.profileStorage.addresses.add(address.record)); } changedGUIDs.forEach(guid => this.profileStorage.addresses.notifyUsed(guid)); this._recordFormFillingTime("address", "manual", timeStartedFillingMS); // Show first time use doorhanger if (Services.prefs.getBoolPref("extensions.formautofill.firstTimeUse")) { Services.prefs.setBoolPref("extensions.formautofill.firstTimeUse", false); FormAutofillDoorhanger.show(target, "firstTimeUse").then((state) => { if (state !== "open-pref") { return; } target.ownerGlobal.openPreferences("panePrivacy", {origin: "autofillDoorhanger"}); }); } else { // We want to exclude the first time form filling. Services.telemetry.scalarAdd("formautofill.addresses.fill_type_manual", 1); } } }, async _onCreditCardSubmit(creditCard, target, timeStartedFillingMS) { // We'll show the credit card doorhanger if: // - User applys autofill and changed // - User fills form manually if (creditCard.guid && Object.keys(creditCard.record).every(key => creditCard.untouchedFields.includes(key))) { // Add probe to record credit card autofill(without modification). Services.telemetry.scalarAdd("formautofill.creditCards.fill_type_autofill", 1); this._recordFormFillingTime("creditCard", "autofill", timeStartedFillingMS); return; } // Add the probe to record credit card manual filling or autofill but modified case. if (creditCard.guid) { Services.telemetry.scalarAdd("formautofill.creditCards.fill_type_autofill_modified", 1); this._recordFormFillingTime("creditCard", "autofill-update", timeStartedFillingMS); } else { Services.telemetry.scalarAdd("formautofill.creditCards.fill_type_manual", 1); this._recordFormFillingTime("creditCard", "manual", timeStartedFillingMS); } let state = await FormAutofillDoorhanger.show(target, "creditCard"); if (state == "cancel") { return; } if (state == "disable") { Services.prefs.setBoolPref("extensions.formautofill.creditCards.enabled", false); return; } try { await this.profileStorage.creditCards.normalizeCCNumberFields(creditCard.record); this.profileStorage.creditCards.add(creditCard.record); } catch (e) { if (e.result != Cr.NS_ERROR_ABORT) { throw e; } log.warn("User canceled master password entry"); } }, _onFormSubmit(data, target) { let {profile: {address, creditCard}, timeStartedFillingMS} = data; if (address) { this._onAddressSubmit(address, target, timeStartedFillingMS); } if (creditCard) { this._onCreditCardSubmit(creditCard, target, timeStartedFillingMS); } }, /** * Set the probes for the filling time with specific filling type and form type. * * @private * @param {string} formType * 3 type of form (address/creditcard/address-creditcard). * @param {string} fillingType * 3 filling type (manual/autofill/autofill-update). * @param {int|null} startedFillingMS * Time that form started to filling in ms. Early return if start time is null. */ _recordFormFillingTime(formType, fillingType, startedFillingMS) { if (!startedFillingMS) { return; } let histogram = Services.telemetry.getKeyedHistogramById("FORM_FILLING_REQUIRED_TIME_MS"); histogram.add(`${formType}-${fillingType}`, Date.now() - startedFillingMS); }, };