forked from mirrors/gecko-dev
		
	This change adds support for exposure based experiments by allowing a Nimbus variable/pref to specify the urlbar provider that should trigger an exposure event as well as a secondary boolean variable/pref that controls the visibility of the exposed result. The exposure should be registered when a result 'can be added' but may or may not be shown based on the value of the `displayExposureProvider` variable. * Add exposure event to metrics.yaml * Add new Nimbus variable (`exposureProvider`) to specify the urlbar providers that should trigger exposure events . * Add new Nimbus variable (`displayExposureProvider`) that controls the visibility of the provider results that matched the `exposureProvider` variable. Differential Revision: https://phabricator.services.mozilla.com/D174209
		
			
				
	
	
		
			382 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			382 lines
		
	
	
	
		
			13 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/. */
 | 
						|
 | 
						|
/**
 | 
						|
 * This module exports a urlbar result class, each representing a single result
 | 
						|
 * found by a provider that can be passed from the model to the view through
 | 
						|
 * the controller. It is mainly defined by a result type, and a payload,
 | 
						|
 * containing the data. A few getters allow to retrieve information common to all
 | 
						|
 * the result types.
 | 
						|
 */
 | 
						|
 | 
						|
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
 | 
						|
 | 
						|
const lazy = {};
 | 
						|
 | 
						|
ChromeUtils.defineESModuleGetters(lazy, {
 | 
						|
  JsonSchemaValidator:
 | 
						|
    "resource://gre/modules/components-utils/JsonSchemaValidator.sys.mjs",
 | 
						|
  UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
 | 
						|
  UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs",
 | 
						|
});
 | 
						|
 | 
						|
XPCOMUtils.defineLazyModuleGetters(lazy, {
 | 
						|
  BrowserUIUtils: "resource:///modules/BrowserUIUtils.jsm",
 | 
						|
});
 | 
						|
 | 
						|
/**
 | 
						|
 * Class used to create a single result.
 | 
						|
 */
 | 
						|
