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/. */
5 import { IndexedDBHelper } from "resource://gre/modules/IndexedDBHelper.sys.mjs";
9 ChromeUtils.defineLazyGetter(lazy, "console", () => {
10 let { ConsoleAPI } = ChromeUtils.importESModule(
11 "resource://gre/modules/Console.sys.mjs"
13 return new ConsoleAPI({
14 maxLogLevelPref: "dom.push.loglevel",
19 export function PushDB(dbName, dbVersion, dbStoreName, keyPath, model) {
20 lazy.console.debug("PushDB()");
21 this._dbStoreName = dbStoreName;
22 this._keyPath = keyPath;
25 // set the indexeddb database
26 this.initDBHelper(dbName, dbVersion, [dbStoreName]);
30 __proto__: IndexedDBHelper.prototype,
32 toPushRecord(record) {
36 return new this._model(record);
39 isValidRecord(record) {
42 typeof record.scope == "string" &&
43 typeof record.originAttributes == "string" &&
45 typeof record[this._keyPath] == "string"
49 upgradeSchema(aTransaction, aDb, aOldVersion) {
50 if (aOldVersion <= 3) {
51 // XXXnsm We haven't shipped Push during this upgrade, so I'm just going to throw old
52 // registrations away without even informing the app.
53 if (aDb.objectStoreNames.contains(this._dbStoreName)) {
54 aDb.deleteObjectStore(this._dbStoreName);
57 let objectStore = aDb.createObjectStore(this._dbStoreName, {
58 keyPath: this._keyPath,
61 // index to fetch records based on endpoints. used by unregister
62 objectStore.createIndex("pushEndpoint", "pushEndpoint", { unique: true });
64 // index to fetch records by identifiers.
65 // In the current security model, the originAttributes distinguish between
66 // different 'apps' on the same origin. Since ServiceWorkers are
67 // same-origin to the scope they are registered for, the attributes and
68 // scope are enough to reconstruct a valid principal.
69 objectStore.createIndex("identifiers", ["scope", "originAttributes"], {
72 objectStore.createIndex("originAttributes", "originAttributes", {
77 if (aOldVersion < 4) {
78 let objectStore = aTransaction.objectStore(this._dbStoreName);
80 // index to fetch active and expired registrations.
81 objectStore.createIndex("quota", "quota", { unique: false });
87 * The record to be added.
91 lazy.console.debug("put()", aRecord);
92 if (!this.isValidRecord(aRecord)) {
93 return Promise.reject(
95 "Scope, originAttributes, and quota are required! " +
96 JSON.stringify(aRecord)
101 return new Promise((resolve, reject) =>
106 aTxn.result = undefined;
108 aStore.put(aRecord).onsuccess = aEvent => {
110 "put: Request successful. Updated record",
113 aTxn.result = this.toPushRecord(aRecord);
124 * The ID of record to be deleted.
127 lazy.console.debug("delete()");
129 return new Promise((resolve, reject) =>
134 lazy.console.debug("delete: Removing record", aKeyID);
135 aStore.get(aKeyID).onsuccess = event => {
136 aTxn.result = this.toPushRecord(event.target.result);
137 aStore.delete(aKeyID);
146 // testFn(record) is called with a database record and should return true if
147 // that record should be deleted.
149 lazy.console.debug("clearIf()");
150 return new Promise((resolve, reject) =>
155 aTxn.result = undefined;
157 aStore.openCursor().onsuccess = event => {
158 let cursor = event.target.result;
160 let record = this.toPushRecord(cursor.value);
161 if (testFn(record)) {
162 let deleteRequest = cursor.delete();
163 deleteRequest.onerror = e => {
165 "clearIf: Error removing record",
181 getByPushEndpoint(aPushEndpoint) {
182 lazy.console.debug("getByPushEndpoint()");
184 return new Promise((resolve, reject) =>
189 aTxn.result = undefined;
191 let index = aStore.index("pushEndpoint");
192 index.get(aPushEndpoint).onsuccess = aEvent => {
193 let record = this.toPushRecord(aEvent.target.result);
194 lazy.console.debug("getByPushEndpoint: Got record", record);
195 aTxn.result = record;
205 lazy.console.debug("getByKeyID()");
207 return new Promise((resolve, reject) =>
212 aTxn.result = undefined;
214 aStore.get(aKeyID).onsuccess = aEvent => {
215 let record = this.toPushRecord(aEvent.target.result);
216 lazy.console.debug("getByKeyID: Got record", record);
217 aTxn.result = record;
227 * Iterates over all records associated with an origin.
229 * @param {String} origin The origin, matched as a prefix against the scope.
230 * @param {String} originAttributes Additional origin attributes. Requires
232 * @param {Function} callback A function with the signature `(record,
233 * cursor)`, called for each record. `record` is the registration, and
234 * `cursor` is an `IDBCursor`.
235 * @returns {Promise} Resolves once all records have been processed.
237 forEachOrigin(origin, originAttributes, callback) {
238 lazy.console.debug("forEachOrigin()");
240 return new Promise((resolve, reject) =>
245 aTxn.result = undefined;
247 let index = aStore.index("identifiers");
248 let range = IDBKeyRange.bound(
249 [origin, originAttributes],
250 [origin + "\x7f", originAttributes]
252 index.openCursor(range).onsuccess = event => {
253 let cursor = event.target.result;
257 callback(this.toPushRecord(cursor.value), cursor);
267 // Perform a unique match against { scope, originAttributes }
268 getByIdentifiers(aPageRecord) {
269 lazy.console.debug("getByIdentifiers()", aPageRecord);
270 if (!aPageRecord.scope || aPageRecord.originAttributes == undefined) {
272 "getByIdentifiers: Scope and originAttributes are required",
275 return Promise.reject(new TypeError("Invalid page record"));
278 return new Promise((resolve, reject) =>
283 aTxn.result = undefined;
285 let index = aStore.index("identifiers");
286 let request = index.get(
287 IDBKeyRange.only([aPageRecord.scope, aPageRecord.originAttributes])
289 request.onsuccess = aEvent => {
290 aTxn.result = this.toPushRecord(aEvent.target.result);
299 _getAllByKey(aKeyName, aKeyValue) {
300 return new Promise((resolve, reject) =>
305 aTxn.result = undefined;
307 let index = aStore.index(aKeyName);
308 // It seems ok to use getAll here, since unlike contacts or other
309 // high storage APIs, we don't expect more than a handful of
310 // registrations per domain, and usually only one.
311 let getAllReq = index.mozGetAll(aKeyValue);
312 getAllReq.onsuccess = aEvent => {
313 aTxn.result = aEvent.target.result.map(record =>
314 this.toPushRecord(record)
324 // aOriginAttributes must be a string!
325 getAllByOriginAttributes(aOriginAttributes) {
326 if (typeof aOriginAttributes !== "string") {
327 return Promise.reject("Expected string!");
329 return this._getAllByKey("originAttributes", aOriginAttributes);
333 lazy.console.debug("getAllKeyIDs()");
335 return new Promise((resolve, reject) =>
340 aTxn.result = undefined;
341 aStore.mozGetAll().onsuccess = event => {
342 aTxn.result = event.target.result.map(record =>
343 this.toPushRecord(record)
353 _getAllByPushQuota(range) {
354 lazy.console.debug("getAllByPushQuota()");
356 return new Promise((resolve, reject) =>
363 let index = aStore.index("quota");
364 index.openCursor(range).onsuccess = event => {
365 let cursor = event.target.result;
367 aTxn.result.push(this.toPushRecord(cursor.value));
379 lazy.console.debug("getAllUnexpired()");
380 return this._getAllByPushQuota(IDBKeyRange.lowerBound(1));
384 lazy.console.debug("getAllExpired()");
385 return this._getAllByPushQuota(IDBKeyRange.only(0));
389 * Updates an existing push registration.
391 * @param {String} aKeyID The registration ID.
392 * @param {Function} aUpdateFunc A function that receives the existing
393 * registration record as its argument, and returns a new record.
394 * @returns {Promise} A promise resolved with either the updated record.
395 * Rejects if the record does not exist, or the function returns an invalid
398 update(aKeyID, aUpdateFunc) {
399 return new Promise((resolve, reject) =>
404 aStore.get(aKeyID).onsuccess = aEvent => {
405 aTxn.result = undefined;
407 let record = aEvent.target.result;
409 throw new Error("Record " + aKeyID + " does not exist");
411 let newRecord = aUpdateFunc(this.toPushRecord(record));
412 if (!this.isValidRecord(newRecord)) {
414 "update: Ignoring invalid update",
418 throw new Error("Invalid update for record " + aKeyID);
420 function putRecord() {
421 let req = aStore.put(newRecord);
422 req.onsuccess = () => {
424 "update: Update successful",
428 aTxn.result = newRecord;
431 if (aKeyID === newRecord.keyID) {
434 // If we changed the primary key, delete the old record to avoid
435 // unique constraint errors.
436 aStore.delete(aKeyID).onsuccess = putRecord;
447 lazy.console.debug("drop()");
449 return new Promise((resolve, reject) =>
453 function txnCb(aTxn, aStore) {