Source code
Revision control
Copy as Markdown
Other Tools
/* 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
/*
* Storage of Form Autofill passport records.
*
* Unlike addresses and credit cards (kept in autofill-profiles.json), passports
* are persisted in the Application Services `autofill` SQLite store. This module
* adapts that store to the same collection API the JSON-backed collections
* expose (`AutofillRecords` in FormAutofillStorageBase.sys.mjs), so callers use
* `gFormAutofillStorage.passports` without knowing the backend.
*/
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
AutofillDataTypes: "resource://gre/modules/shared/AutofillDataTypes.sys.mjs",
NoSuchRecord:
"moz-src:///toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustAutofill.sys.mjs",
PassportRecord: "resource://gre/modules/shared/PassportRecord.sys.mjs",
UpdatablePassportFields:
"moz-src:///toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustAutofill.sys.mjs",
});
// The collection name this store backs, from the data-type registry.
ChromeUtils.defineLazyGetter(
lazy,
"collectionName",
() =>
lazy.AutofillDataTypes.get(lazy.AutofillDataTypes.PASSPORT).collectionName
);
// The essential passport fields persisted on the Application Services
// `Passport`/`UpdatablePassportFields` struct. Any other fields a record
// carries (the split name parts and combined dates) are computed by
// PassportRecord and never stored.
export const VALID_PASSPORT_FIELDS = [
"passport-name",
"passport-country",
"passport-number",
"passport-issue-date-month",
"passport-issue-date-day",
"passport-issue-date-year",
"passport-expiry-date-month",
"passport-expiry-date-day",
"passport-expiry-date-year",
];
/**
* Convert a Form Autofill record into the `UpdatablePassportFields` the AS store
* expects. Every field is required on the Rust side, so missing values fall back
* to an empty string / 0.
*
* The record is normalized first so a combined `passport-{issue,expiry}-date`
* field is collapsed into the month/day/year components the store persists.
*
* @param {object} record A Form Autofill passport record.
* @returns {UpdatablePassportFields}
*/
function recordToUpdatablePassportFields(record) {
// Work on a clone so normalization doesn't mutate the caller's object.
record = { ...record };
lazy.PassportRecord.normalizeFields(record);
// Every field is required on the Rust side, so a missing string falls back to
// "" and a missing date component to 0.
return new lazy.UpdatablePassportFields({
name: record["passport-name"] ?? "",
country: record["passport-country"] ?? "",
passportNumber: record["passport-number"] ?? "",
issueDateMonth: Number(record["passport-issue-date-month"] ?? 0),
issueDateDay: Number(record["passport-issue-date-day"] ?? 0),
issueDateYear: Number(record["passport-issue-date-year"] ?? 0),
expiryDateMonth: Number(record["passport-expiry-date-month"] ?? 0),
expiryDateDay: Number(record["passport-expiry-date-day"] ?? 0),
expiryDateYear: Number(record["passport-expiry-date-year"] ?? 0),
});
}
/**
* Convert a `Passport` returned by the AS store into a Form Autofill record.
*
* @param {Passport} passport
* @returns {object}
*/
function passportToRecord(passport) {
// The AS store keeps date components as integers (0 means unset), but Form
// Autofill records use strings, so callers can compare and fill uniformly
// with the other autofill types. Map 0 back to "".
const dateComponent = value => (value ? String(value) : "");
const record = {
guid: passport.guid,
"passport-name": passport.name,
"passport-country": passport.country,
"passport-number": passport.passportNumber,
"passport-issue-date-month": dateComponent(passport.issueDateMonth),
"passport-issue-date-day": dateComponent(passport.issueDateDay),
"passport-issue-date-year": dateComponent(passport.issueDateYear),
"passport-expiry-date-month": dateComponent(passport.expiryDateMonth),
"passport-expiry-date-day": dateComponent(passport.expiryDateDay),
"passport-expiry-date-year": dateComponent(passport.expiryDateYear),
// The AS metadata already follows the Form Autofill naming convention.
timeCreated: passport.timeCreated,
timeLastUsed: passport.timeLastUsed,
timeLastModified: passport.timeLastModified,
timesUsed: passport.timesUsed,
};
// Derive the combined date fields so callers filling a single date input have
// them available.
lazy.PassportRecord.computeFields(record);
return record;
}
/**
* Passport collection backed by the Application Services `autofill` store.
* Mirrors the public API of `AutofillRecords` for the operations passport
* storage needs.
*/
export class Passports {
/** @type {Promise<Store>} */
#storePromise;
/**
* @param {Promise<Store>} rustStore The shared Application Services `autofill`
* store, owned by FormAutofillStorageBase#rustStore and opened off the main
* thread. The AS store is local-only and keeps passports in plaintext, so no
* encryption key or further setup is required here.
*/
constructor(rustStore) {
this.#storePromise = rustStore;
}
/**
* @returns {Promise<Store>} The shared, already-opening AS autofill store.
*/
#ensureStore() {
return this.#storePromise;
}
/**
* Open the store eagerly. Provided for parity with the JSON-backed
* collections; callers may also just use the collection directly.
*
* @returns {Promise}
*/
async initialize() {
await this.#ensureStore();
}
async add(record) {
const store = await this.#ensureStore();
const passport = await store.addPassport(
recordToUpdatablePassportFields(record)
);
Services.obs.notifyObservers(
{
wrappedJSObject: {
guid: passport.guid,
collectionName: lazy.collectionName,
},
},
"formautofill-storage-changed",
"add"
);
return passport.guid;
}
async update(guid, record, preserveOldProperties = false) {
// The AS store's UPDATE silently matches no rows for an unknown guid; throw
// here so callers see the same error as the JSON-backed collections.
const existing = await this.get(guid);
if (!existing) {
throw new Error("No matching record.");
}
const merged = preserveOldProperties ? { ...existing, ...record } : record;
const store = await this.#ensureStore();
await store.updatePassport(guid, recordToUpdatablePassportFields(merged));
Services.obs.notifyObservers(
{
wrappedJSObject: {
guid,
collectionName: lazy.collectionName,
},
},
"formautofill-storage-changed",
"update"
);
}
async get(guid) {
try {
const store = await this.#ensureStore();
return passportToRecord(await store.getPassport(guid));
} catch (e) {
if (e instanceof lazy.NoSuchRecord) {
return null;
}
throw e;
}
}
async getAll() {
const store = await this.#ensureStore();
const passports = await store.getAllPassports();
return passports.map(passportToRecord);
}
/**
* Yield stored passports that are duplicates of `record`. The Application
* Services store has no unique key beyond the passport number, so a record is
* treated as a duplicate when it shares that number.
*
* @param {object} record A Form Autofill passport record.
* @yields {object} Each stored record sharing the passport number.
*/
async *getDuplicateRecords(record) {
const number = record["passport-number"]?.toLowerCase();
if (!number) {
return;
}
for (const existing of await this.getAll()) {
if (existing["passport-number"]?.toLowerCase() == number) {
yield existing;
}
}
}
/**
* Yield stored passports that match `record`. The Application Services store
* exposes no richer match semantics than the passport-number dedupe, so this
* mirrors `getDuplicateRecords`.
*
* @param {object} record A Form Autofill passport record.
* @yields {object} Each matching stored record.
*/
async *getMatchRecords(record) {
yield* this.getDuplicateRecords(record);
}
async notifyUsed(guid) {
const store = await this.#ensureStore();
await store.touchPassport(guid);
Services.obs.notifyObservers(
{
wrappedJSObject: {
guid,
collectionName: lazy.collectionName,
},
},
"formautofill-storage-changed",
"notifyUsed"
);
}
async remove(guid) {
const store = await this.#ensureStore();
await store.deletePassport(guid);
Services.obs.notifyObservers(
{
wrappedJSObject: {
guid,
collectionName: lazy.collectionName,
},
},
"formautofill-storage-changed",
"remove"
);
}
async isEmpty() {
const store = await this.#ensureStore();
return (await store.countAllPassports()) == 0;
}
async getSavedFieldNames() {
const records = await this.getAll();
const fieldNames = new Set();
for (const record of records) {
for (const fieldName of VALID_PASSPORT_FIELDS) {
if (record[fieldName]) {
fieldNames.add(fieldName);
}
}
}
return fieldNames;
}
async removeAll() {
const store = await this.#ensureStore();
const passports = await store.getAllPassports();
for (const { guid } of passports) {
await store.deletePassport(guid);
}
Services.obs.notifyObservers(
{
wrappedJSObject: {
collectionName: lazy.collectionName,
},
},
"formautofill-storage-changed",
"removeAll"
);
}
}