extension-storage.sys.mjs (8290B)
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 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 const STORAGE_VERSION = 1; // This needs to be kept in-sync with the rust storage version 6 7 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 8 import { BridgedEngine } from "resource://services-sync/bridged_engine.sys.mjs"; 9 import { SyncEngine, Tracker } from "resource://services-sync/engines.sys.mjs"; 10 11 const lazy = {}; 12 13 ChromeUtils.defineESModuleGetters(lazy, { 14 MULTI_DEVICE_THRESHOLD: "resource://services-sync/constants.sys.mjs", 15 SCORE_INCREMENT_MEDIUM: "resource://services-sync/constants.sys.mjs", 16 Svc: "resource://services-sync/util.sys.mjs", 17 extensionStorageSync: "resource://gre/modules/ExtensionStorageSync.sys.mjs", 18 setupLoggerForTarget: "resource://gre/modules/AppServicesTracing.sys.mjs", 19 storageSyncService: 20 "resource://gre/modules/ExtensionStorageComponents.sys.mjs", 21 22 extensionStorageSyncKinto: 23 "resource://gre/modules/ExtensionStorageSyncKinto.sys.mjs", 24 }); 25 26 const PREF_FORCE_ENABLE = "engine.extension-storage.force"; 27 28 // A helper to indicate whether extension-storage is enabled - it's based on 29 // the "addons" pref. The same logic is shared between both engine impls. 30 function getEngineEnabled() { 31 // By default, we sync extension storage if we sync addons. This 32 // lets us simplify the UX since users probably don't consider 33 // "extension preferences" a separate category of syncing. 34 // However, we also respect engine.extension-storage.force, which 35 // can be set to true or false, if a power user wants to customize 36 // the behavior despite the lack of UI. 37 if ( 38 lazy.Svc.PrefBranch.getPrefType(PREF_FORCE_ENABLE) != 39 Ci.nsIPrefBranch.PREF_INVALID 40 ) { 41 return lazy.Svc.PrefBranch.getBoolPref(PREF_FORCE_ENABLE); 42 } 43 return lazy.Svc.PrefBranch.getBoolPref("engine.addons", false); 44 } 45 46 function setEngineEnabled(enabled) { 47 // This will be called by the engine manager when declined on another device. 48 // Things will go a bit pear-shaped if the engine manager tries to end up 49 // with 'addons' and 'extension-storage' in different states - however, this 50 // *can* happen given we support the `engine.extension-storage.force` 51 // preference. So if that pref exists, we set it to this value. If that pref 52 // doesn't exist, we just ignore it and hope that the 'addons' engine is also 53 // going to be set to the same state. 54 if ( 55 lazy.Svc.PrefBranch.getPrefType(PREF_FORCE_ENABLE) != 56 Ci.nsIPrefBranch.PREF_INVALID 57 ) { 58 lazy.Svc.PrefBranch.setBoolPref(PREF_FORCE_ENABLE, enabled); 59 } 60 } 61 62 // A "bridged engine" to our webext-storage component. 63 export function ExtensionStorageEngineBridge(service) { 64 lazy.setupLoggerForTarget("webext_storage", "Sync.Engine.Extension-Storage"); 65 BridgedEngine.call(this, "Extension-Storage", service); 66 } 67 68 ExtensionStorageEngineBridge.prototype = { 69 syncPriority: 10, 70 71 // Used to override the engine name in telemetry, so that we can distinguish . 72 overrideTelemetryName: "rust-webext-storage", 73 74 async initialize() { 75 await SyncEngine.prototype.initialize.call(this); 76 this._rustStore = await lazy.storageSyncService.getStorageAreaInstance(); 77 this._bridge = await this._rustStore.bridgedEngine(); 78 79 // Uniffi currently only supports async methods, so we'll need to hardcode 80 // these values for now (which is fine for now as these hardly ever change) 81 this._bridge.storageVersion = STORAGE_VERSION; 82 this._bridge.allowSkippedRecord = true; 83 this._bridge.getSyncId = async () => { 84 let syncID = await this._bridge.syncId(); 85 return syncID; 86 }; 87 88 this._log.info("Got a bridged engine!"); 89 this._tracker.modified = true; 90 }, 91 92 async _notifyPendingChanges() { 93 try { 94 let changeSets = await this._rustStore.getSyncedChanges(); 95 96 changeSets.forEach(changeSet => { 97 try { 98 lazy.extensionStorageSync.notifyListeners( 99 changeSet.extId, 100 JSON.parse(changeSet.changes) 101 ); 102 } catch (ex) { 103 this._log.warn( 104 `Error notifying change listeners for ${changeSet.extId}`, 105 ex 106 ); 107 } 108 }); 109 } catch (ex) { 110 this._log.warn("Error fetching pending synced changes", ex); 111 } 112 }, 113 114 async _processIncoming() { 115 await super._processIncoming(); 116 try { 117 await this._notifyPendingChanges(); 118 } catch (ex) { 119 // Failing to notify `storage.onChanged` observers is bad, but shouldn't 120 // interrupt syncing. 121 this._log.warn("Error notifying about synced changes", ex); 122 } 123 }, 124 125 get enabled() { 126 return getEngineEnabled(); 127 }, 128 set enabled(enabled) { 129 setEngineEnabled(enabled); 130 }, 131 }; 132 Object.setPrototypeOf( 133 ExtensionStorageEngineBridge.prototype, 134 BridgedEngine.prototype 135 ); 136 137 /******************************************************************************* 138 * 139 * Deprecated support for Kinto 140 * 141 ******************************************************************************/ 142 143 /** 144 * The Engine that manages syncing for the web extension "storage" 145 * API, and in particular ext.storage.sync. 146 * 147 * ext.storage.sync is implemented using Kinto, so it has mechanisms 148 * for syncing that we do not need to integrate in the Firefox Sync 149 * framework, so this is something of a stub. 150 */ 151 export function ExtensionStorageEngineKinto(service) { 152 SyncEngine.call(this, "Extension-Storage", service); 153 XPCOMUtils.defineLazyPreferenceGetter( 154 this, 155 "_skipPercentageChance", 156 "services.sync.extension-storage.skipPercentageChance", 157 0 158 ); 159 } 160 161 ExtensionStorageEngineKinto.prototype = { 162 _trackerObj: ExtensionStorageTracker, 163 // we don't need these since we implement our own sync logic 164 _storeObj: undefined, 165 _recordObj: undefined, 166 167 syncPriority: 10, 168 allowSkippedRecord: false, 169 170 async _sync() { 171 return lazy.extensionStorageSyncKinto.syncAll(); 172 }, 173 174 get enabled() { 175 return getEngineEnabled(); 176 }, 177 // We only need the enabled setter for the edge-case where info/collections 178 // has `extension-storage` - which could happen if the pref to flip the new 179 // engine on was once set but no longer is. 180 set enabled(enabled) { 181 setEngineEnabled(enabled); 182 }, 183 184 _wipeClient() { 185 return lazy.extensionStorageSyncKinto.clearAll(); 186 }, 187 188 shouldSkipSync(syncReason) { 189 if (syncReason == "user" || syncReason == "startup") { 190 this._log.info( 191 `Not skipping extension storage sync: reason == ${syncReason}` 192 ); 193 // Always sync if a user clicks the button, or if we're starting up. 194 return false; 195 } 196 // Ensure this wouldn't cause a resync... 197 if (this._tracker.score >= lazy.MULTI_DEVICE_THRESHOLD) { 198 this._log.info( 199 "Not skipping extension storage sync: Would trigger resync anyway" 200 ); 201 return false; 202 } 203 204 let probability = this._skipPercentageChance / 100.0; 205 // Math.random() returns a value in the interval [0, 1), so `>` is correct: 206 // if `probability` is 1 skip every time, and if it's 0, never skip. 207 let shouldSkip = probability > Math.random(); 208 209 this._log.info( 210 `Skipping extension-storage sync with a chance of ${probability}: ${shouldSkip}` 211 ); 212 return shouldSkip; 213 }, 214 }; 215 Object.setPrototypeOf( 216 ExtensionStorageEngineKinto.prototype, 217 SyncEngine.prototype 218 ); 219 220 function ExtensionStorageTracker(name, engine) { 221 Tracker.call(this, name, engine); 222 this._ignoreAll = false; 223 } 224 ExtensionStorageTracker.prototype = { 225 get ignoreAll() { 226 return this._ignoreAll; 227 }, 228 229 set ignoreAll(value) { 230 this._ignoreAll = value; 231 }, 232 233 onStart() { 234 lazy.Svc.Obs.add("ext.storage.sync-changed", this.asyncObserver); 235 }, 236 237 onStop() { 238 lazy.Svc.Obs.remove("ext.storage.sync-changed", this.asyncObserver); 239 }, 240 241 async observe(subject, topic) { 242 if (this.ignoreAll) { 243 return; 244 } 245 246 if (topic !== "ext.storage.sync-changed") { 247 return; 248 } 249 250 // Single adds, removes and changes are not so important on their 251 // own, so let's just increment score a bit. 252 this.score += lazy.SCORE_INCREMENT_MEDIUM; 253 }, 254 }; 255 Object.setPrototypeOf(ExtensionStorageTracker.prototype, Tracker.prototype);