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/. */
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",
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.
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",
38 let mozContactInitWarned = false;
40 function Contact() { }
43 __init: function(aProp) {
44 for (let prop in aProp) {
45 this[prop] = aProp[prop];
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.");
57 for (let prop of PROPERTIES) {
58 this[prop] = aProp[prop];
62 setMetadata: function(aId, aPublished, aUpdated) {
65 this.published = aPublished;
68 this.updated = aUpdated;
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,
84 set oncontactchange(aHandler) {
85 this.__DOM_IMPL__.setEventHandler("oncontactchange", aHandler);
88 get oncontactchange() {
89 return this.__DOM_IMPL__.getEventHandler("oncontactchange");
92 _convertContact: function(aContact) {
93 let newContact = new this._window.mozContact(aContact.properties);
94 newContact.setMetadata(aContact.id, aContact.published, aContact.updated);
98 _convertContacts: function(aContacts) {
99 let contacts = new this._window.Array();
100 for (let i in aContacts) {
101 contacts.push(this._convertContact(aContacts[i]));
106 _fireSuccessOrDone: function(aCursor, aResult) {
107 if (aResult == null) {
108 Services.DOMRequest.fireDone(aCursor);
110 Services.DOMRequest.fireSuccess(aCursor, aResult);
114 _pushArray: function(aArr1, aArr2) {
115 aArr1.push.apply(aArr1, aArr2);
118 receiveMessage: function(aMessage) {
119 if (DEBUG) debug("receiveMessage: " + aMessage.name);
120 let msg = aMessage.json;
121 let contacts = msg.contacts;
124 switch (aMessage.name) {
125 case "Contacts:Find:Return:OK":
126 req = this.getRequest(msg.requestID);
128 let result = this._convertContacts(contacts);
129 Services.DOMRequest.fireSuccess(req.request, result);
131 if (DEBUG) debug("no request stored!" + msg.requestID);
134 case "Contacts:GetAll:Next":
135 let data = this.getRequest(msg.cursorId);
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));
147 this.removeRequest(msg.cursorId);
150 if (DEBUG) debug("cursor not waiting, saving");
151 this._pushArray(data.cachedContacts, result);
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]) {
158 this._cachedContacts[msg.requestID].id = msg.contactID;
160 delete this._cachedContacts[msg.requestID];
162 case "Contacts:Clear:Return:OK":
163 case "Contact:Remove:Return:OK":
164 req = this.getRequest(msg.requestID);
166 Services.DOMRequest.fireSuccess(req.request, null);
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);
179 Services.DOMRequest.fireError(req, msg.errorMsg);
182 case "Contacts:GetAll:Return:KO":
183 req = this.getRequest(msg.requestID);
185 Services.DOMRequest.fireError(req.cursor, msg.errorMsg);
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,
195 this.dispatchEvent(event);
197 case "Contacts:Revision":
198 if (DEBUG) debug("new revision: " + msg.revision);
199 req = this.getRequest(msg.requestID);
201 Services.DOMRequest.fireSuccess(req.request, msg.revision);
204 case "Contacts:Count":
205 if (DEBUG) debug("count: " + msg.count);
206 req = this.getRequest(msg.requestID);
208 Services.DOMRequest.fireSuccess(req.request, msg.count);
212 if (DEBUG) debug("Wrong message: " + aMessage.name);
214 this.removeRequest(msg.requestID);
217 dispatchEvent: function(event) {
218 if (this.hasListenPermission) {
219 this.__DOM_IMPL__.dispatchEvent(event);
223 askPermission: function (aAccess, aRequest, aAllowCallback, aCancelCallback) {
224 if (DEBUG) debug("askPermission for contacts");
245 // Shortcut for ALLOW_ACTION so we avoid a parent roundtrip
246 let principal = this._window.document.nodePrincipal;
247 let type = "contacts-" + access;
249 Services.perms.testExactPermissionFromPrincipal(principal, type);
250 DEBUG && debug("Existing permission " + permValue);
251 if (permValue == Ci.nsIPermissionManager.ALLOW_ACTION) {
252 if (aAllowCallback) {
256 } else if (permValue == Ci.nsIPermissionManager.DENY_ACTION ||
257 permValue == Ci.nsIPermissionManager.UNKNOWN_ACTION) {
258 if (aCancelCallback) {
259 aCancelCallback("PERMISSION_DENIED");
264 // Create an array with a single nsIContentPermissionType element.
269 QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPermissionType])
271 let typeArray = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
272 typeArray.appendElement(type, false);
274 // create a nsIContentPermissionRequest
277 principal: principal,
278 QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPermissionRequest]),
280 aAllowCallback && aAllowCallback();
281 DEBUG && debug("Permission granted. Access " + access +"\n");
284 aCancelCallback && aCancelCallback("PERMISSION_DENIED");
285 DEBUG && debug("Permission denied. Access " + access +"\n");
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);
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: {}};
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];
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);
314 let request = this.createRequest();
315 let requestID = this.getRequestId({request: request});
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;
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,
343 let cancelCallback = function(reason) {
344 Services.DOMRequest.fireErrorAsync(request, reason);
347 this.askPermission(reason, request, allowCallback, cancelCallback);
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"}),
363 let cancelCallback = function(reason) {
364 Services.DOMRequest.fireErrorAsync(request, reason);
367 this.askPermission("find", request, allowCallback, cancelCallback);
371 createCursor: function CM_createCursor(aRequest) {
373 cursor: Services.DOMRequest.createCursor(this._window, function() {
374 this.handleContinue(id);
377 waitingForNext: true,
379 let id = this.getRequestId(data);
380 if (DEBUG) debug("saved cursor id: " + id);
381 return [id, data.cursor];
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", {
391 findOptions: aOptions
395 let cancelCallback = function(reason) {
396 Services.DOMRequest.fireErrorAsync(cursor, reason);
399 this.askPermission("find", cursor, allowCallback, cancelCallback);
403 nextTick: function nextTick(aCallback) {
404 Services.tm.currentThread.dispatch(aCallback, Ci.nsIThread.DISPATCH_NORMAL);
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));
415 this.removeRequest(aCursorId);
416 } else if (data.cachedContacts.length === CONTACTS_SENDMORE_MINIMUM) {
417 cpmm.sendAsyncMessage("Contacts:GetAll:SendNow", { cursorId: aCursorId });
420 if (DEBUG) debug("waiting for contact");
421 data.waitingForNext = true;
425 remove: function removeContact(aRecordOrId) {
426 let request = this.createRequest();
428 if (typeof aRecordOrId === "string") {
430 } else if (!aRecordOrId || !aRecordOrId.id) {
431 Services.DOMRequest.fireErrorAsync(request, true);
437 let options = { id: id };
439 let allowCallback = function() {
440 cpmm.sendAsyncMessage("Contact:Remove", {
441 requestID: this.getRequestId({request: request, reason: "remove"}),
446 let cancelCallback = function(reason) {
447 Services.DOMRequest.fireErrorAsync(request, reason);
450 this.askPermission("remove", request, allowCallback, cancelCallback);
455 if (DEBUG) debug("clear");
456 let request = this.createRequest();
459 let allowCallback = function() {
460 cpmm.sendAsyncMessage("Contacts:Clear", {
461 requestID: this.getRequestId({request: request, reason: "remove"}),
466 let cancelCallback = function(reason) {
467 Services.DOMRequest.fireErrorAsync(request, reason);
470 this.askPermission("remove", request, allowCallback, cancelCallback);
474 getRevision: function() {
475 let request = this.createRequest();
477 let allowCallback = function() {
478 cpmm.sendAsyncMessage("Contacts:GetRevision", {
479 requestID: this.getRequestId({ request: request })
483 let cancelCallback = function(reason) {
484 Services.DOMRequest.fireErrorAsync(request, reason);
487 this.askPermission("revision", request, allowCallback, cancelCallback);
491 getCount: function() {
492 let request = this.createRequest();
494 let allowCallback = function() {
495 cpmm.sendAsyncMessage("Contacts:GetCount", {
496 requestID: this.getRequestId({ request: request })
500 let cancelCallback = function(reason) {
501 Services.DOMRequest.fireErrorAsync(request, reason);
504 this.askPermission("count", request, allowCallback, cancelCallback);
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",
515 "Contacts:GetAll:Next", "Contacts:GetAll:Return:KO",
517 "Contacts:Revision", "Contacts:GetRevision:Return:KO",]);
520 let allowCallback = function() {
521 cpmm.sendAsyncMessage("Contacts:RegisterForMessages");
522 this.hasListenPermission = true;
525 this.askPermission("listen", null, allowCallback);
528 classID: Components.ID("{8beb3a66-d70a-4111-b216-b8e995ad3aff}"),
529 contractID: "@mozilla.org/contactManager;1",
530 QueryInterface: XPCOMUtils.generateQI([Ci.nsISupportsWeakReference,
532 Ci.nsIDOMGlobalPropertyInitializer]),
535 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([
536 Contact, ContactManager