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