|
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/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 const DEBUG = false; |
|
8 function debug(s) { dump("-*- ContactManager: " + s + "\n"); } |
|
9 |
|
10 const Cc = Components.classes; |
|
11 const Ci = Components.interfaces; |
|
12 const Cu = Components.utils; |
|
13 |
|
14 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
15 Cu.import("resource://gre/modules/Services.jsm"); |
|
16 Cu.import("resource://gre/modules/DOMRequestHelper.jsm"); |
|
17 |
|
18 XPCOMUtils.defineLazyServiceGetter(Services, "DOMRequest", |
|
19 "@mozilla.org/dom/dom-request-service;1", |
|
20 "nsIDOMRequestService"); |
|
21 |
|
22 XPCOMUtils.defineLazyServiceGetter(this, "cpmm", |
|
23 "@mozilla.org/childprocessmessagemanager;1", |
|
24 "nsIMessageSender"); |
|
25 |
|
26 const CONTACTS_SENDMORE_MINIMUM = 5; |
|
27 |
|
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" |
|
36 ]; |
|
37 |
|
38 let mozContactInitWarned = false; |
|
39 |
|
40 function Contact() { } |
|
41 |
|
42 Contact.prototype = { |
|
43 __init: function(aProp) { |
|
44 for (let prop in aProp) { |
|
45 this[prop] = aProp[prop]; |
|
46 } |
|
47 }, |
|
48 |
|
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 } |
|
56 |
|
57 for (let prop of PROPERTIES) { |
|
58 this[prop] = aProp[prop]; |
|
59 } |
|
60 }, |
|
61 |
|
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 }, |
|
71 |
|
72 classID: Components.ID("{72a5ee28-81d8-4af8-90b3-ae935396cc66}"), |
|
73 contractID: "@mozilla.org/contact;1", |
|
74 QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]), |
|
75 }; |
|
76 |
|
77 function ContactManager() { } |
|
78 |
|
79 ContactManager.prototype = { |
|
80 __proto__: DOMRequestIpcHelper.prototype, |
|
81 hasListenPermission: false, |
|
82 _cachedContacts: [] , |
|
83 |
|
84 set oncontactchange(aHandler) { |
|
85 this.__DOM_IMPL__.setEventHandler("oncontactchange", aHandler); |
|
86 }, |
|
87 |
|
88 get oncontactchange() { |
|
89 return this.__DOM_IMPL__.getEventHandler("oncontactchange"); |
|
90 }, |
|
91 |
|
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 }, |
|
97 |
|
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 }, |
|
105 |
|
106 _fireSuccessOrDone: function(aCursor, aResult) { |
|
107 if (aResult == null) { |
|
108 Services.DOMRequest.fireDone(aCursor); |
|
109 } else { |
|
110 Services.DOMRequest.fireSuccess(aCursor, aResult); |
|
111 } |
|
112 }, |
|
113 |
|
114 _pushArray: function(aArr1, aArr2) { |
|
115 aArr1.push.apply(aArr1, aArr2); |
|
116 }, |
|
117 |
|
118 receiveMessage: function(aMessage) { |
|
119 if (DEBUG) debug("receiveMessage: " + aMessage.name); |
|
120 let msg = aMessage.json; |
|
121 let contacts = msg.contacts; |
|
122 |
|
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 "PermissionPromptHelper:AskPermission:OK": |
|
189 if (DEBUG) debug("id: " + msg.requestID); |
|
190 req = this.getRequest(msg.requestID); |
|
191 if (!req) { |
|
192 break; |
|
193 } |
|
194 |
|
195 if (msg.result == Ci.nsIPermissionManager.ALLOW_ACTION) { |
|
196 req.allow(); |
|
197 } else { |
|
198 req.cancel(); |
|
199 } |
|
200 break; |
|
201 case "Contact:Changed": |
|
202 // Fire oncontactchange event |
|
203 if (DEBUG) debug("Contacts:ContactChanged: " + msg.contactID + ", " + msg.reason); |
|
204 let event = new this._window.MozContactChangeEvent("contactchange", { |
|
205 contactID: msg.contactID, |
|
206 reason: msg.reason |
|
207 }); |
|
208 this.dispatchEvent(event); |
|
209 break; |
|
210 case "Contacts:Revision": |
|
211 if (DEBUG) debug("new revision: " + msg.revision); |
|
212 req = this.getRequest(msg.requestID); |
|
213 if (req) { |
|
214 Services.DOMRequest.fireSuccess(req.request, msg.revision); |
|
215 } |
|
216 break; |
|
217 case "Contacts:Count": |
|
218 if (DEBUG) debug("count: " + msg.count); |
|
219 req = this.getRequest(msg.requestID); |
|
220 if (req) { |
|
221 Services.DOMRequest.fireSuccess(req.request, msg.count); |
|
222 } |
|
223 break; |
|
224 default: |
|
225 if (DEBUG) debug("Wrong message: " + aMessage.name); |
|
226 } |
|
227 this.removeRequest(msg.requestID); |
|
228 }, |
|
229 |
|
230 dispatchEvent: function(event) { |
|
231 if (this.hasListenPermission) { |
|
232 this.__DOM_IMPL__.dispatchEvent(event); |
|
233 } |
|
234 }, |
|
235 |
|
236 askPermission: function (aAccess, aRequest, aAllowCallback, aCancelCallback) { |
|
237 if (DEBUG) debug("askPermission for contacts"); |
|
238 let access; |
|
239 switch(aAccess) { |
|
240 case "create": |
|
241 access = "create"; |
|
242 break; |
|
243 case "update": |
|
244 case "remove": |
|
245 access = "write"; |
|
246 break; |
|
247 case "find": |
|
248 case "listen": |
|
249 case "revision": |
|
250 case "count": |
|
251 access = "read"; |
|
252 break; |
|
253 default: |
|
254 access = "unknown"; |
|
255 } |
|
256 |
|
257 // Shortcut for ALLOW_ACTION so we avoid a parent roundtrip |
|
258 let type = "contacts-" + access; |
|
259 let permValue = |
|
260 Services.perms.testExactPermissionFromPrincipal(this._window.document.nodePrincipal, type); |
|
261 if (permValue == Ci.nsIPermissionManager.ALLOW_ACTION) { |
|
262 aAllowCallback(); |
|
263 return; |
|
264 } |
|
265 |
|
266 let requestID = this.getRequestId({ |
|
267 request: aRequest, |
|
268 allow: function() { |
|
269 aAllowCallback(); |
|
270 }.bind(this), |
|
271 cancel : function() { |
|
272 if (aCancelCallback) { |
|
273 aCancelCallback() |
|
274 } else if (aRequest) { |
|
275 Services.DOMRequest.fireError(aRequest, "Not Allowed"); |
|
276 } |
|
277 }.bind(this) |
|
278 }); |
|
279 |
|
280 let principal = this._window.document.nodePrincipal; |
|
281 cpmm.sendAsyncMessage("PermissionPromptHelper:AskPermission", { |
|
282 type: "contacts", |
|
283 access: access, |
|
284 requestID: requestID, |
|
285 origin: principal.origin, |
|
286 appID: principal.appId, |
|
287 browserFlag: principal.isInBrowserElement, |
|
288 windowID: this._window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).outerWindowID |
|
289 }); |
|
290 }, |
|
291 |
|
292 save: function save(aContact) { |
|
293 // We have to do a deep copy of the contact manually here because |
|
294 // nsFrameMessageManager doesn't know how to create a structured clone of a |
|
295 // mozContact object. |
|
296 let newContact = {properties: {}}; |
|
297 |
|
298 try { |
|
299 for (let field of PROPERTIES) { |
|
300 // This hack makes sure modifications to the sequence attributes get validated. |
|
301 aContact[field] = aContact[field]; |
|
302 newContact.properties[field] = aContact[field]; |
|
303 } |
|
304 } catch (e) { |
|
305 // And then make sure we throw a proper error message (no internal file and line #) |
|
306 throw new this._window.DOMError(e.name, e.message); |
|
307 } |
|
308 |
|
309 let request = this.createRequest(); |
|
310 let requestID = this.getRequestId({request: request}); |
|
311 |
|
312 let reason; |
|
313 if (aContact.id == "undefined") { |
|
314 // for example {25c00f01-90e5-c545-b4d4-21E2ddbab9e0} becomes |
|
315 // 25c00f0190e5c545b4d421E2ddbab9e0 |
|
316 aContact.id = this._getRandomId().replace(/[{}-]/g, ""); |
|
317 // Cache the contact so that its ID may be updated later if necessary |
|
318 this._cachedContacts[requestID] = aContact; |
|
319 reason = "create"; |
|
320 } else { |
|
321 reason = "update"; |
|
322 } |
|
323 |
|
324 newContact.id = aContact.id; |
|
325 newContact.published = aContact.published; |
|
326 newContact.updated = aContact.updated; |
|
327 |
|
328 if (DEBUG) debug("send: " + JSON.stringify(newContact)); |
|
329 |
|
330 let options = { contact: newContact, reason: reason }; |
|
331 let allowCallback = function() { |
|
332 cpmm.sendAsyncMessage("Contact:Save", {requestID: requestID, options: options}); |
|
333 }.bind(this) |
|
334 this.askPermission(reason, request, allowCallback); |
|
335 return request; |
|
336 }, |
|
337 |
|
338 find: function(aOptions) { |
|
339 if (DEBUG) debug("find! " + JSON.stringify(aOptions)); |
|
340 let request = this.createRequest(); |
|
341 let options = { findOptions: aOptions }; |
|
342 let allowCallback = function() { |
|
343 cpmm.sendAsyncMessage("Contacts:Find", {requestID: this.getRequestId({request: request, reason: "find"}), options: options}); |
|
344 }.bind(this) |
|
345 this.askPermission("find", request, allowCallback); |
|
346 return request; |
|
347 }, |
|
348 |
|
349 createCursor: function CM_createCursor(aRequest) { |
|
350 let data = { |
|
351 cursor: Services.DOMRequest.createCursor(this._window, function() { |
|
352 this.handleContinue(id); |
|
353 }.bind(this)), |
|
354 cachedContacts: [], |
|
355 waitingForNext: true, |
|
356 }; |
|
357 let id = this.getRequestId(data); |
|
358 if (DEBUG) debug("saved cursor id: " + id); |
|
359 return [id, data.cursor]; |
|
360 }, |
|
361 |
|
362 getAll: function CM_getAll(aOptions) { |
|
363 if (DEBUG) debug("getAll: " + JSON.stringify(aOptions)); |
|
364 let [cursorId, cursor] = this.createCursor(); |
|
365 let allowCallback = function() { |
|
366 cpmm.sendAsyncMessage("Contacts:GetAll", { |
|
367 cursorId: cursorId, findOptions: aOptions}); |
|
368 }.bind(this); |
|
369 this.askPermission("find", cursor, allowCallback); |
|
370 return cursor; |
|
371 }, |
|
372 |
|
373 nextTick: function nextTick(aCallback) { |
|
374 Services.tm.currentThread.dispatch(aCallback, Ci.nsIThread.DISPATCH_NORMAL); |
|
375 }, |
|
376 |
|
377 handleContinue: function CM_handleContinue(aCursorId) { |
|
378 if (DEBUG) debug("handleContinue: " + aCursorId); |
|
379 let data = this.getRequest(aCursorId); |
|
380 if (data.cachedContacts.length > 0) { |
|
381 if (DEBUG) debug("contact in cache"); |
|
382 let contact = data.cachedContacts.shift(); |
|
383 this.nextTick(this._fireSuccessOrDone.bind(this, data.cursor, contact)); |
|
384 if (!contact) { |
|
385 this.removeRequest(aCursorId); |
|
386 } else if (data.cachedContacts.length === CONTACTS_SENDMORE_MINIMUM) { |
|
387 cpmm.sendAsyncMessage("Contacts:GetAll:SendNow", { cursorId: aCursorId }); |
|
388 } |
|
389 } else { |
|
390 if (DEBUG) debug("waiting for contact"); |
|
391 data.waitingForNext = true; |
|
392 } |
|
393 }, |
|
394 |
|
395 remove: function removeContact(aRecordOrId) { |
|
396 let request = this.createRequest(); |
|
397 let id; |
|
398 if (typeof aRecordOrId === "string") { |
|
399 id = aRecordOrId; |
|
400 } else if (!aRecordOrId || !aRecordOrId.id) { |
|
401 Services.DOMRequest.fireErrorAsync(request, true); |
|
402 return request; |
|
403 } else { |
|
404 id = aRecordOrId.id; |
|
405 } |
|
406 |
|
407 let options = { id: id }; |
|
408 let allowCallback = function() { |
|
409 cpmm.sendAsyncMessage("Contact:Remove", {requestID: this.getRequestId({request: request, reason: "remove"}), options: options}); |
|
410 }.bind(this); |
|
411 this.askPermission("remove", request, allowCallback); |
|
412 return request; |
|
413 }, |
|
414 |
|
415 clear: function() { |
|
416 if (DEBUG) debug("clear"); |
|
417 let request = this.createRequest(); |
|
418 let options = {}; |
|
419 let allowCallback = function() { |
|
420 cpmm.sendAsyncMessage("Contacts:Clear", {requestID: this.getRequestId({request: request, reason: "remove"}), options: options}); |
|
421 }.bind(this); |
|
422 this.askPermission("remove", request, allowCallback); |
|
423 return request; |
|
424 }, |
|
425 |
|
426 getRevision: function() { |
|
427 let request = this.createRequest(); |
|
428 |
|
429 let allowCallback = function() { |
|
430 cpmm.sendAsyncMessage("Contacts:GetRevision", { |
|
431 requestID: this.getRequestId({ request: request }) |
|
432 }); |
|
433 }.bind(this); |
|
434 |
|
435 let cancelCallback = function() { |
|
436 Services.DOMRequest.fireError(request); |
|
437 }; |
|
438 |
|
439 this.askPermission("revision", request, allowCallback, cancelCallback); |
|
440 return request; |
|
441 }, |
|
442 |
|
443 getCount: function() { |
|
444 let request = this.createRequest(); |
|
445 |
|
446 let allowCallback = function() { |
|
447 cpmm.sendAsyncMessage("Contacts:GetCount", { |
|
448 requestID: this.getRequestId({ request: request }) |
|
449 }); |
|
450 }.bind(this); |
|
451 |
|
452 let cancelCallback = function() { |
|
453 Services.DOMRequest.fireError(request); |
|
454 }; |
|
455 |
|
456 this.askPermission("count", request, allowCallback, cancelCallback); |
|
457 return request; |
|
458 }, |
|
459 |
|
460 init: function(aWindow) { |
|
461 // DOMRequestIpcHelper.initHelper sets this._window |
|
462 this.initDOMRequestHelper(aWindow, ["Contacts:Find:Return:OK", "Contacts:Find:Return:KO", |
|
463 "Contacts:Clear:Return:OK", "Contacts:Clear:Return:KO", |
|
464 "Contact:Save:Return:OK", "Contact:Save:Return:KO", |
|
465 "Contact:Remove:Return:OK", "Contact:Remove:Return:KO", |
|
466 "Contact:Changed", |
|
467 "PermissionPromptHelper:AskPermission:OK", |
|
468 "Contacts:GetAll:Next", "Contacts:GetAll:Return:KO", |
|
469 "Contacts:Count", |
|
470 "Contacts:Revision", "Contacts:GetRevision:Return:KO",]); |
|
471 |
|
472 |
|
473 let allowCallback = function() { |
|
474 cpmm.sendAsyncMessage("Contacts:RegisterForMessages"); |
|
475 this.hasListenPermission = true; |
|
476 }.bind(this); |
|
477 |
|
478 this.askPermission("listen", null, allowCallback); |
|
479 }, |
|
480 |
|
481 classID: Components.ID("{8beb3a66-d70a-4111-b216-b8e995ad3aff}"), |
|
482 contractID: "@mozilla.org/contactManager;1", |
|
483 QueryInterface: XPCOMUtils.generateQI([Ci.nsISupportsWeakReference, |
|
484 Ci.nsIObserver, |
|
485 Ci.nsIDOMGlobalPropertyInitializer]), |
|
486 }; |
|
487 |
|
488 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ |
|
489 Contact, ContactManager |
|
490 ]); |