dom/contacts/fallback/ContactDB.jsm

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:9dbc1d60575f
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/. */
4
5 "use strict";
6
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"];
10
11 const DEBUG = false;
12 function debug(s) { dump("-*- ContactDB component: " + s + "\n"); }
13
14 const Cu = Components.utils;
15 const Cc = Components.classes;
16 const Ci = Components.interfaces;
17
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"]);
22
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";
31
32 function exportContact(aRecord) {
33 if (aRecord) {
34 delete aRecord.search;
35 }
36 return aRecord;
37 }
38
39 function ContactDispatcher(aContacts, aFullContacts, aCallback, aNewTxn, aClearDispatcher, aFailureCb) {
40 let nextIndex = 0;
41
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 }
88
89 return {
90 sendNow: function() {
91 sendChunk();
92 }
93 };
94 }
95
96 this.ContactDB = function ContactDB() {
97 if (DEBUG) debug("Constructor");
98 };
99
100 ContactDB.prototype = {
101 __proto__: IndexedDBHelper.prototype,
102
103 _dispatcher: {},
104
105 useFastUpgrade: true,
106
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 }
124
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 }
142
143 let idService = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
144 objectStore = aTransaction.objectStore(STORE_NAME);
145
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);
156
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 }
179
180 let valueUpgradeSteps = [];
181
182 function scheduleValueUpgrade(upgradeFunc) {
183 var length = valueUpgradeSteps.push(upgradeFunc);
184 if (DEBUG) debug("Scheduled a value upgrade function, index " + (length - 1));
185 }
186
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;
192
193 if (aOldVersion === 0 && this.useFastUpgrade) {
194 createFinalSchema();
195 loadInitialContacts();
196 return;
197 }
198
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"});
214
215 // Properties indexes
216 objectStore.createIndex("familyName", "properties.familyName", { multiEntry: true });
217 objectStore.createIndex("givenName", "properties.givenName", { multiEntry: true });
218
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");
227
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 }
237
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 };
253
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 }
265
266 // Delete old email index.
267 if (objectStore.indexNames.contains("email")) {
268 objectStore.deleteIndex("email");
269 }
270
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 };
287
288 // Create new searchable indexes.
289 objectStore.createIndex("email", "search.email", { multiEntry: true });
290 },
291 function upgrade3to4() {
292 if (DEBUG) debug("upgrade 3");
293
294 if (!objectStore) {
295 objectStore = aTransaction.objectStore(STORE_NAME);
296 }
297
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 }
334
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 }
372
373 // Delete old tel index (not on the right field).
374 if (objectStore.indexNames.contains("tel")) {
375 objectStore.deleteIndex("tel");
376 }
377
378 // Create new index for "equals" searches
379 objectStore.createIndex("tel", "search.exactTel", { multiEntry: true });
380
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);
390
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 }
433
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 }
537
538 let modified = removeEmptyStrings(value.search.parsedTel);
539 let modified2 = removeEmptyStrings(value.search.tel);
540 return (modified || modified2);
541 });
542
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"];
553
554 scheduleValueUpgrade(function upgradeValue14to15(value) {
555 let changed = false;
556
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 }
575
576 return changed;
577 });
578
579 next();
580 },
581 function upgrade15to16() {
582 if (DEBUG) debug("Fix Date properties");
583 const DATE_PROPERTIES = ["bday", "anniversary"];
584
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 }
594
595 return changed;
596 });
597
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"];
607
608 const PROPERTIES_WITH_TYPE = ["adr", "email", "url", "impp", "tel"];
609
610 const DATE_PROPERTIES = ["bday", "anniversary"];
611
612 scheduleValueUpgrade(function upgradeValue16to17(value) {
613 let changed;
614
615 function filterInvalidValues(val) {
616 let shouldKeep = val != null; // null or undefined
617 if (!shouldKeep) {
618 changed = true;
619 }
620 return shouldKeep;
621 }
622
623 function filteredArray(array) {
624 return array.filter(filterInvalidValues);
625 }
626
627 let props = value.properties;
628
629 for (let prop of ARRAY_PROPERTIES) {
630
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 }
637
638 if (props[prop] && props[prop].length) {
639 props[prop] = filteredArray(props[prop]);
640
641 if (PROPERTIES_WITH_TYPE.indexOf(prop) !== -1) {
642 let subprop = props[prop];
643
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 }
655
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 }
664
665 if (changed) {
666 value.properties = props;
667 return true;
668 } else {
669 return false;
670 }
671 });
672
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 }
684
685 if (!objectStore) {
686 objectStore = aTransaction.objectStore(STORE_NAME);
687 }
688
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 }
695
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 });
709
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 ];
724
725 let index = aOldVersion;
726 let outer = this;
727
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 });
754
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 }
772
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");
777
778 outer.incrementRevision(aTransaction);
779 }
780
781 function next() {
782 if (index == aNewVersion) {
783 runValueUpgradeSteps(finish);
784 return;
785 }
786
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 }
797
798 function fail(why) {
799 why = why || "";
800 if (this.error) {
801 why += " (root cause: " + this.error.name + ")";
802 }
803
804 debug("Contacts DB upgrade error: " + why);
805 aTransaction.abort();
806 }
807
808 if (aNewVersion > steps.length) {
809 fail("No migration steps for the new version!");
810 }
811
812 this.cpuLock = Cc["@mozilla.org/power/powermanagerservice;1"]
813 .getService(Ci.nsIPowerManagerService)
814 .newWakeLock("cpu");
815
816 function unlockCPU() {
817 if (outer.cpuLock) {
818 if (DEBUG) debug("unlocking cpu wakelock");
819 outer.cpuLock.unlock();
820 outer.cpuLock = null;
821 }
822 }
823
824 aTransaction.addEventListener("complete", unlockCPU);
825 aTransaction.addEventListener("abort", unlockCPU);
826
827 next();
828 },
829
830 makeImport: function makeImport(aContact) {
831 let contact = {properties: {}};
832
833 contact.search = {
834 name: [],
835 givenName: [],
836 familyName: [],
837 email: [],
838 category: [],
839 tel: [],
840 exactTel: [],
841 parsedTel: [],
842 phoneticFamilyName: [],
843 phoneticGivenName: [],
844 };
845
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 = {};
858
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 }
883
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 }
921
922 contact.updated = aContact.updated;
923 contact.published = aContact.published;
924 contact.id = aContact.id;
925
926 return contact;
927 },
928
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 },
938
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 },
967
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 },
976
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 },
983
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));
988
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);
1008 }
1009 }
1010 this.invalidateCache(errorCb);
1011 }.bind(this);
1012
1013 this.incrementRevision(txn);
1014 }.bind(this), successCb, errorCb);
1015 },
1016
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 },
1028
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 },
1036
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]);
1043 }
1044
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);
1049
1050 // send full contacts
1051 aSuccessCb(contactsArray, true);
1052 } else {
1053 aSuccessCb([], true);
1054 }
1055 }.bind(this),
1056 function (aErrorMsg) { aFailureCb(aErrorMsg); },
1057 JSON.parse(aQuery));
1058 },
1059
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);
1074 }
1075 }.bind(this);
1076 req.onerror = function(e) {
1077 aFailureCb(e.target.errorMessage);
1078 };
1079 }.bind(this), null, aFailureCb);
1080 },
1081
1082 sendNow: function CDB_sendNow(aCursorId) {
1083 if (aCursorId in this._dispatcher) {
1084 this._dispatcher[aCursorId].sendNow();
1085 }
1086 },
1087
1088 clearDispatcher: function CDB_clearDispatcher(aCursorId) {
1089 if (DEBUG) debug("clearDispatcher: " + aCursorId);
1090 if (aCursorId in this._dispatcher) {
1091 delete this._dispatcher[aCursorId];
1092 }
1093 },
1094
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);
1113 }
1114 }.bind(this), aFailureCb);
1115 },
1116
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 },
1125
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 },
1134
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" ];
1147 }
1148 },
1149
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);
1161
1162 aResults.sort(function (a, b) {
1163 let x, y;
1164 let result = 0;
1165 let xIndex = 0;
1166 let yIndex = 0;
1167
1168 do {
1169 while (xIndex < sortBy.length && !x) {
1170 x = a.properties[sortBy[xIndex]];
1171 if (x) {
1172 x = x.join("").toLowerCase();
1173 }
1174 xIndex++;
1175 }
1176 while (yIndex < sortBy.length && !y) {
1177 y = b.properties[sortBy[yIndex]];
1178 if (y) {
1179 y = y.join("").toLowerCase();
1180 }
1181 yIndex++;
1182 }
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);
1190 }
1191 } else {
1192 return sortOrder == 'descending' ? 1 : -1;
1193 }
1194 }
1195 if (!y) {
1196 return sortOrder == "ascending" ? 1 : -1;
1197 }
1198
1199 result = x.localeCompare(y);
1200 x = null;
1201 y = null;
1202 } while (result == 0);
1203
1204 return sortOrder == "ascending" ? result : -result;
1205 });
1206 }
1207 if (aFindOptions.filterLimit && aFindOptions.filterLimit != 0) {
1208 if (DEBUG) debug("filterLimit is set: " + aFindOptions.filterLimit);
1209 aResults.splice(aFindOptions.filterLimit, aResults.length);
1210 }
1211 },
1212
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);
1234 }
1235 }, aSuccessCb, aFailureCb);
1236 },
1237
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;
1247 }
1248 }
1249
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])
1255 }
1256 }
1257
1258 // Sorting functions takes care of limit if set.
1259 let limit = options.sortBy === 'undefined' ? options.filterLimit : null;
1260
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);
1279 }
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();
1286 }
1287
1288 let index = store.index("telMatch");
1289 let normalized = PhoneNumberUtils.normalize(options.filterValue,
1290 /*numbersOnly*/ true);
1291
1292 if (!normalized.length) {
1293 dump("ContactDB: normalized filterValue is empty, can't perform match search.\n");
1294 return txn.abort();
1295 }
1296
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);
1301
1302 let substringRequest = index.mozGetAll(substring, limit);
1303
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];
1308 }
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);
1319
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];
1324 }
1325 }.bind(this);
1326 }
1327 }
1328
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");
1335 }
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;
1347 }
1348 lowerCase = tmp;
1349 }
1350 }
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);
1355 }
1356 if (!txn.result)
1357 txn.result = {};
1358
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];
1364 }
1365 }
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);
1370 }
1371 },
1372
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]);
1384 }
1385 }.bind(this);
1386 },
1387
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 },
1393
1394 disableSubstringMatching: function disableSubstringMatching() {
1395 if (DEBUG) debug("MCC disabling substring matching");
1396 delete this.substringMatching;
1397 },
1398
1399 init: function init() {
1400 this.initDBHelper(DB_NAME, DB_VERSION, [STORE_NAME, SAVED_GETALL_STORE_NAME, REVISION_STORE]);
1401 }
1402 };

mercurial