dom/contacts/fallback/ContactDB.jsm

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     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 file,
     3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 "use strict";
     7 // Everything but "ContactDB" is only exported here for testing.
     8 this.EXPORTED_SYMBOLS = ["ContactDB", "DB_NAME", "STORE_NAME", "SAVED_GETALL_STORE_NAME",
     9                          "REVISION_STORE", "DB_VERSION"];
    11 const DEBUG = false;
    12 function debug(s) { dump("-*- ContactDB component: " + s + "\n"); }
    14 const Cu = Components.utils;
    15 const Cc = Components.classes;
    16 const Ci = Components.interfaces;
    18 Cu.import("resource://gre/modules/Services.jsm");
    19 Cu.import("resource://gre/modules/IndexedDBHelper.jsm");
    20 Cu.import("resource://gre/modules/PhoneNumberUtils.jsm");
    21 Cu.importGlobalProperties(["indexedDB"]);
    23 /* all exported symbols need to be bound to this on B2G - Bug 961777 */
    24 this.DB_NAME = "contacts";
    25 this.DB_VERSION = 20;
    26 this.STORE_NAME = "contacts";
    27 this.SAVED_GETALL_STORE_NAME = "getallcache";
    28 const CHUNK_SIZE = 20;
    29 this.REVISION_STORE = "revision";
    30 const REVISION_KEY = "revision";
    32 function exportContact(aRecord) {
    33   if (aRecord) {
    34     delete aRecord.search;
    35   }
    36   return aRecord;
    37 }
    39 function ContactDispatcher(aContacts, aFullContacts, aCallback, aNewTxn, aClearDispatcher, aFailureCb) {
    40   let nextIndex = 0;
    42   let sendChunk;
    43   let count = 0;
    44   if (aFullContacts) {
    45     sendChunk = function() {
    46       try {
    47         let chunk = aContacts.splice(0, CHUNK_SIZE);
    48         if (chunk.length > 0) {
    49           aCallback(chunk);
    50         }
    51         if (aContacts.length === 0) {
    52           aCallback(null);
    53           aClearDispatcher();
    54         }
    55       } catch (e) {
    56         aClearDispatcher();
    57       }
    58     }
    59   } else {
    60     sendChunk = function() {
    61       try {
    62         let start = nextIndex;
    63         nextIndex += CHUNK_SIZE;
    64         let chunk = [];
    65         aNewTxn("readonly", STORE_NAME, function(txn, store) {
    66           for (let i = start; i < Math.min(start+CHUNK_SIZE, aContacts.length); ++i) {
    67             store.get(aContacts[i]).onsuccess = function(e) {
    68               chunk.push(exportContact(e.target.result));
    69               count++;
    70               if (count === aContacts.length) {
    71                 aCallback(chunk);
    72                 aCallback(null);
    73                 aClearDispatcher();
    74               } else if (chunk.length === CHUNK_SIZE) {
    75                 aCallback(chunk);
    76                 chunk.length = 0;
    77               }
    78             }
    79           }
    80         }, null, function(errorMsg) {
    81           aFailureCb(errorMsg);
    82         });
    83       } catch (e) {
    84         aClearDispatcher();
    85       }
    86     }
    87   }
    89   return {
    90     sendNow: function() {
    91       sendChunk();
    92     }
    93   };
    94 }
    96 this.ContactDB = function ContactDB() {
    97   if (DEBUG) debug("Constructor");
    98 };
   100 ContactDB.prototype = {
   101   __proto__: IndexedDBHelper.prototype,
   103   _dispatcher: {},
   105   useFastUpgrade: true,
   107   upgradeSchema: function upgradeSchema(aTransaction, aDb, aOldVersion, aNewVersion) {
   108     let loadInitialContacts = function() {
   109       // Add default contacts
   110       let jsm = {};
   111       Cu.import("resource://gre/modules/FileUtils.jsm", jsm);
   112       Cu.import("resource://gre/modules/NetUtil.jsm", jsm);
   113       // Loading resource://app/defaults/contacts.json doesn't work because
   114       // contacts.json is not in the omnijar.
   115       // So we look for the app dir instead and go from here...
   116       let contactsFile = jsm.FileUtils.getFile("DefRt", ["contacts.json"], false);
   117       if (!contactsFile || (contactsFile && !contactsFile.exists())) {
   118         // For b2g desktop
   119         contactsFile = jsm.FileUtils.getFile("ProfD", ["contacts.json"], false);
   120         if (!contactsFile || (contactsFile && !contactsFile.exists())) {
   121           return;
   122         }
   123       }
   125       let chan = jsm.NetUtil.newChannel(contactsFile);
   126       let stream = chan.open();
   127       // Obtain a converter to read from a UTF-8 encoded input stream.
   128       let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
   129                       .createInstance(Ci.nsIScriptableUnicodeConverter);
   130       converter.charset = "UTF-8";
   131       let rawstr = converter.ConvertToUnicode(jsm.NetUtil.readInputStreamToString(
   132                                               stream,
   133                                               stream.available()) || "");
   134       stream.close();
   135       let contacts;
   136       try {
   137         contacts = JSON.parse(rawstr);
   138       } catch(e) {
   139         if (DEBUG) debug("Error parsing " + contactsFile.path + " : " + e);
   140         return;
   141       }
   143       let idService = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
   144       objectStore = aTransaction.objectStore(STORE_NAME);
   146       for (let i = 0; i < contacts.length; i++) {
   147         let contact = {};
   148         contact.properties = contacts[i];
   149         contact.id = idService.generateUUID().toString().replace(/[{}-]/g, "");
   150         contact = this.makeImport(contact);
   151         this.updateRecordMetadata(contact);
   152         if (DEBUG) debug("import: " + JSON.stringify(contact));
   153         objectStore.put(contact);
   154       }
   155     }.bind(this);
   157     function createFinalSchema() {
   158       if (DEBUG) debug("creating final schema");
   159       let objectStore = aDb.createObjectStore(STORE_NAME, {keyPath: "id"});
   160       objectStore.createIndex("familyName", "properties.familyName", { multiEntry: true });
   161       objectStore.createIndex("givenName",  "properties.givenName",  { multiEntry: true });
   162       objectStore.createIndex("name",      "properties.name",        { multiEntry: true });
   163       objectStore.createIndex("familyNameLowerCase", "search.familyName", { multiEntry: true });
   164       objectStore.createIndex("givenNameLowerCase",  "search.givenName",  { multiEntry: true });
   165       objectStore.createIndex("nameLowerCase",       "search.name",       { multiEntry: true });
   166       objectStore.createIndex("telLowerCase",        "search.tel",        { multiEntry: true });
   167       objectStore.createIndex("emailLowerCase",      "search.email",      { multiEntry: true });
   168       objectStore.createIndex("tel", "search.exactTel", { multiEntry: true });
   169       objectStore.createIndex("category", "properties.category", { multiEntry: true });
   170       objectStore.createIndex("email", "search.email", { multiEntry: true });
   171       objectStore.createIndex("telMatch", "search.parsedTel", {multiEntry: true});
   172       objectStore.createIndex("phoneticFamilyName", "properties.phoneticFamilyName", { multiEntry: true });
   173       objectStore.createIndex("phoneticGivenName", "properties.phoneticGivenName", { multiEntry: true });
   174       objectStore.createIndex("phoneticFamilyNameLowerCase", "search.phoneticFamilyName", { multiEntry: true });
   175       objectStore.createIndex("phoneticGivenNameLowerCase",  "search.phoneticGivenName",  { multiEntry: true });
   176       aDb.createObjectStore(SAVED_GETALL_STORE_NAME);
   177       aDb.createObjectStore(REVISION_STORE).put(0, REVISION_KEY);
   178     }
   180     let valueUpgradeSteps = [];
   182     function scheduleValueUpgrade(upgradeFunc) {
   183       var length = valueUpgradeSteps.push(upgradeFunc);
   184       if (DEBUG) debug("Scheduled a value upgrade function, index " + (length - 1));
   185     }
   187     // We always output this debug line because it's useful and the noise ratio
   188     // very low.
   189     debug("upgrade schema from: " + aOldVersion + " to " + aNewVersion + " called!");
   190     let db = aDb;
   191     let objectStore;
   193     if (aOldVersion === 0 && this.useFastUpgrade) {
   194       createFinalSchema();
   195       loadInitialContacts();
   196       return;
   197     }
   199     let steps = [
   200       function upgrade0to1() {
   201         /**
   202          * Create the initial database schema.
   203          *
   204          * The schema of records stored is as follows:
   205          *
   206          * {id:            "...",       // UUID
   207          *  published:     Date(...),   // First published date.
   208          *  updated:       Date(...),   // Last updated date.
   209          *  properties:    {...}        // Object holding the ContactProperties
   210          * }
   211          */
   212         if (DEBUG) debug("create schema");
   213         objectStore = db.createObjectStore(STORE_NAME, {keyPath: "id"});
   215         // Properties indexes
   216         objectStore.createIndex("familyName", "properties.familyName", { multiEntry: true });
   217         objectStore.createIndex("givenName",  "properties.givenName",  { multiEntry: true });
   219         objectStore.createIndex("familyNameLowerCase", "search.familyName", { multiEntry: true });
   220         objectStore.createIndex("givenNameLowerCase",  "search.givenName",  { multiEntry: true });
   221         objectStore.createIndex("telLowerCase",        "search.tel",        { multiEntry: true });
   222         objectStore.createIndex("emailLowerCase",      "search.email",      { multiEntry: true });
   223         next();
   224       },
   225       function upgrade1to2() {
   226         if (DEBUG) debug("upgrade 1");
   228         // Create a new scheme for the tel field. We move from an array of tel-numbers to an array of
   229         // ContactTelephone.
   230         if (!objectStore) {
   231           objectStore = aTransaction.objectStore(STORE_NAME);
   232         }
   233         // Delete old tel index.
   234         if (objectStore.indexNames.contains("tel")) {
   235           objectStore.deleteIndex("tel");
   236         }
   238         // Upgrade existing tel field in the DB.
   239         objectStore.openCursor().onsuccess = function(event) {
   240           let cursor = event.target.result;
   241           if (cursor) {
   242             if (DEBUG) debug("upgrade tel1: " + JSON.stringify(cursor.value));
   243             for (let number in cursor.value.properties.tel) {
   244               cursor.value.properties.tel[number] = {number: number};
   245             }
   246             cursor.update(cursor.value);
   247             if (DEBUG) debug("upgrade tel2: " + JSON.stringify(cursor.value));
   248             cursor.continue();
   249           } else {
   250             next();
   251           }
   252         };
   254         // Create new searchable indexes.
   255         objectStore.createIndex("tel", "search.tel", { multiEntry: true });
   256         objectStore.createIndex("category", "properties.category", { multiEntry: true });
   257       },
   258       function upgrade2to3() {
   259         if (DEBUG) debug("upgrade 2");
   260         // Create a new scheme for the email field. We move from an array of emailaddresses to an array of
   261         // ContactEmail.
   262         if (!objectStore) {
   263           objectStore = aTransaction.objectStore(STORE_NAME);
   264         }
   266         // Delete old email index.
   267         if (objectStore.indexNames.contains("email")) {
   268           objectStore.deleteIndex("email");
   269         }
   271         // Upgrade existing email field in the DB.
   272         objectStore.openCursor().onsuccess = function(event) {
   273           let cursor = event.target.result;
   274           if (cursor) {
   275             if (cursor.value.properties.email) {
   276               if (DEBUG) debug("upgrade email1: " + JSON.stringify(cursor.value));
   277               cursor.value.properties.email =
   278                 cursor.value.properties.email.map(function(address) { return { address: address }; });
   279               cursor.update(cursor.value);
   280               if (DEBUG) debug("upgrade email2: " + JSON.stringify(cursor.value));
   281             }
   282             cursor.continue();
   283           } else {
   284             next();
   285           }
   286         };
   288         // Create new searchable indexes.
   289         objectStore.createIndex("email", "search.email", { multiEntry: true });
   290       },
   291       function upgrade3to4() {
   292         if (DEBUG) debug("upgrade 3");
   294         if (!objectStore) {
   295           objectStore = aTransaction.objectStore(STORE_NAME);
   296         }
   298         // Upgrade existing impp field in the DB.
   299         objectStore.openCursor().onsuccess = function(event) {
   300           let cursor = event.target.result;
   301           if (cursor) {
   302             if (cursor.value.properties.impp) {
   303               if (DEBUG) debug("upgrade impp1: " + JSON.stringify(cursor.value));
   304               cursor.value.properties.impp =
   305                 cursor.value.properties.impp.map(function(value) { return { value: value }; });
   306               cursor.update(cursor.value);
   307               if (DEBUG) debug("upgrade impp2: " + JSON.stringify(cursor.value));
   308             }
   309             cursor.continue();
   310           }
   311         };
   312         // Upgrade existing url field in the DB.
   313         objectStore.openCursor().onsuccess = function(event) {
   314           let cursor = event.target.result;
   315           if (cursor) {
   316             if (cursor.value.properties.url) {
   317               if (DEBUG) debug("upgrade url1: " + JSON.stringify(cursor.value));
   318               cursor.value.properties.url =
   319                 cursor.value.properties.url.map(function(value) { return { value: value }; });
   320               cursor.update(cursor.value);
   321               if (DEBUG) debug("upgrade impp2: " + JSON.stringify(cursor.value));
   322             }
   323             cursor.continue();
   324           } else {
   325             next();
   326           }
   327         };
   328       },
   329       function upgrade4to5() {
   330         if (DEBUG) debug("Add international phone numbers upgrade");
   331         if (!objectStore) {
   332           objectStore = aTransaction.objectStore(STORE_NAME);
   333         }
   335         objectStore.openCursor().onsuccess = function(event) {
   336           let cursor = event.target.result;
   337           if (cursor) {
   338             if (cursor.value.properties.tel) {
   339               if (DEBUG) debug("upgrade : " + JSON.stringify(cursor.value));
   340               cursor.value.properties.tel.forEach(
   341                 function(duple) {
   342                   let parsedNumber = PhoneNumberUtils.parse(duple.value.toString());
   343                   if (parsedNumber) {
   344                     if (DEBUG) {
   345                       debug("InternationalFormat: " + parsedNumber.internationalFormat);
   346                       debug("InternationalNumber: " + parsedNumber.internationalNumber);
   347                       debug("NationalNumber: " + parsedNumber.nationalNumber);
   348                       debug("NationalFormat: " + parsedNumber.nationalFormat);
   349                     }
   350                     if (duple.value.toString() !== parsedNumber.internationalNumber) {
   351                       cursor.value.search.tel.push(parsedNumber.internationalNumber);
   352                     }
   353                   } else {
   354                     dump("Warning: No international number found for " + duple.value + "\n");
   355                   }
   356                 }
   357               )
   358               cursor.update(cursor.value);
   359             }
   360             if (DEBUG) debug("upgrade2 : " + JSON.stringify(cursor.value));
   361             cursor.continue();
   362           } else {
   363             next();
   364           }
   365         };
   366       },
   367       function upgrade5to6() {
   368         if (DEBUG) debug("Add index for equals tel searches");
   369         if (!objectStore) {
   370           objectStore = aTransaction.objectStore(STORE_NAME);
   371         }
   373         // Delete old tel index (not on the right field).
   374         if (objectStore.indexNames.contains("tel")) {
   375           objectStore.deleteIndex("tel");
   376         }
   378         // Create new index for "equals" searches
   379         objectStore.createIndex("tel", "search.exactTel", { multiEntry: true });
   381         objectStore.openCursor().onsuccess = function(event) {
   382           let cursor = event.target.result;
   383           if (cursor) {
   384             if (cursor.value.properties.tel) {
   385               if (DEBUG) debug("upgrade : " + JSON.stringify(cursor.value));
   386               cursor.value.properties.tel.forEach(
   387                 function(duple) {
   388                   let number = duple.value.toString();
   389                   let parsedNumber = PhoneNumberUtils.parse(number);
   391                   cursor.value.search.exactTel = [number];
   392                   if (parsedNumber &&
   393                       parsedNumber.internationalNumber &&
   394                       number !== parsedNumber.internationalNumber) {
   395                     cursor.value.search.exactTel.push(parsedNumber.internationalNumber);
   396                   }
   397                 }
   398               )
   399               cursor.update(cursor.value);
   400             }
   401             if (DEBUG) debug("upgrade : " + JSON.stringify(cursor.value));
   402             cursor.continue();
   403           } else {
   404             next();
   405           }
   406         };
   407       },
   408       function upgrade6to7() {
   409         if (!objectStore) {
   410           objectStore = aTransaction.objectStore(STORE_NAME);
   411         }
   412         let names = objectStore.indexNames;
   413         let whiteList = ["tel", "familyName", "givenName",  "familyNameLowerCase",
   414                          "givenNameLowerCase", "telLowerCase", "category", "email",
   415                          "emailLowerCase"];
   416         for (var i = 0; i < names.length; i++) {
   417           if (whiteList.indexOf(names[i]) < 0) {
   418             objectStore.deleteIndex(names[i]);
   419           }
   420         }
   421         next();
   422       },
   423       function upgrade7to8() {
   424         if (DEBUG) debug("Adding object store for cached searches");
   425         db.createObjectStore(SAVED_GETALL_STORE_NAME);
   426         next();
   427       },
   428       function upgrade8to9() {
   429         if (DEBUG) debug("Make exactTel only contain the value entered by the user");
   430         if (!objectStore) {
   431           objectStore = aTransaction.objectStore(STORE_NAME);
   432         }
   434         objectStore.openCursor().onsuccess = function(event) {
   435           let cursor = event.target.result;
   436           if (cursor) {
   437             if (cursor.value.properties.tel) {
   438               cursor.value.search.exactTel = [];
   439               cursor.value.properties.tel.forEach(
   440                 function(tel) {
   441                   let normalized = PhoneNumberUtils.normalize(tel.value.toString());
   442                   cursor.value.search.exactTel.push(normalized);
   443                 }
   444               );
   445               cursor.update(cursor.value);
   446             }
   447             cursor.continue();
   448           } else {
   449             next();
   450           }
   451         };
   452       },
   453       function upgrade9to10() {
   454         // no-op, see https://bugzilla.mozilla.org/show_bug.cgi?id=883770#c16
   455         next();
   456       },
   457       function upgrade10to11() {
   458         if (DEBUG) debug("Adding object store for database revision");
   459         db.createObjectStore(REVISION_STORE).put(0, REVISION_KEY);
   460         next();
   461       },
   462       function upgrade11to12() {
   463         if (DEBUG) debug("Add a telMatch index with national and international numbers");
   464         if (!objectStore) {
   465           objectStore = aTransaction.objectStore(STORE_NAME);
   466         }
   467         if (!objectStore.indexNames.contains("telMatch")) {
   468           objectStore.createIndex("telMatch", "search.parsedTel", {multiEntry: true});
   469         }
   470         objectStore.openCursor().onsuccess = function(event) {
   471           let cursor = event.target.result;
   472           if (cursor) {
   473             if (cursor.value.properties.tel) {
   474               cursor.value.search.parsedTel = [];
   475               cursor.value.properties.tel.forEach(
   476                 function(tel) {
   477                   let parsed = PhoneNumberUtils.parse(tel.value.toString());
   478                   if (parsed) {
   479                     cursor.value.search.parsedTel.push(parsed.nationalNumber);
   480                     cursor.value.search.parsedTel.push(PhoneNumberUtils.normalize(parsed.nationalFormat));
   481                     cursor.value.search.parsedTel.push(parsed.internationalNumber);
   482                     cursor.value.search.parsedTel.push(PhoneNumberUtils.normalize(parsed.internationalFormat));
   483                   }
   484                   cursor.value.search.parsedTel.push(PhoneNumberUtils.normalize(tel.value.toString()));
   485                 }
   486               );
   487               cursor.update(cursor.value);
   488             }
   489             cursor.continue();
   490           } else {
   491             next();
   492           }
   493         };
   494       },
   495       function upgrade12to13() {
   496         if (DEBUG) debug("Add phone substring to the search index if appropriate for country");
   497         if (this.substringMatching) {
   498           scheduleValueUpgrade(function upgradeValue12to13(value) {
   499             if (value.properties.tel) {
   500               value.search.parsedTel = value.search.parsedTel || [];
   501               value.properties.tel.forEach(
   502                 function(tel) {
   503                   let normalized = PhoneNumberUtils.normalize(tel.value.toString());
   504                   if (normalized) {
   505                     if (this.substringMatching && normalized.length > this.substringMatching) {
   506                       let sub = normalized.slice(-this.substringMatching);
   507                       if (value.search.parsedTel.indexOf(sub) === -1) {
   508                         if (DEBUG) debug("Adding substring index: " + tel + ", " + sub);
   509                         value.search.parsedTel.push(sub);
   510                       }
   511                     }
   512                   }
   513                 }.bind(this)
   514               );
   515               return true;
   516             } else {
   517               return false;
   518             }
   519           }.bind(this));
   520         }
   521         next();
   522       },
   523       function upgrade13to14() {
   524         if (DEBUG) debug("Cleaning up empty substring entries in telMatch index");
   525         scheduleValueUpgrade(function upgradeValue13to14(value) {
   526           function removeEmptyStrings(value) {
   527             if (value) {
   528               const oldLength = value.length;
   529               for (let i = 0; i < value.length; ++i) {
   530                 if (!value[i] || value[i] == "null") {
   531                   value.splice(i, 1);
   532                 }
   533               }
   534               return oldLength !== value.length;
   535             }
   536           }
   538           let modified = removeEmptyStrings(value.search.parsedTel);
   539           let modified2 = removeEmptyStrings(value.search.tel);
   540           return (modified || modified2);
   541         });
   543         next();
   544       },
   545       function upgrade14to15() {
   546         if (DEBUG) debug("Fix array properties saved as scalars");
   547         const ARRAY_PROPERTIES = ["photo", "adr", "email", "url", "impp", "tel",
   548                                  "name", "honorificPrefix", "givenName",
   549                                  "additionalName", "familyName", "honorificSuffix",
   550                                  "nickname", "category", "org", "jobTitle",
   551                                  "note", "key"];
   552         const PROPERTIES_WITH_TYPE = ["adr", "email", "url", "impp", "tel"];
   554         scheduleValueUpgrade(function upgradeValue14to15(value) {
   555           let changed = false;
   557           let props = value.properties;
   558           for (let prop of ARRAY_PROPERTIES) {
   559             if (props[prop]) {
   560               if (!Array.isArray(props[prop])) {
   561                 value.properties[prop] = [props[prop]];
   562                 changed = true;
   563               }
   564               if (PROPERTIES_WITH_TYPE.indexOf(prop) !== -1) {
   565                 let subprop = value.properties[prop];
   566                 for (let i = 0; i < subprop.length; ++i) {
   567                   if (!Array.isArray(subprop[i].type)) {
   568                     value.properties[prop][i].type = [subprop[i].type];
   569                     changed = true;
   570                   }
   571                 }
   572               }
   573             }
   574           }
   576           return changed;
   577         });
   579         next();
   580       },
   581       function upgrade15to16() {
   582         if (DEBUG) debug("Fix Date properties");
   583         const DATE_PROPERTIES = ["bday", "anniversary"];
   585         scheduleValueUpgrade(function upgradeValue15to16(value) {
   586           let changed = false;
   587           let props = value.properties;
   588           for (let prop of DATE_PROPERTIES) {
   589             if (props[prop] && !(props[prop] instanceof Date)) {
   590               value.properties[prop] = new Date(props[prop]);
   591               changed = true;
   592             }
   593           }
   595           return changed;
   596         });
   598         next();
   599       },
   600       function upgrade16to17() {
   601         if (DEBUG) debug("Fix array with null values");
   602         const ARRAY_PROPERTIES = ["photo", "adr", "email", "url", "impp", "tel",
   603                                  "name", "honorificPrefix", "givenName",
   604                                  "additionalName", "familyName", "honorificSuffix",
   605                                  "nickname", "category", "org", "jobTitle",
   606                                  "note", "key"];
   608         const PROPERTIES_WITH_TYPE = ["adr", "email", "url", "impp", "tel"];
   610         const DATE_PROPERTIES = ["bday", "anniversary"];
   612         scheduleValueUpgrade(function upgradeValue16to17(value) {
   613           let changed;
   615           function filterInvalidValues(val) {
   616             let shouldKeep = val != null; // null or undefined
   617             if (!shouldKeep) {
   618               changed = true;
   619             }
   620             return shouldKeep;
   621           }
   623           function filteredArray(array) {
   624             return array.filter(filterInvalidValues);
   625           }
   627           let props = value.properties;
   629           for (let prop of ARRAY_PROPERTIES) {
   631             // properties that were empty strings weren't converted to arrays
   632             // in upgrade14to15
   633             if (props[prop] != null && !Array.isArray(props[prop])) {
   634               props[prop] = [props[prop]];
   635               changed = true;
   636             }
   638             if (props[prop] && props[prop].length) {
   639               props[prop] = filteredArray(props[prop]);
   641               if (PROPERTIES_WITH_TYPE.indexOf(prop) !== -1) {
   642                 let subprop = props[prop];
   644                 for (let i = 0; i < subprop.length; ++i) {
   645                   let curSubprop = subprop[i];
   646                   // upgrade14to15 transformed type props into an array
   647                   // without checking invalid values
   648                   if (curSubprop.type) {
   649                     curSubprop.type = filteredArray(curSubprop.type);
   650                   }
   651                 }
   652               }
   653             }
   654           }
   656           for (let prop of DATE_PROPERTIES) {
   657             if (props[prop] != null && !(props[prop] instanceof Date)) {
   658               // props[prop] is probably '' and wasn't converted
   659               // in upgrade15to16
   660               props[prop] = null;
   661               changed = true;
   662             }
   663           }
   665           if (changed) {
   666             value.properties = props;
   667             return true;
   668           } else {
   669             return false;
   670           }
   671         });
   673         next();
   674       },
   675       function upgrade17to18() {
   676         // this upgrade function has been moved to the next upgrade path because
   677         // a previous version of it had a bug
   678         next();
   679       },
   680       function upgrade18to19() {
   681         if (DEBUG) {
   682           debug("Adding the name index");
   683         }
   685         if (!objectStore) {
   686           objectStore = aTransaction.objectStore(STORE_NAME);
   687         }
   689         // an earlier version of this code could have run, so checking whether
   690         // the index exists
   691         if (!objectStore.indexNames.contains("name")) {
   692           objectStore.createIndex("name", "properties.name", { multiEntry: true });
   693           objectStore.createIndex("nameLowerCase", "search.name", { multiEntry: true });
   694         }
   696         scheduleValueUpgrade(function upgradeValue18to19(value) {
   697           value.search.name = [];
   698           if (value.properties.name) {
   699             value.properties.name.forEach(function addNameIndex(name) {
   700               var lowerName = name.toLowerCase();
   701               // an earlier version of this code could have added it already
   702               if (value.search.name.indexOf(lowerName) === -1) {
   703                 value.search.name.push(lowerName);
   704               }
   705             });
   706           }
   707           return true;
   708         });
   710         next();
   711       },
   712       function upgrade19to20() {
   713         if (DEBUG) debug("upgrade19to20 create schema(phonetic)");
   714         if (!objectStore) {
   715           objectStore = aTransaction.objectStore(STORE_NAME);
   716         }
   717         objectStore.createIndex("phoneticFamilyName", "properties.phoneticFamilyName", { multiEntry: true });
   718         objectStore.createIndex("phoneticGivenName", "properties.phoneticGivenName", { multiEntry: true });
   719         objectStore.createIndex("phoneticFamilyNameLowerCase", "search.phoneticFamilyName", { multiEntry: true });
   720         objectStore.createIndex("phoneticGivenNameLowerCase",  "search.phoneticGivenName",  { multiEntry: true });
   721         next();
   722       },
   723     ];
   725     let index = aOldVersion;
   726     let outer = this;
   728     /* This function runs all upgrade functions that are in the
   729      * valueUpgradeSteps array. These functions have the following properties:
   730      * - they must be synchronous
   731      * - they must take the value as parameter and modify it directly. They
   732      *   must not create a new object.
   733      * - they must return a boolean true/false; true if the value was actually
   734      *   changed
   735      */
   736     function runValueUpgradeSteps(done) {
   737       if (DEBUG) debug("Running the value upgrade functions.");
   738       if (!objectStore) {
   739         objectStore = aTransaction.objectStore(STORE_NAME);
   740       }
   741       objectStore.openCursor().onsuccess = function(event) {
   742         let cursor = event.target.result;
   743         if (cursor) {
   744           let changed = false;
   745           let oldValue;
   746           let value = cursor.value;
   747           if (DEBUG) {
   748             oldValue = JSON.stringify(value);
   749           }
   750           valueUpgradeSteps.forEach(function(upgradeFunc, i) {
   751             if (DEBUG) debug("Running upgrade function " + i);
   752             changed = upgradeFunc(value) || changed;
   753           });
   755           if (changed) {
   756             cursor.update(value);
   757           } else if (DEBUG) {
   758             let newValue = JSON.stringify(value);
   759             if (newValue !== oldValue) {
   760               // oops something went wrong
   761               debug("upgrade: `changed` was false and still the value changed! Aborting.");
   762               aTransaction.abort();
   763               return;
   764             }
   765           }
   766           cursor.continue();
   767         } else {
   768           done();
   769         }
   770       };
   771     }
   773     function finish() {
   774       // We always output this debug line because it's useful and the noise ratio
   775       // very low.
   776       debug("Upgrade finished");
   778       outer.incrementRevision(aTransaction);
   779     }
   781     function next() {
   782       if (index == aNewVersion) {
   783         runValueUpgradeSteps(finish);
   784         return;
   785       }
   787       try {
   788         var i = index++;
   789         if (DEBUG) debug("Upgrade step: " + i + "\n");
   790         steps[i].call(outer);
   791       } catch(ex) {
   792         dump("Caught exception" + ex);
   793         aTransaction.abort();
   794         return;
   795       }
   796     }
   798     function fail(why) {
   799       why = why || "";
   800       if (this.error) {
   801         why += " (root cause: " + this.error.name + ")";
   802       }
   804       debug("Contacts DB upgrade error: " + why);
   805       aTransaction.abort();
   806     }
   808     if (aNewVersion > steps.length) {
   809       fail("No migration steps for the new version!");
   810     }
   812     this.cpuLock = Cc["@mozilla.org/power/powermanagerservice;1"]
   813                      .getService(Ci.nsIPowerManagerService)
   814                      .newWakeLock("cpu");
   816     function unlockCPU() {
   817       if (outer.cpuLock) {
   818         if (DEBUG) debug("unlocking cpu wakelock");
   819         outer.cpuLock.unlock();
   820         outer.cpuLock = null;
   821       }
   822     }
   824     aTransaction.addEventListener("complete", unlockCPU);
   825     aTransaction.addEventListener("abort", unlockCPU);
   827     next();
   828   },
   830   makeImport: function makeImport(aContact) {
   831     let contact = {properties: {}};
   833     contact.search = {
   834       name:            [],
   835       givenName:       [],
   836       familyName:      [],
   837       email:           [],
   838       category:        [],
   839       tel:             [],
   840       exactTel:        [],
   841       parsedTel:       [],
   842       phoneticFamilyName:   [],
   843       phoneticGivenName:    [],
   844     };
   846     for (let field in aContact.properties) {
   847       contact.properties[field] = aContact.properties[field];
   848       // Add search fields
   849       if (aContact.properties[field] && contact.search[field]) {
   850         for (let i = 0; i <= aContact.properties[field].length; i++) {
   851           if (aContact.properties[field][i]) {
   852             if (field == "tel" && aContact.properties[field][i].value) {
   853               let number = aContact.properties.tel[i].value.toString();
   854               let normalized = PhoneNumberUtils.normalize(number);
   855               // We use an object here to avoid duplicates
   856               let containsSearch = {};
   857               let matchSearch = {};
   859               if (normalized) {
   860                 // exactTel holds normalized version of entered phone number.
   861                 // normalized: +1 (949) 123 - 4567 -> +19491234567
   862                 contact.search.exactTel.push(normalized);
   863                 // matchSearch holds normalized version of entered phone number,
   864                 // nationalNumber, nationalFormat, internationalNumber, internationalFormat
   865                 matchSearch[normalized] = 1;
   866                 let parsedNumber = PhoneNumberUtils.parse(number);
   867                 if (parsedNumber) {
   868                   if (DEBUG) {
   869                     debug("InternationalFormat: " + parsedNumber.internationalFormat);
   870                     debug("InternationalNumber: " + parsedNumber.internationalNumber);
   871                     debug("NationalNumber: " + parsedNumber.nationalNumber);
   872                     debug("NationalFormat: " + parsedNumber.nationalFormat);
   873                     debug("NationalMatchingFormat: " + parsedNumber.nationalMatchingFormat);
   874                   }
   875                   matchSearch[parsedNumber.nationalNumber] = 1;
   876                   matchSearch[parsedNumber.internationalNumber] = 1;
   877                   matchSearch[PhoneNumberUtils.normalize(parsedNumber.nationalFormat)] = 1;
   878                   matchSearch[PhoneNumberUtils.normalize(parsedNumber.internationalFormat)] = 1;
   879                   matchSearch[PhoneNumberUtils.normalize(parsedNumber.nationalMatchingFormat)] = 1;
   880                 } else if (this.substringMatching && normalized.length > this.substringMatching) {
   881                   matchSearch[normalized.slice(-this.substringMatching)] = 1;
   882                 }
   884                 // containsSearch holds incremental search values for:
   885                 // normalized number and national format
   886                 for (let i = 0; i < normalized.length; i++) {
   887                   containsSearch[normalized.substring(i, normalized.length)] = 1;
   888                 }
   889                 if (parsedNumber && parsedNumber.nationalFormat) {
   890                   let number = PhoneNumberUtils.normalize(parsedNumber.nationalFormat);
   891                   for (let i = 0; i < number.length; i++) {
   892                     containsSearch[number.substring(i, number.length)] = 1;
   893                   }
   894                 }
   895               }
   896               for (let num in containsSearch) {
   897                 if (num && num != "null") {
   898                   contact.search.tel.push(num);
   899                 }
   900               }
   901               for (let num in matchSearch) {
   902                 if (num && num != "null") {
   903                   contact.search.parsedTel.push(num);
   904                 }
   905               }
   906             } else if ((field == "impp" || field == "email") && aContact.properties[field][i].value) {
   907               let value = aContact.properties[field][i].value;
   908               if (value && typeof value == "string") {
   909                 contact.search[field].push(value.toLowerCase());
   910               }
   911             } else {
   912               let val = aContact.properties[field][i];
   913               if (typeof val == "string") {
   914                 contact.search[field].push(val.toLowerCase());
   915               }
   916             }
   917           }
   918         }
   919       }
   920     }
   922     contact.updated = aContact.updated;
   923     contact.published = aContact.published;
   924     contact.id = aContact.id;
   926     return contact;
   927   },
   929   updateRecordMetadata: function updateRecordMetadata(record) {
   930     if (!record.id) {
   931       Cu.reportError("Contact without ID");
   932     }
   933     if (!record.published) {
   934       record.published = new Date();
   935     }
   936     record.updated = new Date();
   937   },
   939   removeObjectFromCache: function CDB_removeObjectFromCache(aObjectId, aCallback, aFailureCb) {
   940     if (DEBUG) debug("removeObjectFromCache: " + aObjectId);
   941     if (!aObjectId) {
   942       if (DEBUG) debug("No object ID passed");
   943       return;
   944     }
   945     this.newTxn("readwrite", SAVED_GETALL_STORE_NAME, function(txn, store) {
   946       store.openCursor().onsuccess = function(e) {
   947         let cursor = e.target.result;
   948         if (cursor) {
   949           for (let i = 0; i < cursor.value.length; ++i) {
   950             if (cursor.value[i] == aObjectId) {
   951               if (DEBUG) debug("id matches cache");
   952               cursor.value.splice(i, 1);
   953               cursor.update(cursor.value);
   954               break;
   955             }
   956           }
   957           cursor.continue();
   958         } else {
   959           aCallback();
   960         }
   961       }.bind(this);
   962     }.bind(this), null,
   963     function(errorMsg) {
   964       aFailureCb(errorMsg);
   965     });
   966   },
   968   // Invalidate the entire cache. It will be incrementally regenerated on demand
   969   // See getCacheForQuery
   970   invalidateCache: function CDB_invalidateCache(aErrorCb) {
   971     if (DEBUG) debug("invalidate cache");
   972     this.newTxn("readwrite", SAVED_GETALL_STORE_NAME, function (txn, store) {
   973       store.clear();
   974     }, aErrorCb);
   975   },
   977   incrementRevision: function CDB_incrementRevision(txn) {
   978     let revStore = txn.objectStore(REVISION_STORE);
   979     revStore.get(REVISION_KEY).onsuccess = function(e) {
   980       revStore.put(parseInt(e.target.result, 10) + 1, REVISION_KEY);
   981     };
   982   },
   984   saveContact: function CDB_saveContact(aContact, successCb, errorCb) {
   985     let contact = this.makeImport(aContact);
   986     this.newTxn("readwrite", STORE_NAME, function (txn, store) {
   987       if (DEBUG) debug("Going to update" + JSON.stringify(contact));
   989       // Look up the existing record and compare the update timestamp.
   990       // If no record exists, just add the new entry.
   991       let newRequest = store.get(contact.id);
   992       newRequest.onsuccess = function (event) {
   993         if (!event.target.result) {
   994           if (DEBUG) debug("new record!")
   995           this.updateRecordMetadata(contact);
   996           store.put(contact);
   997         } else {
   998           if (DEBUG) debug("old record!")
   999           if (new Date(typeof contact.updated === "undefined" ? 0 : contact.updated) < new Date(event.target.result.updated)) {
  1000             if (DEBUG) debug("rev check fail!");
  1001             txn.abort();
  1002             return;
  1003           } else {
  1004             if (DEBUG) debug("rev check OK");
  1005             contact.published = event.target.result.published;
  1006             contact.updated = new Date();
  1007             store.put(contact);
  1010         this.invalidateCache(errorCb);
  1011       }.bind(this);
  1013       this.incrementRevision(txn);
  1014     }.bind(this), successCb, errorCb);
  1015   },
  1017   removeContact: function removeContact(aId, aSuccessCb, aErrorCb) {
  1018     if (DEBUG) debug("removeContact: " + aId);
  1019     this.removeObjectFromCache(aId, function() {
  1020       this.newTxn("readwrite", STORE_NAME, function(txn, store) {
  1021         store.delete(aId).onsuccess = function() {
  1022           aSuccessCb();
  1023         };
  1024         this.incrementRevision(txn);
  1025       }.bind(this), null, aErrorCb);
  1026     }.bind(this), aErrorCb);
  1027   },
  1029   clear: function clear(aSuccessCb, aErrorCb) {
  1030     this.newTxn("readwrite", STORE_NAME, function (txn, store) {
  1031       if (DEBUG) debug("Going to clear all!");
  1032       store.clear();
  1033       this.incrementRevision(txn);
  1034     }.bind(this), aSuccessCb, aErrorCb);
  1035   },
  1037   createCacheForQuery: function CDB_createCacheForQuery(aQuery, aSuccessCb, aFailureCb) {
  1038     this.find(function (aContacts) {
  1039       if (aContacts) {
  1040         let contactsArray = [];
  1041         for (let i in aContacts) {
  1042           contactsArray.push(aContacts[i]);
  1045         // save contact ids in cache
  1046         this.newTxn("readwrite", SAVED_GETALL_STORE_NAME, function(txn, store) {
  1047           store.put(contactsArray.map(function(el) el.id), aQuery);
  1048         }, null, aFailureCb);
  1050         // send full contacts
  1051         aSuccessCb(contactsArray, true);
  1052       } else {
  1053         aSuccessCb([], true);
  1055     }.bind(this),
  1056     function (aErrorMsg) { aFailureCb(aErrorMsg); },
  1057     JSON.parse(aQuery));
  1058   },
  1060   getCacheForQuery: function CDB_getCacheForQuery(aQuery, aSuccessCb, aFailureCb) {
  1061     if (DEBUG) debug("getCacheForQuery");
  1062     // Here we try to get the cached results for query `aQuery'. If they don't
  1063     // exist, it means the cache was invalidated and needs to be recreated, so
  1064     // we do that. Otherwise, we just return the existing cache.
  1065     this.newTxn("readonly", SAVED_GETALL_STORE_NAME, function(txn, store) {
  1066       let req = store.get(aQuery);
  1067       req.onsuccess = function(e) {
  1068         if (e.target.result) {
  1069           if (DEBUG) debug("cache exists");
  1070           aSuccessCb(e.target.result, false);
  1071         } else {
  1072           if (DEBUG) debug("creating cache for query " + aQuery);
  1073           this.createCacheForQuery(aQuery, aSuccessCb);
  1075       }.bind(this);
  1076       req.onerror = function(e) {
  1077         aFailureCb(e.target.errorMessage);
  1078       };
  1079     }.bind(this), null, aFailureCb);
  1080   },
  1082   sendNow: function CDB_sendNow(aCursorId) {
  1083     if (aCursorId in this._dispatcher) {
  1084       this._dispatcher[aCursorId].sendNow();
  1086   },
  1088   clearDispatcher: function CDB_clearDispatcher(aCursorId) {
  1089     if (DEBUG) debug("clearDispatcher: " + aCursorId);
  1090     if (aCursorId in this._dispatcher) {
  1091       delete this._dispatcher[aCursorId];
  1093   },
  1095   getAll: function CDB_getAll(aSuccessCb, aFailureCb, aOptions, aCursorId) {
  1096     if (DEBUG) debug("getAll")
  1097     let optionStr = JSON.stringify(aOptions);
  1098     this.getCacheForQuery(optionStr, function(aCachedResults, aFullContacts) {
  1099       // aFullContacts is true if the cache didn't exist and had to be created.
  1100       // In that case, we receive the full contacts since we already have them
  1101       // in memory to create the cache. This allows us to avoid accessing the
  1102       // object store again.
  1103       if (aCachedResults && aCachedResults.length > 0) {
  1104         let newTxnFn = this.newTxn.bind(this);
  1105         let clearDispatcherFn = this.clearDispatcher.bind(this, aCursorId);
  1106         this._dispatcher[aCursorId] = new ContactDispatcher(aCachedResults, aFullContacts,
  1107                                                             aSuccessCb, newTxnFn,
  1108                                                             clearDispatcherFn, aFailureCb);
  1109         this._dispatcher[aCursorId].sendNow();
  1110       } else { // no contacts
  1111         if (DEBUG) debug("query returned no contacts");
  1112         aSuccessCb(null);
  1114     }.bind(this), aFailureCb);
  1115   },
  1117   getRevision: function CDB_getRevision(aSuccessCb, aErrorCb) {
  1118     if (DEBUG) debug("getRevision");
  1119     this.newTxn("readonly", REVISION_STORE, function (txn, store) {
  1120       store.get(REVISION_KEY).onsuccess = function (e) {
  1121         aSuccessCb(e.target.result);
  1122       };
  1123     },null, aErrorCb);
  1124   },
  1126   getCount: function CDB_getCount(aSuccessCb, aErrorCb) {
  1127     if (DEBUG) debug("getCount");
  1128     this.newTxn("readonly", STORE_NAME, function (txn, store) {
  1129       store.count().onsuccess = function (e) {
  1130         aSuccessCb(e.target.result);
  1131       };
  1132     }, null, aErrorCb);
  1133   },
  1135   getSortByParam: function CDB_getSortByParam(aFindOptions) {
  1136     switch (aFindOptions.sortBy) {
  1137       case "familyName":
  1138         return [ "familyName", "givenName" ];
  1139       case "givenName":
  1140         return [ "givenName" , "familyName" ];
  1141       case "phoneticFamilyName":
  1142         return [ "phoneticFamilyName" , "phoneticGivenName" ];
  1143       case "phoneticGivenName":
  1144         return [ "phoneticGivenName" , "phoneticFamilyName" ];
  1145       default:
  1146         return [ "givenName" , "familyName" ];
  1148   },
  1150   /*
  1151    * Sorting the contacts by sortBy field. aSortBy can either be familyName or givenName.
  1152    * If 2 entries have the same sortyBy field or no sortBy field is present, we continue
  1153    * sorting with the other sortyBy field.
  1154    */
  1155   sortResults: function CDB_sortResults(aResults, aFindOptions) {
  1156     if (!aFindOptions)
  1157       return;
  1158     if (aFindOptions.sortBy != "undefined") {
  1159       const sortOrder = aFindOptions.sortOrder;
  1160       const sortBy = this.getSortByParam(aFindOptions);
  1162       aResults.sort(function (a, b) {
  1163         let x, y;
  1164         let result = 0;
  1165         let xIndex = 0;
  1166         let yIndex = 0;
  1168         do {
  1169           while (xIndex < sortBy.length && !x) {
  1170             x = a.properties[sortBy[xIndex]];
  1171             if (x) {
  1172               x = x.join("").toLowerCase();
  1174             xIndex++;
  1176           while (yIndex < sortBy.length && !y) {
  1177             y = b.properties[sortBy[yIndex]];
  1178             if (y) {
  1179               y = y.join("").toLowerCase();
  1181             yIndex++;
  1183           if (!x) {
  1184             if (!y) {
  1185               let px, py;
  1186               px = JSON.stringify(a.published);
  1187               py = JSON.stringify(b.published);
  1188               if (px && py) {
  1189                 return px.localeCompare(py);
  1191             } else {
  1192               return sortOrder == 'descending' ? 1 : -1;
  1195           if (!y) {
  1196             return sortOrder == "ascending" ? 1 : -1;
  1199           result = x.localeCompare(y);
  1200           x = null;
  1201           y = null;
  1202         } while (result == 0);
  1204         return sortOrder == "ascending" ? result : -result;
  1205       });
  1207     if (aFindOptions.filterLimit && aFindOptions.filterLimit != 0) {
  1208       if (DEBUG) debug("filterLimit is set: " + aFindOptions.filterLimit);
  1209       aResults.splice(aFindOptions.filterLimit, aResults.length);
  1211   },
  1213   /**
  1214    * @param successCb
  1215    *        Callback function to invoke with result array.
  1216    * @param failureCb [optional]
  1217    *        Callback function to invoke when there was an error.
  1218    * @param options [optional]
  1219    *        Object specifying search options. Possible attributes:
  1220    *        - filterBy
  1221    *        - filterOp
  1222    *        - filterValue
  1223    *        - count
  1224    */
  1225   find: function find(aSuccessCb, aFailureCb, aOptions) {
  1226     if (DEBUG) debug("ContactDB:find val:" + aOptions.filterValue + " by: " + aOptions.filterBy + " op: " + aOptions.filterOp);
  1227     let self = this;
  1228     this.newTxn("readonly", STORE_NAME, function (txn, store) {
  1229       let filterOps = ["equals", "contains", "match", "startsWith"];
  1230       if (aOptions && (filterOps.indexOf(aOptions.filterOp) >= 0)) {
  1231         self._findWithIndex(txn, store, aOptions);
  1232       } else {
  1233         self._findAll(txn, store, aOptions);
  1235     }, aSuccessCb, aFailureCb);
  1236   },
  1238   _findWithIndex: function _findWithIndex(txn, store, options) {
  1239     if (DEBUG) debug("_findWithIndex: " + options.filterValue +" " + options.filterOp + " " + options.filterBy + " ");
  1240     let fields = options.filterBy;
  1241     for (let key in fields) {
  1242       if (DEBUG) debug("key: " + fields[key]);
  1243       if (!store.indexNames.contains(fields[key]) && fields[key] != "id") {
  1244         if (DEBUG) debug("Key not valid!" + fields[key] + ", " + JSON.stringify(store.indexNames));
  1245         txn.abort();
  1246         return;
  1250     // lookup for all keys
  1251     if (options.filterBy.length == 0) {
  1252       if (DEBUG) debug("search in all fields!" + JSON.stringify(store.indexNames));
  1253       for(let myIndex = 0; myIndex < store.indexNames.length; myIndex++) {
  1254         fields = Array.concat(fields, store.indexNames[myIndex])
  1258     // Sorting functions takes care of limit if set.
  1259     let limit = options.sortBy === 'undefined' ? options.filterLimit : null;
  1261     let filter_keys = fields.slice();
  1262     for (let key = filter_keys.shift(); key; key = filter_keys.shift()) {
  1263       let request;
  1264       let substringResult = {};
  1265       if (key == "id") {
  1266         // store.get would return an object and not an array
  1267         request = store.mozGetAll(options.filterValue);
  1268       } else if (key == "category") {
  1269         let index = store.index(key);
  1270         request = index.mozGetAll(options.filterValue, limit);
  1271       } else if (options.filterOp == "equals") {
  1272         if (DEBUG) debug("Getting index: " + key);
  1273         // case sensitive
  1274         let index = store.index(key);
  1275         let filterValue = options.filterValue;
  1276         if (key == "tel") {
  1277           filterValue = PhoneNumberUtils.normalize(filterValue,
  1278                                                    /*numbersOnly*/ true);
  1280         request = index.mozGetAll(filterValue, limit);
  1281       } else if (options.filterOp == "match") {
  1282         if (DEBUG) debug("match");
  1283         if (key != "tel") {
  1284           dump("ContactDB: 'match' filterOp only works on tel\n");
  1285           return txn.abort();
  1288         let index = store.index("telMatch");
  1289         let normalized = PhoneNumberUtils.normalize(options.filterValue,
  1290                                                     /*numbersOnly*/ true);
  1292         if (!normalized.length) {
  1293           dump("ContactDB: normalized filterValue is empty, can't perform match search.\n");
  1294           return txn.abort();
  1297         // Some countries need special handling for number matching. Bug 877302
  1298         if (this.substringMatching && normalized.length > this.substringMatching) {
  1299           let substring = normalized.slice(-this.substringMatching);
  1300           if (DEBUG) debug("Substring: " + substring);
  1302           let substringRequest = index.mozGetAll(substring, limit);
  1304           substringRequest.onsuccess = function (event) {
  1305             if (DEBUG) debug("Request successful. Record count: " + event.target.result.length);
  1306             for (let i in event.target.result) {
  1307               substringResult[event.target.result[i].id] = event.target.result[i];
  1309           }.bind(this);
  1310         } else if (normalized[0] !== "+") {
  1311           // We might have an international prefix like '00'
  1312           let parsed = PhoneNumberUtils.parse(normalized);
  1313           if (parsed && parsed.internationalNumber &&
  1314               parsed.nationalNumber  &&
  1315               parsed.nationalNumber !== normalized &&
  1316               parsed.internationalNumber !== normalized) {
  1317             if (DEBUG) debug("Search with " + parsed.internationalNumber);
  1318             let prefixRequest = index.mozGetAll(parsed.internationalNumber, limit);
  1320             prefixRequest.onsuccess = function (event) {
  1321               if (DEBUG) debug("Request successful. Record count: " + event.target.result.length);
  1322               for (let i in event.target.result) {
  1323                 substringResult[event.target.result[i].id] = event.target.result[i];
  1325             }.bind(this);
  1329         request = index.mozGetAll(normalized, limit);
  1330       } else {
  1331         // XXX: "contains" should be handled separately, this is "startsWith"
  1332         if (options.filterOp === 'contains' && key !== 'tel') {
  1333           dump("ContactDB: 'contains' only works for 'tel'. Falling back " +
  1334                "to 'startsWith'.\n");
  1336         // not case sensitive
  1337         let lowerCase = options.filterValue.toString().toLowerCase();
  1338         if (key === "tel") {
  1339           let origLength = lowerCase.length;
  1340           let tmp = PhoneNumberUtils.normalize(lowerCase, /*numbersOnly*/ true);
  1341           if (tmp.length != origLength) {
  1342             let NON_SEARCHABLE_CHARS = /[^#+\*\d\s()-]/;
  1343             // e.g. number "123". find with "(123)" but not with "123a"
  1344             if (tmp === "" || NON_SEARCHABLE_CHARS.test(lowerCase)) {
  1345               if (DEBUG) debug("Call continue!");
  1346               continue;
  1348             lowerCase = tmp;
  1351         if (DEBUG) debug("lowerCase: " + lowerCase);
  1352         let range = IDBKeyRange.bound(lowerCase, lowerCase + "\uFFFF");
  1353         let index = store.index(key + "LowerCase");
  1354         request = index.mozGetAll(range, limit);
  1356       if (!txn.result)
  1357         txn.result = {};
  1359       request.onsuccess = function (event) {
  1360         if (DEBUG) debug("Request successful. Record count: " + event.target.result.length);
  1361         if (Object.keys(substringResult).length > 0) {
  1362           for (let attrname in substringResult) {
  1363             event.target.result[attrname] = substringResult[attrname];
  1366         this.sortResults(event.target.result, options);
  1367         for (let i in event.target.result)
  1368           txn.result[event.target.result[i].id] = exportContact(event.target.result[i]);
  1369       }.bind(this);
  1371   },
  1373   _findAll: function _findAll(txn, store, options) {
  1374     if (DEBUG) debug("ContactDB:_findAll:  " + JSON.stringify(options));
  1375     if (!txn.result)
  1376       txn.result = {};
  1377     // Sorting functions takes care of limit if set.
  1378     let limit = options.sortBy === 'undefined' ? options.filterLimit : null;
  1379     store.mozGetAll(null, limit).onsuccess = function (event) {
  1380       if (DEBUG) debug("Request successful. Record count:" + event.target.result.length);
  1381       this.sortResults(event.target.result, options);
  1382       for (let i in event.target.result) {
  1383         txn.result[event.target.result[i].id] = exportContact(event.target.result[i]);
  1385     }.bind(this);
  1386   },
  1388   // Enable special phone number substring matching. Does not update existing DB entries.
  1389   enableSubstringMatching: function enableSubstringMatching(aDigits) {
  1390     if (DEBUG) debug("MCC enabling substring matching " + aDigits);
  1391     this.substringMatching = aDigits;
  1392   },
  1394   disableSubstringMatching: function disableSubstringMatching() {
  1395     if (DEBUG) debug("MCC disabling substring matching");
  1396     delete this.substringMatching;
  1397   },
  1399   init: function init() {
  1400     this.initDBHelper(DB_NAME, DB_VERSION, [STORE_NAME, SAVED_GETALL_STORE_NAME, REVISION_STORE]);
  1402 };

mercurial