tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

PushDB.sys.mjs (12977B)


      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/. */
      4 
      5 import { IndexedDBHelper } from "resource://gre/modules/IndexedDBHelper.sys.mjs";
      6 
      7 const lazy = {};
      8 
      9 ChromeUtils.defineLazyGetter(lazy, "console", () => {
     10  return console.createInstance({
     11    maxLogLevelPref: "dom.push.loglevel",
     12    prefix: "PushDB",
     13  });
     14 });
     15 
     16 export function PushDB(dbName, dbVersion, dbStoreName, keyPath, model) {
     17  lazy.console.debug("PushDB()");
     18  this._dbStoreName = dbStoreName;
     19  this._keyPath = keyPath;
     20  this._model = model;
     21 
     22  // set the indexeddb database
     23  this.initDBHelper(dbName, dbVersion, [dbStoreName]);
     24 }
     25 
     26 PushDB.prototype = {
     27  __proto__: IndexedDBHelper.prototype,
     28 
     29  toPushRecord(record) {
     30    if (!record) {
     31      return null;
     32    }
     33    return new this._model(record);
     34  },
     35 
     36  isValidRecord(record) {
     37    return (
     38      record &&
     39      typeof record.scope == "string" &&
     40      typeof record.originAttributes == "string" &&
     41      record.quota >= 0 &&
     42      typeof record[this._keyPath] == "string"
     43    );
     44  },
     45 
     46  upgradeSchema(aTransaction, aDb, aOldVersion) {
     47    if (aOldVersion <= 3) {
     48      // XXXnsm We haven't shipped Push during this upgrade, so I'm just going to throw old
     49      // registrations away without even informing the app.
     50      if (aDb.objectStoreNames.contains(this._dbStoreName)) {
     51        aDb.deleteObjectStore(this._dbStoreName);
     52      }
     53 
     54      let objectStore = aDb.createObjectStore(this._dbStoreName, {
     55        keyPath: this._keyPath,
     56      });
     57 
     58      // index to fetch records based on endpoints. used by unregister
     59      objectStore.createIndex("pushEndpoint", "pushEndpoint", { unique: true });
     60 
     61      // index to fetch records by identifiers.
     62      // In the current security model, the originAttributes distinguish between
     63      // different 'apps' on the same origin. Since ServiceWorkers are
     64      // same-origin to the scope they are registered for, the attributes and
     65      // scope are enough to reconstruct a valid principal.
     66      objectStore.createIndex("identifiers", ["scope", "originAttributes"], {
     67        unique: true,
     68      });
     69      objectStore.createIndex("originAttributes", "originAttributes", {
     70        unique: false,
     71      });
     72    }
     73 
     74    if (aOldVersion < 4) {
     75      let objectStore = aTransaction.objectStore(this._dbStoreName);
     76 
     77      // index to fetch active and expired registrations.
     78      objectStore.createIndex("quota", "quota", { unique: false });
     79    }
     80  },
     81 
     82  /**
     83   * @param aRecord
     84   *        The record to be added.
     85   */
     86 
     87  put(aRecord) {
     88    lazy.console.debug("put()", aRecord);
     89    if (!this.isValidRecord(aRecord)) {
     90      return Promise.reject(
     91        new TypeError(
     92          "Scope, originAttributes, and quota are required! " +
     93            JSON.stringify(aRecord)
     94        )
     95      );
     96    }
     97 
     98    return new Promise((resolve, reject) =>
     99      this.newTxn(
    100        "readwrite",
    101        this._dbStoreName,
    102        (aTxn, aStore) => {
    103          aTxn.result = undefined;
    104 
    105          aStore.put(aRecord).onsuccess = aEvent => {
    106            lazy.console.debug(
    107              "put: Request successful. Updated record",
    108              aEvent.target.result
    109            );
    110            aTxn.result = this.toPushRecord(aRecord);
    111          };
    112        },
    113        resolve,
    114        reject
    115      )
    116    );
    117  },
    118 
    119  /**
    120   * @param aKeyID
    121   *        The ID of record to be deleted.
    122   */
    123  delete(aKeyID) {
    124    lazy.console.debug("delete()");
    125 
    126    return new Promise((resolve, reject) =>
    127      this.newTxn(
    128        "readwrite",
    129        this._dbStoreName,
    130        (aTxn, aStore) => {
    131          lazy.console.debug("delete: Removing record", aKeyID);
    132          aStore.get(aKeyID).onsuccess = event => {
    133            aTxn.result = this.toPushRecord(event.target.result);
    134            aStore.delete(aKeyID);
    135          };
    136        },
    137        resolve,
    138        reject
    139      )
    140    );
    141  },
    142 
    143  // testFn(record) is called with a database record and should return true if
    144  // that record should be deleted.
    145  clearIf(testFn) {
    146    lazy.console.debug("clearIf()");
    147    return new Promise((resolve, reject) =>
    148      this.newTxn(
    149        "readwrite",
    150        this._dbStoreName,
    151        (aTxn, aStore) => {
    152          aTxn.result = undefined;
    153 
    154          aStore.openCursor().onsuccess = event => {
    155            let cursor = event.target.result;
    156            if (cursor) {
    157              let record = this.toPushRecord(cursor.value);
    158              if (testFn(record)) {
    159                let deleteRequest = cursor.delete();
    160                deleteRequest.onerror = e => {
    161                  lazy.console.error(
    162                    "clearIf: Error removing record",
    163                    record.keyID,
    164                    e
    165                  );
    166                };
    167              }
    168              cursor.continue();
    169            }
    170          };
    171        },
    172        resolve,
    173        reject
    174      )
    175    );
    176  },
    177 
    178  getByPushEndpoint(aPushEndpoint) {
    179    lazy.console.debug("getByPushEndpoint()");
    180 
    181    return new Promise((resolve, reject) =>
    182      this.newTxn(
    183        "readonly",
    184        this._dbStoreName,
    185        (aTxn, aStore) => {
    186          aTxn.result = undefined;
    187 
    188          let index = aStore.index("pushEndpoint");
    189          index.get(aPushEndpoint).onsuccess = aEvent => {
    190            let record = this.toPushRecord(aEvent.target.result);
    191            lazy.console.debug("getByPushEndpoint: Got record", record);
    192            aTxn.result = record;
    193          };
    194        },
    195        resolve,
    196        reject
    197      )
    198    );
    199  },
    200 
    201  getByKeyID(aKeyID) {
    202    lazy.console.debug("getByKeyID()");
    203 
    204    return new Promise((resolve, reject) =>
    205      this.newTxn(
    206        "readonly",
    207        this._dbStoreName,
    208        (aTxn, aStore) => {
    209          aTxn.result = undefined;
    210 
    211          aStore.get(aKeyID).onsuccess = aEvent => {
    212            let record = this.toPushRecord(aEvent.target.result);
    213            lazy.console.debug("getByKeyID: Got record", record);
    214            aTxn.result = record;
    215          };
    216        },
    217        resolve,
    218        reject
    219      )
    220    );
    221  },
    222 
    223  /**
    224   * Iterates over all records associated with an origin.
    225   *
    226   * @param {string} origin The origin, matched as a prefix against the scope.
    227   * @param {string} originAttributes Additional origin attributes. Requires
    228   *  an exact match.
    229   * @param {Function} callback A function with the signature `(record,
    230   *  cursor)`, called for each record. `record` is the registration, and
    231   *  `cursor` is an `IDBCursor`.
    232   * @returns {Promise} Resolves once all records have been processed.
    233   */
    234  forEachOrigin(origin, originAttributes, callback) {
    235    lazy.console.debug("forEachOrigin()");
    236 
    237    return new Promise((resolve, reject) =>
    238      this.newTxn(
    239        "readwrite",
    240        this._dbStoreName,
    241        (aTxn, aStore) => {
    242          aTxn.result = undefined;
    243 
    244          let index = aStore.index("identifiers");
    245          let range = IDBKeyRange.bound(
    246            [origin, originAttributes],
    247            [origin + "\x7f", originAttributes]
    248          );
    249          index.openCursor(range).onsuccess = event => {
    250            let cursor = event.target.result;
    251            if (!cursor) {
    252              return;
    253            }
    254            callback(this.toPushRecord(cursor.value), cursor);
    255            cursor.continue();
    256          };
    257        },
    258        resolve,
    259        reject
    260      )
    261    );
    262  },
    263 
    264  // Perform a unique match against { scope, originAttributes }
    265  getByIdentifiers(aPageRecord) {
    266    lazy.console.debug("getByIdentifiers()", aPageRecord);
    267    if (!aPageRecord.scope || aPageRecord.originAttributes == undefined) {
    268      lazy.console.error(
    269        "getByIdentifiers: Scope and originAttributes are required",
    270        aPageRecord
    271      );
    272      return Promise.reject(new TypeError("Invalid page record"));
    273    }
    274 
    275    return new Promise((resolve, reject) =>
    276      this.newTxn(
    277        "readonly",
    278        this._dbStoreName,
    279        (aTxn, aStore) => {
    280          aTxn.result = undefined;
    281 
    282          let index = aStore.index("identifiers");
    283          let request = index.get(
    284            IDBKeyRange.only([aPageRecord.scope, aPageRecord.originAttributes])
    285          );
    286          request.onsuccess = aEvent => {
    287            aTxn.result = this.toPushRecord(aEvent.target.result);
    288          };
    289        },
    290        resolve,
    291        reject
    292      )
    293    );
    294  },
    295 
    296  _getAllByKey(aKeyName, aKeyValue) {
    297    return new Promise((resolve, reject) =>
    298      this.newTxn(
    299        "readonly",
    300        this._dbStoreName,
    301        (aTxn, aStore) => {
    302          aTxn.result = undefined;
    303 
    304          let index = aStore.index(aKeyName);
    305          // It seems ok to use getAll here, since unlike contacts or other
    306          // high storage APIs, we don't expect more than a handful of
    307          // registrations per domain, and usually only one.
    308          let getAllReq = index.mozGetAll(aKeyValue);
    309          getAllReq.onsuccess = aEvent => {
    310            aTxn.result = aEvent.target.result.map(record =>
    311              this.toPushRecord(record)
    312            );
    313          };
    314        },
    315        resolve,
    316        reject
    317      )
    318    );
    319  },
    320 
    321  // aOriginAttributes must be a string!
    322  getAllByOriginAttributes(aOriginAttributes) {
    323    if (typeof aOriginAttributes !== "string") {
    324      return Promise.reject("Expected string!");
    325    }
    326    return this._getAllByKey("originAttributes", aOriginAttributes);
    327  },
    328 
    329  getAllKeyIDs() {
    330    lazy.console.debug("getAllKeyIDs()");
    331 
    332    return new Promise((resolve, reject) =>
    333      this.newTxn(
    334        "readonly",
    335        this._dbStoreName,
    336        (aTxn, aStore) => {
    337          aTxn.result = undefined;
    338          aStore.mozGetAll().onsuccess = event => {
    339            aTxn.result = event.target.result.map(record =>
    340              this.toPushRecord(record)
    341            );
    342          };
    343        },
    344        resolve,
    345        reject
    346      )
    347    );
    348  },
    349 
    350  _getAllByPushQuota(range) {
    351    lazy.console.debug("getAllByPushQuota()");
    352 
    353    return new Promise((resolve, reject) =>
    354      this.newTxn(
    355        "readonly",
    356        this._dbStoreName,
    357        (aTxn, aStore) => {
    358          aTxn.result = [];
    359 
    360          let index = aStore.index("quota");
    361          index.openCursor(range).onsuccess = event => {
    362            let cursor = event.target.result;
    363            if (cursor) {
    364              aTxn.result.push(this.toPushRecord(cursor.value));
    365              cursor.continue();
    366            }
    367          };
    368        },
    369        resolve,
    370        reject
    371      )
    372    );
    373  },
    374 
    375  getAllUnexpired() {
    376    lazy.console.debug("getAllUnexpired()");
    377    return this._getAllByPushQuota(IDBKeyRange.lowerBound(1));
    378  },
    379 
    380  getAllExpired() {
    381    lazy.console.debug("getAllExpired()");
    382    return this._getAllByPushQuota(IDBKeyRange.only(0));
    383  },
    384 
    385  /**
    386   * Updates an existing push registration.
    387   *
    388   * @param {string} aKeyID The registration ID.
    389   * @param {Function} aUpdateFunc A function that receives the existing
    390   *  registration record as its argument, and returns a new record.
    391   * @returns {Promise} A promise resolved with either the updated record.
    392   *  Rejects if the record does not exist, or the function returns an invalid
    393   *  record.
    394   */
    395  update(aKeyID, aUpdateFunc) {
    396    return new Promise((resolve, reject) =>
    397      this.newTxn(
    398        "readwrite",
    399        this._dbStoreName,
    400        (aTxn, aStore) => {
    401          aStore.get(aKeyID).onsuccess = aEvent => {
    402            aTxn.result = undefined;
    403 
    404            let record = aEvent.target.result;
    405            if (!record) {
    406              throw new Error("Record " + aKeyID + " does not exist");
    407            }
    408            let newRecord = aUpdateFunc(this.toPushRecord(record));
    409            if (!this.isValidRecord(newRecord)) {
    410              lazy.console.error(
    411                "update: Ignoring invalid update",
    412                aKeyID,
    413                newRecord
    414              );
    415              throw new Error("Invalid update for record " + aKeyID);
    416            }
    417            function putRecord() {
    418              let req = aStore.put(newRecord);
    419              req.onsuccess = () => {
    420                lazy.console.debug(
    421                  "update: Update successful",
    422                  aKeyID,
    423                  newRecord
    424                );
    425                aTxn.result = newRecord;
    426              };
    427            }
    428            if (aKeyID === newRecord.keyID) {
    429              putRecord();
    430            } else {
    431              // If we changed the primary key, delete the old record to avoid
    432              // unique constraint errors.
    433              aStore.delete(aKeyID).onsuccess = putRecord;
    434            }
    435          };
    436        },
    437        resolve,
    438        reject
    439      )
    440    );
    441  },
    442 
    443  drop() {
    444    lazy.console.debug("drop()");
    445 
    446    return new Promise((resolve, reject) =>
    447      this.newTxn(
    448        "readwrite",
    449        this._dbStoreName,
    450        function txnCb(aTxn, aStore) {
    451          aStore.clear();
    452        },
    453        resolve,
    454        reject
    455      )
    456    );
    457  },
    458 };