Bug 674720 - WebContacts (or Contacts+). r=fabrice, jonas, bent, tantek
authorGregor Wagner <anygregor@gmail.com>
Tue, 28 Feb 2012 14:01:48 -0800
changeset 91218 8ec22af2fe914f877284f50dfaf2334e014e603b
parent 91217 8fa62059bd2790859b5fbdefbc0de3739620d63d
child 91219 2742bfe821d27072840432c9b15d3ce21a6b1419
push id136
push userlsblakk@mozilla.com
push dateFri, 01 Jun 2012 02:39:32 +0000
treeherdermozilla-release@7ebf7352c959 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersfabrice, jonas, bent, tantek
bugs674720
milestone13.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 674720 - WebContacts (or Contacts+). r=fabrice, jonas, bent, tantek
b2g/app/b2g.js
b2g/chrome/content/shell.js
b2g/installer/package-manifest.in
browser/installer/package-manifest.in
dom/Makefile.in
dom/contacts/ContactManager.js
dom/contacts/ContactManager.manifest
dom/contacts/Makefile.in
dom/contacts/fallback/ContactDB.jsm
dom/contacts/fallback/ContactService.jsm
dom/contacts/tests/Makefile.in
dom/contacts/tests/test_contacts_basics.html
dom/dom-config.mk
dom/interfaces/contacts/Makefile.in
dom/interfaces/contacts/nsIDOMContactManager.idl
dom/interfaces/contacts/nsIDOMContactProperties.idl
layout/build/Makefile.in
modules/libpref/src/init/all.js
--- a/b2g/app/b2g.js
+++ b/b2g/app/b2g.js
@@ -405,16 +405,20 @@ pref("browser.link.open_newwindow.restri
 // Enable browser frame
 pref("dom.mozBrowserFramesEnabled", true);
 pref("dom.mozBrowserFramesWhitelist", "http://localhost:7777");
 
 // Temporary permission hack for WebSMS
 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);
 
 // controls if we want camera support
 pref("device.camera.enabled", true);
 pref("media.realtime_decoder.enabled", true);
 
 // "Preview" landing of bug 710563, which is bogged down in analysis
