dom/contacts/ContactManager.js

branch
TOR_BUG_3246
changeset 7
129ffea94266
equal deleted inserted replaced
-1:000000000000 0:ee6a13250e21
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 ]);

mercurial