tor-browser

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

ActionsProviderContextualSearch.sys.mjs (12323B)


      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 import { UrlbarUtils } from "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs";
      6 
      7 import {
      8  ActionsProvider,
      9  ActionsResult,
     10 } from "moz-src:///browser/components/urlbar/ActionsProvider.sys.mjs";
     11 
     12 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
     13 
     14 const lazy = {};
     15 
     16 ChromeUtils.defineESModuleGetters(lazy, {
     17  BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
     18  OpenSearchEngine:
     19    "moz-src:///toolkit/components/search/OpenSearchEngine.sys.mjs",
     20  OpenSearchManager:
     21    "moz-src:///browser/components/search/OpenSearchManager.sys.mjs",
     22  loadAndParseOpenSearchEngine:
     23    "moz-src:///toolkit/components/search/OpenSearchLoader.sys.mjs",
     24  PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
     25  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
     26  UrlbarPrefs: "moz-src:///browser/components/urlbar/UrlbarPrefs.sys.mjs",
     27  UrlbarSearchUtils:
     28    "moz-src:///browser/components/urlbar/UrlbarSearchUtils.sys.mjs",
     29 });
     30 
     31 const ENABLED_PREF = "contextualSearch.enabled";
     32 
     33 const INSTALLED_ENGINE = "installed-engine";
     34 const OPEN_SEARCH_ENGINE = "opensearch-engine";
     35 const CONTEXTUAL_SEARCH_ENGINE = "contextual-search-engine";
     36 
     37 const DEFAULT_ICON = "chrome://browser/skin/search-engine-placeholder@2x.png";
     38 
     39 /**
     40 * A provider that returns an option for using the search engine provided
     41 * by the active view if it utilizes OpenSearch.
     42 */
     43 class ProviderContextualSearch extends ActionsProvider {
     44  // Cache the results of engines looked up by host, these can be
     45  // expensive lookups and we don't want to redo the query every time
     46  // the user types when the result will not change.
     47  #hostEngines = new Map();
     48  // Cache the result of the query that checks whether an engines domain
     49  // has been visited recently. We only want to show engines the user
     50  // is using.
     51  #visitedEngineDomains = new Map();
     52 
     53  // Store the engine returned to the user in case they select it.
     54  #resultEngine = null;
     55 
     56  #placesObserver = null;
     57 
     58  constructor() {
     59    super();
     60 
     61    this.#placesObserver = new PlacesWeakCallbackWrapper(
     62      this.handlePlacesEvents.bind(this)
     63    );
     64 
     65    PlacesObservers.addListener(["history-cleared"], this.#placesObserver);
     66  }
     67 
     68  get name() {
     69    return "ActionsProviderContextualSearch";
     70  }
     71 
     72  isActive(queryContext) {
     73    return (
     74      queryContext.trimmedSearchString &&
     75      lazy.UrlbarPrefs.getScotchBonnetPref(ENABLED_PREF) &&
     76      !queryContext.searchMode &&
     77      lazy.UrlbarPrefs.get("suggest.engines")
     78    );
     79  }
     80 
     81  async queryActions(queryContext) {
     82    this.#resultEngine = await this.matchEngine(queryContext);
     83    if (this.#resultEngine) {
     84      return [await this.#createActionResult(this.#resultEngine)];
     85    }
     86    return null;
     87  }
     88 
     89  onSearchSessionEnd() {
     90    // We cache the results for a host while the user is typing, clear
     91    // when the search session ends as the results for the host may
     92    // change by the next search session.
     93    this.#hostEngines.clear();
     94  }
     95 
     96  async #createActionResult({ type, engine, key = "contextual-search" }) {
     97    let icon = engine?.icon || (await engine?.getIconURL?.()) || DEFAULT_ICON;
     98    let result = {
     99      key,
    100      l10nId: "urlbar-result-search-with",
    101      l10nArgs: { engine: engine.name || engine.title },
    102      icon,
    103      onPick: (context, controller) => {
    104        this.pickAction(context, controller);
    105      },
    106    };
    107 
    108    if (type == INSTALLED_ENGINE) {
    109      result.engine = engine.name;
    110      result.dataset = { providesSearchMode: true };
    111    }
    112 
    113    return new ActionsResult(result);
    114  }
    115 
    116  /*
    117   * Searches for engines that we want to present to the user based on their
    118   * current host and the search query they have entered.
    119   */
    120  async matchEngine(queryContext) {
    121    // First find currently installed engines that match the current query
    122    // if the user has DuckDuckGo installed and types "duck", offer that.
    123    let engine = await this.#matchTabToSearchEngine(queryContext);
    124    if (engine) {
    125      return engine;
    126    }
    127 
    128    // Don't match the default engine for non-query-matches.
    129    let defaultEngine = queryContext.isPrivate
    130      ? Services.search.defaultPrivateEngine
    131      : Services.search.defaultEngine;
    132 
    133    let browser =
    134      lazy.BrowserWindowTracker.getTopWindow()?.gBrowser.selectedBrowser;
    135    if (!browser) {
    136      return null;
    137    }
    138 
    139    let host;
    140    try {
    141      host = UrlbarUtils.stripPrefixAndTrim(browser.currentURI.host, {
    142        stripWww: true,
    143      })[0];
    144    } catch (e) {
    145      // about: pages will throw when access currentURI.host, ignore.
    146    }
    147 
    148    // Find engines based on the current host.
    149    if (host && !this.#hostEngines.has(host)) {
    150      // Find currently installed engines that match the current host. If
    151      // the user is on wikipedia.com, offer that.
    152      let hostEngine = await this.#matchInstalledEngine(host);
    153 
    154      if (!hostEngine) {
    155        // Find engines in the search configuration but not installed that match
    156        // the current host. If the user is on ecosia.com and starts searching
    157        // offer ecosia's search.
    158        let contextualEngineConfig =
    159          await Services.search.findContextualSearchEngineByHost(host);
    160        if (contextualEngineConfig) {
    161          hostEngine = {
    162            type: CONTEXTUAL_SEARCH_ENGINE,
    163            engine: contextualEngineConfig,
    164          };
    165        }
    166      }
    167      // Cache the result against this host so we do not need to rerun
    168      // the same query every keystroke.
    169      this.#hostEngines.set(host, hostEngine);
    170      if (hostEngine && hostEngine.engine.name != defaultEngine.name) {
    171        return hostEngine;
    172      }
    173    } else if (host) {
    174      let cachedEngine = this.#hostEngines.get(host);
    175      if (cachedEngine && cachedEngine.engine.name != defaultEngine.name) {
    176        return cachedEngine;
    177      }
    178    }
    179 
    180    // Lastly match any openSearch
    181    if (browser) {
    182      let openSearchEngines = lazy.OpenSearchManager.getEngines(browser);
    183      // We don't need to check if the engine has the same name as the
    184      // default engine because OpenSearchManager already handles that.
    185      if (openSearchEngines.length) {
    186        return { type: OPEN_SEARCH_ENGINE, engine: openSearchEngines[0] };
    187      }
    188    }
    189 
    190    return null;
    191  }
    192 
    193  /**
    194   * Called from `onLocationChange` in browser.js. It is used to update
    195   * the cache for `visitedEngineDomains` so we can avoid expensive places
    196   * queries.
    197   *
    198   * @param {window} window
    199   *  The browser window where the location change happened.
    200   * @param {nsIURI} uri
    201   *  The URI being navigated to.
    202   * @param {nsIWebProgress} _webProgress
    203   *   The progress object, which can have event listeners added to it.
    204   * @param {number} _flags
    205   *   Load flags. See nsIWebProgressListener.idl for possible values.
    206   */
    207  async onLocationChange(window, uri, _webProgress, _flags) {
    208    try {
    209      if (this.#visitedEngineDomains.has(uri.host)) {
    210        this.#visitedEngineDomains.set(uri.host, true);
    211      }
    212    } catch (e) {}
    213  }
    214 
    215  async #matchInstalledEngine(query) {
    216    let engines = await lazy.UrlbarSearchUtils.enginesForDomainPrefix(query, {
    217      matchAllDomainLevels: true,
    218    });
    219    if (engines.length) {
    220      return { type: INSTALLED_ENGINE, engine: engines[0] };
    221    }
    222    return null;
    223  }
    224 
    225  /*
    226   * Matches a users search query to the name of an installed engine.
    227   */
    228  async #matchTabToSearchEngine(queryContext) {
    229    let searchStr = queryContext.trimmedSearchString.toLocaleLowerCase();
    230 
    231    for (let engine of await Services.search.getVisibleEngines()) {
    232      if (
    233        engine.name.toLocaleLowerCase().startsWith(searchStr) &&
    234        ((await this.#shouldskipRecentVisitCheck(searchStr)) ||
    235          (await this.#engineDomainHasRecentVisits(engine.searchUrlDomain)))
    236      ) {
    237        return {
    238          type: INSTALLED_ENGINE,
    239          engine,
    240          key: "matched-contextual-search",
    241        };
    242      }
    243    }
    244    return null;
    245  }
    246 
    247  /*
    248   * Check that an engines domain has been visited within the last 30 days
    249   * before providing as a match to the users query.
    250   */
    251  async #engineDomainHasRecentVisits(host) {
    252    if (this.#visitedEngineDomains.has(host)) {
    253      return this.#visitedEngineDomains.get(host);
    254    }
    255 
    256    let db = await lazy.PlacesUtils.promiseLargeCacheDBConnection();
    257    let rows = await db.executeCached(
    258      `
    259      SELECT 1 FROM moz_places
    260        WHERE rev_host BETWEEN get_unreversed_host(:host || '.') || '.' AND get_unreversed_host(:host || '.') || '/'
    261        AND (foreign_count > 0
    262          OR last_visit_date > strftime('%s','now','localtime','start of day','-30 days','utc') * 1000000)
    263      LIMIT 1;`,
    264      { host }
    265    );
    266 
    267    let visited = !!rows.length;
    268    this.#visitedEngineDomains.set(host, visited);
    269    return visited;
    270  }
    271 
    272  async #shouldskipRecentVisitCheck(query) {
    273    // If the user has entered enough characters they are very likely looking for
    274    // the engine, this avoids confusion for users searching for engines they have
    275    // not visited.
    276    if (query.length > 3) {
    277      return true;
    278    }
    279    // If we do not store history we cannot check whether an engine has been
    280    // visited, in that case we show the engines when matching.
    281    return (
    282      Services.prefs.getBoolPref("places.history.enabled", true) &&
    283      !(
    284        Services.prefs.getBoolPref("privacy.clearOnShutdown.history") ||
    285        lazy.PrivateBrowsingUtils.permanentPrivateBrowsing
    286      )
    287    );
    288  }
    289 
    290  async pickAction(queryContext, controller, _element) {
    291    let { type, engine } = this.#resultEngine;
    292 
    293    if (type == OPEN_SEARCH_ENGINE) {
    294      let originAttributes;
    295      try {
    296        let currentURI = Services.io.newURI(queryContext.currentPage);
    297        originAttributes = {
    298          firstPartyDomain: Services.eTLD.getSchemelessSite(currentURI),
    299        };
    300      } catch {}
    301      let openSearchEngineData = await lazy.loadAndParseOpenSearchEngine(
    302        Services.io.newURI(engine.uri),
    303        null,
    304        originAttributes
    305      );
    306      engine = new lazy.OpenSearchEngine({
    307        engineData: openSearchEngineData,
    308        originAttributes,
    309      });
    310    }
    311 
    312    this.#performSearch(
    313      engine,
    314      queryContext.searchString,
    315      controller.input,
    316      type == INSTALLED_ENGINE
    317    );
    318 
    319    if (
    320      // Do not show the install prompt in non-private windows to have
    321      // consistent behaviour with private windows and avoid linkability
    322      // concerns. tor-browser#44134.
    323      // Maybe re-enable as part of tor-browser#44117.
    324      !AppConstants.BASE_BROWSER_VERSION &&
    325      !queryContext.isPrivate &&
    326      type != INSTALLED_ENGINE &&
    327      (await Services.search.shouldShowInstallPrompt(engine))
    328    ) {
    329      this.#showInstallPrompt(controller, engine);
    330    }
    331  }
    332 
    333  handlePlacesEvents(_events) {
    334    this.#visitedEngineDomains.clear();
    335  }
    336 
    337  async #performSearch(engine, search, input, enterSearchMode) {
    338    const [url] = UrlbarUtils.getSearchQueryUrl(engine, search);
    339    if (enterSearchMode) {
    340      input.search(search, { searchEngine: engine });
    341    }
    342    input.window.gBrowser.fixupAndLoadURIString(url, {
    343      triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
    344    });
    345    input.window.gBrowser.selectedBrowser.focus();
    346  }
    347 
    348  #showInstallPrompt(controller, engineData) {
    349    let win = controller.input.window;
    350    let buttons = [
    351      {
    352        "l10n-id": "install-search-engine-add",
    353        callback() {
    354          Services.search.addSearchEngine(engineData);
    355        },
    356      },
    357      {
    358        "l10n-id": "install-search-engine-no",
    359        callback() {},
    360      },
    361    ];
    362 
    363    win.gNotificationBox.appendNotification(
    364      "install-search-engine",
    365      {
    366        label: {
    367          "l10n-id": "install-search-engine",
    368          "l10n-args": { engineName: engineData.name },
    369        },
    370        image: "chrome://global/skin/icons/question-64.png",
    371        priority: win.gNotificationBox.PRIORITY_INFO_LOW,
    372      },
    373      buttons
    374    );
    375  }
    376 
    377  QueryInterface = ChromeUtils.generateQI([Ci.nsISupportsWeakReference]);
    378 }
    379 
    380 export var ActionsProviderContextualSearch = new ProviderContextualSearch();