1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/dom/contacts/fallback/ContactDB.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,1402 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +// Everything but "ContactDB" is only exported here for testing. 1.11 +this.EXPORTED_SYMBOLS = ["ContactDB", "DB_NAME", "STORE_NAME", "SAVED_GETALL_STORE_NAME", 1.12 + "REVISION_STORE", "DB_VERSION"]; 1.13 + 1.14 +const DEBUG = false; 1.15 +function debug(s) { dump("-*- ContactDB component: " + s + "\n"); } 1.16 + 1.17 +const Cu = Components.utils; 1.18 +const Cc = Components.classes; 1.19 +const Ci = Components.interfaces; 1.20 + 1.21 +Cu.import("resource://gre/modules/Services.jsm"); 1.22 +Cu.import("resource://gre/modules/IndexedDBHelper.jsm"); 1.23 +Cu.import("resource://gre/modules/PhoneNumberUtils.jsm"); 1.24 +Cu.importGlobalProperties(["indexedDB"]); 1.25 + 1.26 +/* all exported symbols need to be bound to this on B2G - Bug 961777 */ 1.27 +this.DB_NAME = "contacts"; 1.28 +this.DB_VERSION = 20; 1.29 +this.STORE_NAME = "contacts"; 1.30 +this.SAVED_GETALL_STORE_NAME = "getallcache"; 1.31 +const CHUNK_SIZE = 20; 1.32 +this.REVISION_STORE = "revision"; 1.33 +const REVISION_KEY = "revision"; 1.34 + 1.35 +function exportContact(aRecord) { 1.36 + if (aRecord) { 1.37 + delete aRecord.search; 1.38 + } 1.39 + return aRecord; 1.40 +} 1.41 + 1.42 +function ContactDispatcher(aContacts, aFullContacts, aCallback, aNewTxn, aClearDispatcher, aFailureCb) { 1.43 + let nextIndex = 0; 1.44 + 1.45 + let sendChunk; 1.46 + let count = 0; 1.47 + if (aFullContacts) { 1.48 + sendChunk = function() { 1.49 + try { 1.50 + let chunk = aContacts.splice(0, CHUNK_SIZE); 1.51 + if (chunk.length > 0) { 1.52 + aCallback(chunk); 1.53 + } 1.54 + if (aContacts.length === 0) { 1.55 + aCallback(null); 1.56 + aClearDispatcher(); 1.57 + } 1.58 + } catch (e) { 1.59 + aClearDispatcher(); 1.60 + } 1.61 + } 1.62 + } else { 1.63 + sendChunk = function() { 1.64 + try { 1.65 + let start = nextIndex; 1.66 + nextIndex += CHUNK_SIZE; 1.67 + let chunk = []; 1.68 + aNewTxn("readonly", STORE_NAME, function(txn, store) { 1.69 + for (let i = start; i < Math.min(start+CHUNK_SIZE, aContacts.length); ++i) { 1.70 + store.get(aContacts[i]).onsuccess = function(e) { 1.71 + chunk.push(exportContact(e.target.result)); 1.72 + count++; 1.73 + if (count === aContacts.length) { 1.74 + aCallback(chunk); 1.75 + aCallback(null); 1.76 + aClearDispatcher(); 1.77 + } else if (chunk.length === CHUNK_SIZE) { 1.78 + aCallback(chunk); 1.79 + chunk.length = 0; 1.80 + } 1.81 + } 1.82 + } 1.83 + }, null, function(errorMsg) { 1.84 + aFailureCb(errorMsg); 1.85 + }); 1.86 + } catch (e) { 1.87 + aClearDispatcher(); 1.88 + } 1.89 + } 1.90 + } 1.91 + 1.92 + return { 1.93 + sendNow: function() { 1.94 + sendChunk(); 1.95 + } 1.96 + }; 1.97 +} 1.98 + 1.99 +this.ContactDB = function ContactDB() { 1.100 + if (DEBUG) debug("Constructor"); 1.101 +}; 1.102 + 1.103 +ContactDB.prototype = { 1.104 + __proto__: IndexedDBHelper.prototype, 1.105 + 1.106 + _dispatcher: {}, 1.107 + 1.108 + useFastUpgrade: true, 1.109 + 1.110 + upgradeSchema: function upgradeSchema(aTransaction, aDb, aOldVersion, aNewVersion) { 1.111 + let loadInitialContacts = function() { 1.112 + // Add default contacts 1.113 + let jsm = {}; 1.114 + Cu.import("resource://gre/modules/FileUtils.jsm", jsm); 1.115 + Cu.import("resource://gre/modules/NetUtil.jsm", jsm); 1.116 + // Loading resource://app/defaults/contacts.json doesn't work because 1.117 + // contacts.json is not in the omnijar. 1.118 + // So we look for the app dir instead and go from here... 1.119 + let contactsFile = jsm.FileUtils.getFile("DefRt", ["contacts.json"], false); 1.120 + if (!contactsFile || (contactsFile && !contactsFile.exists())) { 1.121 + // For b2g desktop 1.122 + contactsFile = jsm.FileUtils.getFile("ProfD", ["contacts.json"], false); 1.123 + if (!contactsFile || (contactsFile && !contactsFile.exists())) { 1.124 + return; 1.125 + } 1.126 + } 1.127 + 1.128 + let chan = jsm.NetUtil.newChannel(contactsFile); 1.129 + let stream = chan.open(); 1.130 + // Obtain a converter to read from a UTF-8 encoded input stream. 1.131 + let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] 1.132 + .createInstance(Ci.nsIScriptableUnicodeConverter); 1.133 + converter.charset = "UTF-8"; 1.134 + let rawstr = converter.ConvertToUnicode(jsm.NetUtil.readInputStreamToString( 1.135 + stream, 1.136 + stream.available()) || ""); 1.137 + stream.close(); 1.138 + let contacts; 1.139 + try { 1.140 + contacts = JSON.parse(rawstr); 1.141 + } catch(e) { 1.142 + if (DEBUG) debug("Error parsing " + contactsFile.path + " : " + e); 1.143 + return; 1.144 + } 1.145 + 1.146 + let idService = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator); 1.147 + objectStore = aTransaction.objectStore(STORE_NAME); 1.148 + 1.149 + for (let i = 0; i < contacts.length; i++) { 1.150 + let contact = {}; 1.151 + contact.properties = contacts[i]; 1.152 + contact.id = idService.generateUUID().toString().replace(/[{}-]/g, ""); 1.153 + contact = this.makeImport(contact); 1.154 + this.updateRecordMetadata(contact); 1.155 + if (DEBUG) debug("import: " + JSON.stringify(contact)); 1.156 + objectStore.put(contact); 1.157 + } 1.158 + }.bind(this); 1.159 + 1.160 + function createFinalSchema() { 1.161 + if (DEBUG) debug("creating final schema"); 1.162 + let objectStore = aDb.createObjectStore(STORE_NAME, {keyPath: "id"}); 1.163 + objectStore.createIndex("familyName", "properties.familyName", { multiEntry: true }); 1.164 + objectStore.createIndex("givenName", "properties.givenName", { multiEntry: true }); 1.165 + objectStore.createIndex("name", "properties.name", { multiEntry: true }); 1.166 + objectStore.createIndex("familyNameLowerCase", "search.familyName", { multiEntry: true }); 1.167 + objectStore.createIndex("givenNameLowerCase", "search.givenName", { multiEntry: true }); 1.168 + objectStore.createIndex("nameLowerCase", "search.name", { multiEntry: true }); 1.169 + objectStore.createIndex("telLowerCase", "search.tel", { multiEntry: true }); 1.170 + objectStore.createIndex("emailLowerCase", "search.email", { multiEntry: true }); 1.171 + objectStore.createIndex("tel", "search.exactTel", { multiEntry: true }); 1.172 + objectStore.createIndex("category", "properties.category", { multiEntry: true }); 1.173 + objectStore.createIndex("email", "search.email", { multiEntry: true }); 1.174 + objectStore.createIndex("telMatch", "search.parsedTel", {multiEntry: true}); 1.175 + objectStore.createIndex("phoneticFamilyName", "properties.phoneticFamilyName", { multiEntry: true }); 1.176 + objectStore.createIndex("phoneticGivenName", "properties.phoneticGivenName", { multiEntry: true }); 1.177 + objectStore.createIndex("phoneticFamilyNameLowerCase", "search.phoneticFamilyName", { multiEntry: true }); 1.178 + objectStore.createIndex("phoneticGivenNameLowerCase", "search.phoneticGivenName", { multiEntry: true }); 1.179 + aDb.createObjectStore(SAVED_GETALL_STORE_NAME); 1.180 + aDb.createObjectStore(REVISION_STORE).put(0, REVISION_KEY); 1.181 + } 1.182 + 1.183 + let valueUpgradeSteps = []; 1.184 + 1.185 + function scheduleValueUpgrade(upgradeFunc) { 1.186 + var length = valueUpgradeSteps.push(upgradeFunc); 1.187 + if (DEBUG) debug("Scheduled a value upgrade function, index " + (length - 1)); 1.188 + } 1.189 + 1.190 + // We always output this debug line because it's useful and the noise ratio 1.191 + // very low. 1.192 + debug("upgrade schema from: " + aOldVersion + " to " + aNewVersion + " called!"); 1.193 + let db = aDb; 1.194 + let objectStore; 1.195 + 1.196 + if (aOldVersion === 0 && this.useFastUpgrade) { 1.197 + createFinalSchema(); 1.198 + loadInitialContacts(); 1.199 + return; 1.200 + } 1.201 + 1.202 + let steps = [ 1.203 + function upgrade0to1() { 1.204 + /** 1.205 + * Create the initial database schema. 1.206 + * 1.207 + * The schema of records stored is as follows: 1.208 + * 1.209 + * {id: "...", // UUID 1.210 + * published: Date(...), // First published date. 1.211 + * updated: Date(...), // Last updated date. 1.212 + * properties: {...} // Object holding the ContactProperties 1.213 + * } 1.214 + */ 1.215 + if (DEBUG) debug("create schema"); 1.216 + objectStore = db.createObjectStore(STORE_NAME, {keyPath: "id"}); 1.217 + 1.218 + // Properties indexes 1.219 + objectStore.createIndex("familyName", "properties.familyName", { multiEntry: true }); 1.220 + objectStore.createIndex("givenName", "properties.givenName", { multiEntry: true }); 1.221 + 1.222 + objectStore.createIndex("familyNameLowerCase", "search.familyName", { multiEntry: true }); 1.223 + objectStore.createIndex("givenNameLowerCase", "search.givenName", { multiEntry: true }); 1.224 + objectStore.createIndex("telLowerCase", "search.tel", { multiEntry: true }); 1.225 + objectStore.createIndex("emailLowerCase", "search.email", { multiEntry: true }); 1.226 + next(); 1.227 + }, 1.228 + function upgrade1to2() { 1.229 + if (DEBUG) debug("upgrade 1"); 1.230 + 1.231 + // Create a new scheme for the tel field. We move from an array of tel-numbers to an array of 1.232 + // ContactTelephone. 1.233 + if (!objectStore) { 1.234 + objectStore = aTransaction.objectStore(STORE_NAME); 1.235 + } 1.236 + // Delete old tel index. 1.237 + if (objectStore.indexNames.contains("tel")) { 1.238 + objectStore.deleteIndex("tel"); 1.239 + } 1.240 + 1.241 + // Upgrade existing tel field in the DB. 1.242 + objectStore.openCursor().onsuccess = function(event) { 1.243 + let cursor = event.target.result; 1.244 + if (cursor) { 1.245 + if (DEBUG) debug("upgrade tel1: " + JSON.stringify(cursor.value)); 1.246 + for (let number in cursor.value.properties.tel) { 1.247 + cursor.value.properties.tel[number] = {number: number}; 1.248 + } 1.249 + cursor.update(cursor.value); 1.250 + if (DEBUG) debug("upgrade tel2: " + JSON.stringify(cursor.value)); 1.251 + cursor.continue(); 1.252 + } else { 1.253 + next(); 1.254 + } 1.255 + }; 1.256 + 1.257 + // Create new searchable indexes. 1.258 + objectStore.createIndex("tel", "search.tel", { multiEntry: true }); 1.259 + objectStore.createIndex("category", "properties.category", { multiEntry: true }); 1.260 + }, 1.261 + function upgrade2to3() { 1.262 + if (DEBUG) debug("upgrade 2"); 1.263 + // Create a new scheme for the email field. We move from an array of emailaddresses to an array of 1.264 + // ContactEmail. 1.265 + if (!objectStore) { 1.266 + objectStore = aTransaction.objectStore(STORE_NAME); 1.267 + } 1.268 + 1.269 + // Delete old email index. 1.270 + if (objectStore.indexNames.contains("email")) { 1.271 + objectStore.deleteIndex("email"); 1.272 + } 1.273 + 1.274 + // Upgrade existing email field in the DB. 1.275 + objectStore.openCursor().onsuccess = function(event) { 1.276 + let cursor = event.target.result; 1.277 + if (cursor) { 1.278 + if (cursor.value.properties.email) { 1.279 + if (DEBUG) debug("upgrade email1: " + JSON.stringify(cursor.value)); 1.280 + cursor.value.properties.email = 1.281 + cursor.value.properties.email.map(function(address) { return { address: address }; }); 1.282 + cursor.update(cursor.value); 1.283 + if (DEBUG) debug("upgrade email2: " + JSON.stringify(cursor.value)); 1.284 + } 1.285 + cursor.continue(); 1.286 + } else { 1.287 + next(); 1.288 + } 1.289 + }; 1.290 + 1.291 + // Create new searchable indexes. 1.292 + objectStore.createIndex("email", "search.email", { multiEntry: true }); 1.293 + }, 1.294 + function upgrade3to4() { 1.295 + if (DEBUG) debug("upgrade 3"); 1.296 + 1.297 + if (!objectStore) { 1.298 + objectStore = aTransaction.objectStore(STORE_NAME); 1.299 + } 1.300 + 1.301 + // Upgrade existing impp field in the DB. 1.302 + objectStore.openCursor().onsuccess = function(event) { 1.303 + let cursor = event.target.result; 1.304 + if (cursor) { 1.305 + if (cursor.value.properties.impp) { 1.306 + if (DEBUG) debug("upgrade impp1: " + JSON.stringify(cursor.value)); 1.307 + cursor.value.properties.impp = 1.308 + cursor.value.properties.impp.map(function(value) { return { value: value }; }); 1.309 + cursor.update(cursor.value); 1.310 + if (DEBUG) debug("upgrade impp2: " + JSON.stringify(cursor.value)); 1.311 + } 1.312 + cursor.continue(); 1.313 + } 1.314 + }; 1.315 + // Upgrade existing url field in the DB. 1.316 + objectStore.openCursor().onsuccess = function(event) { 1.317 + let cursor = event.target.result; 1.318 + if (cursor) { 1.319 + if (cursor.value.properties.url) { 1.320 + if (DEBUG) debug("upgrade url1: " + JSON.stringify(cursor.value)); 1.321 + cursor.value.properties.url = 1.322 + cursor.value.properties.url.map(function(value) { return { value: value }; }); 1.323 + cursor.update(cursor.value); 1.324 + if (DEBUG) debug("upgrade impp2: " + JSON.stringify(cursor.value)); 1.325 + } 1.326 + cursor.continue(); 1.327 + } else { 1.328 + next(); 1.329 + } 1.330 + }; 1.331 + }, 1.332 + function upgrade4to5() { 1.333 + if (DEBUG) debug("Add international phone numbers upgrade"); 1.334 + if (!objectStore) { 1.335 + objectStore = aTransaction.objectStore(STORE_NAME); 1.336 + } 1.337 + 1.338 + objectStore.openCursor().onsuccess = function(event) { 1.339 + let cursor = event.target.result; 1.340 + if (cursor) { 1.341 + if (cursor.value.properties.tel) { 1.342 + if (DEBUG) debug("upgrade : " + JSON.stringify(cursor.value)); 1.343 + cursor.value.properties.tel.forEach( 1.344 + function(duple) { 1.345 + let parsedNumber = PhoneNumberUtils.parse(duple.value.toString()); 1.346 + if (parsedNumber) { 1.347 + if (DEBUG) { 1.348 + debug("InternationalFormat: " + parsedNumber.internationalFormat); 1.349 + debug("InternationalNumber: " + parsedNumber.internationalNumber); 1.350 + debug("NationalNumber: " + parsedNumber.nationalNumber); 1.351 + debug("NationalFormat: " + parsedNumber.nationalFormat); 1.352 + } 1.353 + if (duple.value.toString() !== parsedNumber.internationalNumber) { 1.354 + cursor.value.search.tel.push(parsedNumber.internationalNumber); 1.355 + } 1.356 + } else { 1.357 + dump("Warning: No international number found for " + duple.value + "\n"); 1.358 + } 1.359 + } 1.360 + ) 1.361 + cursor.update(cursor.value); 1.362 + } 1.363 + if (DEBUG) debug("upgrade2 : " + JSON.stringify(cursor.value)); 1.364 + cursor.continue(); 1.365 + } else { 1.366 + next(); 1.367 + } 1.368 + }; 1.369 + }, 1.370 + function upgrade5to6() { 1.371 + if (DEBUG) debug("Add index for equals tel searches"); 1.372 + if (!objectStore) { 1.373 + objectStore = aTransaction.objectStore(STORE_NAME); 1.374 + } 1.375 + 1.376 + // Delete old tel index (not on the right field). 1.377 + if (objectStore.indexNames.contains("tel")) { 1.378 + objectStore.deleteIndex("tel"); 1.379 + } 1.380 + 1.381 + // Create new index for "equals" searches 1.382 + objectStore.createIndex("tel", "search.exactTel", { multiEntry: true }); 1.383 + 1.384 + objectStore.openCursor().onsuccess = function(event) { 1.385 + let cursor = event.target.result; 1.386 + if (cursor) { 1.387 + if (cursor.value.properties.tel) { 1.388 + if (DEBUG) debug("upgrade : " + JSON.stringify(cursor.value)); 1.389 + cursor.value.properties.tel.forEach( 1.390 + function(duple) { 1.391 + let number = duple.value.toString(); 1.392 + let parsedNumber = PhoneNumberUtils.parse(number); 1.393 + 1.394 + cursor.value.search.exactTel = [number]; 1.395 + if (parsedNumber && 1.396 + parsedNumber.internationalNumber && 1.397 + number !== parsedNumber.internationalNumber) { 1.398 + cursor.value.search.exactTel.push(parsedNumber.internationalNumber); 1.399 + } 1.400 + } 1.401 + ) 1.402 + cursor.update(cursor.value); 1.403 + } 1.404 + if (DEBUG) debug("upgrade : " + JSON.stringify(cursor.value)); 1.405 + cursor.continue(); 1.406 + } else { 1.407 + next(); 1.408 + } 1.409 + }; 1.410 + }, 1.411 + function upgrade6to7() { 1.412 + if (!objectStore) { 1.413 + objectStore = aTransaction.objectStore(STORE_NAME); 1.414 + } 1.415 + let names = objectStore.indexNames; 1.416 + let whiteList = ["tel", "familyName", "givenName", "familyNameLowerCase", 1.417 + "givenNameLowerCase", "telLowerCase", "category", "email", 1.418 + "emailLowerCase"]; 1.419 + for (var i = 0; i < names.length; i++) { 1.420 + if (whiteList.indexOf(names[i]) < 0) { 1.421 + objectStore.deleteIndex(names[i]); 1.422 + } 1.423 + } 1.424 + next(); 1.425 + }, 1.426 + function upgrade7to8() { 1.427 + if (DEBUG) debug("Adding object store for cached searches"); 1.428 + db.createObjectStore(SAVED_GETALL_STORE_NAME); 1.429 + next(); 1.430 + }, 1.431 + function upgrade8to9() { 1.432 + if (DEBUG) debug("Make exactTel only contain the value entered by the user"); 1.433 + if (!objectStore) { 1.434 + objectStore = aTransaction.objectStore(STORE_NAME); 1.435 + } 1.436 + 1.437 + objectStore.openCursor().onsuccess = function(event) { 1.438 + let cursor = event.target.result; 1.439 + if (cursor) { 1.440 + if (cursor.value.properties.tel) { 1.441 + cursor.value.search.exactTel = []; 1.442 + cursor.value.properties.tel.forEach( 1.443 + function(tel) { 1.444 + let normalized = PhoneNumberUtils.normalize(tel.value.toString()); 1.445 + cursor.value.search.exactTel.push(normalized); 1.446 + } 1.447 + ); 1.448 + cursor.update(cursor.value); 1.449 + } 1.450 + cursor.continue(); 1.451 + } else { 1.452 + next(); 1.453 + } 1.454 + }; 1.455 + }, 1.456 + function upgrade9to10() { 1.457 + // no-op, see https://bugzilla.mozilla.org/show_bug.cgi?id=883770#c16 1.458 + next(); 1.459 + }, 1.460 + function upgrade10to11() { 1.461 + if (DEBUG) debug("Adding object store for database revision"); 1.462 + db.createObjectStore(REVISION_STORE).put(0, REVISION_KEY); 1.463 + next(); 1.464 + }, 1.465 + function upgrade11to12() { 1.466 + if (DEBUG) debug("Add a telMatch index with national and international numbers"); 1.467 + if (!objectStore) { 1.468 + objectStore = aTransaction.objectStore(STORE_NAME); 1.469 + } 1.470 + if (!objectStore.indexNames.contains("telMatch")) { 1.471 + objectStore.createIndex("telMatch", "search.parsedTel", {multiEntry: true}); 1.472 + } 1.473 + objectStore.openCursor().onsuccess = function(event) { 1.474 + let cursor = event.target.result; 1.475 + if (cursor) { 1.476 + if (cursor.value.properties.tel) { 1.477 + cursor.value.search.parsedTel = []; 1.478 + cursor.value.properties.tel.forEach( 1.479 + function(tel) { 1.480 + let parsed = PhoneNumberUtils.parse(tel.value.toString()); 1.481 + if (parsed) { 1.482 + cursor.value.search.parsedTel.push(parsed.nationalNumber); 1.483 + cursor.value.search.parsedTel.push(PhoneNumberUtils.normalize(parsed.nationalFormat)); 1.484 + cursor.value.search.parsedTel.push(parsed.internationalNumber); 1.485 + cursor.value.search.parsedTel.push(PhoneNumberUtils.normalize(parsed.internationalFormat)); 1.486 + } 1.487 + cursor.value.search.parsedTel.push(PhoneNumberUtils.normalize(tel.value.toString())); 1.488 + } 1.489 + ); 1.490 + cursor.update(cursor.value); 1.491 + } 1.492 + cursor.continue(); 1.493 + } else { 1.494 + next(); 1.495 + } 1.496 + }; 1.497 + }, 1.498 + function upgrade12to13() { 1.499 + if (DEBUG) debug("Add phone substring to the search index if appropriate for country"); 1.500 + if (this.substringMatching) { 1.501 + scheduleValueUpgrade(function upgradeValue12to13(value) { 1.502 + if (value.properties.tel) { 1.503 + value.search.parsedTel = value.search.parsedTel || []; 1.504 + value.properties.tel.forEach( 1.505 + function(tel) { 1.506 + let normalized = PhoneNumberUtils.normalize(tel.value.toString()); 1.507 + if (normalized) { 1.508 + if (this.substringMatching && normalized.length > this.substringMatching) { 1.509 + let sub = normalized.slice(-this.substringMatching); 1.510 + if (value.search.parsedTel.indexOf(sub) === -1) { 1.511 + if (DEBUG) debug("Adding substring index: " + tel + ", " + sub); 1.512 + value.search.parsedTel.push(sub); 1.513 + } 1.514 + } 1.515 + } 1.516 + }.bind(this) 1.517 + ); 1.518 + return true; 1.519 + } else { 1.520 + return false; 1.521 + } 1.522 + }.bind(this)); 1.523 + } 1.524 + next(); 1.525 + }, 1.526 + function upgrade13to14() { 1.527 + if (DEBUG) debug("Cleaning up empty substring entries in telMatch index"); 1.528 + scheduleValueUpgrade(function upgradeValue13to14(value) { 1.529 + function removeEmptyStrings(value) { 1.530 + if (value) { 1.531 + const oldLength = value.length; 1.532 + for (let i = 0; i < value.length; ++i) { 1.533 + if (!value[i] || value[i] == "null") { 1.534 + value.splice(i, 1); 1.535 + } 1.536 + } 1.537 + return oldLength !== value.length; 1.538 + } 1.539 + } 1.540 + 1.541 + let modified = removeEmptyStrings(value.search.parsedTel); 1.542 + let modified2 = removeEmptyStrings(value.search.tel); 1.543 + return (modified || modified2); 1.544 + }); 1.545 + 1.546 + next(); 1.547 + }, 1.548 + function upgrade14to15() { 1.549 + if (DEBUG) debug("Fix array properties saved as scalars"); 1.550 + const ARRAY_PROPERTIES = ["photo", "adr", "email", "url", "impp", "tel", 1.551 + "name", "honorificPrefix", "givenName", 1.552 + "additionalName", "familyName", "honorificSuffix", 1.553 + "nickname", "category", "org", "jobTitle", 1.554 + "note", "key"]; 1.555 + const PROPERTIES_WITH_TYPE = ["adr", "email", "url", "impp", "tel"]; 1.556 + 1.557 + scheduleValueUpgrade(function upgradeValue14to15(value) { 1.558 + let changed = false; 1.559 + 1.560 + let props = value.properties; 1.561 + for (let prop of ARRAY_PROPERTIES) { 1.562 + if (props[prop]) { 1.563 + if (!Array.isArray(props[prop])) { 1.564 + value.properties[prop] = [props[prop]]; 1.565 + changed = true; 1.566 + } 1.567 + if (PROPERTIES_WITH_TYPE.indexOf(prop) !== -1) { 1.568 + let subprop = value.properties[prop]; 1.569 + for (let i = 0; i < subprop.length; ++i) { 1.570 + if (!Array.isArray(subprop[i].type)) { 1.571 + value.properties[prop][i].type = [subprop[i].type]; 1.572 + changed = true; 1.573 + } 1.574 + } 1.575 + } 1.576 + } 1.577 + } 1.578 + 1.579 + return changed; 1.580 + }); 1.581 + 1.582 + next(); 1.583 + }, 1.584 + function upgrade15to16() { 1.585 + if (DEBUG) debug("Fix Date properties"); 1.586 + const DATE_PROPERTIES = ["bday", "anniversary"]; 1.587 + 1.588 + scheduleValueUpgrade(function upgradeValue15to16(value) { 1.589 + let changed = false; 1.590 + let props = value.properties; 1.591 + for (let prop of DATE_PROPERTIES) { 1.592 + if (props[prop] && !(props[prop] instanceof Date)) { 1.593 + value.properties[prop] = new Date(props[prop]); 1.594 + changed = true; 1.595 + } 1.596 + } 1.597 + 1.598 + return changed; 1.599 + }); 1.600 + 1.601 + next(); 1.602 + }, 1.603 + function upgrade16to17() { 1.604 + if (DEBUG) debug("Fix array with null values"); 1.605 + const ARRAY_PROPERTIES = ["photo", "adr", "email", "url", "impp", "tel", 1.606 + "name", "honorificPrefix", "givenName", 1.607 + "additionalName", "familyName", "honorificSuffix", 1.608 + "nickname", "category", "org", "jobTitle", 1.609 + "note", "key"]; 1.610 + 1.611 + const PROPERTIES_WITH_TYPE = ["adr", "email", "url", "impp", "tel"]; 1.612 + 1.613 + const DATE_PROPERTIES = ["bday", "anniversary"]; 1.614 + 1.615 + scheduleValueUpgrade(function upgradeValue16to17(value) { 1.616 + let changed; 1.617 + 1.618 + function filterInvalidValues(val) { 1.619 + let shouldKeep = val != null; // null or undefined 1.620 + if (!shouldKeep) { 1.621 + changed = true; 1.622 + } 1.623 + return shouldKeep; 1.624 + } 1.625 + 1.626 + function filteredArray(array) { 1.627 + return array.filter(filterInvalidValues); 1.628 + } 1.629 + 1.630 + let props = value.properties; 1.631 + 1.632 + for (let prop of ARRAY_PROPERTIES) { 1.633 + 1.634 + // properties that were empty strings weren't converted to arrays 1.635 + // in upgrade14to15 1.636 + if (props[prop] != null && !Array.isArray(props[prop])) { 1.637 + props[prop] = [props[prop]]; 1.638 + changed = true; 1.639 + } 1.640 + 1.641 + if (props[prop] && props[prop].length) { 1.642 + props[prop] = filteredArray(props[prop]); 1.643 + 1.644 + if (PROPERTIES_WITH_TYPE.indexOf(prop) !== -1) { 1.645 + let subprop = props[prop]; 1.646 + 1.647 + for (let i = 0; i < subprop.length; ++i) { 1.648 + let curSubprop = subprop[i]; 1.649 + // upgrade14to15 transformed type props into an array 1.650 + // without checking invalid values 1.651 + if (curSubprop.type) { 1.652 + curSubprop.type = filteredArray(curSubprop.type); 1.653 + } 1.654 + } 1.655 + } 1.656 + } 1.657 + } 1.658 + 1.659 + for (let prop of DATE_PROPERTIES) { 1.660 + if (props[prop] != null && !(props[prop] instanceof Date)) { 1.661 + // props[prop] is probably '' and wasn't converted 1.662 + // in upgrade15to16 1.663 + props[prop] = null; 1.664 + changed = true; 1.665 + } 1.666 + } 1.667 + 1.668 + if (changed) { 1.669 + value.properties = props; 1.670 + return true; 1.671 + } else { 1.672 + return false; 1.673 + } 1.674 + }); 1.675 + 1.676 + next(); 1.677 + }, 1.678 + function upgrade17to18() { 1.679 + // this upgrade function has been moved to the next upgrade path because 1.680 + // a previous version of it had a bug 1.681 + next(); 1.682 + }, 1.683 + function upgrade18to19() { 1.684 + if (DEBUG) { 1.685 + debug("Adding the name index"); 1.686 + } 1.687 + 1.688 + if (!objectStore) { 1.689 + objectStore = aTransaction.objectStore(STORE_NAME); 1.690 + } 1.691 + 1.692 + // an earlier version of this code could have run, so checking whether 1.693 + // the index exists 1.694 + if (!objectStore.indexNames.contains("name")) { 1.695 + objectStore.createIndex("name", "properties.name", { multiEntry: true }); 1.696 + objectStore.createIndex("nameLowerCase", "search.name", { multiEntry: true }); 1.697 + } 1.698 + 1.699 + scheduleValueUpgrade(function upgradeValue18to19(value) { 1.700 + value.search.name = []; 1.701 + if (value.properties.name) { 1.702 + value.properties.name.forEach(function addNameIndex(name) { 1.703 + var lowerName = name.toLowerCase(); 1.704 + // an earlier version of this code could have added it already 1.705 + if (value.search.name.indexOf(lowerName) === -1) { 1.706 + value.search.name.push(lowerName); 1.707 + } 1.708 + }); 1.709 + } 1.710 + return true; 1.711 + }); 1.712 + 1.713 + next(); 1.714 + }, 1.715 + function upgrade19to20() { 1.716 + if (DEBUG) debug("upgrade19to20 create schema(phonetic)"); 1.717 + if (!objectStore) { 1.718 + objectStore = aTransaction.objectStore(STORE_NAME); 1.719 + } 1.720 + objectStore.createIndex("phoneticFamilyName", "properties.phoneticFamilyName", { multiEntry: true }); 1.721 + objectStore.createIndex("phoneticGivenName", "properties.phoneticGivenName", { multiEntry: true }); 1.722 + objectStore.createIndex("phoneticFamilyNameLowerCase", "search.phoneticFamilyName", { multiEntry: true }); 1.723 + objectStore.createIndex("phoneticGivenNameLowerCase", "search.phoneticGivenName", { multiEntry: true }); 1.724 + next(); 1.725 + }, 1.726 + ]; 1.727 + 1.728 + let index = aOldVersion; 1.729 + let outer = this; 1.730 + 1.731 + /* This function runs all upgrade functions that are in the 1.732 + * valueUpgradeSteps array. These functions have the following properties: 1.733 + * - they must be synchronous 1.734 + * - they must take the value as parameter and modify it directly. They 1.735 + * must not create a new object. 1.736 + * - they must return a boolean true/false; true if the value was actually 1.737 + * changed 1.738 + */ 1.739 + function runValueUpgradeSteps(done) { 1.740 + if (DEBUG) debug("Running the value upgrade functions."); 1.741 + if (!objectStore) { 1.742 + objectStore = aTransaction.objectStore(STORE_NAME); 1.743 + } 1.744 + objectStore.openCursor().onsuccess = function(event) { 1.745 + let cursor = event.target.result; 1.746 + if (cursor) { 1.747 + let changed = false; 1.748 + let oldValue; 1.749 + let value = cursor.value; 1.750 + if (DEBUG) { 1.751 + oldValue = JSON.stringify(value); 1.752 + } 1.753 + valueUpgradeSteps.forEach(function(upgradeFunc, i) { 1.754 + if (DEBUG) debug("Running upgrade function " + i); 1.755 + changed = upgradeFunc(value) || changed; 1.756 + }); 1.757 + 1.758 + if (changed) { 1.759 + cursor.update(value); 1.760 + } else if (DEBUG) { 1.761 + let newValue = JSON.stringify(value); 1.762 + if (newValue !== oldValue) { 1.763 + // oops something went wrong 1.764 + debug("upgrade: `changed` was false and still the value changed! Aborting."); 1.765 + aTransaction.abort(); 1.766 + return; 1.767 + } 1.768 + } 1.769 + cursor.continue(); 1.770 + } else { 1.771 + done(); 1.772 + } 1.773 + }; 1.774 + } 1.775 + 1.776 + function finish() { 1.777 + // We always output this debug line because it's useful and the noise ratio 1.778 + // very low. 1.779 + debug("Upgrade finished"); 1.780 + 1.781 + outer.incrementRevision(aTransaction); 1.782 + } 1.783 + 1.784 + function next() { 1.785 + if (index == aNewVersion) { 1.786 + runValueUpgradeSteps(finish); 1.787 + return; 1.788 + } 1.789 + 1.790 + try { 1.791 + var i = index++; 1.792 + if (DEBUG) debug("Upgrade step: " + i + "\n"); 1.793 + steps[i].call(outer); 1.794 + } catch(ex) { 1.795 + dump("Caught exception" + ex); 1.796 + aTransaction.abort(); 1.797 + return; 1.798 + } 1.799 + } 1.800 + 1.801 + function fail(why) { 1.802 + why = why || ""; 1.803 + if (this.error) { 1.804 + why += " (root cause: " + this.error.name + ")"; 1.805 + } 1.806 + 1.807 + debug("Contacts DB upgrade error: " + why); 1.808 + aTransaction.abort(); 1.809 + } 1.810 + 1.811 + if (aNewVersion > steps.length) { 1.812 + fail("No migration steps for the new version!"); 1.813 + } 1.814 + 1.815 + this.cpuLock = Cc["@mozilla.org/power/powermanagerservice;1"] 1.816 + .getService(Ci.nsIPowerManagerService) 1.817 + .newWakeLock("cpu"); 1.818 + 1.819 + function unlockCPU() { 1.820 + if (outer.cpuLock) { 1.821 + if (DEBUG) debug("unlocking cpu wakelock"); 1.822 + outer.cpuLock.unlock(); 1.823 + outer.cpuLock = null; 1.824 + } 1.825 + } 1.826 + 1.827 + aTransaction.addEventListener("complete", unlockCPU); 1.828 + aTransaction.addEventListener("abort", unlockCPU); 1.829 + 1.830 + next(); 1.831 + }, 1.832 + 1.833 + makeImport: function makeImport(aContact) { 1.834 + let contact = {properties: {}}; 1.835 + 1.836 + contact.search = { 1.837 + name: [], 1.838 + givenName: [], 1.839 + familyName: [], 1.840 + email: [], 1.841 + category: [], 1.842 + tel: [], 1.843 + exactTel: [], 1.844 + parsedTel: [], 1.845 + phoneticFamilyName: [], 1.846 + phoneticGivenName: [], 1.847 + }; 1.848 + 1.849 + for (let field in aContact.properties) { 1.850 + contact.properties[field] = aContact.properties[field]; 1.851 + // Add search fields 1.852 + if (aContact.properties[field] && contact.search[field]) { 1.853 + for (let i = 0; i <= aContact.properties[field].length; i++) { 1.854 + if (aContact.properties[field][i]) { 1.855 + if (field == "tel" && aContact.properties[field][i].value) { 1.856 + let number = aContact.properties.tel[i].value.toString(); 1.857 + let normalized = PhoneNumberUtils.normalize(number); 1.858 + // We use an object here to avoid duplicates 1.859 + let containsSearch = {}; 1.860 + let matchSearch = {}; 1.861 + 1.862 + if (normalized) { 1.863 + // exactTel holds normalized version of entered phone number. 1.864 + // normalized: +1 (949) 123 - 4567 -> +19491234567 1.865 + contact.search.exactTel.push(normalized); 1.866 + // matchSearch holds normalized version of entered phone number, 1.867 + // nationalNumber, nationalFormat, internationalNumber, internationalFormat 1.868 + matchSearch[normalized] = 1; 1.869 + let parsedNumber = PhoneNumberUtils.parse(number); 1.870 + if (parsedNumber) { 1.871 + if (DEBUG) { 1.872 + debug("InternationalFormat: " + parsedNumber.internationalFormat); 1.873 + debug("InternationalNumber: " + parsedNumber.internationalNumber); 1.874 + debug("NationalNumber: " + parsedNumber.nationalNumber); 1.875 + debug("NationalFormat: " + parsedNumber.nationalFormat); 1.876 + debug("NationalMatchingFormat: " + parsedNumber.nationalMatchingFormat); 1.877 + } 1.878 + matchSearch[parsedNumber.nationalNumber] = 1; 1.879 + matchSearch[parsedNumber.internationalNumber] = 1; 1.880 + matchSearch[PhoneNumberUtils.normalize(parsedNumber.nationalFormat)] = 1; 1.881 + matchSearch[PhoneNumberUtils.normalize(parsedNumber.internationalFormat)] = 1; 1.882 + matchSearch[PhoneNumberUtils.normalize(parsedNumber.nationalMatchingFormat)] = 1; 1.883 + } else if (this.substringMatching && normalized.length > this.substringMatching) { 1.884 + matchSearch[normalized.slice(-this.substringMatching)] = 1; 1.885 + } 1.886 + 1.887 + // containsSearch holds incremental search values for: 1.888 + // normalized number and national format 1.889 + for (let i = 0; i < normalized.length; i++) { 1.890 + containsSearch[normalized.substring(i, normalized.length)] = 1; 1.891 + } 1.892 + if (parsedNumber && parsedNumber.nationalFormat) { 1.893 + let number = PhoneNumberUtils.normalize(parsedNumber.nationalFormat); 1.894 + for (let i = 0; i < number.length; i++) { 1.895 + containsSearch[number.substring(i, number.length)] = 1; 1.896 + } 1.897 + } 1.898 + } 1.899 + for (let num in containsSearch) { 1.900 + if (num && num != "null") { 1.901 + contact.search.tel.push(num); 1.902 + } 1.903 + } 1.904 + for (let num in matchSearch) { 1.905 + if (num && num != "null") { 1.906 + contact.search.parsedTel.push(num); 1.907 + } 1.908 + } 1.909 + } else if ((field == "impp" || field == "email") && aContact.properties[field][i].value) { 1.910 + let value = aContact.properties[field][i].value; 1.911 + if (value && typeof value == "string") { 1.912 + contact.search[field].push(value.toLowerCase()); 1.913 + } 1.914 + } else { 1.915 + let val = aContact.properties[field][i]; 1.916 + if (typeof val == "string") { 1.917 + contact.search[field].push(val.toLowerCase()); 1.918 + } 1.919 + } 1.920 + } 1.921 + } 1.922 + } 1.923 + } 1.924 + 1.925 + contact.updated = aContact.updated; 1.926 + contact.published = aContact.published; 1.927 + contact.id = aContact.id; 1.928 + 1.929 + return contact; 1.930 + }, 1.931 + 1.932 + updateRecordMetadata: function updateRecordMetadata(record) { 1.933 + if (!record.id) { 1.934 + Cu.reportError("Contact without ID"); 1.935 + } 1.936 + if (!record.published) { 1.937 + record.published = new Date(); 1.938 + } 1.939 + record.updated = new Date(); 1.940 + }, 1.941 + 1.942 + removeObjectFromCache: function CDB_removeObjectFromCache(aObjectId, aCallback, aFailureCb) { 1.943 + if (DEBUG) debug("removeObjectFromCache: " + aObjectId); 1.944 + if (!aObjectId) { 1.945 + if (DEBUG) debug("No object ID passed"); 1.946 + return; 1.947 + } 1.948 + this.newTxn("readwrite", SAVED_GETALL_STORE_NAME, function(txn, store) { 1.949 + store.openCursor().onsuccess = function(e) { 1.950 + let cursor = e.target.result; 1.951 + if (cursor) { 1.952 + for (let i = 0; i < cursor.value.length; ++i) { 1.953 + if (cursor.value[i] == aObjectId) { 1.954 + if (DEBUG) debug("id matches cache"); 1.955 + cursor.value.splice(i, 1); 1.956 + cursor.update(cursor.value); 1.957 + break; 1.958 + } 1.959 + } 1.960 + cursor.continue(); 1.961 + } else { 1.962 + aCallback(); 1.963 + } 1.964 + }.bind(this); 1.965 + }.bind(this), null, 1.966 + function(errorMsg) { 1.967 + aFailureCb(errorMsg); 1.968 + }); 1.969 + }, 1.970 + 1.971 + // Invalidate the entire cache. It will be incrementally regenerated on demand 1.972 + // See getCacheForQuery 1.973 + invalidateCache: function CDB_invalidateCache(aErrorCb) { 1.974 + if (DEBUG) debug("invalidate cache"); 1.975 + this.newTxn("readwrite", SAVED_GETALL_STORE_NAME, function (txn, store) { 1.976 + store.clear(); 1.977 + }, aErrorCb); 1.978 + }, 1.979 + 1.980 + incrementRevision: function CDB_incrementRevision(txn) { 1.981 + let revStore = txn.objectStore(REVISION_STORE); 1.982 + revStore.get(REVISION_KEY).onsuccess = function(e) { 1.983 + revStore.put(parseInt(e.target.result, 10) + 1, REVISION_KEY); 1.984 + }; 1.985 + }, 1.986 + 1.987 + saveContact: function CDB_saveContact(aContact, successCb, errorCb) { 1.988 + let contact = this.makeImport(aContact); 1.989 + this.newTxn("readwrite", STORE_NAME, function (txn, store) { 1.990 + if (DEBUG) debug("Going to update" + JSON.stringify(contact)); 1.991 + 1.992 + // Look up the existing record and compare the update timestamp. 1.993 + // If no record exists, just add the new entry. 1.994 + let newRequest = store.get(contact.id); 1.995 + newRequest.onsuccess = function (event) { 1.996 + if (!event.target.result) { 1.997 + if (DEBUG) debug("new record!") 1.998 + this.updateRecordMetadata(contact); 1.999 + store.put(contact); 1.1000 + } else { 1.1001 + if (DEBUG) debug("old record!") 1.1002 + if (new Date(typeof contact.updated === "undefined" ? 0 : contact.updated) < new Date(event.target.result.updated)) { 1.1003 + if (DEBUG) debug("rev check fail!"); 1.1004 + txn.abort(); 1.1005 + return; 1.1006 + } else { 1.1007 + if (DEBUG) debug("rev check OK"); 1.1008 + contact.published = event.target.result.published; 1.1009 + contact.updated = new Date(); 1.1010 + store.put(contact); 1.1011 + } 1.1012 + } 1.1013 + this.invalidateCache(errorCb); 1.1014 + }.bind(this); 1.1015 + 1.1016 + this.incrementRevision(txn); 1.1017 + }.bind(this), successCb, errorCb); 1.1018 + }, 1.1019 + 1.1020 + removeContact: function removeContact(aId, aSuccessCb, aErrorCb) { 1.1021 + if (DEBUG) debug("removeContact: " + aId); 1.1022 + this.removeObjectFromCache(aId, function() { 1.1023 + this.newTxn("readwrite", STORE_NAME, function(txn, store) { 1.1024 + store.delete(aId).onsuccess = function() { 1.1025 + aSuccessCb(); 1.1026 + }; 1.1027 + this.incrementRevision(txn); 1.1028 + }.bind(this), null, aErrorCb); 1.1029 + }.bind(this), aErrorCb); 1.1030 + }, 1.1031 + 1.1032 + clear: function clear(aSuccessCb, aErrorCb) { 1.1033 + this.newTxn("readwrite", STORE_NAME, function (txn, store) { 1.1034 + if (DEBUG) debug("Going to clear all!"); 1.1035 + store.clear(); 1.1036 + this.incrementRevision(txn); 1.1037 + }.bind(this), aSuccessCb, aErrorCb); 1.1038 + }, 1.1039 + 1.1040 + createCacheForQuery: function CDB_createCacheForQuery(aQuery, aSuccessCb, aFailureCb) { 1.1041 + this.find(function (aContacts) { 1.1042 + if (aContacts) { 1.1043 + let contactsArray = []; 1.1044 + for (let i in aContacts) { 1.1045 + contactsArray.push(aContacts[i]); 1.1046 + } 1.1047 + 1.1048 + // save contact ids in cache 1.1049 + this.newTxn("readwrite", SAVED_GETALL_STORE_NAME, function(txn, store) { 1.1050 + store.put(contactsArray.map(function(el) el.id), aQuery); 1.1051 + }, null, aFailureCb); 1.1052 + 1.1053 + // send full contacts 1.1054 + aSuccessCb(contactsArray, true); 1.1055 + } else { 1.1056 + aSuccessCb([], true); 1.1057 + } 1.1058 + }.bind(this), 1.1059 + function (aErrorMsg) { aFailureCb(aErrorMsg); }, 1.1060 + JSON.parse(aQuery)); 1.1061 + }, 1.1062 + 1.1063 + getCacheForQuery: function CDB_getCacheForQuery(aQuery, aSuccessCb, aFailureCb) { 1.1064 + if (DEBUG) debug("getCacheForQuery"); 1.1065 + // Here we try to get the cached results for query `aQuery'. If they don't 1.1066 + // exist, it means the cache was invalidated and needs to be recreated, so 1.1067 + // we do that. Otherwise, we just return the existing cache. 1.1068 + this.newTxn("readonly", SAVED_GETALL_STORE_NAME, function(txn, store) { 1.1069 + let req = store.get(aQuery); 1.1070 + req.onsuccess = function(e) { 1.1071 + if (e.target.result) { 1.1072 + if (DEBUG) debug("cache exists"); 1.1073 + aSuccessCb(e.target.result, false); 1.1074 + } else { 1.1075 + if (DEBUG) debug("creating cache for query " + aQuery); 1.1076 + this.createCacheForQuery(aQuery, aSuccessCb); 1.1077 + } 1.1078 + }.bind(this); 1.1079 + req.onerror = function(e) { 1.1080 + aFailureCb(e.target.errorMessage); 1.1081 + }; 1.1082 + }.bind(this), null, aFailureCb); 1.1083 + }, 1.1084 + 1.1085 + sendNow: function CDB_sendNow(aCursorId) { 1.1086 + if (aCursorId in this._dispatcher) { 1.1087 + this._dispatcher[aCursorId].sendNow(); 1.1088 + } 1.1089 + }, 1.1090 + 1.1091 + clearDispatcher: function CDB_clearDispatcher(aCursorId) { 1.1092 + if (DEBUG) debug("clearDispatcher: " + aCursorId); 1.1093 + if (aCursorId in this._dispatcher) { 1.1094 + delete this._dispatcher[aCursorId]; 1.1095 + } 1.1096 + }, 1.1097 + 1.1098 + getAll: function CDB_getAll(aSuccessCb, aFailureCb, aOptions, aCursorId) { 1.1099 + if (DEBUG) debug("getAll") 1.1100 + let optionStr = JSON.stringify(aOptions); 1.1101 + this.getCacheForQuery(optionStr, function(aCachedResults, aFullContacts) { 1.1102 + // aFullContacts is true if the cache didn't exist and had to be created. 1.1103 + // In that case, we receive the full contacts since we already have them 1.1104 + // in memory to create the cache. This allows us to avoid accessing the 1.1105 + // object store again. 1.1106 + if (aCachedResults && aCachedResults.length > 0) { 1.1107 + let newTxnFn = this.newTxn.bind(this); 1.1108 + let clearDispatcherFn = this.clearDispatcher.bind(this, aCursorId); 1.1109 + this._dispatcher[aCursorId] = new ContactDispatcher(aCachedResults, aFullContacts, 1.1110 + aSuccessCb, newTxnFn, 1.1111 + clearDispatcherFn, aFailureCb); 1.1112 + this._dispatcher[aCursorId].sendNow(); 1.1113 + } else { // no contacts 1.1114 + if (DEBUG) debug("query returned no contacts"); 1.1115 + aSuccessCb(null); 1.1116 + } 1.1117 + }.bind(this), aFailureCb); 1.1118 + }, 1.1119 + 1.1120 + getRevision: function CDB_getRevision(aSuccessCb, aErrorCb) { 1.1121 + if (DEBUG) debug("getRevision"); 1.1122 + this.newTxn("readonly", REVISION_STORE, function (txn, store) { 1.1123 + store.get(REVISION_KEY).onsuccess = function (e) { 1.1124 + aSuccessCb(e.target.result); 1.1125 + }; 1.1126 + },null, aErrorCb); 1.1127 + }, 1.1128 + 1.1129 + getCount: function CDB_getCount(aSuccessCb, aErrorCb) { 1.1130 + if (DEBUG) debug("getCount"); 1.1131 + this.newTxn("readonly", STORE_NAME, function (txn, store) { 1.1132 + store.count().onsuccess = function (e) { 1.1133 + aSuccessCb(e.target.result); 1.1134 + }; 1.1135 + }, null, aErrorCb); 1.1136 + }, 1.1137 + 1.1138 + getSortByParam: function CDB_getSortByParam(aFindOptions) { 1.1139 + switch (aFindOptions.sortBy) { 1.1140 + case "familyName": 1.1141 + return [ "familyName", "givenName" ]; 1.1142 + case "givenName": 1.1143 + return [ "givenName" , "familyName" ]; 1.1144 + case "phoneticFamilyName": 1.1145 + return [ "phoneticFamilyName" , "phoneticGivenName" ]; 1.1146 + case "phoneticGivenName": 1.1147 + return [ "phoneticGivenName" , "phoneticFamilyName" ]; 1.1148 + default: 1.1149 + return [ "givenName" , "familyName" ]; 1.1150 + } 1.1151 + }, 1.1152 + 1.1153 + /* 1.1154 + * Sorting the contacts by sortBy field. aSortBy can either be familyName or givenName. 1.1155 + * If 2 entries have the same sortyBy field or no sortBy field is present, we continue 1.1156 + * sorting with the other sortyBy field. 1.1157 + */ 1.1158 + sortResults: function CDB_sortResults(aResults, aFindOptions) { 1.1159 + if (!aFindOptions) 1.1160 + return; 1.1161 + if (aFindOptions.sortBy != "undefined") { 1.1162 + const sortOrder = aFindOptions.sortOrder; 1.1163 + const sortBy = this.getSortByParam(aFindOptions); 1.1164 + 1.1165 + aResults.sort(function (a, b) { 1.1166 + let x, y; 1.1167 + let result = 0; 1.1168 + let xIndex = 0; 1.1169 + let yIndex = 0; 1.1170 + 1.1171 + do { 1.1172 + while (xIndex < sortBy.length && !x) { 1.1173 + x = a.properties[sortBy[xIndex]]; 1.1174 + if (x) { 1.1175 + x = x.join("").toLowerCase(); 1.1176 + } 1.1177 + xIndex++; 1.1178 + } 1.1179 + while (yIndex < sortBy.length && !y) { 1.1180 + y = b.properties[sortBy[yIndex]]; 1.1181 + if (y) { 1.1182 + y = y.join("").toLowerCase(); 1.1183 + } 1.1184 + yIndex++; 1.1185 + } 1.1186 + if (!x) { 1.1187 + if (!y) { 1.1188 + let px, py; 1.1189 + px = JSON.stringify(a.published); 1.1190 + py = JSON.stringify(b.published); 1.1191 + if (px && py) { 1.1192 + return px.localeCompare(py); 1.1193 + } 1.1194 + } else { 1.1195 + return sortOrder == 'descending' ? 1 : -1; 1.1196 + } 1.1197 + } 1.1198 + if (!y) { 1.1199 + return sortOrder == "ascending" ? 1 : -1; 1.1200 + } 1.1201 + 1.1202 + result = x.localeCompare(y); 1.1203 + x = null; 1.1204 + y = null; 1.1205 + } while (result == 0); 1.1206 + 1.1207 + return sortOrder == "ascending" ? result : -result; 1.1208 + }); 1.1209 + } 1.1210 + if (aFindOptions.filterLimit && aFindOptions.filterLimit != 0) { 1.1211 + if (DEBUG) debug("filterLimit is set: " + aFindOptions.filterLimit); 1.1212 + aResults.splice(aFindOptions.filterLimit, aResults.length); 1.1213 + } 1.1214 + }, 1.1215 + 1.1216 + /** 1.1217 + * @param successCb 1.1218 + * Callback function to invoke with result array. 1.1219 + * @param failureCb [optional] 1.1220 + * Callback function to invoke when there was an error. 1.1221 + * @param options [optional] 1.1222 + * Object specifying search options. Possible attributes: 1.1223 + * - filterBy 1.1224 + * - filterOp 1.1225 + * - filterValue 1.1226 + * - count 1.1227 + */ 1.1228 + find: function find(aSuccessCb, aFailureCb, aOptions) { 1.1229 + if (DEBUG) debug("ContactDB:find val:" + aOptions.filterValue + " by: " + aOptions.filterBy + " op: " + aOptions.filterOp); 1.1230 + let self = this; 1.1231 + this.newTxn("readonly", STORE_NAME, function (txn, store) { 1.1232 + let filterOps = ["equals", "contains", "match", "startsWith"]; 1.1233 + if (aOptions && (filterOps.indexOf(aOptions.filterOp) >= 0)) { 1.1234 + self._findWithIndex(txn, store, aOptions); 1.1235 + } else { 1.1236 + self._findAll(txn, store, aOptions); 1.1237 + } 1.1238 + }, aSuccessCb, aFailureCb); 1.1239 + }, 1.1240 + 1.1241 + _findWithIndex: function _findWithIndex(txn, store, options) { 1.1242 + if (DEBUG) debug("_findWithIndex: " + options.filterValue +" " + options.filterOp + " " + options.filterBy + " "); 1.1243 + let fields = options.filterBy; 1.1244 + for (let key in fields) { 1.1245 + if (DEBUG) debug("key: " + fields[key]); 1.1246 + if (!store.indexNames.contains(fields[key]) && fields[key] != "id") { 1.1247 + if (DEBUG) debug("Key not valid!" + fields[key] + ", " + JSON.stringify(store.indexNames)); 1.1248 + txn.abort(); 1.1249 + return; 1.1250 + } 1.1251 + } 1.1252 + 1.1253 + // lookup for all keys 1.1254 + if (options.filterBy.length == 0) { 1.1255 + if (DEBUG) debug("search in all fields!" + JSON.stringify(store.indexNames)); 1.1256 + for(let myIndex = 0; myIndex < store.indexNames.length; myIndex++) { 1.1257 + fields = Array.concat(fields, store.indexNames[myIndex]) 1.1258 + } 1.1259 + } 1.1260 + 1.1261 + // Sorting functions takes care of limit if set. 1.1262 + let limit = options.sortBy === 'undefined' ? options.filterLimit : null; 1.1263 + 1.1264 + let filter_keys = fields.slice(); 1.1265 + for (let key = filter_keys.shift(); key; key = filter_keys.shift()) { 1.1266 + let request; 1.1267 + let substringResult = {}; 1.1268 + if (key == "id") { 1.1269 + // store.get would return an object and not an array 1.1270 + request = store.mozGetAll(options.filterValue); 1.1271 + } else if (key == "category") { 1.1272 + let index = store.index(key); 1.1273 + request = index.mozGetAll(options.filterValue, limit); 1.1274 + } else if (options.filterOp == "equals") { 1.1275 + if (DEBUG) debug("Getting index: " + key); 1.1276 + // case sensitive 1.1277 + let index = store.index(key); 1.1278 + let filterValue = options.filterValue; 1.1279 + if (key == "tel") { 1.1280 + filterValue = PhoneNumberUtils.normalize(filterValue, 1.1281 + /*numbersOnly*/ true); 1.1282 + } 1.1283 + request = index.mozGetAll(filterValue, limit); 1.1284 + } else if (options.filterOp == "match") { 1.1285 + if (DEBUG) debug("match"); 1.1286 + if (key != "tel") { 1.1287 + dump("ContactDB: 'match' filterOp only works on tel\n"); 1.1288 + return txn.abort(); 1.1289 + } 1.1290 + 1.1291 + let index = store.index("telMatch"); 1.1292 + let normalized = PhoneNumberUtils.normalize(options.filterValue, 1.1293 + /*numbersOnly*/ true); 1.1294 + 1.1295 + if (!normalized.length) { 1.1296 + dump("ContactDB: normalized filterValue is empty, can't perform match search.\n"); 1.1297 + return txn.abort(); 1.1298 + } 1.1299 + 1.1300 + // Some countries need special handling for number matching. Bug 877302 1.1301 + if (this.substringMatching && normalized.length > this.substringMatching) { 1.1302 + let substring = normalized.slice(-this.substringMatching); 1.1303 + if (DEBUG) debug("Substring: " + substring); 1.1304 + 1.1305 + let substringRequest = index.mozGetAll(substring, limit); 1.1306 + 1.1307 + substringRequest.onsuccess = function (event) { 1.1308 + if (DEBUG) debug("Request successful. Record count: " + event.target.result.length); 1.1309 + for (let i in event.target.result) { 1.1310 + substringResult[event.target.result[i].id] = event.target.result[i]; 1.1311 + } 1.1312 + }.bind(this); 1.1313 + } else if (normalized[0] !== "+") { 1.1314 + // We might have an international prefix like '00' 1.1315 + let parsed = PhoneNumberUtils.parse(normalized); 1.1316 + if (parsed && parsed.internationalNumber && 1.1317 + parsed.nationalNumber && 1.1318 + parsed.nationalNumber !== normalized && 1.1319 + parsed.internationalNumber !== normalized) { 1.1320 + if (DEBUG) debug("Search with " + parsed.internationalNumber); 1.1321 + let prefixRequest = index.mozGetAll(parsed.internationalNumber, limit); 1.1322 + 1.1323 + prefixRequest.onsuccess = function (event) { 1.1324 + if (DEBUG) debug("Request successful. Record count: " + event.target.result.length); 1.1325 + for (let i in event.target.result) { 1.1326 + substringResult[event.target.result[i].id] = event.target.result[i]; 1.1327 + } 1.1328 + }.bind(this); 1.1329 + } 1.1330 + } 1.1331 + 1.1332 + request = index.mozGetAll(normalized, limit); 1.1333 + } else { 1.1334 + // XXX: "contains" should be handled separately, this is "startsWith" 1.1335 + if (options.filterOp === 'contains' && key !== 'tel') { 1.1336 + dump("ContactDB: 'contains' only works for 'tel'. Falling back " + 1.1337 + "to 'startsWith'.\n"); 1.1338 + } 1.1339 + // not case sensitive 1.1340 + let lowerCase = options.filterValue.toString().toLowerCase(); 1.1341 + if (key === "tel") { 1.1342 + let origLength = lowerCase.length; 1.1343 + let tmp = PhoneNumberUtils.normalize(lowerCase, /*numbersOnly*/ true); 1.1344 + if (tmp.length != origLength) { 1.1345 + let NON_SEARCHABLE_CHARS = /[^#+\*\d\s()-]/; 1.1346 + // e.g. number "123". find with "(123)" but not with "123a" 1.1347 + if (tmp === "" || NON_SEARCHABLE_CHARS.test(lowerCase)) { 1.1348 + if (DEBUG) debug("Call continue!"); 1.1349 + continue; 1.1350 + } 1.1351 + lowerCase = tmp; 1.1352 + } 1.1353 + } 1.1354 + if (DEBUG) debug("lowerCase: " + lowerCase); 1.1355 + let range = IDBKeyRange.bound(lowerCase, lowerCase + "\uFFFF"); 1.1356 + let index = store.index(key + "LowerCase"); 1.1357 + request = index.mozGetAll(range, limit); 1.1358 + } 1.1359 + if (!txn.result) 1.1360 + txn.result = {}; 1.1361 + 1.1362 + request.onsuccess = function (event) { 1.1363 + if (DEBUG) debug("Request successful. Record count: " + event.target.result.length); 1.1364 + if (Object.keys(substringResult).length > 0) { 1.1365 + for (let attrname in substringResult) { 1.1366 + event.target.result[attrname] = substringResult[attrname]; 1.1367 + } 1.1368 + } 1.1369 + this.sortResults(event.target.result, options); 1.1370 + for (let i in event.target.result) 1.1371 + txn.result[event.target.result[i].id] = exportContact(event.target.result[i]); 1.1372 + }.bind(this); 1.1373 + } 1.1374 + }, 1.1375 + 1.1376 + _findAll: function _findAll(txn, store, options) { 1.1377 + if (DEBUG) debug("ContactDB:_findAll: " + JSON.stringify(options)); 1.1378 + if (!txn.result) 1.1379 + txn.result = {}; 1.1380 + // Sorting functions takes care of limit if set. 1.1381 + let limit = options.sortBy === 'undefined' ? options.filterLimit : null; 1.1382 + store.mozGetAll(null, limit).onsuccess = function (event) { 1.1383 + if (DEBUG) debug("Request successful. Record count:" + event.target.result.length); 1.1384 + this.sortResults(event.target.result, options); 1.1385 + for (let i in event.target.result) { 1.1386 + txn.result[event.target.result[i].id] = exportContact(event.target.result[i]); 1.1387 + } 1.1388 + }.bind(this); 1.1389 + }, 1.1390 + 1.1391 + // Enable special phone number substring matching. Does not update existing DB entries. 1.1392 + enableSubstringMatching: function enableSubstringMatching(aDigits) { 1.1393 + if (DEBUG) debug("MCC enabling substring matching " + aDigits); 1.1394 + this.substringMatching = aDigits; 1.1395 + }, 1.1396 + 1.1397 + disableSubstringMatching: function disableSubstringMatching() { 1.1398 + if (DEBUG) debug("MCC disabling substring matching"); 1.1399 + delete this.substringMatching; 1.1400 + }, 1.1401 + 1.1402 + init: function init() { 1.1403 + this.initDBHelper(DB_NAME, DB_VERSION, [STORE_NAME, SAVED_GETALL_STORE_NAME, REVISION_STORE]); 1.1404 + } 1.1405 +};