tor-browser

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

UrlbarProviderTabToSearch.sys.mjs (12603B)


      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 provider that offers a search engine when the user is
      7 * typing a search engine domain.
      8 */
      9 
     10 import {
     11  UrlbarProvider,
     12  UrlbarUtils,
     13 } from "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs";
     14 
     15 const lazy = {};
     16 
     17 ChromeUtils.defineESModuleGetters(lazy, {
     18  ActionsProviderContextualSearch:
     19    "moz-src:///browser/components/urlbar/ActionsProviderContextualSearch.sys.mjs",
     20  UrlbarView: "moz-src:///browser/components/urlbar/UrlbarView.sys.mjs",
     21  UrlbarPrefs: "moz-src:///browser/components/urlbar/UrlbarPrefs.sys.mjs",
     22  UrlbarProviderAutofill:
     23    "moz-src:///browser/components/urlbar/UrlbarProviderAutofill.sys.mjs",
     24  UrlbarProviderGlobalActions:
     25    "moz-src:///browser/components/urlbar/UrlbarProviderGlobalActions.sys.mjs",
     26  UrlbarResult: "moz-src:///browser/components/urlbar/UrlbarResult.sys.mjs",
     27  UrlbarSearchUtils:
     28    "moz-src:///browser/components/urlbar/UrlbarSearchUtils.sys.mjs",
     29  UrlUtils: "resource://gre/modules/UrlUtils.sys.mjs",
     30 });
     31 
     32 const DYNAMIC_RESULT_TYPE = "onboardTabToSearch";
     33 const VIEW_TEMPLATE = {
     34  attributes: {
     35    selectable: true,
     36  },
     37  children: [
     38    {
     39      name: "no-wrap",
     40      tag: "span",
     41      classList: ["urlbarView-no-wrap"],
     42      children: [
     43        {
     44          name: "icon",
     45          tag: "img",
     46          classList: ["urlbarView-favicon"],
     47        },
     48        {
     49          name: "text-container",
     50          tag: "span",
     51          children: [
     52            {
     53              name: "first-row-container",
     54              tag: "span",
     55              children: [
     56                {
     57                  name: "title",
     58                  tag: "span",
     59                  classList: ["urlbarView-title"],
     60                  children: [
     61                    {
     62                      name: "titleStrong",
     63                      tag: "strong",
     64                    },
     65                  ],
     66                },
     67                {
     68                  name: "title-separator",
     69                  tag: "span",
     70                  classList: ["urlbarView-title-separator"],
     71                },
     72                {
     73                  name: "action",
     74                  tag: "span",
     75                  classList: ["urlbarView-action"],
     76                  attributes: {
     77                    "slide-in": true,
     78                  },
     79                },
     80              ],
     81            },
     82            {
     83              name: "description",
     84              tag: "span",
     85            },
     86          ],
     87        },
     88      ],
     89    },
     90  ],
     91 };
     92 
     93 /**
     94 * Initializes this provider's dynamic result. To be called after the creation
     95 *  of the provider singleton.
     96 */
     97 function initializeDynamicResult() {
     98  lazy.UrlbarResult.addDynamicResultType(DYNAMIC_RESULT_TYPE);
     99  lazy.UrlbarView.addDynamicViewTemplate(DYNAMIC_RESULT_TYPE, VIEW_TEMPLATE);
    100 }
    101 
    102 /**
    103 * Class used to create the provider.
    104 */
    105 export class UrlbarProviderTabToSearch extends UrlbarProvider {
    106  static onboardingInteractionAtTime = null;
    107 
    108  constructor() {
    109    super();
    110  }
    111 
    112  /**
    113   * @returns {Values<typeof UrlbarUtils.PROVIDER_TYPE>}
    114   */
    115  get type() {
    116    return UrlbarUtils.PROVIDER_TYPE.PROFILE;
    117  }
    118 
    119  /**
    120   * Whether this provider should be invoked for the given context.
    121   * If this method returns false, the providers manager won't start a query
    122   * with this provider, to save on resources.
    123   *
    124   * @param {UrlbarQueryContext} queryContext The query context object
    125   */
    126  async isActive(queryContext) {
    127    return (
    128      queryContext.searchString &&
    129      queryContext.tokens.length == 1 &&
    130      !queryContext.searchMode &&
    131      lazy.UrlbarPrefs.get("suggest.engines") &&
    132      !(
    133        (await this.queryInstance
    134          .getProvider(lazy.UrlbarProviderGlobalActions.name)
    135          ?.isActive(queryContext)) &&
    136        lazy.ActionsProviderContextualSearch.isActive(queryContext)
    137      )
    138    );
    139  }
    140 
    141  /**
    142   * Gets the provider's priority.
    143   *
    144   * @returns {number} The provider's priority for the given query.
    145   */
    146  getPriority() {
    147    return 0;
    148  }
    149 
    150  /**
    151   * This is called only for dynamic result types, when the urlbar view updates
    152   * the view of one of the results of the provider.  It should return an object
    153   * describing the view update.
    154   *
    155   * @param {UrlbarResult} result The result whose view will be updated.
    156   * @returns {object} An object describing the view update.
    157   */
    158  getViewUpdate(result) {
    159    return {
    160      icon: {
    161        attributes: {
    162          src: result.payload.icon,
    163        },
    164      },
    165      titleStrong: {
    166        l10n: {
    167          id: "urlbar-result-action-search-w-engine",
    168          args: {
    169            engine: result.payload.engine,
    170          },
    171        },
    172      },
    173      action: {
    174        l10n: {
    175          id: result.payload.isGeneralPurposeEngine
    176            ? "urlbar-result-action-tabtosearch-web"
    177            : "urlbar-result-action-tabtosearch-other-engine",
    178          args: {
    179            engine: result.payload.engine,
    180          },
    181        },
    182      },
    183      description: {
    184        l10n: {
    185          id: "urlbar-tabtosearch-onboard",
    186        },
    187      },
    188    };
    189  }
    190 
    191  /**
    192   * Called when a result from the provider is selected. "Selected" refers to
    193   * the user highlighing the result with the arrow keys/Tab, before it is
    194   * picked. onSelection is also called when a user clicks a result. In the
    195   * event of a click, onSelection is called just before onEngagement.
    196   *
    197   * @param {UrlbarResult} result
    198   *   The result that was selected.
    199   */
    200  onSelection(result) {
    201    // We keep track of the number of times the user interacts with
    202    // tab-to-search onboarding results so we stop showing them after
    203    // `tabToSearch.onboard.interactionsLeft` interactions.
    204    // Also do not increment the counter if the result was interacted with less
    205    // than 5 minutes ago. This is a guard against the user running up the
    206    // counter by interacting with the same result repeatedly.
    207    if (
    208      result.payload.dynamicType &&
    209      (!UrlbarProviderTabToSearch.onboardingInteractionAtTime ||
    210        UrlbarProviderTabToSearch.onboardingInteractionAtTime <
    211          Date.now() - 1000 * 60 * 5)
    212    ) {
    213      let interactionsLeft = lazy.UrlbarPrefs.get(
    214        "tabToSearch.onboard.interactionsLeft"
    215      );
    216 
    217      if (interactionsLeft > 0) {
    218        lazy.UrlbarPrefs.set(
    219          "tabToSearch.onboard.interactionsLeft",
    220          --interactionsLeft
    221        );
    222      }
    223 
    224      UrlbarProviderTabToSearch.onboardingInteractionAtTime = Date.now();
    225    }
    226  }
    227 
    228  onEngagement(queryContext, controller, details) {
    229    let { result, element } = details;
    230    if (result.type == UrlbarUtils.RESULT_TYPE.DYNAMIC) {
    231      // Confirm search mode, but only for the onboarding (dynamic) result. The
    232      // input will handle confirming search mode for the non-onboarding
    233      // `RESULT_TYPE.SEARCH` result since it sets `providesSearchMode`.
    234      element.ownerGlobal.gURLBar.maybeConfirmSearchModeFromResult({
    235        result,
    236        checkValue: false,
    237      });
    238    }
    239  }
    240 
    241  /**
    242   * Defines whether the view should defer user selection events while waiting
    243   * for the first result from this provider.
    244   *
    245   * @returns {boolean} Whether the provider wants to defer user selection
    246   *          events.
    247   */
    248  get deferUserSelection() {
    249    return true;
    250  }
    251 
    252  /**
    253   * Starts querying.
    254   *
    255   * @param {UrlbarQueryContext} queryContext
    256   * @param {(provider: UrlbarProvider, result: UrlbarResult) => void} addCallback
    257   *   Callback invoked by the provider to add a new result.
    258   */
    259  async startQuery(queryContext, addCallback) {
    260    // enginesForDomainPrefix only matches against engine domains.
    261    // Remove trailing slashes and www. from the search string and check if the
    262    // resulting string is worth matching.
    263    let [searchStr] = UrlbarUtils.stripPrefixAndTrim(
    264      queryContext.searchString,
    265      {
    266        stripWww: true,
    267        trimSlash: true,
    268      }
    269    );
    270    // Skip any string that cannot be an origin.
    271    if (
    272      !lazy.UrlUtils.looksLikeOrigin(searchStr, {
    273        ignoreKnownDomains: true,
    274        noIp: true,
    275      })
    276    ) {
    277      return;
    278    }
    279 
    280    // Also remove the public suffix, if present, to allow for partial matches.
    281    if (searchStr.includes(".")) {
    282      searchStr = UrlbarUtils.stripPublicSuffixFromHost(searchStr);
    283    }
    284 
    285    // Add all matching engines.
    286    let engines = await lazy.UrlbarSearchUtils.enginesForDomainPrefix(
    287      searchStr,
    288      {
    289        matchAllDomainLevels: true,
    290      }
    291    );
    292    if (!engines.length) {
    293      return;
    294    }
    295 
    296    const onboardingInteractionsLeft = lazy.UrlbarPrefs.get(
    297      "tabToSearch.onboard.interactionsLeft"
    298    );
    299 
    300    // If the engine host begins with the search string, autofill may happen
    301    // for it, and the Muxer will retain the result only if there's a matching
    302    // autofill heuristic result.
    303    // Otherwise, we may have a partial match, where the search string is at
    304    // the boundary of a host part, for example "wiki" in "en.wikipedia.org".
    305    // We put those engines apart, and later we check if their host satisfies
    306    // the autofill threshold. If they do, we mark them with the
    307    // "satisfiesAutofillThreshold" payload property, so the muxer can avoid
    308    // filtering them out.
    309    let partialMatchEnginesByHost = new Map();
    310 
    311    for (let engine of engines) {
    312      // Trim the engine host. This will also be set as the result url, so the
    313      // Muxer can use it to filter.
    314      let [host] = UrlbarUtils.stripPrefixAndTrim(engine.searchUrlDomain, {
    315        stripWww: true,
    316      });
    317      // Check if the host may be autofilled.
    318      if (host.startsWith(searchStr.toLocaleLowerCase())) {
    319        if (onboardingInteractionsLeft > 0) {
    320          addCallback(this, makeOnboardingResult(engine));
    321        } else {
    322          addCallback(this, makeResult(queryContext, engine));
    323        }
    324        continue;
    325      }
    326 
    327      // Otherwise it may be a partial match that would not be autofilled.
    328      if (host.includes("." + searchStr.toLocaleLowerCase())) {
    329        partialMatchEnginesByHost.set(engine.searchUrlDomain, engine);
    330        // Don't continue here, we are looking for more partial matches.
    331      }
    332      // We also try to match the base domain of the searchUrlDomain,
    333      // because otherwise for an engine like rakuten, we'd check pt.afl.rakuten.co.jp
    334      // which redirects and is thus not saved in the history resulting in a low score.
    335 
    336      let baseDomain = Services.eTLD.getBaseDomainFromHost(
    337        engine.searchUrlDomain
    338      );
    339      if (baseDomain.startsWith(searchStr)) {
    340        partialMatchEnginesByHost.set(baseDomain, engine);
    341      }
    342    }
    343    if (partialMatchEnginesByHost.size) {
    344      let host = await lazy.UrlbarProviderAutofill.getTopHostOverThreshold(
    345        queryContext,
    346        Array.from(partialMatchEnginesByHost.keys())
    347      );
    348      if (host) {
    349        let engine = partialMatchEnginesByHost.get(host);
    350        if (onboardingInteractionsLeft > 0) {
    351          addCallback(this, makeOnboardingResult(engine, true));
    352        } else {
    353          addCallback(this, makeResult(queryContext, engine, true));
    354        }
    355      }
    356    }
    357  }
    358 }
    359 
    360 function makeOnboardingResult(engine, satisfiesAutofillThreshold = false) {
    361  return new lazy.UrlbarResult({
    362    type: UrlbarUtils.RESULT_TYPE.DYNAMIC,
    363    source: UrlbarUtils.RESULT_SOURCE.SEARCH,
    364    resultSpan: 2,
    365    suggestedIndex: 1,
    366    payload: {
    367      engine: engine.name,
    368      searchUrlDomainWithoutSuffix: searchUrlDomainWithoutSuffix(engine),
    369      providesSearchMode: true,
    370      icon: UrlbarUtils.ICON.SEARCH_GLASS,
    371      dynamicType: DYNAMIC_RESULT_TYPE,
    372      satisfiesAutofillThreshold,
    373    },
    374  });
    375 }
    376 
    377 function makeResult(context, engine, satisfiesAutofillThreshold = false) {
    378  return new lazy.UrlbarResult({
    379    type: UrlbarUtils.RESULT_TYPE.SEARCH,
    380    source: UrlbarUtils.RESULT_SOURCE.SEARCH,
    381    suggestedIndex: 1,
    382    payload: {
    383      engine: engine.name,
    384      isGeneralPurposeEngine: engine.isGeneralPurposeEngine,
    385      searchUrlDomainWithoutSuffix: searchUrlDomainWithoutSuffix(engine),
    386      providesSearchMode: true,
    387      icon: UrlbarUtils.ICON.SEARCH_GLASS,
    388      query: "",
    389      satisfiesAutofillThreshold,
    390    },
    391  });
    392 }
    393 
    394 function searchUrlDomainWithoutSuffix(engine) {
    395  let [value] = UrlbarUtils.stripPrefixAndTrim(engine.searchUrlDomain, {
    396    stripWww: true,
    397  });
    398  return value.substr(0, value.length - engine.searchUrlPublicSuffix.length);
    399 }
    400 
    401 initializeDynamicResult();