2012-02-29 02:01:48 +04:00
|
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
|
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
|
|
|
|
"use strict";
|
|
|
|
|
2012-10-31 20:13:28 +04:00
|
|
|
this.EXPORTED_SYMBOLS = ['ContactDB'];
|
2012-02-29 02:01:48 +04:00
|
|
|
|
2012-08-23 01:34:57 +04:00
|
|
|
const DEBUG = false;
|
|
|
|
function debug(s) { dump("-*- ContactDB component: " + s + "\n"); }
|
2012-02-29 02:01:48 +04:00
|
|
|
|
2012-07-02 23:03:49 +04:00
|
|
|
const Cu = Components.utils;
|
2012-02-29 02:01:48 +04:00
|
|
|
const Cc = Components.classes;
|
|
|
|
const Ci = Components.interfaces;
|
|
|
|
|
|
|
|
Cu.import("resource://gre/modules/Services.jsm");
|
2012-04-27 02:10:04 +04:00
|
|
|
Cu.import("resource://gre/modules/IndexedDBHelper.jsm");
|
2012-11-30 02:50:19 +04:00
|
|
|
Cu.import("resource://gre/modules/PhoneNumberUtils.jsm");
|
2012-02-29 02:01:48 +04:00
|
|
|
|
|
|
|
const DB_NAME = "contacts";
|
2013-02-03 16:05:51 +04:00
|
|
|
const DB_VERSION = 8;
|
2012-02-29 02:01:48 +04:00
|
|
|
const STORE_NAME = "contacts";
|
2013-02-03 16:05:51 +04:00
|
|
|
const SAVED_GETALL_STORE_NAME = "getallcache";
|
2012-02-29 02:01:48 +04:00
|
|
|
|
2012-10-31 20:13:28 +04:00
|
|
|
this.ContactDB = function ContactDB(aGlobal) {
|
2012-08-23 01:34:57 +04:00
|
|
|
if (DEBUG) debug("Constructor");
|
2012-03-14 03:12:25 +04:00
|
|
|
this._global = aGlobal;
|
2012-02-29 02:01:48 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
ContactDB.prototype = {
|
2012-04-27 02:10:04 +04:00
|
|
|
__proto__: IndexedDBHelper.prototype,
|
2012-05-11 08:51:48 +04:00
|
|
|
|
2013-02-03 16:05:51 +04:00
|
|
|
cursorData: {},
|
|
|
|
|
2012-05-11 08:51:48 +04:00
|
|
|
upgradeSchema: function upgradeSchema(aTransaction, aDb, aOldVersion, aNewVersion) {
|
2012-08-23 01:34:57 +04:00
|
|
|
if (DEBUG) debug("upgrade schema from: " + aOldVersion + " to " + aNewVersion + " called!");
|
2012-05-11 08:51:48 +04:00
|
|
|
let db = aDb;
|
|
|
|
let objectStore;
|
|
|
|
for (let currVersion = aOldVersion; currVersion < aNewVersion; currVersion++) {
|
|
|
|
if (currVersion == 0) {
|
|
|
|
/**
|
|
|
|
* Create the initial database schema.
|
|
|
|
*
|
|
|
|
* The schema of records stored is as follows:
|
|
|
|
*
|
|
|
|
* {id: "...", // UUID
|
|
|
|
* published: Date(...), // First published date.
|
|
|
|
* updated: Date(...), // Last updated date.
|
|
|
|
* properties: {...} // Object holding the ContactProperties
|
|
|
|
* }
|
|
|
|
*/
|
2012-08-23 01:34:57 +04:00
|
|
|
if (DEBUG) debug("create schema");
|
2013-01-31 06:03:46 +04:00
|
|
|
objectStore = db.createObjectStore(STORE_NAME, {keyPath: "id"});
|
2012-05-11 08:51:48 +04:00
|
|
|
|
|
|
|
// Properties indexes
|
2013-01-31 05:17:07 +04:00
|
|
|
objectStore.createIndex("familyName", "properties.familyName", { multiEntry: true });
|
|
|
|
objectStore.createIndex("givenName", "properties.givenName", { multiEntry: true });
|
2012-05-11 08:51:48 +04:00
|
|
|
|
2013-01-31 05:17:07 +04:00
|
|
|
objectStore.createIndex("familyNameLowerCase", "search.familyName", { multiEntry: true });
|
|
|
|
objectStore.createIndex("givenNameLowerCase", "search.givenName", { multiEntry: true });
|
|
|
|
objectStore.createIndex("telLowerCase", "search.tel", { multiEntry: true });
|
|
|
|
objectStore.createIndex("emailLowerCase", "search.email", { multiEntry: true });
|
2012-05-11 08:51:48 +04:00
|
|
|
} else if (currVersion == 1) {
|
2012-08-23 01:34:57 +04:00
|
|
|
if (DEBUG) debug("upgrade 1");
|
2012-05-11 08:51:48 +04:00
|
|
|
|
2012-07-02 23:03:49 +04:00
|
|
|
// Create a new scheme for the tel field. We move from an array of tel-numbers to an array of
|
2012-05-11 08:51:48 +04:00
|
|
|
// ContactTelephone.
|
|
|
|
if (!objectStore) {
|
|
|
|
objectStore = aTransaction.objectStore(STORE_NAME);
|
|
|
|
}
|
|
|
|
// Delete old tel index.
|
2013-01-09 16:45:48 +04:00
|
|
|
if (objectStore.indexNames.contains("tel")) {
|
|
|
|
objectStore.deleteIndex("tel");
|
|
|
|
}
|
2012-05-11 08:51:48 +04:00
|
|
|
|
|
|
|
// Upgrade existing tel field in the DB.
|
2012-07-02 23:03:49 +04:00
|
|
|
objectStore.openCursor().onsuccess = function(event) {
|
2012-05-11 08:51:48 +04:00
|
|
|
let cursor = event.target.result;
|
|
|
|
if (cursor) {
|
2012-08-23 01:34:57 +04:00
|
|
|
if (DEBUG) debug("upgrade tel1: " + JSON.stringify(cursor.value));
|
2012-05-11 08:51:48 +04:00
|
|
|
for (let number in cursor.value.properties.tel) {
|
|
|
|
cursor.value.properties.tel[number] = {number: number};
|
|
|
|
}
|
|
|
|
cursor.update(cursor.value);
|
2012-08-23 01:34:57 +04:00
|
|
|
if (DEBUG) debug("upgrade tel2: " + JSON.stringify(cursor.value));
|
2012-05-11 08:51:48 +04:00
|
|
|
cursor.continue();
|
2012-07-02 23:03:49 +04:00
|
|
|
}
|
2012-05-11 08:51:48 +04:00
|
|
|
};
|
|
|
|
|
|
|
|
// Create new searchable indexes.
|
2013-01-31 05:17:07 +04:00
|
|
|
objectStore.createIndex("tel", "search.tel", { multiEntry: true });
|
|
|
|
objectStore.createIndex("category", "properties.category", { multiEntry: true });
|
2012-07-03 22:00:53 +04:00
|
|
|
} else if (currVersion == 2) {
|
2012-08-23 01:34:57 +04:00
|
|
|
if (DEBUG) debug("upgrade 2");
|
2013-01-31 06:03:46 +04:00
|
|
|
// Create a new scheme for the email field. We move from an array of emailaddresses to an array of
|
2012-07-03 22:00:53 +04:00
|
|
|
// ContactEmail.
|
|
|
|
if (!objectStore) {
|
|
|
|
objectStore = aTransaction.objectStore(STORE_NAME);
|
|
|
|
}
|
2013-01-31 05:17:07 +04:00
|
|
|
|
2012-07-03 22:00:53 +04:00
|
|
|
// Delete old email index.
|
2013-01-31 05:17:07 +04:00
|
|
|
if (objectStore.indexNames.contains("email")) {
|
|
|
|
objectStore.deleteIndex("email");
|
|
|
|
}
|
2012-07-03 22:00:53 +04:00
|
|
|
|
|
|
|
// Upgrade existing email field in the DB.
|
|
|
|
objectStore.openCursor().onsuccess = function(event) {
|
|
|
|
let cursor = event.target.result;
|
2012-11-30 02:50:19 +04:00
|
|
|
if (cursor) {
|
|
|
|
if (cursor.value.properties.email) {
|
|
|
|
if (DEBUG) debug("upgrade email1: " + JSON.stringify(cursor.value));
|
|
|
|
cursor.value.properties.email =
|
|
|
|
cursor.value.properties.email.map(function(address) { return { address: address }; });
|
|
|
|
cursor.update(cursor.value);
|
|
|
|
if (DEBUG) debug("upgrade email2: " + JSON.stringify(cursor.value));
|
|
|
|
}
|
2012-07-03 22:00:53 +04:00
|
|
|
cursor.continue();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// Create new searchable indexes.
|
2013-01-31 05:17:07 +04:00
|
|
|
objectStore.createIndex("email", "search.email", { multiEntry: true });
|
2012-08-16 03:42:08 +04:00
|
|
|
} else if (currVersion == 3) {
|
2012-08-23 01:34:57 +04:00
|
|
|
if (DEBUG) debug("upgrade 3");
|
2012-08-16 04:49:47 +04:00
|
|
|
|
2012-08-16 03:42:08 +04:00
|
|
|
if (!objectStore) {
|
|
|
|
objectStore = aTransaction.objectStore(STORE_NAME);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Upgrade existing impp field in the DB.
|
|
|
|
objectStore.openCursor().onsuccess = function(event) {
|
|
|
|
let cursor = event.target.result;
|
2012-11-30 02:50:19 +04:00
|
|
|
if (cursor) {
|
|
|
|
if (cursor.value.properties.impp) {
|
|
|
|
if (DEBUG) debug("upgrade impp1: " + JSON.stringify(cursor.value));
|
|
|
|
cursor.value.properties.impp =
|
|
|
|
cursor.value.properties.impp.map(function(value) { return { value: value }; });
|
|
|
|
cursor.update(cursor.value);
|
|
|
|
if (DEBUG) debug("upgrade impp2: " + JSON.stringify(cursor.value));
|
|
|
|
}
|
2012-08-16 03:42:08 +04:00
|
|
|
cursor.continue();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
// Upgrade existing url field in the DB.
|
|
|
|
objectStore.openCursor().onsuccess = function(event) {
|
|
|
|
let cursor = event.target.result;
|
2012-11-30 02:50:19 +04:00
|
|
|
if (cursor) {
|
|
|
|
if (cursor.value.properties.url) {
|
|
|
|
if (DEBUG) debug("upgrade url1: " + JSON.stringify(cursor.value));
|
|
|
|
cursor.value.properties.url =
|
|
|
|
cursor.value.properties.url.map(function(value) { return { value: value }; });
|
|
|
|
cursor.update(cursor.value);
|
|
|
|
if (DEBUG) debug("upgrade impp2: " + JSON.stringify(cursor.value));
|
|
|
|
}
|
|
|
|
cursor.continue();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
} else if (currVersion == 4) {
|
|
|
|
if (DEBUG) debug("Add international phone numbers upgrade");
|
|
|
|
if (!objectStore) {
|
|
|
|
objectStore = aTransaction.objectStore(STORE_NAME);
|
|
|
|
}
|
|
|
|
|
|
|
|
objectStore.openCursor().onsuccess = function(event) {
|
|
|
|
let cursor = event.target.result;
|
|
|
|
if (cursor) {
|
|
|
|
if (cursor.value.properties.tel) {
|
|
|
|
if (DEBUG) debug("upgrade : " + JSON.stringify(cursor.value));
|
|
|
|
cursor.value.properties.tel.forEach(
|
|
|
|
function(duple) {
|
|
|
|
let parsedNumber = PhoneNumberUtils.parse(duple.value.toString());
|
|
|
|
if (parsedNumber) {
|
2013-01-31 05:17:07 +04:00
|
|
|
if (DEBUG) {
|
|
|
|
debug("InternationalFormat: " + parsedNumber.internationalFormat);
|
|
|
|
debug("InternationalNumber: " + parsedNumber.internationalNumber);
|
|
|
|
debug("NationalNumber: " + parsedNumber.nationalNumber);
|
|
|
|
debug("NationalFormat: " + parsedNumber.nationalFormat);
|
|
|
|
}
|
2012-11-30 02:50:19 +04:00
|
|
|
if (duple.value.toString() !== parsedNumber.internationalNumber) {
|
|
|
|
cursor.value.search.tel.push(parsedNumber.internationalNumber);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
dump("Warning: No international number found for " + duple.value + "\n");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
|
|
|
cursor.update(cursor.value);
|
|
|
|
}
|
|
|
|
if (DEBUG) debug("upgrade2 : " + JSON.stringify(cursor.value));
|
2012-08-16 03:42:08 +04:00
|
|
|
cursor.continue();
|
|
|
|
}
|
|
|
|
};
|
2013-01-09 16:45:48 +04:00
|
|
|
} else if (currVersion == 5) {
|
|
|
|
if (DEBUG) debug("Add index for equals tel searches");
|
|
|
|
if (!objectStore) {
|
|
|
|
objectStore = aTransaction.objectStore(STORE_NAME);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Delete old tel index (not on the right field).
|
|
|
|
if (objectStore.indexNames.contains("tel")) {
|
|
|
|
objectStore.deleteIndex("tel");
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create new index for "equals" searches
|
2013-01-31 05:17:07 +04:00
|
|
|
objectStore.createIndex("tel", "search.exactTel", { multiEntry: true });
|
2013-01-09 16:45:48 +04:00
|
|
|
|
|
|
|
objectStore.openCursor().onsuccess = function(event) {
|
|
|
|
let cursor = event.target.result;
|
|
|
|
if (cursor) {
|
|
|
|
if (cursor.value.properties.tel) {
|
|
|
|
if (DEBUG) debug("upgrade : " + JSON.stringify(cursor.value));
|
|
|
|
cursor.value.properties.tel.forEach(
|
|
|
|
function(duple) {
|
|
|
|
let number = duple.value.toString();
|
|
|
|
let parsedNumber = PhoneNumberUtils.parse(number);
|
|
|
|
|
|
|
|
cursor.value.search.exactTel = [number];
|
|
|
|
if (parsedNumber &&
|
|
|
|
parsedNumber.internationalNumber &&
|
|
|
|
number !== parsedNumber.internationalNumber) {
|
|
|
|
cursor.value.search.exactTel.push(parsedNumber.internationalNumber);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
|
|
|
cursor.update(cursor.value);
|
|
|
|
}
|
|
|
|
if (DEBUG) debug("upgrade : " + JSON.stringify(cursor.value));
|
|
|
|
cursor.continue();
|
|
|
|
}
|
|
|
|
};
|
2013-01-31 05:17:07 +04:00
|
|
|
} else if (currVersion == 6) {
|
|
|
|
if (!objectStore) {
|
|
|
|
objectStore = aTransaction.objectStore(STORE_NAME);
|
|
|
|
}
|
|
|
|
let names = objectStore.indexNames;
|
|
|
|
let blackList = ["tel", "familyName", "givenName", "familyNameLowerCase",
|
|
|
|
"givenNameLowerCase", "telLowerCase", "category", "email",
|
|
|
|
"emailLowerCase"];
|
|
|
|
for (var i = 0; i < names.length; i++) {
|
|
|
|
if (blackList.indexOf(names[i]) < 0) {
|
|
|
|
objectStore.deleteIndex(names[i]);
|
|
|
|
}
|
|
|
|
}
|
2013-02-03 16:05:51 +04:00
|
|
|
} else if (currVersion == 7) {
|
|
|
|
if (DEBUG) debug("Adding object store for cached searches");
|
|
|
|
db.createObjectStore(SAVED_GETALL_STORE_NAME);
|
2012-05-11 08:51:48 +04:00
|
|
|
}
|
|
|
|
}
|
2012-02-29 02:01:48 +04:00
|
|
|
},
|
|
|
|
|
|
|
|
makeImport: function makeImport(aContact) {
|
|
|
|
let contact = {};
|
|
|
|
contact.properties = {
|
|
|
|
name: [],
|
|
|
|
honorificPrefix: [],
|
|
|
|
givenName: [],
|
|
|
|
additionalName: [],
|
|
|
|
familyName: [],
|
|
|
|
honorificSuffix: [],
|
|
|
|
nickname: [],
|
|
|
|
email: [],
|
|
|
|
photo: [],
|
|
|
|
url: [],
|
|
|
|
category: [],
|
|
|
|
adr: [],
|
|
|
|
tel: [],
|
|
|
|
org: [],
|
2012-05-11 08:51:48 +04:00
|
|
|
jobTitle: [],
|
2012-02-29 02:01:48 +04:00
|
|
|
bday: null,
|
|
|
|
note: [],
|
|
|
|
impp: [],
|
|
|
|
anniversary: null,
|
|
|
|
sex: null,
|
|
|
|
genderIdentity: null
|
|
|
|
};
|
|
|
|
|
2012-03-14 03:12:25 +04:00
|
|
|
contact.search = {
|
|
|
|
givenName: [],
|
|
|
|
familyName: [],
|
|
|
|
email: [],
|
|
|
|
category: [],
|
|
|
|
tel: [],
|
2013-01-31 05:17:07 +04:00
|
|
|
exactTel: []
|
2012-03-14 03:12:25 +04:00
|
|
|
};
|
|
|
|
|
2012-02-29 02:01:48 +04:00
|
|
|
for (let field in aContact.properties) {
|
|
|
|
contact.properties[field] = aContact.properties[field];
|
2012-03-14 03:12:25 +04:00
|
|
|
// Add search fields
|
|
|
|
if (aContact.properties[field] && contact.search[field]) {
|
|
|
|
for (let i = 0; i <= aContact.properties[field].length; i++) {
|
2012-03-21 22:45:03 +04:00
|
|
|
if (aContact.properties[field][i]) {
|
|
|
|
if (field == "tel") {
|
2012-07-02 23:03:49 +04:00
|
|
|
// Special case telephone number.
|
2012-03-21 22:45:03 +04:00
|
|
|
// "+1-234-567" should also be found with 1234, 234-56, 23456
|
|
|
|
|
|
|
|
// Chop off the first characters
|
2012-08-16 03:42:08 +04:00
|
|
|
let number = aContact.properties[field][i].value;
|
2013-01-09 16:45:48 +04:00
|
|
|
contact.search.exactTel.push(number);
|
2012-12-07 06:23:53 +04:00
|
|
|
let search = {};
|
2012-08-16 03:42:08 +04:00
|
|
|
if (number) {
|
2012-08-16 04:49:47 +04:00
|
|
|
for (let i = 0; i < number.length; i++) {
|
2012-12-07 06:23:53 +04:00
|
|
|
search[number.substring(i, number.length)] = 1;
|
2012-08-16 03:42:08 +04:00
|
|
|
}
|
|
|
|
// Store +1-234-567 as ["1234567", "234567"...]
|
|
|
|
let digits = number.match(/\d/g);
|
|
|
|
if (digits && number.length != digits.length) {
|
|
|
|
digits = digits.join('');
|
|
|
|
for(let i = 0; i < digits.length; i++) {
|
2012-12-07 06:23:53 +04:00
|
|
|
search[digits.substring(i, digits.length)] = 1;
|
2012-11-30 02:50:19 +04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if (DEBUG) debug("lookup: " + JSON.stringify(contact.search[field]));
|
|
|
|
let parsedNumber = PhoneNumberUtils.parse(number.toString());
|
|
|
|
if (parsedNumber) {
|
2013-01-31 05:17:07 +04:00
|
|
|
if (DEBUG) {
|
|
|
|
debug("InternationalFormat: " + parsedNumber.internationalFormat);
|
|
|
|
debug("InternationalNumber: " + parsedNumber.internationalNumber);
|
|
|
|
debug("NationalNumber: " + parsedNumber.nationalNumber);
|
|
|
|
debug("NationalFormat: " + parsedNumber.nationalFormat);
|
|
|
|
}
|
2012-12-13 04:38:27 +04:00
|
|
|
if (parsedNumber.internationalNumber &&
|
|
|
|
number.toString() !== parsedNumber.internationalNumber) {
|
2013-01-09 16:45:48 +04:00
|
|
|
contact.search.exactTel.push(parsedNumber.internationalNumber);
|
2012-12-07 06:23:53 +04:00
|
|
|
let digits = parsedNumber.internationalNumber.match(/\d/g);
|
|
|
|
if (digits) {
|
|
|
|
digits = digits.join('');
|
|
|
|
for(let i = 0; i < digits.length; i++) {
|
|
|
|
search[digits.substring(i, digits.length)] = 1;
|
|
|
|
}
|
|
|
|
}
|
2012-08-16 03:42:08 +04:00
|
|
|
}
|
2012-11-30 02:50:19 +04:00
|
|
|
} else {
|
|
|
|
dump("Warning: No international number found for " + number + "\n");
|
2012-03-21 22:45:03 +04:00
|
|
|
}
|
2012-08-16 03:42:08 +04:00
|
|
|
}
|
2012-12-07 06:23:53 +04:00
|
|
|
for (let num in search) {
|
|
|
|
contact.search[field].push(num);
|
|
|
|
}
|
2012-07-03 22:00:53 +04:00
|
|
|
} else if (field == "email") {
|
2012-08-16 03:42:08 +04:00
|
|
|
let address = aContact.properties[field][i].value;
|
2012-07-03 22:00:53 +04:00
|
|
|
if (address && typeof address == "string") {
|
|
|
|
contact.search[field].push(address.toLowerCase());
|
|
|
|
}
|
2012-08-16 03:42:08 +04:00
|
|
|
} else if (field == "impp") {
|
|
|
|
let value = aContact.properties[field][i].value;
|
|
|
|
if (value && typeof value == "string") {
|
|
|
|
contact.search[field].push(value.toLowerCase());
|
|
|
|
}
|
2012-03-21 22:45:03 +04:00
|
|
|
} else {
|
2012-07-03 22:00:53 +04:00
|
|
|
let val = aContact.properties[field][i];
|
|
|
|
if (typeof val == "string") {
|
|
|
|
contact.search[field].push(val.toLowerCase());
|
|
|
|
}
|
2012-03-21 22:45:03 +04:00
|
|
|
}
|
|
|
|
}
|
2012-03-14 03:12:25 +04:00
|
|
|
}
|
|
|
|
}
|
2012-02-29 02:01:48 +04:00
|
|
|
}
|
2012-08-23 01:34:57 +04:00
|
|
|
if (DEBUG) debug("contact:" + JSON.stringify(contact));
|
2012-02-29 02:01:48 +04:00
|
|
|
|
|
|
|
contact.updated = aContact.updated;
|
|
|
|
contact.published = aContact.published;
|
|
|
|
contact.id = aContact.id;
|
|
|
|
|
|
|
|
return contact;
|
|
|
|
},
|
|
|
|
|
|
|
|
makeExport: function makeExport(aRecord) {
|
|
|
|
let contact = {};
|
|
|
|
contact.properties = aRecord.properties;
|
|
|
|
|
|
|
|
for (let field in aRecord.properties)
|
|
|
|
contact.properties[field] = aRecord.properties[field];
|
|
|
|
|
|
|
|
contact.updated = aRecord.updated;
|
|
|
|
contact.published = aRecord.published;
|
|
|
|
contact.id = aRecord.id;
|
|
|
|
return contact;
|
|
|
|
},
|
|
|
|
|
|
|
|
updateRecordMetadata: function updateRecordMetadata(record) {
|
|
|
|
if (!record.id) {
|
|
|
|
Cu.reportError("Contact without ID");
|
|
|
|
}
|
|
|
|
if (!record.published) {
|
|
|
|
record.published = new Date();
|
|
|
|
}
|
|
|
|
record.updated = new Date();
|
|
|
|
},
|
|
|
|
|
2013-02-03 16:05:51 +04:00
|
|
|
removeObjectFromCache: function CDB_removeObjectFromCache(aObjectId, aCallback) {
|
|
|
|
if (DEBUG) debug("removeObjectFromCache: " + aObjectId);
|
|
|
|
if (!aObjectId) {
|
|
|
|
if (DEBUG) debug("No object ID passed");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.newTxn("readwrite", SAVED_GETALL_STORE_NAME, function(txn, store) {
|
|
|
|
store.openCursor().onsuccess = function(e) {
|
|
|
|
let cursor = e.target.result;
|
|
|
|
if (cursor) {
|
|
|
|
for (let i = 0; i < cursor.value.length; ++i) {
|
|
|
|
if (cursor.value[i] == aObjectId) {
|
|
|
|
if (DEBUG) debug("id matches cache");
|
|
|
|
cursor.value.splice(i, 1);
|
|
|
|
cursor.update(cursor.value);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
cursor.continue();
|
|
|
|
} else {
|
|
|
|
aCallback();
|
|
|
|
}
|
|
|
|
}.bind(this);
|
|
|
|
}.bind(this));
|
|
|
|
},
|
|
|
|
|
|
|
|
// Invalidate the entire cache. It will be incrementally regenerated on demand
|
|
|
|
// See getCacheForQuery
|
|
|
|
invalidateCache: function CDB_invalidateCache() {
|
|
|
|
if (DEBUG) debug("invalidate cache");
|
|
|
|
this.newTxn("readwrite", SAVED_GETALL_STORE_NAME, function (txn, store) {
|
|
|
|
store.clear();
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2012-02-29 02:01:48 +04:00
|
|
|
saveContact: function saveContact(aContact, successCb, errorCb) {
|
|
|
|
let contact = this.makeImport(aContact);
|
2013-01-31 06:03:46 +04:00
|
|
|
this.newTxn("readwrite", STORE_NAME, function (txn, store) {
|
2012-08-23 01:34:57 +04:00
|
|
|
if (DEBUG) debug("Going to update" + JSON.stringify(contact));
|
2012-02-29 02:01:48 +04:00
|
|
|
|
|
|
|
// Look up the existing record and compare the update timestamp.
|
|
|
|
// If no record exists, just add the new entry.
|
|
|
|
let newRequest = store.get(contact.id);
|
|
|
|
newRequest.onsuccess = function (event) {
|
|
|
|
if (!event.target.result) {
|
2012-08-23 01:34:57 +04:00
|
|
|
if (DEBUG) debug("new record!")
|
2012-02-29 02:01:48 +04:00
|
|
|
this.updateRecordMetadata(contact);
|
|
|
|
store.put(contact);
|
|
|
|
} else {
|
2012-08-23 01:34:57 +04:00
|
|
|
if (DEBUG) debug("old record!")
|
2012-02-29 02:01:48 +04:00
|
|
|
if (new Date(typeof contact.updated === "undefined" ? 0 : contact.updated) < new Date(event.target.result.updated)) {
|
2012-08-23 01:34:57 +04:00
|
|
|
if (DEBUG) debug("rev check fail!");
|
2012-02-29 02:01:48 +04:00
|
|
|
txn.abort();
|
|
|
|
return;
|
|
|
|
} else {
|
2012-08-23 01:34:57 +04:00
|
|
|
if (DEBUG) debug("rev check OK");
|
2012-02-29 02:01:48 +04:00
|
|
|
contact.published = event.target.result.published;
|
|
|
|
contact.updated = new Date();
|
|
|
|
store.put(contact);
|
|
|
|
}
|
|
|
|
}
|
2013-02-03 16:05:51 +04:00
|
|
|
this.invalidateCache();
|
2012-02-29 02:01:48 +04:00
|
|
|
}.bind(this);
|
|
|
|
}.bind(this), successCb, errorCb);
|
|
|
|
},
|
|
|
|
|
|
|
|
removeContact: function removeContact(aId, aSuccessCb, aErrorCb) {
|
2013-02-03 16:05:51 +04:00
|
|
|
if (DEBUG) debug("removeContact: " + aId);
|
|
|
|
this.removeObjectFromCache(aId, function() {
|
|
|
|
this.newTxn("readwrite", STORE_NAME, function(txn, store) {
|
|
|
|
store.delete(aId).onsuccess = function() {
|
|
|
|
aSuccessCb();
|
|
|
|
};
|
|
|
|
}, null, aErrorCb);
|
|
|
|
}.bind(this));
|
2012-02-29 02:01:48 +04:00
|
|
|
},
|
|
|
|
|
|
|
|
clear: function clear(aSuccessCb, aErrorCb) {
|
2013-01-31 06:03:46 +04:00
|
|
|
this.newTxn("readwrite", STORE_NAME, function (txn, store) {
|
2012-08-23 01:34:57 +04:00
|
|
|
if (DEBUG) debug("Going to clear all!");
|
2012-02-29 02:01:48 +04:00
|
|
|
store.clear();
|
|
|
|
}, aSuccessCb, aErrorCb);
|
|
|
|
},
|
|
|
|
|
2013-02-03 16:05:51 +04:00
|
|
|
getObjectById: function CDB_getObjectById(aStore, aObjectId, aCallback) {
|
|
|
|
if (DEBUG) debug("getObjectById: " + aStore + ":" + aObjectId);
|
|
|
|
this.newTxn("readonly", aStore, function (txn, store) {
|
|
|
|
let req = store.get(aObjectId);
|
|
|
|
req.onsuccess = function (event) {
|
|
|
|
aCallback(event.target.result);
|
|
|
|
};
|
|
|
|
req.onerror = function (event) {
|
|
|
|
aCallback(null);
|
|
|
|
};
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
getCacheForQuery: function CDB_getCacheForQuery(aQuery, aCursorId, aSuccessCb) {
|
|
|
|
if (DEBUG) debug("getCacheForQuery");
|
|
|
|
// Here we try to get the cached results for query `aQuery'. If they don't
|
|
|
|
// exist, it means the cache was invalidated and needs to be recreated, so
|
|
|
|
// we do that. Otherwise, we just return the existing cache.
|
|
|
|
this.getObjectById(SAVED_GETALL_STORE_NAME, aQuery, function (aCache) {
|
|
|
|
if (!aCache) {
|
|
|
|
if (DEBUG) debug("creating cache for query " + aQuery);
|
|
|
|
this.createCacheForQuery(aQuery, aCursorId, aSuccessCb);
|
|
|
|
} else {
|
|
|
|
if (DEBUG) debug("cache exists");
|
|
|
|
if (!this.cursorData[aCursorId]) {
|
|
|
|
this.cursorData[aCursorId] = aCache;
|
|
|
|
}
|
|
|
|
aSuccessCb(aCache);
|
|
|
|
}
|
|
|
|
}.bind(this));
|
|
|
|
},
|
|
|
|
|
|
|
|
setCacheForQuery: function CDB_setCacheForQuery(aQuery, aCache, aCallback) {
|
|
|
|
this.newTxn("readwrite", SAVED_GETALL_STORE_NAME, function (txn, store) {
|
|
|
|
let req = store.put(aCache, aQuery);
|
|
|
|
if (!aCallback) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
req.onsuccess = function () {
|
|
|
|
aCallback(true);
|
|
|
|
};
|
|
|
|
req.onerror = function () {
|
|
|
|
aCallback(false);
|
|
|
|
};
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
createCacheForQuery: function CDB_createCacheForQuery(aQuery, aCursorId, aSuccessCb, aFailureCb) {
|
|
|
|
this.find(function (aContacts) {
|
|
|
|
if (aContacts) {
|
|
|
|
let contactsArray = [];
|
|
|
|
for (let i in aContacts) {
|
|
|
|
contactsArray.push(aContacts[i].id);
|
|
|
|
}
|
|
|
|
|
|
|
|
this.setCacheForQuery(aQuery, contactsArray);
|
|
|
|
this.cursorData[aCursorId] = contactsArray;
|
|
|
|
aSuccessCb(contactsArray);
|
|
|
|
} else {
|
|
|
|
aSuccessCb(null);
|
|
|
|
}
|
|
|
|
}.bind(this),
|
|
|
|
function (aErrorMsg) { aFailureCb(aErrorMsg); },
|
|
|
|
JSON.parse(aQuery));
|
|
|
|
},
|
|
|
|
|
|
|
|
getAll: function CDB_getAll(aSuccessCb, aFailureCb, aOptions, aCursorId) {
|
|
|
|
// Recreate the cache for this query if needed
|
|
|
|
let optionStr = JSON.stringify(aOptions);
|
|
|
|
this.getCacheForQuery(optionStr, aCursorId, function (aCachedResults) {
|
|
|
|
if (aCachedResults && aCachedResults.length > 0) {
|
|
|
|
if (DEBUG) debug("query returned at least one contact");
|
|
|
|
this.getObjectById(STORE_NAME, aCachedResults[0], function (aContact) {
|
|
|
|
this.cursorData[aCursorId].shift();
|
|
|
|
aSuccessCb(aContact);
|
|
|
|
}.bind(this));
|
|
|
|
} else { // no contacts
|
|
|
|
if (DEBUG) debug("query returned no contacts");
|
|
|
|
aSuccessCb(null);
|
|
|
|
}
|
|
|
|
}.bind(this));
|
|
|
|
},
|
|
|
|
|
|
|
|
getNext: function CDB_getNext(aSuccessCb, aFailureCb, aCursorId) {
|
|
|
|
if (DEBUG) debug("ContactDB:getNext: " + aCursorId);
|
|
|
|
let aCachedResults = this.cursorData[aCursorId];
|
|
|
|
if (DEBUG) debug("got transient cache");
|
|
|
|
if (aCachedResults.length > 0) {
|
|
|
|
this.getObjectById(STORE_NAME, aCachedResults[0], function(aContact) {
|
|
|
|
this.cursorData[aCursorId].shift();
|
|
|
|
if (aContact) {
|
|
|
|
aSuccessCb(aContact);
|
|
|
|
} else {
|
|
|
|
// If the contact ID in cache is invalid, it was removed recently and
|
|
|
|
// the cache hasn't been updated to reflect the change, so we skip it.
|
|
|
|
if (DEBUG) debug("invalid contact in cache: " + aCachedResults[0]);
|
|
|
|
return this.getNext(aSuccessCb, aFailureCb, aCursorId);
|
|
|
|
}
|
|
|
|
}.bind(this));
|
|
|
|
} else { // last contact
|
|
|
|
delete this.cursorData[aCursorId];
|
|
|
|
aSuccessCb(null);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
releaseCursors: function CDB_releaseCursors(aCursors) {
|
|
|
|
for (let i of aCursors) {
|
|
|
|
delete this.cursorData[i];
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Sorting the contacts by sortBy field. aSortBy can either be familyName or givenName.
|
|
|
|
* If 2 entries have the same sortyBy field or no sortBy field is present, we continue
|
|
|
|
* sorting with the other sortyBy field.
|
|
|
|
*/
|
|
|
|
sortResults: function CDB_sortResults(aResults, aFindOptions) {
|
|
|
|
if (!aFindOptions)
|
|
|
|
return;
|
|
|
|
if (aFindOptions.sortBy != "undefined") {
|
|
|
|
aResults.sort(function (a, b) {
|
|
|
|
let x, y;
|
|
|
|
let result = 0;
|
|
|
|
let sortOrder = aFindOptions.sortOrder;
|
|
|
|
let sortBy = aFindOptions.sortBy == "familyName" ? [ "familyName", "givenName" ] : [ "givenName" , "familyName" ];
|
|
|
|
let xIndex = 0;
|
|
|
|
let yIndex = 0;
|
|
|
|
|
|
|
|
do {
|
|
|
|
while (xIndex < sortBy.length && !x) {
|
|
|
|
x = a.properties[sortBy[xIndex]] ? a.properties[sortBy[xIndex]][0].toLowerCase() : null;
|
|
|
|
xIndex++;
|
|
|
|
}
|
|
|
|
if (!x) {
|
|
|
|
return sortOrder == 'ascending' ? 1 : -1;
|
|
|
|
}
|
|
|
|
while (yIndex < sortBy.length && !y) {
|
|
|
|
y = b.properties[sortBy[yIndex]] ? b.properties[sortBy[yIndex]][0].toLowerCase() : null;
|
|
|
|
yIndex++;
|
|
|
|
}
|
|
|
|
if (!y) {
|
|
|
|
return sortOrder == 'ascending' ? 1 : -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
result = x.localeCompare(y);
|
|
|
|
x = null;
|
|
|
|
y = null;
|
|
|
|
} while (result == 0);
|
|
|
|
|
|
|
|
return sortOrder == 'ascending' ? result : -result;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
if (aFindOptions.filterLimit && aFindOptions.filterLimit != 0) {
|
|
|
|
if (DEBUG) debug("filterLimit is set: " + aFindOptions.filterLimit);
|
|
|
|
aResults.splice(aFindOptions.filterLimit, aResults.length);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2012-02-29 02:01:48 +04:00
|
|
|
/**
|
|
|
|
* @param successCb
|
|
|
|
* Callback function to invoke with result array.
|
|
|
|
* @param failureCb [optional]
|
|
|
|
* Callback function to invoke when there was an error.
|
|
|
|
* @param options [optional]
|
|
|
|
* Object specifying search options. Possible attributes:
|
|
|
|
* - filterBy
|
|
|
|
* - filterOp
|
|
|
|
* - filterValue
|
|
|
|
* - count
|
|
|
|
*/
|
|
|
|
find: function find(aSuccessCb, aFailureCb, aOptions) {
|
2013-02-03 16:05:51 +04:00
|
|
|
if (DEBUG) debug("ContactDB:find val:" + aOptions.filterValue + " by: " + aOptions.filterBy + " op: " + aOptions.filterOp);
|
2012-02-29 02:01:48 +04:00
|
|
|
let self = this;
|
2013-01-31 06:03:46 +04:00
|
|
|
this.newTxn("readonly", STORE_NAME, function (txn, store) {
|
2012-03-14 03:12:25 +04:00
|
|
|
if (aOptions && (aOptions.filterOp == "equals" || aOptions.filterOp == "contains")) {
|
2012-02-29 02:01:48 +04:00
|
|
|
self._findWithIndex(txn, store, aOptions);
|
|
|
|
} else {
|
|
|
|
self._findAll(txn, store, aOptions);
|
|
|
|
}
|
|
|
|
}, aSuccessCb, aFailureCb);
|
|
|
|
},
|
|
|
|
|
|
|
|
_findWithIndex: function _findWithIndex(txn, store, options) {
|
2012-08-23 01:34:57 +04:00
|
|
|
if (DEBUG) debug("_findWithIndex: " + options.filterValue +" " + options.filterOp + " " + options.filterBy + " ");
|
2012-02-29 02:01:48 +04:00
|
|
|
let fields = options.filterBy;
|
|
|
|
for (let key in fields) {
|
2012-08-23 01:34:57 +04:00
|
|
|
if (DEBUG) debug("key: " + fields[key]);
|
2013-02-03 16:05:51 +04:00
|
|
|
if (!store.indexNames.contains(fields[key]) && fields[key] != "id") {
|
2012-08-23 01:34:57 +04:00
|
|
|
if (DEBUG) debug("Key not valid!" + fields[key] + ", " + store.indexNames);
|
2012-02-29 02:01:48 +04:00
|
|
|
txn.abort();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// lookup for all keys
|
|
|
|
if (options.filterBy.length == 0) {
|
2012-08-23 01:34:57 +04:00
|
|
|
if (DEBUG) debug("search in all fields!" + JSON.stringify(store.indexNames));
|
2012-02-29 02:01:48 +04:00
|
|
|
for(let myIndex = 0; myIndex < store.indexNames.length; myIndex++) {
|
|
|
|
fields = Array.concat(fields, store.indexNames[myIndex])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-04-03 03:39:57 +04:00
|
|
|
// Sorting functions takes care of limit if set.
|
|
|
|
let limit = options.sortBy === 'undefined' ? options.filterLimit : null;
|
|
|
|
|
2012-02-29 02:01:48 +04:00
|
|
|
let filter_keys = fields.slice();
|
|
|
|
for (let key = filter_keys.shift(); key; key = filter_keys.shift()) {
|
|
|
|
let request;
|
|
|
|
if (key == "id") {
|
|
|
|
// store.get would return an object and not an array
|
2012-07-02 23:03:49 +04:00
|
|
|
request = store.mozGetAll(options.filterValue);
|
2012-05-11 08:51:48 +04:00
|
|
|
} else if (key == "category") {
|
|
|
|
let index = store.index(key);
|
2012-07-02 23:03:49 +04:00
|
|
|
request = index.mozGetAll(options.filterValue, limit);
|
2012-03-14 03:12:25 +04:00
|
|
|
} else if (options.filterOp == "equals") {
|
2012-08-23 01:34:57 +04:00
|
|
|
if (DEBUG) debug("Getting index: " + key);
|
2012-03-14 03:12:25 +04:00
|
|
|
// case sensitive
|
2012-02-29 02:01:48 +04:00
|
|
|
let index = store.index(key);
|
2012-07-02 23:03:49 +04:00
|
|
|
request = index.mozGetAll(options.filterValue, limit);
|
2012-03-14 03:12:25 +04:00
|
|
|
} else {
|
|
|
|
// not case sensitive
|
2012-07-03 22:00:53 +04:00
|
|
|
let tmp = typeof options.filterValue == "string"
|
|
|
|
? options.filterValue.toLowerCase()
|
|
|
|
: options.filterValue.toString().toLowerCase();
|
2012-12-07 06:23:53 +04:00
|
|
|
if (key === 'tel') {
|
|
|
|
let digits = tmp.match(/\d/g);
|
|
|
|
if (digits) {
|
|
|
|
tmp = digits.join('');
|
|
|
|
}
|
|
|
|
}
|
2012-03-14 03:12:25 +04:00
|
|
|
let range = this._global.IDBKeyRange.bound(tmp, tmp + "\uFFFF");
|
|
|
|
let index = store.index(key + "LowerCase");
|
2012-07-02 23:03:49 +04:00
|
|
|
request = index.mozGetAll(range, limit);
|
2012-02-29 02:01:48 +04:00
|
|
|
}
|
|
|
|
if (!txn.result)
|
|
|
|
txn.result = {};
|
|
|
|
|
|
|
|
request.onsuccess = function (event) {
|
2013-02-03 16:05:51 +04:00
|
|
|
if (DEBUG) debug("Request successful. Record count: " + event.target.result.length);
|
|
|
|
this.sortResults(event.target.result, options);
|
2012-02-29 02:01:48 +04:00
|
|
|
for (let i in event.target.result)
|
|
|
|
txn.result[event.target.result[i].id] = this.makeExport(event.target.result[i]);
|
|
|
|
}.bind(this);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
_findAll: function _findAll(txn, store, options) {
|
2012-08-23 01:34:57 +04:00
|
|
|
if (DEBUG) debug("ContactDB:_findAll: " + JSON.stringify(options));
|
2012-02-29 02:01:48 +04:00
|
|
|
if (!txn.result)
|
|
|
|
txn.result = {};
|
2012-04-03 03:39:57 +04:00
|
|
|
// Sorting functions takes care of limit if set.
|
|
|
|
let limit = options.sortBy === 'undefined' ? options.filterLimit : null;
|
2012-07-02 23:03:49 +04:00
|
|
|
store.mozGetAll(null, limit).onsuccess = function (event) {
|
2013-02-03 16:05:51 +04:00
|
|
|
if (DEBUG) debug("Request successful. Record count:" + event.target.result.length);
|
|
|
|
this.sortResults(event.target.result, options);
|
|
|
|
for (let i in event.target.result) {
|
2012-02-29 02:01:48 +04:00
|
|
|
txn.result[event.target.result[i].id] = this.makeExport(event.target.result[i]);
|
2013-02-03 16:05:51 +04:00
|
|
|
}
|
2012-02-29 02:01:48 +04:00
|
|
|
}.bind(this);
|
2012-04-27 02:10:04 +04:00
|
|
|
},
|
|
|
|
|
|
|
|
init: function init(aGlobal) {
|
2013-02-03 16:05:51 +04:00
|
|
|
this.initDBHelper(DB_NAME, DB_VERSION, [STORE_NAME, SAVED_GETALL_STORE_NAME], aGlobal);
|
2012-02-29 02:01:48 +04:00
|
|
|
}
|
|
|
|
};
|