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 }