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