mirror of
				https://github.com/mozilla/gecko-dev.git
				synced 2025-11-04 10:18:41 +02:00 
			
		
		
		
	Original Revision: https://phabricator.services.mozilla.com/D213671 Differential Revision: https://phabricator.services.mozilla.com/D214414
		
			
				
	
	
		
			584 lines
		
	
	
	
		
			17 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			584 lines
		
	
	
	
		
			17 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/. */
 | 
						|
 | 
						|
/* eslint no-shadow: error, mozilla/no-aArgs: error */
 | 
						|
 | 
						|
import {
 | 
						|
  SearchEngine,
 | 
						|
  EngineURL,
 | 
						|
} from "resource://gre/modules/SearchEngine.sys.mjs";
 | 
						|
 | 
						|
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
 | 
						|
 | 
						|
const lazy = {};
 | 
						|
 | 
						|
ChromeUtils.defineESModuleGetters(lazy, {
 | 
						|
  RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
 | 
						|
  SearchUtils: "resource://gre/modules/SearchUtils.sys.mjs",
 | 
						|
});
 | 
						|
 | 
						|
XPCOMUtils.defineLazyServiceGetter(
 | 
						|
  lazy,
 | 
						|
  "idleService",
 | 
						|
  "@mozilla.org/widget/useridleservice;1",
 | 
						|
  "nsIUserIdleService"
 | 
						|
);
 | 
						|
 | 
						|
// After the user has been idle for 30s, we'll update icons if we need to.
 | 
						|
const ICON_UPDATE_ON_IDLE_DELAY = 30;
 | 
						|
 | 
						|
/**
 | 
						|
 * Handles loading application provided search engine icons from remote settings.
 | 
						|
 */
 | 
						|
