summaryrefslogtreecommitdiffstats
path: root/toolkit/components/search/SearchSettings.jsm
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /toolkit/components/search/SearchSettings.jsm
parentInitial commit. (diff)
downloadfirefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz
firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/search/SearchSettings.jsm')
-rw-r--r--toolkit/components/search/SearchSettings.jsm372
1 files changed, 372 insertions, 0 deletions
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 `<name>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;
+ }
+ }
+}