Bumping manifests a=b2g-bump
[gecko.git] / dom / notification / NotificationDB.jsm
blob09cf91a85a8420aba6ee9c1f1a663c62d2373307
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 this.EXPORTED_SYMBOLS = [];
9 const DEBUG = false;
10 function debug(s) { dump("-*- NotificationDB component: " + s + "\n"); }
12 const Cu = Components.utils;
13 const Cc = Components.classes;
14 const Ci = Components.interfaces;
16 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
17 Cu.import("resource://gre/modules/osfile.jsm");
18 Cu.import("resource://gre/modules/Promise.jsm");
20 XPCOMUtils.defineLazyModuleGetter(this, "Services",
21                                   "resource://gre/modules/Services.jsm");
23 XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
24                                    "@mozilla.org/parentprocessmessagemanager;1",
25                                    "nsIMessageListenerManager");
27 XPCOMUtils.defineLazyServiceGetter(this, "notificationStorage",
28                                    "@mozilla.org/notificationStorage;1",
29                                    "nsINotificationStorage");
31 const NOTIFICATION_STORE_DIR = OS.Constants.Path.profileDir;
32 const NOTIFICATION_STORE_PATH =
33         OS.Path.join(NOTIFICATION_STORE_DIR, "notificationstore.json");
35 const kMessages = [
36   "Notification:Save",
37   "Notification:Delete",
38   "Notification:GetAll",
39   "Notification:GetAllCrossOrigin"
42 let NotificationDB = {
44   // Ensure we won't call init() while xpcom-shutdown is performed
45   _shutdownInProgress: false,
47   init: function() {
48     if (this._shutdownInProgress) {
49       return;
50     }
52     this.notifications = {};
53     this.byTag = {};
54     this.loaded = false;
56     this.tasks = []; // read/write operation queue
57     this.runningTask = null;
59     Services.obs.addObserver(this, "xpcom-shutdown", false);
60     this.registerListeners();
61   },
63   registerListeners: function() {
64     for (let message of kMessages) {
65       ppmm.addMessageListener(message, this);
66     }
67   },
69   unregisterListeners: function() {
70     for (let message of kMessages) {
71       ppmm.removeMessageListener(message, this);
72     }
73   },
75   observe: function(aSubject, aTopic, aData) {
76     if (DEBUG) debug("Topic: " + aTopic);
77     if (aTopic == "xpcom-shutdown") {
78       this._shutdownInProgress = true;
79       Services.obs.removeObserver(this, "xpcom-shutdown");
80       this.unregisterListeners();
81     }
82   },
84   filterNonAppNotifications: function(notifications) {
85     let origins = Object.keys(notifications);
86     for (let origin of origins) {
87       let canPut = notificationStorage.canPut(origin);
88       if (!canPut) {
89         if (DEBUG) debug("Origin " + origin + " is not linked to an app manifest, deleting.");
90         delete notifications[origin];
91       }
92     }
93     return notifications;
94   },
96   // Attempt to read notification file, if it's not there we will create it.
97   load: function() {
98     var promise = OS.File.read(NOTIFICATION_STORE_PATH, { encoding: "utf-8"});
99     return promise.then(
100       function onSuccess(data) {
101         if (data.length > 0) {
102           // Preprocessing phase intends to cleanly separate any migration-related
103           // tasks.
104           this.notifications = this.filterNonAppNotifications(JSON.parse(data));
105         }
107         // populate the list of notifications by tag
108         if (this.notifications) {
109           for (var origin in this.notifications) {
110             this.byTag[origin] = {};
111             for (var id in this.notifications[origin]) {
112               var curNotification = this.notifications[origin][id];
113               if (curNotification.tag) {
114                 this.byTag[origin][curNotification.tag] = curNotification;
115               }
116             }
117           }
118         }
120         this.loaded = true;
121       }.bind(this),
123       // If read failed, we assume we have no notifications to load.
124       function onFailure(reason) {
125         this.loaded = true;
126         return this.createStore();
127       }.bind(this)
128     );
129   },
131   // Creates the notification directory.
132   createStore: function() {
133     var promise = OS.File.makeDir(NOTIFICATION_STORE_DIR, {
134       ignoreExisting: true
135     });
136     return promise.then(
137       this.createFile.bind(this)
138     );
139   },
141   // Creates the notification file once the directory is created.
142   createFile: function() {
143     return OS.File.writeAtomic(NOTIFICATION_STORE_PATH, "");
144   },
146   // Save current notifications to the file.
147   save: function() {
148     var data = JSON.stringify(this.notifications);
149     return OS.File.writeAtomic(NOTIFICATION_STORE_PATH, data, { encoding: "utf-8"});
150   },
152   // Helper function: promise will be resolved once file exists and/or is loaded.
153   ensureLoaded: function() {
154     if (!this.loaded) {
155       return this.load();
156     } else {
157       return Promise.resolve();
158     }
159   },
161   receiveMessage: function(message) {
162     if (DEBUG) { debug("Received message:" + message.name); }
164     // sendAsyncMessage can fail if the child process exits during a
165     // notification storage operation, so always wrap it in a try/catch.
166     function returnMessage(name, data) {
167       try {
168         message.target.sendAsyncMessage(name, data);
169       } catch (e) {
170         if (DEBUG) { debug("Return message failed, " + name); }
171       }
172     }
174     switch (message.name) {
175       case "Notification:GetAll":
176         this.queueTask("getall", message.data).then(function(notifications) {
177           returnMessage("Notification:GetAll:Return:OK", {
178             requestID: message.data.requestID,
179             origin: message.data.origin,
180             notifications: notifications
181           });
182         }).catch(function(error) {
183           returnMessage("Notification:GetAll:Return:KO", {
184             requestID: message.data.requestID,
185             origin: message.data.origin,
186             errorMsg: error
187           });
188         });
189         break;
191       case "Notification:GetAllCrossOrigin":
192         this.queueTask("getallaccrossorigin", message.data).then(
193           function(notifications) {
194             returnMessage("Notification:GetAllCrossOrigin:Return:OK", {
195               notifications: notifications
196             });
197           }).catch(function(error) {
198             returnMessage("Notification:GetAllCrossOrigin:Return:KO", {
199               errorMsg: error
200             });
201           });
202         break;
204       case "Notification:Save":
205         this.queueTask("save", message.data).then(function() {
206           returnMessage("Notification:Save:Return:OK", {
207             requestID: message.data.requestID
208           });
209         }).catch(function(error) {
210           returnMessage("Notification:Save:Return:KO", {
211             requestID: message.data.requestID,
212             errorMsg: error
213           });
214         });
215         break;
217       case "Notification:Delete":
218         this.queueTask("delete", message.data).then(function() {
219           returnMessage("Notification:Delete:Return:OK", {
220             requestID: message.data.requestID
221           });
222         }).catch(function(error) {
223           returnMessage("Notification:Delete:Return:KO", {
224             requestID: message.data.requestID,
225             errorMsg: error
226           });
227         });
228         break;
230       default:
231         if (DEBUG) { debug("Invalid message name" + message.name); }
232     }
233   },
235   // We need to make sure any read/write operations are atomic,
236   // so use a queue to run each operation sequentially.
237   queueTask: function(operation, data) {
238     if (DEBUG) { debug("Queueing task: " + operation); }
240     var defer = {};
242     this.tasks.push({
243       operation: operation,
244       data: data,
245       defer: defer
246     });
248     var promise = new Promise(function(resolve, reject) {
249       defer.resolve = resolve;
250       defer.reject = reject;
251     });
253     // Only run immediately if we aren't currently running another task.
254     if (!this.runningTask) {
255       if (DEBUG) { debug("Task queue was not running, starting now..."); }
256       this.runNextTask();
257     }
259     return promise;
260   },
262   runNextTask: function() {
263     if (this.tasks.length === 0) {
264       if (DEBUG) { debug("No more tasks to run, queue depleted"); }
265       this.runningTask = null;
266       return;
267     }
268     this.runningTask = this.tasks.shift();
270     // Always make sure we are loaded before performing any read/write tasks.
271     this.ensureLoaded()
272     .then(function() {
273       var task = this.runningTask;
275       switch (task.operation) {
276         case "getall":
277           return this.taskGetAll(task.data);
278           break;
280         case "getallaccrossorigin":
281           return this.taskGetAllCrossOrigin();
282           break;
284         case "save":
285           return this.taskSave(task.data);
286           break;
288         case "delete":
289           return this.taskDelete(task.data);
290           break;
291       }
293     }.bind(this))
294     .then(function(payload) {
295       if (DEBUG) {
296         debug("Finishing task: " + this.runningTask.operation);
297       }
298       this.runningTask.defer.resolve(payload);
299     }.bind(this))
300     .catch(function(err) {
301       if (DEBUG) {
302         debug("Error while running " + this.runningTask.operation + ": " + err);
303       }
304       this.runningTask.defer.reject(new String(err));
305     }.bind(this))
306     .then(function() {
307       this.runNextTask();
308     }.bind(this));
309   },
311   taskGetAll: function(data) {
312     if (DEBUG) { debug("Task, getting all"); }
313     var origin = data.origin;
314     var notifications = [];
315     // Grab only the notifications for specified origin.
316     if (this.notifications[origin]) {
317       for (var i in this.notifications[origin]) {
318         notifications.push(this.notifications[origin][i]);
319       }
320     }
321     return Promise.resolve(notifications);
322   },
324   taskGetAllCrossOrigin: function() {
325     if (DEBUG) { debug("Task, getting all whatever origin"); }
326     var notifications = [];
327     for (var origin in this.notifications) {
328       if (!this.notifications[origin]) {
329         continue;
330       }
332       for (var i in this.notifications[origin]) {
333         var notification = this.notifications[origin][i];
335         // Notifications without the alertName field cannot be resent by
336         // mozResendAllNotifications, so we just skip them. They will
337         // still be available to applications via Notification.get()
338         if (!('alertName' in notification)) {
339           continue;
340         }
342         notification.origin = origin;
343         notifications.push(notification);
344       }
345     }
346     return Promise.resolve(notifications);
347   },
349   taskSave: function(data) {
350     if (DEBUG) { debug("Task, saving"); }
351     var origin = data.origin;
352     var notification = data.notification;
353     if (!this.notifications[origin]) {
354       this.notifications[origin] = {};
355       this.byTag[origin] = {};
356     }
358     // We might have existing notification with this tag,
359     // if so we need to remove it before saving the new one.
360     if (notification.tag) {
361       var oldNotification = this.byTag[origin][notification.tag];
362       if (oldNotification) {
363         delete this.notifications[origin][oldNotification.id];
364       }
365       this.byTag[origin][notification.tag] = notification;
366     }
368     this.notifications[origin][notification.id] = notification;
369     return this.save();
370   },
372   taskDelete: function(data) {
373     if (DEBUG) { debug("Task, deleting"); }
374     var origin = data.origin;
375     var id = data.id;
376     if (!this.notifications[origin]) {
377       if (DEBUG) { debug("No notifications found for origin: " + origin); }
378       return Promise.resolve();
379     }
381     // Make sure we can find the notification to delete.
382     var oldNotification = this.notifications[origin][id];
383     if (!oldNotification) {
384       if (DEBUG) { debug("No notification found with id: " + id); }
385       return Promise.resolve();
386     }
388     if (oldNotification.tag) {
389       delete this.byTag[origin][oldNotification.tag];
390     }
391     delete this.notifications[origin][id];
392     return this.save();
393   }
396 NotificationDB.init();