nsContextMenu.sys.mjs (93837B)
1 /* -*- tab-width: 2; indent-tabs-mode: nil; js-indent-level: 2 -*- */ 2 /* vim: set ts=2 sw=2 sts=2 et tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 const lazy = {}; 8 9 ChromeUtils.defineESModuleGetters(lazy, { 10 BrowserSearchTelemetry: 11 "moz-src:///browser/components/search/BrowserSearchTelemetry.sys.mjs", 12 BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs", 13 BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs", 14 ContextualIdentityService: 15 "resource://gre/modules/ContextualIdentityService.sys.mjs", 16 DevToolsShim: "chrome://devtools-startup/content/DevToolsShim.sys.mjs", 17 E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs", 18 // GenAI.sys.mjs and LinkPreview.sys.mjs are missing. tor-browser#44045. 19 LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs", 20 LoginManagerContextMenu: 21 "resource://gre/modules/LoginManagerContextMenu.sys.mjs", 22 NetUtil: "resource://gre/modules/NetUtil.sys.mjs", 23 NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", 24 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", 25 ScreenshotsUtils: "resource:///modules/ScreenshotsUtils.sys.mjs", 26 SearchUIUtils: "moz-src:///browser/components/search/SearchUIUtils.sys.mjs", 27 SearchUtils: "moz-src:///toolkit/components/search/SearchUtils.sys.mjs", 28 ShortcutUtils: "resource://gre/modules/ShortcutUtils.sys.mjs", 29 TranslationsParent: "resource://gre/actors/TranslationsParent.sys.mjs", 30 WebsiteFilter: "resource:///modules/policies/WebsiteFilter.sys.mjs", 31 }); 32 33 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 34 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; 35 36 ChromeUtils.defineLazyGetter(lazy, "ReferrerInfo", () => 37 Components.Constructor( 38 "@mozilla.org/referrer-info;1", 39 "nsIReferrerInfo", 40 "init" 41 ) 42 ); 43 44 XPCOMUtils.defineLazyPreferenceGetter( 45 lazy, 46 "TEXT_RECOGNITION_ENABLED", 47 "dom.text-recognition.enabled", 48 false 49 ); 50 51 XPCOMUtils.defineLazyPreferenceGetter( 52 lazy, 53 "STRIP_ON_SHARE_ENABLED", 54 "privacy.query_stripping.strip_on_share.enabled", 55 false 56 ); 57 58 XPCOMUtils.defineLazyPreferenceGetter( 59 lazy, 60 "PDFJS_ENABLE_COMMENT", 61 "pdfjs.enableComment", 62 false 63 ); 64 65 XPCOMUtils.defineLazyPreferenceGetter( 66 lazy, 67 "gPrintEnabled", 68 "print.enabled", 69 false 70 ); 71 72 XPCOMUtils.defineLazyServiceGetter( 73 lazy, 74 "QueryStringStripper", 75 "@mozilla.org/url-query-string-stripper;1", 76 Ci.nsIURLQueryStringStripper 77 ); 78 79 XPCOMUtils.defineLazyServiceGetter( 80 lazy, 81 "clipboard", 82 "@mozilla.org/widget/clipboardhelper;1", 83 Ci.nsIClipboardHelper 84 ); 85 86 XPCOMUtils.defineLazyPreferenceGetter( 87 lazy, 88 "TEXT_FRAGMENTS_ENABLED", 89 "dom.text_fragments.enabled", 90 false 91 ); 92 93 const PASSWORD_FIELDNAME_HINTS = ["current-password", "new-password"]; 94 const USERNAME_FIELDNAME_HINT = "username"; 95 96 export class nsContextMenu { 97 /** 98 * A promise to retrieve the translations language pair 99 * if the context menu was opened in a context relevant to 100 * open the SelectTranslationsPanel. 101 * 102 * @type {Promise<{sourceLanguage: string, targetLanguage: string}>} 103 */ 104 #translationsLangPairPromise; 105 106 /** 107 * The value of the `main-context-menu-new-feature-badge` l10n string. Fetched 108 * lazily. 109 * 110 * @type {string} 111 */ 112 #newFeatureBadgeL10nString; 113 114 constructor(aXulMenu, aIsShift) { 115 this.window = aXulMenu.ownerGlobal; 116 this.document = aXulMenu.ownerDocument; 117 118 // Get contextual info. 119 this.setContext(); 120 121 if (!this.shouldDisplay) { 122 return; 123 } 124 125 const { gBrowser } = this.window; 126 127 this.isContentSelected = !this.selectionInfo.docSelectionIsCollapsed; 128 if (!aIsShift) { 129 let tab = 130 gBrowser && gBrowser.getTabForBrowser 131 ? gBrowser.getTabForBrowser(this.browser) 132 : undefined; 133 134 let subject = { 135 menu: aXulMenu, 136 tab, 137 timeStamp: this.timeStamp, 138 isContentSelected: this.isContentSelected, 139 inFrame: this.inFrame, 140 isTextSelected: this.isTextSelected, 141 onTextInput: this.onTextInput, 142 onLink: this.onLink, 143 onImage: this.onImage, 144 onVideo: this.onVideo, 145 onAudio: this.onAudio, 146 onCanvas: this.onCanvas, 147 onEditable: this.onEditable, 148 onSpellcheckable: this.onSpellcheckable, 149 onPassword: this.onPassword, 150 passwordRevealed: this.passwordRevealed, 151 srcUrl: this.originalMediaURL, 152 frameUrl: this.contentData ? this.contentData.docLocation : undefined, 153 pageUrl: this.browser ? this.browser.currentURI.spec : undefined, 154 linkText: this.linkTextStr, 155 linkUrl: this.linkURL, 156 linkURI: this.linkURI, 157 selectionText: this.isTextSelected 158 ? this.selectionInfo.fullText 159 : undefined, 160 frameId: this.frameID, 161 webExtBrowserType: this.webExtBrowserType, 162 webExtContextData: this.contentData 163 ? this.contentData.webExtContextData 164 : undefined, 165 }; 166 subject.wrappedJSObject = subject; 167 Services.obs.notifyObservers(subject, "on-build-contextmenu"); 168 } 169 170 this.viewFrameSourceElement = this.document.getElementById( 171 "context-viewframesource" 172 ); 173 174 // Reset after "on-build-contextmenu" notification in case selection was 175 // changed during the notification. 176 this.isContentSelected = !this.selectionInfo.docSelectionIsCollapsed; 177 this.onPlainTextLink = false; 178 179 // Initialize (disable/remove) menu items. 180 this.initItems(aXulMenu); 181 } 182 183 setContext() { 184 let context = Object.create(null); 185 186 if (nsContextMenu.contentData) { 187 this.contentData = nsContextMenu.contentData; 188 context = this.contentData.context; 189 nsContextMenu.contentData = null; 190 } 191 192 const { gBrowser } = this.window; 193 194 this.shouldDisplay = context.shouldDisplay; 195 this.timeStamp = context.timeStamp; 196 197 // Assign what's _possibly_ needed from `context` sent by ContextMenuChild.sys.mjs 198 // Keep this consistent with the similar code in ContextMenu's _setContext 199 this.imageDescURL = context.imageDescURL; 200 this.imageInfo = context.imageInfo; 201 this.mediaURL = context.mediaURL || context.bgImageURL; 202 this.originalMediaURL = context.originalMediaURL || this.mediaURL; 203 this.webExtBrowserType = context.webExtBrowserType; 204 205 this.canSpellCheck = context.canSpellCheck; 206 this.hasBGImage = context.hasBGImage; 207 this.hasMultipleBGImages = context.hasMultipleBGImages; 208 this.isDesignMode = context.isDesignMode; 209 this.inFrame = context.inFrame; 210 this.inPDFViewer = context.inPDFViewer; 211 this.inPDFEditor = context.inPDFEditor; 212 this.inSrcdocFrame = context.inSrcdocFrame; 213 this.inSyntheticDoc = context.inSyntheticDoc; 214 this.inTabBrowser = context.inTabBrowser; 215 this.inWebExtBrowser = context.inWebExtBrowser; 216 217 this.link = context.link; 218 this.linkDownload = context.linkDownload; 219 this.linkProtocol = context.linkProtocol; 220 this.linkTextStr = context.linkTextStr; 221 this.linkURL = context.linkURL; 222 this.linkURI = this.getLinkURI(); // can't send; regenerate 223 224 this.onAudio = context.onAudio; 225 this.onCanvas = context.onCanvas; 226 this.onCompletedImage = context.onCompletedImage; 227 this.onDRMMedia = context.onDRMMedia; 228 this.onPiPVideo = context.onPiPVideo; 229 this.onEditable = context.onEditable; 230 this.onImage = context.onImage; 231 this.onSearchField = context.onSearchField; 232 this.onLink = context.onLink; 233 this.onLoadedImage = context.onLoadedImage; 234 this.onMailtoLink = context.onMailtoLink; 235 this.onTelLink = context.onTelLink; 236 this.onMozExtLink = context.onMozExtLink; 237 this.onNumeric = context.onNumeric; 238 this.onPassword = context.onPassword; 239 this.passwordRevealed = context.passwordRevealed; 240 this.onSaveableLink = context.onSaveableLink; 241 this.onSpellcheckable = context.onSpellcheckable; 242 this.onTextInput = context.onTextInput; 243 this.onVideo = context.onVideo; 244 245 this.pdfEditorStates = context.pdfEditorStates; 246 247 this.target = context.target; 248 this.targetIdentifier = context.targetIdentifier; 249 250 this.principal = context.principal; 251 this.storagePrincipal = context.storagePrincipal; 252 this.frameID = context.frameID; 253 this.frameOuterWindowID = context.frameOuterWindowID; 254 this.frameBrowsingContext = BrowsingContext.get( 255 context.frameBrowsingContextID 256 ); 257 258 this.inSyntheticDoc = context.inSyntheticDoc; 259 this.inAboutDevtoolsToolbox = context.inAboutDevtoolsToolbox; 260 261 this.isSponsoredLink = context.isSponsoredLink; 262 263 // Everything after this isn't sent directly from ContextMenu 264 if (this.target) { 265 this.ownerDoc = this.target.ownerDocument; 266 } 267 268 this.policyContainer = lazy.E10SUtils.deserializePolicyContainer( 269 context.policyContainer 270 ); 271 272 if (this.contentData) { 273 this.browser = this.contentData.browser; 274 this.selectionInfo = this.contentData.selectionInfo; 275 this.actor = this.contentData.actor; 276 } else { 277 const { SelectionUtils } = ChromeUtils.importESModule( 278 "resource://gre/modules/SelectionUtils.sys.mjs" 279 ); 280 281 this.browser = this.ownerDoc.defaultView.docShell.chromeEventHandler; 282 this.selectionInfo = SelectionUtils.getSelectionDetails( 283 this.browser.ownerGlobal 284 ); 285 this.actor = 286 this.browser.browsingContext.currentWindowGlobal.getActor( 287 "ContextMenu" 288 ); 289 } 290 291 this.remoteType = this.actor.manager.domProcess.remoteType; 292 293 this.selectedText = this.selectionInfo.text; 294 this.isTextSelected = !!this.selectedText.length; 295 this.webExtBrowserType = this.browser.getAttribute( 296 "webextension-view-type" 297 ); 298 this.inWebExtBrowser = !!this.webExtBrowserType; 299 this.inTabBrowser = 300 gBrowser && gBrowser.getTabForBrowser 301 ? !!gBrowser.getTabForBrowser(this.browser) 302 : false; 303 304 let { InlineSpellCheckerUI } = this.window; 305 if (context.shouldInitInlineSpellCheckerUINoChildren) { 306 InlineSpellCheckerUI.initFromRemote( 307 this.contentData.spellInfo, 308 this.actor.manager 309 ); 310 } 311 312 if (this.contentData.spellInfo) { 313 this.spellSuggestions = this.contentData.spellInfo.spellSuggestions; 314 } 315 316 if (context.shouldInitInlineSpellCheckerUIWithChildren) { 317 InlineSpellCheckerUI.initFromRemote( 318 this.contentData.spellInfo, 319 this.actor.manager 320 ); 321 let canSpell = InlineSpellCheckerUI.canSpellCheck && this.canSpellCheck; 322 this.showItem("spell-check-enabled", canSpell); 323 } 324 325 this.hasTextFragments = context.hasTextFragments; 326 this.textFragmentURL = null; 327 } // setContext 328 329 hiding(aXulMenu) { 330 if (this.actor) { 331 this.actor.hiding(); 332 } 333 334 aXulMenu.showHideSeparators = null; 335 336 this.contentData = null; 337 this.window.InlineSpellCheckerUI.clearSuggestionsFromMenu(); 338 this.window.InlineSpellCheckerUI.clearDictionaryListFromMenu(); 339 this.window.InlineSpellCheckerUI.uninit(); 340 if ( 341 Cu.isESModuleLoaded( 342 "resource://gre/modules/LoginManagerContextMenu.sys.mjs" 343 ) 344 ) { 345 lazy.LoginManagerContextMenu.clearLoginsFromMenu(this.document); 346 } 347 348 // This handler self-deletes, only run it if it is still there: 349 if (this._onPopupHiding) { 350 this._onPopupHiding(); 351 } 352 } 353 354 initItems(aXulMenu) { 355 this.initOpenItems(); 356 this.initNavigationItems(); 357 this.initViewItems(); 358 this.initImageItems(); 359 this.initMiscItems(); 360 this.initSpellingItems(); 361 this.initSaveItems(); 362 this.initSyncItems(); 363 this.initClipboardItems(); 364 this.initMediaPlayerItems(); 365 this.initLeaveDOMFullScreenItems(); 366 this.initPasswordManagerItems(); 367 this.initViewSourceItems(); 368 this.initScreenshotItem(); 369 this.initPasswordControlItems(); 370 this.initPDFItems(); 371 this.initTextFragmentItems(); 372 373 this.showHideSeparators(aXulMenu); 374 if (!aXulMenu.showHideSeparators) { 375 // Set the showHideSeparators function on the menu itself so that 376 // the extension code (ext-menus.js) can call it after modifying 377 // the menus. 378 aXulMenu.showHideSeparators = () => { 379 this.showHideSeparators(aXulMenu); 380 }; 381 } 382 } 383 384 initPDFItems() { 385 for (const id of [ 386 "context-pdfjs-undo", 387 "context-pdfjs-redo", 388 "context-sep-pdfjs-redo", 389 "context-pdfjs-cut", 390 "context-pdfjs-copy", 391 "context-pdfjs-paste", 392 "context-pdfjs-delete", 393 "context-pdfjs-selectall", 394 "context-sep-pdfjs-selectall", 395 ]) { 396 this.showItem(id, this.inPDFEditor); 397 } 398 399 const hasSelectedText = this.pdfEditorStates?.hasSelectedText ?? false; 400 this.showItem( 401 "context-pdfjs-comment-selection", 402 lazy.PDFJS_ENABLE_COMMENT && hasSelectedText 403 ); 404 this.showItem("context-pdfjs-highlight-selection", hasSelectedText); 405 406 if (!this.inPDFEditor) { 407 return; 408 } 409 410 const { 411 isEmpty, 412 hasSomethingToUndo, 413 hasSomethingToRedo, 414 hasSelectedEditor, 415 } = this.pdfEditorStates; 416 417 const hasEmptyClipboard = !Services.clipboard.hasDataMatchingFlavors( 418 ["application/pdfjs"], 419 Ci.nsIClipboard.kGlobalClipboard 420 ); 421 422 this.setItemAttr("context-pdfjs-undo", "disabled", !hasSomethingToUndo); 423 this.setItemAttr("context-pdfjs-redo", "disabled", !hasSomethingToRedo); 424 this.setItemAttr( 425 "context-sep-pdfjs-redo", 426 "disabled", 427 !hasSomethingToUndo && !hasSomethingToRedo 428 ); 429 this.setItemAttr( 430 "context-pdfjs-cut", 431 "disabled", 432 isEmpty || !hasSelectedEditor 433 ); 434 this.setItemAttr( 435 "context-pdfjs-copy", 436 "disabled", 437 isEmpty || !hasSelectedEditor 438 ); 439 this.setItemAttr("context-pdfjs-paste", "disabled", hasEmptyClipboard); 440 this.setItemAttr( 441 "context-pdfjs-delete", 442 "disabled", 443 isEmpty || !hasSelectedEditor 444 ); 445 this.setItemAttr("context-pdfjs-selectall", "disabled", isEmpty); 446 this.setItemAttr("context-sep-pdfjs-selectall", "disabled", isEmpty); 447 } 448 449 initTextFragmentItems() { 450 const shouldShow = 451 lazy.TEXT_FRAGMENTS_ENABLED && 452 !( 453 this.inPDFViewer || 454 this.inFrame || 455 this.onEditable || 456 this.browser.currentURI.schemeIs("view-source") 457 ) && 458 (this.hasTextFragments || this.isContentSelected); 459 this.showItem("context-copy-link-to-highlight", shouldShow); 460 this.showItem( 461 "context-copy-clean-link-to-highlight", 462 shouldShow && lazy.STRIP_ON_SHARE_ENABLED 463 ); 464 465 // disables both options by default, while API tries to build a text fragment 466 this.setItemAttr("context-copy-link-to-highlight", "disabled", true); 467 this.setItemAttr("context-copy-clean-link-to-highlight", "disabled", true); 468 469 // Only show remove option if there are text fragments on the page. 470 this.showItem("context-sep-highlights", this.hasTextFragments); 471 this.showItem("context-remove-highlight", this.hasTextFragments); 472 } 473 474 async getTextDirective() { 475 if (!lazy.TEXT_FRAGMENTS_ENABLED) { 476 return; 477 } 478 this.textFragmentURL = await this.actor.getTextDirective(); 479 480 // enable menu items when a text fragment can be built 481 if (this.textFragmentURL) { 482 this.setItemAttr("context-copy-link-to-highlight", "disabled", null); 483 let link = this.getLinkURI(this.textFragmentURL); 484 let disabledAttr = this.#canStripParams(link) ? null : true; 485 this.setItemAttr( 486 "context-copy-clean-link-to-highlight", 487 "disabled", 488 disabledAttr 489 ); 490 } 491 } 492 493 async removeAllTextFragments() { 494 await this.actor.removeAllTextFragments(); 495 } 496 497 copyLinkToHighlight(stripSiteTracking = false) { 498 if (this.textFragmentURL) { 499 if (stripSiteTracking) { 500 const uri = this.getLinkURI(this.textFragmentURL); 501 this.copyStrippedLink(uri); 502 } else { 503 this.copyLink(this.textFragmentURL); 504 } 505 } 506 } 507 508 initOpenItems() { 509 var isMailtoInternal = false; 510 if (this.onMailtoLink) { 511 var mailtoHandler = Cc[ 512 "@mozilla.org/uriloader/external-protocol-service;1" 513 ] 514 .getService(Ci.nsIExternalProtocolService) 515 .getProtocolHandlerInfo("mailto"); 516 isMailtoInternal = 517 !mailtoHandler.alwaysAskBeforeHandling && 518 mailtoHandler.preferredAction == Ci.nsIHandlerInfo.useHelperApp && 519 mailtoHandler.preferredApplicationHandler instanceof 520 Ci.nsIWebHandlerApp; 521 } 522 523 if ( 524 this.isTextSelected && 525 !this.onLink && 526 this.selectionInfo && 527 this.selectionInfo.linkURL 528 ) { 529 this.linkURL = this.selectionInfo.linkURL; 530 this.linkURI = this.getLinkURI(); 531 532 this.linkTextStr = this.selectionInfo.linkText; 533 this.onPlainTextLink = true; 534 } 535 536 let { window, document } = this; 537 var inContainer = false; 538 if (this.contentData.userContextId) { 539 inContainer = true; 540 var item = document.getElementById("context-openlinkincontainertab"); 541 542 item.setAttribute("data-usercontextid", this.contentData.userContextId); 543 544 var label = lazy.ContextualIdentityService.getUserContextLabel( 545 this.contentData.userContextId 546 ); 547 548 document.l10n.setAttributes( 549 item, 550 "main-context-menu-open-link-in-container-tab", 551 { 552 containerName: label, 553 } 554 ); 555 } 556 557 var shouldShow = 558 this.onSaveableLink || isMailtoInternal || this.onPlainTextLink; 559 var isWindowPrivate = lazy.PrivateBrowsingUtils.isWindowPrivate(window); 560 let showContainers = 561 Services.prefs.getBoolPref("privacy.userContext.enabled") && 562 lazy.ContextualIdentityService.getPublicIdentities().length; 563 this.showItem("context-openlink", shouldShow && !isWindowPrivate); 564 this.showItem( 565 "context-openlinkprivate", 566 shouldShow && lazy.PrivateBrowsingUtils.enabled 567 ); 568 this.showItem("context-openlinkintab", shouldShow && !inContainer); 569 this.showItem("context-openlinkincontainertab", shouldShow && inContainer); 570 this.showItem( 571 "context-openlinkinusercontext-menu", 572 shouldShow && !isWindowPrivate && showContainers 573 ); 574 this.showItem("context-openlinkincurrent", this.onPlainTextLink); 575 // LinkPreview.sys.mjs is missing. tor-browser#44045. 576 this.showItem("context-previewlink", false); 577 } 578 579 initNavigationItems() { 580 var shouldShow = 581 !( 582 this.isContentSelected || 583 this.onLink || 584 this.onImage || 585 this.onCanvas || 586 this.onVideo || 587 this.onAudio || 588 this.onTextInput 589 ) && this.inTabBrowser; 590 if (AppConstants.platform == "macosx") { 591 for (let id of [ 592 "context-back", 593 "context-forward", 594 "context-reload", 595 "context-stop", 596 "context-sep-navigation", 597 ]) { 598 this.showItem(id, shouldShow); 599 } 600 } else { 601 this.showItem("context-navigation", shouldShow); 602 } 603 604 let stopped = 605 this.window.XULBrowserWindow.stopCommand.getAttribute("disabled") == 606 "true"; 607 608 let stopReloadItem = ""; 609 if (shouldShow) { 610 stopReloadItem = stopped ? "reload" : "stop"; 611 } 612 613 this.showItem("context-reload", stopReloadItem == "reload"); 614 this.showItem("context-stop", stopReloadItem == "stop"); 615 616 let { document } = this; 617 let initBackForwardMenuItemTooltip = (menuItemId, l10nId, shortcutId) => { 618 // On macOS regular menuitems are used and the shortcut isn't added 619 if (AppConstants.platform == "macosx") { 620 return; 621 } 622 623 let shortcut = document.getElementById(shortcutId); 624 if (shortcut) { 625 shortcut = lazy.ShortcutUtils.prettifyShortcut(shortcut); 626 } else { 627 // Sidebar doesn't have navigation buttons or shortcuts, but we still 628 // want to format the menu item tooltip to remove "$shortcut" string. 629 shortcut = ""; 630 } 631 632 let menuItem = document.getElementById(menuItemId); 633 document.l10n.setAttributes(menuItem, l10nId, { shortcut }); 634 }; 635 636 initBackForwardMenuItemTooltip( 637 "context-back", 638 "main-context-menu-back-2", 639 "goBackKb" 640 ); 641 642 initBackForwardMenuItemTooltip( 643 "context-forward", 644 "main-context-menu-forward-2", 645 "goForwardKb" 646 ); 647 } 648 649 initLeaveDOMFullScreenItems() { 650 // only show the option if the user is in DOM fullscreen 651 var shouldShow = this.target.ownerDocument.fullscreen; 652 this.showItem("context-leave-dom-fullscreen", shouldShow); 653 } 654 655 initSaveItems() { 656 var shouldShow = !( 657 this.onTextInput || 658 this.onLink || 659 this.isContentSelected || 660 this.onImage || 661 this.onCanvas || 662 this.onVideo || 663 this.onAudio 664 ); 665 this.showItem("context-savepage", shouldShow); 666 667 // Save link depends on whether we're in a link, or selected text matches valid URL pattern. 668 this.showItem( 669 "context-savelink", 670 this.onSaveableLink || this.onPlainTextLink 671 ); 672 if ( 673 (this.onSaveableLink || this.onPlainTextLink) && 674 Services.policies.status === Services.policies.ACTIVE 675 ) { 676 this.setItemAttr( 677 "context-savelink", 678 "disabled", 679 !lazy.WebsiteFilter.isAllowed(this.linkURL) 680 ); 681 } 682 683 // Save video and audio don't rely on whether it has loaded or not. 684 this.showItem("context-savevideo", this.onVideo); 685 this.showItem("context-saveaudio", this.onAudio); 686 this.showItem("context-video-saveimage", this.onVideo); 687 this.setItemAttr("context-savevideo", "disabled", !this.mediaURL); 688 this.setItemAttr("context-saveaudio", "disabled", !this.mediaURL); 689 this.showItem("context-sendvideo", this.onVideo); 690 this.showItem("context-sendaudio", this.onAudio); 691 let mediaIsBlob = this.mediaURL.startsWith("blob:"); 692 this.setItemAttr( 693 "context-sendvideo", 694 "disabled", 695 !this.mediaURL || mediaIsBlob 696 ); 697 this.setItemAttr( 698 "context-sendaudio", 699 "disabled", 700 !this.mediaURL || mediaIsBlob 701 ); 702 703 if ( 704 Services.policies.status === Services.policies.ACTIVE && 705 !Services.policies.isAllowed("filepickers") 706 ) { 707 // When file pickers are disallowed by enterprise policy, 708 // these items silently fail. So to avoid confusion, we 709 // disable them. 710 for (let item of [ 711 "context-savepage", 712 "context-savelink", 713 "context-savevideo", 714 "context-saveaudio", 715 "context-video-saveimage", 716 "context-saveaudio", 717 ]) { 718 this.setItemAttr(item, "disabled", true); 719 } 720 } 721 } 722 723 initImageItems() { 724 // Reload image depends on an image that's not fully loaded 725 this.showItem( 726 "context-reloadimage", 727 this.onImage && !this.onCompletedImage 728 ); 729 730 // View image depends on having an image that's not standalone 731 // (or is in a frame), or a canvas. If this isn't an image, check 732 // if there is a background image. 733 let showViewImage = 734 ((this.onImage && (!this.inSyntheticDoc || this.inFrame)) || 735 this.onCanvas) && 736 !this.inPDFViewer; 737 let showBGImage = 738 this.hasBGImage && 739 !this.hasMultipleBGImages && 740 !this.inSyntheticDoc && 741 !this.inPDFViewer && 742 !this.isContentSelected && 743 !this.onImage && 744 !this.onCanvas && 745 !this.onVideo && 746 !this.onAudio && 747 !this.onLink && 748 !this.onTextInput; 749 this.showItem("context-viewimage", showViewImage || showBGImage); 750 751 // Save image depends on having loaded its content. 752 this.showItem( 753 "context-saveimage", 754 (this.onLoadedImage || this.onCanvas) && !this.inPDFEditor 755 ); 756 757 if (Services.policies.status === Services.policies.ACTIVE) { 758 // When file pickers are disallowed by enterprise policy, 759 // this item silently fails. So to avoid confusion, we 760 // disable it. 761 this.setItemAttr( 762 "context-saveimage", 763 "disabled", 764 !Services.policies.isAllowed("filepickers") 765 ); 766 } 767 768 // Copy image contents depends on whether we're on an image. 769 // Note: the element doesn't exist on all platforms, but showItem() takes 770 // care of that by itself. 771 this.showItem("context-copyimage-contents", this.onImage); 772 773 // Copy image location depends on whether we're on an image. 774 this.showItem("context-copyimage", this.onImage || showBGImage); 775 776 // Performing text recognition only works on images, and if the feature is enabled. 777 this.showItem( 778 "context-imagetext", 779 this.onImage && 780 Services.appinfo.isTextRecognitionSupported && 781 lazy.TEXT_RECOGNITION_ENABLED 782 ); 783 784 // Send media URL (but not for canvas, since it's a big data: URL) 785 this.showItem("context-sendimage", this.onImage || showBGImage); 786 787 // View Image Info defaults to false, user can enable 788 var showViewImageInfo = 789 this.onImage && 790 Services.prefs.getBoolPref("browser.menu.showViewImageInfo", false); 791 792 this.showItem("context-viewimageinfo", showViewImageInfo); 793 // The image info popup is broken for WebExtension popups, since the browser 794 // is destroyed when the popup is closed. 795 this.setItemAttr( 796 "context-viewimageinfo", 797 "disabled", 798 this.webExtBrowserType === "popup" 799 ); 800 // Open the link to more details about the image. Does not apply to 801 // background images. 802 this.showItem( 803 "context-viewimagedesc", 804 this.onImage && this.imageDescURL !== "" 805 ); 806 807 this.showAndFormatVisualSearchContextItem(); 808 809 // Set as Desktop background depends on whether an image was clicked on, 810 // and only works if we have a shell service. 811 var haveSetDesktopBackground = false; 812 813 if ( 814 AppConstants.HAVE_SHELL_SERVICE && 815 Services.policies.isAllowed("setDesktopBackground") 816 ) { 817 // Only enable Set as Desktop Background if we can get the shell service. 818 var shell = this.window.getShellService(); 819 if (shell) { 820 haveSetDesktopBackground = shell.canSetDesktopBackground; 821 } 822 } 823 824 this.showItem( 825 "context-setDesktopBackground", 826 haveSetDesktopBackground && this.onLoadedImage 827 ); 828 829 if (haveSetDesktopBackground && this.onLoadedImage) { 830 this.document.getElementById("context-setDesktopBackground").disabled = 831 this.contentData.disableSetDesktopBackground; 832 } 833 } 834 835 initViewItems() { 836 // View source is always OK, unless in directory listing. 837 this.showItem( 838 "context-viewpartialsource-selection", 839 !this.inAboutDevtoolsToolbox && 840 this.isContentSelected && 841 this.selectionInfo.isDocumentLevelSelection 842 ); 843 844 this.showItem( 845 "context-print-selection", 846 !this.inAboutDevtoolsToolbox && 847 this.isContentSelected && 848 this.selectionInfo.isDocumentLevelSelection && 849 lazy.gPrintEnabled 850 ); 851 852 var shouldShow = !( 853 this.isContentSelected || 854 this.onImage || 855 this.onCanvas || 856 this.onVideo || 857 this.onAudio || 858 this.onLink || 859 this.onTextInput 860 ); 861 862 var showInspect = 863 this.inTabBrowser && 864 !this.inAboutDevtoolsToolbox && 865 Services.prefs.getBoolPref("devtools.inspector.enabled", true) && 866 !Services.prefs.getBoolPref("devtools.policy.disabled", false); 867 868 var showInspectA11Y = 869 showInspect && 870 Services.prefs.getBoolPref("devtools.accessibility.enabled", false) && 871 Services.prefs.getBoolPref("devtools.enabled", true) && 872 (Services.prefs.getBoolPref("devtools.everOpened", false) || 873 // Note: this is a legacy usecase, we will remove it in bug 1695257, 874 // once existing users have had time to set devtools.everOpened 875 // through normal use, and we've passed an ESR cycle (91). 876 lazy.DevToolsShim.isDevToolsUser()); 877 878 this.showItem("context-viewsource", shouldShow); 879 this.showItem("context-inspect", showInspect); 880 881 this.showItem("context-inspect-a11y", showInspectA11Y); 882 883 // View video depends on not having a standalone video. 884 this.showItem( 885 "context-viewvideo", 886 this.onVideo && (!this.inSyntheticDoc || this.inFrame) 887 ); 888 this.setItemAttr("context-viewvideo", "disabled", !this.mediaURL); 889 } 890 891 initMiscItems() { 892 let { window, document } = this; 893 // Use "Bookmark Link…" if on a link. 894 let bookmarkPage = document.getElementById("context-bookmarkpage"); 895 this.showItem( 896 bookmarkPage, 897 !( 898 this.isContentSelected || 899 this.onTextInput || 900 this.onLink || 901 this.onImage || 902 this.onVideo || 903 this.onAudio || 904 this.onCanvas || 905 this.inWebExtBrowser 906 ) 907 ); 908 909 this.showItem( 910 "context-bookmarklink", 911 (this.onLink && 912 !this.onMailtoLink && 913 !this.onTelLink && 914 !this.onMozExtLink) || 915 this.onPlainTextLink 916 ); 917 this.showItem("context-add-engine", this.shouldShowAddEngine()); 918 this.showItem("frame", this.inFrame); 919 920 if (this.inFrame) { 921 // To make it easier to debug the browser running with out-of-process iframes, we 922 // display the process PID of the iframe in the context menu for the subframe. 923 let frameOsPid = 924 this.actor.manager.browsingContext.currentWindowGlobal.osPid; 925 this.setItemAttr("context-frameOsPid", "label", "PID: " + frameOsPid); 926 } 927 928 this.showAndFormatSearchContextItem(); 929 this.showTranslateSelectionItem(); 930 // GenAI.sys.mjs is missing. tor-browser#44045. 931 // Need to remove the element from the DOM since otherwise it will cause an 932 // error due to `.menupopup === null`. 933 document.getElementById("context-ask-chat")?.remove(); 934 935 // srcdoc cannot be opened separately due to concerns about web 936 // content with about:srcdoc in location bar masquerading as trusted 937 // chrome/addon content. 938 // No need to also test for this.inFrame as this is checked in the parent 939 // submenu. 940 this.showItem("context-showonlythisframe", !this.inSrcdocFrame); 941 this.showItem("context-openframeintab", !this.inSrcdocFrame); 942 this.showItem("context-openframe", !this.inSrcdocFrame); 943 this.showItem("context-bookmarkframe", !this.inSrcdocFrame); 944 this.showItem("context-printframe", lazy.gPrintEnabled); 945 this.showItem("print-frame-sep", lazy.gPrintEnabled); 946 947 // Hide menu entries for images, show otherwise 948 if (this.inFrame) { 949 this.viewFrameSourceElement.hidden = 950 !lazy.BrowserUtils.mimeTypeIsTextBased( 951 this.target.ownerDocument.contentType 952 ); 953 } 954 955 // BiDi UI 956 this.showItem( 957 "context-bidi-text-direction-toggle", 958 this.onTextInput && !this.onNumeric && window.top.gBidiUI 959 ); 960 this.showItem( 961 "context-bidi-page-direction-toggle", 962 !this.onTextInput && window.top.gBidiUI 963 ); 964 } 965 966 initSpellingItems() { 967 let { document } = this; 968 let { InlineSpellCheckerUI } = this.window; 969 var canSpell = 970 InlineSpellCheckerUI.canSpellCheck && 971 !InlineSpellCheckerUI.initialSpellCheckPending && 972 this.canSpellCheck; 973 let showDictionaries = canSpell && InlineSpellCheckerUI.enabled; 974 var onMisspelling = InlineSpellCheckerUI.overMisspelling; 975 var showUndo = canSpell && InlineSpellCheckerUI.canUndo(); 976 this.showItem("spell-check-enabled", canSpell); 977 document 978 .getElementById("spell-check-enabled") 979 .toggleAttribute("checked", canSpell && InlineSpellCheckerUI.enabled); 980 981 this.showItem("spell-add-to-dictionary", onMisspelling); 982 this.showItem("spell-undo-add-to-dictionary", showUndo); 983 984 // suggestion list 985 if (onMisspelling) { 986 var suggestionsSeparator = document.getElementById( 987 "spell-add-to-dictionary" 988 ); 989 var numsug = InlineSpellCheckerUI.addSuggestionsToMenu( 990 suggestionsSeparator.parentNode, 991 suggestionsSeparator, 992 this.spellSuggestions 993 ); 994 this.showItem("spell-no-suggestions", numsug == 0); 995 } else { 996 this.showItem("spell-no-suggestions", false); 997 } 998 999 // dictionary list 1000 this.showItem("spell-dictionaries", showDictionaries); 1001 if (canSpell) { 1002 var dictMenu = document.getElementById("spell-dictionaries-menu"); 1003 var dictSep = document.getElementById("spell-language-separator"); 1004 InlineSpellCheckerUI.addDictionaryListToMenu(dictMenu, dictSep); 1005 this.showItem("spell-add-dictionaries-main", false); 1006 } else if (this.onSpellcheckable) { 1007 // when there is no spellchecker but we might be able to spellcheck 1008 // add the add to dictionaries item. This will ensure that people 1009 // with no dictionaries will be able to download them 1010 this.showItem("spell-add-dictionaries-main", showDictionaries); 1011 } else { 1012 this.showItem("spell-add-dictionaries-main", false); 1013 } 1014 } 1015 1016 initClipboardItems() { 1017 // Copy depends on whether there is selected text. 1018 // Enabling this context menu item is now done through the global 1019 // command updating system 1020 // this.setItemAttr( "context-copy", "disabled", !this.isTextSelected() ); 1021 this.window.goUpdateGlobalEditMenuItems(); 1022 1023 this.showItem("context-undo", this.onTextInput); 1024 this.showItem("context-redo", this.onTextInput); 1025 this.showItem("context-cut", this.onTextInput); 1026 this.showItem("context-copy", this.isContentSelected || this.onTextInput); 1027 this.showItem("context-paste", this.onTextInput); 1028 this.showItem("context-paste-no-formatting", this.isDesignMode); 1029 this.showItem("context-delete", this.onTextInput); 1030 this.showItem( 1031 "context-selectall", 1032 !( 1033 this.onLink || 1034 this.onImage || 1035 this.onVideo || 1036 this.onAudio || 1037 this.inSyntheticDoc || 1038 this.inPDFEditor 1039 ) || this.isDesignMode 1040 ); 1041 1042 // XXX dr 1043 // ------ 1044 // nsDocumentViewer.cpp has code to determine whether we're 1045 // on a link or an image. we really ought to be using that... 1046 1047 // Copy email link depends on whether we're on an email link. 1048 this.showItem("context-copyemail", this.onMailtoLink); 1049 1050 // Copy phone link depends on whether we're on a phone link. 1051 this.showItem("context-copyphone", this.onTelLink); 1052 1053 // Copy link location depends on whether we're on a non-mailto link. 1054 this.showItem( 1055 "context-copylink", 1056 this.onLink && !this.onMailtoLink && !this.onTelLink 1057 ); 1058 1059 // Showing "Copy Clean link" depends on whether the strip-on-share feature is enabled 1060 // and the user is selecting a URL 1061 this.showItem( 1062 "context-stripOnShareLink", 1063 lazy.STRIP_ON_SHARE_ENABLED && 1064 (this.onLink || this.onPlainTextLink) && 1065 !this.onMailtoLink && 1066 !this.onTelLink && 1067 !this.onMozExtLink && 1068 !this.isSecureAboutPage() 1069 ); 1070 1071 let disabledAttr = this.#canStripParams() ? null : true; 1072 this.setItemAttr("context-stripOnShareLink", "disabled", disabledAttr); 1073 1074 let sendLinkSeparator = this.document.getElementById( 1075 "context-sep-sendlinktodevice" 1076 ); 1077 sendLinkSeparator.toggleAttribute("ensureHidden", !this.syncItemsShown); 1078 1079 this.showItem("context-copyvideourl", this.onVideo); 1080 this.showItem("context-copyaudiourl", this.onAudio); 1081 this.setItemAttr("context-copyvideourl", "disabled", !this.mediaURL); 1082 this.setItemAttr("context-copyaudiourl", "disabled", !this.mediaURL); 1083 } 1084 1085 initMediaPlayerItems() { 1086 var onMedia = this.onVideo || this.onAudio; 1087 // Several mutually exclusive items... play/pause, mute/unmute, show/hide 1088 this.showItem( 1089 "context-media-play", 1090 onMedia && (this.target.paused || this.target.ended) 1091 ); 1092 this.showItem( 1093 "context-media-pause", 1094 onMedia && !this.target.paused && !this.target.ended 1095 ); 1096 this.showItem("context-media-mute", onMedia && !this.target.muted); 1097 this.showItem("context-media-unmute", onMedia && this.target.muted); 1098 this.showItem( 1099 "context-media-playbackrate", 1100 onMedia && this.target.duration != Number.POSITIVE_INFINITY 1101 ); 1102 this.showItem("context-media-loop", onMedia); 1103 this.showItem( 1104 "context-media-showcontrols", 1105 onMedia && !this.target.controls 1106 ); 1107 this.showItem( 1108 "context-media-hidecontrols", 1109 this.target.controls && 1110 (this.onVideo || (this.onAudio && !this.inSyntheticDoc)) 1111 ); 1112 this.showItem( 1113 "context-video-fullscreen", 1114 this.onVideo && !this.target.ownerDocument.fullscreen 1115 ); 1116 { 1117 let shouldDisplay = 1118 Services.prefs.getBoolPref( 1119 "media.videocontrols.picture-in-picture.enabled" 1120 ) && 1121 this.onVideo && 1122 !this.target.ownerDocument.fullscreen && 1123 this.target.readyState > 0; 1124 this.showItem("context-video-pictureinpicture", shouldDisplay); 1125 } 1126 this.showItem("context-media-eme-learnmore", this.onDRMMedia); 1127 1128 // Disable them when there isn't a valid media source loaded. 1129 if (onMedia) { 1130 this.setItemAttr( 1131 "context-media-playbackrate-050x", 1132 "checked", 1133 this.target.playbackRate == 0.5 1134 ); 1135 this.setItemAttr( 1136 "context-media-playbackrate-100x", 1137 "checked", 1138 this.target.playbackRate == 1.0 1139 ); 1140 this.setItemAttr( 1141 "context-media-playbackrate-125x", 1142 "checked", 1143 this.target.playbackRate == 1.25 1144 ); 1145 this.setItemAttr( 1146 "context-media-playbackrate-150x", 1147 "checked", 1148 this.target.playbackRate == 1.5 1149 ); 1150 this.setItemAttr( 1151 "context-media-playbackrate-200x", 1152 "checked", 1153 this.target.playbackRate == 2.0 1154 ); 1155 this.setItemAttr("context-media-loop", "checked", this.target.loop); 1156 var hasError = 1157 this.target.error != null || 1158 this.target.networkState == this.target.NETWORK_NO_SOURCE; 1159 this.setItemAttr("context-media-play", "disabled", hasError); 1160 this.setItemAttr("context-media-pause", "disabled", hasError); 1161 this.setItemAttr("context-media-mute", "disabled", hasError); 1162 this.setItemAttr("context-media-unmute", "disabled", hasError); 1163 this.setItemAttr("context-media-playbackrate", "disabled", hasError); 1164 this.setItemAttr("context-media-playbackrate-050x", "disabled", hasError); 1165 this.setItemAttr("context-media-playbackrate-100x", "disabled", hasError); 1166 this.setItemAttr("context-media-playbackrate-125x", "disabled", hasError); 1167 this.setItemAttr("context-media-playbackrate-150x", "disabled", hasError); 1168 this.setItemAttr("context-media-playbackrate-200x", "disabled", hasError); 1169 this.setItemAttr("context-media-showcontrols", "disabled", hasError); 1170 this.setItemAttr("context-media-hidecontrols", "disabled", hasError); 1171 if (this.onVideo) { 1172 let canSaveSnapshot = 1173 !this.onDRMMedia && 1174 this.target.readyState >= this.target.HAVE_CURRENT_DATA; 1175 this.setItemAttr( 1176 "context-video-saveimage", 1177 "disabled", 1178 !canSaveSnapshot 1179 ); 1180 this.setItemAttr("context-video-fullscreen", "disabled", hasError); 1181 this.setItemAttr( 1182 "context-video-pictureinpicture", 1183 "checked", 1184 this.onPiPVideo 1185 ); 1186 this.setItemAttr( 1187 "context-video-pictureinpicture", 1188 "disabled", 1189 !this.onPiPVideo && hasError 1190 ); 1191 } 1192 } 1193 } 1194 1195 initPasswordManagerItems() { 1196 let { document } = this; 1197 let showUseSavedLogin = false; 1198 let showGenerate = false; 1199 let showManage = false; 1200 let enableGeneration = Services.logins.isLoggedIn; 1201 try { 1202 // If we could not find a password field we don't want to 1203 // show the form fill, manage logins and the password generation items. 1204 if (!this.isLoginForm()) { 1205 return; 1206 } 1207 showManage = true; 1208 1209 // Disable the fill option if the user hasn't unlocked with their primary password 1210 // or if the password field or target field are disabled. 1211 // XXX: Bug 1529025 to maybe respect signon.rememberSignons. 1212 let loginFillInfo = this.contentData?.loginFillInfo; 1213 let disableFill = 1214 !Services.logins.isLoggedIn || 1215 loginFillInfo?.passwordField.disabled || 1216 loginFillInfo?.activeField.disabled; 1217 this.setItemAttr("fill-login", "disabled", disableFill); 1218 1219 let onPasswordLikeField = PASSWORD_FIELDNAME_HINTS.includes( 1220 loginFillInfo.activeField.fieldNameHint 1221 ); 1222 1223 // Set the correct label for the fill menu 1224 let fillMenu = document.getElementById("fill-login"); 1225 document.l10n.setAttributes( 1226 fillMenu, 1227 "main-context-menu-use-saved-password" 1228 ); 1229 1230 let documentURI = this.contentData?.documentURIObject; 1231 let formOrigin = lazy.LoginHelper.getLoginOrigin(documentURI?.spec); 1232 let isGeneratedPasswordEnabled = 1233 lazy.LoginHelper.generationAvailable && 1234 lazy.LoginHelper.generationEnabled; 1235 showGenerate = 1236 onPasswordLikeField && 1237 isGeneratedPasswordEnabled && 1238 Services.logins.getLoginSavingEnabled(formOrigin); 1239 1240 if (disableFill) { 1241 showUseSavedLogin = true; 1242 1243 // No need to update the submenu if the fill item is disabled. 1244 return; 1245 } 1246 1247 // Update sub-menu items. 1248 this.updatePasswordManagerSubMenuItems(document, formOrigin); 1249 } finally { 1250 const documentURI = this.contentData?.documentURIObject; 1251 const showRelay = 1252 this.contentData?.context.showRelay && 1253 lazy.LoginHelper.getLoginOrigin(documentURI?.spec); 1254 1255 this.showItem("fill-login", showUseSavedLogin); 1256 this.showItem("fill-login-generated-password", showGenerate); 1257 this.showItem("use-relay-mask", showRelay); 1258 this.showItem("manage-saved-logins", showManage); 1259 this.setItemAttr( 1260 "fill-login-generated-password", 1261 "disabled", 1262 !enableGeneration 1263 ); 1264 this.setItemAttr( 1265 "passwordmgr-items-separator", 1266 "ensureHidden", 1267 showUseSavedLogin || showGenerate || showManage || showRelay 1268 ? null 1269 : true 1270 ); 1271 } 1272 } 1273 1274 async updatePasswordManagerSubMenuItems(document, formOrigin) { 1275 const fragment = await lazy.LoginManagerContextMenu.addLoginsToMenu( 1276 this.targetIdentifier, 1277 this.browser, 1278 formOrigin 1279 ); 1280 1281 if (!fragment) { 1282 return; 1283 } 1284 1285 let popup = document.getElementById("fill-login-popup"); 1286 popup.appendChild(fragment); 1287 1288 this.showItem("fill-login", true); 1289 1290 this.setItemAttr("passwordmgr-items-separator", "ensureHidden", null); 1291 } 1292 1293 initSyncItems() { 1294 this.syncItemsShown = this.window.gSync.updateContentContextMenu(this); 1295 } 1296 1297 initViewSourceItems() { 1298 const getString = aName => { 1299 const { bundle } = this.window.gViewSourceUtils.getPageActor( 1300 this.browser 1301 ); 1302 return bundle.GetStringFromName(aName); 1303 }; 1304 const showViewSourceItem = (id, check, accesskey) => { 1305 const fullId = `context-viewsource-${id}`; 1306 this.showItem(fullId, onViewSource); 1307 if (!onViewSource) { 1308 return; 1309 } 1310 this.setItemAttr(fullId, "checked", check()); 1311 this.setItemAttr(fullId, "label", getString(`context_${id}_label`)); 1312 if (accesskey) { 1313 this.setItemAttr( 1314 fullId, 1315 "accesskey", 1316 getString(`context_${id}_accesskey`) 1317 ); 1318 } 1319 }; 1320 1321 const onViewSource = this.browser.currentURI.schemeIs("view-source"); 1322 1323 showViewSourceItem("goToLine", () => false, true); 1324 showViewSourceItem("wrapLongLines", () => 1325 Services.prefs.getBoolPref("view_source.wrap_long_lines", false) 1326 ); 1327 showViewSourceItem("highlightSyntax", () => 1328 Services.prefs.getBoolPref("view_source.syntax_highlight", false) 1329 ); 1330 } 1331 1332 // Iterate over the visible items on the menu and its submenus and 1333 // hide any duplicated separators next to each other. 1334 // The attribute "ensureHidden" will override this process and keep a particular separator hidden in special cases. 1335 showHideSeparators(aPopup) { 1336 let lastVisibleSeparator = null; 1337 let count = 0; 1338 for (let menuItem of aPopup.children) { 1339 // Skip any items that were added by the page menu. 1340 if (menuItem.hasAttribute("generateditemid")) { 1341 count++; 1342 continue; 1343 } 1344 1345 if (menuItem.localName == "menuseparator") { 1346 // Individual separators can have the `ensureHidden` attribute added to avoid them 1347 // becoming visible. We also set `count` to 0 below because otherwise the 1348 // next separator would be made visible, with the same visual effect. 1349 if (!count || menuItem.hasAttribute("ensureHidden")) { 1350 menuItem.hidden = true; 1351 } else { 1352 menuItem.hidden = false; 1353 lastVisibleSeparator = menuItem; 1354 } 1355 1356 count = 0; 1357 } else if (!menuItem.hidden) { 1358 if (menuItem.localName == "menu" && menuItem.menupopup) { 1359 this.showHideSeparators(menuItem.menupopup); 1360 } else if (menuItem.localName == "menugroup") { 1361 this.showHideSeparators(menuItem); 1362 } 1363 count++; 1364 } 1365 } 1366 1367 // If count is 0 yet lastVisibleSeparator is set, then there must be a separator 1368 // visible at the end of the menu, so hide it. Note that there could be more than 1369 // one but this isn't handled here. 1370 if (!count && lastVisibleSeparator) { 1371 lastVisibleSeparator.hidden = true; 1372 } 1373 } 1374 1375 shouldShowTakeScreenshot() { 1376 let shouldShow = 1377 lazy.ScreenshotsUtils.screenshotsEnabled && 1378 this.inTabBrowser && 1379 !this.onTextInput && 1380 !this.onLink && 1381 !this.onPlainTextLink && 1382 !this.onAudio && 1383 !this.onEditable && 1384 !this.onPassword; 1385 1386 return shouldShow; 1387 } 1388 1389 initScreenshotItem() { 1390 let shouldShow = this.shouldShowTakeScreenshot(); 1391 1392 this.showItem("context-sep-screenshots", shouldShow); 1393 this.showItem("context-take-screenshot", shouldShow); 1394 } 1395 1396 initPasswordControlItems() { 1397 let shouldShow = this.onPassword; 1398 if (shouldShow) { 1399 let revealPassword = this.document.getElementById( 1400 "context-reveal-password" 1401 ); 1402 revealPassword.toggleAttribute("checked", this.passwordRevealed); 1403 } 1404 this.showItem("context-reveal-password", shouldShow); 1405 } 1406 1407 toggleRevealPassword() { 1408 this.actor.toggleRevealPassword(this.targetIdentifier); 1409 } 1410 1411 openPasswordManager() { 1412 lazy.LoginHelper.openPasswordManager(this.window, { 1413 entryPoint: "Contextmenu", 1414 }); 1415 } 1416 1417 useRelayMask() { 1418 const documentURI = this.contentData?.documentURIObject; 1419 const aOrigin = lazy.LoginHelper.getLoginOrigin(documentURI?.spec); 1420 this.actor.useRelayMask(this.targetIdentifier, aOrigin); 1421 } 1422 1423 useGeneratedPassword() { 1424 lazy.LoginManagerContextMenu.useGeneratedPassword(this.targetIdentifier); 1425 } 1426 1427 isLoginForm() { 1428 let loginFillInfo = this.contentData?.loginFillInfo; 1429 let documentURI = this.contentData?.documentURIObject; 1430 1431 // If we could not find a password field or this is not a username-only 1432 // form, then don't treat this as a login form. 1433 return ( 1434 (loginFillInfo?.passwordField?.found || 1435 loginFillInfo?.activeField.fieldNameHint == USERNAME_FIELDNAME_HINT) && 1436 !documentURI?.schemeIs("about") && 1437 this.browser.contentPrincipal.spec != "resource://pdf.js/web/viewer.html" 1438 ); 1439 } 1440 1441 inspectNode() { 1442 return lazy.DevToolsShim.inspectNode( 1443 this.window.gBrowser.selectedTab, 1444 this.targetIdentifier 1445 ); 1446 } 1447 1448 inspectA11Y() { 1449 return lazy.DevToolsShim.inspectA11Y( 1450 this.window.gBrowser.selectedTab, 1451 this.targetIdentifier 1452 ); 1453 } 1454 1455 _openLinkInParameters(extra) { 1456 let params = { 1457 charset: this.contentData.charSet, 1458 originPrincipal: this.principal, 1459 originStoragePrincipal: this.storagePrincipal, 1460 triggeringPrincipal: this.principal, 1461 triggeringRemoteType: this.remoteType, 1462 policyContainer: this.policyContainer, 1463 frameID: this.contentData.frameID, 1464 hasValidUserGestureActivation: true, 1465 textDirectiveUserActivation: true, 1466 }; 1467 for (let p in extra) { 1468 params[p] = extra[p]; 1469 } 1470 1471 let referrerInfo = this.onLink 1472 ? this.contentData.linkReferrerInfo 1473 : this.contentData.referrerInfo; 1474 // If we want to change userContextId, we must be sure that we don't 1475 // propagate the referrer. 1476 if ( 1477 ("userContextId" in params && 1478 params.userContextId != this.contentData.userContextId) || 1479 this.onPlainTextLink 1480 ) { 1481 referrerInfo = new lazy.ReferrerInfo( 1482 referrerInfo.referrerPolicy, 1483 false, 1484 referrerInfo.originalReferrer 1485 ); 1486 } 1487 1488 params.referrerInfo = referrerInfo; 1489 return params; 1490 } 1491 1492 _getGlobalHistoryOptions() { 1493 if (this.isSponsoredLink) { 1494 return { 1495 globalHistoryOptions: { 1496 triggeringSponsoredURL: this.linkURL, 1497 triggeringSource: "newtab", 1498 }, 1499 }; 1500 } else if (this.browser.hasAttribute("triggeringSponsoredURL")) { 1501 return { 1502 globalHistoryOptions: { 1503 triggeringSponsoredURL: this.browser.getAttribute( 1504 "triggeringSponsoredURL" 1505 ), 1506 triggeringSponsoredURLVisitTimeMS: this.browser.getAttribute( 1507 "triggeringSponsoredURLVisitTimeMS" 1508 ), 1509 triggeringSource: this.browser.getAttribute("triggeringSource"), 1510 }, 1511 }; 1512 } 1513 return {}; 1514 } 1515 1516 // Open linked-to URL in a new window. 1517 openLink() { 1518 const params = this._getGlobalHistoryOptions(); 1519 1520 this.window.openLinkIn( 1521 this.linkURL, 1522 "window", 1523 this._openLinkInParameters(params) 1524 ); 1525 } 1526 1527 // Open linked-to URL in a new private window. 1528 openLinkInPrivateWindow() { 1529 this.window.openLinkIn( 1530 this.linkURL, 1531 "window", 1532 this._openLinkInParameters({ private: true }) 1533 ); 1534 } 1535 1536 // Open linked-to URL in a new tab. 1537 openLinkInTab(event) { 1538 let params = { 1539 userContextId: parseInt(event.target.getAttribute("data-usercontextid")), 1540 ...this._getGlobalHistoryOptions(), 1541 }; 1542 1543 this.window.openLinkIn( 1544 this.linkURL, 1545 "tab", 1546 this._openLinkInParameters(params) 1547 ); 1548 } 1549 1550 // open URL in current tab 1551 openLinkInCurrent() { 1552 this.window.openLinkIn( 1553 this.linkURL, 1554 "current", 1555 this._openLinkInParameters() 1556 ); 1557 } 1558 1559 // Open frame in a new tab. 1560 openFrameInTab() { 1561 this.window.openLinkIn(this.contentData.docLocation, "tab", { 1562 charset: this.contentData.charSet, 1563 triggeringPrincipal: this.browser.contentPrincipal, 1564 policyContainer: this.browser.policyContainer, 1565 referrerInfo: this.contentData.frameReferrerInfo, 1566 }); 1567 } 1568 1569 // Reload clicked-in frame. 1570 reloadFrame(aEvent) { 1571 let forceReload = aEvent.shiftKey; 1572 this.actor.reloadFrame(this.targetIdentifier, forceReload); 1573 } 1574 1575 // Open clicked-in frame in its own window. 1576 openFrame() { 1577 this.window.openLinkIn(this.contentData.docLocation, "window", { 1578 charset: this.contentData.charSet, 1579 triggeringPrincipal: this.browser.contentPrincipal, 1580 policyContainer: this.browser.policyContainer, 1581 referrerInfo: this.contentData.frameReferrerInfo, 1582 }); 1583 } 1584 1585 // Open clicked-in frame in the same window. 1586 showOnlyThisFrame() { 1587 this.window.urlSecurityCheck( 1588 this.contentData.docLocation, 1589 this.browser.contentPrincipal, 1590 Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT 1591 ); 1592 this.window.openWebLinkIn(this.contentData.docLocation, "current", { 1593 referrerInfo: this.contentData.frameReferrerInfo, 1594 triggeringPrincipal: this.browser.contentPrincipal, 1595 }); 1596 } 1597 1598 takeScreenshot() { 1599 Services.obs.notifyObservers( 1600 this.window, 1601 "menuitem-screenshot", 1602 "ContextMenu" 1603 ); 1604 } 1605 1606 pdfJSCmd(aName) { 1607 if (["cut", "copy", "paste"].includes(aName)) { 1608 const cmd = `cmd_${aName}`; 1609 this.document.commandDispatcher 1610 .getControllerForCommand(cmd) 1611 .doCommand(cmd); 1612 if (Cu.isInAutomation) { 1613 this.browser.sendMessageToActor( 1614 "PDFJS:Editing", 1615 { name: aName }, 1616 "Pdfjs" 1617 ); 1618 } 1619 return; 1620 } 1621 this.browser.sendMessageToActor("PDFJS:Editing", { name: aName }, "Pdfjs"); 1622 } 1623 1624 // View Partial Source 1625 viewPartialSource() { 1626 let { browser } = this; 1627 let openSelectionFn = async () => { 1628 let tabBrowser = this.window.gBrowser; 1629 let relatedToCurrent = tabBrowser?.selectedBrowser === browser; 1630 const inNewWindow = !Services.prefs.getBoolPref("view_source.tab"); 1631 // In the case of popups, we need to find a non-popup browser window. 1632 // We might also not have a tabBrowser reference (if this isn't in a 1633 // a tabbrowser scope) or might have a fake/stub tabbrowser reference 1634 // (in the sidebar). Deal with those cases: 1635 if (!tabBrowser || !tabBrowser.addTab || !this.window.toolbar.visible) { 1636 // This returns only non-popup browser windows by default. 1637 let browserWindow = 1638 lazy.BrowserWindowTracker.getTopWindow() ?? 1639 (await lazy.BrowserWindowTracker.promiseOpenWindow()); 1640 tabBrowser = browserWindow.gBrowser; 1641 } 1642 1643 let tab = tabBrowser.addTab("about:blank", { 1644 relatedToCurrent, 1645 inBackground: inNewWindow, 1646 skipAnimation: inNewWindow, 1647 triggeringPrincipal: 1648 Services.scriptSecurityManager.getSystemPrincipal(), 1649 }); 1650 const viewSourceBrowser = tabBrowser.getBrowserForTab(tab); 1651 if (inNewWindow) { 1652 tabBrowser.hideTab(tab); 1653 tabBrowser.replaceTabsWithWindow(tab); 1654 } 1655 return viewSourceBrowser; 1656 }; 1657 1658 this.window.gViewSourceUtils.viewPartialSourceInBrowser( 1659 this.actor.browsingContext, 1660 openSelectionFn 1661 ); 1662 } 1663 1664 // Open new "view source" window with the frame's URL. 1665 viewFrameSource() { 1666 this.window.BrowserCommands.viewSourceOfDocument({ 1667 browser: this.browser, 1668 URL: this.contentData.docLocation, 1669 outerWindowID: this.frameOuterWindowID, 1670 }); 1671 } 1672 1673 viewInfo() { 1674 this.window.BrowserCommands.pageInfo( 1675 this.contentData.docLocation, 1676 null, 1677 null, 1678 null, 1679 this.browser 1680 ); 1681 } 1682 1683 viewImageInfo() { 1684 this.window.BrowserCommands.pageInfo( 1685 this.contentData.docLocation, 1686 "mediaTab", 1687 this.imageInfo, 1688 null, 1689 this.browser 1690 ); 1691 } 1692 1693 viewImageDesc(e) { 1694 this.window.urlSecurityCheck( 1695 this.imageDescURL, 1696 this.principal, 1697 Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT 1698 ); 1699 this.window.openUILink(this.imageDescURL, e, { 1700 referrerInfo: this.contentData.referrerInfo, 1701 triggeringPrincipal: this.principal, 1702 triggeringRemoteType: this.remoteType, 1703 policyContainer: this.policyContainer, 1704 }); 1705 } 1706 1707 viewFrameInfo() { 1708 this.window.BrowserCommands.pageInfo( 1709 this.contentData.docLocation, 1710 null, 1711 null, 1712 this.actor.browsingContext, 1713 this.browser 1714 ); 1715 } 1716 1717 reloadImage() { 1718 this.window.urlSecurityCheck( 1719 this.mediaURL, 1720 this.principal, 1721 Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT 1722 ); 1723 this.actor.reloadImage(this.targetIdentifier); 1724 } 1725 1726 _canvasToBlobURL(targetIdentifier) { 1727 return this.actor.canvasToBlobURL(targetIdentifier); 1728 } 1729 1730 // Change current window to the URL of the image, video, or audio. 1731 viewMedia(e) { 1732 let where = lazy.BrowserUtils.whereToOpenLink(e, false, false); 1733 if (where == "current") { 1734 where = "tab"; 1735 } 1736 let referrerInfo = this.contentData.referrerInfo; 1737 let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal(); 1738 if (this.onCanvas) { 1739 this._canvasToBlobURL(this.targetIdentifier).then(blobURL => { 1740 this.window.openLinkIn(blobURL, where, { 1741 referrerInfo, 1742 triggeringPrincipal: systemPrincipal, 1743 }); 1744 }, console.error); 1745 } else { 1746 this.window.urlSecurityCheck( 1747 this.mediaURL, 1748 this.principal, 1749 Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT 1750 ); 1751 1752 // Default to opening in a new tab. 1753 this.window.openLinkIn(this.mediaURL, where, { 1754 referrerInfo, 1755 forceAllowDataURI: true, 1756 triggeringPrincipal: this.principal, 1757 triggeringRemoteType: this.remoteType, 1758 policyContainer: this.policyContainer, 1759 }); 1760 } 1761 } 1762 1763 saveVideoFrameAsImage() { 1764 let isPrivate = lazy.PrivateBrowsingUtils.isBrowserPrivate(this.browser); 1765 1766 let aName = ""; 1767 if (this.mediaURL) { 1768 try { 1769 let uri = this.window.makeURI(this.mediaURL); 1770 let url = uri.QueryInterface(Ci.nsIURL); 1771 if (url.fileBaseName) { 1772 aName = decodeURI(url.fileBaseName) + ".jpg"; 1773 } 1774 } catch (e) {} 1775 } 1776 if (!aName) { 1777 aName = "snapshot.jpg"; 1778 } 1779 1780 // Cache this because we fetch the data async 1781 let referrerInfo = this.contentData.referrerInfo; 1782 let cookieJarSettings = this.contentData.cookieJarSettings; 1783 1784 this.actor.saveVideoFrameAsImage(this.targetIdentifier).then(dataURL => { 1785 // FIXME can we switch this to a blob URL? 1786 this.window.internalSave( 1787 dataURL, 1788 null, // originalURL 1789 null, // document 1790 aName, 1791 null, // content disposition 1792 "image/jpeg", // content type - keep in sync with ContextMenuChild! 1793 true, // bypass cache 1794 "SaveImageTitle", 1795 null, // chosen data 1796 referrerInfo, 1797 cookieJarSettings, 1798 null, // initiating doc 1799 false, // don't skip prompt for where to save 1800 null, // cache key 1801 isPrivate, 1802 this.principal 1803 ); 1804 }); 1805 } 1806 1807 leaveDOMFullScreen() { 1808 this.document.exitFullscreen(); 1809 } 1810 1811 // Change current window to the URL of the background image. 1812 viewBGImage(e) { 1813 this.window.urlSecurityCheck( 1814 this.bgImageURL, 1815 this.principal, 1816 Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT 1817 ); 1818 1819 this.window.openUILink(this.bgImageURL, e, { 1820 referrerInfo: this.contentData.referrerInfo, 1821 forceAllowDataURI: true, 1822 triggeringPrincipal: this.principal, 1823 triggeringRemoteType: this.remoteType, 1824 policyContainer: this.policyContainer, 1825 }); 1826 } 1827 1828 setDesktopBackground() { 1829 if (!Services.policies.isAllowed("setDesktopBackground")) { 1830 return; 1831 } 1832 1833 this.actor 1834 .setAsDesktopBackground(this.targetIdentifier) 1835 .then(({ failed, dataURL, imageName }) => { 1836 if (failed) { 1837 return; 1838 } 1839 1840 let image = this.document.createElementNS( 1841 "http://www.w3.org/1999/xhtml", 1842 "img" 1843 ); 1844 image.src = dataURL; 1845 1846 // Confirm since it's annoying if you hit this accidentally. 1847 const kDesktopBackgroundURL = 1848 "chrome://browser/content/setDesktopBackground.xhtml"; 1849 1850 if (AppConstants.platform == "macosx") { 1851 // On Mac, the Set Desktop Background window is not modal. 1852 // Don't open more than one Set Desktop Background window. 1853 let dbWin = Services.wm.getMostRecentWindow( 1854 "Shell:SetDesktopBackground" 1855 ); 1856 if (dbWin) { 1857 dbWin.gSetBackground.init(image, imageName); 1858 dbWin.focus(); 1859 } else { 1860 this.window.openDialog( 1861 kDesktopBackgroundURL, 1862 "", 1863 "centerscreen,chrome,dialog=no,dependent,resizable=no", 1864 image, 1865 imageName 1866 ); 1867 } 1868 } else { 1869 // On non-Mac platforms, the Set Wallpaper dialog is modal. 1870 this.window.openDialog( 1871 kDesktopBackgroundURL, 1872 "", 1873 "centerscreen,chrome,dialog,modal,dependent", 1874 image, 1875 imageName 1876 ); 1877 } 1878 }); 1879 } 1880 1881 // Save URL of clicked-on frame. 1882 saveFrame() { 1883 this.window.saveBrowser(this.browser, false, this.frameBrowsingContext); 1884 } 1885 1886 // Helper function to wait for appropriate MIME-type headers and 1887 // then prompt the user with a file picker 1888 saveHelper( 1889 linkURL, 1890 linkText, 1891 dialogTitle, 1892 bypassCache, 1893 doc, 1894 referrerInfo, 1895 cookieJarSettings, 1896 windowID, 1897 linkDownload, 1898 isContentWindowPrivate 1899 ) { 1900 // canonical def in nsURILoader.h 1901 const NS_ERROR_SAVE_LINK_AS_TIMEOUT = 0x805d0020; 1902 1903 // an object to proxy the data through to 1904 // nsIExternalHelperAppService.doContent, which will wait for the 1905 // appropriate MIME-type headers and then prompt the user with a 1906 // file picker 1907 function saveAsListener(principal, aWindow) { 1908 this._triggeringPrincipal = principal; 1909 this._window = aWindow; 1910 } 1911 saveAsListener.prototype = { 1912 extListener: null, 1913 1914 onStartRequest: function saveLinkAs_onStartRequest(aRequest) { 1915 // if the timer fired, the error status will have been caused by that, 1916 // and we'll be restarting in onStopRequest, so no reason to notify 1917 // the user 1918 if (aRequest.status == NS_ERROR_SAVE_LINK_AS_TIMEOUT) { 1919 return; 1920 } 1921 1922 timer.cancel(); 1923 1924 // some other error occured; notify the user... 1925 if (!Components.isSuccessCode(aRequest.status)) { 1926 try { 1927 const l10n = new Localization(["browser/downloads.ftl"], true); 1928 1929 let msg = null; 1930 try { 1931 const channel = aRequest.QueryInterface(Ci.nsIChannel); 1932 const reason = channel.loadInfo.requestBlockingReason; 1933 if ( 1934 reason == Ci.nsILoadInfo.BLOCKING_REASON_EXTENSION_WEBREQUEST 1935 ) { 1936 try { 1937 const properties = channel.QueryInterface(Ci.nsIPropertyBag); 1938 const id = properties.getProperty("cancelledByExtension"); 1939 msg = l10n.formatValueSync("downloads-error-blocked-by", { 1940 extension: WebExtensionPolicy.getByID(id).name, 1941 }); 1942 } catch (err) { 1943 // "cancelledByExtension" doesn't have to be available. 1944 msg = l10n.formatValueSync("downloads-error-extension"); 1945 } 1946 } 1947 } catch (ex) {} 1948 msg ??= l10n.formatValueSync("downloads-error-generic"); 1949 1950 const win = Services.wm.getOuterWindowWithId(windowID); 1951 const title = l10n.formatValueSync("downloads-error-alert-title"); 1952 Services.prompt.alert(win, title, msg); 1953 } catch (ex) {} 1954 return; 1955 } 1956 1957 let extHelperAppSvc = Cc[ 1958 "@mozilla.org/uriloader/external-helper-app-service;1" 1959 ].getService(Ci.nsIExternalHelperAppService); 1960 let channel = aRequest.QueryInterface(Ci.nsIChannel); 1961 this.extListener = extHelperAppSvc.doContent( 1962 channel.contentType, 1963 aRequest, 1964 null, 1965 true, 1966 this._window 1967 ); 1968 this.extListener.onStartRequest(aRequest); 1969 }, 1970 1971 onStopRequest: function saveLinkAs_onStopRequest(aRequest, aStatusCode) { 1972 if (aStatusCode == NS_ERROR_SAVE_LINK_AS_TIMEOUT) { 1973 // do it the old fashioned way, which will pick the best filename 1974 // it can without waiting. 1975 this._window.saveURL( 1976 linkURL, 1977 null, 1978 linkText, 1979 dialogTitle, 1980 bypassCache, 1981 false, 1982 referrerInfo, 1983 cookieJarSettings, 1984 doc, 1985 isContentWindowPrivate, 1986 this._triggeringPrincipal 1987 ); 1988 } 1989 if (this.extListener) { 1990 this.extListener.onStopRequest(aRequest, aStatusCode); 1991 } 1992 }, 1993 1994 onDataAvailable: function saveLinkAs_onDataAvailable( 1995 aRequest, 1996 aInputStream, 1997 aOffset, 1998 aCount 1999 ) { 2000 this.extListener.onDataAvailable( 2001 aRequest, 2002 aInputStream, 2003 aOffset, 2004 aCount 2005 ); 2006 }, 2007 }; 2008 2009 function callbacks() {} 2010 callbacks.prototype = { 2011 getInterface: function sLA_callbacks_getInterface(aIID) { 2012 if (aIID.equals(Ci.nsIAuthPrompt) || aIID.equals(Ci.nsIAuthPrompt2)) { 2013 // If the channel demands authentication prompt, we must cancel it 2014 // because the save-as-timer would expire and cancel the channel 2015 // before we get credentials from user. Both authentication dialog 2016 // and save as dialog would appear on the screen as we fall back to 2017 // the old fashioned way after the timeout. 2018 timer.cancel(); 2019 channel.cancel(NS_ERROR_SAVE_LINK_AS_TIMEOUT); 2020 } 2021 throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); 2022 }, 2023 }; 2024 2025 // if it we don't have the headers after a short time, the user 2026 // won't have received any feedback from their click. that's bad. so 2027 // we give up waiting for the filename. 2028 function timerCallback() {} 2029 timerCallback.prototype = { 2030 notify: function sLA_timer_notify() { 2031 channel.cancel(NS_ERROR_SAVE_LINK_AS_TIMEOUT); 2032 }, 2033 }; 2034 2035 // setting up a new channel for 'right click - save link as ...' 2036 var channel = lazy.NetUtil.newChannel({ 2037 uri: this.window.makeURI(linkURL), 2038 loadingPrincipal: this.principal, 2039 contentPolicyType: Ci.nsIContentPolicy.TYPE_SAVEAS_DOWNLOAD, 2040 securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT, 2041 }); 2042 2043 if (linkDownload) { 2044 channel.contentDispositionFilename = linkDownload; 2045 } 2046 if (channel instanceof Ci.nsIPrivateBrowsingChannel) { 2047 let docIsPrivate = lazy.PrivateBrowsingUtils.isBrowserPrivate( 2048 this.browser 2049 ); 2050 channel.setPrivate(docIsPrivate); 2051 } 2052 channel.notificationCallbacks = new callbacks(); 2053 2054 let flags = Ci.nsIChannel.LOAD_CALL_CONTENT_SNIFFERS; 2055 2056 if (bypassCache) { 2057 flags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; 2058 } 2059 2060 if (channel instanceof Ci.nsICachingChannel) { 2061 flags |= Ci.nsICachingChannel.LOAD_BYPASS_LOCAL_CACHE_IF_BUSY; 2062 } 2063 2064 channel.loadFlags |= flags; 2065 2066 if (channel instanceof Ci.nsIHttpChannel) { 2067 channel.referrerInfo = referrerInfo; 2068 if (channel instanceof Ci.nsIHttpChannelInternal) { 2069 channel.forceAllowThirdPartyCookie = true; 2070 } 2071 2072 channel.loadInfo.cookieJarSettings = cookieJarSettings; 2073 } 2074 2075 // fallback to the old way if we don't see the headers quickly 2076 var timeToWait = Services.prefs.getIntPref( 2077 "browser.download.saveLinkAsFilenameTimeout" 2078 ); 2079 var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); 2080 timer.initWithCallback( 2081 new timerCallback(), 2082 timeToWait, 2083 timer.TYPE_ONE_SHOT 2084 ); 2085 2086 // kick off the channel with our proxy object as the listener 2087 channel.asyncOpen(new saveAsListener(this.principal, this.window)); 2088 } 2089 2090 // Save URL of clicked-on link. 2091 saveLink() { 2092 let referrerInfo = this.onLink 2093 ? this.contentData.linkReferrerInfo 2094 : this.contentData.referrerInfo; 2095 2096 let isPrivate = lazy.PrivateBrowsingUtils.isBrowserPrivate(this.browser); 2097 this.saveHelper( 2098 this.linkURL, 2099 this.linkTextStr, 2100 null, 2101 true, 2102 this.ownerDoc, 2103 referrerInfo, 2104 this.contentData.cookieJarSettings, 2105 this.frameOuterWindowID, 2106 this.linkDownload, 2107 isPrivate 2108 ); 2109 } 2110 2111 // Backwards-compatibility wrapper 2112 saveImage() { 2113 if (this.onCanvas || this.onImage) { 2114 this.saveMedia(); 2115 } 2116 } 2117 2118 // Save URL of the clicked upon image, video, or audio. 2119 saveMedia() { 2120 let doc = this.ownerDoc; 2121 let isPrivate = lazy.PrivateBrowsingUtils.isBrowserPrivate(this.browser); 2122 let referrerInfo = this.contentData.referrerInfo; 2123 let cookieJarSettings = this.contentData.cookieJarSettings; 2124 if (this.onCanvas) { 2125 // Bypass cache, since it's a data: URL. 2126 this._canvasToBlobURL(this.targetIdentifier).then(blobURL => { 2127 this.window.internalSave( 2128 blobURL, 2129 null, // originalURL 2130 null, // document 2131 "canvas.png", 2132 null, // content disposition 2133 "image/png", // _canvasToBlobURL uses image/png by default. 2134 true, // bypass cache 2135 "SaveImageTitle", 2136 null, // chosen data 2137 referrerInfo, 2138 cookieJarSettings, 2139 null, // initiating doc 2140 false, // don't skip prompt for where to save 2141 null, // cache key 2142 isPrivate, 2143 this.document.nodePrincipal /* system, because blob: */ 2144 ); 2145 }, console.error); 2146 } else if (this.onImage) { 2147 this.window.urlSecurityCheck(this.mediaURL, this.principal); 2148 this.window.internalSave( 2149 this.mediaURL, 2150 null, // originalURL 2151 null, // document 2152 null, // file name; we'll take it from the URL 2153 this.contentData.contentDisposition, 2154 this.contentData.contentType, 2155 false, // do not bypass the cache 2156 "SaveImageTitle", 2157 null, // chosen data 2158 referrerInfo, 2159 cookieJarSettings, 2160 null, // initiating doc 2161 false, // don't skip prompt for where to save 2162 null, // cache key 2163 isPrivate, 2164 this.principal 2165 ); 2166 } else if (this.onVideo || this.onAudio) { 2167 let defaultFileName = ""; 2168 if (this.mediaURL.startsWith("data")) { 2169 // Use default file name "Untitled" for data URIs 2170 defaultFileName = 2171 this.window.ContentAreaUtils.stringBundle.GetStringFromName( 2172 "UntitledSaveFileName" 2173 ); 2174 } 2175 2176 var dialogTitle = this.onVideo ? "SaveVideoTitle" : "SaveAudioTitle"; 2177 this.saveHelper( 2178 this.mediaURL, 2179 null, 2180 dialogTitle, 2181 false, 2182 doc, 2183 referrerInfo, 2184 cookieJarSettings, 2185 this.frameOuterWindowID, 2186 defaultFileName, 2187 isPrivate 2188 ); 2189 } 2190 } 2191 2192 // Backwards-compatibility wrapper 2193 sendImage() { 2194 if (this.onCanvas || this.onImage) { 2195 this.sendMedia(); 2196 } 2197 } 2198 2199 sendMedia() { 2200 this.window.MailIntegration.sendMessage(this.mediaURL, ""); 2201 } 2202 2203 // Generate email address and put it on clipboard. 2204 copyEmail() { 2205 // Copy the comma-separated list of email addresses only. 2206 // There are other ways of embedding email addresses in a mailto: 2207 // link, but such complex parsing is beyond us. 2208 var url = this.linkURL; 2209 var qmark = url.indexOf("?"); 2210 var addresses; 2211 2212 // 7 == length of "mailto:" 2213 addresses = qmark > 7 ? url.substring(7, qmark) : url.substr(7); 2214 2215 // Let's try to unescape it using a character set 2216 // in case the address is not ASCII. 2217 try { 2218 addresses = Services.textToSubURI.unEscapeURIForUI(addresses); 2219 } catch (ex) { 2220 // Do nothing. 2221 } 2222 2223 lazy.clipboard.copyString( 2224 addresses, 2225 this.actor.manager.browsingContext.currentWindowGlobal 2226 ); 2227 } 2228 2229 // Extract phone and put it on clipboard 2230 copyPhone() { 2231 // Copies the phone number only. We won't be doing any complex parsing 2232 var url = this.linkURL; 2233 var phone = url.substr(4); 2234 2235 // Let's try to unescape it using a character set 2236 // in case the phone number is not ASCII. 2237 try { 2238 phone = Services.textToSubURI.unEscapeURIForUI(phone); 2239 } catch (ex) { 2240 // Do nothing. 2241 } 2242 2243 lazy.clipboard.copyString( 2244 phone, 2245 this.actor.manager.browsingContext.currentWindowGlobal 2246 ); 2247 } 2248 2249 copyLink(url = this.linkURL) { 2250 // If we're in a view source tab, remove the view-source: prefix 2251 let linkURL = url.replace(/^view-source:/, ""); 2252 lazy.clipboard.copyString( 2253 linkURL, 2254 this.actor.manager.browsingContext.currentWindowGlobal 2255 ); 2256 } 2257 2258 previewLink(_url = this.linkURL) { 2259 // LinkPreview.sys.mjs is missing. Unexpected to reach here since 2260 // #context-previewlink is hidden. tor-browser#44045. 2261 } 2262 2263 /** 2264 * Copies a stripped version of a URI to the clipboard. 2265 * 'Stripped' means that query parameters for tracking/ link decoration 2266 * that are known to us will be removed from the URI. 2267 */ 2268 copyStrippedLink(uri = this.linkURI) { 2269 let strippedLinkURI = this.getStrippedLink(uri); 2270 let strippedLinkURL = 2271 Services.io.createExposableURI(strippedLinkURI)?.displaySpec; 2272 if (strippedLinkURL) { 2273 lazy.clipboard.copyString( 2274 strippedLinkURL, 2275 this.actor.manager.browsingContext.currentWindowGlobal 2276 ); 2277 } 2278 } 2279 2280 async addSearchFieldAsEngine() { 2281 let { url, formData, charset, method } = 2282 await this.actor.getSearchFieldEngineData(this.targetIdentifier); 2283 2284 for (let value of formData.values()) { 2285 if (typeof value != "string") { 2286 throw new Error("Non-string values are not supported."); 2287 } 2288 } 2289 2290 let { engineInfo } = await this.window.gDialogBox.open( 2291 "chrome://browser/content/search/addEngine.xhtml", 2292 { 2293 mode: "FORM", 2294 title: true, 2295 nameTemplate: Services.io.newURI(url).host, 2296 } 2297 ); 2298 2299 // If the user saved, engineInfo contains `name` and `alias`. 2300 // Otherwise, it's undefined. 2301 if (engineInfo) { 2302 let searchEngine = await Services.search.addUserEngine({ 2303 name: engineInfo.name, 2304 alias: engineInfo.alias, 2305 url, 2306 params: new URLSearchParams(formData), 2307 charset, 2308 method, 2309 }); 2310 2311 this.window.gURLBar.search("", { searchEngine }); 2312 } 2313 } 2314 2315 /** 2316 * Utilities 2317 */ 2318 2319 /** 2320 * Show/hide one item (specified via name or the item element itself). 2321 * If the element is not found, then this function finishes silently. 2322 * 2323 * @param {Element | string} aItemOrId The item element or the name of the element 2324 * to show. 2325 * @param {boolean} aShow Set to true to show the item, false to hide it. 2326 */ 2327 showItem(aItemOrId, aShow) { 2328 var item = 2329 aItemOrId.constructor == String 2330 ? this.document.getElementById(aItemOrId) 2331 : aItemOrId; 2332 if (item) { 2333 item.hidden = !aShow; 2334 } 2335 } 2336 2337 // Set given attribute of specified context-menu item. If the 2338 // value is null, then it removes the attribute (which works 2339 // nicely for the disabled attribute). 2340 setItemAttr(aID, aAttr, aVal) { 2341 var elem = this.document.getElementById(aID); 2342 if (!elem) { 2343 return; 2344 } 2345 if (aVal == null) { 2346 // null indicates attr should be removed. 2347 elem.removeAttribute(aAttr); 2348 return; 2349 } 2350 if (typeof aVal == "boolean") { 2351 // TODO(emilio): Replace this with toggleAttribute, but needs test fixes. 2352 if (aVal) { 2353 elem.setAttribute(aAttr, aVal); 2354 } else { 2355 elem.removeAttribute(aAttr); 2356 } 2357 return; 2358 } 2359 // Set attr=val. 2360 elem.setAttribute(aAttr, aVal); 2361 } 2362 2363 // Temporary workaround for DOM api not yet implemented by XUL nodes. 2364 cloneNode(aItem) { 2365 // Create another element like the one we're cloning. 2366 var node = this.document.createElement(aItem.tagName); 2367 2368 // Copy attributes from argument item to the new one. 2369 var attrs = aItem.attributes; 2370 for (var i = 0; i < attrs.length; i++) { 2371 var attr = attrs.item(i); 2372 node.setAttribute(attr.nodeName, attr.nodeValue); 2373 } 2374 2375 // Voila! 2376 return node; 2377 } 2378 2379 getLinkURI(url = this.linkURL) { 2380 try { 2381 return this.window.makeURI(url); 2382 } catch (ex) { 2383 // e.g. empty URL string 2384 } 2385 2386 return null; 2387 } 2388 2389 /** 2390 * Strips any known query params from the link URI. 2391 * 2392 * @returns {nsIURI|null} - the stripped version of the URI, 2393 * or the original URI if we could not strip any query parameter. 2394 */ 2395 getStrippedLink(uri = this.linkURI) { 2396 if (!uri) { 2397 return null; 2398 } 2399 let strippedLinkURI = null; 2400 try { 2401 strippedLinkURI = lazy.QueryStringStripper.stripForCopyOrShare(uri); 2402 } catch (e) { 2403 console.warn(`getStrippedLink: ${e.message}`); 2404 return uri; 2405 } 2406 2407 // If nothing can be stripped, we return the original URI 2408 // so the feature can still be used. 2409 return strippedLinkURI ?? uri; 2410 } 2411 2412 /** 2413 * Checks if there is a query parameter that can be stripped 2414 * 2415 * @returns {boolean} 2416 */ 2417 #canStripParams(uri = this.linkURI) { 2418 if (!uri) { 2419 return false; 2420 } 2421 try { 2422 return lazy.QueryStringStripper.canStripForShare(uri); 2423 } catch (e) { 2424 console.warn("canStripForShare failed!", e); 2425 return false; 2426 } 2427 } 2428 2429 /** 2430 * Checks if a webpage is a secure interal webpage 2431 * 2432 * @returns {boolean} 2433 */ 2434 isSecureAboutPage() { 2435 let { currentURI } = this.browser; 2436 if (currentURI?.schemeIs("about")) { 2437 let module = lazy.E10SUtils.getAboutModule(currentURI); 2438 if (module) { 2439 let flags = module.getURIFlags(currentURI); 2440 return !!(flags & Ci.nsIAboutModule.IS_SECURE_CHROME_UI); 2441 } 2442 } 2443 return false; 2444 } 2445 2446 // Kept for addon compat 2447 linkText() { 2448 return this.linkTextStr; 2449 } 2450 2451 // Determines whether or not the separator with the specified ID should be 2452 // shown or not by determining if there are any non-hidden items between it 2453 // and the previous separator. 2454 shouldShowSeparator(aSeparatorID) { 2455 var separator = this.document.getElementById(aSeparatorID); 2456 if (separator) { 2457 var sibling = separator.previousSibling; 2458 while (sibling && sibling.localName != "menuseparator") { 2459 if (!sibling.hidden) { 2460 return true; 2461 } 2462 sibling = sibling.previousSibling; 2463 } 2464 } 2465 return false; 2466 } 2467 2468 shouldShowAddEngine() { 2469 let uri = this.browser.currentURI; 2470 2471 return ( 2472 this.onTextInput && 2473 this.onSearchField && 2474 !this.isLoginForm() && 2475 (uri.schemeIs("http") || uri.schemeIs("https")) 2476 ); 2477 } 2478 2479 addDictionaries() { 2480 var uri = Services.urlFormatter.formatURLPref( 2481 "browser.dictionaries.download.url" 2482 ); 2483 2484 var locale = "-"; 2485 try { 2486 locale = Services.locale.acceptLanguages; 2487 } catch (e) {} 2488 2489 var version = "-"; 2490 try { 2491 version = Services.appinfo.version; 2492 } catch (e) {} 2493 2494 uri = uri.replace(/%LOCALE%/, escape(locale)).replace(/%VERSION%/, version); 2495 2496 var newWindowPref = Services.prefs.getIntPref( 2497 "browser.link.open_newwindow" 2498 ); 2499 var where = newWindowPref == 3 ? "tab" : "window"; 2500 2501 this.window.openTrustedLinkIn(uri, where); 2502 } 2503 2504 bookmarkThisPage() { 2505 this.window.top.PlacesCommandHook.bookmarkPage().catch(console.error); 2506 } 2507 2508 bookmarkLink() { 2509 this.window.top.PlacesCommandHook.bookmarkLink( 2510 this.linkURL, 2511 this.linkTextStr 2512 ).catch(console.error); 2513 } 2514 2515 addBookmarkForFrame() { 2516 let uri = this.contentData.documentURIObject; 2517 2518 this.actor.getFrameTitle(this.targetIdentifier).then(title => { 2519 this.window.top.PlacesCommandHook.bookmarkLink(uri.spec, title).catch( 2520 console.error 2521 ); 2522 }); 2523 } 2524 2525 savePageAs() { 2526 this.window.saveBrowser(this.browser); 2527 } 2528 2529 printFrame() { 2530 this.window.PrintUtils.startPrintWindow(this.actor.browsingContext, { 2531 printFrameOnly: true, 2532 }); 2533 } 2534 2535 printSelection() { 2536 this.window.PrintUtils.startPrintWindow(this.actor.browsingContext, { 2537 printSelectionOnly: true, 2538 }); 2539 } 2540 2541 switchPageDirection() { 2542 this.window.gBrowser.selectedBrowser.sendMessageToActor( 2543 "SwitchDocumentDirection", 2544 {}, 2545 "SwitchDocumentDirection", 2546 "roots" 2547 ); 2548 } 2549 2550 mediaCommand(command, data) { 2551 this.actor.mediaCommand(this.targetIdentifier, command, data); 2552 } 2553 2554 copyMediaLocation() { 2555 lazy.clipboard.copyString( 2556 this.originalMediaURL, 2557 this.actor.manager.browsingContext.currentWindowGlobal 2558 ); 2559 } 2560 2561 getImageText() { 2562 let dialogBox = this.window.gBrowser.getTabDialogBox(this.browser); 2563 const imageTextResult = this.actor.getImageText(this.targetIdentifier); 2564 let timerId = Glean.textRecognition.apiPerformance.start(); 2565 const { dialog } = dialogBox.open( 2566 "chrome://browser/content/textrecognition/textrecognition.html", 2567 { 2568 features: "resizable=no", 2569 modalType: Services.prompt.MODAL_TYPE_CONTENT, 2570 }, 2571 imageTextResult, 2572 () => dialog.resizeVertically(), 2573 this.window.openLinkIn, 2574 timerId 2575 ); 2576 } 2577 2578 drmLearnMore(aEvent) { 2579 let drmInfoURL = 2580 Services.urlFormatter.formatURLPref("app.support.baseURL") + 2581 "drm-content"; 2582 let dest = lazy.BrowserUtils.whereToOpenLink(aEvent); 2583 // Don't ever want this to open in the same tab as it'll unload the 2584 // DRM'd video, which is going to be a bad idea in most cases. 2585 if (dest == "current") { 2586 dest = "tab"; 2587 } 2588 this.window.openTrustedLinkIn(drmInfoURL, dest); 2589 } 2590 2591 /** 2592 * Opens the SelectTranslationsPanel singleton instance. 2593 * 2594 * @param {Event} event - The triggering event for opening the panel. 2595 */ 2596 openSelectTranslationsPanel(event) { 2597 const context = this.contentData.context; 2598 let screenX = context.screenXDevPx / this.window.devicePixelRatio; 2599 let screenY = context.screenYDevPx / this.window.devicePixelRatio; 2600 this.window.SelectTranslationsPanel.open( 2601 event, 2602 screenX, 2603 screenY, 2604 this.#getTextToTranslate(), 2605 this.isTextSelected, 2606 this.#translationsLangPairPromise 2607 ).catch(console.error); 2608 } 2609 2610 /** 2611 * Localizes the translate-selection menuitem. 2612 * 2613 * The item will either be localized with a target language's display name 2614 * or localized in a generic way without a target language. 2615 * 2616 * @param {Element} translateSelectionItem 2617 * @returns {Promise<void>} 2618 */ 2619 async localizeTranslateSelectionItem(translateSelectionItem) { 2620 const { targetLanguage } = await this.#translationsLangPairPromise; 2621 2622 if (targetLanguage) { 2623 // A valid to-language exists, so localize the menuitem for that language. 2624 let displayName; 2625 2626 try { 2627 const languageDisplayNames = 2628 lazy.TranslationsParent.createLanguageDisplayNames(); 2629 displayName = languageDisplayNames.of(targetLanguage); 2630 } catch { 2631 // languageDisplayNames.of threw, do nothing. 2632 } 2633 2634 if (displayName) { 2635 translateSelectionItem.setAttribute("target-language", targetLanguage); 2636 this.document.l10n.setAttributes( 2637 translateSelectionItem, 2638 this.isTextSelected 2639 ? "main-context-menu-translate-selection-to-language" 2640 : "main-context-menu-translate-link-text-to-language", 2641 { language: displayName } 2642 ); 2643 return; 2644 } 2645 } 2646 2647 // Either no to-language exists, or an error occurred, 2648 // so localize the menuitem without a target language. 2649 translateSelectionItem.removeAttribute("target-language"); 2650 this.document.l10n.setAttributes( 2651 translateSelectionItem, 2652 this.isTextSelected 2653 ? "main-context-menu-translate-selection" 2654 : "main-context-menu-translate-link-text" 2655 ); 2656 } 2657 2658 /** 2659 * Fetches text for translation, prioritizing selected text over link text. 2660 * 2661 * @returns {string} The text to translate. 2662 */ 2663 #getTextToTranslate() { 2664 if (this.isTextSelected) { 2665 // If there is an active selection, we will always offer to translate. 2666 return this.selectionInfo.fullText.trim(); 2667 } 2668 2669 const linkText = this.linkTextStr.trim(); 2670 if (!linkText) { 2671 // There was no underlying link text, so do not offer to translate. 2672 return ""; 2673 } 2674 2675 if (URL.canParse(linkText)) { 2676 // The underlying link text is a URL, we should not offer to translate. 2677 return ""; 2678 } 2679 2680 // Since the underlying link text is not a URL, we should offer to translate it. 2681 return linkText; 2682 } 2683 2684 /** 2685 * Displays or hides the translate-selection item in the context menu. 2686 */ 2687 showTranslateSelectionItem() { 2688 const translateSelectionItem = this.document.getElementById( 2689 "context-translate-selection" 2690 ); 2691 const translationsEnabled = Services.prefs.getBoolPref( 2692 "browser.translations.enable" 2693 ); 2694 const selectTranslationsEnabled = Services.prefs.getBoolPref( 2695 "browser.translations.select.enable" 2696 ); 2697 2698 const textToTranslate = this.#getTextToTranslate(); 2699 2700 translateSelectionItem.hidden = 2701 // Only show the item if the feature is enabled. 2702 !(translationsEnabled && selectTranslationsEnabled) || 2703 // Only show the item if Translations is supported on this hardware. 2704 !lazy.TranslationsParent.getIsTranslationsEngineSupported() || 2705 // If there is no text to translate, we have nothing to do. 2706 textToTranslate.length === 0; 2707 2708 if (translateSelectionItem.hidden) { 2709 translateSelectionItem.removeAttribute("target-language"); 2710 return; 2711 } 2712 2713 this.#translationsLangPairPromise = 2714 this.window.SelectTranslationsPanel.getLangPairPromise(textToTranslate); 2715 this.localizeTranslateSelectionItem(translateSelectionItem); 2716 } 2717 2718 // Formats the 'Search <engine> for "<selection or link text>"' context menu. 2719 showAndFormatSearchContextItem() { 2720 let selectedText = this.isTextSelected 2721 ? this.selectedText 2722 : this.linkTextStr; 2723 2724 let { document } = this.window; 2725 let menuItem = document.getElementById("context-searchselect"); 2726 let menuItemPrivate = document.getElementById( 2727 "context-searchselect-private" 2728 ); 2729 2730 let opts = { 2731 isContextRelevant: (this.isTextSelected || this.onLink) && !this.onImage, 2732 searchTerms: selectedText, 2733 searchUrlType: lazy.SearchUtils.URL_TYPE.SEARCH, 2734 }; 2735 this.#updateSearchMenuitem({ 2736 ...opts, 2737 menuitem: menuItem, 2738 }); 2739 this.#updateSearchMenuitem({ 2740 ...opts, 2741 menuitem: menuItemPrivate, 2742 isPrivateSearchMenuitem: true, 2743 }); 2744 2745 let frameSeparator = document.getElementById("frame-sep"); 2746 2747 // Add a divider between "Search X for Y" and "This Frame", and between 2748 // "Search X for Y" and "Check Spelling", but no divider in other cases. 2749 frameSeparator.toggleAttribute( 2750 "ensureHidden", 2751 menuItem.hidden && this.inFrame 2752 ); 2753 2754 // If we're not showing the menu items, we can skip formatting the labels. 2755 if (menuItem.hidden && menuItemPrivate.hidden) { 2756 return; 2757 } 2758 2759 // Copied to alert.js' prefillAlertInfo(). 2760 // If the JS character after our truncation point is a trail surrogate, 2761 // include it in the truncated string to avoid splitting a surrogate pair. 2762 if (selectedText.length > 15) { 2763 let truncLength = 15; 2764 let truncChar = selectedText[15].charCodeAt(0); 2765 if (truncChar >= 0xdc00 && truncChar <= 0xdfff) { 2766 truncLength++; 2767 } 2768 selectedText = 2769 selectedText.substr(0, truncLength) + Services.locale.ellipsis; 2770 } 2771 2772 const { gNavigatorBundle } = this.window; 2773 // format "Search <engine> for <selection>" string to show in menu 2774 let engineName = Services.search.defaultEngine.name; 2775 let privateEngineName = Services.search.defaultPrivateEngine.name; 2776 if (!menuItem.hidden) { 2777 const docIsPrivate = lazy.PrivateBrowsingUtils.isBrowserPrivate( 2778 this.browser 2779 ); 2780 2781 let menuLabel = gNavigatorBundle.getFormattedString("contextMenuSearch", [ 2782 docIsPrivate ? privateEngineName : engineName, 2783 selectedText, 2784 ]); 2785 menuItem.label = menuLabel; 2786 menuItem.accessKey = gNavigatorBundle.getString( 2787 "contextMenuSearch.accesskey" 2788 ); 2789 } 2790 2791 if (!menuItemPrivate.hidden) { 2792 let otherEngine = engineName != privateEngineName; 2793 let accessKey = "contextMenuPrivateSearch.accesskey"; 2794 if (otherEngine) { 2795 menuItemPrivate.label = gNavigatorBundle.getFormattedString( 2796 "contextMenuPrivateSearchOtherEngine", 2797 [privateEngineName] 2798 ); 2799 accessKey = "contextMenuPrivateSearchOtherEngine.accesskey"; 2800 } else { 2801 menuItemPrivate.label = gNavigatorBundle.getString( 2802 "contextMenuPrivateSearch" 2803 ); 2804 } 2805 menuItemPrivate.accessKey = gNavigatorBundle.getString(accessKey); 2806 } 2807 } 2808 2809 #updateSearchMenuitem({ 2810 menuitem, 2811 isContextRelevant, 2812 searchTerms, 2813 searchUrlType, 2814 isPrivateSearchMenuitem = false, 2815 }) { 2816 if (!menuitem) { 2817 return; 2818 } 2819 if (!Services.search.hasSuccessfullyInitialized) { 2820 menuitem.hidden = true; 2821 return; 2822 } 2823 2824 if (isPrivateSearchMenuitem && !lazy.PrivateBrowsingUtils.enabled) { 2825 menuitem.hidden = true; 2826 return; 2827 } 2828 2829 let isBrowserPrivate = lazy.PrivateBrowsingUtils.isBrowserPrivate( 2830 this.browser 2831 ); 2832 let engine = 2833 isBrowserPrivate || isPrivateSearchMenuitem 2834 ? Services.search.defaultPrivateEngine 2835 : Services.search.defaultEngine; 2836 2837 menuitem.hidden = 2838 !isContextRelevant || 2839 this.inAboutDevtoolsToolbox || 2840 !engine?.supportsResponseType(searchUrlType) || 2841 // Don't show the private search item when we're already in a private 2842 // browsing window. 2843 (isPrivateSearchMenuitem && 2844 (isBrowserPrivate || 2845 !Services.prefs.getBoolPref( 2846 "browser.search.separatePrivateDefault.ui.enabled" 2847 ))); 2848 2849 if (!menuitem.hidden) { 2850 let url = engine.wrappedJSObject.getURLOfType(searchUrlType); 2851 if ( 2852 url?.acceptedContentTypes && 2853 (!this.contentData?.contentType || 2854 !url.acceptedContentTypes.includes(this.contentData.contentType)) 2855 ) { 2856 menuitem.hidden = true; 2857 } 2858 } 2859 2860 if (!menuitem.hidden) { 2861 menuitem.engine = engine; 2862 menuitem.searchTerms = searchTerms; 2863 menuitem.principal = this.principal; 2864 menuitem.policyContainer = this.policyContainer; 2865 menuitem.usePrivate = isPrivateSearchMenuitem || isBrowserPrivate; 2866 } 2867 } 2868 2869 /** 2870 * Shows or hides as appropriate the visual search context menu item: 2871 * "Search Image with {engine}". 2872 */ 2873 showAndFormatVisualSearchContextItem() { 2874 let menuitem = this.window.document.getElementById("context-visual-search"); 2875 this.#updateSearchMenuitem({ 2876 menuitem, 2877 isContextRelevant: 2878 this.onImage && 2879 this.imageInfo?.currentSrc && 2880 // Google Lens seems not to support images encoded as data URIs on its 2881 // GET endpoint, so we hide the visual search item for them. If we ever 2882 // add support for its POST endpoint or another visual engine that does 2883 // support data URIs, we should revisit this. 2884 !this.imageInfo.currentSrc.startsWith("data:") && 2885 !this.contentData.contentDisposition?.startsWith("attachment"), 2886 searchTerms: this.imageInfo?.currentSrc, 2887 searchUrlType: lazy.SearchUtils.URL_TYPE.VISUAL_SEARCH, 2888 }); 2889 2890 if (!menuitem.hidden) { 2891 // Record the Nimbus exposure if the menu item is shown *or would have 2892 // been shown* if the feature were enabled. 2893 lazy.NimbusFeatures.search.recordExposureEvent(); 2894 2895 // If the feature is not enabled, hide the menu item. 2896 if ( 2897 !Services.prefs.getBoolPref("browser.search.visualSearch.featureGate") 2898 ) { 2899 menuitem.hidden = true; 2900 return; 2901 } 2902 2903 let visualSearchUrl = menuitem.engine.wrappedJSObject.getURLOfType( 2904 lazy.SearchUtils.URL_TYPE.VISUAL_SEARCH 2905 ); 2906 this.window.document.l10n.setAttributes( 2907 menuitem, 2908 "main-context-menu-visual-search-2", 2909 { 2910 engine: visualSearchUrl.displayName || menuitem.engine.name, 2911 } 2912 ); 2913 this.#setNewFeatureBadge(menuitem, visualSearchUrl.isNew()); 2914 lazy.BrowserSearchTelemetry.recordSapImpression( 2915 this.browser, 2916 menuitem.engine, 2917 "contextmenu_visual" 2918 ); 2919 } 2920 } 2921 2922 /** 2923 * Loads a search engine SERP based on the data that this class previously 2924 * attached to `event.target`, which is expected to be a context menu item. 2925 * 2926 * @param {object} options 2927 * Options objects. 2928 * @param {Event} options.event 2929 * The event on a context menu item that triggered the search. 2930 * @param {SearchUtils.URL_TYPE} options.searchUrlType 2931 * A `SearchUtils.URL_TYPE` value indicating the type of search that should 2932 * be performed. A falsey value is equivalent to 2933 * `SearchUtils.URL_TYPE.SEARCH` and will perform a usual web search. 2934 */ 2935 loadSearch({ event, searchUrlType = null }) { 2936 let { engine, searchTerms, usePrivate, principal, policyContainer } = 2937 event.target; 2938 lazy.SearchUIUtils.loadSearchFromContext({ 2939 event, 2940 engine, 2941 policyContainer, 2942 searchUrlType, 2943 usePrivateWindow: usePrivate, 2944 window: this.window, 2945 searchText: searchTerms, 2946 triggeringPrincipal: principal, 2947 }); 2948 } 2949 2950 createContainerMenu(aEvent) { 2951 let createMenuOptions = { 2952 isContextMenu: true, 2953 excludeUserContextId: this.contentData.userContextId, 2954 }; 2955 return this.window.createUserContextMenu(aEvent, createMenuOptions); 2956 } 2957 2958 /** 2959 * Sets or removes the `badge` attribute on a menuitem. If it should be set, 2960 * it will be set to the value of the `main-context-menu-new-feature-badge` 2961 * l10n string. If the string has already been cached, the badge is set 2962 * synchronously, so there won't be any visual pop-in. Otherwise the string is 2963 * first fetched and cached, and then the badge is set asynchronously. 2964 * 2965 * This method is async but only for ease of implementation. It doesn't need 2966 * to be awaited unless you need to block until the badge is set. 2967 * 2968 * @param {Element} 2969 * The menuitem that should be badged. 2970 */ 2971 async #setNewFeatureBadge(menuitem, shouldShow) { 2972 menuitem.classList.toggle("badge-new", shouldShow); 2973 2974 if (!shouldShow) { 2975 menuitem.removeAttribute("badge"); 2976 return; 2977 } 2978 2979 if (this.#newFeatureBadgeL10nString) { 2980 menuitem.setAttribute("badge", this.#newFeatureBadgeL10nString); 2981 return; 2982 } 2983 2984 let value = await this.window.document.l10n.formatValue( 2985 "main-context-menu-new-feature-badge" 2986 ); 2987 if (value) { 2988 this.#newFeatureBadgeL10nString = value; 2989 this.#setNewFeatureBadge(menuitem, shouldShow); 2990 } 2991 } 2992 }