--- a/b2g/chrome/content/shell.js
+++ b/b2g/chrome/content/shell.js
@@ -11,16 +11,17 @@ const CC = Components.Constructor;
 const Cr = Components.results;
 
 const LocalFile = CC('@mozilla.org/file/local;1',
                      'nsILocalFile',
                      'initWithPath');
 
 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']
            .getService(Ci.nsIEnvironment);
 });
 
 XPCOMUtils.defineLazyGetter(Services, 'ss', function() {
   return Cc['@mozilla.org/content/style-sheet-service;1']
@@ -55,17 +56,17 @@ function startupHttpd(baseDir, port) {
 #endif
 
 // FIXME Bug 707625
 // until we have a proper security model, add some rights to
 // the pre-installed web applications
 // 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);
     let allow = Ci.nsIPermissionManager.ALLOW_ACTION;
 
     permissions.forEach(function(permission) {
       Services.perms.add(uri, permission, allow);
     });
--- a/b2g/installer/package-manifest.in
+++ b/b2g/installer/package-manifest.in
@@ -150,16 +150,17 @@
 @BINPATH@/components/dom_wifi.xpt
 @BINPATH@/components/dom_system_b2g.xpt
 #endif
 @BINPATH@/components/dom_battery.xpt
 #ifdef MOZ_B2G_BT
 @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
 @BINPATH@/components/dom_geolocation.xpt
 @BINPATH@/components/dom_network.xpt
 @BINPATH@/components/dom_notification.xpt
 @BINPATH@/components/dom_html.xpt
 @BINPATH@/components/dom_indexeddb.xpt
@@ -289,16 +290,18 @@
 @BINPATH@/components/xuldoc.xpt
 @BINPATH@/components/xultmpl.xpt
 @BINPATH@/components/zipwriter.xpt
 @BINPATH@/components/webapps.xpt
 
 ; 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
 @BINPATH@/components/FeedConverter.js
 @BINPATH@/components/FeedWriter.js
 @BINPATH@/components/fuelApplication.manifest
 @BINPATH@/components/fuelApplication.js
 @BINPATH@/components/WebContentConverter.js
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -150,16 +150,17 @@
 @BINPATH@/components/dom_wifi.xpt
 @BINPATH@/components/dom_system_b2g.xpt
 #endif
 @BINPATH@/components/dom_battery.xpt
 #ifdef MOZ_B2G_BT
 @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
 @BINPATH@/components/dom_geolocation.xpt
 @BINPATH@/components/dom_network.xpt
 @BINPATH@/components/dom_notification.xpt
 @BINPATH@/components/dom_html.xpt
 @BINPATH@/components/dom_indexeddb.xpt
@@ -404,16 +405,19 @@
 @BINPATH@/components/SyncComponents.manifest
 @BINPATH@/components/Weave.js
 #endif
 @BINPATH@/components/TelemetryPing.js
 @BINPATH@/components/TelemetryPing.manifest
 @BINPATH@/components/messageWakeupService.js
 @BINPATH@/components/messageWakeupService.manifest
 
+@BINPATH@/components/ContactManager.js
+@BINPATH@/components/ContactManager.manifest
+
 ; Modules
 @BINPATH@/modules/*
 
 ; Safe Browsing
 @BINPATH@/components/nsSafebrowsingApplication.manifest
 @BINPATH@/components/nsSafebrowsingApplication.js
 @BINPATH@/components/nsURLClassifier.manifest
 @BINPATH@/components/nsUrlClassifierHashCompleter.js
--- a/dom/Makefile.in
+++ b/dom/Makefile.in
@@ -42,16 +42,17 @@ VPATH		= @srcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 MODULE		= dom
 
 DIRS = \
   interfaces/base \
   interfaces/canvas \
+  interfaces/contacts \
   interfaces/core \
   interfaces/html \
   interfaces/events \
   interfaces/stylesheets \
   interfaces/sidebar \
   interfaces/css \
   interfaces/traversal \
   interfaces/range \
@@ -72,16 +73,17 @@ ifeq (gonk,$(MOZ_WIDGET_TOOLKIT))
 DIRS += \
   interfaces/apps \
   $(NULL)
 endif
 
 DIRS += \
   base \
   battery \
+  contacts \
   power \
   sms \
   src \
   locales \
   network \
   plugins/base \
   plugins/ipc \
   indexedDB \
new file mode 100644
--- /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])
new file mode 100644
--- /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
new file mode 100644
--- /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
new file mode 100644
--- /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);
+  }
+};
new file mode 100644
--- /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();
new file mode 100644
--- /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)
+
new file mode 100644
--- /dev/null
+++ b/dom/contacts/tests/test_contacts_basics.html
@@ -0,0 +1,666 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id={674720}
+-->
+<head>
+  <title>Test for Bug {674720} WebContacts</title>
+  <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id={674720}">Mozilla Bug {674720}</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+"use strict"
+
+netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+Components.classes["@mozilla.org/permissionmanager;1"]
+          .getService(Components.interfaces.nsIPermissionManager)
+          .add(SpecialPowers.getDocumentURIObject(window.document),
+               "webcontacts-manage",
+               Components.interfaces.nsIPermissionManager.ALLOW_ACTION);
+
+var adr1 = {
+  streetAddress: "street 1",
+  locality: "locality 1",
+  region: "region 1",
+  postalCode: "postal code 1",
+  countryName: "country 1"
+};
+
+var adr2 = {
+  streetAddress: "street2",
+  locality: "locality2",
+  region: "region2",
+  postalCode: "postal code2",
+  countryName: "country2"
+};
+
+var properties1 = {
+  name: "Testname1",
+  familyName: "TestFamilyName",
+  givenName: ["Test1","Test2"],
+  nickname: "nicktest",
+  tel: ["123456"],
+  adr: adr1
+};
+
+var properties2 = {
+  name: ["dummyName", "dummyName2"],
+  familyName: "dummyFamilyName",
+  givenName: "dummyGivenName",
+  honorificPrefix: ["dummyHonorificPrefix","dummyHonorificPrefix2"],
+  honorificSuffix: "dummyHonorificSuffix",
+  additionalName: "dummyadditionalName",
+  nickname: "dummyNickname",
+  tel: ["123456789", "234567890"],
+  email: ["a@b.c", "b@c.d"],
+  adr: [adr1, adr2],
+  impp: ["im1", "im2"],
+  org: ["org1", "org2"],
+  bday: new Date("1980, 12, 01"),
+  note: "test note",
+  photo: ["pic1", "pic2"],
+  category: ["cat1", "cat2"],
+  url: ["www.1.com", "www.2.com"],
+  anniversary: new Date("2000, 12, 01"),
+  sex: "male",
+  genderIdentity: "test"
+};
+
+var sample_id1;
+var sample_id2;
+
+var createResult1;
+var createResult2;
+
+var findResult1;
+var findResult2;
+
+function clearTemps() {
+  sample_id1 = null;
+  sample_id2 = null;
+  createResult1 = null;
+  createResult2 = null;
+  findResult1 = null;
+  findResult2 = null;
+}
+
+function onUnwantedSuccess() {
+  ok(false, "onUnwantedSuccess: shouldn't get here");
+}
+
+function onFailure() {
+  ok(false, "in on Failure!");
+}
+
+function checkStr(str1, str2, msg) {
+  if (str1)
+    ok(typeof str1 == "string" ? [str1] : str1, (typeof str2 == "string") ? [str2] : str2, msg);
+}
+
+function checkAddress(adr1, adr2) {
+  checkStr(adr1.streetAddress, adr2.streetAddress, "Same streetAddress");
+  checkStr(adr1.locality, adr2.locality, "Same locality");
+  checkStr(adr1.region, adr2.region, "Same region");
+  checkStr(adr1.postalCode, adr2.postalCode, "Same postalCode");
+  checkStr(adr1.countryName, adr2.countryName, "Same countryName");
+}
+
+function checkContacts(contact1, contact2) {
+  checkStr(contact1.name, contact2.name, "Same name");
+  checkStr(contact1.honorificPrefix, contact2.honorificPrefix, "Same honorificPrefix");
+  checkStr(contact1.givenName, contact2.givenName, "Same givenName");
+  checkStr(contact1.additionalName, contact2.additionalName, "Same additionalName");
+  checkStr(contact1.familyName, contact2.familyName, "Same familyName");
+  checkStr(contact1.honorificSuffix, contact2.honorificSuffix, "Same honorificSuffix");
+  checkStr(contact1.nickname, contact2.nickname, "Same nickname");
+  checkStr(contact1.email, contact2.email, "Same email");
+  checkStr(contact1.photo, contact2.photo, "Same photo");
+  checkStr(contact1.url, contact2.url, "Same url");
+  checkStr(contact1.category, contact2.category, "Same category");
+  is(contact1.bday ? contact1.bday.valueOf() : null, contact2.bday ? contact2.bday.valueOf() : null, "Same birthday");
+  checkStr(contact1.note, contact2.note, "Same note");
+  checkStr(contact1.impp, contact2.impp, "Same impp");
+  is(contact1.anniversary ? contact1.anniversary.valueOf() : null , contact2.anniversary ? contact2.anniversary.valueOf() : null, "Same anniversary");
+  is(contact1.sex, contact2.sex, "Same sex");
+  is(contact1.genderIdentity, contact2.genderIdentity, "Same genderIdentity");
+
+  for (var i in contact1.adr)
+    checkAddress(contact1.adr[i], contact2.adr[i]);
+}
+
+var req;
+var index = 0;
+
+var mozContacts = window.navigator.mozContacts
+
+var steps = [
+  function () {
+    ok(true, "Deleting database");
+    req = mozContacts.clear();
+    req.onsuccess = function () {
+      ok(true, "Deleted the database");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving all contacts");
+    req = mozContacts.find({});
+    req.onsuccess = function () {
+      ok(req.result.length == 0, "Empty database is empty.");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Adding empty contact");
+    createResult1 = new mozContact();
+    createResult1.init({});
+    req = navigator.mozContacts.save(createResult1);
+    req.onsuccess = function () {
+      ok(createResult1.id, "The contact now has an ID.");
+      sample_id1 = createResult1.id;
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving all contacts");
+    req = mozContacts.find({});
+    req.onsuccess = function () {
+      ok(req.result.length == 1, "One contact.");
+      findResult1 = req.result[0];
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Deleting empty contact");
+    req = navigator.mozContacts.remove(findResult1);
+    req.onsuccess = function () {
+      var req2 = mozContacts.find({});
+      req2.onsuccess = function () {
+        ok(req2.result.length == 0, "Empty Database.");
+        clearTemps();
+        next();
+      }
+      req2.onerror = onFailure;
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Adding a new contact1");
+    createResult1 = new mozContact();
+    createResult1.init(properties1);
+    req = navigator.mozContacts.save(createResult1);
+    req.onsuccess = function () {
+      ok(createResult1.id, "The contact now has an ID.");
+      sample_id1 = createResult1.id;
+      checkContacts(properties1, createResult1);
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving all contacts");
+    req = mozContacts.find({});
+    req.onsuccess = function() {
+      ok(req.result.length == 1, "Found exactly 1 contact.");
+      findResult1 = req.result[0];
+      ok(findResult1.id == sample_id1, "Same ID");
+      checkContacts(createResult1, findResult1);
+      ok(findResult1.updated, "Has updated field");
+      ok(findResult1.published, "Has published field");
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Modifying contact1");
+    findResult1.impp = properties1.impp = (["phil impp"]);
+    req = navigator.mozContacts.save(findResult1);
+    req.onsuccess = function () {
+      var req2 = mozContacts.find({});
+      req2.onsuccess = function() {
+        ok(req2.result.length == 1, "Found exactly 1 contact.");
+        findResult2 = req2.result[0];
+        ok(findResult2.id == sample_id1, "Same ID");
+        checkContacts(findResult2, properties1);
+        ok(findResult2.impp.length == 1, "Found exactly 1 IMS info.");
+        next();
+      };
+      req2.onerror = onFailure;
+    };
+    req.onerror = onFailure;
+  },
+  function() {
+    ok(true, "Saving old contact, should abort!");
+    req = mozContacts.save(createResult1);
+    req.onsuccess = onUnwantedSuccess;
+    req.onerror   = function() { ok(true, "Successfully declined updating old contact!"); next(); };
+  },
+  function() {
+    ok(true, "Saving old contact, should abort!");
+    req = mozContacts.save(findResult1)
+    req.onsuccess = onUnwantedSuccess;
+    req.onerror   = function() { ok(true, "Successfully declined updating old contact!"); next(); };
+  },
+  function () {
+    ok(true, "Retrieving a specific contact by ID");
+    var options = {filterBy: ["id"],
+                   filterOp: "equals",
+                   filterValue: sample_id1};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      ok(req.result.length == 1, "Found exactly 1 contact.");
+      findResult1 = req.result[0];
+      ok(findResult1.id == sample_id1, "Same ID");
+      checkContacts(createResult1, properties1);
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving a specific contact by givenName");
+    var options = {filterBy: ["givenName"],
+                   filterOp: "equals",
+                   filterValue: properties1.givenName[0]};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      ok(req.result.length == 1, "Found exactly 1 contact.");
+      findResult1 = req.result[0];
+      ok(findResult1.id == sample_id1, "Same ID");
+      checkContacts(findResult1, createResult1);
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Modifying contact2");
+    findResult1.impp = properties1.impp = (["phil impp"]);
+    req = mozContacts.save(findResult1);
+    req.onsuccess = function () {
+      var req2 = mozContacts.find({});
+      req2.onsuccess = function () {
+        ok(req2.result.length == 1, "Found exactly 1 contact.");
+        findResult1 = req2.result[0];
+        ok(findResult1.id == sample_id1, "Same ID");
+        checkContacts(findResult1, createResult1);
+        ok(findResult1.impp.length == 1, "Found exactly 1 IMS info.");
+        next();
+      }
+      req2.onerror = onFailure;
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Searching contacts by query");
+    var options = {filterBy: ["name", "email"],
+                   filterOp: "icontains",
+                   filterValue: properties1.name[0].substring(0,4)};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      ok(req.result.length == 1, "Found exactly 1 contact.");
+      findResult1 = req.result[0];
+      ok(findResult1.id == sample_id1, "Same ID");
+      checkContacts(findResult1, createResult1);
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Searching contacts by query");
+    var options = {filterBy: ["nickname", "email"],
+                   filterOp: "icontains",
+                   filterValue: properties1.nickname};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      ok(req.result.length == 1, "Found exactly 1 contact.");
+      findResult1 = req.result[0];
+      ok(findResult1.id == sample_id1, "Same ID");
+      checkContacts(findResult1, createResult1);
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Searching contacts with multiple indices");
+    var options = {filterBy: ["nickname", "email", "name"],
+                   filterOp: "equals",
+                   filterValue: properties1.nickname};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      ok(req.result.length == 1, "Found exactly 1 contact.");
+      findResult1 = req.result[0];
+      ok(findResult1.id == sample_id1, "Same ID");
+      checkContacts(findResult1, createResult1);
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Modifying contact3");
+    findResult1.email = (properties1.nickname);
+    findResult1.nickname = "TEST";
+    var newContact = new mozContact();
+    newContact.init(findResult1);
+    req = mozContacts.save(newContact);
+    req.onsuccess = function () {
+      var options = {filterBy: ["nickname", "email", "name"],
+                     filterOp: "equals",
+                     filterValue: properties1.nickname};
+      // One contact has it in nickname and the other in email
+      var req2 = mozContacts.find(options);
+      req2.onsuccess = function () {
+        ok(req2.result.length == 2, "Found exactly 2 contacts.");
+        ok(req2.result[0].id != req2.result[1].id, "Different ID");
+        next();
+      }
+      req2.onerror = onFailure;
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Deleting contact" + findResult1.id);
+    req = mozContacts.remove(findResult1);
+    req.onsuccess = function () {
+      var req2 = mozContacts.find({});
+      req2.onsuccess = function () {
+        ok(req2.result.length == 1, "One contact left.");
+        findResult1 = req2.result[0];
+        next();
+      }
+      req2.onerror = onFailure;
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Deleting database");
+    req = mozContacts.remove(findResult1);
+    req.onsuccess =  function () {
+      clearTemps();
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Adding a new contact");
+    createResult1 = new mozContact();
+    createResult1.init(properties1);
+    req = mozContacts.save(createResult1)
+    req.onsuccess = function () {
+      ok(createResult1.id, "The contact now has an ID.");
+      sample_id1 = createResult1.id;
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Adding a new contact2");
+    createResult2 = new mozContact();
+    createResult2.init(properties2);
+    req = mozContacts.save(createResult2);
+    req.onsuccess = function () {
+      ok(createResult2.id, "The contact now has an ID.");
+      sample_id2 = createResult2.id;
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving all contacts");
+    req = mozContacts.find({})
+    req.onsuccess = function () {
+      ok(req.result.length == 2, "Found exactly 2 contact.");
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    console.log("Searching contacts by query1");
+    var options = {filterBy: ["name", "email"],
+                   filterOp: "icontains",
+                   filterValue: properties1.name[0].substring(0, 4)}
+    req = mozContacts.find(options)
+    req.onsuccess = function () {
+      ok(req.result.length == 1, "Found exactly 1 contact.");
+      findResult1 = req.result[0];
+      ok(findResult1.id == sample_id1, "Same ID");
+      checkContacts(findResult1, createResult1);
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Searching contacts by query2");
+    var options = {filterBy: ["name", "email"],
+                   filterOp: "icontains",
+                   filterValue: properties2.name[0].substring(0, 4)};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      ok(req.result.length == 1, "Found exactly 1 contact.");
+      findResult1 = req.result[0];
+      ok(findResult1.adr.length == 2, "Adr length 2");
+      checkContacts(findResult1, createResult2);
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Searching contacts by tel");
+    var options = {filterBy: ["tel"],
+                   filterOp: "contains",
+                   filterValue: properties2.tel[0].substring(0, 7)};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      ok(req.result.length == 1, "Found exactly 1 contact.");
+      findResult1 = req.result[0];
+      ok(findResult1.id == sample_id2, "Same ID");
+      checkContacts(findResult1, createResult2);
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Searching contacts by email");
+    var options = {filterBy: ["email"],
+                   filterOp: "contains",
+                   filterValue: properties2.email[0].substring(0, 4)};
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      ok(req.result.length == 1, "Found exactly 1 contact.");
+      findResult1 = req.result[0];
+      ok(findResult1.id == sample_id2, "Same ID");
+      checkContacts(findResult1, createResult2);
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Deleting database");
+    req = mozContacts.clear();
+    req.onsuccess = function () {
+      clearTemps();
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Adding 100 contacts");
+    for (var i=0; i<99; i++) {
+      createResult1 = new mozContact();
+      createResult1.init(properties1);
+      req = mozContacts.save(createResult1);
+      req.onsuccess = function () {
+        ok(createResult1.id, "The contact now has an ID.");
+      };
+      req.onerror = onFailure;
+    };
+    createResult1 = new mozContact();
+    createResult1.init(properties1);
+    req = mozContacts.save(createResult1);
+    req.onsuccess = function () {
+      ok(createResult1.id, "The contact now has an ID.");
+      ok(createResult1.name == properties1.name, "Same Name");
+      next();
+    };
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving all contacts");
+    req = mozContacts.find({});
+    req.onsuccess = function () {
+      ok(req.result.length == 100, "100 Entries.");
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving all contacts with limit 10");
+    var options = { filterLimit: 10 };
+    req = mozContacts.find(options);
+    req.onsuccess = function () {
+      ok(req.result.length == 10, "10 Entries.");
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving all contacts2");
+    var options = {filterBy: ["name"],
+                   filterOp: "icontains",
+                   filterValue: properties2.name[0].substring(0, 4)};
+    req = mozContacts.find({});
+    req.onsuccess = function () {
+      ok(req.result.length == 100, "100 Entries.");
+      checkContacts(createResult1, req.result[99]);
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Deleting database");
+    req = mozContacts.clear();
+    req.onsuccess = function () {
+      clearTemps();
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Testing clone contact");
+    createResult1 = new mozContact();
+    createResult1.init(properties1);
+    req = mozContacts.save(createResult1);
+    req.onsuccess = function () {
+      ok(createResult1.id, "The contact now has an ID.");
+      ok(createResult1.name == properties1.name, "Same Name");
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function() {
+    ok(true, "Testing clone contact2");
+    var cloned = new mozContact(createResult1);
+    ok(cloned.id != createResult1.id, "Cloned contact has new ID");
+    cloned.email = "new email!";
+    cloned.givenName = "Tom";
+    req = mozContacts.save(cloned);
+    req.onsuccess = function () {
+      ok(cloned.id, "The contact now has an ID.");
+      ok(cloned.email == "new email!", "Same Email");
+      ok(createResult1.email != cloned.email, "Clone has different email");
+      ok(cloned.givenName == "Tom", "New Name");
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Retrieving all contacts");
+    var options = {filterBy: ["name"],
+                   filterOp: "icontains",
+                   filterValue: properties2.name[0].substring(0, 4)};
+    req = mozContacts.find({});
+    req.onsuccess = function () {
+      ok(req.result.length == 2, "2 Entries.");
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Search with redundant fields should only return 1 contact");
+    createResult1 = new mozContact();
+    createResult1.init({name: "XXX", nickname: "XXX", email: "XXX", tel: "XXX"});
+    req = mozContacts.save(createResult1);
+    req.onsuccess = function() {
+      var options = {filterBy: [],
+                     filterOp: "equals",
+                     filterValue: "XXX"};
+      var req2 = mozContacts.find(options);
+      req2.onsuccess = function() {
+        ok(req2.result.length == 1, "1 Entry");
+        next();
+      }
+      req2.onerror = onFailure;
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "Deleting database");
+    req = mozContacts.clear()
+    req.onsuccess = function () {
+      ok(true, "Deleted the database");
+      next();
+    }
+    req.onerror = onFailure;
+  },
+  function () {
+    ok(true, "all done!\n");
+    clearTemps();
+
+    SimpleTest.finish();
+  }
+];
+
+function next() {
+  ok(true, "Begin!");
+  if (index >= steps.length) {
+    ok(false, "Shouldn't get here!");
+    return;
+  }
+  try {
+    steps[index]();
+  } catch(ex) {
+    ok(false, "Caught exception", ex);
+  }
+  index += 1;
+}
+
+function permissionTest() {
+  if (gContactsEnabled) {
+    next();
+  } else {
+    is(mozContacts, null, "mozContacts is null when not enabled.");
+    SimpleTest.finish();
+  }
+}
+
+var gContactsEnabled = SpecialPowers.getBoolPref("dom.mozContacts.enabled");
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(permissionTest);
+
+ok(true, "test passed");
+</script>
+</pre>
+</body>
+</html>
\ No newline at end of file
--- a/dom/dom-config.mk
+++ b/dom/dom-config.mk
@@ -1,14 +1,15 @@
 DOM_SRCDIRS = \
   dom/base \
   dom/battery \
   dom/power \
   dom/network/src \
   dom/sms/src \
+  dom/contacts \
   dom/src/events \
   dom/src/storage \
   dom/src/offline \
   dom/src/geolocation \
   dom/src/notification \
   dom/workers \
   content/xbl/src \
   content/xul/document/src \
new file mode 100644
--- /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
new file mode 100644
--- /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
new file mode 100644
--- /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
--- a/layout/build/Makefile.in
+++ b/layout/build/Makefile.in
@@ -88,16 +88,17 @@ SHARED_LIBRARY_LIBS = \
 	$(DEPTH)/content/xslt/src/xml/$(LIB_PREFIX)txxml_s.$(LIB_SUFFIX) \
 	$(DEPTH)/content/xslt/src/xpath/$(LIB_PREFIX)txxpath_s.$(LIB_SUFFIX) \
 	$(DEPTH)/content/xslt/src/xslt/$(LIB_PREFIX)txxslt_s.$(LIB_SUFFIX) \
 	$(DEPTH)/content/xbl/src/$(LIB_PREFIX)gkconxbl_s.$(LIB_SUFFIX) \
 	$(DEPTH)/content/xul/document/src/$(LIB_PREFIX)gkconxuldoc_s.$(LIB_SUFFIX) \
 	$(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) \
 	$(DEPTH)/dom/src/events/$(LIB_PREFIX)jsdomevents_s.$(LIB_SUFFIX) \
 	$(DEPTH)/dom/src/json/$(LIB_PREFIX)json_s.$(LIB_SUFFIX) \
 	$(DEPTH)/dom/src/jsurl/$(LIB_PREFIX)jsurl_s.$(LIB_SUFFIX) \
 	$(DEPTH)/dom/src/storage/$(LIB_PREFIX)jsdomstorage_s.$(LIB_SUFFIX) \
 	$(DEPTH)/dom/src/offline/$(LIB_PREFIX)jsdomoffline_s.$(LIB_SUFFIX) \
@@ -253,16 +254,17 @@ LOCAL_INCLUDES	+= -I$(srcdir)/../base \
 		   -I$(topsrcdir)/content/xbl/src \
 		   -I$(topsrcdir)/view/src \
 		   -I$(topsrcdir)/dom/base \
 		   -I$(topsrcdir)/dom/src/json \
 		   -I$(topsrcdir)/dom/src/jsurl \
 		   -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 \
 		   -I$(topsrcdir)/editor/libeditor/text \
 		   -I$(topsrcdir)/editor/libeditor/html \
 		   -I$(topsrcdir)/editor/txtsvc/src \
 		   -I$(topsrcdir)/editor/composer/src \
 		   -I$(topsrcdir)/js/xpconnect/src \
--- a/modules/libpref/src/init/all.js
+++ b/modules/libpref/src/init/all.js
@@ -3434,16 +3434,20 @@ pref("dom.vibrator.max_vibrate_list_len"
 
 // Battery API
 pref("dom.battery.enabled", true);
 
 // WebSMS
 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);
 
 // SPS Profiler
 pref("profiler.enabled", false);
 pref("profiler.interval", 10);
 pref("profiler.entries", 100000);