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 };