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