michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: const DEBUG = false; michael@0: function debug(s) { dump("-*- ContactManager: " + s + "\n"); } michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cu = Components.utils; michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/DOMRequestHelper.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(Services, "DOMRequest", michael@0: "@mozilla.org/dom/dom-request-service;1", michael@0: "nsIDOMRequestService"); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "cpmm", michael@0: "@mozilla.org/childprocessmessagemanager;1", michael@0: "nsIMessageSender"); michael@0: michael@0: const CONTACTS_SENDMORE_MINIMUM = 5; michael@0: michael@0: // We need this to create a copy of the mozContact object in ContactManager.save michael@0: // Keep in sync with the interfaces. michael@0: const PROPERTIES = [ michael@0: "name", "honorificPrefix", "givenName", "additionalName", "familyName", michael@0: "phoneticGivenName", "phoneticFamilyName", michael@0: "honorificSuffix", "nickname", "photo", "category", "org", "jobTitle", michael@0: "bday", "note", "anniversary", "sex", "genderIdentity", "key", "adr", "email", michael@0: "url", "impp", "tel" michael@0: ]; michael@0: michael@0: let mozContactInitWarned = false; michael@0: michael@0: function Contact() { } michael@0: michael@0: Contact.prototype = { michael@0: __init: function(aProp) { michael@0: for (let prop in aProp) { michael@0: this[prop] = aProp[prop]; michael@0: } michael@0: }, michael@0: michael@0: init: function(aProp) { michael@0: // init is deprecated, warn once in the console if it's used michael@0: if (!mozContactInitWarned) { michael@0: mozContactInitWarned = true; michael@0: Cu.reportError("mozContact.init is DEPRECATED. Use the mozContact constructor instead. " + michael@0: "See https://developer.mozilla.org/docs/WebAPI/Contacts for details."); michael@0: } michael@0: michael@0: for (let prop of PROPERTIES) { michael@0: this[prop] = aProp[prop]; michael@0: } michael@0: }, michael@0: michael@0: setMetadata: function(aId, aPublished, aUpdated) { michael@0: this.id = aId; michael@0: if (aPublished) { michael@0: this.published = aPublished; michael@0: } michael@0: if (aUpdated) { michael@0: this.updated = aUpdated; michael@0: } michael@0: }, michael@0: michael@0: classID: Components.ID("{72a5ee28-81d8-4af8-90b3-ae935396cc66}"), michael@0: contractID: "@mozilla.org/contact;1", michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]), michael@0: }; michael@0: michael@0: function ContactManager() { } michael@0: michael@0: ContactManager.prototype = { michael@0: __proto__: DOMRequestIpcHelper.prototype, michael@0: hasListenPermission: false, michael@0: _cachedContacts: [] , michael@0: michael@0: set oncontactchange(aHandler) { michael@0: this.__DOM_IMPL__.setEventHandler("oncontactchange", aHandler); michael@0: }, michael@0: michael@0: get oncontactchange() { michael@0: return this.__DOM_IMPL__.getEventHandler("oncontactchange"); michael@0: }, michael@0: michael@0: _convertContact: function(aContact) { michael@0: let newContact = new this._window.mozContact(aContact.properties); michael@0: newContact.setMetadata(aContact.id, aContact.published, aContact.updated); michael@0: return newContact; michael@0: }, michael@0: michael@0: _convertContacts: function(aContacts) { michael@0: let contacts = new this._window.Array(); michael@0: for (let i in aContacts) { michael@0: contacts.push(this._convertContact(aContacts[i])); michael@0: } michael@0: return contacts; michael@0: }, michael@0: michael@0: _fireSuccessOrDone: function(aCursor, aResult) { michael@0: if (aResult == null) { michael@0: Services.DOMRequest.fireDone(aCursor); michael@0: } else { michael@0: Services.DOMRequest.fireSuccess(aCursor, aResult); michael@0: } michael@0: }, michael@0: michael@0: _pushArray: function(aArr1, aArr2) { michael@0: aArr1.push.apply(aArr1, aArr2); michael@0: }, michael@0: michael@0: receiveMessage: function(aMessage) { michael@0: if (DEBUG) debug("receiveMessage: " + aMessage.name); michael@0: let msg = aMessage.json; michael@0: let contacts = msg.contacts; michael@0: michael@0: let req; michael@0: switch (aMessage.name) { michael@0: case "Contacts:Find:Return:OK": michael@0: req = this.getRequest(msg.requestID); michael@0: if (req) { michael@0: let result = this._convertContacts(contacts); michael@0: Services.DOMRequest.fireSuccess(req.request, result); michael@0: } else { michael@0: if (DEBUG) debug("no request stored!" + msg.requestID); michael@0: } michael@0: break; michael@0: case "Contacts:GetAll:Next": michael@0: let data = this.getRequest(msg.cursorId); michael@0: if (!data) { michael@0: break; michael@0: } michael@0: let result = contacts ? this._convertContacts(contacts) : [null]; michael@0: if (data.waitingForNext) { michael@0: if (DEBUG) debug("cursor waiting for contact, sending"); michael@0: data.waitingForNext = false; michael@0: let contact = result.shift(); michael@0: this._pushArray(data.cachedContacts, result); michael@0: this.nextTick(this._fireSuccessOrDone.bind(this, data.cursor, contact)); michael@0: if (!contact) { michael@0: this.removeRequest(msg.cursorId); michael@0: } michael@0: } else { michael@0: if (DEBUG) debug("cursor not waiting, saving"); michael@0: this._pushArray(data.cachedContacts, result); michael@0: } michael@0: break; michael@0: case "Contact:Save:Return:OK": michael@0: // If a cached contact was saved and a new contact ID was returned, update the contact's ID michael@0: if (this._cachedContacts[msg.requestID]) { michael@0: if (msg.contactID) { michael@0: this._cachedContacts[msg.requestID].id = msg.contactID; michael@0: } michael@0: delete this._cachedContacts[msg.requestID]; michael@0: } michael@0: case "Contacts:Clear:Return:OK": michael@0: case "Contact:Remove:Return:OK": michael@0: req = this.getRequest(msg.requestID); michael@0: if (req) michael@0: Services.DOMRequest.fireSuccess(req.request, null); michael@0: break; michael@0: case "Contacts:Find:Return:KO": michael@0: case "Contact:Save:Return:KO": michael@0: case "Contact:Remove:Return:KO": michael@0: case "Contacts:Clear:Return:KO": michael@0: case "Contacts:GetRevision:Return:KO": michael@0: case "Contacts:Count:Return:KO": michael@0: req = this.getRequest(msg.requestID); michael@0: if (req) { michael@0: if (req.request) { michael@0: req = req.request; michael@0: } michael@0: Services.DOMRequest.fireError(req, msg.errorMsg); michael@0: } michael@0: break; michael@0: case "Contacts:GetAll:Return:KO": michael@0: req = this.getRequest(msg.requestID); michael@0: if (req) { michael@0: Services.DOMRequest.fireError(req.cursor, msg.errorMsg); michael@0: } michael@0: break; michael@0: case "PermissionPromptHelper:AskPermission:OK": michael@0: if (DEBUG) debug("id: " + msg.requestID); michael@0: req = this.getRequest(msg.requestID); michael@0: if (!req) { michael@0: break; michael@0: } michael@0: michael@0: if (msg.result == Ci.nsIPermissionManager.ALLOW_ACTION) { michael@0: req.allow(); michael@0: } else { michael@0: req.cancel(); michael@0: } michael@0: break; michael@0: case "Contact:Changed": michael@0: // Fire oncontactchange event michael@0: if (DEBUG) debug("Contacts:ContactChanged: " + msg.contactID + ", " + msg.reason); michael@0: let event = new this._window.MozContactChangeEvent("contactchange", { michael@0: contactID: msg.contactID, michael@0: reason: msg.reason michael@0: }); michael@0: this.dispatchEvent(event); michael@0: break; michael@0: case "Contacts:Revision": michael@0: if (DEBUG) debug("new revision: " + msg.revision); michael@0: req = this.getRequest(msg.requestID); michael@0: if (req) { michael@0: Services.DOMRequest.fireSuccess(req.request, msg.revision); michael@0: } michael@0: break; michael@0: case "Contacts:Count": michael@0: if (DEBUG) debug("count: " + msg.count); michael@0: req = this.getRequest(msg.requestID); michael@0: if (req) { michael@0: Services.DOMRequest.fireSuccess(req.request, msg.count); michael@0: } michael@0: break; michael@0: default: michael@0: if (DEBUG) debug("Wrong message: " + aMessage.name); michael@0: } michael@0: this.removeRequest(msg.requestID); michael@0: }, michael@0: michael@0: dispatchEvent: function(event) { michael@0: if (this.hasListenPermission) { michael@0: this.__DOM_IMPL__.dispatchEvent(event); michael@0: } michael@0: }, michael@0: michael@0: askPermission: function (aAccess, aRequest, aAllowCallback, aCancelCallback) { michael@0: if (DEBUG) debug("askPermission for contacts"); michael@0: let access; michael@0: switch(aAccess) { michael@0: case "create": michael@0: access = "create"; michael@0: break; michael@0: case "update": michael@0: case "remove": michael@0: access = "write"; michael@0: break; michael@0: case "find": michael@0: case "listen": michael@0: case "revision": michael@0: case "count": michael@0: access = "read"; michael@0: break; michael@0: default: michael@0: access = "unknown"; michael@0: } michael@0: michael@0: // Shortcut for ALLOW_ACTION so we avoid a parent roundtrip michael@0: let type = "contacts-" + access; michael@0: let permValue = michael@0: Services.perms.testExactPermissionFromPrincipal(this._window.document.nodePrincipal, type); michael@0: if (permValue == Ci.nsIPermissionManager.ALLOW_ACTION) { michael@0: aAllowCallback(); michael@0: return; michael@0: } michael@0: michael@0: let requestID = this.getRequestId({ michael@0: request: aRequest, michael@0: allow: function() { michael@0: aAllowCallback(); michael@0: }.bind(this), michael@0: cancel : function() { michael@0: if (aCancelCallback) { michael@0: aCancelCallback() michael@0: } else if (aRequest) { michael@0: Services.DOMRequest.fireError(aRequest, "Not Allowed"); michael@0: } michael@0: }.bind(this) michael@0: }); michael@0: michael@0: let principal = this._window.document.nodePrincipal; michael@0: cpmm.sendAsyncMessage("PermissionPromptHelper:AskPermission", { michael@0: type: "contacts", michael@0: access: access, michael@0: requestID: requestID, michael@0: origin: principal.origin, michael@0: appID: principal.appId, michael@0: browserFlag: principal.isInBrowserElement, michael@0: windowID: this._window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).outerWindowID michael@0: }); michael@0: }, michael@0: michael@0: save: function save(aContact) { michael@0: // We have to do a deep copy of the contact manually here because michael@0: // nsFrameMessageManager doesn't know how to create a structured clone of a michael@0: // mozContact object. michael@0: let newContact = {properties: {}}; michael@0: michael@0: try { michael@0: for (let field of PROPERTIES) { michael@0: // This hack makes sure modifications to the sequence attributes get validated. michael@0: aContact[field] = aContact[field]; michael@0: newContact.properties[field] = aContact[field]; michael@0: } michael@0: } catch (e) { michael@0: // And then make sure we throw a proper error message (no internal file and line #) michael@0: throw new this._window.DOMError(e.name, e.message); michael@0: } michael@0: michael@0: let request = this.createRequest(); michael@0: let requestID = this.getRequestId({request: request}); michael@0: michael@0: let reason; michael@0: if (aContact.id == "undefined") { michael@0: // for example {25c00f01-90e5-c545-b4d4-21E2ddbab9e0} becomes michael@0: // 25c00f0190e5c545b4d421E2ddbab9e0 michael@0: aContact.id = this._getRandomId().replace(/[{}-]/g, ""); michael@0: // Cache the contact so that its ID may be updated later if necessary michael@0: this._cachedContacts[requestID] = aContact; michael@0: reason = "create"; michael@0: } else { michael@0: reason = "update"; michael@0: } michael@0: michael@0: newContact.id = aContact.id; michael@0: newContact.published = aContact.published; michael@0: newContact.updated = aContact.updated; michael@0: michael@0: if (DEBUG) debug("send: " + JSON.stringify(newContact)); michael@0: michael@0: let options = { contact: newContact, reason: reason }; michael@0: let allowCallback = function() { michael@0: cpmm.sendAsyncMessage("Contact:Save", {requestID: requestID, options: options}); michael@0: }.bind(this) michael@0: this.askPermission(reason, request, allowCallback); michael@0: return request; michael@0: }, michael@0: michael@0: find: function(aOptions) { michael@0: if (DEBUG) debug("find! " + JSON.stringify(aOptions)); michael@0: let request = this.createRequest(); michael@0: let options = { findOptions: aOptions }; michael@0: let allowCallback = function() { michael@0: cpmm.sendAsyncMessage("Contacts:Find", {requestID: this.getRequestId({request: request, reason: "find"}), options: options}); michael@0: }.bind(this) michael@0: this.askPermission("find", request, allowCallback); michael@0: return request; michael@0: }, michael@0: michael@0: createCursor: function CM_createCursor(aRequest) { michael@0: let data = { michael@0: cursor: Services.DOMRequest.createCursor(this._window, function() { michael@0: this.handleContinue(id); michael@0: }.bind(this)), michael@0: cachedContacts: [], michael@0: waitingForNext: true, michael@0: }; michael@0: let id = this.getRequestId(data); michael@0: if (DEBUG) debug("saved cursor id: " + id); michael@0: return [id, data.cursor]; michael@0: }, michael@0: michael@0: getAll: function CM_getAll(aOptions) { michael@0: if (DEBUG) debug("getAll: " + JSON.stringify(aOptions)); michael@0: let [cursorId, cursor] = this.createCursor(); michael@0: let allowCallback = function() { michael@0: cpmm.sendAsyncMessage("Contacts:GetAll", { michael@0: cursorId: cursorId, findOptions: aOptions}); michael@0: }.bind(this); michael@0: this.askPermission("find", cursor, allowCallback); michael@0: return cursor; michael@0: }, michael@0: michael@0: nextTick: function nextTick(aCallback) { michael@0: Services.tm.currentThread.dispatch(aCallback, Ci.nsIThread.DISPATCH_NORMAL); michael@0: }, michael@0: michael@0: handleContinue: function CM_handleContinue(aCursorId) { michael@0: if (DEBUG) debug("handleContinue: " + aCursorId); michael@0: let data = this.getRequest(aCursorId); michael@0: if (data.cachedContacts.length > 0) { michael@0: if (DEBUG) debug("contact in cache"); michael@0: let contact = data.cachedContacts.shift(); michael@0: this.nextTick(this._fireSuccessOrDone.bind(this, data.cursor, contact)); michael@0: if (!contact) { michael@0: this.removeRequest(aCursorId); michael@0: } else if (data.cachedContacts.length === CONTACTS_SENDMORE_MINIMUM) { michael@0: cpmm.sendAsyncMessage("Contacts:GetAll:SendNow", { cursorId: aCursorId }); michael@0: } michael@0: } else { michael@0: if (DEBUG) debug("waiting for contact"); michael@0: data.waitingForNext = true; michael@0: } michael@0: }, michael@0: michael@0: remove: function removeContact(aRecordOrId) { michael@0: let request = this.createRequest(); michael@0: let id; michael@0: if (typeof aRecordOrId === "string") { michael@0: id = aRecordOrId; michael@0: } else if (!aRecordOrId || !aRecordOrId.id) { michael@0: Services.DOMRequest.fireErrorAsync(request, true); michael@0: return request; michael@0: } else { michael@0: id = aRecordOrId.id; michael@0: } michael@0: michael@0: let options = { id: id }; michael@0: let allowCallback = function() { michael@0: cpmm.sendAsyncMessage("Contact:Remove", {requestID: this.getRequestId({request: request, reason: "remove"}), options: options}); michael@0: }.bind(this); michael@0: this.askPermission("remove", request, allowCallback); michael@0: return request; michael@0: }, michael@0: michael@0: clear: function() { michael@0: if (DEBUG) debug("clear"); michael@0: let request = this.createRequest(); michael@0: let options = {}; michael@0: let allowCallback = function() { michael@0: cpmm.sendAsyncMessage("Contacts:Clear", {requestID: this.getRequestId({request: request, reason: "remove"}), options: options}); michael@0: }.bind(this); michael@0: this.askPermission("remove", request, allowCallback); michael@0: return request; michael@0: }, michael@0: michael@0: getRevision: function() { michael@0: let request = this.createRequest(); michael@0: michael@0: let allowCallback = function() { michael@0: cpmm.sendAsyncMessage("Contacts:GetRevision", { michael@0: requestID: this.getRequestId({ request: request }) michael@0: }); michael@0: }.bind(this); michael@0: michael@0: let cancelCallback = function() { michael@0: Services.DOMRequest.fireError(request); michael@0: }; michael@0: michael@0: this.askPermission("revision", request, allowCallback, cancelCallback); michael@0: return request; michael@0: }, michael@0: michael@0: getCount: function() { michael@0: let request = this.createRequest(); michael@0: michael@0: let allowCallback = function() { michael@0: cpmm.sendAsyncMessage("Contacts:GetCount", { michael@0: requestID: this.getRequestId({ request: request }) michael@0: }); michael@0: }.bind(this); michael@0: michael@0: let cancelCallback = function() { michael@0: Services.DOMRequest.fireError(request); michael@0: }; michael@0: michael@0: this.askPermission("count", request, allowCallback, cancelCallback); michael@0: return request; michael@0: }, michael@0: michael@0: init: function(aWindow) { michael@0: // DOMRequestIpcHelper.initHelper sets this._window michael@0: this.initDOMRequestHelper(aWindow, ["Contacts:Find:Return:OK", "Contacts:Find:Return:KO", michael@0: "Contacts:Clear:Return:OK", "Contacts:Clear:Return:KO", michael@0: "Contact:Save:Return:OK", "Contact:Save:Return:KO", michael@0: "Contact:Remove:Return:OK", "Contact:Remove:Return:KO", michael@0: "Contact:Changed", michael@0: "PermissionPromptHelper:AskPermission:OK", michael@0: "Contacts:GetAll:Next", "Contacts:GetAll:Return:KO", michael@0: "Contacts:Count", michael@0: "Contacts:Revision", "Contacts:GetRevision:Return:KO",]); michael@0: michael@0: michael@0: let allowCallback = function() { michael@0: cpmm.sendAsyncMessage("Contacts:RegisterForMessages"); michael@0: this.hasListenPermission = true; michael@0: }.bind(this); michael@0: michael@0: this.askPermission("listen", null, allowCallback); michael@0: }, michael@0: michael@0: classID: Components.ID("{8beb3a66-d70a-4111-b216-b8e995ad3aff}"), michael@0: contractID: "@mozilla.org/contactManager;1", michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsISupportsWeakReference, michael@0: Ci.nsIObserver, michael@0: Ci.nsIDOMGlobalPropertyInitializer]), michael@0: }; michael@0: michael@0: this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ michael@0: Contact, ContactManager michael@0: ]);