tor-browser

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

UrlbarProviderHeuristicFallback.sys.mjs (11408B)


      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 provides a heuristic result. The result
      7 * either vists a URL or does a search with the current engine. This result is
      8 * always the ultimate fallback for any query, so this provider is always active.
      9 */
     10 
     11 import {
     12  UrlbarProvider,
     13  UrlbarUtils,
     14 } from "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs";
     15 
     16 const lazy = {};
     17 
     18 ChromeUtils.defineESModuleGetters(lazy, {
     19  UrlbarPrefs: "moz-src:///browser/components/urlbar/UrlbarPrefs.sys.mjs",
     20  UrlbarResult: "moz-src:///browser/components/urlbar/UrlbarResult.sys.mjs",
     21  UrlbarSearchUtils:
     22    "moz-src:///browser/components/urlbar/UrlbarSearchUtils.sys.mjs",
     23  UrlbarTokenizer:
     24    "moz-src:///browser/components/urlbar/UrlbarTokenizer.sys.mjs",
     25  UrlUtils: "resource://gre/modules/UrlUtils.sys.mjs",
     26 });
     27 
     28 /**
     29 * Class used to create the provider.
     30 */
     31 export class UrlbarProviderHeuristicFallback extends UrlbarProvider {
     32  constructor() {
     33    super();
     34  }
     35 
     36  /**
     37   * @returns {Values<typeof UrlbarUtils.PROVIDER_TYPE>}
     38   */
     39  get type() {
     40    return UrlbarUtils.PROVIDER_TYPE.HEURISTIC;
     41  }
     42 
     43  /**
     44   * Whether this provider should be invoked for the given context.
     45   * If this method returns false, the providers manager won't start a query
     46   * with this provider, to save on resources.
     47   *
     48   * @param {UrlbarQueryContext} queryContext
     49   */
     50  async isActive(queryContext) {
     51    return !!queryContext.searchString.length;
     52  }
     53 
     54  /**
     55   * Gets the provider's priority.
     56   *
     57   * @returns {number} The provider's priority for the given query.
     58   */
     59  getPriority() {
     60    return 0;
     61  }
     62 
     63  /**
     64   * Starts querying.
     65   *
     66   * @param {UrlbarQueryContext} queryContext
     67   * @param {(provider: UrlbarProvider, result: UrlbarResult) => void} addCallback
     68   *   Callback invoked by the provider to add a new result.
     69   */
     70  async startQuery(queryContext, addCallback) {
     71    let instance = this.queryInstance;
     72 
     73    if (queryContext.sapName != "searchbar") {
     74      let result = this._matchUnknownUrl(queryContext);
     75      if (result) {
     76        addCallback(this, result);
     77        // Since we can't tell if this is a real URL and whether the user wants
     78        // to visit or search for it, we provide an alternative searchengine
     79        // match if the string looks like an alphanumeric origin or an e-mail.
     80        let str = queryContext.searchString;
     81        if (!URL.canParse(str)) {
     82          if (
     83            lazy.UrlbarPrefs.get("keyword.enabled") &&
     84            (lazy.UrlUtils.looksLikeOrigin(str, {
     85              noIp: true,
     86              noPort: true,
     87            }) ||
     88              lazy.UrlUtils.REGEXP_COMMON_EMAIL.test(str))
     89          ) {
     90            let searchResult = await this._engineSearchResult({ queryContext });
     91            if (instance != this.queryInstance) {
     92              return;
     93            }
     94            addCallback(this, searchResult);
     95          }
     96        }
     97        return;
     98      }
     99    }
    100 
    101    let result = await this._searchModeKeywordResult(queryContext);
    102    if (instance != this.queryInstance) {
    103      return;
    104    }
    105    if (result) {
    106      addCallback(this, result);
    107      return;
    108    }
    109 
    110    if (
    111      queryContext.sapName == "searchbar" ||
    112      lazy.UrlbarPrefs.get("keyword.enabled") ||
    113      queryContext.restrictSource == UrlbarUtils.RESULT_SOURCE.SEARCH ||
    114      queryContext.searchMode
    115    ) {
    116      result = await this._engineSearchResult({
    117        queryContext,
    118        heuristic: true,
    119      });
    120      if (instance != this.queryInstance) {
    121        return;
    122      }
    123      if (result) {
    124        addCallback(this, result);
    125      }
    126    }
    127  }
    128 
    129  // TODO (bug 1054814): Use visited URLs to inform which scheme to use, if the
    130  // scheme isn't specificed.
    131  _matchUnknownUrl(queryContext) {
    132    // The user may have typed something like "word?" to run a search.  We
    133    // should not convert that to a URL.  We should also never convert actual
    134    // URLs into URL results when search mode is active or a search mode
    135    // restriction token was typed.
    136    if (
    137      queryContext.restrictSource == UrlbarUtils.RESULT_SOURCE.SEARCH ||
    138      lazy.UrlbarTokenizer.SEARCH_MODE_RESTRICT.has(
    139        queryContext.restrictToken?.value
    140      ) ||
    141      queryContext.searchMode
    142    ) {
    143      return null;
    144    }
    145 
    146    let unescapedSearchString = UrlbarUtils.unEscapeURIForUI(
    147      queryContext.searchString
    148    );
    149    let [prefix, suffix] = UrlbarUtils.stripURLPrefix(unescapedSearchString);
    150    if (!suffix && prefix) {
    151      // The user just typed a stripped protocol, don't build a non-sense url
    152      // like http://http/ for it.
    153      return null;
    154    }
    155 
    156    let searchUrl = queryContext.trimmedSearchString;
    157 
    158    if (queryContext.fixupError) {
    159      if (
    160        queryContext.fixupError == Cr.NS_ERROR_MALFORMED_URI &&
    161        !lazy.UrlbarPrefs.get("keyword.enabled")
    162      ) {
    163        return new lazy.UrlbarResult({
    164          type: UrlbarUtils.RESULT_TYPE.URL,
    165          source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
    166          heuristic: true,
    167          payload: {
    168            title: searchUrl,
    169            url: searchUrl,
    170          },
    171        });
    172      }
    173 
    174      return null;
    175    }
    176 
    177    // If the URI cannot be fixed or the preferred URI would do a keyword search,
    178    // that basically means this isn't useful to us. Note that
    179    // fixupInfo.keywordAsSent will never be true if the keyword.enabled pref
    180    // is false or there are no engines, so in that case we will always return
    181    // a "visit".
    182    if (!queryContext.fixupInfo?.href || queryContext.fixupInfo?.isSearch) {
    183      return null;
    184    }
    185 
    186    let uri = new URL(queryContext.fixupInfo.href);
    187    // Check the host, as "http:///" is a valid nsIURI, but not useful to us.
    188    // But, some schemes are expected to have no host. So we check just against
    189    // schemes we know should have a host. This allows new schemes to be
    190    // implemented without us accidentally blocking access to them.
    191    let hostExpected = ["http:", "https:", "ftp:", "chrome:"].includes(
    192      uri.protocol
    193    );
    194    if (hostExpected && !uri.host) {
    195      return null;
    196    }
    197 
    198    // getFixupURIInfo() escaped the URI, so it may not be pretty.  Embed the
    199    // escaped URL in the result since that URL should be "canonical".  But
    200    // pass the pretty, unescaped URL as the result's title, since it is
    201    // displayed to the user.
    202    let escapedURL = uri.toString();
    203    let displayURL = UrlbarUtils.prepareUrlForDisplay(uri, {
    204      trimURL: false,
    205      // If the user didn't type a protocol, and we added one, don't show it,
    206      // as https-first may upgrade it, potentially breaking expectations.
    207      schemeless: !prefix,
    208    });
    209 
    210    // We don't know if this url is in Places or not, and checking that would
    211    // be expensive. Thus we also don't know if we may have an icon.
    212    // If we'd just try to fetch the icon for the typed string, we'd cause icon
    213    // flicker, since the url keeps changing while the user types.
    214    // By default we won't provide an icon, but for the subset of urls with a
    215    // host we'll check for a typed slash and set favicon for the host part.
    216    let iconUri;
    217    if (hostExpected && (searchUrl.endsWith("/") || uri.pathname.length > 1)) {
    218      // Look for an icon with the entire URL except for the pathname, including
    219      // scheme, usernames, passwords, hostname, and port.
    220      let pathIndex = uri.toString().lastIndexOf(uri.pathname);
    221      let prePath = uri.toString().slice(0, pathIndex);
    222      iconUri = `page-icon:${prePath}/`;
    223    }
    224 
    225    return new lazy.UrlbarResult({
    226      type: UrlbarUtils.RESULT_TYPE.URL,
    227      source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
    228      heuristic: true,
    229      payload: {
    230        title: displayURL,
    231        url: escapedURL,
    232        icon: iconUri,
    233      },
    234    });
    235  }
    236 
    237  async _searchModeKeywordResult(queryContext) {
    238    if (!queryContext.tokens.length || queryContext.sapName != "urlbar") {
    239      return null;
    240    }
    241 
    242    let firstToken = queryContext.tokens[0].value;
    243    if (!lazy.UrlbarTokenizer.SEARCH_MODE_RESTRICT.has(firstToken)) {
    244      return null;
    245    }
    246 
    247    // At this point, the search string starts with a token that can be
    248    // converted into search mode.
    249    // Now we need to determine what to do based on the remainder of the search
    250    // string.  If the remainder starts with a space, then we should enter
    251    // search mode, so we should continue below and create the result.
    252    // Otherwise, we should not enter search mode, and in that case, the search
    253    // string will look like one of the following:
    254    //
    255    // * The search string ends with the restriction token (e.g., the user
    256    //   has typed only the token by itself, with no trailing spaces).
    257    // * More tokens exist, but there's no space between the restriction
    258    //   token and the following token.  This is possible because the tokenizer
    259    //   does not require spaces between a restriction token and the remainder
    260    //   of the search string.  In this case, we should not enter search mode.
    261    //
    262    // If we return null here and thereby do not enter search mode, then we'll
    263    // continue on to _engineSearchResult, and the heuristic will be a
    264    // default engine search result.
    265    let query = UrlbarUtils.substringAfter(
    266      queryContext.searchString,
    267      firstToken
    268    );
    269    if (!lazy.UrlUtils.REGEXP_SPACES_START.test(query)) {
    270      return null;
    271    }
    272 
    273    if (queryContext.restrictSource == UrlbarUtils.RESULT_SOURCE.SEARCH) {
    274      return await this._engineSearchResult({
    275        queryContext,
    276        keyword: firstToken,
    277        heuristic: true,
    278      });
    279    }
    280 
    281    query = query.trimStart();
    282    return new lazy.UrlbarResult({
    283      type: UrlbarUtils.RESULT_TYPE.SEARCH,
    284      source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
    285      heuristic: true,
    286      payload: {
    287        query,
    288        title: query,
    289        keyword: firstToken,
    290      },
    291    });
    292  }
    293 
    294  async _engineSearchResult({
    295    queryContext,
    296    keyword = null,
    297    heuristic = false,
    298  }) {
    299    let engine;
    300    if (queryContext.searchMode?.engineName) {
    301      engine = lazy.UrlbarSearchUtils.getEngineByName(
    302        queryContext.searchMode.engineName
    303      );
    304    } else {
    305      engine = lazy.UrlbarSearchUtils.getDefaultEngine(queryContext.isPrivate);
    306    }
    307 
    308    if (!engine) {
    309      return null;
    310    }
    311 
    312    // Strip a leading search restriction char, because we prepend it to text
    313    // when the search shortcut is used and it's not user typed. Don't strip
    314    // other restriction chars, so that it's possible to search for things
    315    // including one of those (e.g. "c#").
    316    let query = queryContext.searchString;
    317    if (
    318      queryContext.tokens[0] &&
    319      queryContext.tokens[0].value === lazy.UrlbarTokenizer.RESTRICT.SEARCH
    320    ) {
    321      query = UrlbarUtils.substringAfter(
    322        query,
    323        queryContext.tokens[0].value
    324      ).trim();
    325    }
    326 
    327    return new lazy.UrlbarResult({
    328      type: UrlbarUtils.RESULT_TYPE.SEARCH,
    329      source: UrlbarUtils.RESULT_SOURCE.SEARCH,
    330      heuristic,
    331      payload: {
    332        engine: engine.name,
    333        icon: UrlbarUtils.ICON.SEARCH_GLASS,
    334        query,
    335        title: query,
    336        keyword,
    337      },
    338      highlights: {
    339        engine: UrlbarUtils.HIGHLIGHT.TYPED,
    340      },
    341    });
    342  }
    343 }