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