tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

UrlbarResult.sys.mjs (13770B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 /**
      6 * This module exports a urlbar result class, each representing a single result
      7 * found by a provider that can be passed from the model to the view through
      8 * the controller. It is mainly defined by a result type, and a payload,
      9 * containing the data. A few getters allow to retrieve information common to all
     10 * the result types.
     11 */
     12 
     13 const lazy = {};
     14 
     15 ChromeUtils.defineESModuleGetters(lazy, {
     16  JsonSchemaValidator:
     17    "resource://gre/modules/components-utils/JsonSchemaValidator.sys.mjs",
     18  ObjectUtils: "resource://gre/modules/ObjectUtils.sys.mjs",
     19  UrlbarUtils: "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs",
     20 });
     21 
     22 /**
     23 * @typedef UrlbarAutofillData
     24 * @property {string} value
     25 *   The value to insert for autofill.
     26 * @property {number} selectionStart
     27 *   Where to start the selection for the autofill.
     28 * @property {number} selectionEnd
     29 *   Where to end the selection for the autofill.
     30 * @property {string} [type]
     31 *   The type of the autofill.
     32 * @property {string} [adaptiveHistoryInput]
     33 *   The input string associated with this autofill item.
     34 */
     35 
     36 /**
     37 * Class used to create a single result.
     38 */
     39 export class UrlbarResult {
     40  /**
     41   * @typedef {{ [name: string]: any }} Payload
     42   *
     43   * @typedef {typeof lazy.UrlbarUtils.HIGHLIGHT} HighlightType
     44   * @typedef {Array<[number, number]>} HighlightIndexes e.g. [[index, length],,]
     45   * @typedef {Record<string, HighlightType | HighlightIndexes>} Highlights
     46   */
     47 
     48  /**
     49   * @param {object} params
     50   * @param {Values<typeof lazy.UrlbarUtils.RESULT_TYPE>} params.type
     51   * @param {Values<typeof lazy.UrlbarUtils.RESULT_SOURCE>} params.source
     52   * @param {UrlbarAutofillData} [params.autofill]
     53   * @param {number} [params.exposureTelemetry]
     54   * @param {Values<typeof lazy.UrlbarUtils.RESULT_GROUP>} [params.group]
     55   * @param {boolean} [params.heuristic]
     56   * @param {boolean} [params.hideRowLabel]
     57   * @param {boolean} [params.isBestMatch]
     58   * @param {boolean} [params.isRichSuggestion]
     59   * @param {boolean} [params.isSuggestedIndexRelativeToGroup]
     60   * @param {string} [params.providerName]
     61   * @param {number} [params.resultSpan]
     62   * @param {number} [params.richSuggestionIconSize]
     63   * @param {string} [params.richSuggestionIconVariation]
     64   * @param {string} [params.rowLabel]
     65   * @param {boolean} [params.showFeedbackMenu]
     66   * @param {number} [params.suggestedIndex]
     67   * @param {Payload} [params.payload]
     68   * @param {Highlights} [params.highlights]
     69   * @param {boolean} [params.testForceNewContent] Used for test only.
     70   */
     71  constructor({
     72    type,
     73    source,
     74    autofill,
     75    exposureTelemetry = lazy.UrlbarUtils.EXPOSURE_TELEMETRY.NONE,
     76    group,
     77    heuristic = false,
     78    hideRowLabel = false,
     79    isBestMatch = false,
     80    isRichSuggestion = false,
     81    isSuggestedIndexRelativeToGroup = false,
     82    providerName,
     83    resultSpan,
     84    richSuggestionIconSize,
     85    richSuggestionIconVariation,
     86    rowLabel,
     87    showFeedbackMenu = false,
     88    suggestedIndex,
     89    payload,
     90    highlights = null,
     91    testForceNewContent,
     92  }) {
     93    // Type describes the payload and visualization that should be used for
     94    // this result.
     95    if (!Object.values(lazy.UrlbarUtils.RESULT_TYPE).includes(type)) {
     96      throw new Error("Invalid result type");
     97    }
     98    this.#type = type;
     99 
    100    // Source describes which data has been used to derive this result. In case
    101    // multiple sources are involved, use the more privacy restricted.
    102    if (!Object.values(lazy.UrlbarUtils.RESULT_SOURCE).includes(source)) {
    103      throw new Error("Invalid result source");
    104    }
    105    this.#source = source;
    106 
    107    // The payload contains result data. Some of the data is common across
    108    // multiple types, but most of it will vary.
    109    if (!payload || typeof payload != "object") {
    110      throw new Error("Invalid result payload");
    111    }
    112 
    113    payload = Object.fromEntries(
    114      Object.entries(payload).filter(([_, v]) => v != undefined)
    115    );
    116 
    117    if (highlights) {
    118      this.#highlights = Object.freeze(highlights);
    119    }
    120 
    121    this.#payload = this.#validatePayload(payload);
    122 
    123    this.#autofill = autofill;
    124    this.#exposureTelemetry = exposureTelemetry;
    125    this.#group = group;
    126    this.#heuristic = heuristic;
    127    this.#hideRowLabel = hideRowLabel;
    128    this.#isBestMatch = isBestMatch;
    129    this.#isRichSuggestion = isRichSuggestion;
    130    this.#isSuggestedIndexRelativeToGroup = isSuggestedIndexRelativeToGroup;
    131    this.#richSuggestionIconSize = richSuggestionIconSize;
    132    this.#richSuggestionIconVariation = richSuggestionIconVariation;
    133    this.#providerName = providerName;
    134    this.#resultSpan = resultSpan;
    135    this.#rowLabel = rowLabel;
    136    this.#showFeedbackMenu = showFeedbackMenu;
    137    this.#suggestedIndex = suggestedIndex;
    138 
    139    if (this.#type == lazy.UrlbarUtils.RESULT_TYPE.TIP) {
    140      this.#isRichSuggestion = true;
    141      this.#richSuggestionIconSize = 24;
    142    }
    143 
    144    this.#testForceNewContent = testForceNewContent;
    145  }
    146 
    147  /**
    148   * @type {number}
    149   *   The index of the row where this result is in the suggestions. This is
    150   *   updated by UrlbarView when new result sets are displayed.
    151   */
    152  rowIndex = undefined;
    153 
    154  get type() {
    155    return this.#type;
    156  }
    157 
    158  get source() {
    159    return this.#source;
    160  }
    161 
    162  get autofill() {
    163    return this.#autofill;
    164  }
    165 
    166  get exposureTelemetry() {
    167    return this.#exposureTelemetry;
    168  }
    169  set exposureTelemetry(value) {
    170    this.#exposureTelemetry = value;
    171  }
    172 
    173  get group() {
    174    return this.#group;
    175  }
    176 
    177  get heuristic() {
    178    return this.#heuristic;
    179  }
    180 
    181  get hideRowLabel() {
    182    return this.#hideRowLabel;
    183  }
    184 
    185  get isBestMatch() {
    186    return this.#isBestMatch;
    187  }
    188 
    189  get isRichSuggestion() {
    190    return this.#isRichSuggestion;
    191  }
    192  set isRichSuggestion(value) {
    193    this.#isRichSuggestion = value;
    194  }
    195 
    196  get isSuggestedIndexRelativeToGroup() {
    197    return this.#isSuggestedIndexRelativeToGroup;
    198  }
    199  set isSuggestedIndexRelativeToGroup(value) {
    200    this.#isSuggestedIndexRelativeToGroup = value;
    201  }
    202 
    203  get providerName() {
    204    return this.#providerName;
    205  }
    206  set providerName(value) {
    207    this.#providerName = value;
    208  }
    209 
    210  /**
    211   * The type of the UrlbarProvider providing the result.
    212   *
    213   * @type {?Values<typeof lazy.UrlbarUtils.PROVIDER_TYPE>}
    214   */
    215  get providerType() {
    216    return this.#providerType;
    217  }
    218  set providerType(value) {
    219    this.#providerType = value;
    220  }
    221 
    222  get resultSpan() {
    223    return this.#resultSpan;
    224  }
    225 
    226  get richSuggestionIconSize() {
    227    return this.#richSuggestionIconSize;
    228  }
    229 
    230  get richSuggestionIconVariation() {
    231    return this.#richSuggestionIconVariation;
    232  }
    233  set richSuggestionIconSize(value) {
    234    this.#richSuggestionIconSize = value;
    235  }
    236 
    237  get rowLabel() {
    238    return this.#rowLabel;
    239  }
    240 
    241  get showFeedbackMenu() {
    242    return this.#showFeedbackMenu;
    243  }
    244 
    245  get suggestedIndex() {
    246    return this.#suggestedIndex;
    247  }
    248  set suggestedIndex(value) {
    249    this.#suggestedIndex = value;
    250  }
    251 
    252  get payload() {
    253    return this.#payload;
    254  }
    255 
    256  get testForceNewContent() {
    257    return this.#testForceNewContent;
    258  }
    259 
    260  /**
    261   * Returns an icon url.
    262   *
    263   * @returns {string} url of the icon.
    264   */
    265  get icon() {
    266    return this.payload.icon;
    267  }
    268 
    269  /**
    270   * Returns whether the result's `suggestedIndex` property is defined.
    271   * `suggestedIndex` is an optional hint to the muxer that can be set to
    272   * suggest a specific position among the results.
    273   *
    274   * @returns {boolean} Whether `suggestedIndex` is defined.
    275   */
    276  get hasSuggestedIndex() {
    277    return typeof this.suggestedIndex == "number";
    278  }
    279 
    280  /**
    281   * Convenience getter that returns whether the result's exposure telemetry
    282   * indicates it should be hidden.
    283   *
    284   * @returns {boolean}
    285   *   Whether the result should be hidden.
    286   */
    287  get isHiddenExposure() {
    288    return this.exposureTelemetry == lazy.UrlbarUtils.EXPOSURE_TELEMETRY.HIDDEN;
    289  }
    290 
    291  /**
    292   * Get value and highlights of given payloadName that can display in the view.
    293   *
    294   * @param {string} payloadName
    295   *   The payload name to want to get the value.
    296   * @param {object} options
    297   * @param {object} [options.tokens]
    298   *   Make highlighting that matches this tokens.
    299   *   If no specific tokens, this function returns only value.
    300   * @param {object} [options.isURL]
    301   *   If true, the value will be from UrlbarUtils.prepareUrlForDisplay().
    302   */
    303  getDisplayableValueAndHighlights(payloadName, options = {}) {
    304    if (!this.#displayValuesCache) {
    305      this.#displayValuesCache = new Map();
    306    }
    307 
    308    if (this.#displayValuesCache.has(payloadName)) {
    309      let cached = this.#displayValuesCache.get(payloadName);
    310      // If the different options are specified, ignore the cache.
    311      // NOTE: If options.tokens is undefined, use cache as it is.
    312      if (
    313        options.isURL == cached.options.isURL &&
    314        (options.tokens == undefined ||
    315          lazy.ObjectUtils.deepEqual(options.tokens, cached.options.tokens))
    316      ) {
    317        return this.#displayValuesCache.get(payloadName);
    318      }
    319    }
    320 
    321    let value = this.payload[payloadName];
    322    if (!value) {
    323      return {};
    324    }
    325 
    326    if (options.isURL) {
    327      value = lazy.UrlbarUtils.prepareUrlForDisplay(value);
    328    }
    329 
    330    if (typeof value == "string") {
    331      value = value.substring(0, lazy.UrlbarUtils.MAX_TEXT_LENGTH);
    332    }
    333 
    334    if (Array.isArray(this.#highlights?.[payloadName])) {
    335      return { value, highlights: this.#highlights[payloadName] };
    336    }
    337 
    338    let highlightType = this.#highlights?.[payloadName];
    339 
    340    if (!options.tokens?.length || !highlightType) {
    341      let cached = { value, options };
    342      this.#displayValuesCache.set(payloadName, cached);
    343      return cached;
    344    }
    345 
    346    let highlights = Array.isArray(value)
    347      ? value.map(subval =>
    348          lazy.UrlbarUtils.getTokenMatches(
    349            options.tokens,
    350            subval,
    351            highlightType
    352          )
    353        )
    354      : lazy.UrlbarUtils.getTokenMatches(options.tokens, value, highlightType);
    355 
    356    let cached = { value, highlights, options };
    357    this.#displayValuesCache.set(payloadName, cached);
    358    return cached;
    359  }
    360 
    361  /**
    362   * Returns the given payload if it's valid or throws an error if it's not.
    363   * The schemas in UrlbarUtils.RESULT_PAYLOAD_SCHEMA are used for validation.
    364   *
    365   * @param {object} payload The payload object.
    366   * @returns {object} `payload` if it's valid.
    367   */
    368  #validatePayload(payload) {
    369    let schema = lazy.UrlbarUtils.getPayloadSchema(this.type);
    370    if (!schema) {
    371      throw new Error(`Unrecognized result type: ${this.type}`);
    372    }
    373    let result = lazy.JsonSchemaValidator.validate(payload, schema, {
    374      allowExplicitUndefinedProperties: true,
    375      allowNullAsUndefinedProperties: true,
    376      allowAdditionalProperties:
    377        this.type == lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC,
    378    });
    379    if (!result.valid) {
    380      throw result.error;
    381    }
    382    return payload;
    383  }
    384 
    385  static _dynamicResultTypesByName = new Map();
    386 
    387  /**
    388   * Registers a dynamic result type.  Dynamic result types are types that are
    389   * created at runtime, for example by an extension.  A particular type should
    390   * be added only once; if this method is called for a type more than once, the
    391   * `type` in the last call overrides those in previous calls.
    392   *
    393   * @param {string} name
    394   *   The name of the type.  This is used in CSS selectors, so it shouldn't
    395   *   contain any spaces or punctuation except for -, _, etc.
    396   * @param {object} type
    397   *   An object that describes the type.  Currently types do not have any
    398   *   associated metadata, so this object should be empty.
    399   */
    400  static addDynamicResultType(name, type = {}) {
    401    if (/[^a-z0-9_-]/i.test(name)) {
    402      console.error(`Illegal dynamic type name: ${name}`);
    403      return;
    404    }
    405    this._dynamicResultTypesByName.set(name, type);
    406  }
    407 
    408  /**
    409   * Unregisters a dynamic result type.
    410   *
    411   * @param {string} name
    412   *   The name of the type.
    413   */
    414  static removeDynamicResultType(name) {
    415    let type = this._dynamicResultTypesByName.get(name);
    416    if (type) {
    417      this._dynamicResultTypesByName.delete(name);
    418    }
    419  }
    420 
    421  /**
    422   * Returns an object describing a registered dynamic result type.
    423   *
    424   * @param {string} name
    425   *   The name of the type.
    426   * @returns {object}
    427   *   Currently types do not have any associated metadata, so the return value
    428   *   is an empty object if the type exists.  If the type doesn't exist,
    429   *   undefined is returned.
    430   */
    431  static getDynamicResultType(name) {
    432    return this._dynamicResultTypesByName.get(name);
    433  }
    434 
    435  /**
    436   * This is useful for logging results. If you need the full payload, then it's
    437   * better to JSON.stringify the result object itself.
    438   *
    439   * @returns {string} string representation of the result.
    440   */
    441  toString() {
    442    if (this.payload.url) {
    443      return this.payload.title + " - " + this.payload.url.substr(0, 100);
    444    }
    445    if (this.payload.keyword) {
    446      return this.payload.keyword + " - " + this.payload.query;
    447    }
    448    if (this.payload.suggestion) {
    449      return this.payload.engine + " - " + this.payload.suggestion;
    450    }
    451    if (this.payload.engine) {
    452      return this.payload.engine + " - " + this.payload.query;
    453    }
    454    return JSON.stringify(this);
    455  }
    456 
    457  #type;
    458  #source;
    459  #autofill;
    460  #exposureTelemetry;
    461  #group;
    462  #heuristic;
    463  #hideRowLabel;
    464  #isBestMatch;
    465  #isRichSuggestion;
    466  #isSuggestedIndexRelativeToGroup;
    467  #providerName;
    468  #providerType;
    469  #resultSpan;
    470  #richSuggestionIconSize;
    471  #richSuggestionIconVariation;
    472  #rowLabel;
    473  #showFeedbackMenu;
    474  #suggestedIndex;
    475  #payload;
    476  #highlights;
    477  #displayValuesCache;
    478  #testForceNewContent;
    479 }