class IconHandler {
 | 
						|
  /**
 | 
						|
   * The remote settings client for the search engine icons.
 | 
						|
   *
 | 
						|
   * @type {?RemoteSettingsClient}
 | 
						|
   */
 | 
						|
  #iconCollection = null;
 | 
						|
 | 
						|
  /**
 | 
						|
   * The list of icon records from the remote settings collection.
 | 
						|
   *
 | 
						|
   * @type {?object[]}
 | 
						|
   */
 | 
						|
  #iconList = null;
 | 
						|
 | 
						|
  /**
 | 
						|
   * A flag that indicates if we have queued an idle observer to update icons.
 | 
						|
   *
 | 
						|
   * @type {boolean}
 | 
						|
   */
 | 
						|
  #queuedIdle = false;
 | 
						|
 | 
						|
  /**
 | 
						|
   * A map of pending updates that need to be applied to the engines. This is
 | 
						|
   * keyed via record id, so that if multiple updates are queued for the same
 | 
						|
   * record, then we will only update the engine once.
 | 
						|
   *
 | 
						|
   * @type {Map<string, object>}
 | 
						|
   */
 | 
						|
  #pendingUpdatesMap = new Map();
 | 
						|
 | 
						|
  constructor() {
 | 
						|
    this.#iconCollection = lazy.RemoteSettings("search-config-icons");
 | 
						|
    this.#iconCollection.on("sync", this._onIconListUpdated.bind(this));
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Returns the icon for the record that matches the engine identifier
 | 
						|
   * and the preferred width.
 | 
						|
   *
 | 
						|
   * @param {string} engineIdentifier
 | 
						|
   *   The identifier of the engine to match against.
 | 
						|
   * @param {number} preferredWidth
 | 
						|
   *   The preferred with of the icon.
 | 
						|
   * @returns {string}
 | 
						|
   *   An object URL that can be used to reference the contents of the specified
 | 
						|
   *   source object.
 | 
						|
   */
 | 
						|
  async getIcon(engineIdentifier, preferredWidth) {
 | 
						|
    if (!this.#iconList) {
 | 
						|
      await this.#getIconList();
 | 
						|
    }
 | 
						|
 | 
						|
    let iconRecords = this.#iconList.filter(r =>
 | 
						|
      this._identifierMatches(engineIdentifier, r.engineIdentifiers)
 | 
						|
    );
 | 
						|
 | 
						|
    if (!iconRecords.length) {
 | 
						|
      console.warn("No icon found for", engineIdentifier);
 | 
						|
      return null;
 | 
						|
    }
 | 
						|
 | 
						|
    // Default to the first record, in the event we don't have any records
 | 
						|
    // that match the width.
 | 
						|
    let iconRecord = iconRecords[0];
 | 
						|
    for (let record of iconRecords) {
 | 
						|
      // TODO: Bug 1655070. We should be using the closest size, but for now use
 | 
						|
      // an exact match.
 | 
						|
      if (record.imageSize == preferredWidth) {
 | 
						|
        iconRecord = record;
 | 
						|
        break;
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    let iconData;
 | 
						|
    try {
 | 
						|
      iconData = await this.#iconCollection.attachments.get(iconRecord);
 | 
						|
    } catch (ex) {
 | 
						|
      console.error(ex);
 | 
						|
    }
 | 
						|
    if (!iconData) {
 | 
						|
      console.warn("Unable to find the icon for", engineIdentifier);
 | 
						|
      // Queue an update in case we haven't downloaded it yet.
 | 
						|
      this.#pendingUpdatesMap.set(iconRecord.id, iconRecord);
 | 
						|
      this.#maybeQueueIdle();
 | 
						|
      return null;
 | 
						|
    }
 | 
						|
 | 
						|
    if (iconData.record.last_modified != iconRecord.last_modified) {
 | 
						|
      // The icon we have stored is out of date, queue an update so that we'll
 | 
						|
      // download the new icon.
 | 
						|
      this.#pendingUpdatesMap.set(iconRecord.id, iconRecord);
 | 
						|
      this.#maybeQueueIdle();
 | 
						|
    }
 | 
						|
    return URL.createObjectURL(
 | 
						|
      new Blob([iconData.buffer], { type: iconRecord.attachment.mimetype })
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  QueryInterface = ChromeUtils.generateQI(["nsIObserver"]);
 | 
						|
 | 
						|
  /**
 | 
						|
   * Called when there is an update queued and the user has been observed to be
 | 
						|
   * idle for ICON_UPDATE_ON_IDLE_DELAY seconds.
 | 
						|
   *
 | 
						|
   * This will always download new icons (added or updated), even if there is
 | 
						|
   * no current engine that matches the identifiers. This is to ensure that we
 | 
						|
   * have pre-populated the cache if the engine is added later for this user.
 | 
						|
   *
 | 
						|
   * We do not handle deletes, as remote settings will handle the cleanup of
 | 
						|
   * removed records. We also do not expect the case where an icon is removed
 | 
						|
   * for an active engine.
 | 
						|
   *
 | 
						|
   * @param {nsISupports} subject
 | 
						|
   *   The subject of the observer.
 | 
						|
   * @param {string} topic
 | 
						|
   *   The topic of the observer.
 | 
						|
   */
 | 
						|
  async observe(subject, topic) {
 | 
						|
    if (topic != "idle") {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    this.#queuedIdle = false;
 | 
						|
    lazy.idleService.removeIdleObserver(this, ICON_UPDATE_ON_IDLE_DELAY);
 | 
						|
 | 
						|
    // Update the icon list, in case engines will call getIcon() again.
 | 
						|
    await this.#getIconList();
 | 
						|
 | 
						|
    let appProvidedEngines = await Services.search.getAppProvidedEngines();
 | 
						|
    for (let record of this.#pendingUpdatesMap.values()) {
 | 
						|
      let iconData;
 | 
						|
      try {
 | 
						|
        iconData = await this.#iconCollection.attachments.download(record);
 | 
						|
      } catch (ex) {
 | 
						|
        console.error("Could not download new icon", ex);
 | 
						|
        continue;
 | 
						|
      }
 | 
						|
 | 
						|
      for (let engine of appProvidedEngines) {
 | 
						|
        await engine.maybeUpdateIconURL(
 | 
						|
          record.engineIdentifiers,
 | 
						|
          URL.createObjectURL(
 | 
						|
            new Blob([iconData.buffer], {
 | 
						|
              type: record.attachment.mimetype,
 | 
						|
            })
 | 
						|
          )
 | 
						|
        );
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    this.#pendingUpdatesMap.clear();
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Checks if the identifier matches any of the engine identifiers.
 | 
						|
   *
 | 
						|
   * @param {string} identifier
 | 
						|
   *   The identifier of the engine.
 | 
						|
   * @param {string[]} engineIdentifiers
 | 
						|
   *   The list of engine identifiers to match against. This can include
 | 
						|
   *   wildcards at the end of strings.
 | 
						|
   * @returns {boolean}
 | 
						|
   *   Returns true if the identifier matches any of the engine identifiers.
 | 
						|
   */
 | 
						|
  _identifierMatches(identifier, engineIdentifiers) {
 | 
						|
    return engineIdentifiers.some(i => {
 | 
						|
      if (i.endsWith("*")) {
 | 
						|
        return identifier.startsWith(i.slice(0, -1));
 | 
						|
      }
 | 
						|
      return identifier == i;
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Obtains the icon list from the remote settings collection.
 | 
						|
   */
 | 
						|
  async #getIconList() {
 | 
						|
    try {
 | 
						|
      this.#iconList = await this.#iconCollection.get();
 | 
						|
    } catch (ex) {
 | 
						|
      console.error(ex);
 | 
						|
      this.#iconList = [];
 | 
						|
    }
 | 
						|
    if (!this.#iconList.length) {
 | 
						|
      console.error("Failed to obtain search engine icon list records");
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Called via a callback when remote settings updates the icon list. This
 | 
						|
   * stores potential updates and queues an idle observer to apply them.
 | 
						|
   *
 | 
						|
   * @param {object} payload
 | 
						|
   *   The payload from the remote settings collection.
 | 
						|
   * @param {object} payload.data
 | 
						|
   *   The payload data from the remote settings collection.
 | 
						|
   * @param {object[]} payload.data.created
 | 
						|
   *    The list of created records.
 | 
						|
   * @param {object[]} payload.data.updated
 | 
						|
   *    The list of updated records.
 | 
						|
   */
 | 
						|
  async _onIconListUpdated({ data: { created, updated } }) {
 | 
						|
    created.forEach(record => {
 | 
						|
      this.#pendingUpdatesMap.set(record.id, record);
 | 
						|
    });
 | 
						|
    for (let record of updated) {
 | 
						|
      if (record.new) {
 | 
						|
        this.#pendingUpdatesMap.set(record.new.id, record.new);
 | 
						|
      }
 | 
						|
    }
 | 
						|
    this.#maybeQueueIdle();
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Queues an idle observer if there are pending updates.
 | 
						|
   */
 | 
						|
  #maybeQueueIdle() {
 | 
						|
    if (this.#pendingUpdatesMap && !this.#queuedIdle) {
 | 
						|
      this.#queuedIdle = true;
 | 
						|
      lazy.idleService.addIdleObserver(this, ICON_UPDATE_ON_IDLE_DELAY);
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * AppProvidedSearchEngine represents a search engine defined by the
 | 
						|
 * search configuration.
 | 
						|
 */
 | 
						|
export class AppProvidedSearchEngine extends SearchEngine {
 | 
						|
  static URL_TYPE_MAP = new Map([
 | 
						|
    ["search", lazy.SearchUtils.URL_TYPE.SEARCH],
 | 
						|
    ["suggestions", lazy.SearchUtils.URL_TYPE.SUGGEST_JSON],
 | 
						|
    ["trending", lazy.SearchUtils.URL_TYPE.TRENDING_JSON],
 | 
						|
  ]);
 | 
						|
  static iconHandler = new IconHandler();
 | 
						|
 | 
						|
  /**
 | 
						|
   * A promise for the blob URL of the icon. We save the promise to avoid
 | 
						|
   * reentrancy issues.
 | 
						|
   *
 | 
						|
   * @type {?Promise<string>}
 | 
						|
   */
 | 
						|
  #blobURLPromise = null;
 | 
						|
 | 
						|
  /**
 | 
						|
   * The identifier from the configuration.
 | 
						|
   *
 | 
						|
   * @type {?string}
 | 
						|
   */
 | 
						|
  #configurationId = null;
 | 
						|
 | 
						|
  /**
 | 
						|
   * Whether or not this is a general purpose search engine.
 | 
						|
   *
 | 
						|
   * @type {boolean}
 | 
						|
   */
 | 
						|
  #isGeneralPurposeSearchEngine = false;
 | 
						|
 | 
						|
  /**
 | 
						|
   * @param {object} options
 | 
						|
   *   The options for this search engine.
 | 
						|
   * @param {object} options.config
 | 
						|
   *   The engine config from Remote Settings.
 | 
						|
   * @param {object} [options.settings]
 | 
						|
   *   The saved settings for the user.
 | 
						|
   */
 | 
						|
  constructor({ config, settings }) {
 | 
						|
    // TODO Bug 1875912 - Remove the webextension.id and webextension.locale when
 | 
						|
    // we're ready to remove old search-config and use search-config-v2 for all
 | 
						|
    // clients. The id in appProvidedSearchEngine should be changed to
 | 
						|
    // engine.identifier.
 | 
						|
    let extensionId = config.webExtension.id;
 | 
						|
    let id = config.webExtension.id + config.webExtension.locale;
 | 
						|
 | 
						|
    super({
 | 
						|
      loadPath: "[app]" + extensionId,
 | 
						|
      isAppProvided: true,
 | 
						|
      id,
 | 
						|
    });
 | 
						|
 | 
						|
    this._extensionID = extensionId;
 | 
						|
    this._locale = config.webExtension.locale;
 | 
						|
 | 
						|
    this.#configurationId = config.identifier;
 | 
						|
    this.#init(config);
 | 
						|
 | 
						|
    this._loadSettings(settings);
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Used to clean up the engine when it is removed. This will revoke the blob
 | 
						|
   * URL for the icon.
 | 
						|
   */
 | 
						|
  async cleanup() {
 | 
						|
    if (this.#blobURLPromise) {
 | 
						|
      URL.revokeObjectURL(await this.#blobURLPromise);
 | 
						|
      this.#blobURLPromise = null;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Update this engine based on new config, used during
 | 
						|
   * config upgrades.
 | 
						|
 | 
						|
   * @param {object} options
 | 
						|
   *   The options object.
 | 
						|
   *
 | 
						|
   * @param {object} options.configuration
 | 
						|
   *   The search engine configuration for application provided engines.
 | 
						|
   */
 | 
						|
  update({ configuration } = {}) {
 | 
						|
    this._urls = [];
 | 
						|
    this.#init(configuration);
 | 
						|
    lazy.SearchUtils.notifyAction(this, lazy.SearchUtils.MODIFIED_TYPE.CHANGED);
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * This will update the application provided search engine if there is no
 | 
						|
   * name change.
 | 
						|
   *
 | 
						|
   * @param {object} options
 | 
						|
   *   The options object.
 | 
						|
   * @param {object} [options.configuration]
 | 
						|
   *   The search engine configuration for application provided engines.
 | 
						|
   * @param {string} [options.locale]
 | 
						|
   *   The locale to use for getting details of the search engine.
 | 
						|
   * @returns {boolean}
 | 
						|
   *   Returns true if the engine was updated, false otherwise.
 | 
						|
   */
 | 
						|
  async updateIfNoNameChange({ configuration, locale }) {
 | 
						|
    if (this.name != configuration.name.trim()) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
 | 
						|
    this.update({ locale, configuration });
 | 
						|
    return true;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Whether or not this engine is provided by the application, e.g. it is
 | 
						|
   * in the list of configured search engines. Overrides the definition in
 | 
						|
   * `SearchEngine`.
 | 
						|
   *
 | 
						|
   * @returns {boolean}
 | 
						|
   */
 | 
						|
  get isAppProvided() {
 | 
						|
    return true;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Whether or not this engine is an in-memory only search engine.
 | 
						|
   * These engines are typically application provided or policy engines,
 | 
						|
   * where they are loaded every time on SearchService initialization
 | 
						|
   * using the policy JSON or the extension manifest. Minimal details of the
 | 
						|
   * in-memory engines are saved to disk, but they are never loaded
 | 
						|
   * from the user's saved settings file.
 | 
						|
   *
 | 
						|
   * @returns {boolean}
 | 
						|
   *   Only returns true for application provided engines.
 | 
						|
   */
 | 
						|
  get inMemory() {
 | 
						|
    return true;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Whether or not this engine is a "general" search engine, e.g. is it for
 | 
						|
   * generally searching the web, or does it have a specific purpose like
 | 
						|
   * shopping.
 | 
						|
   *
 | 
						|
   * @returns {boolean}
 | 
						|
   */
 | 
						|
  get isGeneralPurposeEngine() {
 | 
						|
    return this.#isGeneralPurposeSearchEngine;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Returns the icon URL for the search engine closest to the preferred width.
 | 
						|
   *
 | 
						|
   * @param {number} preferredWidth
 | 
						|
   *   The preferred width of the image.
 | 
						|
   * @returns {Promise<string>}
 | 
						|
   *   A promise that resolves to the URL of the icon.
 | 
						|
   */
 | 
						|
  async getIconURL(preferredWidth) {
 | 
						|
    if (this.#blobURLPromise) {
 | 
						|
      return this.#blobURLPromise;
 | 
						|
    }
 | 
						|
    this.#blobURLPromise = AppProvidedSearchEngine.iconHandler.getIcon(
 | 
						|
      this.#configurationId,
 | 
						|
      preferredWidth
 | 
						|
    );
 | 
						|
    return this.#blobURLPromise;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * This will update the icon URL for the search engine if the engine
 | 
						|
   * identifier matches the given engine identifiers.
 | 
						|
   *
 | 
						|
   * @param {string[]} engineIdentifiers
 | 
						|
   *   The engine identifiers to check against.
 | 
						|
   * @param {string} blobURL
 | 
						|
   *   The new icon URL for the search engine.
 | 
						|
   */
 | 
						|
  async maybeUpdateIconURL(engineIdentifiers, blobURL) {
 | 
						|
    // TODO: Bug 1875912. Once newSearchConfigEnabled has been enabled, we will
 | 
						|
    // be able to use `this.id` instead of `this.#configurationId`. At that
 | 
						|
    // point, `IconHandler._identifierMatches` can be made into a private
 | 
						|
    // function, as this if statement can be handled within `IconHandler.observe`.
 | 
						|
    if (
 | 
						|
      !AppProvidedSearchEngine.iconHandler._identifierMatches(
 | 
						|
        this.#configurationId,
 | 
						|
        engineIdentifiers
 | 
						|
      )
 | 
						|
    ) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    if (this.#blobURLPromise) {
 | 
						|
      URL.revokeObjectURL(await this.#blobURLPromise);
 | 
						|
      this.#blobURLPromise = null;
 | 
						|
    }
 | 
						|
    this.#blobURLPromise = Promise.resolve(blobURL);
 | 
						|
    lazy.SearchUtils.notifyAction(
 | 
						|
      this,
 | 
						|
      lazy.SearchUtils.MODIFIED_TYPE.ICON_CHANGED
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Creates a JavaScript object that represents this engine.
 | 
						|
   *
 | 
						|
   * @returns {object}
 | 
						|
   *   An object suitable for serialization as JSON.
 | 
						|
   */
 | 
						|
  toJSON() {
 | 
						|
    // For applicaiton provided engines we don't want to store all their data in
 | 
						|
    // the settings file so just store the relevant metadata.
 | 
						|
    return {
 | 
						|
      id: this.id,
 | 
						|
      _name: this.name,
 | 
						|
      _isAppProvided: true,
 | 
						|
      _metaData: this._metaData,
 | 
						|
    };
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Initializes the engine.
 | 
						|
   *
 | 
						|
   * @param {object} [engineConfig]
 | 
						|
   *   The search engine configuration for application provided engines.
 | 
						|
   */
 | 
						|
  #init(engineConfig) {
 | 
						|
    this._orderHint = engineConfig.orderHint;
 | 
						|
    this._telemetryId = engineConfig.identifier;
 | 
						|
    this.#isGeneralPurposeSearchEngine =
 | 
						|
      engineConfig.classification == "general";
 | 
						|
 | 
						|
    if (engineConfig.charset) {
 | 
						|
      this._queryCharset = engineConfig.charset;
 | 
						|
    }
 | 
						|
 | 
						|
    if (engineConfig.telemetrySuffix) {
 | 
						|
      this._telemetryId += `-${engineConfig.telemetrySuffix}`;
 | 
						|
    }
 | 
						|
 | 
						|
    if (engineConfig.clickUrl) {
 | 
						|
      this.clickUrl = engineConfig.clickUrl;
 | 
						|
    }
 | 
						|
 | 
						|
    this._name = engineConfig.name.trim();
 | 
						|
    this._definedAliases =
 | 
						|
      engineConfig.aliases?.map(alias => `@${alias}`) ?? [];
 | 
						|
 | 
						|
    for (const [type, urlData] of Object.entries(engineConfig.urls)) {
 | 
						|
      this.#setUrl(type, urlData, engineConfig.partnerCode);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * This sets the urls for the search engine based on the supplied parameters.
 | 
						|
   *
 | 
						|
   * @param {string} type
 | 
						|
   *   The type of url. This could be a url for search, suggestions, or trending.
 | 
						|
   * @param {object} urlData
 | 
						|
   *   The url data contains the template/base url and url params.
 | 
						|
   * @param {string} partnerCode
 | 
						|
   *   The partner code associated with the search engine.
 | 
						|
   */
 | 
						|
  #setUrl(type, urlData, partnerCode) {
 | 
						|
    let urlType = AppProvidedSearchEngine.URL_TYPE_MAP.get(type);
 | 
						|
 | 
						|
    if (!urlType) {
 | 
						|
      console.warn("unexpected engine url type.", type);
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    let engineURL = new EngineURL(
 | 
						|
      urlType,
 | 
						|
      urlData.method || "GET",
 | 
						|
      urlData.base
 | 
						|
    );
 | 
						|
 | 
						|
    if (urlData.params) {
 | 
						|
      for (const param of urlData.params) {
 | 
						|
        switch (true) {
 | 
						|
          case "value" in param:
 | 
						|
            engineURL.addParam(
 | 
						|
              param.name,
 | 
						|
              param.value == "{partnerCode}" ? partnerCode : param.value
 | 
						|
            );
 | 
						|
            break;
 | 
						|
          case "experimentConfig" in param:
 | 
						|
            engineURL._addMozParam({
 | 
						|
              name: param.name,
 | 
						|
              pref: param.experimentConfig,
 | 
						|
              condition: "pref",
 | 
						|
            });
 | 
						|
            break;
 | 
						|
          case "searchAccessPoint" in param:
 | 
						|
            for (const [key, value] of Object.entries(
 | 
						|
              param.searchAccessPoint
 | 
						|
            )) {
 | 
						|
              engineURL.addParam(
 | 
						|
                param.name,
 | 
						|
                value,
 | 
						|
                key == "addressbar" ? "keyword" : key
 | 
						|
              );
 | 
						|
            }
 | 
						|
            break;
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    if (
 | 
						|
      !("searchTermParamName" in urlData) &&
 | 
						|
      !urlData.base.includes("{searchTerms}") &&
 | 
						|
      !urlType.includes("trending")
 | 
						|
    ) {
 | 
						|
      throw new Error("Search terms missing from engine URL.");
 | 
						|
    }
 | 
						|
 | 
						|
    if ("searchTermParamName" in urlData) {
 | 
						|
      // The search term parameter is always added last, which will add it to the
 | 
						|
      // end of the URL. This is because in the past we have seen users trying to
 | 
						|
      // modify their searches by altering the end of the URL.
 | 
						|
      engineURL.addParam(urlData.searchTermParamName, "{searchTerms}");
 | 
						|
    }
 | 
						|
 | 
						|
    this._urls.push(engineURL);
 | 
						|
  }
 | 
						|
}
 |