gecko/dom/notification/NotificationDB.jsm

397 lines
12 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = [];
const DEBUG = false;
function debug(s) { dump("-*- NotificationDB component: " + s + "\n"); }
const Cu = Components.utils;
const Cc = Components.classes;
const Ci = Components.interfaces;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/osfile.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
"@mozilla.org/parentprocessmessagemanager;1",
"nsIMessageListenerManager");
XPCOMUtils.defineLazyServiceGetter(this, "notificationStorage",
"@mozilla.org/notificationStorage;1",
"nsINotificationStorage");
const NOTIFICATION_STORE_DIR = OS.Constants.Path.profileDir;
const NOTIFICATION_STORE_PATH =
OS.Path.join(NOTIFICATION_STORE_DIR, "notificationstore.json");
const kMessages = [
"Notification:Save",
"Notification:Delete",
"Notification:GetAll",
"Notification:GetAllCrossOrigin"
];
var NotificationDB = {
// Ensure we won't call init() while xpcom-shutdown is performed
_shutdownInProgress: false,
init: function() {
if (this._shutdownInProgress) {
return;
}
this.notifications = {};
this.byTag = {};
this.loaded = false;
this.tasks = []; // read/write operation queue
this.runningTask = null;
Services.obs.addObserver(this, "xpcom-shutdown", false);
this.registerListeners();
},
registerListeners: function() {
for (let message of kMessages) {
ppmm.addMessageListener(message, this);
}
},
unregisterListeners: function() {
for (let message of kMessages) {
ppmm.removeMessageListener(message, this);
}
},
observe: function(aSubject, aTopic, aData) {
if (DEBUG) debug("Topic: " + aTopic);
if (aTopic == "xpcom-shutdown") {
this._shutdownInProgress = true;
Services.obs.removeObserver(this, "xpcom-shutdown");
this.unregisterListeners();
}
},
filterNonAppNotifications: function(notifications) {
let origins = Object.keys(notifications);
for (let origin of origins) {
let canPut = notificationStorage.canPut(origin);
if (!canPut) {
if (DEBUG) debug("Origin " + origin + " is not linked to an app manifest, deleting.");
delete notifications[origin];
}
}
return notifications;
},
// Attempt to read notification file, if it's not there we will create it.
load: function() {
var promise = OS.File.read(NOTIFICATION_STORE_PATH, { encoding: "utf-8"});
return promise.then(
function onSuccess(data) {
if (data.length > 0) {
// Preprocessing phase intends to cleanly separate any migration-related
// tasks.
this.notifications = this.filterNonAppNotifications(JSON.parse(data));
}
// populate the list of notifications by tag
if (this.notifications) {
for (var origin in this.notifications) {
this.byTag[origin] = {};
for (var id in this.notifications[origin]) {
var curNotification = this.notifications[origin][id];
if (curNotification.tag) {
this.byTag[origin][curNotification.tag] = curNotification;
}
}
}
}
this.loaded = true;
}.bind(this),
// If read failed, we assume we have no notifications to load.
function onFailure(reason) {
this.loaded = true;
return this.createStore();
}.bind(this)
);
},
// Creates the notification directory.
createStore: function() {
var promise = OS.File.makeDir(NOTIFICATION_STORE_DIR, {
ignoreExisting: true
});
return promise.then(
this.createFile.bind(this)
);
},
// Creates the notification file once the directory is created.
createFile: function() {
return OS.File.writeAtomic(NOTIFICATION_STORE_PATH, "");
},
// Save current notifications to the file.
save: function() {
var data = JSON.stringify(this.notifications);
return OS.File.writeAtomic(NOTIFICATION_STORE_PATH, data, { encoding: "utf-8"});
},
// Helper function: promise will be resolved once file exists and/or is loaded.
ensureLoaded: function() {
if (!this.loaded) {
return this.load();
} else {
return Promise.resolve();
}
},
receiveMessage: function(message) {
if (DEBUG) { debug("Received message:" + message.name); }
// sendAsyncMessage can fail if the child process exits during a
// notification storage operation, so always wrap it in a try/catch.
function returnMessage(name, data) {
try {
message.target.sendAsyncMessage(name, data);
} catch (e) {
if (DEBUG) { debug("Return message failed, " + name); }
}
}
switch (message.name) {
case "Notification:GetAll":
this.queueTask("getall", message.data).then(function(notifications) {
returnMessage("Notification:GetAll:Return:OK", {
requestID: message.data.requestID,
origin: message.data.origin,
notifications: notifications
});
}).catch(function(error) {
returnMessage("Notification:GetAll:Return:KO", {
requestID: message.data.requestID,
origin: message.data.origin,
errorMsg: error
});
});
break;
case "Notification:GetAllCrossOrigin":
this.queueTask("getallaccrossorigin", message.data).then(
function(notifications) {
returnMessage("Notification:GetAllCrossOrigin:Return:OK", {
notifications: notifications
});
}).catch(function(error) {
returnMessage("Notification:GetAllCrossOrigin:Return:KO", {
errorMsg: error
});
});
break;
case "Notification:Save":
this.queueTask("save", message.data).then(function() {
returnMessage("Notification:Save:Return:OK", {
requestID: message.data.requestID
});
}).catch(function(error) {
returnMessage("Notification:Save:Return:KO", {
requestID: message.data.requestID,
errorMsg: error
});
});
break;
case "Notification:Delete":
this.queueTask("delete", message.data).then(function() {
returnMessage("Notification:Delete:Return:OK", {
requestID: message.data.requestID
});
}).catch(function(error) {
returnMessage("Notification:Delete:Return:KO", {
requestID: message.data.requestID,
errorMsg: error
});
});
break;
default:
if (DEBUG) { debug("Invalid message name" + message.name); }
}
},
// We need to make sure any read/write operations are atomic,
// so use a queue to run each operation sequentially.
queueTask: function(operation, data) {
if (DEBUG) { debug("Queueing task: " + operation); }
var defer = {};
this.tasks.push({
operation: operation,
data: data,
defer: defer
});
var promise = new Promise(function(resolve, reject) {
defer.resolve = resolve;
defer.reject = reject;
});
// Only run immediately if we aren't currently running another task.
if (!this.runningTask) {
if (DEBUG) { debug("Task queue was not running, starting now..."); }
this.runNextTask();
}
return promise;
},
runNextTask: function() {
if (this.tasks.length === 0) {
if (DEBUG) { debug("No more tasks to run, queue depleted"); }
this.runningTask = null;
return;
}
this.runningTask = this.tasks.shift();
// Always make sure we are loaded before performing any read/write tasks.
this.ensureLoaded()
.then(function() {
var task = this.runningTask;
switch (task.operation) {
case "getall":
return this.taskGetAll(task.data);
break;
case "getallaccrossorigin":
return this.taskGetAllCrossOrigin();
break;
case "save":
return this.taskSave(task.data);
break;
case "delete":
return this.taskDelete(task.data);
break;
}
}.bind(this))
.then(function(payload) {
if (DEBUG) {
debug("Finishing task: " + this.runningTask.operation);
}
this.runningTask.defer.resolve(payload);
}.bind(this))
.catch(function(err) {
if (DEBUG) {
debug("Error while running " + this.runningTask.operation + ": " + err);
}
this.runningTask.defer.reject(new String(err));
}.bind(this))
.then(function() {
this.runNextTask();
}.bind(this));
},
taskGetAll: function(data) {
if (DEBUG) { debug("Task, getting all"); }
var origin = data.origin;
var notifications = [];
// Grab only the notifications for specified origin.
if (this.notifications[origin]) {
for (var i in this.notifications[origin]) {
notifications.push(this.notifications[origin][i]);
}
}
return Promise.resolve(notifications);
},
taskGetAllCrossOrigin: function() {
if (DEBUG) { debug("Task, getting all whatever origin"); }
var notifications = [];
for (var origin in this.notifications) {
if (!this.notifications[origin]) {
continue;
}
for (var i in this.notifications[origin]) {
var notification = this.notifications[origin][i];
// Notifications without the alertName field cannot be resent by
// mozResendAllNotifications, so we just skip them. They will
// still be available to applications via Notification.get()
if (!('alertName' in notification)) {
continue;
}
notification.origin = origin;
notifications.push(notification);
}
}
return Promise.resolve(notifications);
},
taskSave: function(data) {
if (DEBUG) { debug("Task, saving"); }
var origin = data.origin;
var notification = data.notification;
if (!this.notifications[origin]) {
this.notifications[origin] = {};
this.byTag[origin] = {};
}
// We might have existing notification with this tag,
// if so we need to remove it before saving the new one.
if (notification.tag) {
var oldNotification = this.byTag[origin][notification.tag];
if (oldNotification) {
delete this.notifications[origin][oldNotification.id];
}
this.byTag[origin][notification.tag] = notification;
}
this.notifications[origin][notification.id] = notification;
return this.save();
},
taskDelete: function(data) {
if (DEBUG) { debug("Task, deleting"); }
var origin = data.origin;
var id = data.id;
if (!this.notifications[origin]) {
if (DEBUG) { debug("No notifications found for origin: " + origin); }
return Promise.resolve();
}
// Make sure we can find the notification to delete.
var oldNotification = this.notifications[origin][id];
if (!oldNotification) {
if (DEBUG) { debug("No notification found with id: " + id); }
return Promise.resolve();
}
if (oldNotification.tag) {
delete this.byTag[origin][oldNotification.tag];
}
delete this.notifications[origin][id];
return this.save();
}
};
NotificationDB.init();