tor-browser

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

SearchUIUtils.sys.mjs (16936B)


      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 * Various utilities for search related UI.
      7 */
      8 
      9 /**
     10 * @import { SearchUtils } from "moz-src:///toolkit/components/search/SearchUtils.sys.mjs"
     11 * @import { UrlbarInput } from "chrome://browser/content/urlbar/UrlbarInput.mjs";
     12 */
     13 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
     14 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
     15 
     16 const lazy = XPCOMUtils.declareLazy({
     17  BrowserSearchTelemetry:
     18    "moz-src:///browser/components/search/BrowserSearchTelemetry.sys.mjs",
     19  BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
     20  BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
     21  CustomizableUI:
     22    "moz-src:///browser/components/customizableui/CustomizableUI.sys.mjs",
     23  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
     24  SearchUtils: "moz-src:///toolkit/components/search/SearchUtils.sys.mjs",
     25  SearchUIUtilsL10n: () => {
     26    return new Localization(["browser/search.ftl", "branding/brand.ftl"]);
     27  },
     28 });
     29 
     30 export var SearchUIUtils = {
     31  initialized: false,
     32 
     33  init() {
     34    if (!this.initialized) {
     35      Services.obs.addObserver(this, "browser-search-engine-modified");
     36      this.initialized = true;
     37    }
     38  },
     39 
     40  observe(engine, topic, data) {
     41    switch (data) {
     42      case "engine-default":
     43        this.updatePlaceholderNamePreference(engine, false);
     44        break;
     45      case "engine-default-private":
     46        this.updatePlaceholderNamePreference(engine, true);
     47        break;
     48    }
     49  },
     50 
     51  /**
     52   * This function is called by the category manager for the
     53   * `search-service-notification` category.
     54   *
     55   * It allows the SearchService (in toolkit) to display
     56   * notifications in the browser for certain events.
     57   *
     58   * @param {string} notificationType
     59   *   Determines the function displaying the notification.
     60   * @param  {...any} args
     61   *   The arguments for that function.
     62   */
     63  showSearchServiceNotification(notificationType, ...args) {
     64    switch (notificationType) {
     65      case "search-engine-removal": {
     66        let [oldEngine, newEngine] = args;
     67        this.removalOfSearchEngineNotificationBox(oldEngine, newEngine);
     68        break;
     69      }
     70      case "search-settings-reset": {
     71        let [newEngine] = args;
     72        this.searchSettingsResetNotificationBox(newEngine);
     73        break;
     74      }
     75    }
     76  },
     77 
     78  /**
     79   * Infobar to notify the user's search engine has been removed
     80   * and replaced with an application default search engine.
     81   *
     82   * @param {string} oldEngine
     83   *   name of the engine to be moved and replaced.
     84   * @param {string} newEngine
     85   *   name of the application default engine to replaced the removed engine.
     86   */
     87  async removalOfSearchEngineNotificationBox(oldEngine, newEngine) {
     88    let win = lazy.BrowserWindowTracker.getTopWindow({
     89      allowFromInactiveWorkspace: true,
     90    });
     91 
     92    let buttons = [
     93      {
     94        "l10n-id": "remove-search-engine-button",
     95        primary: true,
     96        callback() {
     97          const notificationBox = win.gNotificationBox.getNotificationWithValue(
     98            "search-engine-removal"
     99          );
    100          win.gNotificationBox.removeNotification(notificationBox);
    101        },
    102      },
    103      {
    104        supportPage: "search-engine-removal",
    105      },
    106    ];
    107 
    108    await win.gNotificationBox.appendNotification(
    109      "search-engine-removal",
    110      {
    111        label: {
    112          "l10n-id": "removed-search-engine-message2",
    113          "l10n-args": { oldEngine, newEngine },
    114        },
    115        priority: win.gNotificationBox.PRIORITY_SYSTEM,
    116      },
    117      buttons
    118    );
    119 
    120    // _updatePlaceholderFromDefaultEngine only updates the pref if the search service
    121    // hasn't finished initializing, so we explicitly update it here to be sure.
    122    SearchUIUtils.updatePlaceholderNamePreference(
    123      await Services.search.getDefault(),
    124      false
    125    );
    126    SearchUIUtils.updatePlaceholderNamePreference(
    127      await Services.search.getDefaultPrivate(),
    128      true
    129    );
    130 
    131    for (let openWin of lazy.BrowserWindowTracker.orderedWindows) {
    132      openWin.gURLBar
    133        ?._updatePlaceholderFromDefaultEngine()
    134        .catch(console.error);
    135    }
    136  },
    137 
    138  /**
    139   * Infobar informing the user that the search settings had to be reset
    140   * and what their new default engine is.
    141   *
    142   * @param {string} newEngine
    143   *   Name of the new default engine.
    144   */
    145  async searchSettingsResetNotificationBox(newEngine) {
    146    let win = lazy.BrowserWindowTracker.getTopWindow({
    147      allowFromInactiveWorkspace: true,
    148    });
    149 
    150    let buttons = [
    151      {
    152        "l10n-id": "reset-search-settings-button",
    153        primary: true,
    154        callback() {
    155          const notificationBox = win.gNotificationBox.getNotificationWithValue(
    156            "search-settings-reset"
    157          );
    158          win.gNotificationBox.removeNotification(notificationBox);
    159        },
    160      },
    161      {
    162        supportPage: "prefs-search",
    163      },
    164    ];
    165 
    166    await win.gNotificationBox.appendNotification(
    167      "search-settings-reset",
    168      {
    169        label: {
    170          "l10n-id": "reset-search-settings-message",
    171          "l10n-args": { newEngine },
    172        },
    173        priority: win.gNotificationBox.PRIORITY_SYSTEM,
    174      },
    175      buttons
    176    );
    177  },
    178 
    179  /**
    180   * Adds an open search engine and handles error UI.
    181   *
    182   * @param {string} locationURL
    183   *   The URL where the OpenSearch definition is located.
    184   * @param {string} image
    185   *   A URL string to an icon file to be used as the search engine's
    186   *   icon. This value may be overridden by an icon specified in the
    187   *   engine description file.
    188   * @param {object} browsingContext
    189   *   The browsing context any error prompt should be opened for.
    190   * @returns {Promise<boolean>}
    191   *   Returns true if the engine was added.
    192   */
    193  async addOpenSearchEngine(locationURL, image, browsingContext) {
    194    try {
    195      await Services.search.addOpenSearchEngine(
    196        locationURL,
    197        image,
    198        browsingContext?.embedderElement?.contentPrincipal?.originAttributes
    199      );
    200    } catch (ex) {
    201      let titleMsgName;
    202      let descMsgName;
    203      switch (ex.result) {
    204        case Ci.nsISearchService.ERROR_DUPLICATE_ENGINE:
    205          titleMsgName = "opensearch-error-duplicate-title";
    206          descMsgName = "opensearch-error-duplicate-desc";
    207          break;
    208        case Ci.nsISearchService.ERROR_ENGINE_CORRUPTED:
    209          titleMsgName = "opensearch-error-format-title";
    210          descMsgName = "opensearch-error-format-desc";
    211          break;
    212        default:
    213          // i.e. ERROR_DOWNLOAD_FAILURE
    214          titleMsgName = "opensearch-error-download-title";
    215          descMsgName = "opensearch-error-download-desc";
    216          break;
    217      }
    218 
    219      let [title, text] = await lazy.SearchUIUtilsL10n.formatValues([
    220        {
    221          id: titleMsgName,
    222        },
    223        {
    224          id: descMsgName,
    225          args: {
    226            "location-url": locationURL,
    227          },
    228        },
    229      ]);
    230 
    231      Services.prompt.alertBC(
    232        browsingContext,
    233        Ci.nsIPrompt.MODAL_TYPE_CONTENT,
    234        title,
    235        text
    236      );
    237      return false;
    238    }
    239    return true;
    240  },
    241 
    242  /**
    243   * Returns the URL to use for where to get more search engines.
    244   *
    245   * @returns {string}
    246   */
    247  get searchEnginesURL() {
    248    return Services.urlFormatter.formatURLPref(
    249      "browser.search.searchEnginesURL"
    250    );
    251  },
    252 
    253  /**
    254   * Update the placeholderName preference for the default search engine.
    255   *
    256   * @param {nsISearchEngine} engine The new default search engine.
    257   * @param {boolean} isPrivate Whether this change applies to private windows.
    258   */
    259  updatePlaceholderNamePreference(engine, isPrivate) {
    260    const prefName =
    261      "browser.urlbar.placeholderName" + (isPrivate ? ".private" : "");
    262    if (engine.isConfigEngine) {
    263      Services.prefs.setStringPref(prefName, engine.name);
    264    } else {
    265      Services.prefs.clearUserPref(prefName);
    266    }
    267  },
    268 
    269  /**
    270   * Focuses the search bar if present on the toolbar, or the address bar,
    271   * putting it in search mode. Will do so in an existing non-popup browser
    272   * window or open a new one if necessary.
    273   *
    274   * @param {WindowProxy} window
    275   *   The window where the seach was triggered.
    276   */
    277  webSearch(window) {
    278    if (
    279      window.location.href != AppConstants.BROWSER_CHROME_URL ||
    280      window.gURLBar.readOnly
    281    ) {
    282      let topWindow = lazy.BrowserWindowTracker.getTopWindow();
    283      if (topWindow && !topWindow.gURLBar.readOnly) {
    284        // If there's an open browser window, it should handle this command.
    285        topWindow.focus();
    286        SearchUIUtils.webSearch(topWindow);
    287      } else {
    288        // If there are no open browser windows, open a new one.
    289        let newWindow = window.openDialog(
    290          AppConstants.BROWSER_CHROME_URL,
    291          "_blank",
    292          "chrome,all,dialog=no",
    293          "about:blank"
    294        );
    295 
    296        let observer = subject => {
    297          if (subject == newWindow) {
    298            SearchUIUtils.webSearch(newWindow);
    299            Services.obs.removeObserver(
    300              observer,
    301              "browser-delayed-startup-finished"
    302            );
    303          }
    304        };
    305        Services.obs.addObserver(observer, "browser-delayed-startup-finished");
    306      }
    307      return;
    308    }
    309 
    310    /** @type {(searchBar: MozSearchbar | UrlbarInput) => void} */
    311    let focusUrlBarIfSearchFieldIsNotActive = function (searchBar) {
    312      if (!searchBar || window.document.activeElement != searchBar.inputField) {
    313        // Limit the results to search suggestions, like the search bar.
    314        window.gURLBar.searchModeShortcut();
    315      }
    316    };
    317 
    318    let searchBar = /** @type {MozSearchbar | UrlbarInput} */ (
    319      window.document.getElementById(
    320        Services.prefs.getBoolPref("browser.search.widget.new")
    321          ? "searchbar-new"
    322          : "searchbar"
    323      )
    324    );
    325    let placement =
    326      lazy.CustomizableUI.getPlacementOfWidget("search-container");
    327    let focusSearchBar = () => {
    328      searchBar = /** @type {MozSearchbar | UrlbarInput} */ (
    329        window.document.getElementById(
    330          Services.prefs.getBoolPref("browser.search.widget.new")
    331            ? "searchbar-new"
    332            : "searchbar"
    333        )
    334      );
    335      searchBar.select();
    336      focusUrlBarIfSearchFieldIsNotActive(searchBar);
    337    };
    338    if (
    339      placement &&
    340      searchBar &&
    341      ((searchBar.parentElement.getAttribute("overflowedItem") == "true" &&
    342        placement.area == lazy.CustomizableUI.AREA_NAVBAR) ||
    343        placement.area == lazy.CustomizableUI.AREA_FIXED_OVERFLOW_PANEL)
    344    ) {
    345      let navBar = window.document.getElementById(
    346        lazy.CustomizableUI.AREA_NAVBAR
    347      );
    348      // @ts-expect-error - Navbar receives the overflowable property upon registration.
    349      navBar.overflowable.show().then(focusSearchBar);
    350      return;
    351    }
    352    if (searchBar) {
    353      if (window.fullScreen) {
    354        window.FullScreen.showNavToolbox();
    355      }
    356      searchBar.select();
    357    }
    358    focusUrlBarIfSearchFieldIsNotActive(searchBar);
    359  },
    360 
    361  /**
    362   * Opens a search results page, given a set of search terms.
    363   *
    364   * @param {object} options
    365   *   Options objects.
    366   * @param {WindowProxy} options.window
    367   *   The window where the search was triggered.
    368   * @param {string} options.searchText
    369   *   The search terms to use for the search.
    370   * @param {?string} [options.where]
    371   *   String indicating where the search should load. Most commonly used
    372   *   are ``tab`` or ``window``, defaults to ``current``.
    373   * @param {boolean} [options.usePrivateWindow]
    374   *   Whether to open the window in private browsing mode (if opening a window).
    375   *   Defaults to the type of window that ``options.window` is.
    376   * @param {nsIPrincipal} options.triggeringPrincipal
    377   *   The principal to use for a new window or tab.
    378   * @param {nsIPolicyContainer} [options.policyContainer]
    379   *   The policyContainer to use for a new window or tab.
    380   * @param {boolean} [options.inBackground]
    381   *   Set to true for the tab to be loaded in the background.
    382   * @param {?nsISearchEngine} [options.engine]
    383   *   The search engine to use for the search. If not supplied, this will default
    384   *   to the default search engine for normal or private mode, depending on
    385   *   ``options.usePrivateWindow``.
    386   * @param {?MozTabbrowserTab} [options.tab]
    387   *   The tab to show the search result.
    388   * @param {?Values<typeof SearchUtils.URL_TYPE>} [options.searchUrlType]
    389   *   A `SearchUtils.URL_TYPE` value indicating the type of search that should
    390   *   be performed. A falsey value is equivalent to
    391   *   `SearchUtils.URL_TYPE.SEARCH`, which will perform a usual web search.
    392   * @param {string} options.sapSource
    393   *   The search access point source, see
    394   *   {@link lazy.BrowserSearchTelemetry.KNOWN_SEARCH_SOURCES}
    395   */
    396  async loadSearch({
    397    window,
    398    searchText,
    399    where,
    400    usePrivateWindow = lazy.PrivateBrowsingUtils.isWindowPrivate(window),
    401    triggeringPrincipal,
    402    policyContainer,
    403    inBackground = false,
    404    engine,
    405    tab,
    406    searchUrlType,
    407    sapSource,
    408  }) {
    409    if (!triggeringPrincipal) {
    410      throw new Error(
    411        "Required argument triggeringPrincipal missing within loadSearch"
    412      );
    413    }
    414 
    415    if (!engine) {
    416      engine = usePrivateWindow
    417        ? await Services.search.getDefaultPrivate()
    418        : await Services.search.getDefault();
    419    }
    420 
    421    let submission = engine.getSubmission(searchText, searchUrlType);
    422 
    423    // getSubmission can return null if the engine doesn't have a URL
    424    // for the given response type. This is an error if it occurs, since
    425    // we should only get here if the engine supports the URL type begin
    426    // passed.
    427    if (!submission) {
    428      throw new Error(`No submission URL found for ${searchUrlType}`);
    429    }
    430 
    431    window.openLinkIn(submission.uri.spec, where || "current", {
    432      private: usePrivateWindow,
    433      postData: submission.postData,
    434      inBackground,
    435      relatedToCurrent: true,
    436      triggeringPrincipal,
    437      policyContainer,
    438      targetBrowser: tab?.linkedBrowser,
    439      globalHistoryOptions: {
    440        triggeringSearchEngine: engine.name,
    441      },
    442    });
    443 
    444    lazy.BrowserSearchTelemetry.recordSearch(
    445      window.gBrowser.selectedBrowser,
    446      engine,
    447      sapSource,
    448      { searchUrlType }
    449    );
    450  },
    451 
    452  /**
    453   * Perform a search initiated from the context menu.
    454   * Note: This should only be called from the context menu.
    455   *
    456   * @param {object} options
    457   *   Options object.
    458   * @param {nsISearchEngine} options.engine
    459   *   The engine to search with.
    460   * @param {WindowProxy} options.window
    461   *   The window where the search was triggered.
    462   * @param {string} options.searchText
    463   *   The search terms to use for the search.
    464   * @param {boolean} [options.usePrivateWindow]
    465   *   Whether to open the window in private browsing mode (if opening a window).
    466   *   Defaults to the type of window that ``options.window` is.
    467   * @param {nsIPrincipal} options.triggeringPrincipal
    468   *   The principal of the document whose context menu was clicked.
    469   * @param {nsIPolicyContainer} options.policyContainer
    470   *   The policyContainer to use for a new window or tab.
    471   * @param {XULCommandEvent|PointerEvent} options.event
    472   *   The event triggering the search.
    473   * @param {?Values<typeof SearchUtils.URL_TYPE>} [options.searchUrlType]
    474   *   A `SearchUtils.URL_TYPE` value indicating the type of search that should
    475   *   be performed. A falsey value is equivalent to
    476   *   `SearchUtils.URL_TYPE.SEARCH` and will perform a usual web search.
    477   */
    478  async loadSearchFromContext({
    479    window,
    480    engine,
    481    searchText,
    482    usePrivateWindow,
    483    triggeringPrincipal,
    484    policyContainer,
    485    event,
    486    searchUrlType = null,
    487  }) {
    488    event = lazy.BrowserUtils.getRootEvent(event);
    489    let where = lazy.BrowserUtils.whereToOpenLink(event);
    490    if (where == "current") {
    491      // override: historically search opens in new tab
    492      where = "tab";
    493    }
    494    if (
    495      usePrivateWindow &&
    496      !lazy.PrivateBrowsingUtils.isWindowPrivate(window)
    497    ) {
    498      where = "window";
    499    }
    500    let inBackground = Services.prefs.getBoolPref(
    501      "browser.search.context.loadInBackground"
    502    );
    503    if (event.button == 1 || event.ctrlKey) {
    504      inBackground = !inBackground;
    505    }
    506 
    507    return this.loadSearch({
    508      window,
    509      engine,
    510      searchText,
    511      searchUrlType,
    512      where,
    513      usePrivateWindow,
    514      triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal(
    515        triggeringPrincipal.originAttributes
    516      ),
    517      policyContainer,
    518      inBackground,
    519      sapSource:
    520        searchUrlType == lazy.SearchUtils.URL_TYPE.VISUAL_SEARCH
    521          ? "contextmenu_visual"
    522          : "contextmenu",
    523    });
    524  },
    525 };