forked from mirrors/gecko-dev
		
	 b568f4ff2d
			
		
	
	
		b568f4ff2d
		
	
	
	
	
		
			
			This patch creates a cache of the user search settings file that is read from disk. We use the cached settings to determine if the settings has been updated or changed. If there has been a change, we allow the settings to be written and saved, otherwise if there's no change, there is no need to write and save the file. Public setter and getter methods have also been created to modify or retrieve the metadata of the engines. Differential Revision: https://phabricator.services.mozilla.com/D151551
		
			
				
	
	
		
			484 lines
		
	
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			484 lines
		
	
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* 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/. */
 | |
| 
 | |
| import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
 | |
| 
 | |
| const lazy = {};
 | |
| 
 | |
| ChromeUtils.defineESModuleGetters(lazy, {
 | |
|   SearchUtils: "resource://gre/modules/SearchUtils.sys.mjs",
 | |
| });
 | |
| 
 | |
| XPCOMUtils.defineLazyModuleGetters(lazy, {
 | |
|   DeferredTask: "resource://gre/modules/DeferredTask.jsm",
 | |
|   ObjectUtils: "resource://gre/modules/ObjectUtils.jsm",
 | |
| });
 | |
| 
 | |
| XPCOMUtils.defineLazyGetter(lazy, "logConsole", () => {
 | |
|   return console.createInstance({
 | |
|     prefix: "SearchSettings",
 | |
|     maxLogLevel: lazy.SearchUtils.loggingEnabled ? "Debug" : "Warn",
 | |
|   });
 | |
| });
 | |
| 
 | |
| 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.
 | |
|  */
 | |
| export 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;
 | |
| 
 | |
|   /**
 | |
|    * A reference to the search service so that we can save the engines list.
 | |
|    */
 | |
|   _searchService = null;
 | |
| 
 | |
|   /*
 | |
|    * The user's settings file read from disk so we can persist metadata for
 | |
|    * engines that are default or hidden, the user's locale and region, hashes
 | |
|    * for the loadPath, and hashes for default and private default engines.
 | |
|    * This is the JSON we read from disk and save to disk when there's an update
 | |
|    * to the settings.
 | |
|    *
 | |
|    * Structre of settings:
 | |
|    * Object { version: <number>,
 | |
|    *          engines: [...],
 | |
|    *          metaData: {...},
 | |
|    *        }
 | |
|    *
 | |
|    * Settings metaData is the active metadata for setting and getting attributes.
 | |
|    * When a new metadata attribute is set, we save it to #settings.metaData and
 | |
|    * write #settings to disk.
 | |
|    *
 | |
|    * #settings.metaData attributes:
 | |
|    * @property {string} current
 | |
|    *    The current user-set default engine. The associated hash is called
 | |
|    *    'hash'.
 | |
|    * @property {string} private
 | |
|    *    The current user-set private engine. The associated hash is called
 | |
|    *    'privateHash'.
 | |
|    *    The current and prviate objects have associated hash fields to validate
 | |
|    *    the value is set by the application.
 | |
|    * @property {string} appDefaultEngine
 | |
|    * @property {string} channel
 | |
|    *    Configuration is restricted to the specified channel. ESR is an example
 | |
|    *    of a channel.
 | |
|    * @property {string} distroID
 | |
|    *    Specifies which distribution the default engine is included in.
 | |
|    * @property {string} experiment
 | |
|    *    Specifies if the application is running on an experiment.
 | |
|    * @property {string} locale
 | |
|    * @property {string} region
 | |
|    * @property {boolean} useSavedOrder
 | |
|    *    True if the user's order information stored in settings is used.
 | |
|    *
 | |
|    */
 | |
|   #settings = null;
 | |
| 
 | |
|   /**
 | |
|    * @type {object} A deep copy of #settings.
 | |
|    *  #cachedSettings is updated when we read the settings from disk and when we
 | |
|    *  write settings to disk. #cachedSettings is compared with #settings before
 | |
|    *  we do a write to disk. If there's no change to the settings attributes,
 | |
|    *  then we don't write to disk.
 | |
|    */
 | |
|   #cachedSettings = {};
 | |
| 
 | |
