summaryrefslogtreecommitdiffstats
path: root/dom/push/PushDB.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'dom/push/PushDB.sys.mjs')
-rw-r--r--dom/push/PushDB.sys.mjs462
1 files changed, 462 insertions, 0 deletions
diff --git a/dom/push/PushDB.sys.mjs b/dom/push/PushDB.sys.mjs
new file mode 100644
index 0000000000..ee2ef9e5c1
--- /dev/null
+++ b/dom/push/PushDB.sys.mjs
@@ -0,0 +1,462 @@
+/* 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/. */
+
+import { IndexedDBHelper } from "resource://gre/modules/IndexedDBHelper.sys.mjs";
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+XPCOMUtils.defineLazyGetter(lazy, "console", () => {
+ let { ConsoleAPI } = ChromeUtils.importESModule(
+ "resource://gre/modules/Console.sys.mjs"
+ );
+ return new ConsoleAPI({
+ maxLogLevelPref: "dom.push.loglevel",
+ prefix: "PushDB",
+ });
+});
+
+export function PushDB(dbName, dbVersion, dbStoreName, keyPath, model) {
+ lazy.console.debug("PushDB()");
+ this._dbStoreName = dbStoreName;
+ this._keyPath = keyPath;
+ this._model = model;
+
+ // set the indexeddb database
+ this.initDBHelper(dbName, dbVersion, [dbStoreName]);
+}
+
+PushDB.prototype = {
+ __proto__: IndexedDBHelper.prototype,
+
+ toPushRecord(record) {
+ if (!record) {
+ return null;
+ }
+ return new this._model(record);
+ },
+
+ isValidRecord(record) {
+ return (
+ record &&
+ typeof record.scope == "string" &&
+ typeof record.originAttributes == "string" &&
+ record.quota >= 0 &&
+ typeof record[this._keyPath] == "string"
+ );
+ },
+
+ upgradeSchema(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(aRecord) {
+ lazy.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 => {
+ lazy.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(aKeyID) {
+ lazy.console.debug("delete()");
+
+ return new Promise((resolve, reject) =>
+ this.newTxn(
+ "readwrite",
+ this._dbStoreName,
+ (aTxn, aStore) => {
+ lazy.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(testFn) {
+ lazy.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 => {
+ lazy.console.error(
+ "clearIf: Error removing record",
+ record.keyID,
+ e
+ );
+ };
+ }
+ cursor.continue();
+ }
+ };
+ },
+ resolve,
+ reject
+ )
+ );
+ },
+
+ getByPushEndpoint(aPushEndpoint) {
+ lazy.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);
+ lazy.console.debug("getByPushEndpoint: Got record", record);
+ aTxn.result = record;
+ };
+ },
+ resolve,
+ reject
+ )
+ );
+ },
+
+ getByKeyID(aKeyID) {
+ lazy.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);
+ lazy.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(origin, originAttributes, callback) {
+ lazy.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(aPageRecord) {
+ lazy.console.debug("getByIdentifiers()", aPageRecord);
+ if (!aPageRecord.scope || aPageRecord.originAttributes == undefined) {
+ lazy.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(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(aOriginAttributes) {
+ if (typeof aOriginAttributes !== "string") {
+ return Promise.reject("Expected string!");
+ }
+ return this._getAllByKey("originAttributes", aOriginAttributes);
+ },
+
+ getAllKeyIDs() {
+ lazy.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(range) {
+ lazy.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() {
+ lazy.console.debug("getAllUnexpired()");
+ return this._getAllByPushQuota(IDBKeyRange.lowerBound(1));
+ },
+
+ getAllExpired() {
+ lazy.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(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)) {
+ lazy.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 => {
+ lazy.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() {
+ lazy.console.debug("drop()");
+
+ return new Promise((resolve, reject) =>
+ this.newTxn(
+ "readwrite",
+ this._dbStoreName,
+ function txnCb(aTxn, aStore) {
+ aStore.clear();
+ },
+ resolve,
+ reject
+ )
+ );
+ },
+};