diff options
Diffstat (limited to 'browser/base/content/browser-commands.js')
-rw-r--r-- | browser/base/content/browser-commands.js | 576 |
1 files changed, 576 insertions, 0 deletions
diff --git a/browser/base/content/browser-commands.js b/browser/base/content/browser-commands.js new file mode 100644 index 0000000000..40eb4d5baa --- /dev/null +++ b/browser/base/content/browser-commands.js @@ -0,0 +1,576 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* eslint-env mozilla/browser-window */ + +"use strict"; + +var kSkipCacheFlags = + Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY | + Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE; + +var BrowserCommands = { + back(aEvent) { + const where = BrowserUtils.whereToOpenLink(aEvent, false, true); + + if (where == "current") { + try { + gBrowser.goBack(); + } catch (ex) {} + } else { + duplicateTabIn(gBrowser.selectedTab, where, -1); + } + }, + + forward(aEvent) { + const where = BrowserUtils.whereToOpenLink(aEvent, false, true); + + if (where == "current") { + try { + gBrowser.goForward(); + } catch (ex) {} + } else { + duplicateTabIn(gBrowser.selectedTab, where, 1); + } + }, + + handleBackspace() { + switch (Services.prefs.getIntPref("browser.backspace_action")) { + case 0: + this.back(); + break; + case 1: + goDoCommand("cmd_scrollPageUp"); + break; + } + }, + + handleShiftBackspace() { + switch (Services.prefs.getIntPref("browser.backspace_action")) { + case 0: + this.forward(); + break; + case 1: + goDoCommand("cmd_scrollPageDown"); + break; + } + }, + + gotoHistoryIndex(aEvent) { + aEvent = BrowserUtils.getRootEvent(aEvent); + + const index = aEvent.target.getAttribute("index"); + if (!index) { + return false; + } + + const where = BrowserUtils.whereToOpenLink(aEvent); + + if (where == "current") { + // Normal click. Go there in the current tab and update session history. + + try { + gBrowser.gotoIndex(index); + } catch (ex) { + return false; + } + return true; + } + // Modified click. Go there in a new tab/window. + + const historyindex = aEvent.target.getAttribute("historyindex"); + duplicateTabIn(gBrowser.selectedTab, where, Number(historyindex)); + return true; + }, + + reloadOrDuplicate(aEvent) { + aEvent = BrowserUtils.getRootEvent(aEvent); + const accelKeyPressed = + AppConstants.platform == "macosx" ? aEvent.metaKey : aEvent.ctrlKey; + const backgroundTabModifier = aEvent.button == 1 || accelKeyPressed; + + if (aEvent.shiftKey && !backgroundTabModifier) { + this.reloadSkipCache(); + return; + } + + const where = BrowserUtils.whereToOpenLink(aEvent, false, true); + if (where == "current") { + this.reload(); + } else { + duplicateTabIn(gBrowser.selectedTab, where); + } + }, + + reload() { + if (gBrowser.currentURI.schemeIs("view-source")) { + // Bug 1167797: For view source, we always skip the cache + this.reloadSkipCache(); + return; + } + this.reloadWithFlags(Ci.nsIWebNavigation.LOAD_FLAGS_NONE); + }, + + reloadSkipCache() { + // Bypass proxy and cache. + this.reloadWithFlags(kSkipCacheFlags); + }, + + reloadWithFlags(reloadFlags) { + const unchangedRemoteness = []; + + for (const tab of gBrowser.selectedTabs) { + const browser = tab.linkedBrowser; + const url = browser.currentURI; + const urlSpec = url.spec; + // We need to cache the content principal here because the browser will be + // reconstructed when the remoteness changes and the content prinicpal will + // be cleared after reconstruction. + const principal = tab.linkedBrowser.contentPrincipal; + if (gBrowser.updateBrowserRemotenessByURL(browser, urlSpec)) { + // If the remoteness has changed, the new browser doesn't have any + // information of what was loaded before, so we need to load the previous + // URL again. + if (tab.linkedPanel) { + loadBrowserURI(browser, url, principal); + } else { + // Shift to fully loaded browser and make + // sure load handler is instantiated. + tab.addEventListener( + "SSTabRestoring", + () => loadBrowserURI(browser, url, principal), + { once: true } + ); + gBrowser._insertBrowser(tab); + } + } else { + unchangedRemoteness.push(tab); + } + } + + if (!unchangedRemoteness.length) { + return; + } + + // Reset temporary permissions on the remaining tabs to reload. + // This is done here because we only want to reset + // permissions on user reload. + for (const tab of unchangedRemoteness) { + SitePermissions.clearTemporaryBlockPermissions(tab.linkedBrowser); + // Also reset DOS mitigations for the basic auth prompt on reload. + delete tab.linkedBrowser.authPromptAbuseCounter; + } + gIdentityHandler.hidePopup(); + gPermissionPanel.hidePopup(); + + const handlingUserInput = document.hasValidTransientUserGestureActivation; + + for (const tab of unchangedRemoteness) { + if (tab.linkedPanel) { + sendReloadMessage(tab); + } else { + // Shift to fully loaded browser and make + // sure load handler is instantiated. + tab.addEventListener("SSTabRestoring", () => sendReloadMessage(tab), { + once: true, + }); + gBrowser._insertBrowser(tab); + } + } + + function loadBrowserURI(browser, url, principal) { + browser.loadURI(url, { + flags: reloadFlags, + triggeringPrincipal: principal, + }); + } + + function sendReloadMessage(tab) { + tab.linkedBrowser.sendMessageToActor( + "Browser:Reload", + { flags: reloadFlags, handlingUserInput }, + "BrowserTab" + ); + } + }, + + stop() { + gBrowser.webNavigation.stop(Ci.nsIWebNavigation.STOP_ALL); + }, + + home(aEvent) { + if (aEvent?.button == 2) { + // right-click: do nothing + return; + } + + const homePage = HomePage.get(window); + let where = BrowserUtils.whereToOpenLink(aEvent, false, true); + + // Don't load the home page in pinned or hidden tabs (e.g. Firefox View). + if ( + where == "current" && + (gBrowser?.selectedTab.pinned || gBrowser?.selectedTab.hidden) + ) { + where = "tab"; + } + + // openTrustedLinkIn in utilityOverlay.js doesn't handle loading multiple pages + let notifyObservers; + switch (where) { + case "current": + // If we're going to load an initial page in the current tab as the + // home page, we set initialPageLoadedFromURLBar so that the URL + // bar is cleared properly (even during a remoteness flip). + if (isInitialPage(homePage)) { + gBrowser.selectedBrowser.initialPageLoadedFromUserAction = homePage; + } + loadOneOrMoreURIs( + homePage, + Services.scriptSecurityManager.getSystemPrincipal(), + null + ); + if (isBlankPageURL(homePage)) { + gURLBar.select(); + } else { + gBrowser.selectedBrowser.focus(); + } + notifyObservers = true; + aEvent?.preventDefault(); + break; + case "tabshifted": + case "tab": { + const urls = homePage.split("|"); + const loadInBackground = Services.prefs.getBoolPref( + "browser.tabs.loadBookmarksInBackground", + false + ); + // The homepage observer event should only be triggered when the homepage opens + // in the foreground. This is mostly to support the homepage changed by extension + // doorhanger which doesn't currently support background pages. This may change in + // bug 1438396. + notifyObservers = !loadInBackground; + gBrowser.loadTabs(urls, { + inBackground: loadInBackground, + triggeringPrincipal: + Services.scriptSecurityManager.getSystemPrincipal(), + csp: null, + }); + if (!loadInBackground) { + if (isBlankPageURL(homePage)) { + gURLBar.select(); + } else { + gBrowser.selectedBrowser.focus(); + } + } + aEvent?.preventDefault(); + break; + } + case "window": + // OpenBrowserWindow will trigger the observer event, so no need to do so here. + notifyObservers = false; + OpenBrowserWindow(); + aEvent?.preventDefault(); + break; + } + + if (notifyObservers) { + // A notification for when a user has triggered their homepage. This is used + // to display a doorhanger explaining that an extension has modified the + // homepage, if necessary. Observers are only notified if the homepage + // becomes the active page. + Services.obs.notifyObservers(null, "browser-open-homepage-start"); + } + }, + + openTab({ event, url } = {}) { + let werePassedURL = !!url; + url ??= BROWSER_NEW_TAB_URL; + let searchClipboard = + gMiddleClickNewTabUsesPasteboard && event?.button == 1; + + let relatedToCurrent = false; + let where = "tab"; + + if (event) { + where = whereToOpenLink(event, false, true); + + switch (where) { + case "tab": + case "tabshifted": + // When accel-click or middle-click are used, open the new tab as + // related to the current tab. + relatedToCurrent = true; + break; + case "current": + where = "tab"; + break; + } + } + + // A notification intended to be useful for modular peformance tracking + // starting as close as is reasonably possible to the time when the user + // expressed the intent to open a new tab. Since there are a lot of + // entry points, this won't catch every single tab created, but most + // initiated by the user should go through here. + // + // Note 1: This notification gets notified with a promise that resolves + // with the linked browser when the tab gets created + // Note 2: This is also used to notify a user that an extension has changed + // the New Tab page. + Services.obs.notifyObservers( + { + wrappedJSObject: new Promise(resolve => { + let options = { + relatedToCurrent, + resolveOnNewTabCreated: resolve, + }; + if (!werePassedURL && searchClipboard) { + let clipboard = readFromClipboard(); + clipboard = + UrlbarUtils.stripUnsafeProtocolOnPaste(clipboard).trim(); + if (clipboard) { + url = clipboard; + options.allowThirdPartyFixup = true; + } + } + openTrustedLinkIn(url, where, options); + }), + }, + "browser-open-newtab-start" + ); + }, + + openFileWindow() { + // Get filepicker component. + try { + const nsIFilePicker = Ci.nsIFilePicker; + const fp = Cc["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker); + const fpCallback = function fpCallback_done(aResult) { + if (aResult == nsIFilePicker.returnOK) { + try { + if (fp.file) { + gLastOpenDirectory.path = fp.file.parent.QueryInterface( + Ci.nsIFile + ); + } + } catch (ex) {} + openTrustedLinkIn(fp.fileURL.spec, "current"); + } + }; + + fp.init( + window.browsingContext, + gNavigatorBundle.getString("openFile"), + nsIFilePicker.modeOpen + ); + fp.appendFilters( + nsIFilePicker.filterAll | + nsIFilePicker.filterText | + nsIFilePicker.filterImages | + nsIFilePicker.filterXML | + nsIFilePicker.filterHTML | + nsIFilePicker.filterPDF + ); + fp.displayDirectory = gLastOpenDirectory.path; + fp.open(fpCallback); + } catch (ex) {} + }, + + closeTabOrWindow(event) { + // If we're not a browser window, just close the window. + if (window.location.href != AppConstants.BROWSER_CHROME_URL) { + closeWindow(true); + return; + } + + // In a multi-select context, close all selected tabs + if (gBrowser.multiSelectedTabsCount) { + gBrowser.removeMultiSelectedTabs(); + return; + } + + // Keyboard shortcuts that would close a tab that is pinned select the first + // unpinned tab instead. + if ( + event && + (event.ctrlKey || event.metaKey || event.altKey) && + gBrowser.selectedTab.pinned + ) { + if (gBrowser.visibleTabs.length > gBrowser._numPinnedTabs) { + gBrowser.tabContainer.selectedIndex = gBrowser._numPinnedTabs; + } + return; + } + + // If the current tab is the last one, this will close the window. + gBrowser.removeCurrentTab({ animate: true }); + }, + + tryToCloseWindow(event) { + if (WindowIsClosing(event)) { + window.close(); + } // WindowIsClosing does all the necessary checks + }, + + /** + * Open the View Source dialog. + * + * @param args + * An object with the following properties: + * + * URL (required): + * A string URL for the page we'd like to view the source of. + * browser (optional): + * The browser containing the document that we would like to view the + * source of. This is required if outerWindowID is passed. + * outerWindowID (optional): + * The outerWindowID of the content window containing the document that + * we want to view the source of. You only need to provide this if you + * want to attempt to retrieve the document source from the network + * cache. + * lineNumber (optional): + * The line number to focus on once the source is loaded. + */ + async viewSourceOfDocument(args) { + // Check if external view source is enabled. If so, try it. If it fails, + // fallback to internal view source. + if (Services.prefs.getBoolPref("view_source.editor.external")) { + try { + await top.gViewSourceUtils.openInExternalEditor(args); + return; + } catch (data) {} + } + + let tabBrowser = gBrowser; + let preferredRemoteType; + let initialBrowsingContextGroupId; + if (args.browser) { + preferredRemoteType = args.browser.remoteType; + initialBrowsingContextGroupId = args.browser.browsingContext.group.id; + } else { + if (!tabBrowser) { + throw new Error( + "viewSourceOfDocument should be passed the " + + "subject browser if called from a window without " + + "gBrowser defined." + ); + } + // Some internal URLs (such as specific chrome: and about: URLs that are + // not yet remote ready) cannot be loaded in a remote browser. View + // source in tab expects the new view source browser's remoteness to match + // that of the original URL, so disable remoteness if necessary for this + // URL. + const oa = E10SUtils.predictOriginAttributes({ window }); + preferredRemoteType = E10SUtils.getRemoteTypeForURI( + args.URL, + gMultiProcessBrowser, + gFissionBrowser, + E10SUtils.DEFAULT_REMOTE_TYPE, + null, + oa + ); + } + + // In the case of popups, we need to find a non-popup browser window. + if (!tabBrowser || !window.toolbar.visible) { + // This returns only non-popup browser windows by default. + const browserWindow = BrowserWindowTracker.getTopWindow(); + tabBrowser = browserWindow.gBrowser; + } + + const inNewWindow = !Services.prefs.getBoolPref("view_source.tab"); + + // `viewSourceInBrowser` will load the source content from the page + // descriptor for the tab (when possible) or fallback to the network if + // that fails. Either way, the view source module will manage the tab's + // location, so use "about:blank" here to avoid unnecessary redundant + // requests. + const tab = tabBrowser.addTab("about:blank", { + relatedToCurrent: true, + inBackground: inNewWindow, + skipAnimation: inNewWindow, + preferredRemoteType, + initialBrowsingContextGroupId, + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + skipLoad: true, + }); + args.viewSourceBrowser = tabBrowser.getBrowserForTab(tab); + top.gViewSourceUtils.viewSourceInBrowser(args); + + if (inNewWindow) { + tabBrowser.hideTab(tab); + tabBrowser.replaceTabWithWindow(tab); + } + }, + + /** + * Opens the View Source dialog for the source loaded in the root + * top-level document of the browser. This is really just a + * convenience wrapper around viewSourceOfDocument. + * + * @param browser + * The browser that we want to load the source of. + */ + viewSource(browser) { + this.viewSourceOfDocument({ + browser, + outerWindowID: browser.outerWindowID, + URL: browser.currentURI.spec, + }); + }, + + /** + * @param documentURL URL of the document to view, or null for this window's document + * @param initialTab name of the initial tab to display, or null for the first tab + * @param imageElement image to load in the Media Tab of the Page Info window; can be null/omitted + * @param browsingContext the browsingContext of the frame that we want to view information about; can be null/omitted + * @param browser the browser containing the document we're interested in inspecting; can be null/omitted + */ + pageInfo(documentURL, initialTab, imageElement, browsingContext, browser) { + const args = { initialTab, imageElement, browsingContext, browser }; + + documentURL = + documentURL || window.gBrowser.selectedBrowser.currentURI.spec; + + const isPrivate = PrivateBrowsingUtils.isWindowPrivate(window); + + // Check for windows matching the url + for (const currentWindow of Services.wm.getEnumerator( + "Browser:page-info" + )) { + if (currentWindow.closed) { + continue; + } + if ( + currentWindow.document.documentElement.getAttribute("relatedUrl") == + documentURL && + PrivateBrowsingUtils.isWindowPrivate(currentWindow) == isPrivate + ) { + currentWindow.focus(); + currentWindow.resetPageInfo(args); + return currentWindow; + } + } + + // We didn't find a matching window, so open a new one. + let options = "chrome,toolbar,dialog=no,resizable"; + + // Ensure the window groups correctly in the Windows taskbar + if (isPrivate) { + options += ",private"; + } + return openDialog( + "chrome://browser/content/pageinfo/pageInfo.xhtml", + "", + options, + args + ); + }, + + fullScreen() { + window.fullScreen = !window.fullScreen || BrowserHandler.kiosk; + }, +}; |