|
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 }; |