tor-browser

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

browser-commands.js (19228B)


      1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
      2 * This Source Code Form is subject to the terms of the Mozilla Public
      3 * License, v. 2.0. If a copy of the MPL was not distributed with this
      4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      5 
      6 "use strict";
      7 
      8 var kSkipCacheFlags =
      9  Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY |
     10  Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
     11 
     12 var BrowserCommands = {
     13  back(aEvent) {
     14    const where = BrowserUtils.whereToOpenLink(aEvent, false, true);
     15 
     16    if (where == "current") {
     17      try {
     18        gBrowser.goBack();
     19      } catch (ex) {}
     20    } else {
     21      duplicateTabIn(gBrowser.selectedTab, where, -1);
     22    }
     23  },
     24 
     25  forward(aEvent) {
     26    const where = BrowserUtils.whereToOpenLink(aEvent, false, true);
     27 
     28    if (where == "current") {
     29      try {
     30        gBrowser.goForward();
     31      } catch (ex) {}
     32    } else {
     33      duplicateTabIn(gBrowser.selectedTab, where, 1);
     34    }
     35  },
     36 
     37  handleBackspace() {
     38    switch (Services.prefs.getIntPref("browser.backspace_action")) {
     39      case 0:
     40        this.back();
     41        break;
     42      case 1:
     43        goDoCommand("cmd_scrollPageUp");
     44        break;
     45    }
     46  },
     47 
     48  handleShiftBackspace() {
     49    switch (Services.prefs.getIntPref("browser.backspace_action")) {
     50      case 0:
     51        this.forward();
     52        break;
     53      case 1:
     54        goDoCommand("cmd_scrollPageDown");
     55        break;
     56    }
     57  },
     58 
     59  gotoHistoryIndex(aEvent) {
     60    aEvent = BrowserUtils.getRootEvent(aEvent);
     61 
     62    const index = aEvent.target.getAttribute("index");
     63    if (!index) {
     64      return false;
     65    }
     66 
     67    const where = BrowserUtils.whereToOpenLink(aEvent);
     68 
     69    if (where == "current") {
     70      // Normal click. Go there in the current tab and update session history.
     71 
     72      try {
     73        gBrowser.gotoIndex(index);
     74      } catch (ex) {
     75        return false;
     76      }
     77      return true;
     78    }
     79    // Modified click. Go there in a new tab/window.
     80 
     81    const historyindex = aEvent.target.getAttribute("historyindex");
     82    duplicateTabIn(gBrowser.selectedTab, where, Number(historyindex));
     83    return true;
     84  },
     85 
     86  reloadOrDuplicate(aEvent) {
     87    aEvent = BrowserUtils.getRootEvent(aEvent);
     88    const accelKeyPressed =
     89      AppConstants.platform == "macosx" ? aEvent.metaKey : aEvent.ctrlKey;
     90    const backgroundTabModifier = aEvent.button == 1 || accelKeyPressed;
     91 
     92    if (aEvent.shiftKey && !backgroundTabModifier) {
     93      this.reloadSkipCache();
     94      return;
     95    }
     96 
     97    const where = BrowserUtils.whereToOpenLink(aEvent, false, true);
     98    if (where == "current") {
     99      this.reload();
    100    } else {
    101      duplicateTabIn(gBrowser.selectedTab, where);
    102    }
    103  },
    104 
    105  reload() {
    106    if (gBrowser.currentURI.schemeIs("view-source")) {
    107      // Bug 1167797: For view source, we always skip the cache
    108      this.reloadSkipCache();
    109      return;
    110    }
    111    this.reloadWithFlags(Ci.nsIWebNavigation.LOAD_FLAGS_NONE);
    112  },
    113 
    114  reloadSkipCache() {
    115    // Bypass proxy and cache.
    116    this.reloadWithFlags(kSkipCacheFlags);
    117  },
    118 
    119  reloadWithFlags(reloadFlags) {
    120    const unchangedRemoteness = [];
    121 
    122    for (const tab of gBrowser.selectedTabs) {
    123      const browser = tab.linkedBrowser;
    124      const url = browser.currentURI;
    125      const urlSpec = url.spec;
    126      // We need to cache the content principal here because the browser will be
    127      // reconstructed when the remoteness changes and the content prinicpal will
    128      // be cleared after reconstruction.
    129      const principal = tab.linkedBrowser.contentPrincipal;
    130      if (gBrowser.updateBrowserRemotenessByURL(browser, urlSpec)) {
    131        // If the remoteness has changed, the new browser doesn't have any
    132        // information of what was loaded before, so we need to load the previous
    133        // URL again.
    134        if (tab.linkedPanel) {
    135          loadBrowserURI(browser, url, principal);
    136        } else {
    137          // Shift to fully loaded browser and make
    138          // sure load handler is instantiated.
    139          tab.addEventListener(
    140            "SSTabRestoring",
    141            () => loadBrowserURI(browser, url, principal),
    142            { once: true }
    143          );
    144          gBrowser._insertBrowser(tab);
    145        }
    146      } else {
    147        unchangedRemoteness.push(tab);
    148      }
    149    }
    150 
    151    if (!unchangedRemoteness.length) {
    152      return;
    153    }
    154 
    155    // Reset temporary permissions on the remaining tabs to reload.
    156    // This is done here because we only want to reset
    157    // permissions on user reload.
    158    for (const tab of unchangedRemoteness) {
    159      SitePermissions.clearTemporaryBlockPermissions(tab.linkedBrowser);
    160      // Also reset DOS mitigations for the basic auth prompt on reload.
    161      delete tab.linkedBrowser.authPromptAbuseCounter;
    162    }
    163    gIdentityHandler.hidePopup();
    164    gPermissionPanel.hidePopup();
    165 
    166    if (document.hasValidTransientUserGestureActivation) {
    167      reloadFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_USER_ACTIVATION;
    168    }
    169 
    170    for (const tab of unchangedRemoteness) {
    171      reloadBrowser(tab, reloadFlags);
    172    }
    173 
    174    function reloadBrowser(tab) {
    175      if (tab.linkedPanel) {
    176        const { browsingContext } = tab.linkedBrowser;
    177        const { sessionHistory } = browsingContext;
    178        if (sessionHistory) {
    179          sessionHistory.reload(reloadFlags);
    180        } else {
    181          browsingContext.reload(reloadFlags);
    182        }
    183      } else {
    184        // Shift to fully loaded browser and make
    185        // sure load handler is instantiated.
    186        tab.addEventListener(
    187          "SSTabRestoring",
    188          () => tab.linkedBrowser.browsingContext.reload(reloadFlags),
    189          {
    190            once: true,
    191          }
    192        );
    193        gBrowser._insertBrowser(tab);
    194      }
    195    }
    196 
    197    function loadBrowserURI(browser, url, principal) {
    198      browser.loadURI(url, {
    199        loadFlags: reloadFlags,
    200        triggeringPrincipal: principal,
    201      });
    202    }
    203  },
    204 
    205  stop() {
    206    gBrowser.webNavigation.stop(Ci.nsIWebNavigation.STOP_ALL);
    207  },
    208 
    209  home(aEvent) {
    210    if (aEvent?.button == 2) {
    211      // right-click: do nothing
    212      return;
    213    }
    214 
    215    const homePage = HomePage.get(window);
    216    let where = BrowserUtils.whereToOpenLink(aEvent, false, true);
    217 
    218    // Don't load the home page in pinned or hidden tabs (e.g. Firefox View).
    219    if (
    220      where == "current" &&
    221      (gBrowser?.selectedTab.pinned || gBrowser?.selectedTab.hidden)
    222    ) {
    223      where = "tab";
    224    }
    225 
    226    // openTrustedLinkIn in utilityOverlay.js doesn't handle loading multiple pages
    227    let notifyObservers;
    228    switch (where) {
    229      case "current":
    230        // If we're going to load an initial page in the current tab as the
    231        // home page, we set initialPageLoadedFromURLBar so that the URL
    232        // bar is cleared properly (even during a remoteness flip).
    233        if (isInitialPage(homePage)) {
    234          gBrowser.selectedBrowser.initialPageLoadedFromUserAction = homePage;
    235        }
    236        loadOneOrMoreURIs(
    237          homePage,
    238          Services.scriptSecurityManager.getSystemPrincipal(),
    239          null
    240        );
    241        if (isBlankPageURL(homePage)) {
    242          gURLBar.select();
    243        } else {
    244          gBrowser.selectedBrowser.focus();
    245        }
    246        notifyObservers = true;
    247        aEvent?.preventDefault();
    248        break;
    249      case "tabshifted":
    250      case "tab": {
    251        const urls = homePage.split("|");
    252        const loadInBackground = Services.prefs.getBoolPref(
    253          "browser.tabs.loadBookmarksInBackground",
    254          false
    255        );
    256        // The homepage observer event should only be triggered when the homepage opens
    257        // in the foreground. This is mostly to support the homepage changed by extension
    258        // doorhanger which doesn't currently support background pages. This may change in
    259        // bug 1438396.
    260        notifyObservers = !loadInBackground;
    261        gBrowser.loadTabs(urls, {
    262          inBackground: loadInBackground,
    263          triggeringPrincipal:
    264            Services.scriptSecurityManager.getSystemPrincipal(),
    265        });
    266        if (!loadInBackground) {
    267          if (isBlankPageURL(homePage)) {
    268            gURLBar.select();
    269          } else {
    270            gBrowser.selectedBrowser.focus();
    271          }
    272        }
    273        aEvent?.preventDefault();
    274        break;
    275      }
    276      case "window":
    277        // OpenBrowserWindow will trigger the observer event, so no need to do so here.
    278        notifyObservers = false;
    279        OpenBrowserWindow();
    280        aEvent?.preventDefault();
    281        break;
    282    }
    283 
    284    if (notifyObservers) {
    285      // A notification for when a user has triggered their homepage. This is used
    286      // to display a doorhanger explaining that an extension has modified the
    287      // homepage, if necessary. Observers are only notified if the homepage
    288      // becomes the active page.
    289      Services.obs.notifyObservers(null, "browser-open-homepage-start");
    290    }
    291  },
    292 
    293  openTab({ event, url } = {}) {
    294    let werePassedURL = !!url;
    295    url ??= BROWSER_NEW_TAB_URL;
    296    let searchClipboard =
    297      gMiddleClickNewTabUsesPasteboard && event?.button == 1;
    298 
    299    let relatedToCurrent = false;
    300    let where = "tab";
    301 
    302    if (event) {
    303      where = BrowserUtils.whereToOpenLink(event, false, true);
    304 
    305      switch (where) {
    306        case "tab":
    307        case "tabshifted":
    308          // When accel-click or middle-click are used, open the new tab as
    309          // related to the current tab.
    310          relatedToCurrent = true;
    311          break;
    312        case "current":
    313          where = "tab";
    314          break;
    315      }
    316    }
    317 
    318    // A notification intended to be useful for modular peformance tracking
    319    // starting as close as is reasonably possible to the time when the user
    320    // expressed the intent to open a new tab.  Since there are a lot of
    321    // entry points, this won't catch every single tab created, but most
    322    // initiated by the user should go through here.
    323    //
    324    // Note 1: This notification gets notified with a promise that resolves
    325    //         with the linked browser when the tab gets created
    326    // Note 2: This is also used to notify a user that an extension has changed
    327    //         the New Tab page.
    328    Services.obs.notifyObservers(
    329      {
    330        wrappedJSObject: new Promise(resolve => {
    331          let options = {
    332            relatedToCurrent,
    333            resolveOnNewTabCreated: resolve,
    334          };
    335          if (!werePassedURL && searchClipboard) {
    336            let clipboard = readFromClipboard();
    337            clipboard =
    338              UrlbarUtils.stripUnsafeProtocolOnPaste(clipboard).trim();
    339            if (clipboard) {
    340              url = clipboard;
    341              options.allowThirdPartyFixup = true;
    342            }
    343          }
    344          openTrustedLinkIn(url, where, options);
    345        }),
    346      },
    347      "browser-open-newtab-start"
    348    );
    349  },
    350 
    351  openFileWindow() {
    352    // Get filepicker component.
    353    try {
    354      const nsIFilePicker = Ci.nsIFilePicker;
    355      const fp = Cc["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);
    356      const fpCallback = function fpCallback_done(aResult) {
    357        if (aResult == nsIFilePicker.returnOK) {
    358          try {
    359            if (fp.file) {
    360              gLastOpenDirectory.path = fp.file.parent.QueryInterface(
    361                Ci.nsIFile
    362              );
    363            }
    364          } catch (ex) {}
    365          openTrustedLinkIn(fp.fileURL.spec, "current");
    366        }
    367      };
    368 
    369      fp.init(
    370        window.browsingContext,
    371        gNavigatorBundle.getString("openFile"),
    372        nsIFilePicker.modeOpen
    373      );
    374      fp.appendFilters(
    375        nsIFilePicker.filterAll |
    376          nsIFilePicker.filterText |
    377          nsIFilePicker.filterImages |
    378          nsIFilePicker.filterXML |
    379          nsIFilePicker.filterHTML |
    380          nsIFilePicker.filterPDF
    381      );
    382      fp.displayDirectory = gLastOpenDirectory.path;
    383      fp.open(fpCallback);
    384    } catch (ex) {}
    385  },
    386 
    387  closeTabOrWindow(event) {
    388    // If we're not a browser window, just close the window.
    389    if (window.location.href != AppConstants.BROWSER_CHROME_URL) {
    390      closeWindow(true);
    391      return;
    392    }
    393 
    394    // In a multi-select context, close all selected tabs
    395    if (gBrowser.multiSelectedTabsCount) {
    396      gBrowser.removeMultiSelectedTabs(
    397        gBrowser.TabMetrics.userTriggeredContext()
    398      );
    399      return;
    400    }
    401 
    402    // Keyboard shortcuts that would close a tab that is pinned select the first
    403    // unpinned tab instead.
    404    if (
    405      event &&
    406      (event.ctrlKey || event.metaKey || event.altKey) &&
    407      gBrowser.selectedTab.pinned
    408    ) {
    409      if (gBrowser.visibleTabs.length > gBrowser.pinnedTabCount) {
    410        gBrowser.tabContainer.selectedIndex = gBrowser.pinnedTabCount;
    411      }
    412      return;
    413    }
    414 
    415    // If the current tab is the last one, this will close the window.
    416    gBrowser.removeCurrentTab({
    417      animate: true,
    418      ...gBrowser.TabMetrics.userTriggeredContext(),
    419    });
    420  },
    421 
    422  tryToCloseWindow(event) {
    423    if (WindowIsClosing(event)) {
    424      window.close();
    425    } // WindowIsClosing does all the necessary checks
    426  },
    427 
    428  /**
    429   * Open the View Source dialog.
    430   *
    431   * @param args
    432   *        An object with the following properties:
    433   *
    434   *        URL (required):
    435   *          A string URL for the page we'd like to view the source of.
    436   *        browser (optional):
    437   *          The browser containing the document that we would like to view the
    438   *          source of. This is required if outerWindowID is passed.
    439   *        outerWindowID (optional):
    440   *          The outerWindowID of the content window containing the document that
    441   *          we want to view the source of. You only need to provide this if you
    442   *          want to attempt to retrieve the document source from the network
    443   *          cache.
    444   *        lineNumber (optional):
    445   *          The line number to focus on once the source is loaded.
    446   */
    447  async viewSourceOfDocument(args) {
    448    // Check if external view source is enabled.  If so, try it.  If it fails,
    449    // fallback to internal view source.
    450    if (Services.prefs.getBoolPref("view_source.editor.external")) {
    451      try {
    452        await top.gViewSourceUtils.openInExternalEditor(args);
    453        return;
    454      } catch (data) {}
    455    }
    456 
    457    let tabBrowser = gBrowser;
    458    let preferredRemoteType;
    459    let initialBrowsingContextGroupId;
    460    if (args.browser) {
    461      preferredRemoteType = args.browser.remoteType;
    462      initialBrowsingContextGroupId = args.browser.browsingContext.group.id;
    463    } else {
    464      if (!tabBrowser) {
    465        throw new Error(
    466          "viewSourceOfDocument should be passed the " +
    467            "subject browser if called from a window without " +
    468            "gBrowser defined."
    469        );
    470      }
    471      // Some internal URLs (such as specific chrome: and about: URLs that are
    472      // not yet remote ready) cannot be loaded in a remote browser.  View
    473      // source in tab expects the new view source browser's remoteness to match
    474      // that of the original URL, so disable remoteness if necessary for this
    475      // URL.
    476      const oa = E10SUtils.predictOriginAttributes({ window });
    477      preferredRemoteType = E10SUtils.getRemoteTypeForURI(
    478        args.URL,
    479        gMultiProcessBrowser,
    480        gFissionBrowser,
    481        E10SUtils.DEFAULT_REMOTE_TYPE,
    482        null,
    483        oa
    484      );
    485    }
    486 
    487    // In the case of popups, we need to find a non-popup browser window.
    488    if (!tabBrowser || !window.toolbar.visible) {
    489      // This returns only non-popup browser windows by default.
    490      const browserWindow =
    491        BrowserWindowTracker.getTopWindow() ??
    492        (await BrowserWindowTracker.promiseOpenWindow());
    493      tabBrowser = browserWindow.gBrowser;
    494    }
    495 
    496    const inNewWindow = !Services.prefs.getBoolPref("view_source.tab");
    497 
    498    // `viewSourceInBrowser` will load the source content from the page
    499    // descriptor for the tab (when possible) or fallback to the network if
    500    // that fails.  Either way, the view source module will manage the tab's
    501    // location, so use "about:blank" here to avoid unnecessary redundant
    502    // requests.
    503    const tab = tabBrowser.addTab("about:blank", {
    504      relatedToCurrent: true,
    505      inBackground: inNewWindow,
    506      skipAnimation: inNewWindow,
    507      preferredRemoteType,
    508      initialBrowsingContextGroupId,
    509      triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
    510      skipLoad: true,
    511    });
    512    args.viewSourceBrowser = tabBrowser.getBrowserForTab(tab);
    513    top.gViewSourceUtils.viewSourceInBrowser(args);
    514 
    515    if (inNewWindow) {
    516      tabBrowser.hideTab(tab);
    517      tabBrowser.replaceTabWithWindow(tab);
    518    }
    519  },
    520 
    521  /**
    522   * Opens the View Source dialog for the source loaded in the root
    523   * top-level document of the browser. This is really just a
    524   * convenience wrapper around viewSourceOfDocument.
    525   *
    526   * @param browser
    527   *        The browser that we want to load the source of.
    528   */
    529  viewSource(browser) {
    530    this.viewSourceOfDocument({
    531      browser,
    532      outerWindowID: browser.outerWindowID,
    533      URL: browser.currentURI.spec,
    534    });
    535  },
    536 
    537  /**
    538   * @param documentURL URL of the document to view, or null for this window's document
    539   * @param initialTab name of the initial tab to display, or null for the first tab
    540   * @param imageElement image to load in the Media Tab of the Page Info window; can be null/omitted
    541   * @param browsingContext the browsingContext of the frame that we want to view information about; can be null/omitted
    542   * @param browser the browser containing the document we're interested in inspecting; can be null/omitted
    543   */
    544  pageInfo(documentURL, initialTab, imageElement, browsingContext, browser) {
    545    const args = { initialTab, imageElement, browsingContext, browser };
    546 
    547    documentURL =
    548      documentURL || window.gBrowser.selectedBrowser.currentURI.spec;
    549 
    550    const isPrivate = PrivateBrowsingUtils.isWindowPrivate(window);
    551 
    552    // Check for windows matching the url
    553    for (const currentWindow of Services.wm.getEnumerator(
    554      "Browser:page-info"
    555    )) {
    556      if (currentWindow.closed) {
    557        continue;
    558      }
    559      if (
    560        currentWindow.document.documentElement.getAttribute("relatedUrl") ==
    561          documentURL &&
    562        PrivateBrowsingUtils.isWindowPrivate(currentWindow) == isPrivate
    563      ) {
    564        currentWindow.focus();
    565        currentWindow.resetPageInfo(args);
    566        return currentWindow;
    567      }
    568    }
    569 
    570    // We didn't find a matching window, so open a new one.
    571    let options = "chrome,toolbar,dialog=no,resizable";
    572 
    573    // Ensure the window groups correctly in the Windows taskbar
    574    if (isPrivate) {
    575      options += ",private";
    576    }
    577    return openDialog(
    578      "chrome://browser/content/pageinfo/pageInfo.xhtml",
    579      "",
    580      options,
    581      args
    582    );
    583  },
    584 
    585  fullScreen() {
    586    window.fullScreen = !window.fullScreen || BrowserHandler.kiosk;
    587  },
    588 
    589  downloadsUI() {
    590    if (PrivateBrowsingUtils.isWindowPrivate(window)) {
    591      openTrustedLinkIn("about:downloads", "tab");
    592    } else {
    593      PlacesCommandHook.showPlacesOrganizer("Downloads");
    594    }
    595  },
    596 
    597  forceEncodingDetection() {
    598    gBrowser.selectedBrowser.forceEncodingDetection();
    599    BrowserCommands.reloadWithFlags(
    600      Ci.nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE
    601    );
    602  },
    603 
    604  processCloseRequest() {
    605    gBrowser.selectedBrowser.processCloseRequest();
    606  },
    607 };