export class UrlbarResult {
 | 
						|
  /**
 | 
						|
   * Creates a result.
 | 
						|
   *
 | 
						|
   * @param {integer} resultType one of UrlbarUtils.RESULT_TYPE.* values
 | 
						|
   * @param {integer} resultSource one of UrlbarUtils.RESULT_SOURCE.* values
 | 
						|
   * @param {object} payload data for this result. A payload should always
 | 
						|
   *        contain a way to extract a final url to visit. The url getter
 | 
						|
   *        should have a case for each of the types.
 | 
						|
   * @param {object} [payloadHighlights] payload highlights, if any. Each
 | 
						|
   *        property in the payload may have a corresponding property in this
 | 
						|
   *        object. The value of each property should be an array of [index,
 | 
						|
   *        length] tuples. Each tuple indicates a substring in the corresponding
 | 
						|
   *        payload property.
 | 
						|
   */
 | 
						|
  constructor(resultType, resultSource, payload, payloadHighlights = {}) {
 | 
						|
    // Type describes the payload and visualization that should be used for
 | 
						|
    // this result.
 | 
						|
    if (!Object.values(lazy.UrlbarUtils.RESULT_TYPE).includes(resultType)) {
 | 
						|
      throw new Error("Invalid result type");
 | 
						|
    }
 | 
						|
    this.type = resultType;
 | 
						|
 | 
						|
    // Source describes which data has been used to derive this result. In case
 | 
						|
    // multiple sources are involved, use the more privacy restricted.
 | 
						|
    if (!Object.values(lazy.UrlbarUtils.RESULT_SOURCE).includes(resultSource)) {
 | 
						|
      throw new Error("Invalid result source");
 | 
						|
    }
 | 
						|
    this.source = resultSource;
 | 
						|
 | 
						|
    // UrlbarView is responsible for updating this.
 | 
						|
    this.rowIndex = -1;
 | 
						|
 | 
						|
    // May be used to indicate an heuristic result. Heuristic results can bypass
 | 
						|
    // source filters in the ProvidersManager, that otherwise may skip them.
 | 
						|
    this.heuristic = false;
 | 
						|
 | 
						|
    // Exposure specific properties. These allow us to track the exposure
 | 
						|
    // of a result through the query process.
 | 
						|
    // A non-zero value here indicates that this result's exposure should be
 | 
						|
    // recorded in the exposure event.
 | 
						|
    this.exposureResultType = "";
 | 
						|
 | 
						|
    // Determines if the exposure result should be hidden from the view.
 | 
						|
    this.exposureResultHidden = false;
 | 
						|
 | 
						|
    // The payload contains result data. Some of the data is common across
 | 
						|
    // multiple types, but most of it will vary.
 | 
						|
    if (!payload || typeof payload != "object") {
 | 
						|
      throw new Error("Invalid result payload");
 | 
						|
    }
 | 
						|
    this.payload = this.validatePayload(payload);
 | 
						|
 | 
						|
    if (!payloadHighlights || typeof payloadHighlights != "object") {
 | 
						|
      throw new Error("Invalid result payload highlights");
 | 
						|
    }
 | 
						|
    this.payloadHighlights = payloadHighlights;
 | 
						|
 | 
						|
    // Make sure every property in the payload has an array of highlights.  If a
 | 
						|
    // payload property does not have a highlights array, then give it one now.
 | 
						|
    // That way the consumer doesn't need to check whether it exists.
 | 
						|
    for (let name in payload) {
 | 
						|
      if (!(name in this.payloadHighlights)) {
 | 
						|
        this.payloadHighlights[name] = [];
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Returns a title that could be used as a label for this result.
 | 
						|
   *
 | 
						|
   * @returns {string} The label to show in a simplified title / url view.
 | 
						|
   */
 | 
						|
  get title() {
 | 
						|
    return this._titleAndHighlights[0];
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Returns an array of highlights for the title.
 | 
						|
   *
 | 
						|
   * @returns {Array} The array of highlights.
 | 
						|
   */
 | 
						|
  get titleHighlights() {
 | 
						|
    return this._titleAndHighlights[1];
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Returns an array [title, highlights].
 | 
						|
   *
 | 
						|
   * @returns {Array} The title and array of highlights.
 | 
						|
   */
 | 
						|
  get _titleAndHighlights() {
 | 
						|
    switch (this.type) {
 | 
						|
      case lazy.UrlbarUtils.RESULT_TYPE.KEYWORD:
 | 
						|
      case lazy.UrlbarUtils.RESULT_TYPE.TAB_SWITCH:
 | 
						|
      case lazy.UrlbarUtils.RESULT_TYPE.URL:
 | 
						|
      case lazy.UrlbarUtils.RESULT_TYPE.OMNIBOX:
 | 
						|
      case lazy.UrlbarUtils.RESULT_TYPE.REMOTE_TAB:
 | 
						|
        if (this.payload.qsSuggestion) {
 | 
						|
          return [
 | 
						|
            // We will initially only be targeting en-US users with this experiment
 | 
						|
            // but will need to change this to work properly with l10n.
 | 
						|
            this.payload.qsSuggestion + " — " + this.payload.title,
 | 
						|
            this.payloadHighlights.qsSuggestion,
 | 
						|
          ];
 | 
						|
        }
 | 
						|
 | 
						|
        if (this.payload.fallbackTitle) {
 | 
						|
          return [
 | 
						|
            this.payload.fallbackTitle,
 | 
						|
            this.payloadHighlights.fallbackTitle,
 | 
						|
          ];
 | 
						|
        }
 | 
						|
 | 
						|
        if (this.payload.title) {
 | 
						|
          return [this.payload.title, this.payloadHighlights.title];
 | 
						|
        }
 | 
						|
 | 
						|
        return [this.payload.url ?? "", this.payloadHighlights.url ?? []];
 | 
						|
      case lazy.UrlbarUtils.RESULT_TYPE.SEARCH:
 | 
						|
        if (this.payload.providesSearchMode) {
 | 
						|
          return ["", []];
 | 
						|
        }
 | 
						|
        if (this.payload.tail && this.payload.tailOffsetIndex >= 0) {
 | 
						|
          return [this.payload.tail, this.payloadHighlights.tail];
 | 
						|
        } else if (this.payload.suggestion) {
 | 
						|
          return [this.payload.suggestion, this.payloadHighlights.suggestion];
 | 
						|
        }
 | 
						|
        return [this.payload.query, this.payloadHighlights.query];
 | 
						|
      default:
 | 
						|
        return ["", []];
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Returns an icon url.
 | 
						|
   *
 | 
						|
   * @returns {string} url of the icon.
 | 
						|
   */
 | 
						|
  get icon() {
 | 
						|
    return this.payload.icon;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Returns whether the result's `suggestedIndex` property is defined.
 | 
						|
   * `suggestedIndex` is an optional hint to the muxer that can be set to
 | 
						|
   * suggest a specific position among the results.
 | 
						|
   *
 | 
						|
   * @returns {boolean} Whether `suggestedIndex` is defined.
 | 
						|
   */
 | 
						|
  get hasSuggestedIndex() {
 | 
						|
    return typeof this.suggestedIndex == "number";
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Returns the given payload if it's valid or throws an error if it's not.
 | 
						|
   * The schemas in UrlbarUtils.RESULT_PAYLOAD_SCHEMA are used for validation.
 | 
						|
   *
 | 
						|
   * @param {object} payload The payload object.
 | 
						|
   * @returns {object} `payload` if it's valid.
 | 
						|
   */
 | 
						|
  validatePayload(payload) {
 | 
						|
    let schema = lazy.UrlbarUtils.getPayloadSchema(this.type);
 | 
						|
    if (!schema) {
 | 
						|
      throw new Error(`Unrecognized result type: ${this.type}`);
 | 
						|
    }
 | 
						|
    let result = lazy.JsonSchemaValidator.validate(payload, schema, {
 | 
						|
      allowExplicitUndefinedProperties: true,
 | 
						|
      allowNullAsUndefinedProperties: true,
 | 
						|
      allowExtraProperties: this.type == lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC,
 | 
						|
    });
 | 
						|
    if (!result.valid) {
 | 
						|
      throw result.error;
 | 
						|
    }
 | 
						|
    return payload;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * A convenience function that takes a payload annotated with
 | 
						|
   * UrlbarUtils.HIGHLIGHT enums and returns the payload and the payload's
 | 
						|
   * highlights. Use this function when the highlighting required by your
 | 
						|
   * payload is based on simple substring matching, as done by
 | 
						|
   * UrlbarUtils.getTokenMatches(). Pass the return values as the `payload` and
 | 
						|
   * `payloadHighlights` params of the UrlbarResult constructor.
 | 
						|
   * `payloadHighlights` is optional. If omitted, payload will not be
 | 
						|
   * highlighted.
 | 
						|
   *
 | 
						|
   * If the payload doesn't have a title or has an empty title, and it also has
 | 
						|
   * a URL, then this function also sets the title to the URL's domain.
 | 
						|
   *
 | 
						|
   * @param {Array} tokens The tokens that should be highlighted in each of the
 | 
						|
   *        payload properties.
 | 
						|
   * @param {object} payloadInfo An object that looks like this:
 | 
						|
   *        { payloadPropertyName: payloadPropertyInfo }
 | 
						|
   *
 | 
						|
   *        Each payloadPropertyInfo may be either a string or an array.  If
 | 
						|
   *        it's a string, then the property value will be that string, and no
 | 
						|
   *        highlighting will be applied to it.  If it's an array, then it
 | 
						|
   *        should look like this: [payloadPropertyValue, highlightType].
 | 
						|
   *        payloadPropertyValue may be a string or an array of strings.  If
 | 
						|
   *        it's a string, then the payloadHighlights in the return value will
 | 
						|
   *        be an array of match highlights as described in
 | 
						|
   *        UrlbarUtils.getTokenMatches().  If it's an array, then
 | 
						|
   *        payloadHighlights will be an array of arrays of match highlights,
 | 
						|
   *        one element per element in payloadPropertyValue.
 | 
						|
   * @returns {Array} An array [payload, payloadHighlights].
 | 
						|
   */
 | 
						|
  static payloadAndSimpleHighlights(tokens, payloadInfo) {
 | 
						|
    // Convert scalar values in payloadInfo to [value] arrays.
 | 
						|
    for (let [name, info] of Object.entries(payloadInfo)) {
 | 
						|
      if (!Array.isArray(info)) {
 | 
						|
        payloadInfo[name] = [info];
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    if (
 | 
						|
      (!payloadInfo.title || !payloadInfo.title[0]) &&
 | 
						|
      !payloadInfo.fallbackTitle &&
 | 
						|
      payloadInfo.url &&
 | 
						|
      typeof payloadInfo.url[0] == "string"
 | 
						|
    ) {
 | 
						|
      // If there's no title, show the domain as the title.  Not all valid URLs
 | 
						|
      // have a domain.
 | 
						|
      payloadInfo.title = payloadInfo.title || [
 | 
						|
        "",
 | 
						|
        lazy.UrlbarUtils.HIGHLIGHT.TYPED,
 | 
						|
      ];
 | 
						|
      try {
 | 
						|
        payloadInfo.title[0] = new URL(payloadInfo.url[0]).host;
 | 
						|
      } catch (e) {}
 | 
						|
    }
 | 
						|
 | 
						|
    if (payloadInfo.url) {
 | 
						|
      // For display purposes we need to unescape the url.
 | 
						|
      payloadInfo.displayUrl = [...payloadInfo.url];
 | 
						|
      let url = payloadInfo.displayUrl[0];
 | 
						|
      if (url && lazy.UrlbarPrefs.get("trimURLs")) {
 | 
						|
        url = lazy.BrowserUIUtils.removeSingleTrailingSlashFromURL(url);
 | 
						|
        if (url.startsWith("https://")) {
 | 
						|
          url = url.substring(8);
 | 
						|
          if (url.startsWith("www.")) {
 | 
						|
            url = url.substring(4);
 | 
						|
          }
 | 
						|
        }
 | 
						|
      }
 | 
						|
      payloadInfo.displayUrl[0] = lazy.UrlbarUtils.unEscapeURIForUI(url);
 | 
						|
    }
 | 
						|
 | 
						|
    // For performance reasons limit excessive string lengths, to reduce the
 | 
						|
    // amount of string matching we do here, and avoid wasting resources to
 | 
						|
    // handle long textruns that the user would never see anyway.
 | 
						|
    for (let prop of ["displayUrl", "title", "suggestion"]) {
 | 
						|
      let val = payloadInfo[prop]?.[0];
 | 
						|
      if (typeof val == "string") {
 | 
						|
        payloadInfo[prop][0] = val.substring(
 | 
						|
          0,
 | 
						|
          lazy.UrlbarUtils.MAX_TEXT_LENGTH
 | 
						|
        );
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    let entries = Object.entries(payloadInfo);
 | 
						|
    return [
 | 
						|
      entries.reduce((payload, [name, [val, _]]) => {
 | 
						|
        payload[name] = val;
 | 
						|
        return payload;
 | 
						|
      }, {}),
 | 
						|
      entries.reduce((highlights, [name, [val, highlightType]]) => {
 | 
						|
        if (highlightType) {
 | 
						|
          highlights[name] = !Array.isArray(val)
 | 
						|
            ? lazy.UrlbarUtils.getTokenMatches(tokens, val || "", highlightType)
 | 
						|
            : val.map(subval =>
 | 
						|
                lazy.UrlbarUtils.getTokenMatches(tokens, subval, highlightType)
 | 
						|
              );
 | 
						|
        }
 | 
						|
        return highlights;
 | 
						|
      }, {}),
 | 
						|
    ];
 | 
						|
  }
 | 
						|
 | 
						|
  static _dynamicResultTypesByName = new Map();
 | 
						|
 | 
						|
  /**
 | 
						|
   * Registers a dynamic result type.  Dynamic result types are types that are
 | 
						|
   * created at runtime, for example by an extension.  A particular type should
 | 
						|
   * be added only once; if this method is called for a type more than once, the
 | 
						|
   * `type` in the last call overrides those in previous calls.
 | 
						|
   *
 | 
						|
   * @param {string} name
 | 
						|
   *   The name of the type.  This is used in CSS selectors, so it shouldn't
 | 
						|
   *   contain any spaces or punctuation except for -, _, etc.
 | 
						|
   * @param {object} type
 | 
						|
   *   An object that describes the type.  Currently types do not have any
 | 
						|
   *   associated metadata, so this object should be empty.
 | 
						|
   */
 | 
						|
  static addDynamicResultType(name, type = {}) {
 | 
						|
    if (/[^a-z0-9_-]/i.test(name)) {
 | 
						|
      this.logger.error(`Illegal dynamic type name: ${name}`);
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    this._dynamicResultTypesByName.set(name, type);
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Unregisters a dynamic result type.
 | 
						|
   *
 | 
						|
   * @param {string} name
 | 
						|
   *   The name of the type.
 | 
						|
   */
 | 
						|
  static removeDynamicResultType(name) {
 | 
						|
    let type = this._dynamicResultTypesByName.get(name);
 | 
						|
    if (type) {
 | 
						|
      this._dynamicResultTypesByName.delete(name);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Returns an object describing a registered dynamic result type.
 | 
						|
   *
 | 
						|
   * @param {string} name
 | 
						|
   *   The name of the type.
 | 
						|
   * @returns {object}
 | 
						|
   *   Currently types do not have any associated metadata, so the return value
 | 
						|
   *   is an empty object if the type exists.  If the type doesn't exist,
 | 
						|
   *   undefined is returned.
 | 
						|
   */
 | 
						|
  static getDynamicResultType(name) {
 | 
						|
    return this._dynamicResultTypesByName.get(name);
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * This is useful for logging results. If you need the full payload, then it's
 | 
						|
   * better to JSON.stringify the result object itself.
 | 
						|
   *
 | 
						|
   * @returns {string} string representation of the result.
 | 
						|
   */
 | 
						|
  toString() {
 | 
						|
    if (this.payload.url) {
 | 
						|
      return this.payload.title + " - " + this.payload.url.substr(0, 100);
 | 
						|
    }
 | 
						|
    if (this.payload.keyword) {
 | 
						|
      return this.payload.keyword + " - " + this.payload.query;
 | 
						|
    }
 | 
						|
    if (this.payload.suggestion) {
 | 
						|
      return this.payload.engine + " - " + this.payload.suggestion;
 | 
						|
    }
 | 
						|
    if (this.payload.engine) {
 | 
						|
      return this.payload.engine + " - " + this.payload.query;
 | 
						|
    }
 | 
						|
    return JSON.stringify(this);
 | 
						|
  }
 | 
						|
}
 |