summaryrefslogtreecommitdiffstats
path: root/dom/push/PushDB.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'dom/push/PushDB.jsm')
-rw-r--r--dom/push/PushDB.jsm440
1 files changed, 440 insertions, 0 deletions
diff --git a/dom/push/PushDB.jsm b/dom/push/PushDB.jsm
new file mode 100644
index 000000000..02f623fa7
--- /dev/null
+++ b/dom/push/PushDB.jsm
@@ -0,0 +1,440 @@
+/* jshint moz: true, esnext: true */
+/* 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";
+
+const Cu = Components.utils;
+Cu.import("resource://gre/modules/IndexedDBHelper.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.importGlobalProperties(["indexedDB"]);
+
+this.EXPORTED_SYMBOLS = ["PushDB"];
+
+XPCOMUtils.defineLazyGetter(this, "console", () => {
+ let {ConsoleAPI} = Cu.import("resource://gre/modules/Console.jsm", {});
+ return new ConsoleAPI({
+ maxLogLevelPref: "dom.push.loglevel",
+ prefix: "PushDB",
+ });
+});
+
+this.PushDB = function PushDB(dbName, dbVersion, dbStoreName, keyPath, model) {
+ console.debug("PushDB()");
+ this._dbStoreName = dbStoreName;
+ this._keyPath = keyPath;
+ this._model = model;
+
+ // set the indexeddb database
+ this.initDBHelper(dbName, dbVersion,
+ [dbStoreName]);
+};
+
+this.PushDB.prototype = {
+ __proto__: IndexedDBHelper.prototype,
+
+ toPushRecord: function(record) {
+ if (!record) {
+ return;
+ }
+ return new this._model(record);
+ },
+
+ isValidRecord: function(record) {
+ return record && typeof record.scope == "string" &&
+ typeof record.originAttributes == "string" &&
+ record.quota >= 0 &&
+ typeof record[this._keyPath] == "string";
+ },
+
+ upgradeSchema: function(aTransaction, aDb, aOldVersion, aNewVersion) {
+ if (aOldVersion <= 3) {
+ //XXXnsm We haven't shipped Push during this upgrade, so I'm just going to throw old
+ //registrations away without even informing the app.
+ if (aDb.objectStoreNames.contains(this._dbStoreName)) {
+ aDb.deleteObjectStore(this._dbStoreName);
+ }
+
+ let objectStore = aDb.createObjectStore(this._dbStoreName,
+ { keyPath: this._keyPath });
+
+ // index to fetch records based on endpoints. used by unregister
+ objectStore.createIndex("pushEndpoint", "pushEndpoint", { unique: true });
+
+ // index to fetch records by identifiers.
+ // In the current security model, the originAttributes distinguish between
+ // different 'apps' on the same origin. Since ServiceWorkers are
+ // same-origin to the scope they are registered for, the attributes and
+ // scope are enough to reconstruct a valid principal.
+ objectStore.createIndex("identifiers", ["scope", "originAttributes"], { unique: true });
+ objectStore.createIndex("originAttributes", "originAttributes", { unique: false });
+ }
+
+ if (aOldVersion < 4) {
+ let objectStore = aTransaction.objectStore(this._dbStoreName);
+
+ // index to fetch active and expired registrations.
+ objectStore.createIndex("quota", "quota", { unique: false });
+ }
+ },
+
+ /*
+ * @param aRecord
+ * The record to be added.
+ */
+
+ put: function(aRecord) {
+ console.debug("put()", aRecord);
+ if (!this.isValidRecord(aRecord)) {
+ return Promise.reject(new TypeError(
+ "Scope, originAttributes, and quota are required! " +
+ JSON.stringify(aRecord)
+ )
+ );
+ }
+
+ return new Promise((resolve, reject) =>
+ this.newTxn(
+ "readwrite",
+ this._dbStoreName,
+ (aTxn, aStore) => {
+ aTxn.result = undefined;
+
+ aStore.put(aRecord).onsuccess = aEvent => {
+ console.debug("put: Request successful. Updated record",
+ aEvent.target.result);
+ aTxn.result = this.toPushRecord(aRecord);
+ };
+ },
+ resolve,
+ reject
+ )
+ );
+ },
+
+ /*
+ * @param aKeyID
+ * The ID of record to be deleted.
+ */
+ delete: function(aKeyID) {
+ console.debug("delete()");
+
+ return new Promise((resolve, reject) =>
+ this.newTxn(
+ "readwrite",
+ this._dbStoreName,
+ (aTxn, aStore) => {
+ console.debug("delete: Removing record", aKeyID);
+ aStore.get(aKeyID).onsuccess = event => {
+ aTxn.result = this.toPushRecord(event.target.result);
+ aStore.delete(aKeyID);
+ };
+ },
+ resolve,
+ reject
+ )
+ );
+ },
+
+ // testFn(record) is called with a database record and should return true if
+ // that record should be deleted.
+ clearIf: function(testFn) {
+ console.debug("clearIf()");
+ return new Promise((resolve, reject) =>
+ this.newTxn(
+ "readwrite",
+ this._dbStoreName,
+ (aTxn, aStore) => {
+ aTxn.result = undefined;
+
+ aStore.openCursor().onsuccess = event => {
+ let cursor = event.target.result;
+ if (cursor) {
+ let record = this.toPushRecord(cursor.value);
+ if (testFn(record)) {
+ let deleteRequest = cursor.delete();
+ deleteRequest.onerror = e => {
+ console.error("clearIf: Error removing record",
+ record.keyID, e);
+ }
+ }
+ cursor.continue();
+ }
+ }
+ },
+ resolve,
+ reject
+ )
+ );
+ },
+
+ getByPushEndpoint: function(aPushEndpoint) {
+ console.debug("getByPushEndpoint()");
+
+ return new Promise((resolve, reject) =>
+ this.newTxn(
+ "readonly",
+ this._dbStoreName,
+ (aTxn, aStore) => {
+ aTxn.result = undefined;
+
+ let index = aStore.index("pushEndpoint");
+ index.get(aPushEndpoint).onsuccess = aEvent => {
+ let record = this.toPushRecord(aEvent.target.result);
+ console.debug("getByPushEndpoint: Got record", record);
+ aTxn.result = record;
+ };
+ },
+ resolve,
+ reject
+ )
+ );
+ },
+
+ getByKeyID: function(aKeyID) {
+ console.debug("getByKeyID()");
+
+ return new Promise((resolve, reject) =>
+ this.newTxn(
+ "readonly",
+ this._dbStoreName,
+ (aTxn, aStore) => {
+ aTxn.result = undefined;
+
+ aStore.get(aKeyID).onsuccess = aEvent => {
+ let record = this.toPushRecord(aEvent.target.result);
+ console.debug("getByKeyID: Got record", record);
+ aTxn.result = record;
+ };
+ },
+ resolve,
+ reject
+ )
+ );
+ },
+
+ /**
+ * Iterates over all records associated with an origin.
+ *
+ * @param {String} origin The origin, matched as a prefix against the scope.
+ * @param {String} originAttributes Additional origin attributes. Requires
+ * an exact match.
+ * @param {Function} callback A function with the signature `(record,
+ * cursor)`, called for each record. `record` is the registration, and
+ * `cursor` is an `IDBCursor`.
+ * @returns {Promise} Resolves once all records have been processed.
+ */
+ forEachOrigin: function(origin, originAttributes, callback) {
+ console.debug("forEachOrigin()");
+
+ return new Promise((resolve, reject) =>
+ this.newTxn(
+ "readwrite",
+ this._dbStoreName,
+ (aTxn, aStore) => {
+ aTxn.result = undefined;
+
+ let index = aStore.index("identifiers");
+ let range = IDBKeyRange.bound(
+ [origin, originAttributes],
+ [origin + "\x7f", originAttributes]
+ );
+ index.openCursor(range).onsuccess = event => {
+ let cursor = event.target.result;
+ if (!cursor) {
+ return;
+ }
+ callback(this.toPushRecord(cursor.value), cursor);
+ cursor.continue();
+ };
+ },
+ resolve,
+ reject
+ )
+ );
+ },
+
+ // Perform a unique match against { scope, originAttributes }
+ getByIdentifiers: function(aPageRecord) {
+ console.debug("getByIdentifiers()", aPageRecord);
+ if (!aPageRecord.scope || aPageRecord.originAttributes == undefined) {
+ console.error("getByIdentifiers: Scope and originAttributes are required",
+ aPageRecord);
+ return Promise.reject(new TypeError("Invalid page record"));
+ }
+
+ return new Promise((resolve, reject) =>
+ this.newTxn(
+ "readonly",
+ this._dbStoreName,
+ (aTxn, aStore) => {
+ aTxn.result = undefined;
+
+ let index = aStore.index("identifiers");
+ let request = index.get(IDBKeyRange.only([aPageRecord.scope, aPageRecord.originAttributes]));
+ request.onsuccess = aEvent => {
+ aTxn.result = this.toPushRecord(aEvent.target.result);
+ };
+ },
+ resolve,
+ reject
+ )
+ );
+ },
+
+ _getAllByKey: function(aKeyName, aKeyValue) {
+ return new Promise((resolve, reject) =>
+ this.newTxn(
+ "readonly",
+ this._dbStoreName,
+ (aTxn, aStore) => {
+ aTxn.result = undefined;
+
+ let index = aStore.index(aKeyName);
+ // It seems ok to use getAll here, since unlike contacts or other
+ // high storage APIs, we don't expect more than a handful of
+ // registrations per domain, and usually only one.
+ let getAllReq = index.mozGetAll(aKeyValue);
+ getAllReq.onsuccess = aEvent => {
+ aTxn.result = aEvent.target.result.map(
+ record => this.toPushRecord(record));
+ };
+ },
+ resolve,
+ reject
+ )
+ );
+ },
+
+ // aOriginAttributes must be a string!
+ getAllByOriginAttributes: function(aOriginAttributes) {
+ if (typeof aOriginAttributes !== "string") {
+ return Promise.reject("Expected string!");
+ }
+ return this._getAllByKey("originAttributes", aOriginAttributes);
+ },
+
+ getAllKeyIDs: function() {
+ console.debug("getAllKeyIDs()");
+
+ return new Promise((resolve, reject) =>
+ this.newTxn(
+ "readonly",
+ this._dbStoreName,
+ (aTxn, aStore) => {
+ aTxn.result = undefined;
+ aStore.mozGetAll().onsuccess = event => {
+ aTxn.result = event.target.result.map(
+ record => this.toPushRecord(record));
+ };
+ },
+ resolve,
+ reject
+ )
+ );
+ },
+
+ _getAllByPushQuota: function(range) {
+ console.debug("getAllByPushQuota()");
+
+ return new Promise((resolve, reject) =>
+ this.newTxn(
+ "readonly",
+ this._dbStoreName,
+ (aTxn, aStore) => {
+ aTxn.result = [];
+
+ let index = aStore.index("quota");
+ index.openCursor(range).onsuccess = event => {
+ let cursor = event.target.result;
+ if (cursor) {
+ aTxn.result.push(this.toPushRecord(cursor.value));
+ cursor.continue();
+ }
+ };
+ },
+ resolve,
+ reject
+ )
+ );
+ },
+
+ getAllUnexpired: function() {
+ console.debug("getAllUnexpired()");
+ return this._getAllByPushQuota(IDBKeyRange.lowerBound(1));
+ },
+
+ getAllExpired: function() {
+ console.debug("getAllExpired()");
+ return this._getAllByPushQuota(IDBKeyRange.only(0));
+ },
+
+ /**
+ * Updates an existing push registration.
+ *
+ * @param {String} aKeyID The registration ID.
+ * @param {Function} aUpdateFunc A function that receives the existing
+ * registration record as its argument, and returns a new record.
+ * @returns {Promise} A promise resolved with either the updated record.
+ * Rejects if the record does not exist, or the function returns an invalid
+ * record.
+ */
+ update: function(aKeyID, aUpdateFunc) {
+ return new Promise((resolve, reject) =>
+ this.newTxn(
+ "readwrite",
+ this._dbStoreName,
+ (aTxn, aStore) => {
+ aStore.get(aKeyID).onsuccess = aEvent => {
+ aTxn.result = undefined;
+
+ let record = aEvent.target.result;
+ if (!record) {
+ throw new Error("Record " + aKeyID + " does not exist");
+ }
+ let newRecord = aUpdateFunc(this.toPushRecord(record));
+ if (!this.isValidRecord(newRecord)) {
+ console.error("update: Ignoring invalid update",
+ aKeyID, newRecord);
+ throw new Error("Invalid update for record " + aKeyID);
+ }
+ function putRecord() {
+ let req = aStore.put(newRecord);
+ req.onsuccess = aEvent => {
+ console.debug("update: Update successful", aKeyID, newRecord);
+ aTxn.result = newRecord;
+ };
+ }
+ if (aKeyID === newRecord.keyID) {
+ putRecord();
+ } else {
+ // If we changed the primary key, delete the old record to avoid
+ // unique constraint errors.
+ aStore.delete(aKeyID).onsuccess = putRecord;
+ }
+ };
+ },
+ resolve,
+ reject
+ )
+ );
+ },
+
+ drop: function() {
+ console.debug("drop()");
+
+ return new Promise((resolve, reject) =>
+ this.newTxn(
+ "readwrite",
+ this._dbStoreName,
+ function txnCb(aTxn, aStore) {
+ aStore.clear();
+ },
+ resolve,
+ reject
+ )
+ );
+ },
+};