Merge mozilla-central to autoland. a=merge CLOSED TREE
[gecko.git] / dom / push / PushDB.sys.mjs
blobaa35093200fc5b33c3854a837855673232fcc90f
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";
7 const lazy = {};
9 ChromeUtils.defineLazyGetter(lazy, "console", () => {
10   let { ConsoleAPI } = ChromeUtils.importESModule(
11     "resource://gre/modules/Console.sys.mjs"
12   );
13   return new ConsoleAPI({
14     maxLogLevelPref: "dom.push.loglevel",
15     prefix: "PushDB",
16   });
17 });
19 export function PushDB(dbName, dbVersion, dbStoreName, keyPath, model) {
20   lazy.console.debug("PushDB()");
21   this._dbStoreName = dbStoreName;
22   this._keyPath = keyPath;
23   this._model = model;
25   // set the indexeddb database
26   this.initDBHelper(dbName, dbVersion, [dbStoreName]);
29 PushDB.prototype = {
30   __proto__: IndexedDBHelper.prototype,
32   toPushRecord(record) {
33     if (!record) {
34       return null;
35     }
36     return new this._model(record);
37   },
39   isValidRecord(record) {
40     return (
41       record &&
42       typeof record.scope == "string" &&
43       typeof record.originAttributes == "string" &&
44       record.quota >= 0 &&
45       typeof record[this._keyPath] == "string"
46     );
47   },
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);
55       }
57       let objectStore = aDb.createObjectStore(this._dbStoreName, {
58         keyPath: this._keyPath,
59       });
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"], {
70         unique: true,
71       });
72       objectStore.createIndex("originAttributes", "originAttributes", {
73         unique: false,
74       });
75     }
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 });
82     }
83   },
85   /*
86    * @param aRecord
87    *        The record to be added.
88    */
90   put(aRecord) {
91     lazy.console.debug("put()", aRecord);
92     if (!this.isValidRecord(aRecord)) {
93       return Promise.reject(
94         new TypeError(
95           "Scope, originAttributes, and quota are required! " +
96             JSON.stringify(aRecord)
97         )
98       );
99     }
101     return new Promise((resolve, reject) =>
102       this.newTxn(
103         "readwrite",
104         this._dbStoreName,
105         (aTxn, aStore) => {
106           aTxn.result = undefined;
108           aStore.put(aRecord).onsuccess = aEvent => {
109             lazy.console.debug(
110               "put: Request successful. Updated record",
111               aEvent.target.result
112             );
113             aTxn.result = this.toPushRecord(aRecord);
114           };
115         },
116         resolve,
117         reject
118       )
119     );
120   },
122   /*
123    * @param aKeyID
124    *        The ID of record to be deleted.
125    */
126   delete(aKeyID) {
127     lazy.console.debug("delete()");
129     return new Promise((resolve, reject) =>
130       this.newTxn(
131         "readwrite",
132         this._dbStoreName,
133         (aTxn, aStore) => {
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);
138           };
139         },
140         resolve,
141         reject
142       )
143     );
144   },
146   // testFn(record) is called with a database record and should return true if
147   // that record should be deleted.
148   clearIf(testFn) {
149     lazy.console.debug("clearIf()");
150     return new Promise((resolve, reject) =>
151       this.newTxn(
152         "readwrite",
153         this._dbStoreName,
154         (aTxn, aStore) => {
155           aTxn.result = undefined;
157           aStore.openCursor().onsuccess = event => {
158             let cursor = event.target.result;
159             if (cursor) {
160               let record = this.toPushRecord(cursor.value);
161               if (testFn(record)) {
162                 let deleteRequest = cursor.delete();
163                 deleteRequest.onerror = e => {
164                   lazy.console.error(
165                     "clearIf: Error removing record",
166                     record.keyID,
167                     e
168                   );
169                 };
170               }
171               cursor.continue();
172             }
173           };
174         },
175         resolve,
176         reject
177       )
178     );
179   },
181   getByPushEndpoint(aPushEndpoint) {
182     lazy.console.debug("getByPushEndpoint()");
184     return new Promise((resolve, reject) =>
185       this.newTxn(
186         "readonly",
187         this._dbStoreName,
188         (aTxn, aStore) => {
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;
196           };
197         },
198         resolve,
199         reject
200       )
201     );
202   },
204   getByKeyID(aKeyID) {
205     lazy.console.debug("getByKeyID()");
207     return new Promise((resolve, reject) =>
208       this.newTxn(
209         "readonly",
210         this._dbStoreName,
211         (aTxn, aStore) => {
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;
218           };
219         },
220         resolve,
221         reject
222       )
223     );
224   },
226   /**
227    * Iterates over all records associated with an origin.
228    *
229    * @param {String} origin The origin, matched as a prefix against the scope.
230    * @param {String} originAttributes Additional origin attributes. Requires
231    *  an exact match.
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.
236    */
237   forEachOrigin(origin, originAttributes, callback) {
238     lazy.console.debug("forEachOrigin()");
240     return new Promise((resolve, reject) =>
241       this.newTxn(
242         "readwrite",
243         this._dbStoreName,
244         (aTxn, aStore) => {
245           aTxn.result = undefined;
247           let index = aStore.index("identifiers");
248           let range = IDBKeyRange.bound(
249             [origin, originAttributes],
250             [origin + "\x7f", originAttributes]
251           );
252           index.openCursor(range).onsuccess = event => {
253             let cursor = event.target.result;
254             if (!cursor) {
255               return;
256             }
257             callback(this.toPushRecord(cursor.value), cursor);
258             cursor.continue();
259           };
260         },
261         resolve,
262         reject
263       )
264     );
265   },
267   // Perform a unique match against { scope, originAttributes }
268   getByIdentifiers(aPageRecord) {
269     lazy.console.debug("getByIdentifiers()", aPageRecord);
270     if (!aPageRecord.scope || aPageRecord.originAttributes == undefined) {
271       lazy.console.error(
272         "getByIdentifiers: Scope and originAttributes are required",
273         aPageRecord
274       );
275       return Promise.reject(new TypeError("Invalid page record"));
276     }
278     return new Promise((resolve, reject) =>
279       this.newTxn(
280         "readonly",
281         this._dbStoreName,
282         (aTxn, aStore) => {
283           aTxn.result = undefined;
285           let index = aStore.index("identifiers");
286           let request = index.get(
287             IDBKeyRange.only([aPageRecord.scope, aPageRecord.originAttributes])
288           );
289           request.onsuccess = aEvent => {
290             aTxn.result = this.toPushRecord(aEvent.target.result);
291           };
292         },
293         resolve,
294         reject
295       )
296     );
297   },
299   _getAllByKey(aKeyName, aKeyValue) {
300     return new Promise((resolve, reject) =>
301       this.newTxn(
302         "readonly",
303         this._dbStoreName,
304         (aTxn, aStore) => {
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)
315             );
316           };
317         },
318         resolve,
319         reject
320       )
321     );
322   },
324   // aOriginAttributes must be a string!
325   getAllByOriginAttributes(aOriginAttributes) {
326     if (typeof aOriginAttributes !== "string") {
327       return Promise.reject("Expected string!");
328     }
329     return this._getAllByKey("originAttributes", aOriginAttributes);
330   },
332   getAllKeyIDs() {
333     lazy.console.debug("getAllKeyIDs()");
335     return new Promise((resolve, reject) =>
336       this.newTxn(
337         "readonly",
338         this._dbStoreName,
339         (aTxn, aStore) => {
340           aTxn.result = undefined;
341           aStore.mozGetAll().onsuccess = event => {
342             aTxn.result = event.target.result.map(record =>
343               this.toPushRecord(record)
344             );
345           };
346         },
347         resolve,
348         reject
349       )
350     );
351   },
353   _getAllByPushQuota(range) {
354     lazy.console.debug("getAllByPushQuota()");
356     return new Promise((resolve, reject) =>
357       this.newTxn(
358         "readonly",
359         this._dbStoreName,
360         (aTxn, aStore) => {
361           aTxn.result = [];
363           let index = aStore.index("quota");
364           index.openCursor(range).onsuccess = event => {
365             let cursor = event.target.result;
366             if (cursor) {
367               aTxn.result.push(this.toPushRecord(cursor.value));
368               cursor.continue();
369             }
370           };
371         },
372         resolve,
373         reject
374       )
375     );
376   },
378   getAllUnexpired() {
379     lazy.console.debug("getAllUnexpired()");
380     return this._getAllByPushQuota(IDBKeyRange.lowerBound(1));
381   },
383   getAllExpired() {
384     lazy.console.debug("getAllExpired()");
385     return this._getAllByPushQuota(IDBKeyRange.only(0));
386   },
388   /**
389    * Updates an existing push registration.
390    *
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
396    *  record.
397    */
398   update(aKeyID, aUpdateFunc) {
399     return new Promise((resolve, reject) =>
400       this.newTxn(
401         "readwrite",
402         this._dbStoreName,
403         (aTxn, aStore) => {
404           aStore.get(aKeyID).onsuccess = aEvent => {
405             aTxn.result = undefined;
407             let record = aEvent.target.result;
408             if (!record) {
409               throw new Error("Record " + aKeyID + " does not exist");
410             }
411             let newRecord = aUpdateFunc(this.toPushRecord(record));
412             if (!this.isValidRecord(newRecord)) {
413               lazy.console.error(
414                 "update: Ignoring invalid update",
415                 aKeyID,
416                 newRecord
417               );
418               throw new Error("Invalid update for record " + aKeyID);
419             }
420             function putRecord() {
421               let req = aStore.put(newRecord);
422               req.onsuccess = () => {
423                 lazy.console.debug(
424                   "update: Update successful",
425                   aKeyID,
426                   newRecord
427                 );
428                 aTxn.result = newRecord;
429               };
430             }
431             if (aKeyID === newRecord.keyID) {
432               putRecord();
433             } else {
434               // If we changed the primary key, delete the old record to avoid
435               // unique constraint errors.
436               aStore.delete(aKeyID).onsuccess = putRecord;
437             }
438           };
439         },
440         resolve,
441         reject
442       )
443     );
444   },
446   drop() {
447     lazy.console.debug("drop()");
449     return new Promise((resolve, reject) =>
450       this.newTxn(
451         "readwrite",
452         this._dbStoreName,
453         function txnCb(aTxn, aStore) {
454           aStore.clear();
455         },
456         resolve,
457         reject
458       )
459     );
460   },