diff --git a/b2g/app/b2g.js b/b2g/app/b2g.js index 611647a0f79a..ea78fb7a1472 100644 --- a/b2g/app/b2g.js +++ b/b2g/app/b2g.js @@ -410,6 +410,10 @@ pref("dom.mozBrowserFramesWhitelist", "http://localhost:7777"); pref("dom.sms.enabled", true); pref("dom.sms.whitelist", "file://,http://localhost:7777"); +// Temporary permission hack for WebContacts +pref("dom.mozContacts.enabled", true); +pref("dom.mozContacts.whitelist", "http://localhost:7777"); + // Ignore X-Frame-Options headers. pref("b2g.ignoreXFrameOptions", true); diff --git a/b2g/chrome/content/shell.js b/b2g/chrome/content/shell.js index 45c66aa0a81a..ec3c8e5b02d4 100644 --- a/b2g/chrome/content/shell.js +++ b/b2g/chrome/content/shell.js @@ -16,6 +16,7 @@ const LocalFile = CC('@mozilla.org/file/local;1', Cu.import('resource://gre/modules/XPCOMUtils.jsm'); Cu.import('resource://gre/modules/Services.jsm'); +Cu.import('resource://gre/modules/ContactService.jsm'); XPCOMUtils.defineLazyGetter(Services, 'env', function() { return Cc['@mozilla.org/process/environment;1'] @@ -60,7 +61,7 @@ function startupHttpd(baseDir, port) { // XXX never grant 'content-camera' to non-gaia apps function addPermissions(urls) { let permissions = [ - 'indexedDB', 'indexedDB-unlimited', 'webapps-manage', 'offline-app', 'content-camera' + 'indexedDB', 'indexedDB-unlimited', 'webapps-manage', 'offline-app', 'content-camera', 'webcontacts-manage' ]; urls.forEach(function(url) { let uri = Services.io.newURI(url, null, null); diff --git a/b2g/installer/package-manifest.in b/b2g/installer/package-manifest.in index d8c60779378f..7252db360e28 100644 --- a/b2g/installer/package-manifest.in +++ b/b2g/installer/package-manifest.in @@ -155,6 +155,7 @@ @BINPATH@/components/dom_bluetooth.xpt #endif @BINPATH@/components/dom_canvas.xpt +@BINPATH@/components/dom_contacts.xpt @BINPATH@/components/dom_core.xpt @BINPATH@/components/dom_css.xpt @BINPATH@/components/dom_events.xpt @@ -294,6 +295,8 @@ ; JavaScript components @BINPATH@/components/ConsoleAPI.manifest @BINPATH@/components/ConsoleAPI.js +@BINPATH@/components/ContactManager.js +@BINPATH@/components/ContactManager.manifest @BINPATH@/components/FeedProcessor.manifest @BINPATH@/components/FeedProcessor.js @BINPATH@/components/BrowserFeeds.manifest diff --git a/browser/installer/package-manifest.in b/browser/installer/package-manifest.in index 7b4ade489b6c..b796cec7b4d4 100644 --- a/browser/installer/package-manifest.in +++ b/browser/installer/package-manifest.in @@ -155,6 +155,7 @@ @BINPATH@/components/dom_bluetooth.xpt #endif @BINPATH@/components/dom_canvas.xpt +@BINPATH@/components/dom_contacts.xpt @BINPATH@/components/dom_core.xpt @BINPATH@/components/dom_css.xpt @BINPATH@/components/dom_events.xpt @@ -409,6 +410,9 @@ @BINPATH@/components/messageWakeupService.js @BINPATH@/components/messageWakeupService.manifest +@BINPATH@/components/ContactManager.js +@BINPATH@/components/ContactManager.manifest + ; Modules @BINPATH@/modules/* diff --git a/dom/Makefile.in b/dom/Makefile.in index 91a0480f65e5..a159938f6663 100644 --- a/dom/Makefile.in +++ b/dom/Makefile.in @@ -47,6 +47,7 @@ MODULE = dom DIRS = \ interfaces/base \ interfaces/canvas \ + interfaces/contacts \ interfaces/core \ interfaces/html \ interfaces/events \ @@ -77,6 +78,7 @@ endif DIRS += \ base \ battery \ + contacts \ power \ sms \ src \ diff --git a/dom/contacts/ContactManager.js b/dom/contacts/ContactManager.js new file mode 100644 index 000000000000..9821330351d1 --- /dev/null +++ b/dom/contacts/ContactManager.js @@ -0,0 +1,404 @@ +/* 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" + +/* static functions */ +let DEBUG = 0; +if (DEBUG) + debug = function (s) { dump("-*- ContactManager: " + s + "\n"); } +else + debug = function (s) {} + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +const nsIClassInfo = Ci.nsIClassInfo; +const CONTACTPROPERTIES_CID = Components.ID("{53ed7c20-ceda-11e0-9572-0800200c9a66}"); +const nsIDOMContactProperties = Ci.nsIDOMContactProperties; + +// ContactProperties is not directly instantiated. It is used as interface. + +ContactProperties.prototype = { + + classID : CONTACTPROPERTIES_CID, + classInfo : XPCOMUtils.generateCI({classID: CONTACTPROPERTIES_CID, + contractID:"@mozilla.org/contactProperties;1", + classDescription: "ContactProperties", + interfaces: [nsIDOMContactProperties], + flags: nsIClassInfo.DOM_OBJECT}), + + QueryInterface : XPCOMUtils.generateQI([nsIDOMContactProperties]) +} + +//ContactAddress + +const CONTACTADDRESS_CONTRACTID = "@mozilla.org/contactAddress;1"; +const CONTACTADDRESS_CID = Components.ID("{27a568b0-cee1-11e0-9572-0800200c9a66}"); +const nsIDOMContactAddress = Components.interfaces.nsIDOMContactAddress; + +function ContactAddress(aStreetAddress, aLocality, aRegion, aPostalCode, aCountryName) { + this.streetAddress = aStreetAddress || null; + this.locality = aLocality || null; + this.region = aRegion || null; + this.postalCode = aPostalCode || null; + this.countryName = aCountryName || null; +}; + +function ContactProperties(aProp) { debug("ContactProperties Constructor"); } + +ContactAddress.prototype = { + + classID : CONTACTADDRESS_CID, + classInfo : XPCOMUtils.generateCI({classID: CONTACTADDRESS_CID, + contractID: CONTACTADDRESS_CONTRACTID, + classDescription: "ContactAddress", + interfaces: [nsIDOMContactAddress], + flags: nsIClassInfo.DOM_OBJECT}), + + QueryInterface : XPCOMUtils.generateQI([nsIDOMContactAddress]) +} + +//ContactFindOptions + +const CONTACTFINDOPTIONS_CONTRACTID = "@mozilla.org/contactFindOptions;1"; +const CONTACTFINDOPTIONS_CID = Components.ID("{e31daea0-0cb6-11e1-be50-0800200c9a66}"); +const nsIDOMContactFindOptions = Components.interfaces.nsIDOMContactFindOptions; + +function ContactFindOptions(aFilterValue, aFilterBy, aFilterOp, aFilterLimit) { + this.filterValue = aFilterValue || ''; + + this.filterBy = new Array(); + for (let field in aFilterBy) + this.filterBy.push(field); + + this.filterOp = aFilterOp || ''; + this.filterLimit = aFilterLimit || 0; +}; + +ContactFindOptions.prototype = { + + classID : CONTACTFINDOPTIONS_CID, + classInfo : XPCOMUtils.generateCI({classID: CONTACTFINDOPTIONS_CID, + contractID: CONTACTFINDOPTIONS_CONTRACTID, + classDescription: "ContactFindOptions", + interfaces: [nsIDOMContactFindOptions], + flags: nsIClassInfo.DOM_OBJECT}), + + QueryInterface : XPCOMUtils.generateQI([nsIDOMContactFindOptions]) +} + +//Contact + +const CONTACT_CONTRACTID = "@mozilla.org/contact;1"; +const CONTACT_CID = Components.ID("{da0f7040-388b-11e1-b86c-0800200c9a66}"); +const nsIDOMContact = Components.interfaces.nsIDOMContact; + +function Contact() { debug("Contact constr: "); }; + +Contact.prototype = { + + init: function init(aProp) { + // Accept non-array strings for DOMString[] properties and convert them. + function _create(aField) { + if (typeof aField == "string") + return new Array(aField); + return aField; + }; + + this.name = _create(aProp.name) || null; + this.honorificPrefix = _create(aProp.honorificPrefix) || null; + this.givenName = _create(aProp.givenName) || null; + this.additionalName = _create(aProp.additionalName) || null; + this.familyName = _create(aProp.familyName) || null; + this.honorificSuffix = _create(aProp.honorificSuffix) || null; + this.nickname = _create(aProp.nickname) || null; + this.email = _create(aProp.email) || null; + this.photo = _create(aProp.photo) || null; + this.url = _create(aProp.url) || null; + this.category = _create(aProp.category) || null; + + if (aProp.adr) { + // Make sure adr argument is an array. Instanceof doesn't work. + aProp.adr = aProp.adr.length == undefined ? [aProp.adr] : aProp.adr; + + this.adr = new Array(); + for (let i = 0; i < aProp.adr.length; i++) + this.adr.push(new ContactAddress(aProp.adr[i].streetAddress, aProp.adr[i].locality, + aProp.adr[i].region, aProp.adr[i].postalCode, + aProp.adr[i].countryName)); + } else { + this.adr = null; + } + + this.tel = _create(aProp.tel) || null; + this.org = _create(aProp.org) || null; + this.bday = (aProp.bday == "undefined" || aProp.bday == null) ? null : new Date(aProp.bday); + this.note = _create(aProp.note) || null; + this.impp = _create(aProp.impp) || null; + this.anniversary = (aProp.anniversary == "undefined" || aProp.anniversary == null) ? null : new Date(aProp.anniversary); + this.sex = (aProp.sex != "undefined") ? aProp.sex : null; + this.genderIdentity = (aProp.genderIdentity != "undefined") ? aProp.genderIdentity : null; + }, + + get published () { + return this._published; + }, + + set published(aPublished) { + this._published = aPublished; + }, + + get updated () { + return this._updated; + }, + + set updated(aUpdated) { + this._updated = aUpdated; + }, + + classID : CONTACT_CID, + classInfo : XPCOMUtils.generateCI({classID: CONTACT_CID, + contractID: CONTACT_CONTRACTID, + classDescription: "Contact", + interfaces: [nsIDOMContact, nsIDOMContactProperties], + flags: nsIClassInfo.DOM_OBJECT}), + + QueryInterface : XPCOMUtils.generateQI([nsIDOMContact, nsIDOMContactProperties]) +} + +// ContactManager + +const CONTACTMANAGER_CONTRACTID = "@mozilla.org/contactManager;1"; +const CONTACTMANAGER_CID = Components.ID("{50a820b0-ced0-11e0-9572-0800200c9a66}"); +const nsIDOMContactManager = Components.interfaces.nsIDOMContactManager; + +function ContactManager() +{ + debug("Constructor"); +} + +ContactManager.prototype = { + + save: function save(aContact) { + let request; + if (this.hasPrivileges) { + debug("save: " + JSON.stringify(aContact) + " :" + aContact.id); + let newContact = {}; + newContact.properties = { + name: [], + honorificPrefix: [], + givenName: [], + additionalName: [], + familyName: [], + honorificSuffix: [], + nickname: [], + email: [], + photo: [], + url: [], + category: [], + adr: [], + tel: [], + org: [], + bday: null, + note: [], + impp: [], + anniversary: null, + sex: null, + genderIdentity: null + }; + for (let field in newContact.properties) + newContact.properties[field] = aContact[field]; + + if (aContact.id == "undefined") { + debug("Create id!"); + aContact.id = this._getRandomId(); + } + + this._setMetaData(newContact, aContact); + debug("send: " + JSON.stringify(newContact)); + request = this._rs.createRequest(this._window); + this._mm.sendAsyncMessage("Contact:Save", {contact: newContact, + requestID: this.getRequestId({ request: request })}); + return request; + } else { + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + } + }, + + remove: function removeContact(aRecord) { + let request; + if (this.hasPrivileges) { + request = this._rs.createRequest(this._window); + this._mm.sendAsyncMessage("Contact:Remove", {id: aRecord.id, + requestID: this.getRequestId({ request: request })}); + return request; + } else { + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + } + }, + + _setMetaData: function(aNewContact, aRecord) { + aNewContact.id = aRecord.id; + aNewContact.published = aRecord.published; + aNewContact.updated = aRecord.updated; + }, + + _convertContactsArray: function(aContacts) { + let contacts = new Array(); + for (let i in aContacts) { + let newContact = new Contact(); + newContact.init(aContacts[i].properties); + this._setMetaData(newContact, aContacts[i]); + contacts.push(newContact); + } + return contacts; + }, + + getRequestId: function(aRequest) { + let id = "id" + this._getRandomId(); + this._requests[id] = aRequest; + return id; + }, + + getRequest: function(aId) { + if (this._requests[aId]) + return this._requests[aId].request; + }, + + removeRequest: function(aId) { + if (this._requests[aId]) + delete this._requests[aId]; + }, + + _getRandomId: function() { + return Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator).generateUUID().toString(); + }, + + receiveMessage: function(aMessage) { + debug("Contactmanager::receiveMessage: " + aMessage.name); + let msg = aMessage.json; + let contacts = msg.contacts; + + switch (aMessage.name) { + case "Contacts:Find:Return:OK": + let req = this.getRequest(msg.requestID); + if (req) { + let result = this._convertContactsArray(contacts); + debug("result: " + JSON.stringify(result)); + this._rs.fireSuccess(req, result); + } else { + debug("no request stored!" + msg.requestID); + } + break; + case "Contact:Save:Return:OK": + case "Contacts:Clear:Return:OK": + case "Contact:Remove:Return:OK": + req = this.getRequest(msg.requestID); + if (req) + this._rs.fireSuccess(req, 0); + break; + case "Contacts:Find:Return:KO": + case "Contact:Save:Return:KO": + case "Contact:Remove:Return:KO": + case "Contacts:Clear:Return:KO": + req = this.getRequest(msg.requestID); + if (req) + this._rs.fireError(req, msg.errorMsg); + break; + default: + debug("Wrong message: " + aMessage.name); + } + this.removeRequest(msg.requestID); + }, + + find: function(aOptions) { + let request; + if (this.hasPrivileges) { + request = this._rs.createRequest(this._window); + this._mm.sendAsyncMessage("Contacts:Find", {findOptions: aOptions, + requestID: this.getRequestId({ request: request })}); + return request; + } else { + debug("find not allowed"); + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + } + }, + + clear: function() { + let request; + if (this.hasPrivileges) { + request = this._rs.createRequest(this._window); + this._mm.sendAsyncMessage("Contacts:Clear", {requestID: this.getRequestId({ request: request })}); + return request; + } else { + debug("clear not allowed"); + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + } + }, + + init: function(aWindow) { + // Set navigator.mozContacts to null. + if (!Services.prefs.getBoolPref("dom.mozContacts.enabled")) + return null; + + this._window = aWindow; + this._messages = ["Contacts:Find:Return:OK", "Contacts:Find:Return:KO", + "Contacts:Clear:Return:OK", "Contacts:Clear:Return:KO", + "Contact:Save:Return:OK", "Contact:Save:Return:KO", + "Contact:Remove:Return:OK", "Contact:Remove:Return:KO"]; + + this._mm = Cc["@mozilla.org/childprocessmessagemanager;1"].getService(Ci.nsIFrameMessageManager); + this._messages.forEach((function(msgName) { + this._mm.addMessageListener(msgName, this); + }).bind(this)); + + this._rs = Cc["@mozilla.org/dom/dom-request-service;1"].getService(Ci.nsIDOMRequestService); + this._requests = []; + Services.obs.addObserver(this, "inner-window-destroyed", false); + let util = this._window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); + this._innerWindowID = util.currentInnerWindowID; + + let principal = aWindow.document.nodePrincipal; + let secMan = Cc["@mozilla.org/scriptsecuritymanager;1"].getService(Ci.nsIScriptSecurityManager); + + let perm = principal == secMan.getSystemPrincipal() ? + Ci.nsIPermissionManager.ALLOW_ACTION : + Services.perms.testExactPermission(principal.URI, "webcontacts-manage"); + + //only pages with perm set can use the contacts + this.hasPrivileges = perm == Ci.nsIPermissionManager.ALLOW_ACTION; + debug("has privileges :" + this.hasPrivileges); + }, + + observe: function(aSubject, aTopic, aData) { + let wId = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data; + if (wId == this.innerWindowID) { + Services.obs.removeObserver(this, "inner-window-destroyed"); + this._messages.forEach((function(msgName) { + this._mm.removeMessageListener(msgName, this); + }).bind(this)); + this._mm = null; + this._messages = null; + this._requests = null; + this._window = null; + this._innerWindowID = null; + } + }, + + classID : CONTACTMANAGER_CID, + QueryInterface : XPCOMUtils.generateQI([nsIDOMContactManager, Ci.nsIDOMGlobalPropertyInitializer]), + + classInfo : XPCOMUtils.generateCI({classID: CONTACTMANAGER_CID, + contractID: CONTACTMANAGER_CONTRACTID, + classDescription: "ContactManager", + interfaces: [nsIDOMContactManager], + flags: nsIClassInfo.DOM_OBJECT}) +} + +const NSGetFactory = XPCOMUtils.generateNSGetFactory([Contact, ContactManager, ContactProperties, ContactAddress, ContactFindOptions]) diff --git a/dom/contacts/ContactManager.manifest b/dom/contacts/ContactManager.manifest new file mode 100644 index 000000000000..01929e20c96a --- /dev/null +++ b/dom/contacts/ContactManager.manifest @@ -0,0 +1,16 @@ +component {53ed7c20-ceda-11e0-9572-0800200c9a66} ContactManager.js +contract @mozilla.org/contactProperties;1 {53ed7c20-ceda-11e0-9572-0800200c9a66} + +component {27a568b0-cee1-11e0-9572-0800200c9a66} ContactManager.js +contract @mozilla.org/contactAddress;1 {27a568b0-cee1-11e0-9572-0800200c9a66} + +component {e31daea0-0cb6-11e1-be50-0800200c9a66} ContactManager.js +contract @mozilla.org/contactFindOptions;1 {e31daea0-0cb6-11e1-be50-0800200c9a66} + +component {da0f7040-388b-11e1-b86c-0800200c9a66} ContactManager.js +contract @mozilla.org/contact;1 {da0f7040-388b-11e1-b86c-0800200c9a66} +category JavaScript-global-constructor mozContact @mozilla.org/contact;1 + +component {50a820b0-ced0-11e0-9572-0800200c9a66} ContactManager.js +contract @mozilla.org/contactManager;1 {50a820b0-ced0-11e0-9572-0800200c9a66} +category JavaScript-navigator-property mozContacts @mozilla.org/contactManager;1 diff --git a/dom/contacts/Makefile.in b/dom/contacts/Makefile.in new file mode 100644 index 000000000000..0249129fa3c5 --- /dev/null +++ b/dom/contacts/Makefile.in @@ -0,0 +1,47 @@ +# 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/. + +DEPTH = ../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = \ + $(srcdir) \ + $(NULL) + +include $(DEPTH)/config/autoconf.mk + +ifeq ($(MOZ_WIDGET_TOOLKIT),gonk) +VPATH += $(srcdir)/fallback +endif + +MODULE = dom +LIBRARY_NAME = jsdomcontacts_s +LIBXUL_LIBRARY = 1 + +EXTRA_COMPONENTS = \ + ContactManager.js \ + ContactManager.manifest \ + $(NULL) + +ifeq ($(MOZ_WIDGET_TOOLKIT),gonk) +EXTRA_JS_MODULES = ContactService.jsm \ + $(NULL) + +EXTRA_JS_MODULES += ContactDB.jsm \ + $(NULL) +endif + +ifdef ENABLE_TESTS +DIRS += tests +endif + +# Add VPATH to LOCAL_INCLUDES so we are going to include the correct backend +# subdirectory (and the ipc one). +LOCAL_INCLUDES += $(VPATH:%=-I%) + +include $(topsrcdir)/config/config.mk +include $(topsrcdir)/ipc/chromium/chromium-config.mk +include $(topsrcdir)/config/rules.mk + +DEFINES += -D_IMPL_NS_LAYOUT diff --git a/dom/contacts/fallback/ContactDB.jsm b/dom/contacts/fallback/ContactDB.jsm new file mode 100644 index 000000000000..c1a42582eaca --- /dev/null +++ b/dom/contacts/fallback/ContactDB.jsm @@ -0,0 +1,410 @@ +/* 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"; + +const EXPORTED_SYMBOLS = ['ContactDB']; + +let DEBUG = 0; +/* static functions */ +if (DEBUG) + debug = function (s) { dump("-*- ContactDB component: " + s + "\n"); } +else + debug = function (s) {} + +const Cu = Components.utils; +const Cc = Components.classes; +const Ci = Components.interfaces; + +Cu.import("resource://gre/modules/Services.jsm"); + +const DB_NAME = "contacts"; +const DB_VERSION = 1; +const STORE_NAME = "contacts"; + +function ContactDB(aGlobal) { + debug("Constructor"); + this._indexedDB = aGlobal.mozIndexedDB; +} + +ContactDB.prototype = { + + // Cache the DB + db: null, + + close: function close() { + debug("close"); + if (this.db) + this.db.close(); + }, + + /** + * Prepare the database. This may include opening the database and upgrading + * it to the latest schema version. + * + * @return (via callback) a database ready for use. + */ + ensureDB: function ensureDB(callback, failureCb) { + if (this.db) { + debug("ensureDB: already have a database, returning early."); + callback(this.db); + return; + } + + let self = this; + debug("try to open database:" + DB_NAME + " " + DB_VERSION); + let request = this._indexedDB.open(DB_NAME, DB_VERSION); + request.onsuccess = function (event) { + debug("Opened database:", DB_NAME, DB_VERSION); + self.db = event.target.result; + self.db.onversionchange = function(event) { + debug("WARNING: DB modified from a different window."); + } + callback(self.db); + }; + request.onupgradeneeded = function (event) { + debug("Database needs upgrade:" + DB_NAME + event.oldVersion + event.newVersion); + debug("Correct new database version:" + event.newVersion == DB_VERSION); + + let db = event.target.result; + switch (event.oldVersion) { + case 0: + debug("New database"); + self.createSchema(db); + break; + + default: + debug("No idea what to do with old database version:" + event.oldVersion); + failureCb(event.target.errorMessage); + break; + } + }; + request.onerror = function (event) { + debug("Failed to open database:", DB_NAME); + failureCb(event.target.errorMessage); + }; + request.onblocked = function (event) { + debug("Opening database request is blocked."); + }; + }, + + /** + * 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 + * } + */ + createSchema: function createSchema(db) { + let objectStore = db.createObjectStore(STORE_NAME, {keyPath: "id"}); + + // Metadata indexes + objectStore.createIndex("published", "published", { unique: false }); + objectStore.createIndex("updated", "updated", { unique: false }); + + // Properties indexes + objectStore.createIndex("nickname", "properties.nickname", { unique: false, multiEntry: true }); + objectStore.createIndex("name", "properties.name", { unique: false, multiEntry: true }); + objectStore.createIndex("familyName", "properties.familyName", { unique: false, multiEntry: true }); + objectStore.createIndex("givenName", "properties.givenName", { unique: false, multiEntry: true }); + objectStore.createIndex("tel", "properties.tel", { unique: false, multiEntry: true }); + objectStore.createIndex("email", "properties.email", { unique: false, multiEntry: true }); + objectStore.createIndex("note", "properties.note", { unique: false, multiEntry: true }); + + debug("Created object stores and indexes"); + }, + + /** + * Start a new transaction. + * + * @param txn_type + * Type of transaction (e.g. IDBTransaction.READ_WRITE) + * @param callback + * Function to call when the transaction is available. It will + * be invoked with the transaction and the 'contacts' object store. + * @param successCb [optional] + * Success callback to call on a successful transaction commit. + * @param failureCb [optional] + * Error callback to call when an error is encountered. + */ + newTxn: function newTxn(txn_type, callback, successCb, failureCb) { + this.ensureDB(function (db) { + debug("Starting new transaction" + txn_type); + let txn = db.transaction(STORE_NAME, txn_type); + debug("Retrieving object store", STORE_NAME); + let store = txn.objectStore(STORE_NAME); + + txn.oncomplete = function (event) { + debug("Transaction complete. Returning to callback."); + successCb(txn.result); + }; + + txn.onabort = function (event) { + debug("Caught error on transaction" + event.target.errorCode); + switch(event.target.errorCode) { + case Ci.nsIIDBDatabaseException.ABORT_ERR: + case Ci.nsIIDBDatabaseException.CONSTRAINT_ERR: + case Ci.nsIIDBDatabaseException.DATA_ERR: + case Ci.nsIIDBDatabaseException.TRANSIENT_ERR: + case Ci.nsIIDBDatabaseException.NOT_ALLOWED_ERR: + case Ci.nsIIDBDatabaseException.NOT_FOUND_ERR: + case Ci.nsIIDBDatabaseException.QUOTA_ERR: + case Ci.nsIIDBDatabaseException.READ_ONLY_ERR: + case Ci.nsIIDBDatabaseException.TIMEOUT_ERR: + case Ci.nsIIDBDatabaseException.TRANSACTION_INACTIVE_ERR: + case Ci.nsIIDBDatabaseException.VERSION_ERR: + case Ci.nsIIDBDatabaseException.UNKNOWN_ERR: + failureCb("UnknownError"); + break; + default: + debug("Unknown errorCode", event.target.errorCode); + failureCb("UnknownError"); + break; + } + }; + callback(txn, store); + }, failureCb); + }, + + // Todo: add searchfields. "Tom" should be a result with T, t, To, to... + makeImport: function makeImport(aContact) { + let contact = {}; + contact.properties = { + name: [], + honorificPrefix: [], + givenName: [], + additionalName: [], + familyName: [], + honorificSuffix: [], + nickname: [], + email: [], + photo: [], + url: [], + category: [], + adr: [], + tel: [], + org: [], + bday: null, + note: [], + impp: [], + anniversary: null, + sex: null, + genderIdentity: null + }; + + for (let field in aContact.properties) { + contact.properties[field] = aContact.properties[field]; + } + + contact.updated = aContact.updated; + contact.published = aContact.published; + contact.id = aContact.id; + + return contact; + }, + + // Needed to remove searchfields + 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(); + }, + + saveContact: function saveContact(aContact, successCb, errorCb) { + let contact = this.makeImport(aContact); + this.newTxn(Ci.nsIIDBTransaction.READ_WRITE, function (txn, store) { + debug("Going to update" + JSON.stringify(contact)); + + // 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) { + debug("new record!") + this.updateRecordMetadata(contact); + store.put(contact); + } else { + debug("old record!") + if (new Date(typeof contact.updated === "undefined" ? 0 : contact.updated) < new Date(event.target.result.updated)) { + debug("rev check fail!"); + txn.abort(); + return; + } else { + debug("rev check OK"); + contact.published = event.target.result.published; + contact.updated = new Date(); + store.put(contact); + } + } + }.bind(this); + }.bind(this), successCb, errorCb); + }, + + removeContact: function removeContact(aId, aSuccessCb, aErrorCb) { + this.newTxn(Ci.nsIIDBTransaction.READ_WRITE, function (txn, store) { + debug("Going to delete" + aId); + store.delete(aId); + }, aSuccessCb, aErrorCb); + }, + + clear: function clear(aSuccessCb, aErrorCb) { + this.newTxn(Ci.nsIIDBTransaction.READ_WRITE, function (txn, store) { + debug("Going to clear all!"); + store.clear(); + }, aSuccessCb, aErrorCb); + }, + + /** + * @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 + * Possibly supported in the future: + * - fields + * - sortBy + * - sortOrder + * - startIndex + */ + find: function find(aSuccessCb, aFailureCb, aOptions) { + debug("ContactDB:find val:" + aOptions.filterValue + " by: " + aOptions.filterBy + " op: " + aOptions.filterOp + "\n"); + + let self = this; + this.newTxn(Ci.nsIIDBTransaction.READ_ONLY, function (txn, store) { + if (aOptions && aOptions.filterOp == "equals") { + self._findWithIndex(txn, store, aOptions); + } else if (aOptions && aOptions.filterBy) { + self._findWithSearch(txn, store, aOptions); + } else { + self._findAll(txn, store, aOptions); + } + }, aSuccessCb, aFailureCb); + }, + + _findWithIndex: function _findWithIndex(txn, store, options) { + debug("_findWithIndex: " + options.filterValue +" " + options.filterOp + " " + options.filterBy + " "); + let fields = options.filterBy; + for (let key in fields) { + debug("key: " + fields[key]); + if (!store.indexNames.contains(fields[key]) && !fields[key] == "id") { + debug("Key not valid!" + fields[key] + ", " + store.indexNames); + txn.abort(); + return; + } + } + + // lookup for all keys + if (options.filterBy.length == 0) { + debug("search in all fields!" + JSON.stringify(store.indexNames)); + for(let myIndex = 0; myIndex < store.indexNames.length; myIndex++) { + fields = Array.concat(fields, store.indexNames[myIndex]) + } + } + + 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 + request = store.getAll(options.filterValue); + } else { + debug("Getting index: " + key); + let index = store.index(key); + request = index.getAll(options.filterValue, options.filterLimit); + } + if (!txn.result) + txn.result = {}; + + request.onsuccess = function (event) { + debug("Request successful. Record count:" + event.target.result.length); + for (let i in event.target.result) + txn.result[event.target.result[i].id] = this.makeExport(event.target.result[i]); + }.bind(this); + } + }, + + // Will be replaced by _findWithIndex once all searchfields are added. + _findWithSearch: function _findWithSearch(txn, store, options) { + debug("_findWithSearch:" + options.filterValue + options.filterOp) + store.getAll().onsuccess = function (event) { + debug("Request successful." + event.target.result); + txn.result = event.target.result.filter(function (record) { + let properties = record.properties; + for (let i = 0; i < options.filterBy.length; i++) { + let field = options.filterBy[i]; + if (!properties[field]) + continue; + let value = ''; + switch (field) { + case "name": + case "familyName": + case "givenName": + case "nickname": + case "email": + case "tel": + case "note": + value = [f for each (f in [properties[field]])].join("\n") || ''; + break; + default: + value = properties[field]; + debug("unknown field: " + field); + } + let match = false; + switch (options.filterOp) { + case "icontains": + match = value.toLowerCase().indexOf(options.filterValue.toLowerCase()) != -1; + break; + case "contains": + match = value.indexOf(options.filterValue) != -1; + break; + case "equals": + match = value == options.filterValue; + break + } + if (match) + return true; + } + return false; + }).map(this.makeExport.bind(this)); + }.bind(this); + }, + + _findAll: function _findAll(txn, store, options) { + debug("ContactDB:_findAll: " + JSON.stringify(options)); + if (!txn.result) + txn.result = {}; + + store.getAll(null, options.filterLimit).onsuccess = function (event) { + debug("Request successful. Record count:", event.target.result.length); + for (let i in event.target.result) + txn.result[event.target.result[i].id] = this.makeExport(event.target.result[i]); + }.bind(this); + } +}; diff --git a/dom/contacts/fallback/ContactService.jsm b/dom/contacts/fallback/ContactService.jsm new file mode 100644 index 000000000000..9b76da688b17 --- /dev/null +++ b/dom/contacts/fallback/ContactService.jsm @@ -0,0 +1,96 @@ +/* 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" + +let DEBUG = 0; +if (DEBUG) + debug = function (s) { dump("-*- Fallback ContactService component: " + s + "\n"); } +else + debug = function (s) {} + +const Cu = Components.utils; +const Cc = Components.classes; +const Ci = Components.interfaces; + +let EXPORTED_SYMBOLS = ["DOMContactManager"]; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/ContactDB.jsm"); + +let myGlobal = this; + +let DOMContactManager = { + + init: function() { + debug("Init"); + this._mm = Cc["@mozilla.org/parentprocessmessagemanager;1"].getService(Ci.nsIFrameMessageManager); + this._messages = ["Contacts:Find", "Contacts:Clear", "Contact:Save", "Contact:Remove"]; + this._messages.forEach((function(msgName) { + this._mm.addMessageListener(msgName, this); + }).bind(this)); + + var idbManager = Components.classes["@mozilla.org/dom/indexeddb/manager;1"].getService(Ci.nsIIndexedDatabaseManager); + idbManager.initWindowless(myGlobal); + this._db = new ContactDB(myGlobal); + + Services.obs.addObserver(this, "profile-before-change", false); + + try { + let hosts = Services.prefs.getCharPref("dom.mozContacts.whitelist") + hosts.split(",").forEach(function(aHost) { + debug("Add host: " + JSON.stringify(aHost)); + if (aHost.length > 0) + Services.perms.add(Services.io.newURI(aHost, null, null), "webcontacts-manage", + Ci.nsIPermissionManager.ALLOW_ACTION); + }); + } catch(e) { debug(e); } + }, + + observe: function(aSubject, aTopic, aData) { + myGlobal = null; + this._messages.forEach((function(msgName) { + this._mm.removeMessageListener(msgName, this); + }).bind(this)); + Services.obs.removeObserver(this, "profile-before-change"); + this._mm = null; + this._messages = null; + if (this._db) + this._db.close(); + }, + + receiveMessage: function(aMessage) { + debug("Fallback DOMContactManager::receiveMessage " + aMessage.name); + let msg = aMessage.json; + switch (aMessage.name) { + case "Contacts:Find": + let result = new Array(); + this._db.find( + function(contacts) { + for (let i in contacts) + result.push(contacts[i]); + debug("result:" + JSON.stringify(result)); + this._mm.sendAsyncMessage("Contacts:Find:Return:OK", {requestID: msg.requestID, contacts: result}); + }.bind(this), + function(aErrorMsg) { this._mm.sendAsyncMessage("Contacts:Find:Return:KO", { requestID: msg.requestID, errorMsg: aErrorMsg }) }.bind(this), + msg.findOptions); + break; + case "Contact:Save": + this._db.saveContact(msg.contact, function() {this._mm.sendAsyncMessage("Contact:Save:Return:OK", { requestID: msg.requestID }); }.bind(this), + function(aErrorMsg) { this._mm.sendAsyncMessage("Contact:Save:Return:KO", { requestID: msg.requestID, errorMsg: aErrorMsg }); }.bind(this)); + break; + case "Contact:Remove": + this._db.removeContact(msg.id, + function() {this._mm.sendAsyncMessage("Contact:Remove:Return:OK", { requestID: msg.requestID }); }.bind(this), + function(aErrorMsg) {this._mm.sendAsyncMessage("Contact:Remove:Return:KO", { requestID: msg.requestID, errorMsg: aErrorMsg }); }.bind(this)); + break; + case "Contacts:Clear": + this._db.clear(function() { this._mm.sendAsyncMessage("Contacts:Clear:Return:OK", { requestID: msg.requestID }); }.bind(this), + function(aErrorMsg) { this._mm.sendAsyncMessage("Contacts:Clear:Return:KO", { requestID: msg.requestID, errorMsg: aErrorMsg }); }.bind(this)); + } + } +} + +DOMContactManager.init(); diff --git a/dom/contacts/tests/Makefile.in b/dom/contacts/tests/Makefile.in new file mode 100644 index 000000000000..d599e0c80ffc --- /dev/null +++ b/dom/contacts/tests/Makefile.in @@ -0,0 +1,28 @@ +# 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/. + +DEPTH = ../../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +relativesrcdir = dom/contacts/tests + +include $(DEPTH)/config/autoconf.mk + +DIRS = \ + $(NULL) + +include $(topsrcdir)/config/rules.mk + +_TEST_FILES = \ + test_contacts_basics.html \ + $(NULL) + +_CHROME_TEST_FILES = \ + $(NULL) + +libs:: $(_TEST_FILES) + $(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/tests/$(relativesrcdir) + diff --git a/dom/contacts/tests/test_contacts_basics.html b/dom/contacts/tests/test_contacts_basics.html new file mode 100644 index 000000000000..0b59521de052 --- /dev/null +++ b/dom/contacts/tests/test_contacts_basics.html @@ -0,0 +1,666 @@ + + + + + Test for Bug {674720} WebContacts + + + + + + +Mozilla Bug {674720} +