|   addObservers() {
 | |
|     Services.obs.addObserver(this, lazy.SearchUtils.TOPIC_ENGINE_MODIFIED);
 | |
|     Services.obs.addObserver(this, lazy.SearchUtils.TOPIC_SEARCH_SERVICE);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Cleans up, removing observers.
 | |
|    */
 | |
|   removeObservers() {
 | |
|     Services.obs.removeObserver(this, lazy.SearchUtils.TOPIC_ENGINE_MODIFIED);
 | |
|     Services.obs.removeObserver(this, lazy.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 = PathUtils.join(
 | |
|         PathUtils.profileDir,
 | |
|         SETTINGS_FILENAME
 | |
|       );
 | |
|       json = await IOUtils.readJSON(settingsFilePath, { decompress: true });
 | |
|       if (!json.engines || !json.engines.length) {
 | |
|         throw new Error("no engine in the file");
 | |
|       }
 | |
|     } catch (ex) {
 | |
|       lazy.logConsole.warn("get: No settings file exists, new profile?", ex);
 | |
|       json = {};
 | |
|     }
 | |
| 
 | |
|     this.#settings = json;
 | |
|     this.#cachedSettings = structuredClone(json);
 | |
| 
 | |
|     if (!this.#settings.metaData) {
 | |
|       this.#settings.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 (
 | |
|       this.#settings.version < 6 ||
 | |
|       !("useSavedOrder" in this.#settings.metaData)
 | |
|     ) {
 | |
|       const prefName = lazy.SearchUtils.BROWSER_SEARCH_PREF + "useDBForOrder";
 | |
|       let useSavedOrder = Services.prefs.getBoolPref(prefName, false);
 | |
| 
 | |
|       this.setMetaDataAttribute("useSavedOrder", useSavedOrder);
 | |
| 
 | |
|       // Clear the old pref so it isn't lying around.
 | |
|       Services.prefs.clearUserPref(prefName);
 | |
|     }
 | |
| 
 | |
|     return structuredClone(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;
 | |
|         }
 | |
|         lazy.logConsole.debug("batchTask: Invalidating engine settings");
 | |
|         await this._write();
 | |
|       };
 | |
|       this._batchTask = new lazy.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;
 | |
|     }
 | |
|     lazy.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 = lazy.SearchUtils.SETTINGS_VERSION;
 | |
|     settings.engines = [...this._searchService._engines.values()].map(engine =>
 | |
|       JSON.parse(JSON.stringify(engine))
 | |
|     );
 | |
|     settings.metaData = this.#settings.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.#settings?.engines) {
 | |
|       for (let engine of this.#settings.engines) {
 | |
|         let included = settings.engines.some(e => e._name == engine._name);
 | |
|         if (engine._isAppProvided && !included) {
 | |
|           settings.engines.push(engine);
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // Update the local copy.
 | |
|     this.#settings = settings;
 | |
| 
 | |
|     try {
 | |
|       if (!settings.engines.length) {
 | |
|         throw new Error("cannot write without any engine.");
 | |
|       }
 | |
| 
 | |
|       if (this.isCurrentAndCachedSettingsEqual()) {
 | |
|         lazy.logConsole.debug(
 | |
|           "_write: Settings unchanged. Did not write to disk."
 | |
|         );
 | |
|         Services.obs.notifyObservers(
 | |
|           null,
 | |
|           lazy.SearchUtils.TOPIC_SEARCH_SERVICE,
 | |
|           "write-prevented-when-settings-unchanged"
 | |
|         );
 | |
|         Services.obs.notifyObservers(
 | |
|           null,
 | |
|           lazy.SearchUtils.TOPIC_SEARCH_SERVICE,
 | |
|           "write-settings-to-disk-complete"
 | |
|         );
 | |
| 
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       // At this point, the settings and cached settings are different. We
 | |
|       // write settings to disk and update #cachedSettings.
 | |
|       this.#cachedSettings = structuredClone(this.#settings);
 | |
| 
 | |
|       lazy.logConsole.debug("_write: Writing to settings file.");
 | |
|       let path = PathUtils.join(PathUtils.profileDir, SETTINGS_FILENAME);
 | |
|       await IOUtils.writeJSON(path, settings, {
 | |
|         compress: true,
 | |
|         tmpPath: path + ".tmp",
 | |
|       });
 | |
|       lazy.logConsole.debug("_write: settings file written to disk.");
 | |
|       Services.obs.notifyObservers(
 | |
|         null,
 | |
|         lazy.SearchUtils.TOPIC_SEARCH_SERVICE,
 | |
|         "write-settings-to-disk-complete"
 | |
|       );
 | |
|     } catch (ex) {
 | |
|       lazy.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.
 | |
|    */
 | |
|   setMetaDataAttribute(name, val) {
 | |
|     this.#settings.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.
 | |
|    */
 | |
|   setVerifiedMetaDataAttribute(name, val) {
 | |
|     this.#settings.metaData[name] = val;
 | |
|     this.#settings.metaData[
 | |
|       this.getHashName(name)
 | |
|     ] = lazy.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.
 | |
|    */
 | |
|   getMetaDataAttribute(name) {
 | |
|     return this.#settings.metaData[name] ?? undefined;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Gets a copy of the settings metadata.
 | |
|    *
 | |
|    * @returns {*}
 | |
|    *   A copy of the settings metadata object.
 | |
|    *
 | |
|    */
 | |
|   getSettingsMetaData() {
 | |
|     return { ...this.#settings.metaData };
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * 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.
 | |
|    */
 | |
|   getVerifiedMetaDataAttribute(name) {
 | |
|     let val = this.getMetaDataAttribute(name);
 | |
|     if (
 | |
|       val &&
 | |
|       this.getMetaDataAttribute(this.getHashName(name)) !=
 | |
|         lazy.SearchUtils.getVerificationHash(val)
 | |
|     ) {
 | |
|       lazy.logConsole.warn("getVerifiedGlobalAttr, invalid hash for", name);
 | |
|       return undefined;
 | |
|     }
 | |
|     return val;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Sets an attribute in #settings.engines._metaData
 | |
|    *
 | |
|    * @param {string} engineName
 | |
|    *   The name of the engine.
 | |
|    * @param {string} property
 | |
|    *   The name of the attribute to set.
 | |
|    * @param {*} value
 | |
|    *   The value to set.
 | |
|    */
 | |
|   setEngineMetaDataAttribute(engineName, property, value) {
 | |
|     let engines = [...this._searchService._engines.values()];
 | |
|     let engine = engines.find(engine => engine._name == engineName);
 | |
|     if (engine) {
 | |
|       engine._metaData[property] = value;
 | |
|       this._delayedWrite();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Gets an attribute from #settings.engines._metaData
 | |
|    *
 | |
|    * @param {string} engineName
 | |
|    *   The name of the engine.
 | |
|    * @param {string} property
 | |
|    *   The name of the attribute to get.
 | |
|    * @returns {*}
 | |
|    *   The value of the attribute, or undefined if not known.
 | |
|    */
 | |
|   getEngineMetaDataAttribute(engineName, property) {
 | |
|     let engine = this.#settings.engines.find(
 | |
|       engine => engine._name == engineName
 | |
|     );
 | |
|     return engine._metaData[property] ?? undefined;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * 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 lazy.SearchUtils.TOPIC_ENGINE_MODIFIED:
 | |
|         switch (verb) {
 | |
|           case lazy.SearchUtils.MODIFIED_TYPE.ADDED:
 | |
|           case lazy.SearchUtils.MODIFIED_TYPE.CHANGED:
 | |
|           case lazy.SearchUtils.MODIFIED_TYPE.REMOVED:
 | |
|             this._delayedWrite();
 | |
|             break;
 | |
|         }
 | |
|         break;
 | |
|       case lazy.SearchUtils.TOPIC_SEARCH_SERVICE:
 | |
|         switch (verb) {
 | |
|           case "init-complete":
 | |
|           case "engines-reloaded":
 | |
|             this._delayedWrite();
 | |
|             break;
 | |
|         }
 | |
|         break;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Compares the #settings and #cachedSettings objects.
 | |
|    *
 | |
|    * @returns {boolean}
 | |
|    *   True if the objects have the same property and values.
 | |
|    */
 | |
|   isCurrentAndCachedSettingsEqual() {
 | |
|     return lazy.ObjectUtils.deepEqual(this.#settings, this.#cachedSettings);
 | |
|   }
 | |
| }
 |