diff options
Diffstat (limited to 'dom/push/PushDB.sys.mjs')
-rw-r--r-- | dom/push/PushDB.sys.mjs | 462 |
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 + ) + ); + }, +}; |