diff options
Diffstat (limited to 'toolkit/components/satchel/megalist/aggregator/datasources/BankCardDataSource.sys.mjs')
-rw-r--r-- | toolkit/components/satchel/megalist/aggregator/datasources/BankCardDataSource.sys.mjs | 339 |
1 files changed, 339 insertions, 0 deletions
diff --git a/toolkit/components/satchel/megalist/aggregator/datasources/BankCardDataSource.sys.mjs b/toolkit/components/satchel/megalist/aggregator/datasources/BankCardDataSource.sys.mjs new file mode 100644 index 0000000000..06266a7979 --- /dev/null +++ b/toolkit/components/satchel/megalist/aggregator/datasources/BankCardDataSource.sys.mjs @@ -0,0 +1,339 @@ +/* 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/. */ + +import { DataSourceBase } from "resource://gre/modules/megalist/aggregator/datasources/DataSourceBase.sys.mjs"; +import { CreditCardRecord } from "resource://gre/modules/shared/CreditCardRecord.sys.mjs"; +import { formAutofillStorage } from "resource://autofill/FormAutofillStorage.sys.mjs"; +import { OSKeyStore } from "resource://gre/modules/OSKeyStore.sys.mjs"; + +async function decryptCard(card) { + if (card["cc-number-encrypted"] && !card["cc-number-decrypted"]) { + try { + card["cc-number-decrypted"] = await OSKeyStore.decrypt( + card["cc-number-encrypted"], + false + ); + card["cc-number"] = card["cc-number-decrypted"]; + } catch (e) { + console.error(e); + } + } +} + +async function updateCard(card, field, value) { + try { + await decryptCard(card); + const newCard = { + ...card, + [field]: value ?? "", + }; + formAutofillStorage.INTERNAL_FIELDS.forEach(name => delete newCard[name]); + formAutofillStorage.creditCards.VALID_COMPUTED_FIELDS.forEach( + name => delete newCard[name] + ); + delete newCard["cc-number-decrypted"]; + CreditCardRecord.normalizeFields(newCard); + + if (card.guid) { + await formAutofillStorage.creditCards.update(card.guid, newCard); + } else { + await formAutofillStorage.creditCards.add(newCard); + } + } catch (error) { + //todo + console.error("failed to modify credit card", error); + return false; + } + + return true; +} + +/** + * Data source for Bank Cards. + * + * Each card is represented by 3 lines: card number, expiration date and holder name. + * + * Protypes are used to reduce memory need because for different records + * similar lines will differ in values only. + */ +export class BankCardDataSource extends DataSourceBase { + #cardNumberPrototype; + #expirationPrototype; + #holderNamePrototype; + #cardsDisabledMessage; + #enabled; + #header; + + constructor(...args) { + super(...args); + // Wait for Fluent to provide strings before loading data + this.formatMessages( + "payments-section-label", + "card-number-label", + "card-expiration-label", + "card-holder-label", + "command-copy", + "command-reveal", + "command-conceal", + "payments-disabled", + "command-delete", + "command-edit", + "payments-command-create" + ).then( + ([ + headerLabel, + numberLabel, + expirationLabel, + holderLabel, + copyCommandLabel, + revealCommandLabel, + concealCommandLabel, + cardsDisabled, + deleteCommandLabel, + editCommandLabel, + cardsCreateCommandLabel, + ]) => { + const copyCommand = { id: "Copy", label: copyCommandLabel }; + const editCommand = { + id: "Edit", + label: editCommandLabel, + verify: true, + }; + const deleteCommand = { + id: "Delete", + label: deleteCommandLabel, + verify: true, + }; + this.#cardsDisabledMessage = cardsDisabled; + this.#header = this.createHeaderLine(headerLabel); + this.#header.commands.push({ + id: "Create", + label: cardsCreateCommandLabel, + }); + this.#cardNumberPrototype = this.prototypeDataLine({ + label: { value: numberLabel }, + concealed: { value: true, writable: true }, + start: { value: true }, + value: { + async get() { + if (this.editingValue !== undefined) { + return this.editingValue; + } + + if (this.concealed) { + return ( + "••••••••" + + this.record["cc-number"].replaceAll("*", "").substr(-4) + ); + } + + await decryptCard(this.record); + return this.record["cc-number-decrypted"]; + }, + }, + valueIcon: { + get() { + const typeToImage = { + amex: "third-party/cc-logo-amex.png", + cartebancaire: "third-party/cc-logo-cartebancaire.png", + diners: "third-party/cc-logo-diners.svg", + discover: "third-party/cc-logo-discover.png", + jcb: "third-party/cc-logo-jcb.svg", + mastercard: "third-party/cc-logo-mastercard.svg", + mir: "third-party/cc-logo-mir.svg", + unionpay: "third-party/cc-logo-unionpay.svg", + visa: "third-party/cc-logo-visa.svg", + }; + return ( + "chrome://formautofill/content/" + + (typeToImage[this.record["cc-type"]] ?? + "icon-credit-card-generic.svg") + ); + }, + }, + commands: { + get() { + const commands = [ + { id: "Conceal", label: concealCommandLabel }, + { ...copyCommand, verify: true }, + editCommand, + "-", + deleteCommand, + ]; + if (this.concealed) { + commands[0] = { + id: "Reveal", + label: revealCommandLabel, + verify: true, + }; + } + return commands; + }, + }, + executeReveal: { + value() { + this.concealed = false; + this.refreshOnScreen(); + }, + }, + executeConceal: { + value() { + this.concealed = true; + this.refreshOnScreen(); + }, + }, + executeCopy: { + async value() { + await decryptCard(this.record); + this.copyToClipboard(this.record["cc-number-decrypted"]); + }, + }, + executeEdit: { + async value() { + await decryptCard(this.record); + this.editingValue = this.record["cc-number-decrypted"] ?? ""; + this.refreshOnScreen(); + }, + }, + executeSave: { + async value(value) { + if (updateCard(this.record, "cc-number", value)) { + this.executeCancel(); + } + }, + }, + }); + this.#expirationPrototype = this.prototypeDataLine({ + label: { value: expirationLabel }, + value: { + get() { + return `${this.record["cc-exp-month"]}/${this.record["cc-exp-year"]}`; + }, + }, + commands: { + value: [copyCommand, editCommand, "-", deleteCommand], + }, + }); + this.#holderNamePrototype = this.prototypeDataLine({ + label: { value: holderLabel }, + end: { value: true }, + value: { + get() { + return this.editingValue ?? this.record["cc-name"]; + }, + }, + commands: { + value: [copyCommand, editCommand, "-", deleteCommand], + }, + executeEdit: { + value() { + this.editingValue = this.record["cc-name"] ?? ""; + this.refreshOnScreen(); + }, + }, + executeSave: { + async value(value) { + if (updateCard(this.record, "cc-name", value)) { + this.executeCancel(); + } + }, + }, + }); + + Services.obs.addObserver(this, "formautofill-storage-changed"); + Services.prefs.addObserver( + "extensions.formautofill.creditCards.enabled", + this + ); + this.#reloadDataSource(); + } + ); + } + + /** + * Enumerate all the lines provided by this data source. + * + * @param {string} searchText used to filter data + */ + *enumerateLines(searchText) { + if (this.#enabled === undefined) { + // Async Fluent API makes it possible to have data source waiting + // for the localized strings, which can be detected by undefined in #enabled. + return; + } + + yield this.#header; + if (this.#header.collapsed || !this.#enabled) { + return; + } + + const stats = { count: 0, total: 0 }; + searchText = searchText.toUpperCase(); + yield* this.enumerateLinesForMatchingRecords( + searchText, + stats, + card => + (card["cc-number-decrypted"] || card["cc-number"]) + .toUpperCase() + .includes(searchText) || + `${card["cc-exp-month"]}/${card["cc-exp-year"]}` + .toUpperCase() + .includes(searchText) || + card["cc-name"].toUpperCase().includes(searchText) + ); + + this.formatMessages({ + id: + stats.count == stats.total + ? "payments-count" + : "payments-filtered-count", + args: stats, + }).then(([headerLabel]) => { + this.#header.value = headerLabel; + }); + } + + /** + * Sync lines array with the actual data source. + * This function reads all cards from the storage, adds or updates lines and + * removes lines for the removed cards. + */ + async #reloadDataSource() { + this.#enabled = Services.prefs.getBoolPref( + "extensions.formautofill.creditCards.enabled" + ); + if (!this.#enabled) { + this.#reloadEmptyDataSource(); + return; + } + + await formAutofillStorage.initialize(); + const cards = await formAutofillStorage.creditCards.getAll(); + this.beforeReloadingDataSource(); + cards.forEach(card => { + const lineId = `${card["cc-name"]}:${card.guid}`; + + this.addOrUpdateLine(card, lineId + "0", this.#cardNumberPrototype); + this.addOrUpdateLine(card, lineId + "1", this.#expirationPrototype); + this.addOrUpdateLine(card, lineId + "2", this.#holderNamePrototype); + }); + this.afterReloadingDataSource(); + } + + #reloadEmptyDataSource() { + this.lines.length = 0; + //todo: user can enable credit cards by activating header line + this.#header.value = this.#cardsDisabledMessage; + this.refreshAllLinesOnScreen(); + } + + observe(_subj, topic, message) { + if ( + topic == "formautofill-storage-changed" || + message == "extensions.formautofill.creditCards.enabled" + ) { + this.#reloadDataSource(); + } + } +} |