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 5var EXPORTED_SYMBOLS = ["SearchSettings"]; 6 7const { XPCOMUtils } = ChromeUtils.import( 8 "resource://gre/modules/XPCOMUtils.jsm" 9); 10 11XPCOMUtils.defineLazyModuleGetters(this, { 12 DeferredTask: "resource://gre/modules/DeferredTask.jsm", 13 SearchUtils: "resource://gre/modules/SearchUtils.jsm", 14 Services: "resource://gre/modules/Services.jsm", 15}); 16 17XPCOMUtils.defineLazyGetter(this, "logConsole", () => { 18 return console.createInstance({ 19 prefix: "SearchSettings", 20 maxLogLevel: SearchUtils.loggingEnabled ? "Debug" : "Warn", 21 }); 22}); 23 24const SETTINGS_FILENAME = "search.json.mozlz4"; 25 26/** 27 * This class manages the saves search settings. 28 * 29 * Global settings can be saved and obtained from this class via the 30 * `*Attribute` methods. 31 */ 32class SearchSettings { 33 constructor(searchService) { 34 this._searchService = searchService; 35 } 36 37 QueryInterface = ChromeUtils.generateQI([Ci.nsIObserver]); 38 39 // Delay for batching invalidation of the JSON settings (ms) 40 static SETTINGS_INVALIDATION_DELAY = 1000; 41 42 /** 43 * A reference to the pending DeferredTask, if there is one. 44 */ 45 _batchTask = null; 46 47 /** 48 * The current metadata stored in the settings. This stores: 49 * - current 50 * The current user-set default engine. The associated hash is called 51 * 'hash'. 52 * - private 53 * The current user-set private engine. The associated hash is called 54 * 'privateHash'. 55 * 56 * All of the above have associated hash fields to validate the value is set 57 * by the application. 58 */ 59 _metaData = {}; 60 61 /** 62 * A reference to the search service so that we can save the engines list. 63 */ 64 _searchService = null; 65 66 /* 67 * A copy of the settings so we can persist metadata for engines that 68 * are not currently active. 69 */ 70 _currentSettings = null; 71 72 addObservers() { 73 Services.obs.addObserver(this, SearchUtils.TOPIC_ENGINE_MODIFIED); 74 Services.obs.addObserver(this, SearchUtils.TOPIC_SEARCH_SERVICE); 75 } 76 77 /** 78 * Cleans up, removing observers. 79 */ 80 removeObservers() { 81 Services.obs.removeObserver(this, SearchUtils.TOPIC_ENGINE_MODIFIED); 82 Services.obs.removeObserver(this, SearchUtils.TOPIC_SEARCH_SERVICE); 83 } 84 85 /** 86 * Reads the settings file. 87 * 88 * @param {string} origin 89 * If this parameter is "test", then the settings will not be written. As 90 * some tests manipulate the settings directly, we allow turning off writing to 91 * avoid writing stale settings data. 92 * @returns {object} 93 * Returns the settings file data. 94 */ 95 async get(origin = "") { 96 let json; 97 await this._ensurePendingWritesCompleted(origin); 98 try { 99 let settingsFilePath = PathUtils.join( 100 await PathUtils.getProfileDir(), 101 SETTINGS_FILENAME 102 ); 103 json = await IOUtils.readJSON(settingsFilePath, { decompress: true }); 104 if (!json.engines || !json.engines.length) { 105 throw new Error("no engine in the file"); 106 } 107 } catch (ex) { 108 logConsole.warn("get: No settings file exists, new profile?", ex); 109 json = {}; 110 } 111 if (json.metaData) { 112 this._metaData = json.metaData; 113 } 114 // Versions of gecko older than 82 stored the order flag as a preference. 115 // This was changed in version 6 of the settings file. 116 if (json.version < 6 || !("useSavedOrder" in this._metaData)) { 117 const prefName = SearchUtils.BROWSER_SEARCH_PREF + "useDBForOrder"; 118 let useSavedOrder = Services.prefs.getBoolPref(prefName, false); 119 120 this.setAttribute("useSavedOrder", useSavedOrder); 121 122 // Clear the old pref so it isn't lying around. 123 Services.prefs.clearUserPref(prefName); 124 } 125 126 this._currentSettings = json; 127 return json; 128 } 129 130 /** 131 * Queues writing the settings until after SETTINGS_INVALIDATION_DELAY. If there 132 * is a currently queued task then it will be restarted. 133 */ 134 _delayedWrite() { 135 if (this._batchTask) { 136 this._batchTask.disarm(); 137 } else { 138 let task = async () => { 139 if ( 140 !this._searchService.isInitialized || 141 this._searchService._reloadingEngines 142 ) { 143 // Re-arm the task as we don't want to save potentially incomplete 144 // information during the middle of (re-)initializing. 145 this._batchTask.arm(); 146 return; 147 } 148 logConsole.debug("batchTask: Invalidating engine settings"); 149 await this._write(); 150 }; 151 this._batchTask = new DeferredTask( 152 task, 153 SearchSettings.SETTINGS_INVALIDATION_DELAY 154 ); 155 } 156 this._batchTask.arm(); 157 } 158 159 /** 160 * Ensures any pending writes of the settings are completed. 161 * 162 * @param {string} origin 163 * If this parameter is "test", then the settings will not be written. As 164 * some tests manipulate the settings directly, we allow turning off writing to 165 * avoid writing stale settings data. 166 */ 167 async _ensurePendingWritesCompleted(origin = "") { 168 // Before we read the settings file, first make sure all pending tasks are clear. 169 if (!this._batchTask) { 170 return; 171 } 172 logConsole.debug("finalizing batch task"); 173 let task = this._batchTask; 174 this._batchTask = null; 175 // Tests manipulate the settings directly, so let's not double-write with 176 // stale settings data here. 177 if (origin == "test") { 178 task.disarm(); 179 } else { 180 await task.finalize(); 181 } 182 } 183 184 /** 185 * Writes the settings to disk (no delay). 186 */ 187 async _write() { 188 if (this._batchTask) { 189 this._batchTask.disarm(); 190 } 191 192 let settings = {}; 193 194 // Allows us to force a settings refresh should the settings format change. 195 settings.version = SearchUtils.SETTINGS_VERSION; 196 settings.engines = [...this._searchService._engines.values()]; 197 settings.metaData = this._metaData; 198 199 // Persist metadata for AppProvided engines even if they aren't currently 200 // active, this means if they become active again their settings 201 // will be restored. 202 if (this._currentSettings?.engines) { 203 for (let engine of this._currentSettings.engines) { 204 let included = settings.engines.some(e => e._name == engine._name); 205 if (engine._isAppProvided && !included) { 206 settings.engines.push(engine); 207 } 208 } 209 } 210 211 // Update the local copy. 212 this._currentSettings = settings; 213 214 try { 215 if (!settings.engines.length) { 216 throw new Error("cannot write without any engine."); 217 } 218 219 logConsole.debug("_write: Writing to settings file."); 220 let path = PathUtils.join( 221 await PathUtils.getProfileDir(), 222 SETTINGS_FILENAME 223 ); 224 await IOUtils.writeJSON(path, settings, { compress: true }); 225 logConsole.debug("_write: settings file written to disk."); 226 Services.obs.notifyObservers( 227 null, 228 SearchUtils.TOPIC_SEARCH_SERVICE, 229 "write-settings-to-disk-complete" 230 ); 231 } catch (ex) { 232 logConsole.error("_write: Could not write to settings file:", ex); 233 } 234 } 235 236 /** 237 * Sets an attribute without verification. 238 * 239 * @param {string} name 240 * The name of the attribute to set. 241 * @param {*} val 242 * The value to set. 243 */ 244 setAttribute(name, val) { 245 this._metaData[name] = val; 246 this._delayedWrite(); 247 } 248 249 /** 250 * Sets a verified attribute. This will save an additional hash 251 * value, that can be verified when reading back. 252 * 253 * @param {string} name 254 * The name of the attribute to set. 255 * @param {*} val 256 * The value to set. 257 */ 258 setVerifiedAttribute(name, val) { 259 this._metaData[name] = val; 260 this._metaData[this.getHashName(name)] = SearchUtils.getVerificationHash( 261 val 262 ); 263 this._delayedWrite(); 264 } 265 266 /** 267 * Gets an attribute without verification. 268 * 269 * @param {string} name 270 * The name of the attribute to get. 271 * @returns {*} 272 * The value of the attribute, or undefined if not known. 273 */ 274 getAttribute(name) { 275 return this._metaData[name] ?? undefined; 276 } 277 278 /** 279 * Gets a verified attribute. 280 * 281 * @param {string} name 282 * The name of the attribute to get. 283 * @returns {*} 284 * The value of the attribute, or undefined if not known or an empty strings 285 * if it does not match the verification hash. 286 */ 287 getVerifiedAttribute(name) { 288 let val = this.getAttribute(name); 289 if ( 290 val && 291 this.getAttribute(this.getHashName(name)) != 292 SearchUtils.getVerificationHash(val) 293 ) { 294 logConsole.warn("getVerifiedGlobalAttr, invalid hash for", name); 295 return undefined; 296 } 297 return val; 298 } 299 300 /** 301 * Returns the name for the hash for a particular attribute. This is 302 * necessary because the normal default engine is named `current` with 303 * its hash as `hash`. All other hashes are in the `<name>Hash` format. 304 * 305 * @param {string} name 306 * The name of the attribute to get the hash name for. 307 * @returns {string} 308 * The hash name to use. 309 */ 310 getHashName(name) { 311 if (name == "current") { 312 return "hash"; 313 } 314 return name + "Hash"; 315 } 316 317 /** 318 * Handles shutdown; writing the settings if necessary. 319 * 320 * @param {object} state 321 * The shutdownState object that is used to help analyzing the shutdown 322 * state in case of a crash or shutdown timeout. 323 */ 324 async shutdown(state) { 325 if (!this._batchTask) { 326 return; 327 } 328 state.step = "Finalizing batched task"; 329 try { 330 await this._batchTask.finalize(); 331 state.step = "Batched task finalized"; 332 } catch (ex) { 333 state.step = "Batched task failed to finalize"; 334 335 state.latestError.message = "" + ex; 336 if (ex && typeof ex == "object") { 337 state.latestError.stack = ex.stack || undefined; 338 } 339 } 340 } 341 342 // nsIObserver 343 observe(engine, topic, verb) { 344 switch (topic) { 345 case SearchUtils.TOPIC_ENGINE_MODIFIED: 346 switch (verb) { 347 case SearchUtils.MODIFIED_TYPE.ADDED: 348 case SearchUtils.MODIFIED_TYPE.CHANGED: 349 case SearchUtils.MODIFIED_TYPE.REMOVED: 350 this._delayedWrite(); 351 break; 352 } 353 break; 354 case SearchUtils.TOPIC_SEARCH_SERVICE: 355 switch (verb) { 356 case "init-complete": 357 case "engines-reloaded": 358 this._delayedWrite(); 359 break; 360 } 361 break; 362 } 363 } 364} 365