From 2aa4a82499d4becd2284cdb482213d541b8804dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 28 Apr 2024 16:29:10 +0200 Subject: Adding upstream version 86.0.1. Signed-off-by: Daniel Baumann --- toolkit/components/search/SearchSettings.jsm | 372 +++++++++++++++++++++++++++ 1 file changed, 372 insertions(+) create mode 100644 toolkit/components/search/SearchSettings.jsm (limited to 'toolkit/components/search/SearchSettings.jsm') diff --git a/toolkit/components/search/SearchSettings.jsm b/toolkit/components/search/SearchSettings.jsm new file mode 100644 index 0000000000..97ec66f8e5 --- /dev/null +++ b/toolkit/components/search/SearchSettings.jsm @@ -0,0 +1,372 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var EXPORTED_SYMBOLS = ["SearchSettings"]; + +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); + +XPCOMUtils.defineLazyModuleGetters(this, { + DeferredTask: "resource://gre/modules/DeferredTask.jsm", + OS: "resource://gre/modules/osfile.jsm", + SearchUtils: "resource://gre/modules/SearchUtils.jsm", + Services: "resource://gre/modules/Services.jsm", +}); + +XPCOMUtils.defineLazyGetter(this, "logConsole", () => { + return console.createInstance({ + prefix: "SearchSettings", + maxLogLevel: SearchUtils.loggingEnabled ? "Debug" : "Warn", + }); +}); + +// A text encoder to UTF8, used whenever we commit the settings to disk. +XPCOMUtils.defineLazyGetter(this, "gEncoder", function() { + return new TextEncoder(); +}); + +const SETTINGS_FILENAME = "search.json.mozlz4"; + +/** + * This class manages the saves search settings. + * + * Global settings can be saved and obtained from this class via the + * `*Attribute` methods. + */ +class SearchSettings { + constructor(searchService) { + this._searchService = searchService; + } + + QueryInterface = ChromeUtils.generateQI([Ci.nsIObserver]); + + // Delay for batching invalidation of the JSON settings (ms) + static SETTINGS_INVALIDATION_DELAY = 1000; + + /** + * A reference to the pending DeferredTask, if there is one. + */ + _batchTask = null; + + /** + * The current metadata stored in the settings. This stores: + * - current + * The current user-set default engine. The associated hash is called + * 'hash'. + * - private + * The current user-set private engine. The associated hash is called + * 'privateHash'. + * + * All of the above have associated hash fields to validate the value is set + * by the application. + */ + _metaData = {}; + + /** + * A reference to the search service so that we can save the engines list. + */ + _searchService = null; + + /* + * A copy of the settings so we can persist metadata for engines that + * are not currently active. + */ + _currentSettings = null; + + addObservers() { + Services.obs.addObserver(this, SearchUtils.TOPIC_ENGINE_MODIFIED); + Services.obs.addObserver(this, SearchUtils.TOPIC_SEARCH_SERVICE); + } + + /** + * Cleans up, removing observers. + */ + removeObservers() { + Services.obs.removeObserver(this, SearchUtils.TOPIC_ENGINE_MODIFIED); + Services.obs.removeObserver(this, SearchUtils.TOPIC_SEARCH_SERVICE); + } + + /** + * Reads the settings file. + * + * @param {string} origin + * If this parameter is "test", then the settings will not be written. As + * some tests manipulate the settings directly, we allow turning off writing to + * avoid writing stale settings data. + * @returns {object} + * Returns the settings file data. + */ + async get(origin = "") { + let json; + await this._ensurePendingWritesCompleted(origin); + try { + let settingsFilePath = OS.Path.join( + OS.Constants.Path.profileDir, + SETTINGS_FILENAME + ); + let bytes = await OS.File.read(settingsFilePath, { compression: "lz4" }); + json = JSON.parse(new TextDecoder().decode(bytes)); + if (!json.engines || !json.engines.length) { + throw new Error("no engine in the file"); + } + } catch (ex) { + logConsole.warn("get: No settings file exists, new profile?", ex); + json = {}; + } + if (json.metaData) { + this._metaData = json.metaData; + } + // Versions of gecko older than 82 stored the order flag as a preference. + // This was changed in version 6 of the settings file. + if (json.version < 6 || !("useSavedOrder" in this._metaData)) { + const prefName = SearchUtils.BROWSER_SEARCH_PREF + "useDBForOrder"; + let useSavedOrder = Services.prefs.getBoolPref(prefName, false); + + this.setAttribute("useSavedOrder", useSavedOrder); + + // Clear the old pref so it isn't lying around. + Services.prefs.clearUserPref(prefName); + } + + this._currentSettings = json; + return json; + } + + /** + * Queues writing the settings until after SETTINGS_INVALIDATION_DELAY. If there + * is a currently queued task then it will be restarted. + */ + _delayedWrite() { + if (this._batchTask) { + this._batchTask.disarm(); + } else { + let task = async () => { + if ( + !this._searchService.isInitialized || + this._searchService._reloadingEngines + ) { + // Re-arm the task as we don't want to save potentially incomplete + // information during the middle of (re-)initializing. + this._batchTask.arm(); + return; + } + logConsole.debug("batchTask: Invalidating engine settings"); + await this._write(); + }; + this._batchTask = new DeferredTask( + task, + SearchSettings.SETTINGS_INVALIDATION_DELAY + ); + } + this._batchTask.arm(); + } + + /** + * Ensures any pending writes of the settings are completed. + * + * @param {string} origin + * If this parameter is "test", then the settings will not be written. As + * some tests manipulate the settings directly, we allow turning off writing to + * avoid writing stale settings data. + */ + async _ensurePendingWritesCompleted(origin = "") { + // Before we read the settings file, first make sure all pending tasks are clear. + if (!this._batchTask) { + return; + } + logConsole.debug("finalizing batch task"); + let task = this._batchTask; + this._batchTask = null; + // Tests manipulate the settings directly, so let's not double-write with + // stale settings data here. + if (origin == "test") { + task.disarm(); + } else { + await task.finalize(); + } + } + + /** + * Writes the settings to disk (no delay). + */ + async _write() { + if (this._batchTask) { + this._batchTask.disarm(); + } + + let settings = {}; + + // Allows us to force a settings refresh should the settings format change. + settings.version = SearchUtils.SETTINGS_VERSION; + settings.engines = [...this._searchService._engines.values()]; + settings.metaData = this._metaData; + + // Persist metadata for AppProvided engines even if they aren't currently + // active, this means if they become active again their settings + // will be restored. + if (this._currentSettings?.engines) { + for (let engine of this._currentSettings.engines) { + let included = settings.engines.some(e => e._name == engine._name); + if (engine._isAppProvided && !included) { + settings.engines.push(engine); + } + } + } + + // Update the local copy. + this._currentSettings = settings; + + try { + if (!settings.engines.length) { + throw new Error("cannot write without any engine."); + } + + logConsole.debug("_write: Writing to settings file."); + let path = OS.Path.join(OS.Constants.Path.profileDir, SETTINGS_FILENAME); + let data = gEncoder.encode(JSON.stringify(settings)); + await OS.File.writeAtomic(path, data, { + compression: "lz4", + tmpPath: path + ".tmp", + }); + logConsole.debug("_write: settings file written to disk."); + Services.obs.notifyObservers( + null, + SearchUtils.TOPIC_SEARCH_SERVICE, + "write-settings-to-disk-complete" + ); + } catch (ex) { + logConsole.error("_write: Could not write to settings file:", ex); + } + } + + /** + * Sets an attribute without verification. + * + * @param {string} name + * The name of the attribute to set. + * @param {*} val + * The value to set. + */ + setAttribute(name, val) { + this._metaData[name] = val; + this._delayedWrite(); + } + + /** + * Sets a verified attribute. This will save an additional hash + * value, that can be verified when reading back. + * + * @param {string} name + * The name of the attribute to set. + * @param {*} val + * The value to set. + */ + setVerifiedAttribute(name, val) { + this._metaData[name] = val; + this._metaData[this.getHashName(name)] = SearchUtils.getVerificationHash( + val + ); + this._delayedWrite(); + } + + /** + * Gets an attribute without verification. + * + * @param {string} name + * The name of the attribute to get. + * @returns {*} + * The value of the attribute, or undefined if not known. + */ + getAttribute(name) { + return this._metaData[name] ?? undefined; + } + + /** + * Gets a verified attribute. + * + * @param {string} name + * The name of the attribute to get. + * @returns {*} + * The value of the attribute, or undefined if not known or an empty strings + * if it does not match the verification hash. + */ + getVerifiedAttribute(name) { + let val = this.getAttribute(name); + if ( + val && + this.getAttribute(this.getHashName(name)) != + SearchUtils.getVerificationHash(val) + ) { + logConsole.warn("getVerifiedGlobalAttr, invalid hash for", name); + return undefined; + } + return val; + } + + /** + * Returns the name for the hash for a particular attribute. This is + * necessary because the normal default engine is named `current` with + * its hash as `hash`. All other hashes are in the `Hash` format. + * + * @param {string} name + * The name of the attribute to get the hash name for. + * @returns {string} + * The hash name to use. + */ + getHashName(name) { + if (name == "current") { + return "hash"; + } + return name + "Hash"; + } + + /** + * Handles shutdown; writing the settings if necessary. + * + * @param {object} state + * The shutdownState object that is used to help analyzing the shutdown + * state in case of a crash or shutdown timeout. + */ + async shutdown(state) { + if (!this._batchTask) { + return; + } + state.step = "Finalizing batched task"; + try { + await this._batchTask.finalize(); + state.step = "Batched task finalized"; + } catch (ex) { + state.step = "Batched task failed to finalize"; + + state.latestError.message = "" + ex; + if (ex && typeof ex == "object") { + state.latestError.stack = ex.stack || undefined; + } + } + } + + // nsIObserver + observe(engine, topic, verb) { + switch (topic) { + case SearchUtils.TOPIC_ENGINE_MODIFIED: + switch (verb) { + case SearchUtils.MODIFIED_TYPE.ADDED: + case SearchUtils.MODIFIED_TYPE.CHANGED: + case SearchUtils.MODIFIED_TYPE.REMOVED: + this._delayedWrite(); + break; + } + break; + case SearchUtils.TOPIC_SEARCH_SERVICE: + switch (verb) { + case "init-complete": + case "engines-reloaded": + this._delayedWrite(); + break; + } + break; + } + } +} -- cgit v1.2.3