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