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