+ +
+
+
+ + \ No newline at end of file diff --git a/dom/dom-config.mk b/dom/dom-config.mk index 69004508d428..dbc0f3390e29 100644 --- a/dom/dom-config.mk +++ b/dom/dom-config.mk @@ -4,6 +4,7 @@ DOM_SRCDIRS = \ dom/power \ dom/network/src \ dom/sms/src \ + dom/contacts \ dom/src/events \ dom/src/storage \ dom/src/offline \ diff --git a/dom/interfaces/contacts/Makefile.in b/dom/interfaces/contacts/Makefile.in new file mode 100644 index 000000000000..6d6caa7c84de --- /dev/null +++ b/dom/interfaces/contacts/Makefile.in @@ -0,0 +1,21 @@ +# 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/. + +DEPTH = ../../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +include $(DEPTH)/config/autoconf.mk + +MODULE = dom +XPIDL_MODULE = dom_contacts +GRE_MODULE = 1 + +XPIDLSRCS = \ + nsIDOMContactProperties.idl \ + nsIDOMContactManager.idl \ + $(NULL) + +include $(topsrcdir)/config/rules.mk diff --git a/dom/interfaces/contacts/nsIDOMContactManager.idl b/dom/interfaces/contacts/nsIDOMContactManager.idl new file mode 100644 index 000000000000..90c650159f2b --- /dev/null +++ b/dom/interfaces/contacts/nsIDOMContactManager.idl @@ -0,0 +1,33 @@ +/* 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/. */ + +#include "domstubs.idl" +#include "nsIDOMContactProperties.idl" + +interface nsIArray; +interface nsIDOMContactFindOptions; +interface nsIDOMContactProperties; +interface nsIDOMDOMRequest; + +[scriptable, uuid(da0f7040-388b-11e1-b86c-0800200c9a66)] +interface nsIDOMContact : nsIDOMContactProperties +{ + attribute DOMString id; + readonly attribute jsval published; + readonly attribute jsval updated; + + void init(in nsIDOMContactProperties properties); // Workaround BUG 723206 +}; + +[scriptable, uuid(50a820b0-ced0-11e0-9572-0800200c9a66)] +interface nsIDOMContactManager : nsISupports +{ + nsIDOMDOMRequest find(in nsIDOMContactFindOptions options); + + nsIDOMDOMRequest clear(); + + nsIDOMDOMRequest save(in nsIDOMContact contact); + + nsIDOMDOMRequest remove(in nsIDOMContact contact); +}; \ No newline at end of file diff --git a/dom/interfaces/contacts/nsIDOMContactProperties.idl b/dom/interfaces/contacts/nsIDOMContactProperties.idl new file mode 100644 index 000000000000..7fff1a2a851a --- /dev/null +++ b/dom/interfaces/contacts/nsIDOMContactProperties.idl @@ -0,0 +1,52 @@ +/* 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/. */ + +#include "domstubs.idl" + +interface nsIArray; +interface nsIDOMContact; + +[scriptable, uuid(27a568b0-cee1-11e0-9572-0800200c9a66)] +interface nsIDOMContactAddress : nsISupports +{ + attribute DOMString streetAddress; + attribute DOMString locality; + attribute DOMString region; + attribute DOMString postalCode; + attribute DOMString countryName; +}; + +[scriptable, uuid(e31daea0-0cb6-11e1-be50-0800200c9a66)] +interface nsIDOMContactFindOptions : nsISupports +{ + attribute DOMString filterValue; // e.g. "Tom" + attribute DOMString filterOp; // e.g. "contains" + attribute jsval filterBy; // DOMString[], e.g. ["givenName", "nickname"] + attribute unsigned long filterLimit; +}; + +[scriptable, uuid(53ed7c20-ceda-11e0-9572-0800200c9a66)] +interface nsIDOMContactProperties : nsISupports +{ + attribute jsval name; // DOMString[] + attribute jsval honorificPrefix; // DOMString[] + attribute jsval givenName; // DOMString[] + attribute jsval additionalName; // DOMString[] + attribute jsval familyName; // DOMString[] + attribute jsval honorificSuffix; // DOMString[] + attribute jsval nickname; // DOMString[] + attribute jsval email; // DOMString[] + attribute jsval photo; // DOMString[] + attribute jsval url; // DOMString[] + attribute jsval category; // DOMString[] + attribute jsval adr; // ContactAddress[] + attribute jsval tel; // DOMString[] + attribute jsval org; // DOMString[] + attribute jsval bday; // Date + attribute jsval note; // DOMString[] + attribute jsval impp; // DOMString[] + attribute jsval anniversary; // Date + attribute jsval sex; // DOMString + attribute jsval genderIdentity; // DOMString +}; \ No newline at end of file diff --git a/layout/build/Makefile.in b/layout/build/Makefile.in index 0f899c5dfac2..823955c6cb7e 100644 --- a/layout/build/Makefile.in +++ b/layout/build/Makefile.in @@ -93,6 +93,7 @@ SHARED_LIBRARY_LIBS = \ $(DEPTH)/view/src/$(LIB_PREFIX)gkview_s.$(LIB_SUFFIX) \ $(DEPTH)/dom/base/$(LIB_PREFIX)jsdombase_s.$(LIB_SUFFIX) \ $(DEPTH)/dom/battery/$(LIB_PREFIX)dom_battery_s.$(LIB_SUFFIX) \ + $(DEPTH)/dom/contacts/$(LIB_PREFIX)jsdomcontacts_s.$(LIB_SUFFIX) \ $(DEPTH)/dom/power/$(LIB_PREFIX)dom_power_s.$(LIB_SUFFIX) \ $(DEPTH)/dom/network/src/$(LIB_PREFIX)dom_network_s.$(LIB_SUFFIX) \ $(DEPTH)/dom/sms/src/$(LIB_PREFIX)dom_sms_s.$(LIB_SUFFIX) \ @@ -258,6 +259,7 @@ LOCAL_INCLUDES += -I$(srcdir)/../base \ -I$(topsrcdir)/dom/src/storage \ -I$(topsrcdir)/dom/src/offline \ -I$(topsrcdir)/dom/src/geolocation \ + -I$(topsrcdir)/dom/contacts \ -I$(topsrcdir)/dom/telephony \ -I. \ -I$(topsrcdir)/editor/libeditor/base \ diff --git a/modules/libpref/src/init/all.js b/modules/libpref/src/init/all.js index 59d82be91c49..8bdfd7ecc6dd 100644 --- a/modules/libpref/src/init/all.js +++ b/modules/libpref/src/init/all.js @@ -3439,6 +3439,10 @@ pref("dom.battery.enabled", true); pref("dom.sms.enabled", false); pref("dom.sms.whitelist", ""); +// WebContacts +pref("dom.mozContacts.enabled", false); +pref("dom.mozContacts.whitelist", ""); + // enable JS dump() function. pref("browser.dom.window.dump.enabled", false);