1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5/*
6 * Interface for the storage of Form Autofill.
7 *
8 * The data is stored in JSON format, without indentation and the computed
9 * fields, using UTF-8 encoding. With indentation and computed fields applied,
10 * the schema would look like this:
11 *
12 * {
13 *   version: 1,
14 *   addresses: [
15 *     {
16 *       guid,                 // 12 characters
17 *       version,              // schema version in integer
18 *
19 *       // address fields
20 *       given-name,
21 *       additional-name,
22 *       family-name,
23 *       organization,         // Company
24 *       street-address,       // (Multiline)
25 *       address-level3,       // Suburb/Sublocality
26 *       address-level2,       // City/Town
27 *       address-level1,       // Province (Standardized code if possible)
28 *       postal-code,
29 *       country,              // ISO 3166
30 *       tel,                  // Stored in E.164 format
31 *       email,
32 *
33 *       // computed fields (These fields are computed based on the above fields
34 *       // and are not allowed to be modified directly.)
35 *       name,
36 *       address-line1,
37 *       address-line2,
38 *       address-line3,
39 *       country-name,
40 *       tel-country-code,
41 *       tel-national,
42 *       tel-area-code,
43 *       tel-local,
44 *       tel-local-prefix,
45 *       tel-local-suffix,
46 *
47 *       // metadata
48 *       timeCreated,          // in ms
49 *       timeLastUsed,         // in ms
50 *       timeLastModified,     // in ms
51 *       timesUsed
52 *       _sync: { ... optional sync metadata },
53 *     }
54 *   ],
55 *   creditCards: [
56 *     {
57 *       guid,                 // 12 characters
58 *       version,              // schema version in integer
59 *
60 *       // credit card fields
61 *       billingAddressGUID,   // An optional GUID of an autofill address record
62 *                                which may or may not exist locally.
63 *
64 *       cc-name,
65 *       cc-number,            // will be stored in masked format (************1234)
66 *                             // (see details below)
67 *       cc-exp-month,
68 *       cc-exp-year,          // 2-digit year will be converted to 4 digits
69 *                             // upon saving
70 *       cc-type,              // Optional card network id (instrument type)
71 *
72 *       // computed fields (These fields are computed based on the above fields
73 *       // and are not allowed to be modified directly.)
74 *       cc-given-name,
75 *       cc-additional-name,
76 *       cc-family-name,
77 *       cc-number-encrypted,  // encrypted from the original unmasked "cc-number"
78 *                             // (see details below)
79 *       cc-exp,
80 *
81 *       // metadata
82 *       timeCreated,          // in ms
83 *       timeLastUsed,         // in ms
84 *       timeLastModified,     // in ms
85 *       timesUsed
86 *       _sync: { ... optional sync metadata },
87 *     }
88 *   ]
89 * }
90 *
91 *
92 * Encrypt-related Credit Card Fields (cc-number & cc-number-encrypted):
93 *
94 * When saving or updating a credit-card record, the storage will encrypt the
95 * value of "cc-number", store the encrypted number in "cc-number-encrypted"
96 * field, and replace "cc-number" field with the masked number. These all happen
97 * in "computeFields". We do reverse actions in "_stripComputedFields", which
98 * decrypts "cc-number-encrypted", restores it to "cc-number", and deletes
99 * "cc-number-encrypted". Therefore, calling "_stripComputedFields" followed by
100 * "computeFields" can make sure the encrypt-related fields are up-to-date.
101 *
102 * In general, you have to decrypt the number by your own outside FormAutofillStorage
103 * when necessary. However, you will get the decrypted records when querying
104 * data with "rawData=true" to ensure they're ready to sync.
105 *
106 *
107 * Sync Metadata:
108 *
109 * Records may also have a _sync field, which consists of:
110 * {
111 *   changeCounter,    // integer - the number of changes made since the last
112 *                     // sync.
113 *   lastSyncedFields, // object - hashes of the original values for fields
114 *                     // changed since the last sync.
115 * }
116 *
117 * Records with such a field have previously been synced. Records without such
118 * a field are yet to be synced, so are treated specially in some cases (eg,
119 * they don't need a tombstone, de-duping logic treats them as special etc).
120 * Records without the field are always considered "dirty" from Sync's POV
121 * (meaning they will be synced on the next sync), at which time they will gain
122 * this new field.
123 */
124
125"use strict";
126
127this.EXPORTED_SYMBOLS = [
128  "FormAutofillStorageBase",
129  "CreditCardsBase",
130  "AddressesBase",
131];
132
133const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
134
135const { FormAutofill } = ChromeUtils.import(
136  "resource://autofill/FormAutofill.jsm"
137);
138
139ChromeUtils.defineModuleGetter(
140  this,
141  "CreditCard",
142  "resource://gre/modules/CreditCard.jsm"
143);
144ChromeUtils.defineModuleGetter(
145  this,
146  "FormAutofillNameUtils",
147  "resource://autofill/FormAutofillNameUtils.jsm"
148);
149ChromeUtils.defineModuleGetter(
150  this,
151  "FormAutofillUtils",
152  "resource://autofill/FormAutofillUtils.jsm"
153);
154ChromeUtils.defineModuleGetter(
155  this,
156  "OSKeyStore",
157  "resource://gre/modules/OSKeyStore.jsm"
158);
159ChromeUtils.defineModuleGetter(
160  this,
161  "PhoneNumber",
162  "resource://autofill/phonenumberutils/PhoneNumber.jsm"
163);
164
165const CryptoHash = Components.Constructor(
166  "@mozilla.org/security/hash;1",
167  "nsICryptoHash",
168  "initWithString"
169);
170
171const STORAGE_SCHEMA_VERSION = 1;
172const ADDRESS_SCHEMA_VERSION = 1;
173const CREDIT_CARD_SCHEMA_VERSION = 3;
174
175const VALID_ADDRESS_FIELDS = [
176  "given-name",
177  "additional-name",
178  "family-name",
179  "organization",
180  "street-address",
181  "address-level3",
182  "address-level2",
183  "address-level1",
184  "postal-code",
185  "country",
186  "tel",
187  "email",
188];
189
190const STREET_ADDRESS_COMPONENTS = [
191  "address-line1",
192  "address-line2",
193  "address-line3",
194];
195
196const TEL_COMPONENTS = [
197  "tel-country-code",
198  "tel-national",
199  "tel-area-code",
200  "tel-local",
201  "tel-local-prefix",
202  "tel-local-suffix",
203];
204
205const VALID_ADDRESS_COMPUTED_FIELDS = ["name", "country-name"].concat(
206  STREET_ADDRESS_COMPONENTS,
207  TEL_COMPONENTS
208);
209
210const VALID_CREDIT_CARD_FIELDS = [
211  "billingAddressGUID",
212  "cc-name",
213  "cc-number",
214  "cc-exp-month",
215  "cc-exp-year",
216  "cc-type",
217];
218
219const VALID_CREDIT_CARD_COMPUTED_FIELDS = [
220  "cc-given-name",
221  "cc-additional-name",
222  "cc-family-name",
223  "cc-number-encrypted",
224  "cc-exp",
225];
226
227const INTERNAL_FIELDS = [
228  "guid",
229  "version",
230  "timeCreated",
231  "timeLastUsed",
232  "timeLastModified",
233  "timesUsed",
234];
235
236function sha512(string) {
237  if (string == null) {
238    return null;
239  }
240  let encoder = new TextEncoder("utf-8");
241  let bytes = encoder.encode(string);
242  let hash = new CryptoHash("sha512");
243  hash.update(bytes, bytes.length);
244  return hash.finish(/* base64 */ true);
245}
246
247/**
248 * Class that manipulates records in a specified collection.
249 *
250 * Note that it is responsible for converting incoming data to a consistent
251 * format in the storage. For example, computed fields will be transformed to
252 * the original fields and 2-digit years will be calculated into 4 digits.
253 */
254class AutofillRecords {
255  /**
256   * Creates an AutofillRecords.
257   *
258   * @param {JSONFile} store
259   *        An instance of JSONFile.
260   * @param {string} collectionName
261   *        A key of "store.data".
262   * @param {Array.<string>} validFields
263   *        A list containing non-metadata field names.
264   * @param {Array.<string>} validComputedFields
265   *        A list containing computed field names.
266   * @param {number} schemaVersion
267   *        The schema version for the new record.
268   */
269  constructor(
270    store,
271    collectionName,
272    validFields,
273    validComputedFields,
274    schemaVersion
275  ) {
276    FormAutofill.defineLazyLogGetter(this, "AutofillRecords:" + collectionName);
277
278    this.VALID_FIELDS = validFields;
279    this.VALID_COMPUTED_FIELDS = validComputedFields;
280
281    this._store = store;
282    this._collectionName = collectionName;
283    this._schemaVersion = schemaVersion;
284
285    this._initialize();
286  }
287
288  _initialize() {
289    this._initializePromise = Promise.all(
290      this._data.map(async (record, index) =>
291        this._migrateRecord(record, index)
292      )
293    ).then(hasChangesArr => {
294      let dataHasChanges = hasChangesArr.includes(true);
295      if (dataHasChanges) {
296        this._store.saveSoon();
297      }
298    });
299  }
300
301  /**
302   * Gets the schema version number.
303   *
304   * @returns {number}
305   *          The current schema version number.
306   */
307  get version() {
308    return this._schemaVersion;
309  }
310
311  /**
312   * Gets the data of this collection.
313   *
314   * @returns {array}
315   *          The data object.
316   */
317  get _data() {
318    return this._getData();
319  }
320
321  _getData() {
322    return this._store.data[this._collectionName];
323  }
324
325  // Ensures that we don't try to apply synced records with newer schema
326  // versions. This is a temporary measure to ensure we don't accidentally
327  // bump the schema version without a syncing strategy in place (bug 1377204).
328  _ensureMatchingVersion(record) {
329    if (record.version != this.version) {
330      throw new Error(
331        `Got unknown record version ${record.version}; want ${this.version}`
332      );
333    }
334  }
335
336  /**
337   * Initialize the records in the collection, resolves when the migration completes.
338   * @returns {Promise}
339   */
340  initialize() {
341    return this._initializePromise;
342  }
343
344  /**
345   * Adds a new record.
346   *
347   * @param {Object} record
348   *        The new record for saving.
349   * @param {boolean} [options.sourceSync = false]
350   *        Did sync generate this addition?
351   * @returns {Promise<string>}
352   *          The GUID of the newly added item..
353   */
354  async add(record, { sourceSync = false } = {}) {
355    let recordToSave = this._clone(record);
356
357    if (sourceSync) {
358      // Remove tombstones for incoming items that were changed on another
359      // device. Local deletions always lose to avoid data loss.
360      let index = this._findIndexByGUID(recordToSave.guid, {
361        includeDeleted: true,
362      });
363      if (index > -1) {
364        let existing = this._data[index];
365        if (existing.deleted) {
366          this._data.splice(index, 1);
367        } else {
368          throw new Error(`Record ${recordToSave.guid} already exists`);
369        }
370      }
371    } else if (!recordToSave.deleted) {
372      this._normalizeRecord(recordToSave);
373      // _normalizeRecord shouldn't do any validation (throw) because in the
374      // `update` case it is called with partial records whereas
375      // `_validateFields` is called with a complete one.
376      this._validateFields(recordToSave);
377
378      recordToSave.guid = this._generateGUID();
379      recordToSave.version = this.version;
380
381      // Metadata
382      let now = Date.now();
383      recordToSave.timeCreated = now;
384      recordToSave.timeLastModified = now;
385      recordToSave.timeLastUsed = 0;
386      recordToSave.timesUsed = 0;
387    }
388
389    return this._saveRecord(recordToSave, { sourceSync });
390  }
391
392  async _saveRecord(record, { sourceSync = false } = {}) {
393    if (!record.guid) {
394      throw new Error("Record missing GUID");
395    }
396
397    let recordToSave;
398    if (record.deleted) {
399      if (this._findByGUID(record.guid, { includeDeleted: true })) {
400        throw new Error("a record with this GUID already exists");
401      }
402      recordToSave = {
403        guid: record.guid,
404        timeLastModified: record.timeLastModified || Date.now(),
405        deleted: true,
406      };
407    } else {
408      this._ensureMatchingVersion(record);
409      recordToSave = record;
410      await this.computeFields(recordToSave);
411    }
412
413    if (sourceSync) {
414      let sync = this._getSyncMetaData(recordToSave, true);
415      sync.changeCounter = 0;
416    }
417
418    this._data.push(recordToSave);
419
420    this.updateUseCountTelemetry();
421
422    this._store.saveSoon();
423
424    Services.obs.notifyObservers(
425      {
426        wrappedJSObject: {
427          sourceSync,
428          guid: record.guid,
429          collectionName: this._collectionName,
430        },
431      },
432      "formautofill-storage-changed",
433      "add"
434    );
435    return recordToSave.guid;
436  }
437
438  _generateGUID() {
439    let guid;
440    while (!guid || this._findByGUID(guid)) {
441      guid = Services.uuid
442        .generateUUID()
443        .toString()
444        .replace(/[{}-]/g, "")
445        .substring(0, 12);
446    }
447    return guid;
448  }
449
450  /**
451   * Update the specified record.
452   *
453   * @param  {string} guid
454   *         Indicates which record to update.
455   * @param  {Object} record
456   *         The new record used to overwrite the old one.
457   * @param  {Promise<boolean>} [preserveOldProperties = false]
458   *         Preserve old record's properties if they don't exist in new record.
459   */
460  async update(guid, record, preserveOldProperties = false) {
461    this.log.debug(`update: ${guid}`);
462
463    let recordFoundIndex = this._findIndexByGUID(guid);
464    if (recordFoundIndex == -1) {
465      throw new Error("No matching record.");
466    }
467
468    // Clone the record before modifying it to avoid exposing incomplete changes.
469    let recordFound = this._clone(this._data[recordFoundIndex]);
470    await this._stripComputedFields(recordFound);
471
472    let recordToUpdate = this._clone(record);
473    this._normalizeRecord(recordToUpdate, true);
474
475    let hasValidField = false;
476    for (let field of this.VALID_FIELDS) {
477      let oldValue = recordFound[field];
478      let newValue = recordToUpdate[field];
479
480      // Resume the old field value in the perserve case
481      if (preserveOldProperties && newValue === undefined) {
482        newValue = oldValue;
483      }
484
485      if (newValue === undefined || newValue === "") {
486        delete recordFound[field];
487      } else {
488        hasValidField = true;
489        recordFound[field] = newValue;
490      }
491
492      this._maybeStoreLastSyncedField(recordFound, field, oldValue);
493    }
494
495    if (!hasValidField) {
496      throw new Error("Record contains no valid field.");
497    }
498
499    // _normalizeRecord above is called with the `record` argument provided to
500    // `update` which may not contain all resulting fields when
501    // `preserveOldProperties` is used. This means we need to validate for
502    // missing fields after we compose the record (`recordFound`) with the stored
503    // record like we do in the loop above.
504    this._validateFields(recordFound);
505
506    recordFound.timeLastModified = Date.now();
507    let syncMetadata = this._getSyncMetaData(recordFound);
508    if (syncMetadata) {
509      syncMetadata.changeCounter += 1;
510    }
511
512    await this.computeFields(recordFound);
513    this._data[recordFoundIndex] = recordFound;
514
515    this._store.saveSoon();
516
517    Services.obs.notifyObservers(
518      {
519        wrappedJSObject: {
520          guid,
521          collectionName: this._collectionName,
522        },
523      },
524      "formautofill-storage-changed",
525      "update"
526    );
527  }
528
529  /**
530   * Notifies the storage of the use of the specified record, so we can update
531   * the metadata accordingly. This does not bump the Sync change counter, since
532   * we don't sync `timesUsed` or `timeLastUsed`.
533   *
534   * @param  {string} guid
535   *         Indicates which record to be notified.
536   */
537  notifyUsed(guid) {
538    this.log.debug("notifyUsed:", guid);
539
540    let recordFound = this._findByGUID(guid);
541    if (!recordFound) {
542      throw new Error("No matching record.");
543    }
544
545    recordFound.timesUsed++;
546    recordFound.timeLastUsed = Date.now();
547
548    this.updateUseCountTelemetry();
549
550    this._store.saveSoon();
551    Services.obs.notifyObservers(
552      {
553        wrappedJSObject: {
554          guid,
555          collectionName: this._collectionName,
556        },
557      },
558      "formautofill-storage-changed",
559      "notifyUsed"
560    );
561  }
562
563  updateUseCountTelemetry() {}
564
565  /**
566   * Removes the specified record. No error occurs if the record isn't found.
567   *
568   * @param  {string} guid
569   *         Indicates which record to remove.
570   * @param  {boolean} [options.sourceSync = false]
571   *         Did Sync generate this removal?
572   */
573  remove(guid, { sourceSync = false } = {}) {
574    this.log.debug("remove:", guid);
575
576    if (sourceSync) {
577      this._removeSyncedRecord(guid);
578    } else {
579      let index = this._findIndexByGUID(guid, { includeDeleted: false });
580      if (index == -1) {
581        this.log.warn("attempting to remove non-existing entry", guid);
582        return;
583      }
584      let existing = this._data[index];
585      if (existing.deleted) {
586        return; // already a tombstone - don't touch it.
587      }
588      let existingSync = this._getSyncMetaData(existing);
589      if (existingSync) {
590        // existing sync metadata means it has been synced. This means we must
591        // leave a tombstone behind.
592        this._data[index] = {
593          guid,
594          timeLastModified: Date.now(),
595          deleted: true,
596          _sync: existingSync,
597        };
598        existingSync.changeCounter++;
599      } else {
600        // If there's no sync meta-data, this record has never been synced, so
601        // we can delete it.
602        this._data.splice(index, 1);
603      }
604    }
605
606    this.updateUseCountTelemetry();
607
608    this._store.saveSoon();
609    Services.obs.notifyObservers(
610      {
611        wrappedJSObject: {
612          sourceSync,
613          guid,
614          collectionName: this._collectionName,
615        },
616      },
617      "formautofill-storage-changed",
618      "remove"
619    );
620  }
621
622  /**
623   * Returns the record with the specified GUID.
624   *
625   * @param   {string} guid
626   *          Indicates which record to retrieve.
627   * @param   {boolean} [options.rawData = false]
628   *          Returns a raw record without modifications and the computed fields
629   *          (this includes private fields)
630   * @returns {Promise<Object>}
631   *          A clone of the record.
632   */
633  async get(guid, { rawData = false } = {}) {
634    this.log.debug(`get: ${guid}`);
635
636    let recordFound = this._findByGUID(guid);
637    if (!recordFound) {
638      return null;
639    }
640
641    // The record is cloned to avoid accidental modifications from outside.
642    let clonedRecord = this._cloneAndCleanUp(recordFound);
643    if (rawData) {
644      await this._stripComputedFields(clonedRecord);
645    } else {
646      this._recordReadProcessor(clonedRecord);
647    }
648    return clonedRecord;
649  }
650
651  /**
652   * Returns all records.
653   *
654   * @param   {boolean} [options.rawData = false]
655   *          Returns raw records without modifications and the computed fields.
656   * @param   {boolean} [options.includeDeleted = false]
657   *          Also return any tombstone records.
658   * @returns {Promise<Array.<Object>>}
659   *          An array containing clones of all records.
660   */
661  async getAll({ rawData = false, includeDeleted = false } = {}) {
662    this.log.debug(`getAll. includeDeleted = ${includeDeleted}`);
663
664    let records = this._data.filter(r => !r.deleted || includeDeleted);
665    // Records are cloned to avoid accidental modifications from outside.
666    let clonedRecords = records.map(r => this._cloneAndCleanUp(r));
667    await Promise.all(
668      clonedRecords.map(async record => {
669        if (rawData) {
670          await this._stripComputedFields(record);
671        } else {
672          this._recordReadProcessor(record);
673        }
674      })
675    );
676    return clonedRecords;
677  }
678
679  /**
680   * Return all saved field names in the collection.
681   *
682   * @returns {Promise<Set>} Set containing saved field names.
683   */
684  async getSavedFieldNames() {
685    this.log.debug("getSavedFieldNames");
686
687    let records = this._data.filter(r => !r.deleted);
688    records
689      .map(record => this._cloneAndCleanUp(record))
690      .forEach(record => this._recordReadProcessor(record));
691
692    let fieldNames = new Set();
693    for (let record of records) {
694      for (let fieldName of Object.keys(record)) {
695        if (INTERNAL_FIELDS.includes(fieldName) || !record[fieldName]) {
696          continue;
697        }
698        fieldNames.add(fieldName);
699      }
700    }
701
702    return fieldNames;
703  }
704
705  /**
706   * Functions intended to be used in the support of Sync.
707   */
708
709  /**
710   * Stores a hash of the last synced value for a field in a locally updated
711   * record. We use this value to rebuild the shared parent, or base, when
712   * reconciling incoming records that may have changed on another device.
713   *
714   * Storing the hash of the values that we last wrote to the Sync server lets
715   * us determine if a remote change conflicts with a local change. If the
716   * hashes for the base, current local value, and remote value all differ, we
717   * have a conflict.
718   *
719   * These fields are not themselves synced, and will be removed locally as
720   * soon as we have successfully written the record to the Sync server - so
721   * it is expected they will not remain for long, as changes which cause a
722   * last synced field to be written will itself cause a sync.
723   *
724   * We also skip this for updates made by Sync, for internal fields, for
725   * records that haven't been uploaded yet, and for fields which have already
726   * been changed since the last sync.
727   *
728   * @param   {Object} record
729   *          The updated local record.
730   * @param   {string} field
731   *          The field name.
732   * @param   {string} lastSyncedValue
733   *          The last synced field value.
734   */
735  _maybeStoreLastSyncedField(record, field, lastSyncedValue) {
736    let sync = this._getSyncMetaData(record);
737    if (!sync) {
738      // The record hasn't been uploaded yet, so we can't end up with merge
739      // conflicts.
740      return;
741    }
742    let alreadyChanged = field in sync.lastSyncedFields;
743    if (alreadyChanged) {
744      // This field was already changed multiple times since the last sync.
745      return;
746    }
747    let newValue = record[field];
748    if (lastSyncedValue != newValue) {
749      sync.lastSyncedFields[field] = sha512(lastSyncedValue);
750    }
751  }
752
753  /**
754   * Attempts a three-way merge between a changed local record, an incoming
755   * remote record, and the shared parent that we synthesize from the last
756   * synced fields - see _maybeStoreLastSyncedField.
757   *
758   * @param   {Object} strippedLocalRecord
759   *          The changed local record, currently in storage. Computed fields
760   *          are stripped.
761   * @param   {Object} remoteRecord
762   *          The remote record.
763   * @returns {Object|null}
764   *          The merged record, or `null` if there are conflicts and the
765   *          records can't be merged.
766   */
767  _mergeSyncedRecords(strippedLocalRecord, remoteRecord) {
768    let sync = this._getSyncMetaData(strippedLocalRecord, true);
769
770    // Copy all internal fields from the remote record. We'll update their
771    // values in `_replaceRecordAt`.
772    let mergedRecord = {};
773    for (let field of INTERNAL_FIELDS) {
774      if (remoteRecord[field] != null) {
775        mergedRecord[field] = remoteRecord[field];
776      }
777    }
778
779    for (let field of this.VALID_FIELDS) {
780      let isLocalSame = false;
781      let isRemoteSame = false;
782      if (field in sync.lastSyncedFields) {
783        // If the field has changed since the last sync, compare hashes to
784        // determine if the local and remote values are different. Hashing is
785        // expensive, but we don't expect this to happen frequently.
786        let lastSyncedValue = sync.lastSyncedFields[field];
787        isLocalSame = lastSyncedValue == sha512(strippedLocalRecord[field]);
788        isRemoteSame = lastSyncedValue == sha512(remoteRecord[field]);
789      } else {
790        // Otherwise, if the field hasn't changed since the last sync, we know
791        // it's the same locally.
792        isLocalSame = true;
793        isRemoteSame = strippedLocalRecord[field] == remoteRecord[field];
794      }
795
796      let value;
797      if (isLocalSame && isRemoteSame) {
798        // Local and remote are the same; doesn't matter which one we pick.
799        value = strippedLocalRecord[field];
800      } else if (isLocalSame && !isRemoteSame) {
801        value = remoteRecord[field];
802      } else if (!isLocalSame && isRemoteSame) {
803        // We don't need to bump the change counter when taking the local
804        // change, because the counter must already be > 0 if we're attempting
805        // a three-way merge.
806        value = strippedLocalRecord[field];
807      } else if (strippedLocalRecord[field] == remoteRecord[field]) {
808        // Shared parent doesn't match either local or remote, but the values
809        // are identical, so there's no conflict.
810        value = strippedLocalRecord[field];
811      } else {
812        // Both local and remote changed to different values. We'll need to fork
813        // the local record to resolve the conflict.
814        return null;
815      }
816
817      if (value != null) {
818        mergedRecord[field] = value;
819      }
820    }
821
822    return mergedRecord;
823  }
824
825  /**
826   * Replaces a local record with a remote or merged record, copying internal
827   * fields and Sync metadata.
828   *
829   * @param   {number} index
830   * @param   {Object} remoteRecord
831   * @param   {Promise<boolean>} [options.keepSyncMetadata = false]
832   *          Should we copy Sync metadata? This is true if `remoteRecord` is a
833   *          merged record with local changes that we need to upload. Passing
834   *          `keepSyncMetadata` retains the record's change counter and
835   *          last synced fields, so that we don't clobber the local change if
836   *          the sync is interrupted after the record is merged, but before
837   *          it's uploaded.
838   */
839  async _replaceRecordAt(
840    index,
841    remoteRecord,
842    { keepSyncMetadata = false } = {}
843  ) {
844    let localRecord = this._data[index];
845    let newRecord = this._clone(remoteRecord);
846
847    await this._stripComputedFields(newRecord);
848
849    this._data[index] = newRecord;
850
851    if (keepSyncMetadata) {
852      // It's safe to move the Sync metadata from the old record to the new
853      // record, since we always clone records when we return them, and we
854      // never hand out references to the metadata object via public methods.
855      newRecord._sync = localRecord._sync;
856    } else {
857      // As a side effect, `_getSyncMetaData` marks the record as syncing if the
858      // existing `localRecord` is a dupe of `remoteRecord`, and we're replacing
859      // local with remote.
860      let sync = this._getSyncMetaData(newRecord, true);
861      sync.changeCounter = 0;
862    }
863
864    if (
865      !newRecord.timeCreated ||
866      localRecord.timeCreated < newRecord.timeCreated
867    ) {
868      newRecord.timeCreated = localRecord.timeCreated;
869    }
870
871    if (
872      !newRecord.timeLastModified ||
873      localRecord.timeLastModified > newRecord.timeLastModified
874    ) {
875      newRecord.timeLastModified = localRecord.timeLastModified;
876    }
877
878    // Copy local-only fields from the existing local record.
879    for (let field of ["timeLastUsed", "timesUsed"]) {
880      if (localRecord[field] != null) {
881        newRecord[field] = localRecord[field];
882      }
883    }
884
885    await this.computeFields(newRecord);
886  }
887
888  /**
889   * Clones a local record, giving the clone a new GUID and Sync metadata. The
890   * original record remains unchanged in storage.
891   *
892   * @param   {Object} strippedLocalRecord
893   *          The local record. Computed fields are stripped.
894   * @returns {string}
895   *          A clone of the local record with a new GUID.
896   */
897  async _forkLocalRecord(strippedLocalRecord) {
898    let forkedLocalRecord = this._cloneAndCleanUp(strippedLocalRecord);
899    forkedLocalRecord.guid = this._generateGUID();
900
901    // Give the record fresh Sync metadata and bump its change counter as a
902    // side effect. This also excludes the forked record from de-duping on the
903    // next sync, if the current sync is interrupted before the record can be
904    // uploaded.
905    this._getSyncMetaData(forkedLocalRecord, true);
906
907    await this.computeFields(forkedLocalRecord);
908    this._data.push(forkedLocalRecord);
909
910    return forkedLocalRecord;
911  }
912
913  /**
914   * Reconciles an incoming remote record into the matching local record. This
915   * method is only used by Sync; other callers should use `merge`.
916   *
917   * @param   {Object} remoteRecord
918   *          The incoming record. `remoteRecord` must not be a tombstone, and
919   *          must have a matching local record with the same GUID. Use
920   *          `add` to insert remote records that don't exist locally, and
921   *          `remove` to apply remote tombstones.
922   * @returns {Promise<Object>}
923   *          A `{forkedGUID}` tuple. `forkedGUID` is `null` if the merge
924   *          succeeded without conflicts, or a new GUID referencing the
925   *          existing locally modified record if the conflicts could not be
926   *          resolved.
927   */
928  async reconcile(remoteRecord) {
929    this._ensureMatchingVersion(remoteRecord);
930    if (remoteRecord.deleted) {
931      throw new Error(`Can't reconcile tombstone ${remoteRecord.guid}`);
932    }
933
934    let localIndex = this._findIndexByGUID(remoteRecord.guid);
935    if (localIndex < 0) {
936      throw new Error(`Record ${remoteRecord.guid} not found`);
937    }
938
939    let localRecord = this._data[localIndex];
940    let sync = this._getSyncMetaData(localRecord, true);
941
942    let forkedGUID = null;
943
944    if (sync.changeCounter === 0) {
945      // Local not modified. Replace local with remote.
946      await this._replaceRecordAt(localIndex, remoteRecord, {
947        keepSyncMetadata: false,
948      });
949    } else {
950      let strippedLocalRecord = this._clone(localRecord);
951      await this._stripComputedFields(strippedLocalRecord);
952
953      let mergedRecord = this._mergeSyncedRecords(
954        strippedLocalRecord,
955        remoteRecord
956      );
957      if (mergedRecord) {
958        // Local and remote modified, but we were able to merge. Replace the
959        // local record with the merged record.
960        await this._replaceRecordAt(localIndex, mergedRecord, {
961          keepSyncMetadata: true,
962        });
963      } else {
964        // Merge conflict. Fork the local record, then replace the original
965        // with the merged record.
966        let forkedLocalRecord = await this._forkLocalRecord(
967          strippedLocalRecord
968        );
969        forkedGUID = forkedLocalRecord.guid;
970        await this._replaceRecordAt(localIndex, remoteRecord, {
971          keepSyncMetadata: false,
972        });
973      }
974    }
975
976    this._store.saveSoon();
977    Services.obs.notifyObservers(
978      {
979        wrappedJSObject: {
980          sourceSync: true,
981          guid: remoteRecord.guid,
982          forkedGUID,
983          collectionName: this._collectionName,
984        },
985      },
986      "formautofill-storage-changed",
987      "reconcile"
988    );
989
990    return { forkedGUID };
991  }
992
993  _removeSyncedRecord(guid) {
994    let index = this._findIndexByGUID(guid, { includeDeleted: true });
995    if (index == -1) {
996      // Removing a record we don't know about. It may have been synced and
997      // removed by another device before we saw it. Store the tombstone in
998      // case the server is later wiped and we need to reupload everything.
999      let tombstone = {
1000        guid,
1001        timeLastModified: Date.now(),
1002        deleted: true,
1003      };
1004
1005      let sync = this._getSyncMetaData(tombstone, true);
1006      sync.changeCounter = 0;
1007      this._data.push(tombstone);
1008      return;
1009    }
1010
1011    let existing = this._data[index];
1012    let sync = this._getSyncMetaData(existing, true);
1013    if (sync.changeCounter > 0) {
1014      // Deleting a record with unsynced local changes. To avoid potential
1015      // data loss, we ignore the deletion in favor of the changed record.
1016      this.log.info(
1017        "Ignoring deletion for record with local changes",
1018        existing
1019      );
1020      return;
1021    }
1022
1023    if (existing.deleted) {
1024      this.log.info("Ignoring deletion for tombstone", existing);
1025      return;
1026    }
1027
1028    // Removing a record that's not changed locally, and that's not already
1029    // deleted. Replace the record with a synced tombstone.
1030    this._data[index] = {
1031      guid,
1032      timeLastModified: Date.now(),
1033      deleted: true,
1034      _sync: sync,
1035    };
1036  }
1037
1038  /**
1039   * Provide an object that describes the changes to sync.
1040   *
1041   * This is called at the start of the sync process to determine what needs
1042   * to be updated on the server. As the server is updated, sync will update
1043   * entries in the returned object, and when sync is complete it will pass
1044   * the object to pushSyncChanges, which will apply the changes to the store.
1045   *
1046   * @returns {object}
1047   *          An object describing the changes to sync.
1048   */
1049  pullSyncChanges() {
1050    let changes = {};
1051
1052    let profiles = this._data;
1053    for (let profile of profiles) {
1054      let sync = this._getSyncMetaData(profile, true);
1055      if (sync.changeCounter < 1) {
1056        if (sync.changeCounter != 0) {
1057          this.log.error("negative change counter", profile);
1058        }
1059        continue;
1060      }
1061      changes[profile.guid] = {
1062        profile,
1063        counter: sync.changeCounter,
1064        modified: profile.timeLastModified,
1065        synced: false,
1066      };
1067    }
1068    this._store.saveSoon();
1069
1070    return changes;
1071  }
1072
1073  /**
1074   * Apply the metadata changes made by Sync.
1075   *
1076   * This is called with metadata about what was synced - see pullSyncChanges.
1077   *
1078   * @param {object} changes
1079   *        The possibly modified object obtained via pullSyncChanges.
1080   */
1081  pushSyncChanges(changes) {
1082    for (let [guid, { counter, synced }] of Object.entries(changes)) {
1083      if (!synced) {
1084        continue;
1085      }
1086      let recordFound = this._findByGUID(guid, { includeDeleted: true });
1087      if (!recordFound) {
1088        this.log.warn("No profile found to persist changes for guid " + guid);
1089        continue;
1090      }
1091      let sync = this._getSyncMetaData(recordFound, true);
1092      sync.changeCounter = Math.max(0, sync.changeCounter - counter);
1093      if (sync.changeCounter === 0) {
1094        // Clear the shared parent fields once we've uploaded all pending
1095        // changes, since the server now matches what we have locally.
1096        sync.lastSyncedFields = {};
1097      }
1098    }
1099    this._store.saveSoon();
1100  }
1101
1102  /**
1103   * Reset all sync metadata for all items.
1104   *
1105   * This is called when Sync is disconnected from this device. All sync
1106   * metadata for all items is removed.
1107   */
1108  resetSync() {
1109    for (let record of this._data) {
1110      delete record._sync;
1111    }
1112    // XXX - we should probably also delete all tombstones?
1113    this.log.info("All sync metadata was reset");
1114  }
1115
1116  /**
1117   * Changes the GUID of an item. This should be called only by Sync. There
1118   * must be an existing record with oldID and it must never have been synced
1119   * or an error will be thrown. There must be no existing record with newID.
1120   *
1121   * No tombstone will be created for the old GUID - we check it hasn't
1122   * been synced, so no tombstone is necessary.
1123   *
1124   * @param   {string} oldID
1125   *          GUID of the existing item to change the GUID of.
1126   * @param   {string} newID
1127   *          The new GUID for the item.
1128   */
1129  changeGUID(oldID, newID) {
1130    this.log.debug("changeGUID: ", oldID, newID);
1131    if (oldID == newID) {
1132      throw new Error("changeGUID: old and new IDs are the same");
1133    }
1134    if (this._findIndexByGUID(newID) >= 0) {
1135      throw new Error("changeGUID: record with destination id exists already");
1136    }
1137
1138    let index = this._findIndexByGUID(oldID);
1139    let profile = this._data[index];
1140    if (!profile) {
1141      throw new Error("changeGUID: no source record");
1142    }
1143    if (this._getSyncMetaData(profile)) {
1144      throw new Error("changeGUID: existing record has already been synced");
1145    }
1146
1147    profile.guid = newID;
1148
1149    this._store.saveSoon();
1150  }
1151
1152  // Used to get, and optionally create, sync metadata. Brand new records will
1153  // *not* have sync meta-data - it will be created when they are first
1154  // synced.
1155  _getSyncMetaData(record, forceCreate = false) {
1156    if (!record._sync && forceCreate) {
1157      // create default metadata and indicate we need to save.
1158      record._sync = {
1159        changeCounter: 1,
1160        lastSyncedFields: {},
1161      };
1162      this._store.saveSoon();
1163    }
1164    return record._sync;
1165  }
1166
1167  /**
1168   * Finds a local record with matching common fields and a different GUID.
1169   * Sync uses this method to find and update unsynced local records with
1170   * fields that match incoming remote records. This avoids creating
1171   * duplicate profiles with the same information.
1172   *
1173   * @param   {Object} remoteRecord
1174   *          The remote record.
1175   * @returns {Promise<string|null>}
1176   *          The GUID of the matching local record, or `null` if no records
1177   *          match.
1178   */
1179  async findDuplicateGUID(remoteRecord) {
1180    if (!remoteRecord.guid) {
1181      throw new Error("Record missing GUID");
1182    }
1183    this._ensureMatchingVersion(remoteRecord);
1184    if (remoteRecord.deleted) {
1185      // Tombstones don't carry enough info to de-dupe, and we should have
1186      // handled them separately when applying the record.
1187      throw new Error("Tombstones can't have duplicates");
1188    }
1189    let localRecords = this._data;
1190    for (let localRecord of localRecords) {
1191      if (localRecord.deleted) {
1192        continue;
1193      }
1194      if (localRecord.guid == remoteRecord.guid) {
1195        throw new Error(`Record ${remoteRecord.guid} already exists`);
1196      }
1197      if (this._getSyncMetaData(localRecord)) {
1198        // This local record has already been uploaded, so it can't be a dupe of
1199        // another incoming item.
1200        continue;
1201      }
1202
1203      // Ignore computed fields when matching records as they aren't synced at all.
1204      let strippedLocalRecord = this._clone(localRecord);
1205      await this._stripComputedFields(strippedLocalRecord);
1206
1207      let keys = new Set(Object.keys(remoteRecord));
1208      for (let key of Object.keys(strippedLocalRecord)) {
1209        keys.add(key);
1210      }
1211      // Ignore internal fields when matching records. Internal fields are synced,
1212      // but almost certainly have different values than the local record, and
1213      // we'll update them in `reconcile`.
1214      for (let field of INTERNAL_FIELDS) {
1215        keys.delete(field);
1216      }
1217      if (!keys.size) {
1218        // This shouldn't ever happen; a valid record will always have fields
1219        // that aren't computed or internal. Sync can't do anything about that,
1220        // so we ignore the dubious local record instead of throwing.
1221        continue;
1222      }
1223      let same = true;
1224      for (let key of keys) {
1225        // For now, we ensure that both (or neither) records have the field
1226        // with matching values. This doesn't account for the version yet
1227        // (bug 1377204).
1228        same =
1229          key in strippedLocalRecord == key in remoteRecord &&
1230          strippedLocalRecord[key] == remoteRecord[key];
1231        if (!same) {
1232          break;
1233        }
1234      }
1235      if (same) {
1236        return strippedLocalRecord.guid;
1237      }
1238    }
1239    return null;
1240  }
1241
1242  /**
1243   * Internal helper functions.
1244   */
1245
1246  _clone(record) {
1247    return Object.assign({}, record);
1248  }
1249
1250  _cloneAndCleanUp(record) {
1251    let result = {};
1252    for (let key in record) {
1253      // Do not expose hidden fields and fields with empty value (mainly used
1254      // as placeholders of the computed fields).
1255      if (!key.startsWith("_") && record[key] !== "") {
1256        result[key] = record[key];
1257      }
1258    }
1259    return result;
1260  }
1261
1262  _findByGUID(guid, { includeDeleted = false } = {}) {
1263    let found = this._findIndexByGUID(guid, { includeDeleted });
1264    return found < 0 ? undefined : this._data[found];
1265  }
1266
1267  _findIndexByGUID(guid, { includeDeleted = false } = {}) {
1268    return this._data.findIndex(record => {
1269      return record.guid == guid && (!record.deleted || includeDeleted);
1270    });
1271  }
1272
1273  async _migrateRecord(record, index) {
1274    let hasChanges = false;
1275
1276    if (record.deleted) {
1277      return hasChanges;
1278    }
1279
1280    if (!record.version || isNaN(record.version) || record.version < 1) {
1281      this.log.warn("Invalid record version:", record.version);
1282
1283      // Force to run the migration.
1284      record.version = 0;
1285    }
1286
1287    if (record.version < this.version) {
1288      hasChanges = true;
1289
1290      record = await this._computeMigratedRecord(record);
1291
1292      if (record.deleted) {
1293        // record is deleted by _computeMigratedRecord(),
1294        // go ahead and put it in the store.
1295        this._data[index] = record;
1296        return hasChanges;
1297      }
1298
1299      // Compute the computed fields before putting it to store.
1300      await this.computeFields(record);
1301      this._data[index] = record;
1302
1303      return hasChanges;
1304    }
1305
1306    hasChanges |= await this.computeFields(record);
1307    return hasChanges;
1308  }
1309
1310  _normalizeRecord(record, preserveEmptyFields = false) {
1311    this._normalizeFields(record);
1312
1313    for (let key in record) {
1314      if (!this.VALID_FIELDS.includes(key)) {
1315        throw new Error(`"${key}" is not a valid field.`);
1316      }
1317      if (typeof record[key] !== "string" && typeof record[key] !== "number") {
1318        throw new Error(
1319          `"${key}" contains invalid data type: ${typeof record[key]}`
1320        );
1321      }
1322      if (!preserveEmptyFields && record[key] === "") {
1323        delete record[key];
1324      }
1325    }
1326
1327    if (!Object.keys(record).length) {
1328      throw new Error("Record contains no valid field.");
1329    }
1330  }
1331
1332  /**
1333   * Merge the record if storage has multiple mergeable records.
1334   * @param {Object} targetRecord
1335   *        The record for merge.
1336   * @param {boolean} [strict = false]
1337   *        In strict merge mode, we'll treat the subset record with empty field
1338   *        as unable to be merged, but mergeable if in non-strict mode.
1339   * @returns {Array.<string>}
1340   *          Return an array of the merged GUID string.
1341   */
1342  async mergeToStorage(targetRecord, strict = false) {
1343    let mergedGUIDs = [];
1344    for (let record of this._data) {
1345      if (
1346        !record.deleted &&
1347        (await this.mergeIfPossible(record.guid, targetRecord, strict))
1348      ) {
1349        mergedGUIDs.push(record.guid);
1350      }
1351    }
1352    this.log.debug(
1353      "Existing records matching and merging count is",
1354      mergedGUIDs.length
1355    );
1356    return mergedGUIDs;
1357  }
1358
1359  /**
1360   * Unconditionally remove all data and tombstones for this collection.
1361   */
1362  removeAll({ sourceSync = false } = {}) {
1363    this._store.data[this._collectionName] = [];
1364    this._store.saveSoon();
1365    Services.obs.notifyObservers(
1366      {
1367        wrappedJSObject: {
1368          sourceSync,
1369          collectionName: this._collectionName,
1370        },
1371      },
1372      "formautofill-storage-changed",
1373      "removeAll"
1374    );
1375  }
1376
1377  /**
1378   * Strip the computed fields based on the record version.
1379   * @param   {Object} record      The record to migrate
1380   * @returns {Object}             Migrated record.
1381   *                               Record is always cloned, with version updated,
1382   *                               with computed fields stripped.
1383   *                               Could be a tombstone record, if the record
1384   *                               should be discorded.
1385   */
1386  async _computeMigratedRecord(record) {
1387    if (!record.deleted) {
1388      record = this._clone(record);
1389      await this._stripComputedFields(record);
1390      record.version = this.version;
1391    }
1392    return record;
1393  }
1394
1395  async _stripComputedFields(record) {
1396    this.VALID_COMPUTED_FIELDS.forEach(field => delete record[field]);
1397  }
1398
1399  // An interface to be inherited.
1400  _recordReadProcessor(record) {}
1401
1402  // An interface to be inherited.
1403  async computeFields(record) {}
1404
1405  /**
1406   * An interface to be inherited to mutate the argument to normalize it.
1407   *
1408   * @param {object} partialRecord containing the record passed by the consumer of
1409   *                               storage and in the case of `update` with
1410   *                               `preserveOldProperties` will only include the
1411   *                               properties that the user is changing so the
1412   *                               lack of a field doesn't mean that the record
1413   *                               won't have that field.
1414   */
1415  _normalizeFields(partialRecord) {}
1416
1417  /**
1418   * An interface to be inherited to validate that the complete record is
1419   * consistent and isn't missing required fields. Overrides should throw for
1420   * invalid records.
1421   *
1422   * @param {object} record containing the complete record that would be stored
1423   *                        if this doesn't throw due to an error.
1424   * @throws
1425   */
1426  _validateFields(record) {}
1427
1428  // An interface to be inherited.
1429  async mergeIfPossible(guid, record, strict) {}
1430}
1431
1432class AddressesBase extends AutofillRecords {
1433  constructor(store) {
1434    super(
1435      store,
1436      "addresses",
1437      VALID_ADDRESS_FIELDS,
1438      VALID_ADDRESS_COMPUTED_FIELDS,
1439      ADDRESS_SCHEMA_VERSION
1440    );
1441  }
1442
1443  _recordReadProcessor(address) {
1444    if (address.country && !FormAutofill.countries.has(address.country)) {
1445      delete address.country;
1446      delete address["country-name"];
1447    }
1448  }
1449
1450  async computeFields(address) {
1451    // NOTE: Remember to bump the schema version number if any of the existing
1452    //       computing algorithm changes. (No need to bump when just adding new
1453    //       computed fields.)
1454
1455    // NOTE: Computed fields should be always present in the storage no matter
1456    //       it's empty or not.
1457
1458    let hasNewComputedFields = false;
1459
1460    if (address.deleted) {
1461      return hasNewComputedFields;
1462    }
1463
1464    // Compute name
1465    if (!("name" in address)) {
1466      let name = FormAutofillNameUtils.joinNameParts({
1467        given: address["given-name"],
1468        middle: address["additional-name"],
1469        family: address["family-name"],
1470      });
1471      address.name = name;
1472      hasNewComputedFields = true;
1473    }
1474
1475    // Compute address lines
1476    if (!("address-line1" in address)) {
1477      let streetAddress = [];
1478      if (address["street-address"]) {
1479        streetAddress = address["street-address"]
1480          .split("\n")
1481          .map(s => s.trim());
1482      }
1483      for (let i = 0; i < 3; i++) {
1484        address["address-line" + (i + 1)] = streetAddress[i] || "";
1485      }
1486      if (streetAddress.length > 3) {
1487        address["address-line3"] = FormAutofillUtils.toOneLineAddress(
1488          streetAddress.splice(2)
1489        );
1490      }
1491      hasNewComputedFields = true;
1492    }
1493
1494    // Compute country name
1495    if (!("country-name" in address)) {
1496      if (address.country) {
1497        try {
1498          address[
1499            "country-name"
1500          ] = Services.intl.getRegionDisplayNames(undefined, [address.country]);
1501        } catch (e) {
1502          address["country-name"] = "";
1503        }
1504      } else {
1505        address["country-name"] = "";
1506      }
1507      hasNewComputedFields = true;
1508    }
1509
1510    // Compute tel
1511    if (!("tel-national" in address)) {
1512      if (address.tel) {
1513        let tel = PhoneNumber.Parse(
1514          address.tel,
1515          address.country || FormAutofill.DEFAULT_REGION
1516        );
1517        if (tel) {
1518          if (tel.countryCode) {
1519            address["tel-country-code"] = tel.countryCode;
1520          }
1521          if (tel.nationalNumber) {
1522            address["tel-national"] = tel.nationalNumber;
1523          }
1524
1525          // PhoneNumberUtils doesn't support parsing the components of a telephone
1526          // number so we hard coded the parser for US numbers only. We will need
1527          // to figure out how to parse numbers from other regions when we support
1528          // new countries in the future.
1529          if (tel.nationalNumber && tel.countryCode == "+1") {
1530            let telComponents = tel.nationalNumber.match(
1531              /(\d{3})((\d{3})(\d{4}))$/
1532            );
1533            if (telComponents) {
1534              address["tel-area-code"] = telComponents[1];
1535              address["tel-local"] = telComponents[2];
1536              address["tel-local-prefix"] = telComponents[3];
1537              address["tel-local-suffix"] = telComponents[4];
1538            }
1539          }
1540        } else {
1541          // Treat "tel" as "tel-national" directly if it can't be parsed.
1542          address["tel-national"] = address.tel;
1543        }
1544      }
1545
1546      TEL_COMPONENTS.forEach(c => {
1547        address[c] = address[c] || "";
1548      });
1549    }
1550
1551    return hasNewComputedFields;
1552  }
1553
1554  _normalizeFields(address) {
1555    this._normalizeName(address);
1556    this._normalizeAddress(address);
1557    this._normalizeCountry(address);
1558    this._normalizeTel(address);
1559  }
1560
1561  _normalizeName(address) {
1562    if (address.name) {
1563      let nameParts = FormAutofillNameUtils.splitName(address.name);
1564      if (!address["given-name"] && nameParts.given) {
1565        address["given-name"] = nameParts.given;
1566      }
1567      if (!address["additional-name"] && nameParts.middle) {
1568        address["additional-name"] = nameParts.middle;
1569      }
1570      if (!address["family-name"] && nameParts.family) {
1571        address["family-name"] = nameParts.family;
1572      }
1573    }
1574    delete address.name;
1575  }
1576
1577  _normalizeAddress(address) {
1578    if (STREET_ADDRESS_COMPONENTS.some(c => !!address[c])) {
1579      // Treat "street-address" as "address-line1" if it contains only one line
1580      // and "address-line1" is omitted.
1581      if (
1582        !address["address-line1"] &&
1583        address["street-address"] &&
1584        !address["street-address"].includes("\n")
1585      ) {
1586        address["address-line1"] = address["street-address"];
1587        delete address["street-address"];
1588      }
1589
1590      // Concatenate "address-line*" if "street-address" is omitted.
1591      if (!address["street-address"]) {
1592        address["street-address"] = STREET_ADDRESS_COMPONENTS.map(
1593          c => address[c]
1594        )
1595          .join("\n")
1596          .replace(/\n+$/, "");
1597      }
1598    }
1599    STREET_ADDRESS_COMPONENTS.forEach(c => delete address[c]);
1600  }
1601
1602  _normalizeCountry(address) {
1603    let country;
1604
1605    if (address.country) {
1606      country = address.country.toUpperCase();
1607    } else if (address["country-name"]) {
1608      country = FormAutofillUtils.identifyCountryCode(address["country-name"]);
1609    }
1610
1611    // Only values included in the region list will be saved.
1612    let hasLocalizedName = false;
1613    try {
1614      if (country) {
1615        let localizedName = Services.intl.getRegionDisplayNames(undefined, [
1616          country,
1617        ]);
1618        hasLocalizedName = localizedName != country;
1619      }
1620    } catch (e) {}
1621
1622    if (country && hasLocalizedName) {
1623      address.country = country;
1624    } else {
1625      delete address.country;
1626    }
1627
1628    delete address["country-name"];
1629  }
1630
1631  _normalizeTel(address) {
1632    if (address.tel || TEL_COMPONENTS.some(c => !!address[c])) {
1633      FormAutofillUtils.compressTel(address);
1634
1635      let possibleRegion = address.country || FormAutofill.DEFAULT_REGION;
1636      let tel = PhoneNumber.Parse(address.tel, possibleRegion);
1637
1638      if (tel && tel.internationalNumber) {
1639        // Force to save numbers in E.164 format if parse success.
1640        address.tel = tel.internationalNumber;
1641      }
1642    }
1643    TEL_COMPONENTS.forEach(c => delete address[c]);
1644  }
1645
1646  /**
1647   * Merge new address into the specified address if mergeable.
1648   *
1649   * @param  {string} guid
1650   *         Indicates which address to merge.
1651   * @param  {Object} address
1652   *         The new address used to merge into the old one.
1653   * @param  {boolean} strict
1654   *         In strict merge mode, we'll treat the subset record with empty field
1655   *         as unable to be merged, but mergeable if in non-strict mode.
1656   * @returns {Promise<boolean>}
1657   *          Return true if address is merged into target with specific guid or false if not.
1658   */
1659  async mergeIfPossible(guid, address, strict) {
1660    throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
1661  }
1662}
1663
1664class CreditCardsBase extends AutofillRecords {
1665  constructor(store) {
1666    super(
1667      store,
1668      "creditCards",
1669      VALID_CREDIT_CARD_FIELDS,
1670      VALID_CREDIT_CARD_COMPUTED_FIELDS,
1671      CREDIT_CARD_SCHEMA_VERSION
1672    );
1673    Services.obs.addObserver(this, "formautofill-storage-changed");
1674  }
1675
1676  observe(subject, topic, data) {
1677    switch (topic) {
1678      case "formautofill-storage-changed":
1679        let count = this._data.filter(entry => !entry.deleted).length;
1680        Services.telemetry.scalarSet(
1681          "formautofill.creditCards.autofill_profiles_count",
1682          count
1683        );
1684        break;
1685    }
1686  }
1687
1688  async computeFields(creditCard) {
1689    // NOTE: Remember to bump the schema version number if any of the existing
1690    //       computing algorithm changes. (No need to bump when just adding new
1691    //       computed fields.)
1692
1693    // NOTE: Computed fields should be always present in the storage no matter
1694    //       it's empty or not.
1695
1696    let hasNewComputedFields = false;
1697
1698    if (creditCard.deleted) {
1699      return hasNewComputedFields;
1700    }
1701
1702    if ("cc-number" in creditCard && !("cc-type" in creditCard)) {
1703      let type = CreditCard.getType(creditCard["cc-number"]);
1704      if (type) {
1705        creditCard["cc-type"] = type;
1706      }
1707    }
1708
1709    // Compute split names
1710    if (!("cc-given-name" in creditCard)) {
1711      let nameParts = FormAutofillNameUtils.splitName(creditCard["cc-name"]);
1712      creditCard["cc-given-name"] = nameParts.given;
1713      creditCard["cc-additional-name"] = nameParts.middle;
1714      creditCard["cc-family-name"] = nameParts.family;
1715      hasNewComputedFields = true;
1716    }
1717
1718    // Compute credit card expiration date
1719    if (!("cc-exp" in creditCard)) {
1720      if (creditCard["cc-exp-month"] && creditCard["cc-exp-year"]) {
1721        creditCard["cc-exp"] =
1722          String(creditCard["cc-exp-year"]) +
1723          "-" +
1724          String(creditCard["cc-exp-month"]).padStart(2, "0");
1725      } else {
1726        creditCard["cc-exp"] = "";
1727      }
1728      hasNewComputedFields = true;
1729    }
1730
1731    // Encrypt credit card number
1732    await this._encryptNumber(creditCard);
1733
1734    return hasNewComputedFields;
1735  }
1736
1737  async _encryptNumber(creditCard) {
1738    throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
1739  }
1740
1741  async _computeMigratedRecord(creditCard) {
1742    if (creditCard["cc-number-encrypted"]) {
1743      switch (creditCard.version) {
1744        case 1:
1745        case 2: {
1746          // We cannot decrypt the data, so silently remove the record for
1747          // the user.
1748          if (creditCard.deleted) {
1749            break;
1750          }
1751
1752          this.log.warn(
1753            "Removing version",
1754            creditCard.version,
1755            "credit card record to migrate to new encryption:",
1756            creditCard.guid
1757          );
1758
1759          // Replace the record with a tombstone record here,
1760          // regardless of existence of sync metadata.
1761          let existingSync = this._getSyncMetaData(creditCard);
1762          creditCard = {
1763            guid: creditCard.guid,
1764            timeLastModified: Date.now(),
1765            deleted: true,
1766          };
1767
1768          if (existingSync) {
1769            creditCard._sync = existingSync;
1770            existingSync.changeCounter++;
1771          }
1772          break;
1773        }
1774
1775        default:
1776          throw new Error(
1777            "Unknown credit card version to migrate: " + creditCard.version
1778          );
1779      }
1780    }
1781    return super._computeMigratedRecord(creditCard);
1782  }
1783
1784  async _stripComputedFields(creditCard) {
1785    if (creditCard["cc-number-encrypted"]) {
1786      try {
1787        creditCard["cc-number"] = await OSKeyStore.decrypt(
1788          creditCard["cc-number-encrypted"]
1789        );
1790      } catch (ex) {
1791        if (ex.result == Cr.NS_ERROR_ABORT) {
1792          throw ex;
1793        }
1794        // Quietly recover from encryption error,
1795        // so existing credit card entry with undecryptable number
1796        // can be updated.
1797      }
1798    }
1799    await super._stripComputedFields(creditCard);
1800  }
1801
1802  _normalizeFields(creditCard) {
1803    this._normalizeCCName(creditCard);
1804    this._normalizeCCNumber(creditCard);
1805    this._normalizeCCExpirationDate(creditCard);
1806  }
1807
1808  _normalizeCCName(creditCard) {
1809    if (
1810      creditCard["cc-given-name"] ||
1811      creditCard["cc-additional-name"] ||
1812      creditCard["cc-family-name"]
1813    ) {
1814      if (!creditCard["cc-name"]) {
1815        creditCard["cc-name"] = FormAutofillNameUtils.joinNameParts({
1816          given: creditCard["cc-given-name"],
1817          middle: creditCard["cc-additional-name"],
1818          family: creditCard["cc-family-name"],
1819        });
1820      }
1821    }
1822    delete creditCard["cc-given-name"];
1823    delete creditCard["cc-additional-name"];
1824    delete creditCard["cc-family-name"];
1825  }
1826
1827  _normalizeCCNumber(creditCard) {
1828    if (!("cc-number" in creditCard)) {
1829      return;
1830    }
1831    if (!CreditCard.isValidNumber(creditCard["cc-number"])) {
1832      delete creditCard["cc-number"];
1833      return;
1834    }
1835    let card = new CreditCard({ number: creditCard["cc-number"] });
1836    creditCard["cc-number"] = card.number;
1837  }
1838
1839  _normalizeCCExpirationDate(creditCard) {
1840    let normalizedExpiration = CreditCard.normalizeExpiration({
1841      expirationMonth: creditCard["cc-exp-month"],
1842      expirationYear: creditCard["cc-exp-year"],
1843      expirationString: creditCard["cc-exp"],
1844    });
1845    if (normalizedExpiration.month) {
1846      creditCard["cc-exp-month"] = normalizedExpiration.month;
1847    } else {
1848      delete creditCard["cc-exp-month"];
1849    }
1850    if (normalizedExpiration.year) {
1851      creditCard["cc-exp-year"] = normalizedExpiration.year;
1852    } else {
1853      delete creditCard["cc-exp-year"];
1854    }
1855    delete creditCard["cc-exp"];
1856  }
1857
1858  _validateFields(creditCard) {
1859    if (!creditCard["cc-number"]) {
1860      throw new Error("Missing/invalid cc-number");
1861    }
1862  }
1863
1864  _ensureMatchingVersion(record) {
1865    if (!record.version || isNaN(record.version) || record.version < 1) {
1866      throw new Error(
1867        `Got invalid record version ${record.version}; want ${this.version}`
1868      );
1869    }
1870
1871    if (record.version < this.version) {
1872      switch (record.version) {
1873        case 1:
1874        case 2:
1875          // The difference between version 1 and 2 is only about the encryption
1876          // method used for the cc-number-encrypted field.
1877          // The difference between version 2 and 3 is the name of the OS
1878          // key encryption record.
1879          // As long as the record is already decrypted, it is safe to bump the
1880          // version directly.
1881          if (!record["cc-number-encrypted"]) {
1882            record.version = this.version;
1883          } else {
1884            throw new Error(
1885              "Could not migrate record version:",
1886              record.version,
1887              "->",
1888              this.version
1889            );
1890          }
1891          break;
1892        default:
1893          throw new Error(
1894            "Unknown credit card version to match: " + record.version
1895          );
1896      }
1897    }
1898
1899    return super._ensureMatchingVersion(record);
1900  }
1901
1902  /**
1903   * Normalize the given record and return the first matched guid if storage has the same record.
1904   * @param {Object} targetCreditCard
1905   *        The credit card for duplication checking.
1906   * @returns {Promise<string|null>}
1907   *          Return the first guid if storage has the same credit card and null otherwise.
1908   */
1909  async getDuplicateGuid(targetCreditCard) {
1910    let clonedTargetCreditCard = this._clone(targetCreditCard);
1911    this._normalizeRecord(clonedTargetCreditCard);
1912    if (!clonedTargetCreditCard["cc-number"]) {
1913      return null;
1914    }
1915
1916    for (let creditCard of this._data) {
1917      if (creditCard.deleted) {
1918        continue;
1919      }
1920
1921      let decrypted = await OSKeyStore.decrypt(
1922        creditCard["cc-number-encrypted"],
1923        false
1924      );
1925
1926      if (decrypted == clonedTargetCreditCard["cc-number"]) {
1927        return creditCard.guid;
1928      }
1929    }
1930    return null;
1931  }
1932
1933  /**
1934   * Merge new credit card into the specified record if cc-number is identical.
1935   * (Note that credit card records always do non-strict merge.)
1936   *
1937   * @param  {string} guid
1938   *         Indicates which credit card to merge.
1939   * @param  {Object} creditCard
1940   *         The new credit card used to merge into the old one.
1941   * @returns {boolean}
1942   *          Return true if credit card is merged into target with specific guid or false if not.
1943   */
1944  async mergeIfPossible(guid, creditCard) {
1945    throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
1946  }
1947
1948  updateUseCountTelemetry() {
1949    let histogram = Services.telemetry.getHistogramById("CREDITCARD_NUM_USES");
1950    histogram.clear();
1951
1952    let records = this._data.filter(r => !r.deleted);
1953
1954    for (let record of records) {
1955      histogram.add(record.timesUsed);
1956    }
1957  }
1958}
1959
1960class FormAutofillStorageBase {
1961  constructor(path) {
1962    this._path = path;
1963    this._initializePromise = null;
1964    this.INTERNAL_FIELDS = INTERNAL_FIELDS;
1965  }
1966
1967  get version() {
1968    return STORAGE_SCHEMA_VERSION;
1969  }
1970
1971  get addresses() {
1972    return this.getAddresses();
1973  }
1974
1975  get creditCards() {
1976    return this.getCreditCards();
1977  }
1978
1979  getAddresses() {
1980    throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
1981  }
1982
1983  getCreditCards() {
1984    throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
1985  }
1986
1987  /**
1988   * Initialize storage to memory.
1989   *
1990   * @returns {Promise}
1991   * @resolves When the operation finished successfully.
1992   * @rejects  JavaScript exception.
1993   */
1994  initialize() {
1995    if (!this._initializePromise) {
1996      this._store = this._initializeStore();
1997      this._initializePromise = this._store.load().then(() => {
1998        let initializeAutofillRecords = [
1999          this.addresses.initialize(),
2000          this.creditCards.initialize(),
2001        ];
2002        return Promise.all(initializeAutofillRecords);
2003      });
2004    }
2005    return this._initializePromise;
2006  }
2007
2008  _initializeStore() {
2009    throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
2010  }
2011
2012  // For test only.
2013  _saveImmediately() {
2014    return this._store._save();
2015  }
2016
2017  _finalize() {
2018    return this._store.finalize();
2019  }
2020}
2021