utilityOverlay.js (17200B)
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 // Services = object with smart getters for common XPCOM services 7 var { AppConstants } = ChromeUtils.importESModule( 8 "resource://gre/modules/AppConstants.sys.mjs" 9 ); 10 var { XPCOMUtils } = ChromeUtils.importESModule( 11 "resource://gre/modules/XPCOMUtils.sys.mjs" 12 ); 13 14 ChromeUtils.defineESModuleGetters(this, { 15 AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs", 16 AIWindow: 17 "moz-src:///browser/components/aiwindow/ui/modules/AIWindow.sys.mjs", 18 BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs", 19 BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs", 20 ContextualIdentityService: 21 "resource://gre/modules/ContextualIdentityService.sys.mjs", 22 ExtensionSettingsStore: 23 "resource://gre/modules/ExtensionSettingsStore.sys.mjs", 24 ExtensionUtils: "resource://gre/modules/ExtensionUtils.sys.mjs", 25 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", 26 ShellService: "moz-src:///browser/components/shell/ShellService.sys.mjs", 27 URILoadingHelper: "resource:///modules/URILoadingHelper.sys.mjs", 28 }); 29 30 ChromeUtils.defineLazyGetter(this, "ReferrerInfo", () => 31 Components.Constructor( 32 "@mozilla.org/referrer-info;1", 33 "nsIReferrerInfo", 34 "init" 35 ) 36 ); 37 38 Object.defineProperty(this, "BROWSER_NEW_TAB_URL", { 39 enumerable: true, 40 get() { 41 if (PrivateBrowsingUtils.isWindowPrivate(window)) { 42 if ( 43 !PrivateBrowsingUtils.permanentPrivateBrowsing && 44 !AboutNewTab.newTabURLOverridden 45 ) { 46 return "about:privatebrowsing"; 47 } 48 // If an extension controls the setting and does not have private 49 // browsing permission, use the default setting. 50 let extensionControlled = Services.prefs.getBoolPref( 51 "browser.newtab.extensionControlled", 52 false 53 ); 54 let privateAllowed = Services.prefs.getBoolPref( 55 "browser.newtab.privateAllowed", 56 false 57 ); 58 // There is a potential on upgrade that the prefs are not set yet, so we double check 59 // for moz-extension. 60 if ( 61 !privateAllowed && 62 (extensionControlled || 63 ExtensionUtils.isExtensionUrl(AboutNewTab.newTabURL)) 64 ) { 65 return "about:privatebrowsing"; 66 } 67 } 68 if (AIWindow.isAIWindowActive(window)) { 69 return AIWindow.newTabURL; 70 } 71 return AboutNewTab.newTabURL; 72 }, 73 }); 74 75 var TAB_DROP_TYPE = "application/x-moz-tabbrowser-tab"; 76 77 var gBidiUI = false; 78 79 /** 80 * Determines whether the given url is considered a special URL for new tabs. 81 */ 82 function isBlankPageURL(aURL) { 83 return ( 84 aURL == "about:blank" || 85 aURL == "about:home" || 86 aURL == "about:tor" || 87 aURL == BROWSER_NEW_TAB_URL || 88 aURL == "chrome://browser/content/blanktab.html" 89 ); 90 } 91 92 function doGetProtocolFlags(aURI) { 93 return Services.io.getDynamicProtocolFlags(aURI); 94 } 95 96 function openUILink( 97 url, 98 event, 99 aIgnoreButton, 100 aIgnoreAlt, 101 aAllowThirdPartyFixup, 102 aPostData, 103 aReferrerInfo 104 ) { 105 return URILoadingHelper.openUILink( 106 window, 107 url, 108 event, 109 aIgnoreButton, 110 aIgnoreAlt, 111 aAllowThirdPartyFixup, 112 aPostData, 113 aReferrerInfo 114 ); 115 } 116 117 function openTrustedLinkIn(url, where, params) { 118 URILoadingHelper.openTrustedLinkIn(window, url, where, params); 119 } 120 121 function openWebLinkIn(url, where, params) { 122 URILoadingHelper.openWebLinkIn(window, url, where, params); 123 } 124 125 function openLinkIn(url, where, params) { 126 return URILoadingHelper.openLinkIn(window, url, where, params); 127 } 128 129 // Used as an onclick handler for UI elements with link-like behavior. 130 // e.g. onclick="checkForMiddleClick(this, event);" 131 // Not needed for menuitems because those fire command events even on middle clicks. 132 function checkForMiddleClick(node, event) { 133 // We should be using the disabled property here instead of the attribute, 134 // but some elements that this function is used with don't support it (e.g. 135 // menuitem). 136 if (node.getAttribute("disabled") == "true") { 137 return; 138 } // Do nothing 139 140 if (event.target.tagName == "menuitem") { 141 // Menu items fire command on middle-click by themselves. 142 return; 143 } 144 145 if (event.button == 1) { 146 /* Execute the node's oncommand or command. 147 */ 148 149 let cmdEvent = document.createEvent("xulcommandevent"); 150 cmdEvent.initCommandEvent( 151 "command", 152 true, 153 true, 154 window, 155 0, 156 event.ctrlKey, 157 event.altKey, 158 event.shiftKey, 159 event.metaKey, 160 0, 161 event, 162 event.inputSource 163 ); 164 node.dispatchEvent(cmdEvent); 165 166 // Stop the propagation of the click event, to prevent the event from being 167 // handled more than once. 168 // E.g. see https://bugzilla.mozilla.org/show_bug.cgi?id=1657992#c4 169 event.stopPropagation(); 170 event.preventDefault(); 171 172 // If the middle-click was on part of a menu, close the menu. 173 // (Menus close automatically with left-click but not with middle-click.) 174 closeMenus(event.target); 175 } 176 } 177 178 // Populate a menu with user-context menu items. This method should be called 179 // by onpopupshowing passing the event as first argument. 180 function createUserContextMenu( 181 event, 182 { 183 isContextMenu = false, 184 excludeUserContextId = 0, 185 showDefaultTab = false, 186 useAccessKeys = true, 187 } = {} 188 ) { 189 while (event.target.hasChildNodes()) { 190 event.target.firstChild.remove(); 191 } 192 193 MozXULElement.insertFTLIfNeeded("toolkit/global/contextual-identity.ftl"); 194 let docfrag = document.createDocumentFragment(); 195 196 // If we are excluding a userContextId, we want to add a 'no-container' item. 197 if (excludeUserContextId || showDefaultTab) { 198 let menuitem = document.createXULElement("menuitem"); 199 if (useAccessKeys) { 200 document.l10n.setAttributes(menuitem, "user-context-none"); 201 } else { 202 const label = 203 ContextualIdentityService.formatContextLabel("user-context-none"); 204 menuitem.setAttribute("label", label); 205 } 206 menuitem.setAttribute("data-usercontextid", "0"); 207 if (!isContextMenu) { 208 menuitem.setAttribute("command", "Browser:NewUserContextTab"); 209 } 210 211 docfrag.appendChild(menuitem); 212 213 let menuseparator = document.createXULElement("menuseparator"); 214 docfrag.appendChild(menuseparator); 215 } 216 217 ContextualIdentityService.getPublicIdentities().forEach(identity => { 218 if (identity.userContextId == excludeUserContextId) { 219 return; 220 } 221 222 let menuitem = document.createXULElement("menuitem"); 223 menuitem.setAttribute("data-usercontextid", identity.userContextId); 224 if (identity.name) { 225 menuitem.setAttribute("label", identity.name); 226 } else if (useAccessKeys) { 227 document.l10n.setAttributes(menuitem, identity.l10nId); 228 } else { 229 const label = ContextualIdentityService.formatContextLabel( 230 identity.l10nId 231 ); 232 menuitem.setAttribute("label", label); 233 } 234 235 menuitem.classList.add("menuitem-iconic"); 236 menuitem.classList.add("identity-color-" + identity.color); 237 238 if (!isContextMenu) { 239 menuitem.setAttribute("command", "Browser:NewUserContextTab"); 240 } 241 242 menuitem.classList.add("identity-icon-" + identity.icon); 243 244 docfrag.appendChild(menuitem); 245 }); 246 247 if (!isContextMenu) { 248 docfrag.appendChild(document.createXULElement("menuseparator")); 249 250 let menuitem = document.createXULElement("menuitem"); 251 if (useAccessKeys) { 252 document.l10n.setAttributes(menuitem, "user-context-manage-containers"); 253 } else { 254 const label = ContextualIdentityService.formatContextLabel( 255 "user-context-manage-containers" 256 ); 257 menuitem.setAttribute("label", label); 258 } 259 menuitem.setAttribute("command", "Browser:OpenAboutContainers"); 260 docfrag.appendChild(menuitem); 261 } 262 263 event.target.appendChild(docfrag); 264 return true; 265 } 266 267 // Closes all popups that are ancestors of the node. 268 function closeMenus(node) { 269 if ("tagName" in node) { 270 if ( 271 node.namespaceURI == 272 "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" && 273 (node.tagName == "menupopup" || node.tagName == "popup") 274 ) { 275 node.hidePopup(); 276 } 277 278 closeMenus(node.parentNode); 279 } 280 } 281 282 /** 283 * This function takes in a key element and compares it to the keys pressed during an event. 284 * 285 * @param aEvent 286 * The KeyboardEvent event you want to compare against your key. 287 * 288 * @param aKey 289 * The <key> element checked to see if it was called in aEvent. 290 * For example, aKey can be a variable set to document.getElementById("key_close") 291 * to check if the close command key was pressed in aEvent. 292 */ 293 function eventMatchesKey(aEvent, aKey) { 294 let keyPressed = (aKey.getAttribute("key") || "").toLowerCase(); 295 let keyModifiers = aKey.getAttribute("modifiers"); 296 let modifiers = ["Alt", "Control", "Meta", "Shift"]; 297 298 if (aEvent.key != keyPressed) { 299 return false; 300 } 301 let eventModifiers = modifiers.filter(modifier => 302 aEvent.getModifierState(modifier) 303 ); 304 // Check if aEvent has a modifier and aKey doesn't 305 if (eventModifiers.length && !keyModifiers.length) { 306 return false; 307 } 308 // Check whether aKey's modifiers match aEvent's modifiers 309 if (keyModifiers) { 310 keyModifiers = keyModifiers.split(/[\s,]+/); 311 // Capitalize first letter of aKey's modifers to compare to aEvent's modifier 312 keyModifiers.forEach(function (modifier, index) { 313 if (modifier == "accel") { 314 keyModifiers[index] = 315 AppConstants.platform == "macosx" ? "Meta" : "Control"; 316 } else { 317 keyModifiers[index] = modifier[0].toUpperCase() + modifier.slice(1); 318 } 319 }); 320 return modifiers.every( 321 modifier => 322 keyModifiers.includes(modifier) == aEvent.getModifierState(modifier) 323 ); 324 } 325 return true; 326 } 327 328 // Gather all descendent text under given document node. 329 function gatherTextUnder(root) { 330 var text = ""; 331 var node = root.firstChild; 332 var depth = 1; 333 while (node && depth > 0) { 334 // See if this node is text. 335 if (node.nodeType == Node.TEXT_NODE) { 336 // Add this text to our collection. 337 text += " " + node.data; 338 } else if (HTMLImageElement.isInstance(node)) { 339 // If it has an "alt" attribute, add that. 340 var altText = node.getAttribute("alt"); 341 if (altText) { 342 text += " " + altText; 343 } 344 } 345 // Find next node to test. 346 // First, see if this node has children. 347 if (node.hasChildNodes()) { 348 // Go to first child. 349 node = node.firstChild; 350 depth++; 351 } else { 352 // No children, try next sibling (or parent next sibling). 353 while (depth > 0 && !node.nextSibling) { 354 node = node.parentNode; 355 depth--; 356 } 357 if (node.nextSibling) { 358 node = node.nextSibling; 359 } 360 } 361 } 362 // Strip leading and tailing whitespace. 363 text = text.trim(); 364 // Compress remaining whitespace. 365 text = text.replace(/\s+/g, " "); 366 return text; 367 } 368 369 // This function exists for legacy reasons. 370 function getShellService() { 371 return ShellService; 372 } 373 374 function isBidiEnabled() { 375 // first check the pref. 376 if (Services.prefs.getBoolPref("bidi.browser.ui", false)) { 377 return true; 378 } 379 380 // now see if the app locale is an RTL one. 381 const isRTL = Services.locale.isAppLocaleRTL; 382 383 if (isRTL) { 384 Services.prefs.setBoolPref("bidi.browser.ui", true); 385 } 386 return isRTL; 387 } 388 389 function openAboutDialog() { 390 for (let win of Services.wm.getEnumerator("Browser:About")) { 391 // Only open one about window (Bug 599573) 392 if (win.closed) { 393 continue; 394 } 395 win.focus(); 396 return; 397 } 398 399 var features = "chrome,"; 400 if (AppConstants.platform == "win") { 401 features += "centerscreen,dependent"; 402 } else if (AppConstants.platform == "macosx") { 403 features += "centerscreen,resizable=no,minimizable=no"; 404 } else { 405 features += "centerscreen,dependent,dialog=no"; 406 } 407 408 window.openDialog("chrome://browser/content/aboutDialog.xhtml", "", features); 409 } 410 411 async function openPreferences(paneID, extraArgs) { 412 // This function is duplicated from preferences.js. 413 function internalPrefCategoryNameToFriendlyName(aName) { 414 return (aName || "").replace(/^pane./, function (toReplace) { 415 return toReplace[4].toLowerCase(); 416 }); 417 } 418 419 let win = Services.wm.getMostRecentWindow("navigator:browser"); 420 let friendlyCategoryName = internalPrefCategoryNameToFriendlyName(paneID); 421 let params; 422 if (extraArgs && extraArgs.urlParams) { 423 params = new URLSearchParams(); 424 let urlParams = extraArgs.urlParams; 425 for (let name in urlParams) { 426 if (urlParams[name] !== undefined) { 427 params.set(name, urlParams[name]); 428 } 429 } 430 } 431 let preferencesURLSuffix = 432 (params ? "?" + params : "") + 433 (friendlyCategoryName ? "#" + friendlyCategoryName : ""); 434 let newLoad = true; 435 let browser = null; 436 if (!win) { 437 let windowArguments = Cc["@mozilla.org/array;1"].createInstance( 438 Ci.nsIMutableArray 439 ); 440 let supportsStringPrefURL = Cc[ 441 "@mozilla.org/supports-string;1" 442 ].createInstance(Ci.nsISupportsString); 443 supportsStringPrefURL.data = "about:preferences" + preferencesURLSuffix; 444 windowArguments.appendElement(supportsStringPrefURL); 445 446 win = Services.ww.openWindow( 447 null, 448 AppConstants.BROWSER_CHROME_URL, 449 "_blank", 450 "chrome,dialog=no,all", 451 windowArguments 452 ); 453 } else { 454 let shouldReplaceFragment = friendlyCategoryName 455 ? "whenComparingAndReplace" 456 : "whenComparing"; 457 newLoad = !win.switchToTabHavingURI( 458 "about:settings" + preferencesURLSuffix, 459 false, 460 { 461 ignoreFragment: shouldReplaceFragment, 462 replaceQueryString: true, 463 triggeringPrincipal: 464 Services.scriptSecurityManager.getSystemPrincipal(), 465 } 466 ); 467 if (newLoad) { 468 newLoad = !win.switchToTabHavingURI( 469 "about:preferences" + preferencesURLSuffix, 470 true, 471 { 472 ignoreFragment: shouldReplaceFragment, 473 replaceQueryString: true, 474 triggeringPrincipal: 475 Services.scriptSecurityManager.getSystemPrincipal(), 476 } 477 ); 478 } 479 browser = win.gBrowser.selectedBrowser; 480 } 481 482 if (!newLoad && paneID) { 483 if (browser.contentDocument?.readyState != "complete") { 484 await new Promise(resolve => { 485 browser.addEventListener("load", resolve, { 486 capture: true, 487 once: true, 488 }); 489 }); 490 } 491 browser.contentWindow.gotoPref(paneID); 492 } 493 } 494 495 /** 496 * Opens the troubleshooting information (about:support) page for this version 497 * of the application. 498 */ 499 function openTroubleshootingPage() { 500 openTrustedLinkIn("about:support", "tab"); 501 } 502 503 /** 504 * Opens the feedback page for this version of the application. 505 */ 506 function openFeedbackPage() { 507 var url = Services.urlFormatter.formatURLPref("app.feedback.baseURL"); 508 openTrustedLinkIn(url, "tab"); 509 } 510 511 /** 512 * Appends UTM parameters to then opens the SUMO URL for device migration. 513 */ 514 function openSwitchingDevicesPage() { 515 let url = getHelpLinkURL("switching-devices"); 516 let parsedUrl = new URL(url); 517 parsedUrl.searchParams.set("utm_content", "switch-to-new-device"); 518 parsedUrl.searchParams.set("utm_source", "help-menu"); 519 parsedUrl.searchParams.set("utm_medium", "firefox-desktop"); 520 parsedUrl.searchParams.set("utm_campaign", "migration"); 521 openTrustedLinkIn(parsedUrl.href, "tab"); 522 } 523 524 function buildHelpMenu() { 525 document.getElementById("feedbackPage").disabled = 526 !Services.policies.isAllowed("feedbackCommands"); 527 528 document.getElementById("helpSafeMode").disabled = 529 !Services.policies.isAllowed("safeMode"); 530 531 document.getElementById("troubleShooting").disabled = 532 !Services.policies.isAllowed("aboutSupport"); 533 534 let supportMenu = Services.policies.getSupportMenu(); 535 if (supportMenu) { 536 let menuitem = document.getElementById("helpPolicySupport"); 537 menuitem.hidden = false; 538 menuitem.setAttribute("label", supportMenu.Title); 539 if ("AccessKey" in supportMenu) { 540 menuitem.setAttribute("accesskey", supportMenu.AccessKey); 541 } 542 document.getElementById("helpPolicySeparator").hidden = false; 543 } 544 545 // Enable/disable the "Report Web Forgery" menu item. 546 if (typeof gSafeBrowsing != "undefined") { 547 gSafeBrowsing.setReportPhishingMenu(); 548 } 549 } 550 551 function isElementVisible(aElement) { 552 if (!aElement) { 553 return false; 554 } 555 556 // If aElement or a direct or indirect parent is hidden or collapsed, 557 // height, width or both will be 0. 558 var rect = aElement.getBoundingClientRect(); 559 return rect.height > 0 && rect.width > 0; 560 } 561 562 function makeURLAbsolute(aBase, aUrl) { 563 // Note: makeURI() will throw if aUri is not a valid URI 564 return makeURI(aUrl, null, makeURI(aBase)).spec; 565 } 566 567 function getHelpLinkURL(aHelpTopic) { 568 if (aHelpTopic === "firefox-help" || aHelpTopic === "firefox-osxkey") { 569 return "about:manual"; 570 } 571 var url = Services.urlFormatter.formatURLPref("app.support.baseURL"); 572 return url + aHelpTopic; 573 } 574 575 function openHelpLink(aHelpTopic) { 576 openTrustedLinkIn(getHelpLinkURL(aHelpTopic), "tab"); 577 }