tor-browser

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

ASRouterStorage.sys.mjs (10541B)


      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 const lazy = {};
      6 
      7 ChromeUtils.defineESModuleGetters(lazy, {
      8  IndexedDB: "resource://gre/modules/IndexedDB.sys.mjs",
      9  ProfilesDatastoreService:
     10    "moz-src:///toolkit/profile/ProfilesDatastoreService.sys.mjs",
     11  ASRouterPreferences:
     12    "resource:///modules/asrouter/ASRouterPreferences.sys.mjs",
     13 });
     14 
     15 export class ASRouterStorage {
     16  /**
     17   * @param storeNames Array of strings used to create all the required stores
     18   */
     19  constructor({ storeNames, telemetry }) {
     20    if (!storeNames) {
     21      throw new Error("storeNames required");
     22    }
     23 
     24    this.dbName = "ActivityStream";
     25    this.dbVersion = 3;
     26    this.storeNames = storeNames;
     27    this.telemetry = telemetry;
     28  }
     29 
     30  get db() {
     31    return this._db || (this._db = this.createOrOpenDb());
     32  }
     33 
     34  /**
     35   * Public method that binds the store required by the consumer and exposes
     36   * the private db getters and setters.
     37   *
     38   * @param storeName String name of desired store
     39   */
     40  getDbTable(storeName) {
     41    if (this.storeNames.includes(storeName)) {
     42      return {
     43        get: this._get.bind(this, storeName),
     44        getAll: this._getAll.bind(this, storeName),
     45        getAllKeys: this._getAllKeys.bind(this, storeName),
     46        set: this._set.bind(this, storeName),
     47        getSharedMessageImpressions:
     48          this.getSharedMessageImpressions.bind(this),
     49        getSharedMessageBlocklist: this.getSharedMessageBlocklist.bind(this),
     50        setSharedMessageImpressions:
     51          this.setSharedMessageImpressions.bind(this),
     52        setSharedMessageBlocked: this.setSharedMessageBlocked.bind(this),
     53      };
     54    }
     55 
     56    throw new Error(`Store name ${storeName} does not exist.`);
     57  }
     58 
     59  async _getStore(storeName) {
     60    return (await this.db).objectStore(storeName, "readwrite");
     61  }
     62 
     63  _get(storeName, key) {
     64    return this._requestWrapper(async () =>
     65      (await this._getStore(storeName)).get(key)
     66    );
     67  }
     68 
     69  _getAll(storeName) {
     70    return this._requestWrapper(async () =>
     71      (await this._getStore(storeName)).getAll()
     72    );
     73  }
     74 
     75  _getAllKeys(storeName) {
     76    return this._requestWrapper(async () =>
     77      (await this._getStore(storeName)).getAllKeys()
     78    );
     79  }
     80 
     81  _set(storeName, key, value) {
     82    return this._requestWrapper(async () =>
     83      (await this._getStore(storeName)).put(value, key)
     84    );
     85  }
     86 
     87  _openDatabase() {
     88    return lazy.IndexedDB.open(this.dbName, this.dbVersion, db => {
     89      // If provided with array of objectStore names we need to create all the
     90      // individual stores
     91      this.storeNames.forEach(store => {
     92        if (!db.objectStoreNames.contains(store)) {
     93          this._requestWrapper(() => db.createObjectStore(store));
     94        }
     95      });
     96    });
     97  }
     98 
     99  /**
    100   * Open a db (with this.dbName) if it exists. If it does not exist, create it.
    101   * If an error occurs, deleted the db and attempt to re-create it.
    102   *
    103   * @returns Promise that resolves with a db instance
    104   */
    105  async createOrOpenDb() {
    106    try {
    107      const db = await this._openDatabase();
    108      return db;
    109    } catch (e) {
    110      if (this.telemetry) {
    111        this.telemetry.handleUndesiredEvent({ event: "INDEXEDDB_OPEN_FAILED" });
    112      }
    113      await lazy.IndexedDB.deleteDatabase(this.dbName);
    114      return this._openDatabase();
    115    }
    116  }
    117 
    118  async _requestWrapper(request) {
    119    let result = null;
    120    try {
    121      result = await request();
    122    } catch (e) {
    123      if (this.telemetry) {
    124        this.telemetry.handleUndesiredEvent({ event: "TRANSACTION_FAILED" });
    125      }
    126      throw e;
    127    }
    128 
    129    return result;
    130  }
    131 
    132  /**
    133   * Gets all of the message impression data
    134   *
    135   * @returns {object|null} All multiprofile message impressions or null if error occurs
    136   */
    137  async getSharedMessageImpressions() {
    138    try {
    139      const conn = await lazy.ProfilesDatastoreService.getConnection();
    140      if (!conn) {
    141        return null;
    142      }
    143      const rows = await conn.executeCached(
    144        `SELECT messageId, json(impressions) AS impressions FROM MessagingSystemMessageImpressions;`
    145      );
    146 
    147      if (rows.length === 0) {
    148        return null;
    149      }
    150 
    151      const impressionsData = {};
    152 
    153      for (const row of rows) {
    154        const messageId = row.getResultByName("messageId");
    155        const impressions = JSON.parse(row.getResultByName("impressions"));
    156 
    157        impressionsData[messageId] = impressions;
    158      }
    159 
    160      return impressionsData;
    161    } catch (e) {
    162      lazy.ASRouterPreferences.console.error(
    163        `ASRouterStorage: Failed reading from MessagingSystemMessageImpressions`,
    164        e
    165      );
    166      if (this.telemetry) {
    167        this.telemetry.handleUndesiredEvent({
    168          event: "SHARED_DB_READ_FAILED",
    169        });
    170      }
    171      return null;
    172    }
    173  }
    174 
    175  /**
    176   * Gets the message blocklist
    177   *
    178   * @returns {Array|null} The message blocklist, or null if error occurred
    179   */
    180  async getSharedMessageBlocklist() {
    181    try {
    182      const conn = await lazy.ProfilesDatastoreService.getConnection();
    183      if (!conn) {
    184        return null;
    185      }
    186      const rows = await conn.executeCached(
    187        `SELECT messageId FROM MessagingSystemMessageBlocklist;`
    188      );
    189 
    190      return rows.map(row => row.getResultByName("messageId"));
    191    } catch (e) {
    192      lazy.ASRouterPreferences.console.error(
    193        `ASRouterStorage: Failed reading from MessagingSystemMessageBlocklist`,
    194        e
    195      );
    196      if (this.telemetry) {
    197        this.telemetry.handleUndesiredEvent({
    198          event: "SHARED_DB_READ_FAILED",
    199        });
    200      }
    201      return null;
    202    }
    203  }
    204 
    205  /**
    206   * Set the message impressions for a given message ID
    207   *
    208   * @param {string} messageId - The message ID to set the impressions for
    209   * @param {Array|null} impressions - The new value of "impressions" (an array of
    210   *  impression data or an emtpy array, or null to delete)
    211   * @returns {boolean} Success status
    212   */
    213  async setSharedMessageImpressions(messageId, impressions) {
    214    let success = true;
    215    try {
    216      const conn = await lazy.ProfilesDatastoreService.getConnection();
    217      if (!conn) {
    218        return false;
    219      }
    220      if (!messageId) {
    221        throw new Error(
    222          "Failed attempt to set shared message impressions with no message ID."
    223        );
    224      }
    225 
    226      // If impressions is falsy, delete the row (an empty array may indicate a custom
    227      // frequency cap; we still want to track the message ID in that case.)
    228      if (!impressions) {
    229        await conn.executeBeforeShutdown(
    230          "ASRouter: setSharedMessageImpressions",
    231          async () => {
    232            await conn.executeCached(
    233              `DELETE FROM MessagingSystemMessageImpressions WHERE messageId = :messageId;`,
    234              {
    235                messageId,
    236              }
    237            );
    238          }
    239        );
    240      } else {
    241        await conn.executeBeforeShutdown(
    242          "ASRouter: setSharedMessageImpressions",
    243          async () => {
    244            await conn.executeCached(
    245              `INSERT INTO MessagingSystemMessageImpressions (messageId, impressions) VALUES (
    246                :messageId,
    247                jsonb(:impressions)
    248              )
    249              ON CONFLICT (messageId) DO UPDATE SET impressions = excluded.impressions;`,
    250              {
    251                messageId,
    252                impressions: JSON.stringify(impressions),
    253              }
    254            );
    255          }
    256        );
    257      }
    258 
    259      lazy.ProfilesDatastoreService.notify();
    260    } catch (e) {
    261      lazy.ASRouterPreferences.console.error(
    262        `ASRouterStorage: Failed writing to MessagingSystemMessageImpressions`,
    263        e
    264      );
    265      if (this.telemetry) {
    266        this.telemetry.handleUndesiredEvent({
    267          event: "SHARED_DB_WRITE_FAILED",
    268        });
    269      }
    270      success = false;
    271    }
    272 
    273    return success;
    274  }
    275 
    276  /**
    277   * Adds a message ID to the blocklist and removes impressions
    278   * for that message ID from the impressions table when isBlocked is true
    279   * and deletes message ID from the blocklist when isBlocked is false
    280   *
    281   * @param {string} messageId - The message ID to set the blocked status for
    282   * @param {boolean} [isBlocked=true] - If the message should be blocked (true) or unblocked (false)
    283   * @returns {boolean} Success status
    284   */
    285  async setSharedMessageBlocked(messageId, isBlocked = true) {
    286    let success = true;
    287    if (isBlocked) {
    288      // Block the message, and clear impressions
    289      try {
    290        const conn = await lazy.ProfilesDatastoreService.getConnection();
    291        if (!conn) {
    292          return false;
    293        }
    294        await conn.executeTransaction(async () => {
    295          await conn.executeCached(
    296            `INSERT INTO MessagingSystemMessageBlocklist (messageId)
    297                VALUES (:messageId);`,
    298            {
    299              messageId,
    300            }
    301          );
    302          await conn.executeCached(
    303            `DELETE FROM MessagingSystemMessageImpressions
    304                WHERE messageId = :messageId;`,
    305            {
    306              messageId,
    307            }
    308          );
    309        });
    310      } catch (e) {
    311        lazy.ASRouterPreferences.console.error(
    312          `ASRouterStorage: Failed writing to MessagingSystemMessageBlocklist`,
    313          e
    314        );
    315        if (this.telemetry) {
    316          this.telemetry.handleUndesiredEvent({
    317            event: "SHARED_DB_WRITE_FAILED",
    318          });
    319        }
    320        success = false;
    321      }
    322    } else {
    323      // Unblock the message
    324      try {
    325        const conn = await lazy.ProfilesDatastoreService.getConnection();
    326        if (!conn) {
    327          return false;
    328        }
    329        await conn.executeBeforeShutdown(
    330          "ASRouter: setSharedMessageBlocked",
    331          async () => {
    332            await conn.executeCached(
    333              `DELETE FROM MessagingSystemMessageBlocklist WHERE messageId = :messageId;`,
    334              {
    335                messageId,
    336              }
    337            );
    338          }
    339        );
    340      } catch (e) {
    341        lazy.ASRouterPreferences.console.error(
    342          `ASRouterStorage: Failed writing to MessagingSystemMessageBlocklist`,
    343          e
    344        );
    345        if (this.telemetry) {
    346          this.telemetry.handleUndesiredEvent({
    347            event: "SHARED_DB_WRITE_FAILED",
    348          });
    349        }
    350        success = false;
    351      }
    352    }
    353 
    354    lazy.ProfilesDatastoreService.notify();
    355    return success;
    356  }
    357 }
    358 
    359 export function getDefaultOptions(options) {
    360  return { collapsed: !!options.collapsed };
    361 }