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