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/. */
7 this.EXPORTED_SYMBOLS = [];
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");
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,
48 if (this._shutdownInProgress) {
52 this.notifications = {};
56 this.tasks = []; // read/write operation queue
57 this.runningTask = null;
59 Services.obs.addObserver(this, "xpcom-shutdown", false);
60 this.registerListeners();
63 registerListeners: function() {
64 for (let message of kMessages) {
65 ppmm.addMessageListener(message, this);
69 unregisterListeners: function() {
70 for (let message of kMessages) {
71 ppmm.removeMessageListener(message, this);
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();
84 filterNonAppNotifications: function(notifications) {
85 let origins = Object.keys(notifications);
86 for (let origin of origins) {
87 let canPut = notificationStorage.canPut(origin);
89 if (DEBUG) debug("Origin " + origin + " is not linked to an app manifest, deleting.");
90 delete notifications[origin];
96 // Attempt to read notification file, if it's not there we will create it.
98 var promise = OS.File.read(NOTIFICATION_STORE_PATH, { encoding: "utf-8"});
100 function onSuccess(data) {
101 if (data.length > 0) {
102 // Preprocessing phase intends to cleanly separate any migration-related
104 this.notifications = this.filterNonAppNotifications(JSON.parse(data));
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;
123 // If read failed, we assume we have no notifications to load.
124 function onFailure(reason) {
126 return this.createStore();
131 // Creates the notification directory.
132 createStore: function() {
133 var promise = OS.File.makeDir(NOTIFICATION_STORE_DIR, {
137 this.createFile.bind(this)
141 // Creates the notification file once the directory is created.
142 createFile: function() {
143 return OS.File.writeAtomic(NOTIFICATION_STORE_PATH, "");
146 // Save current notifications to the file.
148 var data = JSON.stringify(this.notifications);
149 return OS.File.writeAtomic(NOTIFICATION_STORE_PATH, data, { encoding: "utf-8"});
152 // Helper function: promise will be resolved once file exists and/or is loaded.
153 ensureLoaded: function() {
157 return Promise.resolve();
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) {
168 message.target.sendAsyncMessage(name, data);
170 if (DEBUG) { debug("Return message failed, " + name); }
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
182 }).catch(function(error) {
183 returnMessage("Notification:GetAll:Return:KO", {
184 requestID: message.data.requestID,
185 origin: message.data.origin,
191 case "Notification:GetAllCrossOrigin":
192 this.queueTask("getallaccrossorigin", message.data).then(
193 function(notifications) {
194 returnMessage("Notification:GetAllCrossOrigin:Return:OK", {
195 notifications: notifications
197 }).catch(function(error) {
198 returnMessage("Notification:GetAllCrossOrigin:Return:KO", {
204 case "Notification:Save":
205 this.queueTask("save", message.data).then(function() {
206 returnMessage("Notification:Save:Return:OK", {
207 requestID: message.data.requestID
209 }).catch(function(error) {
210 returnMessage("Notification:Save:Return:KO", {
211 requestID: message.data.requestID,
217 case "Notification:Delete":
218 this.queueTask("delete", message.data).then(function() {
219 returnMessage("Notification:Delete:Return:OK", {
220 requestID: message.data.requestID
222 }).catch(function(error) {
223 returnMessage("Notification:Delete:Return:KO", {
224 requestID: message.data.requestID,
231 if (DEBUG) { debug("Invalid message name" + message.name); }
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); }
243 operation: operation,
248 var promise = new Promise(function(resolve, reject) {
249 defer.resolve = resolve;
250 defer.reject = reject;
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..."); }
262 runNextTask: function() {
263 if (this.tasks.length === 0) {
264 if (DEBUG) { debug("No more tasks to run, queue depleted"); }
265 this.runningTask = null;
268 this.runningTask = this.tasks.shift();
270 // Always make sure we are loaded before performing any read/write tasks.
273 var task = this.runningTask;
275 switch (task.operation) {
277 return this.taskGetAll(task.data);
280 case "getallaccrossorigin":
281 return this.taskGetAllCrossOrigin();
285 return this.taskSave(task.data);
289 return this.taskDelete(task.data);
294 .then(function(payload) {
296 debug("Finishing task: " + this.runningTask.operation);
298 this.runningTask.defer.resolve(payload);
300 .catch(function(err) {
302 debug("Error while running " + this.runningTask.operation + ": " + err);
304 this.runningTask.defer.reject(new String(err));
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]);
321 return Promise.resolve(notifications);
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]) {
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)) {
342 notification.origin = origin;
343 notifications.push(notification);
346 return Promise.resolve(notifications);
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] = {};
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];
365 this.byTag[origin][notification.tag] = notification;
368 this.notifications[origin][notification.id] = notification;
372 taskDelete: function(data) {
373 if (DEBUG) { debug("Task, deleting"); }
374 var origin = data.origin;
376 if (!this.notifications[origin]) {
377 if (DEBUG) { debug("No notifications found for origin: " + origin); }
378 return Promise.resolve();
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();
388 if (oldNotification.tag) {
389 delete this.byTag[origin][oldNotification.tag];
391 delete this.notifications[origin][id];
396 NotificationDB.init();