dom/contacts/fallback/ContactDB.jsm

Sat, 03 Jan 2015 20:18:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Sat, 03 Jan 2015 20:18:00 +0100
branch
TOR_BUG_3246
changeset 7
129ffea94266
permissions
-rw-r--r--

Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.

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

mercurial