dom/contacts/ContactManager.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
michael@0 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 "use strict";
michael@0 6
michael@0 7 const DEBUG = false;
michael@0 8 function debug(s) { dump("-*- ContactManager: " + s + "\n"); }
michael@0 9
michael@0 10 const Cc = Components.classes;
michael@0 11 const Ci = Components.interfaces;
michael@0 12 const Cu = Components.utils;
michael@0 13
michael@0 14 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 15 Cu.import("resource://gre/modules/Services.jsm");
michael@0 16 Cu.import("resource://gre/modules/DOMRequestHelper.jsm");
michael@0 17
michael@0 18 XPCOMUtils.defineLazyServiceGetter(Services, "DOMRequest",
michael@0 19 "@mozilla.org/dom/dom-request-service;1",
michael@0 20 "nsIDOMRequestService");
michael@0 21
michael@0 22 XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
michael@0 23 "@mozilla.org/childprocessmessagemanager;1",
michael@0 24 "nsIMessageSender");
michael@0 25
michael@0 26 const CONTACTS_SENDMORE_MINIMUM = 5;
michael@0 27
michael@0 28 // We need this to create a copy of the mozContact object in ContactManager.save
michael@0 29 // Keep in sync with the interfaces.
michael@0 30 const PROPERTIES = [
michael@0 31 "name", "honorificPrefix", "givenName", "additionalName", "familyName",
michael@0 32 "phoneticGivenName", "phoneticFamilyName",
michael@0 33 "honorificSuffix", "nickname", "photo", "category", "org", "jobTitle",
michael@0 34 "bday", "note", "anniversary", "sex", "genderIdentity", "key", "adr", "email",
michael@0 35 "url", "impp", "tel"
michael@0 36 ];
michael@0 37
michael@0 38 let mozContactInitWarned = false;
michael@0 39
michael@0 40 function Contact() { }
michael@0 41
michael@0 42 Contact.prototype = {
michael@0 43 __init: function(aProp) {
michael@0 44 for (let prop in aProp) {
michael@0 45 this[prop] = aProp[prop];
michael@0 46 }
michael@0 47 },
michael@0 48
michael@0 49 init: function(aProp) {
michael@0 50 // init is deprecated, warn once in the console if it's used
michael@0 51 if (!mozContactInitWarned) {
michael@0 52 mozContactInitWarned = true;
michael@0 53 Cu.reportError("mozContact.init is DEPRECATED. Use the mozContact constructor instead. " +
michael@0 54 "See https://developer.mozilla.org/docs/WebAPI/Contacts for details.");
michael@0 55 }
michael@0 56
michael@0 57 for (let prop of PROPERTIES) {
michael@0 58 this[prop] = aProp[prop];
michael@0 59 }
michael@0 60 },
michael@0 61
michael@0 62 setMetadata: function(aId, aPublished, aUpdated) {
michael@0 63 this.id = aId;
michael@0 64 if (aPublished) {
michael@0 65 this.published = aPublished;
michael@0 66 }
michael@0 67 if (aUpdated) {
michael@0 68 this.updated = aUpdated;
michael@0 69 }
michael@0 70 },
michael@0 71
michael@0 72 classID: Components.ID("{72a5ee28-81d8-4af8-90b3-ae935396cc66}"),
michael@0 73 contractID: "@mozilla.org/contact;1",
michael@0 74 QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]),
michael@0 75 };
michael@0 76
michael@0 77 function ContactManager() { }
michael@0 78
michael@0 79 ContactManager.prototype = {
michael@0 80 __proto__: DOMRequestIpcHelper.prototype,
michael@0 81 hasListenPermission: false,
michael@0 82 _cachedContacts: [] ,
michael@0 83
michael@0 84 set oncontactchange(aHandler) {
michael@0 85 this.__DOM_IMPL__.setEventHandler("oncontactchange", aHandler);
michael@0 86 },
michael@0 87
michael@0 88 get oncontactchange() {
michael@0 89 return this.__DOM_IMPL__.getEventHandler("oncontactchange");
michael@0 90 },
michael@0 91
michael@0 92 _convertContact: function(aContact) {
michael@0 93 let newContact = new this._window.mozContact(aContact.properties);
michael@0 94 newContact.setMetadata(aContact.id, aContact.published, aContact.updated);
michael@0 95 return newContact;
michael@0 96 },
michael@0 97
michael@0 98 _convertContacts: function(aContacts) {
michael@0 99 let contacts = new this._window.Array();
michael@0 100 for (let i in aContacts) {
michael@0 101 contacts.push(this._convertContact(aContacts[i]));
michael@0 102 }
michael@0 103 return contacts;
michael@0 104 },
michael@0 105
michael@0 106 _fireSuccessOrDone: function(aCursor, aResult) {
michael@0 107 if (aResult == null) {
michael@0 108 Services.DOMRequest.fireDone(aCursor);
michael@0 109 } else {
michael@0 110 Services.DOMRequest.fireSuccess(aCursor, aResult);
michael@0 111 }
michael@0 112 },
michael@0 113
michael@0 114 _pushArray: function(aArr1, aArr2) {
michael@0 115 aArr1.push.apply(aArr1, aArr2);
michael@0 116 },
michael@0 117
michael@0 118 receiveMessage: function(aMessage) {
michael@0 119 if (DEBUG) debug("receiveMessage: " + aMessage.name);
michael@0 120 let msg = aMessage.json;
michael@0 121 let contacts = msg.contacts;
michael@0 122
michael@0 123 let req;
michael@0 124 switch (aMessage.name) {
michael@0 125 case "Contacts:Find:Return:OK":
michael@0 126 req = this.getRequest(msg.requestID);
michael@0 127 if (req) {
michael@0 128 let result = this._convertContacts(contacts);
michael@0 129 Services.DOMRequest.fireSuccess(req.request, result);
michael@0 130 } else {
michael@0 131 if (DEBUG) debug("no request stored!" + msg.requestID);
michael@0 132 }
michael@0 133 break;
michael@0 134 case "Contacts:GetAll:Next":
michael@0 135 let data = this.getRequest(msg.cursorId);
michael@0 136 if (!data) {
michael@0 137 break;
michael@0 138 }
michael@0 139 let result = contacts ? this._convertContacts(contacts) : [null];
michael@0 140 if (data.waitingForNext) {
michael@0 141 if (DEBUG) debug("cursor waiting for contact, sending");
michael@0 142 data.waitingForNext = false;
michael@0 143 let contact = result.shift();
michael@0 144 this._pushArray(data.cachedContacts, result);
michael@0 145 this.nextTick(this._fireSuccessOrDone.bind(this, data.cursor, contact));
michael@0 146 if (!contact) {
michael@0 147 this.removeRequest(msg.cursorId);
michael@0 148 }
michael@0 149 } else {
michael@0 150 if (DEBUG) debug("cursor not waiting, saving");
michael@0 151 this._pushArray(data.cachedContacts, result);
michael@0 152 }
michael@0 153 break;
michael@0 154 case "Contact:Save:Return:OK":
michael@0 155 // If a cached contact was saved and a new contact ID was returned, update the contact's ID
michael@0 156 if (this._cachedContacts[msg.requestID]) {
michael@0 157 if (msg.contactID) {
michael@0 158 this._cachedContacts[msg.requestID].id = msg.contactID;
michael@0 159 }
michael@0 160 delete this._cachedContacts[msg.requestID];
michael@0 161 }
michael@0 162 case "Contacts:Clear:Return:OK":
michael@0 163 case "Contact:Remove:Return:OK":
michael@0 164 req = this.getRequest(msg.requestID);
michael@0 165 if (req)
michael@0 166 Services.DOMRequest.fireSuccess(req.request, null);
michael@0 167 break;
michael@0 168 case "Contacts:Find:Return:KO":
michael@0 169 case "Contact:Save:Return:KO":
michael@0 170 case "Contact:Remove:Return:KO":
michael@0 171 case "Contacts:Clear:Return:KO":
michael@0 172 case "Contacts:GetRevision:Return:KO":
michael@0 173 case "Contacts:Count:Return:KO":
michael@0 174 req = this.getRequest(msg.requestID);
michael@0 175 if (req) {
michael@0 176 if (req.request) {
michael@0 177 req = req.request;
michael@0 178 }
michael@0 179 Services.DOMRequest.fireError(req, msg.errorMsg);
michael@0 180 }
michael@0 181 break;
michael@0 182 case "Contacts:GetAll:Return:KO":
michael@0 183 req = this.getRequest(msg.requestID);
michael@0 184 if (req) {
michael@0 185 Services.DOMRequest.fireError(req.cursor, msg.errorMsg);
michael@0 186 }
michael@0 187 break;
michael@0 188 case "PermissionPromptHelper:AskPermission:OK":
michael@0 189 if (DEBUG) debug("id: " + msg.requestID);
michael@0 190 req = this.getRequest(msg.requestID);
michael@0 191 if (!req) {
michael@0 192 break;
michael@0 193 }
michael@0 194
michael@0 195 if (msg.result == Ci.nsIPermissionManager.ALLOW_ACTION) {
michael@0 196 req.allow();
michael@0 197 } else {
michael@0 198 req.cancel();
michael@0 199 }
michael@0 200 break;
michael@0 201 case "Contact:Changed":
michael@0 202 // Fire oncontactchange event
michael@0 203 if (DEBUG) debug("Contacts:ContactChanged: " + msg.contactID + ", " + msg.reason);
michael@0 204 let event = new this._window.MozContactChangeEvent("contactchange", {
michael@0 205 contactID: msg.contactID,
michael@0 206 reason: msg.reason
michael@0 207 });
michael@0 208 this.dispatchEvent(event);
michael@0 209 break;
michael@0 210 case "Contacts:Revision":
michael@0 211 if (DEBUG) debug("new revision: " + msg.revision);
michael@0 212 req = this.getRequest(msg.requestID);
michael@0 213 if (req) {
michael@0 214 Services.DOMRequest.fireSuccess(req.request, msg.revision);
michael@0 215 }
michael@0 216 break;
michael@0 217 case "Contacts:Count":
michael@0 218 if (DEBUG) debug("count: " + msg.count);
michael@0 219 req = this.getRequest(msg.requestID);
michael@0 220 if (req) {
michael@0 221 Services.DOMRequest.fireSuccess(req.request, msg.count);
michael@0 222 }
michael@0 223 break;
michael@0 224 default:
michael@0 225 if (DEBUG) debug("Wrong message: " + aMessage.name);
michael@0 226 }
michael@0 227 this.removeRequest(msg.requestID);
michael@0 228 },
michael@0 229
michael@0 230 dispatchEvent: function(event) {
michael@0 231 if (this.hasListenPermission) {
michael@0 232 this.__DOM_IMPL__.dispatchEvent(event);
michael@0 233 }
michael@0 234 },
michael@0 235
michael@0 236 askPermission: function (aAccess, aRequest, aAllowCallback, aCancelCallback) {
michael@0 237 if (DEBUG) debug("askPermission for contacts");
michael@0 238 let access;
michael@0 239 switch(aAccess) {
michael@0 240 case "create":
michael@0 241 access = "create";
michael@0 242 break;
michael@0 243 case "update":
michael@0 244 case "remove":
michael@0 245 access = "write";
michael@0 246 break;
michael@0 247 case "find":
michael@0 248 case "listen":
michael@0 249 case "revision":
michael@0 250 case "count":
michael@0 251 access = "read";
michael@0 252 break;
michael@0 253 default:
michael@0 254 access = "unknown";
michael@0 255 }
michael@0 256
michael@0 257 // Shortcut for ALLOW_ACTION so we avoid a parent roundtrip
michael@0 258 let type = "contacts-" + access;
michael@0 259 let permValue =
michael@0 260 Services.perms.testExactPermissionFromPrincipal(this._window.document.nodePrincipal, type);
michael@0 261 if (permValue == Ci.nsIPermissionManager.ALLOW_ACTION) {
michael@0 262 aAllowCallback();
michael@0 263 return;
michael@0 264 }
michael@0 265
michael@0 266 let requestID = this.getRequestId({
michael@0 267 request: aRequest,
michael@0 268 allow: function() {
michael@0 269 aAllowCallback();
michael@0 270 }.bind(this),
michael@0 271 cancel : function() {
michael@0 272 if (aCancelCallback) {
michael@0 273 aCancelCallback()
michael@0 274 } else if (aRequest) {
michael@0 275 Services.DOMRequest.fireError(aRequest, "Not Allowed");
michael@0 276 }
michael@0 277 }.bind(this)
michael@0 278 });
michael@0 279
michael@0 280 let principal = this._window.document.nodePrincipal;
michael@0 281 cpmm.sendAsyncMessage("PermissionPromptHelper:AskPermission", {
michael@0 282 type: "contacts",
michael@0 283 access: access,
michael@0 284 requestID: requestID,
michael@0 285 origin: principal.origin,
michael@0 286 appID: principal.appId,
michael@0 287 browserFlag: principal.isInBrowserElement,
michael@0 288 windowID: this._window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).outerWindowID
michael@0 289 });
michael@0 290 },
michael@0 291
michael@0 292 save: function save(aContact) {
michael@0 293 // We have to do a deep copy of the contact manually here because
michael@0 294 // nsFrameMessageManager doesn't know how to create a structured clone of a
michael@0 295 // mozContact object.
michael@0 296 let newContact = {properties: {}};
michael@0 297
michael@0 298 try {
michael@0 299 for (let field of PROPERTIES) {
michael@0 300 // This hack makes sure modifications to the sequence attributes get validated.
michael@0 301 aContact[field] = aContact[field];
michael@0 302 newContact.properties[field] = aContact[field];
michael@0 303 }
michael@0 304 } catch (e) {
michael@0 305 // And then make sure we throw a proper error message (no internal file and line #)
michael@0 306 throw new this._window.DOMError(e.name, e.message);
michael@0 307 }
michael@0 308
michael@0 309 let request = this.createRequest();
michael@0 310 let requestID = this.getRequestId({request: request});
michael@0 311
michael@0 312 let reason;
michael@0 313 if (aContact.id == "undefined") {
michael@0 314 // for example {25c00f01-90e5-c545-b4d4-21E2ddbab9e0} becomes
michael@0 315 // 25c00f0190e5c545b4d421E2ddbab9e0
michael@0 316 aContact.id = this._getRandomId().replace(/[{}-]/g, "");
michael@0 317 // Cache the contact so that its ID may be updated later if necessary
michael@0 318 this._cachedContacts[requestID] = aContact;
michael@0 319 reason = "create";
michael@0 320 } else {
michael@0 321 reason = "update";
michael@0 322 }
michael@0 323
michael@0 324 newContact.id = aContact.id;
michael@0 325 newContact.published = aContact.published;
michael@0 326 newContact.updated = aContact.updated;
michael@0 327
michael@0 328 if (DEBUG) debug("send: " + JSON.stringify(newContact));
michael@0 329
michael@0 330 let options = { contact: newContact, reason: reason };
michael@0 331 let allowCallback = function() {
michael@0 332 cpmm.sendAsyncMessage("Contact:Save", {requestID: requestID, options: options});
michael@0 333 }.bind(this)
michael@0 334 this.askPermission(reason, request, allowCallback);
michael@0 335 return request;
michael@0 336 },
michael@0 337
michael@0 338 find: function(aOptions) {
michael@0 339 if (DEBUG) debug("find! " + JSON.stringify(aOptions));
michael@0 340 let request = this.createRequest();
michael@0 341 let options = { findOptions: aOptions };
michael@0 342 let allowCallback = function() {
michael@0 343 cpmm.sendAsyncMessage("Contacts:Find", {requestID: this.getRequestId({request: request, reason: "find"}), options: options});
michael@0 344 }.bind(this)
michael@0 345 this.askPermission("find", request, allowCallback);
michael@0 346 return request;
michael@0 347 },
michael@0 348
michael@0 349 createCursor: function CM_createCursor(aRequest) {
michael@0 350 let data = {
michael@0 351 cursor: Services.DOMRequest.createCursor(this._window, function() {
michael@0 352 this.handleContinue(id);
michael@0 353 }.bind(this)),
michael@0 354 cachedContacts: [],
michael@0 355 waitingForNext: true,
michael@0 356 };
michael@0 357 let id = this.getRequestId(data);
michael@0 358 if (DEBUG) debug("saved cursor id: " + id);
michael@0 359 return [id, data.cursor];
michael@0 360 },
michael@0 361
michael@0 362 getAll: function CM_getAll(aOptions) {
michael@0 363 if (DEBUG) debug("getAll: " + JSON.stringify(aOptions));
michael@0 364 let [cursorId, cursor] = this.createCursor();
michael@0 365 let allowCallback = function() {
michael@0 366 cpmm.sendAsyncMessage("Contacts:GetAll", {
michael@0 367 cursorId: cursorId, findOptions: aOptions});
michael@0 368 }.bind(this);
michael@0 369 this.askPermission("find", cursor, allowCallback);
michael@0 370 return cursor;
michael@0 371 },
michael@0 372
michael@0 373 nextTick: function nextTick(aCallback) {
michael@0 374 Services.tm.currentThread.dispatch(aCallback, Ci.nsIThread.DISPATCH_NORMAL);
michael@0 375 },
michael@0 376
michael@0 377 handleContinue: function CM_handleContinue(aCursorId) {
michael@0 378 if (DEBUG) debug("handleContinue: " + aCursorId);
michael@0 379 let data = this.getRequest(aCursorId);
michael@0 380 if (data.cachedContacts.length > 0) {
michael@0 381 if (DEBUG) debug("contact in cache");
michael@0 382 let contact = data.cachedContacts.shift();
michael@0 383 this.nextTick(this._fireSuccessOrDone.bind(this, data.cursor, contact));
michael@0 384 if (!contact) {
michael@0 385 this.removeRequest(aCursorId);
michael@0 386 } else if (data.cachedContacts.length === CONTACTS_SENDMORE_MINIMUM) {
michael@0 387 cpmm.sendAsyncMessage("Contacts:GetAll:SendNow", { cursorId: aCursorId });
michael@0 388 }
michael@0 389 } else {
michael@0 390 if (DEBUG) debug("waiting for contact");
michael@0 391 data.waitingForNext = true;
michael@0 392 }
michael@0 393 },
michael@0 394
michael@0 395 remove: function removeContact(aRecordOrId) {
michael@0 396 let request = this.createRequest();
michael@0 397 let id;
michael@0 398 if (typeof aRecordOrId === "string") {
michael@0 399 id = aRecordOrId;
michael@0 400 } else if (!aRecordOrId || !aRecordOrId.id) {
michael@0 401 Services.DOMRequest.fireErrorAsync(request, true);
michael@0 402 return request;
michael@0 403 } else {
michael@0 404 id = aRecordOrId.id;
michael@0 405 }
michael@0 406
michael@0 407 let options = { id: id };
michael@0 408 let allowCallback = function() {
michael@0 409 cpmm.sendAsyncMessage("Contact:Remove", {requestID: this.getRequestId({request: request, reason: "remove"}), options: options});
michael@0 410 }.bind(this);
michael@0 411 this.askPermission("remove", request, allowCallback);
michael@0 412 return request;
michael@0 413 },
michael@0 414
michael@0 415 clear: function() {
michael@0 416 if (DEBUG) debug("clear");
michael@0 417 let request = this.createRequest();
michael@0 418 let options = {};
michael@0 419 let allowCallback = function() {
michael@0 420 cpmm.sendAsyncMessage("Contacts:Clear", {requestID: this.getRequestId({request: request, reason: "remove"}), options: options});
michael@0 421 }.bind(this);
michael@0 422 this.askPermission("remove", request, allowCallback);
michael@0 423 return request;
michael@0 424 },
michael@0 425
michael@0 426 getRevision: function() {
michael@0 427 let request = this.createRequest();
michael@0 428
michael@0 429 let allowCallback = function() {
michael@0 430 cpmm.sendAsyncMessage("Contacts:GetRevision", {
michael@0 431 requestID: this.getRequestId({ request: request })
michael@0 432 });
michael@0 433 }.bind(this);
michael@0 434
michael@0 435 let cancelCallback = function() {
michael@0 436 Services.DOMRequest.fireError(request);
michael@0 437 };
michael@0 438
michael@0 439 this.askPermission("revision", request, allowCallback, cancelCallback);
michael@0 440 return request;
michael@0 441 },
michael@0 442
michael@0 443 getCount: function() {
michael@0 444 let request = this.createRequest();
michael@0 445
michael@0 446 let allowCallback = function() {
michael@0 447 cpmm.sendAsyncMessage("Contacts:GetCount", {
michael@0 448 requestID: this.getRequestId({ request: request })
michael@0 449 });
michael@0 450 }.bind(this);
michael@0 451
michael@0 452 let cancelCallback = function() {
michael@0 453 Services.DOMRequest.fireError(request);
michael@0 454 };
michael@0 455
michael@0 456 this.askPermission("count", request, allowCallback, cancelCallback);
michael@0 457 return request;
michael@0 458 },
michael@0 459
michael@0 460 init: function(aWindow) {
michael@0 461 // DOMRequestIpcHelper.initHelper sets this._window
michael@0 462 this.initDOMRequestHelper(aWindow, ["Contacts:Find:Return:OK", "Contacts:Find:Return:KO",
michael@0 463 "Contacts:Clear:Return:OK", "Contacts:Clear:Return:KO",
michael@0 464 "Contact:Save:Return:OK", "Contact:Save:Return:KO",
michael@0 465 "Contact:Remove:Return:OK", "Contact:Remove:Return:KO",
michael@0 466 "Contact:Changed",
michael@0 467 "PermissionPromptHelper:AskPermission:OK",
michael@0 468 "Contacts:GetAll:Next", "Contacts:GetAll:Return:KO",
michael@0 469 "Contacts:Count",
michael@0 470 "Contacts:Revision", "Contacts:GetRevision:Return:KO",]);
michael@0 471
michael@0 472
michael@0 473 let allowCallback = function() {
michael@0 474 cpmm.sendAsyncMessage("Contacts:RegisterForMessages");
michael@0 475 this.hasListenPermission = true;
michael@0 476 }.bind(this);
michael@0 477
michael@0 478 this.askPermission("listen", null, allowCallback);
michael@0 479 },
michael@0 480
michael@0 481 classID: Components.ID("{8beb3a66-d70a-4111-b216-b8e995ad3aff}"),
michael@0 482 contractID: "@mozilla.org/contactManager;1",
michael@0 483 QueryInterface: XPCOMUtils.generateQI([Ci.nsISupportsWeakReference,
michael@0 484 Ci.nsIObserver,
michael@0 485 Ci.nsIDOMGlobalPropertyInitializer]),
michael@0 486 };
michael@0 487
michael@0 488 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([
michael@0 489 Contact, ContactManager
michael@0 490 ]);

mercurial