michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: // Everything but "ContactDB" is only exported here for testing. michael@0: this.EXPORTED_SYMBOLS = ["ContactDB", "DB_NAME", "STORE_NAME", "SAVED_GETALL_STORE_NAME", michael@0: "REVISION_STORE", "DB_VERSION"]; michael@0: michael@0: const DEBUG = false; michael@0: function debug(s) { dump("-*- ContactDB component: " + s + "\n"); } michael@0: michael@0: const Cu = Components.utils; michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/IndexedDBHelper.jsm"); michael@0: Cu.import("resource://gre/modules/PhoneNumberUtils.jsm"); michael@0: Cu.importGlobalProperties(["indexedDB"]); michael@0: michael@0: /* all exported symbols need to be bound to this on B2G - Bug 961777 */ michael@0: this.DB_NAME = "contacts"; michael@0: this.DB_VERSION = 20; michael@0: this.STORE_NAME = "contacts"; michael@0: this.SAVED_GETALL_STORE_NAME = "getallcache"; michael@0: const CHUNK_SIZE = 20; michael@0: this.REVISION_STORE = "revision"; michael@0: const REVISION_KEY = "revision"; michael@0: michael@0: function exportContact(aRecord) { michael@0: if (aRecord) { michael@0: delete aRecord.search; michael@0: } michael@0: return aRecord; michael@0: } michael@0: michael@0: function ContactDispatcher(aContacts, aFullContacts, aCallback, aNewTxn, aClearDispatcher, aFailureCb) { michael@0: let nextIndex = 0; michael@0: michael@0: let sendChunk; michael@0: let count = 0; michael@0: if (aFullContacts) { michael@0: sendChunk = function() { michael@0: try { michael@0: let chunk = aContacts.splice(0, CHUNK_SIZE); michael@0: if (chunk.length > 0) { michael@0: aCallback(chunk); michael@0: } michael@0: if (aContacts.length === 0) { michael@0: aCallback(null); michael@0: aClearDispatcher(); michael@0: } michael@0: } catch (e) { michael@0: aClearDispatcher(); michael@0: } michael@0: } michael@0: } else { michael@0: sendChunk = function() { michael@0: try { michael@0: let start = nextIndex; michael@0: nextIndex += CHUNK_SIZE; michael@0: let chunk = []; michael@0: aNewTxn("readonly", STORE_NAME, function(txn, store) { michael@0: for (let i = start; i < Math.min(start+CHUNK_SIZE, aContacts.length); ++i) { michael@0: store.get(aContacts[i]).onsuccess = function(e) { michael@0: chunk.push(exportContact(e.target.result)); michael@0: count++; michael@0: if (count === aContacts.length) { michael@0: aCallback(chunk); michael@0: aCallback(null); michael@0: aClearDispatcher(); michael@0: } else if (chunk.length === CHUNK_SIZE) { michael@0: aCallback(chunk); michael@0: chunk.length = 0; michael@0: } michael@0: } michael@0: } michael@0: }, null, function(errorMsg) { michael@0: aFailureCb(errorMsg); michael@0: }); michael@0: } catch (e) { michael@0: aClearDispatcher(); michael@0: } michael@0: } michael@0: } michael@0: michael@0: return { michael@0: sendNow: function() { michael@0: sendChunk(); michael@0: } michael@0: }; michael@0: } michael@0: michael@0: this.ContactDB = function ContactDB() { michael@0: if (DEBUG) debug("Constructor"); michael@0: }; michael@0: michael@0: ContactDB.prototype = { michael@0: __proto__: IndexedDBHelper.prototype, michael@0: michael@0: _dispatcher: {}, michael@0: michael@0: useFastUpgrade: true, michael@0: michael@0: upgradeSchema: function upgradeSchema(aTransaction, aDb, aOldVersion, aNewVersion) { michael@0: let loadInitialContacts = function() { michael@0: // Add default contacts michael@0: let jsm = {}; michael@0: Cu.import("resource://gre/modules/FileUtils.jsm", jsm); michael@0: Cu.import("resource://gre/modules/NetUtil.jsm", jsm); michael@0: // Loading resource://app/defaults/contacts.json doesn't work because michael@0: // contacts.json is not in the omnijar. michael@0: // So we look for the app dir instead and go from here... michael@0: let contactsFile = jsm.FileUtils.getFile("DefRt", ["contacts.json"], false); michael@0: if (!contactsFile || (contactsFile && !contactsFile.exists())) { michael@0: // For b2g desktop michael@0: contactsFile = jsm.FileUtils.getFile("ProfD", ["contacts.json"], false); michael@0: if (!contactsFile || (contactsFile && !contactsFile.exists())) { michael@0: return; michael@0: } michael@0: } michael@0: michael@0: let chan = jsm.NetUtil.newChannel(contactsFile); michael@0: let stream = chan.open(); michael@0: // Obtain a converter to read from a UTF-8 encoded input stream. michael@0: let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] michael@0: .createInstance(Ci.nsIScriptableUnicodeConverter); michael@0: converter.charset = "UTF-8"; michael@0: let rawstr = converter.ConvertToUnicode(jsm.NetUtil.readInputStreamToString( michael@0: stream, michael@0: stream.available()) || ""); michael@0: stream.close(); michael@0: let contacts; michael@0: try { michael@0: contacts = JSON.parse(rawstr); michael@0: } catch(e) { michael@0: if (DEBUG) debug("Error parsing " + contactsFile.path + " : " + e); michael@0: return; michael@0: } michael@0: michael@0: let idService = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator); michael@0: objectStore = aTransaction.objectStore(STORE_NAME); michael@0: michael@0: for (let i = 0; i < contacts.length; i++) { michael@0: let contact = {}; michael@0: contact.properties = contacts[i]; michael@0: contact.id = idService.generateUUID().toString().replace(/[{}-]/g, ""); michael@0: contact = this.makeImport(contact); michael@0: this.updateRecordMetadata(contact); michael@0: if (DEBUG) debug("import: " + JSON.stringify(contact)); michael@0: objectStore.put(contact); michael@0: } michael@0: }.bind(this); michael@0: michael@0: function createFinalSchema() { michael@0: if (DEBUG) debug("creating final schema"); michael@0: let objectStore = aDb.createObjectStore(STORE_NAME, {keyPath: "id"}); michael@0: objectStore.createIndex("familyName", "properties.familyName", { multiEntry: true }); michael@0: objectStore.createIndex("givenName", "properties.givenName", { multiEntry: true }); michael@0: objectStore.createIndex("name", "properties.name", { multiEntry: true }); michael@0: objectStore.createIndex("familyNameLowerCase", "search.familyName", { multiEntry: true }); michael@0: objectStore.createIndex("givenNameLowerCase", "search.givenName", { multiEntry: true }); michael@0: objectStore.createIndex("nameLowerCase", "search.name", { multiEntry: true }); michael@0: objectStore.createIndex("telLowerCase", "search.tel", { multiEntry: true }); michael@0: objectStore.createIndex("emailLowerCase", "search.email", { multiEntry: true }); michael@0: objectStore.createIndex("tel", "search.exactTel", { multiEntry: true }); michael@0: objectStore.createIndex("category", "properties.category", { multiEntry: true }); michael@0: objectStore.createIndex("email", "search.email", { multiEntry: true }); michael@0: objectStore.createIndex("telMatch", "search.parsedTel", {multiEntry: true}); michael@0: objectStore.createIndex("phoneticFamilyName", "properties.phoneticFamilyName", { multiEntry: true }); michael@0: objectStore.createIndex("phoneticGivenName", "properties.phoneticGivenName", { multiEntry: true }); michael@0: objectStore.createIndex("phoneticFamilyNameLowerCase", "search.phoneticFamilyName", { multiEntry: true }); michael@0: objectStore.createIndex("phoneticGivenNameLowerCase", "search.phoneticGivenName", { multiEntry: true }); michael@0: aDb.createObjectStore(SAVED_GETALL_STORE_NAME); michael@0: aDb.createObjectStore(REVISION_STORE).put(0, REVISION_KEY); michael@0: } michael@0: michael@0: let valueUpgradeSteps = []; michael@0: michael@0: function scheduleValueUpgrade(upgradeFunc) { michael@0: var length = valueUpgradeSteps.push(upgradeFunc); michael@0: if (DEBUG) debug("Scheduled a value upgrade function, index " + (length - 1)); michael@0: } michael@0: michael@0: // We always output this debug line because it's useful and the noise ratio michael@0: // very low. michael@0: debug("upgrade schema from: " + aOldVersion + " to " + aNewVersion + " called!"); michael@0: let db = aDb; michael@0: let objectStore; michael@0: michael@0: if (aOldVersion === 0 && this.useFastUpgrade) { michael@0: createFinalSchema(); michael@0: loadInitialContacts(); michael@0: return; michael@0: } michael@0: michael@0: let steps = [ michael@0: function upgrade0to1() { michael@0: /** michael@0: * Create the initial database schema. michael@0: * michael@0: * The schema of records stored is as follows: michael@0: * michael@0: * {id: "...", // UUID michael@0: * published: Date(...), // First published date. michael@0: * updated: Date(...), // Last updated date. michael@0: * properties: {...} // Object holding the ContactProperties michael@0: * } michael@0: */ michael@0: if (DEBUG) debug("create schema"); michael@0: objectStore = db.createObjectStore(STORE_NAME, {keyPath: "id"}); michael@0: michael@0: // Properties indexes michael@0: objectStore.createIndex("familyName", "properties.familyName", { multiEntry: true }); michael@0: objectStore.createIndex("givenName", "properties.givenName", { multiEntry: true }); michael@0: michael@0: objectStore.createIndex("familyNameLowerCase", "search.familyName", { multiEntry: true }); michael@0: objectStore.createIndex("givenNameLowerCase", "search.givenName", { multiEntry: true }); michael@0: objectStore.createIndex("telLowerCase", "search.tel", { multiEntry: true }); michael@0: objectStore.createIndex("emailLowerCase", "search.email", { multiEntry: true }); michael@0: next(); michael@0: }, michael@0: function upgrade1to2() { michael@0: if (DEBUG) debug("upgrade 1"); michael@0: michael@0: // Create a new scheme for the tel field. We move from an array of tel-numbers to an array of michael@0: // ContactTelephone. michael@0: if (!objectStore) { michael@0: objectStore = aTransaction.objectStore(STORE_NAME); michael@0: } michael@0: // Delete old tel index. michael@0: if (objectStore.indexNames.contains("tel")) { michael@0: objectStore.deleteIndex("tel"); michael@0: } michael@0: michael@0: // Upgrade existing tel field in the DB. michael@0: objectStore.openCursor().onsuccess = function(event) { michael@0: let cursor = event.target.result; michael@0: if (cursor) { michael@0: if (DEBUG) debug("upgrade tel1: " + JSON.stringify(cursor.value)); michael@0: for (let number in cursor.value.properties.tel) { michael@0: cursor.value.properties.tel[number] = {number: number}; michael@0: } michael@0: cursor.update(cursor.value); michael@0: if (DEBUG) debug("upgrade tel2: " + JSON.stringify(cursor.value)); michael@0: cursor.continue(); michael@0: } else { michael@0: next(); michael@0: } michael@0: }; michael@0: michael@0: // Create new searchable indexes. michael@0: objectStore.createIndex("tel", "search.tel", { multiEntry: true }); michael@0: objectStore.createIndex("category", "properties.category", { multiEntry: true }); michael@0: }, michael@0: function upgrade2to3() { michael@0: if (DEBUG) debug("upgrade 2"); michael@0: // Create a new scheme for the email field. We move from an array of emailaddresses to an array of michael@0: // ContactEmail. michael@0: if (!objectStore) { michael@0: objectStore = aTransaction.objectStore(STORE_NAME); michael@0: } michael@0: michael@0: // Delete old email index. michael@0: if (objectStore.indexNames.contains("email")) { michael@0: objectStore.deleteIndex("email"); michael@0: } michael@0: michael@0: // Upgrade existing email field in the DB. michael@0: objectStore.openCursor().onsuccess = function(event) { michael@0: let cursor = event.target.result; michael@0: if (cursor) { michael@0: if (cursor.value.properties.email) { michael@0: if (DEBUG) debug("upgrade email1: " + JSON.stringify(cursor.value)); michael@0: cursor.value.properties.email = michael@0: cursor.value.properties.email.map(function(address) { return { address: address }; }); michael@0: cursor.update(cursor.value); michael@0: if (DEBUG) debug("upgrade email2: " + JSON.stringify(cursor.value)); michael@0: } michael@0: cursor.continue(); michael@0: } else { michael@0: next(); michael@0: } michael@0: }; michael@0: michael@0: // Create new searchable indexes. michael@0: objectStore.createIndex("email", "search.email", { multiEntry: true }); michael@0: }, michael@0: function upgrade3to4() { michael@0: if (DEBUG) debug("upgrade 3"); michael@0: michael@0: if (!objectStore) { michael@0: objectStore = aTransaction.objectStore(STORE_NAME); michael@0: } michael@0: michael@0: // Upgrade existing impp field in the DB. michael@0: objectStore.openCursor().onsuccess = function(event) { michael@0: let cursor = event.target.result; michael@0: if (cursor) { michael@0: if (cursor.value.properties.impp) { michael@0: if (DEBUG) debug("upgrade impp1: " + JSON.stringify(cursor.value)); michael@0: cursor.value.properties.impp = michael@0: cursor.value.properties.impp.map(function(value) { return { value: value }; }); michael@0: cursor.update(cursor.value); michael@0: if (DEBUG) debug("upgrade impp2: " + JSON.stringify(cursor.value)); michael@0: } michael@0: cursor.continue(); michael@0: } michael@0: }; michael@0: // Upgrade existing url field in the DB. michael@0: objectStore.openCursor().onsuccess = function(event) { michael@0: let cursor = event.target.result; michael@0: if (cursor) { michael@0: if (cursor.value.properties.url) { michael@0: if (DEBUG) debug("upgrade url1: " + JSON.stringify(cursor.value)); michael@0: cursor.value.properties.url = michael@0: cursor.value.properties.url.map(function(value) { return { value: value }; }); michael@0: cursor.update(cursor.value); michael@0: if (DEBUG) debug("upgrade impp2: " + JSON.stringify(cursor.value)); michael@0: } michael@0: cursor.continue(); michael@0: } else { michael@0: next(); michael@0: } michael@0: }; michael@0: }, michael@0: function upgrade4to5() { michael@0: if (DEBUG) debug("Add international phone numbers upgrade"); michael@0: if (!objectStore) { michael@0: objectStore = aTransaction.objectStore(STORE_NAME); michael@0: } michael@0: michael@0: objectStore.openCursor().onsuccess = function(event) { michael@0: let cursor = event.target.result; michael@0: if (cursor) { michael@0: if (cursor.value.properties.tel) { michael@0: if (DEBUG) debug("upgrade : " + JSON.stringify(cursor.value)); michael@0: cursor.value.properties.tel.forEach( michael@0: function(duple) { michael@0: let parsedNumber = PhoneNumberUtils.parse(duple.value.toString()); michael@0: if (parsedNumber) { michael@0: if (DEBUG) { michael@0: debug("InternationalFormat: " + parsedNumber.internationalFormat); michael@0: debug("InternationalNumber: " + parsedNumber.internationalNumber); michael@0: debug("NationalNumber: " + parsedNumber.nationalNumber); michael@0: debug("NationalFormat: " + parsedNumber.nationalFormat); michael@0: } michael@0: if (duple.value.toString() !== parsedNumber.internationalNumber) { michael@0: cursor.value.search.tel.push(parsedNumber.internationalNumber); michael@0: } michael@0: } else { michael@0: dump("Warning: No international number found for " + duple.value + "\n"); michael@0: } michael@0: } michael@0: ) michael@0: cursor.update(cursor.value); michael@0: } michael@0: if (DEBUG) debug("upgrade2 : " + JSON.stringify(cursor.value)); michael@0: cursor.continue(); michael@0: } else { michael@0: next(); michael@0: } michael@0: }; michael@0: }, michael@0: function upgrade5to6() { michael@0: if (DEBUG) debug("Add index for equals tel searches"); michael@0: if (!objectStore) { michael@0: objectStore = aTransaction.objectStore(STORE_NAME); michael@0: } michael@0: michael@0: // Delete old tel index (not on the right field). michael@0: if (objectStore.indexNames.contains("tel")) { michael@0: objectStore.deleteIndex("tel"); michael@0: } michael@0: michael@0: // Create new index for "equals" searches michael@0: objectStore.createIndex("tel", "search.exactTel", { multiEntry: true }); michael@0: michael@0: objectStore.openCursor().onsuccess = function(event) { michael@0: let cursor = event.target.result; michael@0: if (cursor) { michael@0: if (cursor.value.properties.tel) { michael@0: if (DEBUG) debug("upgrade : " + JSON.stringify(cursor.value)); michael@0: cursor.value.properties.tel.forEach( michael@0: function(duple) { michael@0: let number = duple.value.toString(); michael@0: let parsedNumber = PhoneNumberUtils.parse(number); michael@0: michael@0: cursor.value.search.exactTel = [number]; michael@0: if (parsedNumber && michael@0: parsedNumber.internationalNumber && michael@0: number !== parsedNumber.internationalNumber) { michael@0: cursor.value.search.exactTel.push(parsedNumber.internationalNumber); michael@0: } michael@0: } michael@0: ) michael@0: cursor.update(cursor.value); michael@0: } michael@0: if (DEBUG) debug("upgrade : " + JSON.stringify(cursor.value)); michael@0: cursor.continue(); michael@0: } else { michael@0: next(); michael@0: } michael@0: }; michael@0: }, michael@0: function upgrade6to7() { michael@0: if (!objectStore) { michael@0: objectStore = aTransaction.objectStore(STORE_NAME); michael@0: } michael@0: let names = objectStore.indexNames; michael@0: let whiteList = ["tel", "familyName", "givenName", "familyNameLowerCase", michael@0: "givenNameLowerCase", "telLowerCase", "category", "email", michael@0: "emailLowerCase"]; michael@0: for (var i = 0; i < names.length; i++) { michael@0: if (whiteList.indexOf(names[i]) < 0) { michael@0: objectStore.deleteIndex(names[i]); michael@0: } michael@0: } michael@0: next(); michael@0: }, michael@0: function upgrade7to8() { michael@0: if (DEBUG) debug("Adding object store for cached searches"); michael@0: db.createObjectStore(SAVED_GETALL_STORE_NAME); michael@0: next(); michael@0: }, michael@0: function upgrade8to9() { michael@0: if (DEBUG) debug("Make exactTel only contain the value entered by the user"); michael@0: if (!objectStore) { michael@0: objectStore = aTransaction.objectStore(STORE_NAME); michael@0: } michael@0: michael@0: objectStore.openCursor().onsuccess = function(event) { michael@0: let cursor = event.target.result; michael@0: if (cursor) { michael@0: if (cursor.value.properties.tel) { michael@0: cursor.value.search.exactTel = []; michael@0: cursor.value.properties.tel.forEach( michael@0: function(tel) { michael@0: let normalized = PhoneNumberUtils.normalize(tel.value.toString()); michael@0: cursor.value.search.exactTel.push(normalized); michael@0: } michael@0: ); michael@0: cursor.update(cursor.value); michael@0: } michael@0: cursor.continue(); michael@0: } else { michael@0: next(); michael@0: } michael@0: }; michael@0: }, michael@0: function upgrade9to10() { michael@0: // no-op, see https://bugzilla.mozilla.org/show_bug.cgi?id=883770#c16 michael@0: next(); michael@0: }, michael@0: function upgrade10to11() { michael@0: if (DEBUG) debug("Adding object store for database revision"); michael@0: db.createObjectStore(REVISION_STORE).put(0, REVISION_KEY); michael@0: next(); michael@0: }, michael@0: function upgrade11to12() { michael@0: if (DEBUG) debug("Add a telMatch index with national and international numbers"); michael@0: if (!objectStore) { michael@0: objectStore = aTransaction.objectStore(STORE_NAME); michael@0: } michael@0: if (!objectStore.indexNames.contains("telMatch")) { michael@0: objectStore.createIndex("telMatch", "search.parsedTel", {multiEntry: true}); michael@0: } michael@0: objectStore.openCursor().onsuccess = function(event) { michael@0: let cursor = event.target.result; michael@0: if (cursor) { michael@0: if (cursor.value.properties.tel) { michael@0: cursor.value.search.parsedTel = []; michael@0: cursor.value.properties.tel.forEach( michael@0: function(tel) { michael@0: let parsed = PhoneNumberUtils.parse(tel.value.toString()); michael@0: if (parsed) { michael@0: cursor.value.search.parsedTel.push(parsed.nationalNumber); michael@0: cursor.value.search.parsedTel.push(PhoneNumberUtils.normalize(parsed.nationalFormat)); michael@0: cursor.value.search.parsedTel.push(parsed.internationalNumber); michael@0: cursor.value.search.parsedTel.push(PhoneNumberUtils.normalize(parsed.internationalFormat)); michael@0: } michael@0: cursor.value.search.parsedTel.push(PhoneNumberUtils.normalize(tel.value.toString())); michael@0: } michael@0: ); michael@0: cursor.update(cursor.value); michael@0: } michael@0: cursor.continue(); michael@0: } else { michael@0: next(); michael@0: } michael@0: }; michael@0: }, michael@0: function upgrade12to13() { michael@0: if (DEBUG) debug("Add phone substring to the search index if appropriate for country"); michael@0: if (this.substringMatching) { michael@0: scheduleValueUpgrade(function upgradeValue12to13(value) { michael@0: if (value.properties.tel) { michael@0: value.search.parsedTel = value.search.parsedTel || []; michael@0: value.properties.tel.forEach( michael@0: function(tel) { michael@0: let normalized = PhoneNumberUtils.normalize(tel.value.toString()); michael@0: if (normalized) { michael@0: if (this.substringMatching && normalized.length > this.substringMatching) { michael@0: let sub = normalized.slice(-this.substringMatching); michael@0: if (value.search.parsedTel.indexOf(sub) === -1) { michael@0: if (DEBUG) debug("Adding substring index: " + tel + ", " + sub); michael@0: value.search.parsedTel.push(sub); michael@0: } michael@0: } michael@0: } michael@0: }.bind(this) michael@0: ); michael@0: return true; michael@0: } else { michael@0: return false; michael@0: } michael@0: }.bind(this)); michael@0: } michael@0: next(); michael@0: }, michael@0: function upgrade13to14() { michael@0: if (DEBUG) debug("Cleaning up empty substring entries in telMatch index"); michael@0: scheduleValueUpgrade(function upgradeValue13to14(value) { michael@0: function removeEmptyStrings(value) { michael@0: if (value) { michael@0: const oldLength = value.length; michael@0: for (let i = 0; i < value.length; ++i) { michael@0: if (!value[i] || value[i] == "null") { michael@0: value.splice(i, 1); michael@0: } michael@0: } michael@0: return oldLength !== value.length; michael@0: } michael@0: } michael@0: michael@0: let modified = removeEmptyStrings(value.search.parsedTel); michael@0: let modified2 = removeEmptyStrings(value.search.tel); michael@0: return (modified || modified2); michael@0: }); michael@0: michael@0: next(); michael@0: }, michael@0: function upgrade14to15() { michael@0: if (DEBUG) debug("Fix array properties saved as scalars"); michael@0: const ARRAY_PROPERTIES = ["photo", "adr", "email", "url", "impp", "tel", michael@0: "name", "honorificPrefix", "givenName", michael@0: "additionalName", "familyName", "honorificSuffix", michael@0: "nickname", "category", "org", "jobTitle", michael@0: "note", "key"]; michael@0: const PROPERTIES_WITH_TYPE = ["adr", "email", "url", "impp", "tel"]; michael@0: michael@0: scheduleValueUpgrade(function upgradeValue14to15(value) { michael@0: let changed = false; michael@0: michael@0: let props = value.properties; michael@0: for (let prop of ARRAY_PROPERTIES) { michael@0: if (props[prop]) { michael@0: if (!Array.isArray(props[prop])) { michael@0: value.properties[prop] = [props[prop]]; michael@0: changed = true; michael@0: } michael@0: if (PROPERTIES_WITH_TYPE.indexOf(prop) !== -1) { michael@0: let subprop = value.properties[prop]; michael@0: for (let i = 0; i < subprop.length; ++i) { michael@0: if (!Array.isArray(subprop[i].type)) { michael@0: value.properties[prop][i].type = [subprop[i].type]; michael@0: changed = true; michael@0: } michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: return changed; michael@0: }); michael@0: michael@0: next(); michael@0: }, michael@0: function upgrade15to16() { michael@0: if (DEBUG) debug("Fix Date properties"); michael@0: const DATE_PROPERTIES = ["bday", "anniversary"]; michael@0: michael@0: scheduleValueUpgrade(function upgradeValue15to16(value) { michael@0: let changed = false; michael@0: let props = value.properties; michael@0: for (let prop of DATE_PROPERTIES) { michael@0: if (props[prop] && !(props[prop] instanceof Date)) { michael@0: value.properties[prop] = new Date(props[prop]); michael@0: changed = true; michael@0: } michael@0: } michael@0: michael@0: return changed; michael@0: }); michael@0: michael@0: next(); michael@0: }, michael@0: function upgrade16to17() { michael@0: if (DEBUG) debug("Fix array with null values"); michael@0: const ARRAY_PROPERTIES = ["photo", "adr", "email", "url", "impp", "tel", michael@0: "name", "honorificPrefix", "givenName", michael@0: "additionalName", "familyName", "honorificSuffix", michael@0: "nickname", "category", "org", "jobTitle", michael@0: "note", "key"]; michael@0: michael@0: const PROPERTIES_WITH_TYPE = ["adr", "email", "url", "impp", "tel"]; michael@0: michael@0: const DATE_PROPERTIES = ["bday", "anniversary"]; michael@0: michael@0: scheduleValueUpgrade(function upgradeValue16to17(value) { michael@0: let changed; michael@0: michael@0: function filterInvalidValues(val) { michael@0: let shouldKeep = val != null; // null or undefined michael@0: if (!shouldKeep) { michael@0: changed = true; michael@0: } michael@0: return shouldKeep; michael@0: } michael@0: michael@0: function filteredArray(array) { michael@0: return array.filter(filterInvalidValues); michael@0: } michael@0: michael@0: let props = value.properties; michael@0: michael@0: for (let prop of ARRAY_PROPERTIES) { michael@0: michael@0: // properties that were empty strings weren't converted to arrays michael@0: // in upgrade14to15 michael@0: if (props[prop] != null && !Array.isArray(props[prop])) { michael@0: props[prop] = [props[prop]]; michael@0: changed = true; michael@0: } michael@0: michael@0: if (props[prop] && props[prop].length) { michael@0: props[prop] = filteredArray(props[prop]); michael@0: michael@0: if (PROPERTIES_WITH_TYPE.indexOf(prop) !== -1) { michael@0: let subprop = props[prop]; michael@0: michael@0: for (let i = 0; i < subprop.length; ++i) { michael@0: let curSubprop = subprop[i]; michael@0: // upgrade14to15 transformed type props into an array michael@0: // without checking invalid values michael@0: if (curSubprop.type) { michael@0: curSubprop.type = filteredArray(curSubprop.type); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: for (let prop of DATE_PROPERTIES) { michael@0: if (props[prop] != null && !(props[prop] instanceof Date)) { michael@0: // props[prop] is probably '' and wasn't converted michael@0: // in upgrade15to16 michael@0: props[prop] = null; michael@0: changed = true; michael@0: } michael@0: } michael@0: michael@0: if (changed) { michael@0: value.properties = props; michael@0: return true; michael@0: } else { michael@0: return false; michael@0: } michael@0: }); michael@0: michael@0: next(); michael@0: }, michael@0: function upgrade17to18() { michael@0: // this upgrade function has been moved to the next upgrade path because michael@0: // a previous version of it had a bug michael@0: next(); michael@0: }, michael@0: function upgrade18to19() { michael@0: if (DEBUG) { michael@0: debug("Adding the name index"); michael@0: } michael@0: michael@0: if (!objectStore) { michael@0: objectStore = aTransaction.objectStore(STORE_NAME); michael@0: } michael@0: michael@0: // an earlier version of this code could have run, so checking whether michael@0: // the index exists michael@0: if (!objectStore.indexNames.contains("name")) { michael@0: objectStore.createIndex("name", "properties.name", { multiEntry: true }); michael@0: objectStore.createIndex("nameLowerCase", "search.name", { multiEntry: true }); michael@0: } michael@0: michael@0: scheduleValueUpgrade(function upgradeValue18to19(value) { michael@0: value.search.name = []; michael@0: if (value.properties.name) { michael@0: value.properties.name.forEach(function addNameIndex(name) { michael@0: var lowerName = name.toLowerCase(); michael@0: // an earlier version of this code could have added it already michael@0: if (value.search.name.indexOf(lowerName) === -1) { michael@0: value.search.name.push(lowerName); michael@0: } michael@0: }); michael@0: } michael@0: return true; michael@0: }); michael@0: michael@0: next(); michael@0: }, michael@0: function upgrade19to20() { michael@0: if (DEBUG) debug("upgrade19to20 create schema(phonetic)"); michael@0: if (!objectStore) { michael@0: objectStore = aTransaction.objectStore(STORE_NAME); michael@0: } michael@0: objectStore.createIndex("phoneticFamilyName", "properties.phoneticFamilyName", { multiEntry: true }); michael@0: objectStore.createIndex("phoneticGivenName", "properties.phoneticGivenName", { multiEntry: true }); michael@0: objectStore.createIndex("phoneticFamilyNameLowerCase", "search.phoneticFamilyName", { multiEntry: true }); michael@0: objectStore.createIndex("phoneticGivenNameLowerCase", "search.phoneticGivenName", { multiEntry: true }); michael@0: next(); michael@0: }, michael@0: ]; michael@0: michael@0: let index = aOldVersion; michael@0: let outer = this; michael@0: michael@0: /* This function runs all upgrade functions that are in the michael@0: * valueUpgradeSteps array. These functions have the following properties: michael@0: * - they must be synchronous michael@0: * - they must take the value as parameter and modify it directly. They michael@0: * must not create a new object. michael@0: * - they must return a boolean true/false; true if the value was actually michael@0: * changed michael@0: */ michael@0: function runValueUpgradeSteps(done) { michael@0: if (DEBUG) debug("Running the value upgrade functions."); michael@0: if (!objectStore) { michael@0: objectStore = aTransaction.objectStore(STORE_NAME); michael@0: } michael@0: objectStore.openCursor().onsuccess = function(event) { michael@0: let cursor = event.target.result; michael@0: if (cursor) { michael@0: let changed = false; michael@0: let oldValue; michael@0: let value = cursor.value; michael@0: if (DEBUG) { michael@0: oldValue = JSON.stringify(value); michael@0: } michael@0: valueUpgradeSteps.forEach(function(upgradeFunc, i) { michael@0: if (DEBUG) debug("Running upgrade function " + i); michael@0: changed = upgradeFunc(value) || changed; michael@0: }); michael@0: michael@0: if (changed) { michael@0: cursor.update(value); michael@0: } else if (DEBUG) { michael@0: let newValue = JSON.stringify(value); michael@0: if (newValue !== oldValue) { michael@0: // oops something went wrong michael@0: debug("upgrade: `changed` was false and still the value changed! Aborting."); michael@0: aTransaction.abort(); michael@0: return; michael@0: } michael@0: } michael@0: cursor.continue(); michael@0: } else { michael@0: done(); michael@0: } michael@0: }; michael@0: } michael@0: michael@0: function finish() { michael@0: // We always output this debug line because it's useful and the noise ratio michael@0: // very low. michael@0: debug("Upgrade finished"); michael@0: michael@0: outer.incrementRevision(aTransaction); michael@0: } michael@0: michael@0: function next() { michael@0: if (index == aNewVersion) { michael@0: runValueUpgradeSteps(finish); michael@0: return; michael@0: } michael@0: michael@0: try { michael@0: var i = index++; michael@0: if (DEBUG) debug("Upgrade step: " + i + "\n"); michael@0: steps[i].call(outer); michael@0: } catch(ex) { michael@0: dump("Caught exception" + ex); michael@0: aTransaction.abort(); michael@0: return; michael@0: } michael@0: } michael@0: michael@0: function fail(why) { michael@0: why = why || ""; michael@0: if (this.error) { michael@0: why += " (root cause: " + this.error.name + ")"; michael@0: } michael@0: michael@0: debug("Contacts DB upgrade error: " + why); michael@0: aTransaction.abort(); michael@0: } michael@0: michael@0: if (aNewVersion > steps.length) { michael@0: fail("No migration steps for the new version!"); michael@0: } michael@0: michael@0: this.cpuLock = Cc["@mozilla.org/power/powermanagerservice;1"] michael@0: .getService(Ci.nsIPowerManagerService) michael@0: .newWakeLock("cpu"); michael@0: michael@0: function unlockCPU() { michael@0: if (outer.cpuLock) { michael@0: if (DEBUG) debug("unlocking cpu wakelock"); michael@0: outer.cpuLock.unlock(); michael@0: outer.cpuLock = null; michael@0: } michael@0: } michael@0: michael@0: aTransaction.addEventListener("complete", unlockCPU); michael@0: aTransaction.addEventListener("abort", unlockCPU); michael@0: michael@0: next(); michael@0: }, michael@0: michael@0: makeImport: function makeImport(aContact) { michael@0: let contact = {properties: {}}; michael@0: michael@0: contact.search = { michael@0: name: [], michael@0: givenName: [], michael@0: familyName: [], michael@0: email: [], michael@0: category: [], michael@0: tel: [], michael@0: exactTel: [], michael@0: parsedTel: [], michael@0: phoneticFamilyName: [], michael@0: phoneticGivenName: [], michael@0: }; michael@0: michael@0: for (let field in aContact.properties) { michael@0: contact.properties[field] = aContact.properties[field]; michael@0: // Add search fields michael@0: if (aContact.properties[field] && contact.search[field]) { michael@0: for (let i = 0; i <= aContact.properties[field].length; i++) { michael@0: if (aContact.properties[field][i]) { michael@0: if (field == "tel" && aContact.properties[field][i].value) { michael@0: let number = aContact.properties.tel[i].value.toString(); michael@0: let normalized = PhoneNumberUtils.normalize(number); michael@0: // We use an object here to avoid duplicates michael@0: let containsSearch = {}; michael@0: let matchSearch = {}; michael@0: michael@0: if (normalized) { michael@0: // exactTel holds normalized version of entered phone number. michael@0: // normalized: +1 (949) 123 - 4567 -> +19491234567 michael@0: contact.search.exactTel.push(normalized); michael@0: // matchSearch holds normalized version of entered phone number, michael@0: // nationalNumber, nationalFormat, internationalNumber, internationalFormat michael@0: matchSearch[normalized] = 1; michael@0: let parsedNumber = PhoneNumberUtils.parse(number); michael@0: if (parsedNumber) { michael@0: if (DEBUG) { michael@0: debug("InternationalFormat: " + parsedNumber.internationalFormat); michael@0: debug("InternationalNumber: " + parsedNumber.internationalNumber); michael@0: debug("NationalNumber: " + parsedNumber.nationalNumber); michael@0: debug("NationalFormat: " + parsedNumber.nationalFormat); michael@0: debug("NationalMatchingFormat: " + parsedNumber.nationalMatchingFormat); michael@0: } michael@0: matchSearch[parsedNumber.nationalNumber] = 1; michael@0: matchSearch[parsedNumber.internationalNumber] = 1; michael@0: matchSearch[PhoneNumberUtils.normalize(parsedNumber.nationalFormat)] = 1; michael@0: matchSearch[PhoneNumberUtils.normalize(parsedNumber.internationalFormat)] = 1; michael@0: matchSearch[PhoneNumberUtils.normalize(parsedNumber.nationalMatchingFormat)] = 1; michael@0: } else if (this.substringMatching && normalized.length > this.substringMatching) { michael@0: matchSearch[normalized.slice(-this.substringMatching)] = 1; michael@0: } michael@0: michael@0: // containsSearch holds incremental search values for: michael@0: // normalized number and national format michael@0: for (let i = 0; i < normalized.length; i++) { michael@0: containsSearch[normalized.substring(i, normalized.length)] = 1; michael@0: } michael@0: if (parsedNumber && parsedNumber.nationalFormat) { michael@0: let number = PhoneNumberUtils.normalize(parsedNumber.nationalFormat); michael@0: for (let i = 0; i < number.length; i++) { michael@0: containsSearch[number.substring(i, number.length)] = 1; michael@0: } michael@0: } michael@0: } michael@0: for (let num in containsSearch) { michael@0: if (num && num != "null") { michael@0: contact.search.tel.push(num); michael@0: } michael@0: } michael@0: for (let num in matchSearch) { michael@0: if (num && num != "null") { michael@0: contact.search.parsedTel.push(num); michael@0: } michael@0: } michael@0: } else if ((field == "impp" || field == "email") && aContact.properties[field][i].value) { michael@0: let value = aContact.properties[field][i].value; michael@0: if (value && typeof value == "string") { michael@0: contact.search[field].push(value.toLowerCase()); michael@0: } michael@0: } else { michael@0: let val = aContact.properties[field][i]; michael@0: if (typeof val == "string") { michael@0: contact.search[field].push(val.toLowerCase()); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: contact.updated = aContact.updated; michael@0: contact.published = aContact.published; michael@0: contact.id = aContact.id; michael@0: michael@0: return contact; michael@0: }, michael@0: michael@0: updateRecordMetadata: function updateRecordMetadata(record) { michael@0: if (!record.id) { michael@0: Cu.reportError("Contact without ID"); michael@0: } michael@0: if (!record.published) { michael@0: record.published = new Date(); michael@0: } michael@0: record.updated = new Date(); michael@0: }, michael@0: michael@0: removeObjectFromCache: function CDB_removeObjectFromCache(aObjectId, aCallback, aFailureCb) { michael@0: if (DEBUG) debug("removeObjectFromCache: " + aObjectId); michael@0: if (!aObjectId) { michael@0: if (DEBUG) debug("No object ID passed"); michael@0: return; michael@0: } michael@0: this.newTxn("readwrite", SAVED_GETALL_STORE_NAME, function(txn, store) { michael@0: store.openCursor().onsuccess = function(e) { michael@0: let cursor = e.target.result; michael@0: if (cursor) { michael@0: for (let i = 0; i < cursor.value.length; ++i) { michael@0: if (cursor.value[i] == aObjectId) { michael@0: if (DEBUG) debug("id matches cache"); michael@0: cursor.value.splice(i, 1); michael@0: cursor.update(cursor.value); michael@0: break; michael@0: } michael@0: } michael@0: cursor.continue(); michael@0: } else { michael@0: aCallback(); michael@0: } michael@0: }.bind(this); michael@0: }.bind(this), null, michael@0: function(errorMsg) { michael@0: aFailureCb(errorMsg); michael@0: }); michael@0: }, michael@0: michael@0: // Invalidate the entire cache. It will be incrementally regenerated on demand michael@0: // See getCacheForQuery michael@0: invalidateCache: function CDB_invalidateCache(aErrorCb) { michael@0: if (DEBUG) debug("invalidate cache"); michael@0: this.newTxn("readwrite", SAVED_GETALL_STORE_NAME, function (txn, store) { michael@0: store.clear(); michael@0: }, aErrorCb); michael@0: }, michael@0: michael@0: incrementRevision: function CDB_incrementRevision(txn) { michael@0: let revStore = txn.objectStore(REVISION_STORE); michael@0: revStore.get(REVISION_KEY).onsuccess = function(e) { michael@0: revStore.put(parseInt(e.target.result, 10) + 1, REVISION_KEY); michael@0: }; michael@0: }, michael@0: michael@0: saveContact: function CDB_saveContact(aContact, successCb, errorCb) { michael@0: let contact = this.makeImport(aContact); michael@0: this.newTxn("readwrite", STORE_NAME, function (txn, store) { michael@0: if (DEBUG) debug("Going to update" + JSON.stringify(contact)); michael@0: michael@0: // Look up the existing record and compare the update timestamp. michael@0: // If no record exists, just add the new entry. michael@0: let newRequest = store.get(contact.id); michael@0: newRequest.onsuccess = function (event) { michael@0: if (!event.target.result) { michael@0: if (DEBUG) debug("new record!") michael@0: this.updateRecordMetadata(contact); michael@0: store.put(contact); michael@0: } else { michael@0: if (DEBUG) debug("old record!") michael@0: if (new Date(typeof contact.updated === "undefined" ? 0 : contact.updated) < new Date(event.target.result.updated)) { michael@0: if (DEBUG) debug("rev check fail!"); michael@0: txn.abort(); michael@0: return; michael@0: } else { michael@0: if (DEBUG) debug("rev check OK"); michael@0: contact.published = event.target.result.published; michael@0: contact.updated = new Date(); michael@0: store.put(contact); michael@0: } michael@0: } michael@0: this.invalidateCache(errorCb); michael@0: }.bind(this); michael@0: michael@0: this.incrementRevision(txn); michael@0: }.bind(this), successCb, errorCb); michael@0: }, michael@0: michael@0: removeContact: function removeContact(aId, aSuccessCb, aErrorCb) { michael@0: if (DEBUG) debug("removeContact: " + aId); michael@0: this.removeObjectFromCache(aId, function() { michael@0: this.newTxn("readwrite", STORE_NAME, function(txn, store) { michael@0: store.delete(aId).onsuccess = function() { michael@0: aSuccessCb(); michael@0: }; michael@0: this.incrementRevision(txn); michael@0: }.bind(this), null, aErrorCb); michael@0: }.bind(this), aErrorCb); michael@0: }, michael@0: michael@0: clear: function clear(aSuccessCb, aErrorCb) { michael@0: this.newTxn("readwrite", STORE_NAME, function (txn, store) { michael@0: if (DEBUG) debug("Going to clear all!"); michael@0: store.clear(); michael@0: this.incrementRevision(txn); michael@0: }.bind(this), aSuccessCb, aErrorCb); michael@0: }, michael@0: michael@0: createCacheForQuery: function CDB_createCacheForQuery(aQuery, aSuccessCb, aFailureCb) { michael@0: this.find(function (aContacts) { michael@0: if (aContacts) { michael@0: let contactsArray = []; michael@0: for (let i in aContacts) { michael@0: contactsArray.push(aContacts[i]); michael@0: } michael@0: michael@0: // save contact ids in cache michael@0: this.newTxn("readwrite", SAVED_GETALL_STORE_NAME, function(txn, store) { michael@0: store.put(contactsArray.map(function(el) el.id), aQuery); michael@0: }, null, aFailureCb); michael@0: michael@0: // send full contacts michael@0: aSuccessCb(contactsArray, true); michael@0: } else { michael@0: aSuccessCb([], true); michael@0: } michael@0: }.bind(this), michael@0: function (aErrorMsg) { aFailureCb(aErrorMsg); }, michael@0: JSON.parse(aQuery)); michael@0: }, michael@0: michael@0: getCacheForQuery: function CDB_getCacheForQuery(aQuery, aSuccessCb, aFailureCb) { michael@0: if (DEBUG) debug("getCacheForQuery"); michael@0: // Here we try to get the cached results for query `aQuery'. If they don't michael@0: // exist, it means the cache was invalidated and needs to be recreated, so michael@0: // we do that. Otherwise, we just return the existing cache. michael@0: this.newTxn("readonly", SAVED_GETALL_STORE_NAME, function(txn, store) { michael@0: let req = store.get(aQuery); michael@0: req.onsuccess = function(e) { michael@0: if (e.target.result) { michael@0: if (DEBUG) debug("cache exists"); michael@0: aSuccessCb(e.target.result, false); michael@0: } else { michael@0: if (DEBUG) debug("creating cache for query " + aQuery); michael@0: this.createCacheForQuery(aQuery, aSuccessCb); michael@0: } michael@0: }.bind(this); michael@0: req.onerror = function(e) { michael@0: aFailureCb(e.target.errorMessage); michael@0: }; michael@0: }.bind(this), null, aFailureCb); michael@0: }, michael@0: michael@0: sendNow: function CDB_sendNow(aCursorId) { michael@0: if (aCursorId in this._dispatcher) { michael@0: this._dispatcher[aCursorId].sendNow(); michael@0: } michael@0: }, michael@0: michael@0: clearDispatcher: function CDB_clearDispatcher(aCursorId) { michael@0: if (DEBUG) debug("clearDispatcher: " + aCursorId); michael@0: if (aCursorId in this._dispatcher) { michael@0: delete this._dispatcher[aCursorId]; michael@0: } michael@0: }, michael@0: michael@0: getAll: function CDB_getAll(aSuccessCb, aFailureCb, aOptions, aCursorId) { michael@0: if (DEBUG) debug("getAll") michael@0: let optionStr = JSON.stringify(aOptions); michael@0: this.getCacheForQuery(optionStr, function(aCachedResults, aFullContacts) { michael@0: // aFullContacts is true if the cache didn't exist and had to be created. michael@0: // In that case, we receive the full contacts since we already have them michael@0: // in memory to create the cache. This allows us to avoid accessing the michael@0: // object store again. michael@0: if (aCachedResults && aCachedResults.length > 0) { michael@0: let newTxnFn = this.newTxn.bind(this); michael@0: let clearDispatcherFn = this.clearDispatcher.bind(this, aCursorId); michael@0: this._dispatcher[aCursorId] = new ContactDispatcher(aCachedResults, aFullContacts, michael@0: aSuccessCb, newTxnFn, michael@0: clearDispatcherFn, aFailureCb); michael@0: this._dispatcher[aCursorId].sendNow(); michael@0: } else { // no contacts michael@0: if (DEBUG) debug("query returned no contacts"); michael@0: aSuccessCb(null); michael@0: } michael@0: }.bind(this), aFailureCb); michael@0: }, michael@0: michael@0: getRevision: function CDB_getRevision(aSuccessCb, aErrorCb) { michael@0: if (DEBUG) debug("getRevision"); michael@0: this.newTxn("readonly", REVISION_STORE, function (txn, store) { michael@0: store.get(REVISION_KEY).onsuccess = function (e) { michael@0: aSuccessCb(e.target.result); michael@0: }; michael@0: },null, aErrorCb); michael@0: }, michael@0: michael@0: getCount: function CDB_getCount(aSuccessCb, aErrorCb) { michael@0: if (DEBUG) debug("getCount"); michael@0: this.newTxn("readonly", STORE_NAME, function (txn, store) { michael@0: store.count().onsuccess = function (e) { michael@0: aSuccessCb(e.target.result); michael@0: }; michael@0: }, null, aErrorCb); michael@0: }, michael@0: michael@0: getSortByParam: function CDB_getSortByParam(aFindOptions) { michael@0: switch (aFindOptions.sortBy) { michael@0: case "familyName": michael@0: return [ "familyName", "givenName" ]; michael@0: case "givenName": michael@0: return [ "givenName" , "familyName" ]; michael@0: case "phoneticFamilyName": michael@0: return [ "phoneticFamilyName" , "phoneticGivenName" ]; michael@0: case "phoneticGivenName": michael@0: return [ "phoneticGivenName" , "phoneticFamilyName" ]; michael@0: default: michael@0: return [ "givenName" , "familyName" ]; michael@0: } michael@0: }, michael@0: michael@0: /* michael@0: * Sorting the contacts by sortBy field. aSortBy can either be familyName or givenName. michael@0: * If 2 entries have the same sortyBy field or no sortBy field is present, we continue michael@0: * sorting with the other sortyBy field. michael@0: */ michael@0: sortResults: function CDB_sortResults(aResults, aFindOptions) { michael@0: if (!aFindOptions) michael@0: return; michael@0: if (aFindOptions.sortBy != "undefined") { michael@0: const sortOrder = aFindOptions.sortOrder; michael@0: const sortBy = this.getSortByParam(aFindOptions); michael@0: michael@0: aResults.sort(function (a, b) { michael@0: let x, y; michael@0: let result = 0; michael@0: let xIndex = 0; michael@0: let yIndex = 0; michael@0: michael@0: do { michael@0: while (xIndex < sortBy.length && !x) { michael@0: x = a.properties[sortBy[xIndex]]; michael@0: if (x) { michael@0: x = x.join("").toLowerCase(); michael@0: } michael@0: xIndex++; michael@0: } michael@0: while (yIndex < sortBy.length && !y) { michael@0: y = b.properties[sortBy[yIndex]]; michael@0: if (y) { michael@0: y = y.join("").toLowerCase(); michael@0: } michael@0: yIndex++; michael@0: } michael@0: if (!x) { michael@0: if (!y) { michael@0: let px, py; michael@0: px = JSON.stringify(a.published); michael@0: py = JSON.stringify(b.published); michael@0: if (px && py) { michael@0: return px.localeCompare(py); michael@0: } michael@0: } else { michael@0: return sortOrder == 'descending' ? 1 : -1; michael@0: } michael@0: } michael@0: if (!y) { michael@0: return sortOrder == "ascending" ? 1 : -1; michael@0: } michael@0: michael@0: result = x.localeCompare(y); michael@0: x = null; michael@0: y = null; michael@0: } while (result == 0); michael@0: michael@0: return sortOrder == "ascending" ? result : -result; michael@0: }); michael@0: } michael@0: if (aFindOptions.filterLimit && aFindOptions.filterLimit != 0) { michael@0: if (DEBUG) debug("filterLimit is set: " + aFindOptions.filterLimit); michael@0: aResults.splice(aFindOptions.filterLimit, aResults.length); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * @param successCb michael@0: * Callback function to invoke with result array. michael@0: * @param failureCb [optional] michael@0: * Callback function to invoke when there was an error. michael@0: * @param options [optional] michael@0: * Object specifying search options. Possible attributes: michael@0: * - filterBy michael@0: * - filterOp michael@0: * - filterValue michael@0: * - count michael@0: */ michael@0: find: function find(aSuccessCb, aFailureCb, aOptions) { michael@0: if (DEBUG) debug("ContactDB:find val:" + aOptions.filterValue + " by: " + aOptions.filterBy + " op: " + aOptions.filterOp); michael@0: let self = this; michael@0: this.newTxn("readonly", STORE_NAME, function (txn, store) { michael@0: let filterOps = ["equals", "contains", "match", "startsWith"]; michael@0: if (aOptions && (filterOps.indexOf(aOptions.filterOp) >= 0)) { michael@0: self._findWithIndex(txn, store, aOptions); michael@0: } else { michael@0: self._findAll(txn, store, aOptions); michael@0: } michael@0: }, aSuccessCb, aFailureCb); michael@0: }, michael@0: michael@0: _findWithIndex: function _findWithIndex(txn, store, options) { michael@0: if (DEBUG) debug("_findWithIndex: " + options.filterValue +" " + options.filterOp + " " + options.filterBy + " "); michael@0: let fields = options.filterBy; michael@0: for (let key in fields) { michael@0: if (DEBUG) debug("key: " + fields[key]); michael@0: if (!store.indexNames.contains(fields[key]) && fields[key] != "id") { michael@0: if (DEBUG) debug("Key not valid!" + fields[key] + ", " + JSON.stringify(store.indexNames)); michael@0: txn.abort(); michael@0: return; michael@0: } michael@0: } michael@0: michael@0: // lookup for all keys michael@0: if (options.filterBy.length == 0) { michael@0: if (DEBUG) debug("search in all fields!" + JSON.stringify(store.indexNames)); michael@0: for(let myIndex = 0; myIndex < store.indexNames.length; myIndex++) { michael@0: fields = Array.concat(fields, store.indexNames[myIndex]) michael@0: } michael@0: } michael@0: michael@0: // Sorting functions takes care of limit if set. michael@0: let limit = options.sortBy === 'undefined' ? options.filterLimit : null; michael@0: michael@0: let filter_keys = fields.slice(); michael@0: for (let key = filter_keys.shift(); key; key = filter_keys.shift()) { michael@0: let request; michael@0: let substringResult = {}; michael@0: if (key == "id") { michael@0: // store.get would return an object and not an array michael@0: request = store.mozGetAll(options.filterValue); michael@0: } else if (key == "category") { michael@0: let index = store.index(key); michael@0: request = index.mozGetAll(options.filterValue, limit); michael@0: } else if (options.filterOp == "equals") { michael@0: if (DEBUG) debug("Getting index: " + key); michael@0: // case sensitive michael@0: let index = store.index(key); michael@0: let filterValue = options.filterValue; michael@0: if (key == "tel") { michael@0: filterValue = PhoneNumberUtils.normalize(filterValue, michael@0: /*numbersOnly*/ true); michael@0: } michael@0: request = index.mozGetAll(filterValue, limit); michael@0: } else if (options.filterOp == "match") { michael@0: if (DEBUG) debug("match"); michael@0: if (key != "tel") { michael@0: dump("ContactDB: 'match' filterOp only works on tel\n"); michael@0: return txn.abort(); michael@0: } michael@0: michael@0: let index = store.index("telMatch"); michael@0: let normalized = PhoneNumberUtils.normalize(options.filterValue, michael@0: /*numbersOnly*/ true); michael@0: michael@0: if (!normalized.length) { michael@0: dump("ContactDB: normalized filterValue is empty, can't perform match search.\n"); michael@0: return txn.abort(); michael@0: } michael@0: michael@0: // Some countries need special handling for number matching. Bug 877302 michael@0: if (this.substringMatching && normalized.length > this.substringMatching) { michael@0: let substring = normalized.slice(-this.substringMatching); michael@0: if (DEBUG) debug("Substring: " + substring); michael@0: michael@0: let substringRequest = index.mozGetAll(substring, limit); michael@0: michael@0: substringRequest.onsuccess = function (event) { michael@0: if (DEBUG) debug("Request successful. Record count: " + event.target.result.length); michael@0: for (let i in event.target.result) { michael@0: substringResult[event.target.result[i].id] = event.target.result[i]; michael@0: } michael@0: }.bind(this); michael@0: } else if (normalized[0] !== "+") { michael@0: // We might have an international prefix like '00' michael@0: let parsed = PhoneNumberUtils.parse(normalized); michael@0: if (parsed && parsed.internationalNumber && michael@0: parsed.nationalNumber && michael@0: parsed.nationalNumber !== normalized && michael@0: parsed.internationalNumber !== normalized) { michael@0: if (DEBUG) debug("Search with " + parsed.internationalNumber); michael@0: let prefixRequest = index.mozGetAll(parsed.internationalNumber, limit); michael@0: michael@0: prefixRequest.onsuccess = function (event) { michael@0: if (DEBUG) debug("Request successful. Record count: " + event.target.result.length); michael@0: for (let i in event.target.result) { michael@0: substringResult[event.target.result[i].id] = event.target.result[i]; michael@0: } michael@0: }.bind(this); michael@0: } michael@0: } michael@0: michael@0: request = index.mozGetAll(normalized, limit); michael@0: } else { michael@0: // XXX: "contains" should be handled separately, this is "startsWith" michael@0: if (options.filterOp === 'contains' && key !== 'tel') { michael@0: dump("ContactDB: 'contains' only works for 'tel'. Falling back " + michael@0: "to 'startsWith'.\n"); michael@0: } michael@0: // not case sensitive michael@0: let lowerCase = options.filterValue.toString().toLowerCase(); michael@0: if (key === "tel") { michael@0: let origLength = lowerCase.length; michael@0: let tmp = PhoneNumberUtils.normalize(lowerCase, /*numbersOnly*/ true); michael@0: if (tmp.length != origLength) { michael@0: let NON_SEARCHABLE_CHARS = /[^#+\*\d\s()-]/; michael@0: // e.g. number "123". find with "(123)" but not with "123a" michael@0: if (tmp === "" || NON_SEARCHABLE_CHARS.test(lowerCase)) { michael@0: if (DEBUG) debug("Call continue!"); michael@0: continue; michael@0: } michael@0: lowerCase = tmp; michael@0: } michael@0: } michael@0: if (DEBUG) debug("lowerCase: " + lowerCase); michael@0: let range = IDBKeyRange.bound(lowerCase, lowerCase + "\uFFFF"); michael@0: let index = store.index(key + "LowerCase"); michael@0: request = index.mozGetAll(range, limit); michael@0: } michael@0: if (!txn.result) michael@0: txn.result = {}; michael@0: michael@0: request.onsuccess = function (event) { michael@0: if (DEBUG) debug("Request successful. Record count: " + event.target.result.length); michael@0: if (Object.keys(substringResult).length > 0) { michael@0: for (let attrname in substringResult) { michael@0: event.target.result[attrname] = substringResult[attrname]; michael@0: } michael@0: } michael@0: this.sortResults(event.target.result, options); michael@0: for (let i in event.target.result) michael@0: txn.result[event.target.result[i].id] = exportContact(event.target.result[i]); michael@0: }.bind(this); michael@0: } michael@0: }, michael@0: michael@0: _findAll: function _findAll(txn, store, options) { michael@0: if (DEBUG) debug("ContactDB:_findAll: " + JSON.stringify(options)); michael@0: if (!txn.result) michael@0: txn.result = {}; michael@0: // Sorting functions takes care of limit if set. michael@0: let limit = options.sortBy === 'undefined' ? options.filterLimit : null; michael@0: store.mozGetAll(null, limit).onsuccess = function (event) { michael@0: if (DEBUG) debug("Request successful. Record count:" + event.target.result.length); michael@0: this.sortResults(event.target.result, options); michael@0: for (let i in event.target.result) { michael@0: txn.result[event.target.result[i].id] = exportContact(event.target.result[i]); michael@0: } michael@0: }.bind(this); michael@0: }, michael@0: michael@0: // Enable special phone number substring matching. Does not update existing DB entries. michael@0: enableSubstringMatching: function enableSubstringMatching(aDigits) { michael@0: if (DEBUG) debug("MCC enabling substring matching " + aDigits); michael@0: this.substringMatching = aDigits; michael@0: }, michael@0: michael@0: disableSubstringMatching: function disableSubstringMatching() { michael@0: if (DEBUG) debug("MCC disabling substring matching"); michael@0: delete this.substringMatching; michael@0: }, michael@0: michael@0: init: function init() { michael@0: this.initDBHelper(DB_NAME, DB_VERSION, [STORE_NAME, SAVED_GETALL_STORE_NAME, REVISION_STORE]); michael@0: } michael@0: };