1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/dom/contacts/ContactManager.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,490 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +const DEBUG = false; 1.11 +function debug(s) { dump("-*- ContactManager: " + s + "\n"); } 1.12 + 1.13 +const Cc = Components.classes; 1.14 +const Ci = Components.interfaces; 1.15 +const Cu = Components.utils; 1.16 + 1.17 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.18 +Cu.import("resource://gre/modules/Services.jsm"); 1.19 +Cu.import("resource://gre/modules/DOMRequestHelper.jsm"); 1.20 + 1.21 +XPCOMUtils.defineLazyServiceGetter(Services, "DOMRequest", 1.22 + "@mozilla.org/dom/dom-request-service;1", 1.23 + "nsIDOMRequestService"); 1.24 + 1.25 +XPCOMUtils.defineLazyServiceGetter(this, "cpmm", 1.26 + "@mozilla.org/childprocessmessagemanager;1", 1.27 + "nsIMessageSender"); 1.28 + 1.29 +const CONTACTS_SENDMORE_MINIMUM = 5; 1.30 + 1.31 +// We need this to create a copy of the mozContact object in ContactManager.save 1.32 +// Keep in sync with the interfaces. 1.33 +const PROPERTIES = [ 1.34 + "name", "honorificPrefix", "givenName", "additionalName", "familyName", 1.35 + "phoneticGivenName", "phoneticFamilyName", 1.36 + "honorificSuffix", "nickname", "photo", "category", "org", "jobTitle", 1.37 + "bday", "note", "anniversary", "sex", "genderIdentity", "key", "adr", "email", 1.38 + "url", "impp", "tel" 1.39 +]; 1.40 + 1.41 +let mozContactInitWarned = false; 1.42 + 1.43 +function Contact() { } 1.44 + 1.45 +Contact.prototype = { 1.46 + __init: function(aProp) { 1.47 + for (let prop in aProp) { 1.48 + this[prop] = aProp[prop]; 1.49 + } 1.50 + }, 1.51 + 1.52 + init: function(aProp) { 1.53 + // init is deprecated, warn once in the console if it's used 1.54 + if (!mozContactInitWarned) { 1.55 + mozContactInitWarned = true; 1.56 + Cu.reportError("mozContact.init is DEPRECATED. Use the mozContact constructor instead. " + 1.57 + "See https://developer.mozilla.org/docs/WebAPI/Contacts for details."); 1.58 + } 1.59 + 1.60 + for (let prop of PROPERTIES) { 1.61 + this[prop] = aProp[prop]; 1.62 + } 1.63 + }, 1.64 + 1.65 + setMetadata: function(aId, aPublished, aUpdated) { 1.66 + this.id = aId; 1.67 + if (aPublished) { 1.68 + this.published = aPublished; 1.69 + } 1.70 + if (aUpdated) { 1.71 + this.updated = aUpdated; 1.72 + } 1.73 + }, 1.74 + 1.75 + classID: Components.ID("{72a5ee28-81d8-4af8-90b3-ae935396cc66}"), 1.76 + contractID: "@mozilla.org/contact;1", 1.77 + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]), 1.78 +}; 1.79 + 1.80 +function ContactManager() { } 1.81 + 1.82 +ContactManager.prototype = { 1.83 + __proto__: DOMRequestIpcHelper.prototype, 1.84 + hasListenPermission: false, 1.85 + _cachedContacts: [] , 1.86 + 1.87 + set oncontactchange(aHandler) { 1.88 + this.__DOM_IMPL__.setEventHandler("oncontactchange", aHandler); 1.89 + }, 1.90 + 1.91 + get oncontactchange() { 1.92 + return this.__DOM_IMPL__.getEventHandler("oncontactchange"); 1.93 + }, 1.94 + 1.95 + _convertContact: function(aContact) { 1.96 + let newContact = new this._window.mozContact(aContact.properties); 1.97 + newContact.setMetadata(aContact.id, aContact.published, aContact.updated); 1.98 + return newContact; 1.99 + }, 1.100 + 1.101 + _convertContacts: function(aContacts) { 1.102 + let contacts = new this._window.Array(); 1.103 + for (let i in aContacts) { 1.104 + contacts.push(this._convertContact(aContacts[i])); 1.105 + } 1.106 + return contacts; 1.107 + }, 1.108 + 1.109 + _fireSuccessOrDone: function(aCursor, aResult) { 1.110 + if (aResult == null) { 1.111 + Services.DOMRequest.fireDone(aCursor); 1.112 + } else { 1.113 + Services.DOMRequest.fireSuccess(aCursor, aResult); 1.114 + } 1.115 + }, 1.116 + 1.117 + _pushArray: function(aArr1, aArr2) { 1.118 + aArr1.push.apply(aArr1, aArr2); 1.119 + }, 1.120 + 1.121 + receiveMessage: function(aMessage) { 1.122 + if (DEBUG) debug("receiveMessage: " + aMessage.name); 1.123 + let msg = aMessage.json; 1.124 + let contacts = msg.contacts; 1.125 + 1.126 + let req; 1.127 + switch (aMessage.name) { 1.128 + case "Contacts:Find:Return:OK": 1.129 + req = this.getRequest(msg.requestID); 1.130 + if (req) { 1.131 + let result = this._convertContacts(contacts); 1.132 + Services.DOMRequest.fireSuccess(req.request, result); 1.133 + } else { 1.134 + if (DEBUG) debug("no request stored!" + msg.requestID); 1.135 + } 1.136 + break; 1.137 + case "Contacts:GetAll:Next": 1.138 + let data = this.getRequest(msg.cursorId); 1.139 + if (!data) { 1.140 + break; 1.141 + } 1.142 + let result = contacts ? this._convertContacts(contacts) : [null]; 1.143 + if (data.waitingForNext) { 1.144 + if (DEBUG) debug("cursor waiting for contact, sending"); 1.145 + data.waitingForNext = false; 1.146 + let contact = result.shift(); 1.147 + this._pushArray(data.cachedContacts, result); 1.148 + this.nextTick(this._fireSuccessOrDone.bind(this, data.cursor, contact)); 1.149 + if (!contact) { 1.150 + this.removeRequest(msg.cursorId); 1.151 + } 1.152 + } else { 1.153 + if (DEBUG) debug("cursor not waiting, saving"); 1.154 + this._pushArray(data.cachedContacts, result); 1.155 + } 1.156 + break; 1.157 + case "Contact:Save:Return:OK": 1.158 + // If a cached contact was saved and a new contact ID was returned, update the contact's ID 1.159 + if (this._cachedContacts[msg.requestID]) { 1.160 + if (msg.contactID) { 1.161 + this._cachedContacts[msg.requestID].id = msg.contactID; 1.162 + } 1.163 + delete this._cachedContacts[msg.requestID]; 1.164 + } 1.165 + case "Contacts:Clear:Return:OK": 1.166 + case "Contact:Remove:Return:OK": 1.167 + req = this.getRequest(msg.requestID); 1.168 + if (req) 1.169 + Services.DOMRequest.fireSuccess(req.request, null); 1.170 + break; 1.171 + case "Contacts:Find:Return:KO": 1.172 + case "Contact:Save:Return:KO": 1.173 + case "Contact:Remove:Return:KO": 1.174 + case "Contacts:Clear:Return:KO": 1.175 + case "Contacts:GetRevision:Return:KO": 1.176 + case "Contacts:Count:Return:KO": 1.177 + req = this.getRequest(msg.requestID); 1.178 + if (req) { 1.179 + if (req.request) { 1.180 + req = req.request; 1.181 + } 1.182 + Services.DOMRequest.fireError(req, msg.errorMsg); 1.183 + } 1.184 + break; 1.185 + case "Contacts:GetAll:Return:KO": 1.186 + req = this.getRequest(msg.requestID); 1.187 + if (req) { 1.188 + Services.DOMRequest.fireError(req.cursor, msg.errorMsg); 1.189 + } 1.190 + break; 1.191 + case "PermissionPromptHelper:AskPermission:OK": 1.192 + if (DEBUG) debug("id: " + msg.requestID); 1.193 + req = this.getRequest(msg.requestID); 1.194 + if (!req) { 1.195 + break; 1.196 + } 1.197 + 1.198 + if (msg.result == Ci.nsIPermissionManager.ALLOW_ACTION) { 1.199 + req.allow(); 1.200 + } else { 1.201 + req.cancel(); 1.202 + } 1.203 + break; 1.204 + case "Contact:Changed": 1.205 + // Fire oncontactchange event 1.206 + if (DEBUG) debug("Contacts:ContactChanged: " + msg.contactID + ", " + msg.reason); 1.207 + let event = new this._window.MozContactChangeEvent("contactchange", { 1.208 + contactID: msg.contactID, 1.209 + reason: msg.reason 1.210 + }); 1.211 + this.dispatchEvent(event); 1.212 + break; 1.213 + case "Contacts:Revision": 1.214 + if (DEBUG) debug("new revision: " + msg.revision); 1.215 + req = this.getRequest(msg.requestID); 1.216 + if (req) { 1.217 + Services.DOMRequest.fireSuccess(req.request, msg.revision); 1.218 + } 1.219 + break; 1.220 + case "Contacts:Count": 1.221 + if (DEBUG) debug("count: " + msg.count); 1.222 + req = this.getRequest(msg.requestID); 1.223 + if (req) { 1.224 + Services.DOMRequest.fireSuccess(req.request, msg.count); 1.225 + } 1.226 + break; 1.227 + default: 1.228 + if (DEBUG) debug("Wrong message: " + aMessage.name); 1.229 + } 1.230 + this.removeRequest(msg.requestID); 1.231 + }, 1.232 + 1.233 + dispatchEvent: function(event) { 1.234 + if (this.hasListenPermission) { 1.235 + this.__DOM_IMPL__.dispatchEvent(event); 1.236 + } 1.237 + }, 1.238 + 1.239 + askPermission: function (aAccess, aRequest, aAllowCallback, aCancelCallback) { 1.240 + if (DEBUG) debug("askPermission for contacts"); 1.241 + let access; 1.242 + switch(aAccess) { 1.243 + case "create": 1.244 + access = "create"; 1.245 + break; 1.246 + case "update": 1.247 + case "remove": 1.248 + access = "write"; 1.249 + break; 1.250 + case "find": 1.251 + case "listen": 1.252 + case "revision": 1.253 + case "count": 1.254 + access = "read"; 1.255 + break; 1.256 + default: 1.257 + access = "unknown"; 1.258 + } 1.259 + 1.260 + // Shortcut for ALLOW_ACTION so we avoid a parent roundtrip 1.261 + let type = "contacts-" + access; 1.262 + let permValue = 1.263 + Services.perms.testExactPermissionFromPrincipal(this._window.document.nodePrincipal, type); 1.264 + if (permValue == Ci.nsIPermissionManager.ALLOW_ACTION) { 1.265 + aAllowCallback(); 1.266 + return; 1.267 + } 1.268 + 1.269 + let requestID = this.getRequestId({ 1.270 + request: aRequest, 1.271 + allow: function() { 1.272 + aAllowCallback(); 1.273 + }.bind(this), 1.274 + cancel : function() { 1.275 + if (aCancelCallback) { 1.276 + aCancelCallback() 1.277 + } else if (aRequest) { 1.278 + Services.DOMRequest.fireError(aRequest, "Not Allowed"); 1.279 + } 1.280 + }.bind(this) 1.281 + }); 1.282 + 1.283 + let principal = this._window.document.nodePrincipal; 1.284 + cpmm.sendAsyncMessage("PermissionPromptHelper:AskPermission", { 1.285 + type: "contacts", 1.286 + access: access, 1.287 + requestID: requestID, 1.288 + origin: principal.origin, 1.289 + appID: principal.appId, 1.290 + browserFlag: principal.isInBrowserElement, 1.291 + windowID: this._window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).outerWindowID 1.292 + }); 1.293 + }, 1.294 + 1.295 + save: function save(aContact) { 1.296 + // We have to do a deep copy of the contact manually here because 1.297 + // nsFrameMessageManager doesn't know how to create a structured clone of a 1.298 + // mozContact object. 1.299 + let newContact = {properties: {}}; 1.300 + 1.301 + try { 1.302 + for (let field of PROPERTIES) { 1.303 + // This hack makes sure modifications to the sequence attributes get validated. 1.304 + aContact[field] = aContact[field]; 1.305 + newContact.properties[field] = aContact[field]; 1.306 + } 1.307 + } catch (e) { 1.308 + // And then make sure we throw a proper error message (no internal file and line #) 1.309 + throw new this._window.DOMError(e.name, e.message); 1.310 + } 1.311 + 1.312 + let request = this.createRequest(); 1.313 + let requestID = this.getRequestId({request: request}); 1.314 + 1.315 + let reason; 1.316 + if (aContact.id == "undefined") { 1.317 + // for example {25c00f01-90e5-c545-b4d4-21E2ddbab9e0} becomes 1.318 + // 25c00f0190e5c545b4d421E2ddbab9e0 1.319 + aContact.id = this._getRandomId().replace(/[{}-]/g, ""); 1.320 + // Cache the contact so that its ID may be updated later if necessary 1.321 + this._cachedContacts[requestID] = aContact; 1.322 + reason = "create"; 1.323 + } else { 1.324 + reason = "update"; 1.325 + } 1.326 + 1.327 + newContact.id = aContact.id; 1.328 + newContact.published = aContact.published; 1.329 + newContact.updated = aContact.updated; 1.330 + 1.331 + if (DEBUG) debug("send: " + JSON.stringify(newContact)); 1.332 + 1.333 + let options = { contact: newContact, reason: reason }; 1.334 + let allowCallback = function() { 1.335 + cpmm.sendAsyncMessage("Contact:Save", {requestID: requestID, options: options}); 1.336 + }.bind(this) 1.337 + this.askPermission(reason, request, allowCallback); 1.338 + return request; 1.339 + }, 1.340 + 1.341 + find: function(aOptions) { 1.342 + if (DEBUG) debug("find! " + JSON.stringify(aOptions)); 1.343 + let request = this.createRequest(); 1.344 + let options = { findOptions: aOptions }; 1.345 + let allowCallback = function() { 1.346 + cpmm.sendAsyncMessage("Contacts:Find", {requestID: this.getRequestId({request: request, reason: "find"}), options: options}); 1.347 + }.bind(this) 1.348 + this.askPermission("find", request, allowCallback); 1.349 + return request; 1.350 + }, 1.351 + 1.352 + createCursor: function CM_createCursor(aRequest) { 1.353 + let data = { 1.354 + cursor: Services.DOMRequest.createCursor(this._window, function() { 1.355 + this.handleContinue(id); 1.356 + }.bind(this)), 1.357 + cachedContacts: [], 1.358 + waitingForNext: true, 1.359 + }; 1.360 + let id = this.getRequestId(data); 1.361 + if (DEBUG) debug("saved cursor id: " + id); 1.362 + return [id, data.cursor]; 1.363 + }, 1.364 + 1.365 + getAll: function CM_getAll(aOptions) { 1.366 + if (DEBUG) debug("getAll: " + JSON.stringify(aOptions)); 1.367 + let [cursorId, cursor] = this.createCursor(); 1.368 + let allowCallback = function() { 1.369 + cpmm.sendAsyncMessage("Contacts:GetAll", { 1.370 + cursorId: cursorId, findOptions: aOptions}); 1.371 + }.bind(this); 1.372 + this.askPermission("find", cursor, allowCallback); 1.373 + return cursor; 1.374 + }, 1.375 + 1.376 + nextTick: function nextTick(aCallback) { 1.377 + Services.tm.currentThread.dispatch(aCallback, Ci.nsIThread.DISPATCH_NORMAL); 1.378 + }, 1.379 + 1.380 + handleContinue: function CM_handleContinue(aCursorId) { 1.381 + if (DEBUG) debug("handleContinue: " + aCursorId); 1.382 + let data = this.getRequest(aCursorId); 1.383 + if (data.cachedContacts.length > 0) { 1.384 + if (DEBUG) debug("contact in cache"); 1.385 + let contact = data.cachedContacts.shift(); 1.386 + this.nextTick(this._fireSuccessOrDone.bind(this, data.cursor, contact)); 1.387 + if (!contact) { 1.388 + this.removeRequest(aCursorId); 1.389 + } else if (data.cachedContacts.length === CONTACTS_SENDMORE_MINIMUM) { 1.390 + cpmm.sendAsyncMessage("Contacts:GetAll:SendNow", { cursorId: aCursorId }); 1.391 + } 1.392 + } else { 1.393 + if (DEBUG) debug("waiting for contact"); 1.394 + data.waitingForNext = true; 1.395 + } 1.396 + }, 1.397 + 1.398 + remove: function removeContact(aRecordOrId) { 1.399 + let request = this.createRequest(); 1.400 + let id; 1.401 + if (typeof aRecordOrId === "string") { 1.402 + id = aRecordOrId; 1.403 + } else if (!aRecordOrId || !aRecordOrId.id) { 1.404 + Services.DOMRequest.fireErrorAsync(request, true); 1.405 + return request; 1.406 + } else { 1.407 + id = aRecordOrId.id; 1.408 + } 1.409 + 1.410 + let options = { id: id }; 1.411 + let allowCallback = function() { 1.412 + cpmm.sendAsyncMessage("Contact:Remove", {requestID: this.getRequestId({request: request, reason: "remove"}), options: options}); 1.413 + }.bind(this); 1.414 + this.askPermission("remove", request, allowCallback); 1.415 + return request; 1.416 + }, 1.417 + 1.418 + clear: function() { 1.419 + if (DEBUG) debug("clear"); 1.420 + let request = this.createRequest(); 1.421 + let options = {}; 1.422 + let allowCallback = function() { 1.423 + cpmm.sendAsyncMessage("Contacts:Clear", {requestID: this.getRequestId({request: request, reason: "remove"}), options: options}); 1.424 + }.bind(this); 1.425 + this.askPermission("remove", request, allowCallback); 1.426 + return request; 1.427 + }, 1.428 + 1.429 + getRevision: function() { 1.430 + let request = this.createRequest(); 1.431 + 1.432 + let allowCallback = function() { 1.433 + cpmm.sendAsyncMessage("Contacts:GetRevision", { 1.434 + requestID: this.getRequestId({ request: request }) 1.435 + }); 1.436 + }.bind(this); 1.437 + 1.438 + let cancelCallback = function() { 1.439 + Services.DOMRequest.fireError(request); 1.440 + }; 1.441 + 1.442 + this.askPermission("revision", request, allowCallback, cancelCallback); 1.443 + return request; 1.444 + }, 1.445 + 1.446 + getCount: function() { 1.447 + let request = this.createRequest(); 1.448 + 1.449 + let allowCallback = function() { 1.450 + cpmm.sendAsyncMessage("Contacts:GetCount", { 1.451 + requestID: this.getRequestId({ request: request }) 1.452 + }); 1.453 + }.bind(this); 1.454 + 1.455 + let cancelCallback = function() { 1.456 + Services.DOMRequest.fireError(request); 1.457 + }; 1.458 + 1.459 + this.askPermission("count", request, allowCallback, cancelCallback); 1.460 + return request; 1.461 + }, 1.462 + 1.463 + init: function(aWindow) { 1.464 + // DOMRequestIpcHelper.initHelper sets this._window 1.465 + this.initDOMRequestHelper(aWindow, ["Contacts:Find:Return:OK", "Contacts:Find:Return:KO", 1.466 + "Contacts:Clear:Return:OK", "Contacts:Clear:Return:KO", 1.467 + "Contact:Save:Return:OK", "Contact:Save:Return:KO", 1.468 + "Contact:Remove:Return:OK", "Contact:Remove:Return:KO", 1.469 + "Contact:Changed", 1.470 + "PermissionPromptHelper:AskPermission:OK", 1.471 + "Contacts:GetAll:Next", "Contacts:GetAll:Return:KO", 1.472 + "Contacts:Count", 1.473 + "Contacts:Revision", "Contacts:GetRevision:Return:KO",]); 1.474 + 1.475 + 1.476 + let allowCallback = function() { 1.477 + cpmm.sendAsyncMessage("Contacts:RegisterForMessages"); 1.478 + this.hasListenPermission = true; 1.479 + }.bind(this); 1.480 + 1.481 + this.askPermission("listen", null, allowCallback); 1.482 + }, 1.483 + 1.484 + classID: Components.ID("{8beb3a66-d70a-4111-b216-b8e995ad3aff}"), 1.485 + contractID: "@mozilla.org/contactManager;1", 1.486 + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupportsWeakReference, 1.487 + Ci.nsIObserver, 1.488 + Ci.nsIDOMGlobalPropertyInitializer]), 1.489 +}; 1.490 + 1.491 +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ 1.492 + Contact, ContactManager 1.493 +]);