UrlbarInput.mjs (205352B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 const { XPCOMUtils } = ChromeUtils.importESModule( 6 "resource://gre/modules/XPCOMUtils.sys.mjs" 7 ); 8 9 const { AppConstants } = ChromeUtils.importESModule( 10 "resource://gre/modules/AppConstants.sys.mjs" 11 ); 12 13 /** 14 * @import {UrlbarSearchOneOffs} from "moz-src:///browser/components/urlbar/UrlbarSearchOneOffs.sys.mjs" 15 */ 16 17 const lazy = XPCOMUtils.declareLazy({ 18 ASRouter: "resource:///modules/asrouter/ASRouter.sys.mjs", 19 BrowserSearchTelemetry: 20 "moz-src:///browser/components/search/BrowserSearchTelemetry.sys.mjs", 21 BrowserUIUtils: "resource:///modules/BrowserUIUtils.sys.mjs", 22 BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs", 23 ExtensionSearchHandler: 24 "resource://gre/modules/ExtensionSearchHandler.sys.mjs", 25 ExtensionUtils: "resource://gre/modules/ExtensionUtils.sys.mjs", 26 ObjectUtils: "resource://gre/modules/ObjectUtils.sys.mjs", 27 PartnerLinkAttribution: "resource:///modules/PartnerLinkAttribution.sys.mjs", 28 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", 29 ReaderMode: "moz-src:///toolkit/components/reader/ReaderMode.sys.mjs", 30 SearchModeSwitcher: 31 "moz-src:///browser/components/urlbar/SearchModeSwitcher.sys.mjs", 32 SearchUIUtils: "moz-src:///browser/components/search/SearchUIUtils.sys.mjs", 33 SearchUtils: "moz-src:///toolkit/components/search/SearchUtils.sys.mjs", 34 UrlbarController: 35 "moz-src:///browser/components/urlbar/UrlbarController.sys.mjs", 36 UrlbarEventBufferer: 37 "moz-src:///browser/components/urlbar/UrlbarEventBufferer.sys.mjs", 38 UrlbarPrefs: "moz-src:///browser/components/urlbar/UrlbarPrefs.sys.mjs", 39 UrlbarQueryContext: 40 "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs", 41 UrlbarProviderGlobalActions: 42 "moz-src:///browser/components/urlbar/UrlbarProviderGlobalActions.sys.mjs", 43 UrlbarProviderOpenTabs: 44 "moz-src:///browser/components/urlbar/UrlbarProviderOpenTabs.sys.mjs", 45 UrlbarSearchUtils: 46 "moz-src:///browser/components/urlbar/UrlbarSearchUtils.sys.mjs", 47 UrlbarTokenizer: 48 "moz-src:///browser/components/urlbar/UrlbarTokenizer.sys.mjs", 49 UrlbarUtils: "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs", 50 UrlbarValueFormatter: 51 "moz-src:///browser/components/urlbar/UrlbarValueFormatter.sys.mjs", 52 UrlbarView: "moz-src:///browser/components/urlbar/UrlbarView.sys.mjs", 53 UrlbarSearchTermsPersistence: 54 "moz-src:///browser/components/urlbar/UrlbarSearchTermsPersistence.sys.mjs", 55 UrlUtils: "resource://gre/modules/UrlUtils.sys.mjs", 56 ClipboardHelper: { 57 service: "@mozilla.org/widget/clipboardhelper;1", 58 iid: Ci.nsIClipboardHelper, 59 }, 60 QueryStringStripper: { 61 service: "@mozilla.org/url-query-string-stripper;1", 62 iid: Ci.nsIURLQueryStringStripper, 63 }, 64 QUERY_STRIPPING_STRIP_ON_SHARE: { 65 pref: "privacy.query_stripping.strip_on_share.enabled", 66 default: false, 67 }, 68 logger: () => lazy.UrlbarUtils.getLogger({ prefix: "Input" }), 69 }); 70 71 const UNLIMITED_MAX_RESULTS = 99; 72 73 let getBoundsWithoutFlushing = element => 74 element.ownerGlobal.windowUtils.getBoundsWithoutFlushing(element); 75 let px = number => number.toFixed(2) + "px"; 76 77 /** 78 * Implements the text input part of the address bar UI. 79 */ 80 export class UrlbarInput extends HTMLElement { 81 static get #markup() { 82 return ` 83 <hbox class="urlbar-background"/> 84 <hbox class="urlbar-input-container" 85 flex="1" 86 pageproxystate="invalid"> 87 <moz-urlbar-slot name="remote-control-box"> </moz-urlbar-slot> 88 <toolbarbutton id="urlbar-searchmode-switcher" 89 class="searchmode-switcher chromeclass-toolbar-additional" 90 align="center" 91 aria-expanded="false" 92 aria-haspopup="menu" 93 tooltip="dynamic-shortcut-tooltip" 94 data-l10n-id="urlbar-searchmode-default" 95 type="menu"> 96 <image class="searchmode-switcher-icon toolbarbutton-icon"/> 97 <image class="searchmode-switcher-dropmarker toolbarbutton-icon toolbarbutton-combined-buttons-dropmarker" 98 data-l10n-id="urlbar-searchmode-dropmarker" /> 99 <menupopup class="searchmode-switcher-popup toolbar-menupopup" 100 consumeoutsideclicks="false"> 101 <label class="searchmode-switcher-popup-description" 102 role="heading" /> 103 <menuseparator/> 104 <menuseparator class="searchmode-switcher-popup-footer-separator"/> 105 <menuitem class="searchmode-switcher-popup-search-settings-button menuitem-iconic" 106 data-action="openpreferences" 107 image="chrome://global/skin/icons/settings.svg" 108 data-l10n-id="urlbar-searchmode-popup-search-settings-menuitem"/> 109 </menupopup> 110 </toolbarbutton> 111 <box class="searchmode-switcher-chicklet"> 112 <label class="searchmode-switcher-title" /> 113 <toolbarbutton class="searchmode-switcher-close toolbarbutton-icon close-button" 114 data-action="exitsearchmode" 115 role="button" 116 data-l10n-id="urlbar-searchmode-exit-button" /> 117 </box> 118 <moz-urlbar-slot name="site-info"> </moz-urlbar-slot> 119 <moz-input-box tooltip="aHTMLTooltip" 120 class="urlbar-input-box" 121 flex="1" 122 role="combobox" 123 aria-owns="urlbar-results"> 124 <html:input id="urlbar-scheme" 125 required="required"/> 126 <html:input id="urlbar-input" 127 class="urlbar-input textbox-input" 128 aria-controls="urlbar-results" 129 aria-autocomplete="both" 130 inputmode="mozAwesomebar" 131 data-l10n-id="urlbar-placeholder"/> 132 </moz-input-box> 133 <moz-urlbar-slot name="revert-button"> </moz-urlbar-slot> 134 <image class="urlbar-icon urlbar-go-button" 135 role="button" 136 data-l10n-id="urlbar-go-button"/> 137 <moz-urlbar-slot name="page-actions" hidden=""> </moz-urlbar-slot> 138 </hbox> 139 <vbox class="urlbarView" 140 context="" 141 role="group" 142 tooltip="aHTMLTooltip"> 143 <html:div class="urlbarView-body-outer"> 144 <html:div class="urlbarView-body-inner"> 145 <html:div id="urlbar-results" 146 class="urlbarView-results" 147 role="listbox"/> 148 </html:div> 149 </html:div> 150 <menupopup class="urlbarView-result-menu" 151 consumeoutsideclicks="false"/> 152 <hbox class="search-one-offs" 153 includecurrentengine="true" 154 disabletab="true"/> 155 </vbox>`; 156 } 157 158 /** @type {DocumentFragment} */ 159 static get fragment() { 160 if (!UrlbarInput.#fragment) { 161 UrlbarInput.#fragment = window.MozXULElement.parseXULToFragment( 162 UrlbarInput.#markup 163 ); 164 } 165 // @ts-ignore 166 return document.importNode(UrlbarInput.#fragment, true); 167 } 168 169 /** 170 * @type {DocumentFragment=} 171 * 172 * The cached fragment. 173 */ 174 static #fragment; 175 176 static #inputFieldEvents = [ 177 "compositionstart", 178 "compositionend", 179 "contextmenu", 180 "dragover", 181 "dragstart", 182 "drop", 183 "focus", 184 "blur", 185 "input", 186 "beforeinput", 187 "keydown", 188 "keyup", 189 "mouseover", 190 "overflow", 191 "underflow", 192 "paste", 193 "scrollend", 194 "select", 195 "selectionchange", 196 ]; 197 198 #allowBreakout = false; 199 #gBrowserListenersAdded = false; 200 #breakoutBlockerCount = 0; 201 #isAddressbar = false; 202 #sapName = ""; 203 _userTypedValue = ""; 204 _actionOverrideKeyCount = 0; 205 _lastValidURLStr = ""; 206 _valueOnLastSearch = ""; 207 _suppressStartQuery = false; 208 _suppressPrimaryAdjustment = false; 209 _lastSearchString = ""; 210 // Tracks IME composition. 211 #compositionState = lazy.UrlbarUtils.COMPOSITION.NONE; 212 #compositionClosedPopup = false; 213 214 valueIsTyped = false; 215 216 // Properties accessed in tests. 217 lastQueryContextPromise = Promise.resolve(); 218 _autofillPlaceholder = null; 219 _resultForCurrentValue = null; 220 _untrimmedValue = ""; 221 _enableAutofillPlaceholder = true; 222 223 constructor() { 224 super(); 225 226 this.window = this.ownerGlobal; 227 this.document = this.window.document; 228 this.isPrivate = lazy.PrivateBrowsingUtils.isWindowPrivate(this.window); 229 230 lazy.UrlbarPrefs.addObserver(this); 231 window.addEventListener("unload", () => { 232 // Stop listening to pref changes to make sure we don't init the new 233 // searchbar in closed windows that have not been gc'd yet. 234 lazy.UrlbarPrefs.removeObserver(this); 235 }); 236 } 237 238 /** 239 * Populates moz-urlbar-slots by moving all children with a urlbar-slot 240 * attribute into their moz-urlbar-slots and removing the slots. 241 * 242 * Should only be called once all children have been parsed. 243 */ 244 #populateSlots() { 245 let urlbarSlots = this.querySelectorAll("moz-urlbar-slot[name]"); 246 for (let slot of urlbarSlots) { 247 let slotName = slot.getAttribute("name"); 248 let nodes = this.querySelectorAll(`:scope > [urlbar-slot="${slotName}"]`); 249 250 for (let node of nodes) { 251 slot.parentNode.insertBefore(node, slot); 252 } 253 254 slot.remove(); 255 } 256 257 // Slotted elements only used by the addressbar. 258 // Will be null for searchbar and others. 259 this._identityBox = this.querySelector(".identity-box"); 260 this._revertButton = this.querySelector(".urlbar-revert-button"); 261 // Pre scotch bonnet search mode indicator (addressbar only). 262 this._searchModeIndicator = this.querySelector( 263 "#urlbar-search-mode-indicator" 264 ); 265 this._searchModeIndicatorTitle = this._searchModeIndicator?.querySelector( 266 "#urlbar-search-mode-indicator-title" 267 ); 268 this._searchModeIndicatorClose = this._searchModeIndicator?.querySelector( 269 "#urlbar-search-mode-indicator-close" 270 ); 271 } 272 273 /** 274 * Initialization that happens once on the first connect. 275 */ 276 #initOnce() { 277 this.#sapName = this.getAttribute("sap-name"); 278 this.#isAddressbar = this.#sapName == "urlbar"; 279 280 // This listener must be added before connecting the fragment 281 // because the event could fire while or after connecting it. 282 this.addEventListener( 283 "moz-input-box-rebuilt", 284 this.#onContextMenuRebuilt.bind(this) 285 ); 286 287 this.appendChild(UrlbarInput.fragment); 288 289 // Make sure all children have been parsed before calling #populateSlots. 290 if (document.readyState === "loading") { 291 document.addEventListener( 292 "DOMContentLoaded", 293 () => this.#populateSlots(), 294 { once: true } 295 ); 296 } else { 297 this.#populateSlots(); 298 } 299 300 this.panel = this.querySelector(".urlbarView"); 301 this.inputField = /** @type {HTMLInputElement} */ ( 302 this.querySelector(".urlbar-input") 303 ); 304 if (this.#sapName == "searchbar") { 305 // This adds a native clear button. 306 this.inputField.setAttribute("type", "search"); 307 } 308 this._inputContainer = this.querySelector(".urlbar-input-container"); 309 310 this.controller = new lazy.UrlbarController({ input: this }); 311 this.view = new lazy.UrlbarView(this); 312 this.searchModeSwitcher = new lazy.SearchModeSwitcher(this); 313 314 let searchModeSwitcherDescription = this.querySelector( 315 ".searchmode-switcher-popup-description" 316 ); 317 searchModeSwitcherDescription.setAttribute( 318 "data-l10n-id", 319 this.#isAddressbar 320 ? "urlbar-searchmode-popup-description" 321 : "urlbar-searchmode-popup-sticky-description" 322 ); 323 324 // The event bufferer can be used to defer events that may affect users 325 // muscle memory; for example quickly pressing DOWN+ENTER should end up 326 // on a predictable result, regardless of the search status. The event 327 // bufferer will invoke the handling code at the right time. 328 this.eventBufferer = new lazy.UrlbarEventBufferer(this); 329 330 // Forward certain properties. 331 // Note if you are extending these, you'll also need to extend the inline 332 // type definitions. 333 const READ_WRITE_PROPERTIES = [ 334 "placeholder", 335 "readOnly", 336 "selectionStart", 337 "selectionEnd", 338 ]; 339 340 for (let property of READ_WRITE_PROPERTIES) { 341 Object.defineProperty(this, property, { 342 enumerable: true, 343 get() { 344 return this.inputField[property]; 345 }, 346 set(val) { 347 this.inputField[property] = val; 348 }, 349 }); 350 } 351 352 // The engine name is not known yet, but update placeholder anyway to 353 // reflect value of keyword.enabled or set the searchbar placeholder. 354 this._setPlaceholder(null); 355 } 356 357 connectedCallback() { 358 if ( 359 this.getAttribute("sap-name") == "searchbar" && 360 !lazy.UrlbarPrefs.get("browser.search.widget.new") 361 ) { 362 return; 363 } 364 365 this.#init(); 366 } 367 368 #init() { 369 if (!this.controller) { 370 this.#initOnce(); 371 } 372 373 if (this.sapName == "searchbar") { 374 this.parentNode.setAttribute("overflows", "false"); 375 } 376 377 // Don't attach event listeners if the toolbar is not visible 378 // in this window or the urlbar is readonly. 379 if ( 380 !this.window.toolbar.visible || 381 this.window.document.documentElement.hasAttribute("taskbartab") || 382 this.readOnly 383 ) { 384 return; 385 } 386 387 this._initCopyCutController(); 388 389 for (let event of UrlbarInput.#inputFieldEvents) { 390 this.inputField.addEventListener(event, this); 391 } 392 393 // These are on the window to detect focusing shortcuts like F6. 394 this.window.addEventListener("keydown", this); 395 this.window.addEventListener("keyup", this); 396 397 this.window.addEventListener("mousedown", this); 398 if (AppConstants.platform == "win") { 399 this.window.addEventListener("draggableregionleftmousedown", this); 400 } 401 this.addEventListener("mousedown", this); 402 403 // This listener handles clicks from our children too, included the search mode 404 // indicator close button. 405 this._inputContainer.addEventListener("click", this); 406 407 // This is used to detect commands launched from the panel, to avoid 408 // recording abandonment events when the command causes a blur event. 409 this.view.panel.addEventListener("command", this, true); 410 411 this.window.addEventListener("customizationstarting", this); 412 this.window.addEventListener("aftercustomization", this); 413 this.window.addEventListener("toolbarvisibilitychange", this); 414 let menuToolbar = this.window.document.getElementById("toolbar-menubar"); 415 if (menuToolbar) { 416 menuToolbar.addEventListener("DOMMenuBarInactive", this); 417 menuToolbar.addEventListener("DOMMenuBarActive", this); 418 } 419 420 if (this.window.gBrowser) { 421 // On startup, this will be called again by browser-init.js 422 // once gBrowser has been initialized. 423 this.addGBrowserListeners(); 424 } 425 426 // If the search service is not initialized yet, the placeholder 427 // and icon will be updated in delayedStartupInit. 428 if ( 429 Cu.isESModuleLoaded("resource://gre/modules/SearchService.sys.mjs") && 430 Services.search.isInitialized 431 ) { 432 this.searchModeSwitcher.updateSearchIcon(); 433 this._updatePlaceholderFromDefaultEngine(); 434 } 435 436 // Expanding requires a parent toolbar, and us not being read-only. 437 this.#allowBreakout = !!this.closest("toolbar"); 438 if (this.#allowBreakout) { 439 // TODO(emilio): This could use CSS anchor positioning rather than this 440 // ResizeObserver, eventually. 441 this._resizeObserver = new this.window.ResizeObserver(([entry]) => { 442 this.style.setProperty( 443 "--urlbar-width", 444 px(entry.borderBoxSize[0].inlineSize) 445 ); 446 }); 447 this._resizeObserver.observe(this.parentNode); 448 } 449 450 this.#updateLayoutBreakout(); 451 452 this._addObservers(); 453 } 454 455 disconnectedCallback() { 456 if ( 457 this.getAttribute("sap-name") == "searchbar" && 458 !lazy.UrlbarPrefs.get("browser.search.widget.new") 459 ) { 460 return; 461 } 462 463 this.#uninit(); 464 } 465 466 #uninit() { 467 if (this.sapName == "searchbar") { 468 this.parentNode.removeAttribute("overflows"); 469 470 // Exit search mode to make sure it doesn't become stale while the 471 // searchbar is invisible. Otherwise, the engine might get deleted 472 // but we don't notice because the search service observer is inactive. 473 this.searchMode = null; 474 } 475 476 if (this._copyCutController) { 477 this.inputField.controllers.removeController(this._copyCutController); 478 delete this._copyCutController; 479 } 480 481 for (let event of UrlbarInput.#inputFieldEvents) { 482 this.inputField.removeEventListener(event, this); 483 } 484 485 // These are on the window to detect focusing shortcuts like F6. 486 this.window.removeEventListener("keydown", this); 487 this.window.removeEventListener("keyup", this); 488 489 this.window.removeEventListener("mousedown", this); 490 if (AppConstants.platform == "win") { 491 this.window.removeEventListener("draggableregionleftmousedown", this); 492 } 493 this.removeEventListener("mousedown", this); 494 495 // This listener handles clicks from our children too, included the search mode 496 // indicator close button. 497 this._inputContainer.removeEventListener("click", this); 498 499 // This is used to detect commands launched from the panel, to avoid 500 // recording abandonment events when the command causes a blur event. 501 this.view.panel.removeEventListener("command", this, true); 502 503 this.window.removeEventListener("customizationstarting", this); 504 this.window.removeEventListener("aftercustomization", this); 505 this.window.removeEventListener("toolbarvisibilitychange", this); 506 let menuToolbar = this.window.document.getElementById("toolbar-menubar"); 507 if (menuToolbar) { 508 menuToolbar.removeEventListener("DOMMenuBarInactive", this); 509 menuToolbar.removeEventListener("DOMMenuBarActive", this); 510 } 511 if (this.#gBrowserListenersAdded) { 512 this.window.gBrowser.tabContainer.removeEventListener("TabSelect", this); 513 this.window.gBrowser.tabContainer.removeEventListener("TabClose", this); 514 this.window.gBrowser.removeTabsProgressListener(this); 515 this.#gBrowserListenersAdded = false; 516 } 517 518 this._resizeObserver?.disconnect(); 519 520 this._removeObservers(); 521 } 522 523 /** 524 * This method is used to attach new context menu options to the urlbar 525 * context menu, i.e. the context menu of the moz-input-box. 526 * It is called when the moz-input-box rebuilds its context menu. 527 * 528 * Note that it might be called before #init has finished. 529 */ 530 #onContextMenuRebuilt() { 531 this._initStripOnShare(); 532 this._initPasteAndGo(); 533 } 534 535 addGBrowserListeners() { 536 if (this.window.gBrowser && !this.#gBrowserListenersAdded) { 537 this.window.gBrowser.tabContainer.addEventListener("TabSelect", this); 538 this.window.gBrowser.tabContainer.addEventListener("TabClose", this); 539 this.window.gBrowser.addTabsProgressListener(this); 540 this.#gBrowserListenersAdded = true; 541 } 542 } 543 544 #lazy = XPCOMUtils.declareLazy({ 545 valueFormatter: () => new lazy.UrlbarValueFormatter(this), 546 addSearchEngineHelper: () => new AddSearchEngineHelper(this), 547 }); 548 549 /** 550 * Manages the Add Search Engine contextual menu entries. 551 */ 552 get addSearchEngineHelper() { 553 return this.#lazy.addSearchEngineHelper; 554 } 555 556 /** 557 * The search access point name of the UrlbarInput for use with telemetry or 558 * logging, e.g. `urlbar`, `searchbar`. 559 */ 560 get sapName() { 561 return this.#sapName; 562 } 563 564 blur() { 565 this.inputField.blur(); 566 } 567 568 /** 569 * @type {typeof HTMLInputElement.prototype.placeholder} 570 */ 571 placeholder; 572 573 /** 574 * @type {typeof HTMLInputElement.prototype.readOnly} 575 */ 576 readOnly; 577 578 /** 579 * @type {typeof HTMLInputElement.prototype.selectionStart} 580 */ 581 selectionStart; 582 583 /** 584 * @type {typeof HTMLInputElement.prototype.selectionEnd} 585 */ 586 selectionEnd; 587 588 /** 589 * Called when a urlbar or urlbar related pref changes. 590 * 591 * @param {string} pref 592 * The name of the pref. Relative to `browser.urlbar` for urlbar prefs. 593 */ 594 onPrefChanged(pref) { 595 switch (pref) { 596 case "keyword.enabled": 597 this._updatePlaceholderFromDefaultEngine().catch(e => 598 // This can happen if the search service failed. 599 console.warn("Falied to update urlbar placeholder:", e) 600 ); 601 break; 602 case "browser.search.widget.new": { 603 if (this.getAttribute("sap-name") == "searchbar" && this.isConnected) { 604 if (lazy.UrlbarPrefs.get("browser.search.widget.new")) { 605 // The connectedCallback was skipped. Init now. 606 this.#init(); 607 } else { 608 // Uninit now, the disconnectedCallback will be skipped. 609 this.#uninit(); 610 } 611 } 612 } 613 } 614 } 615 616 /** 617 * Applies styling to the text in the urlbar input, depending on the text. 618 */ 619 formatValue() { 620 // The editor may not exist if the toolbar is not visible. 621 if (this.#isAddressbar && this.editor) { 622 this.#lazy.valueFormatter.update(); 623 } 624 } 625 626 focus() { 627 let beforeFocus = new CustomEvent("beforefocus", { 628 bubbles: true, 629 cancelable: true, 630 }); 631 this.inputField.dispatchEvent(beforeFocus); 632 if (beforeFocus.defaultPrevented) { 633 return; 634 } 635 636 this.inputField.focus(); 637 } 638 639 select() { 640 let beforeSelect = new CustomEvent("beforeselect", { 641 bubbles: true, 642 cancelable: true, 643 }); 644 this.inputField.dispatchEvent(beforeSelect); 645 if (beforeSelect.defaultPrevented) { 646 return; 647 } 648 649 // See _on_select(). HTMLInputElement.select() dispatches a "select" 650 // event but does not set the primary selection. 651 this._suppressPrimaryAdjustment = true; 652 this.inputField.select(); 653 this._suppressPrimaryAdjustment = false; 654 } 655 656 setSelectionRange(selectionStart, selectionEnd) { 657 let beforeSelect = new CustomEvent("beforeselect", { 658 bubbles: true, 659 cancelable: true, 660 }); 661 this.inputField.dispatchEvent(beforeSelect); 662 if (beforeSelect.defaultPrevented) { 663 return; 664 } 665 666 // See _on_select(). HTMLInputElement.select() dispatches a "select" 667 // event but does not set the primary selection. 668 this._suppressPrimaryAdjustment = true; 669 this.inputField.setSelectionRange(selectionStart, selectionEnd); 670 this._suppressPrimaryAdjustment = false; 671 } 672 673 saveSelectionStateForBrowser(browser) { 674 let state = this.getBrowserState(browser); 675 state.selection = { 676 // When the value is empty, we're either on a blank page, or the whole 677 // text has been edited away. In the latter case we'll restore value to 678 // the current URI, and we want to fully select it. 679 start: this.value ? this.selectionStart : 0, 680 end: this.value ? this.selectionEnd : Number.MAX_SAFE_INTEGER, 681 // When restoring a URI from an empty value, we don't want to untrim it. 682 shouldUntrim: this.value && !this._protocolIsTrimmed, 683 }; 684 } 685 686 restoreSelectionStateForBrowser(browser) { 687 // Address bar must be focused to untrim and for selection to make sense. 688 this.focus(); 689 let state = this.getBrowserState(browser); 690 if (state.selection) { 691 if (state.selection.shouldUntrim) { 692 this.#maybeUntrimUrl(); 693 } 694 this.setSelectionRange( 695 state.selection.start, 696 // When selecting all the end value may be larger than the actual value. 697 Math.min(state.selection.end, this.value.length) 698 ); 699 } 700 } 701 702 /** 703 * Sets the URI to display in the location bar. 704 * 705 * @param {object} [options] 706 * @param {?nsIURI} [options.uri] 707 * If this is unspecified, the current URI will be used. 708 * @param {boolean} [options.dueToTabSwitch=false] 709 * Whether this is being called due to switching tabs. 710 * @param {boolean} [options.dueToSessionRestore=false] 711 * Whether this is being called due to session restore. 712 * @param {boolean} [options.hideSearchTerms=false] 713 * True if userTypedValue should not be overidden by search terms 714 * and false otherwise. 715 * @param {boolean} [options.isSameDocument=false] 716 * Whether the caller loaded a new document or not (e.g. location 717 * change from an anchor scroll or a pushState event). 718 */ 719 setURI({ 720 uri = null, 721 dueToTabSwitch = false, 722 dueToSessionRestore = false, 723 hideSearchTerms = false, 724 isSameDocument = false, 725 } = {}) { 726 if (!this.#isAddressbar) { 727 throw new Error( 728 "Cannot set URI for UrlbarInput that is not an address bar" 729 ); 730 } 731 if ( 732 this.window.browsingContext.isDocumentPiP && 733 uri.spec.startsWith("about:blank") 734 ) { 735 // If this is a Document PiP, its url will be about:blank while 736 // the opener will be a secure context, i.e. no about:blank 737 throw new Error("Document PiP should show its opener URL"); 738 } 739 // We only need to update the searchModeUI on tab switch conditionally 740 // as we only persist searchMode with ScotchBonnet enabled. 741 if ( 742 dueToTabSwitch && 743 lazy.UrlbarPrefs.getScotchBonnetPref("scotchBonnet.persistSearchMode") 744 ) { 745 this._updateSearchModeUI(this.searchMode); 746 } 747 748 let state = this.getBrowserState(this.window.gBrowser.selectedBrowser); 749 this.#handlePersistedSearchTerms({ 750 state, 751 uri, 752 dueToTabSwitch, 753 hideSearchTerms, 754 isSameDocument, 755 }); 756 757 let value = this.userTypedValue; 758 let valid = false; 759 let isReverting = !uri; 760 761 // If `value` is null or if it's an empty string and we're switching tabs 762 // set value to the browser's current URI. When a user empties the input, 763 // switches tabs, and switches back, we want the URI to become visible again 764 // so the user knows what URI they're viewing. 765 // An exception to this is made in case of an auth request from a different 766 // base domain. To avoid auth prompt spoofing we already display the url of 767 // the cross domain resource, although the page is not loaded yet. 768 // This url will be set/unset by PromptParent. See bug 791594 for reference. 769 if (value === null || (!value && dueToTabSwitch)) { 770 uri = 771 this.window.gBrowser.selectedBrowser.currentAuthPromptURI || 772 uri || 773 this.#isOpenedPageInBlankTargetLoading || 774 this.window.gBrowser.currentURI; 775 // Strip off usernames and passwords for the location bar 776 try { 777 uri = Services.io.createExposableURI(uri); 778 } catch (e) {} 779 780 let isInitialPageControlledByWebContent = false; 781 782 // Replace initial page URIs with an empty string 783 // only if there's no opener (bug 370555). 784 if ( 785 this.window.isInitialPage(uri) && 786 lazy.BrowserUIUtils.checkEmptyPageOrigin( 787 this.window.gBrowser.selectedBrowser, 788 uri 789 ) 790 ) { 791 value = ""; 792 } else { 793 isInitialPageControlledByWebContent = true; 794 795 // We should deal with losslessDecodeURI throwing for exotic URIs 796 try { 797 value = losslessDecodeURI(uri); 798 } catch (ex) { 799 value = "about:blank"; 800 } 801 } 802 // If we update the URI while restoring a session, set the proxyState to 803 // invalid, because we don't have a valid security state to show via site 804 // identity yet. See Bug 1746383. 805 valid = 806 !dueToSessionRestore && 807 (!this.#canHandleAsBlankPage(uri.spec) || 808 lazy.ExtensionUtils.isExtensionUrl(uri) || 809 isInitialPageControlledByWebContent); 810 } else if ( 811 this.window.isInitialPage(value) && 812 lazy.BrowserUIUtils.checkEmptyPageOrigin( 813 this.window.gBrowser.selectedBrowser 814 ) 815 ) { 816 value = ""; 817 valid = true; 818 } 819 820 const previousUntrimmedValue = this.untrimmedValue; 821 // When calculating the selection indices we must take into account a 822 // trimmed protocol. 823 let offset = this._protocolIsTrimmed 824 ? lazy.BrowserUIUtils.trimURLProtocol.length 825 : 0; 826 const previousSelectionStart = this.selectionStart + offset; 827 const previousSelectionEnd = this.selectionEnd + offset; 828 829 this._setValue(value, { allowTrim: true, valueIsTyped: !valid }); 830 this.toggleAttribute("usertyping", !valid && value); 831 832 if (this.focused && value != previousUntrimmedValue) { 833 if ( 834 previousSelectionStart != previousSelectionEnd && 835 value.substring(previousSelectionStart, previousSelectionEnd) === 836 previousUntrimmedValue.substring( 837 previousSelectionStart, 838 previousSelectionEnd 839 ) 840 ) { 841 // If the same text is in the same place as the previously selected text, 842 // the selection is kept. 843 this.inputField.setSelectionRange( 844 previousSelectionStart - offset, 845 previousSelectionEnd - offset 846 ); 847 } else if ( 848 previousSelectionEnd && 849 (previousUntrimmedValue.length === previousSelectionEnd || 850 value.length <= previousSelectionEnd) 851 ) { 852 // If the previous end caret is not 0 and the caret is at the end of the 853 // input or its position is beyond the end of the new value, keep the 854 // position at the end. 855 this.inputField.setSelectionRange(value.length, value.length); 856 } else { 857 // Otherwise clear selection and set the caret position to the previous 858 // caret end position. 859 this.inputField.setSelectionRange( 860 previousSelectionEnd - offset, 861 previousSelectionEnd - offset 862 ); 863 } 864 } 865 866 // The proxystate must be set before setting search mode below because 867 // search mode depends on it. 868 this.setPageProxyState( 869 valid ? "valid" : "invalid", 870 dueToTabSwitch, 871 !isReverting && 872 dueToTabSwitch && 873 this.getBrowserState(this.window.gBrowser.selectedBrowser) 874 .isUnifiedSearchButtonAvailable 875 ); 876 877 if ( 878 state.persist?.shouldPersist && 879 !lazy.UrlbarSearchTermsPersistence.searchModeMatchesState( 880 this.searchMode, 881 state 882 ) 883 ) { 884 // When search terms persist, on non-default engine search result pages 885 // the address bar should show the same search mode. For default engines, 886 // search mode should not persist. 887 if (state.persist.isDefaultEngine) { 888 this.searchMode = null; 889 } else { 890 this.searchMode = { 891 engineName: state.persist.originalEngineName, 892 source: lazy.UrlbarUtils.RESULT_SOURCE.SEARCH, 893 isPreview: false, 894 }; 895 } 896 } else if (dueToTabSwitch && !valid) { 897 // If we're switching tabs, restore the tab's search mode. 898 this.restoreSearchModeState(); 899 } else if (valid) { 900 // If the URI is valid, exit search mode. This must happen 901 // after setting proxystate above because search mode depends on it. 902 this.searchMode = null; 903 } 904 905 // Dispatch URIUpdate event to synchronize the tab status when switching. 906 let event = new CustomEvent("SetURI", { bubbles: true }); 907 this.inputField.dispatchEvent(event); 908 } 909 910 /** 911 * Converts an internal URI (e.g. a URI with a username or password) into one 912 * which we can expose to the user. 913 * 914 * @param {nsIURI} uri 915 * The URI to be converted 916 * @returns {nsIURI} 917 * The converted, exposable URI 918 */ 919 makeURIReadable(uri) { 920 // Avoid copying 'about:reader?url=', and always provide the original URI: 921 // Reader mode ensures we call createExposableURI itself. 922 let readerStrippedURI = lazy.ReaderMode.getOriginalUrlObjectForDisplay( 923 uri.displaySpec 924 ); 925 if (readerStrippedURI) { 926 return readerStrippedURI; 927 } 928 929 try { 930 return Services.io.createExposableURI(uri); 931 } catch (ex) {} 932 933 return uri; 934 } 935 936 /** 937 * Function for tabs progress listener. 938 * 939 * @param {nsIBrowser} browser 940 * @param {nsIWebProgress} webProgress 941 * The nsIWebProgress instance that fired the notification. 942 * @param {nsIRequest} request 943 * The associated nsIRequest. This may be null in some cases. 944 * @param {nsIURI} locationURI 945 * The URI of the location that is being loaded. 946 */ 947 onLocationChange(browser, webProgress, request, locationURI) { 948 if (!webProgress.isTopLevel) { 949 return; 950 } 951 952 if ( 953 browser != this.window.gBrowser.selectedBrowser && 954 !this.#canHandleAsBlankPage(locationURI.spec) 955 ) { 956 // If the page is loaded on background tab, make Unified Search Button 957 // unavailable when back to the tab. 958 this.getBrowserState(browser).isUnifiedSearchButtonAvailable = false; 959 } 960 961 // Using browser navigation buttons should potentially trigger a bounce 962 // telemetry event. 963 if (webProgress.loadType & Ci.nsIDocShell.LOAD_CMD_HISTORY) { 964 this.controller.engagementEvent.handleBounceEventTrigger(browser); 965 } 966 } 967 968 /** 969 * Passes DOM events to the _on_<event type> methods. 970 * 971 * @param {Event} event The event to handle. 972 */ 973 handleEvent(event) { 974 let methodName = "_on_" + event.type; 975 if (methodName in this) { 976 try { 977 this[methodName](event); 978 } catch (e) { 979 console.error(`Error calling UrlbarInput::${methodName}:`, e); 980 } 981 } else { 982 throw new Error("Unrecognized UrlbarInput event: " + event.type); 983 } 984 } 985 986 /** 987 * Handles an event which might open text or a URL. If the event requires 988 * doing so, handleCommand forwards it to handleNavigation. 989 * 990 * @param {Event} [event] The event triggering the open. 991 */ 992 handleCommand(event = null) { 993 let isMouseEvent = MouseEvent.isInstance(event); 994 if (isMouseEvent && event.button == 2) { 995 // Do nothing for right clicks. 996 return; 997 } 998 999 // Determine whether to use the selected one-off search button. In 1000 // one-off search buttons parlance, "selected" means that the button 1001 // has been navigated to via the keyboard. So we want to use it if 1002 // the triggering event is not a mouse click -- i.e., it's a Return 1003 // key -- or if the one-off was mouse-clicked. 1004 if (this.view.isOpen) { 1005 let selectedOneOff = this.view.oneOffSearchButtons?.selectedButton; 1006 if (selectedOneOff && (!isMouseEvent || event.target == selectedOneOff)) { 1007 this.view.oneOffSearchButtons.handleSearchCommand(event, { 1008 engineName: selectedOneOff.engine?.name, 1009 source: selectedOneOff.source, 1010 entry: "oneoff", 1011 }); 1012 return; 1013 } 1014 } 1015 1016 this.handleNavigation({ event }); 1017 } 1018 1019 /** 1020 * @typedef {object} HandleNavigationOneOffParams 1021 * 1022 * @property {string} openWhere 1023 * Where we expect the result to be opened. 1024 * @property {object} openParams 1025 * The parameters related to where the result will be opened. 1026 * @property {nsISearchEngine} engine 1027 * The selected one-off's engine. 1028 */ 1029 1030 /** 1031 * Handles an event which would cause a URL or text to be opened. 1032 * 1033 * @param {object} options 1034 * Options for the navigation. 1035 * @param {Event} [options.event] 1036 * The event triggering the open. 1037 * @param {HandleNavigationOneOffParams} [options.oneOffParams] 1038 * Optional. Pass if this navigation was triggered by a one-off. Practically 1039 * speaking, UrlbarSearchOneOffs passes this when the user holds certain key 1040 * modifiers while picking a one-off. In those cases, we do an immediate 1041 * search using the one-off's engine instead of entering search mode. 1042 * @param {object} [options.triggeringPrincipal] 1043 * The principal that the action was triggered from. 1044 */ 1045 handleNavigation({ event, oneOffParams, triggeringPrincipal }) { 1046 let element = this.view.selectedElement; 1047 let result = this.view.getResultFromElement(element); 1048 let openParams = oneOffParams?.openParams || { triggeringPrincipal }; 1049 1050 // If the value was submitted during composition, the result may not have 1051 // been updated yet, because the input event happens after composition end. 1052 // We can't trust element nor _resultForCurrentValue targets in that case, 1053 // so we always generate a new heuristic to load. 1054 let isComposing = this.editor.composing; 1055 1056 // Use the selected element if we have one; this is usually the case 1057 // when the view is open. 1058 let selectedPrivateResult = 1059 result && 1060 result.type == lazy.UrlbarUtils.RESULT_TYPE.SEARCH && 1061 result.payload.inPrivateWindow; 1062 let selectedPrivateEngineResult = 1063 selectedPrivateResult && result.payload.isPrivateEngine; 1064 // Whether the user has been editing the value in the URL bar after selecting 1065 // the result. However, if the result type is tip, pick as it is. The result 1066 // heuristic is also kept the behavior as is for safety. 1067 let safeToPickResult = 1068 result && 1069 (result.heuristic || 1070 !this.valueIsTyped || 1071 result.type == lazy.UrlbarUtils.RESULT_TYPE.TIP || 1072 this.value == this.#getValueFromResult(result)); 1073 if ( 1074 !isComposing && 1075 element && 1076 (!oneOffParams?.engine || selectedPrivateEngineResult) && 1077 safeToPickResult 1078 ) { 1079 this.pickElement(element, event); 1080 return; 1081 } 1082 1083 // Use the hidden heuristic if it exists and there's no selection. 1084 if ( 1085 lazy.UrlbarPrefs.get("experimental.hideHeuristic") && 1086 !element && 1087 !isComposing && 1088 !oneOffParams?.engine && 1089 this._resultForCurrentValue?.heuristic 1090 ) { 1091 this.pickResult(this._resultForCurrentValue, event); 1092 return; 1093 } 1094 1095 // We don't select a heuristic result when we're autofilling a token alias, 1096 // but we want pressing Enter to behave like the first result was selected. 1097 if (!result && this.value.startsWith("@")) { 1098 let tokenAliasResult = this.view.getResultAtIndex(0); 1099 if (tokenAliasResult?.autofill && tokenAliasResult?.payload.keyword) { 1100 this.pickResult(tokenAliasResult, event); 1101 return; 1102 } 1103 } 1104 1105 let url; 1106 let selType = this.controller.engagementEvent.typeFromElement( 1107 result, 1108 element 1109 ); 1110 let typedValue = this.value; 1111 if (oneOffParams?.engine) { 1112 selType = "oneoff"; 1113 typedValue = this._lastSearchString; 1114 // If there's a selected one-off button then load a search using 1115 // the button's engine. 1116 result = this._resultForCurrentValue; 1117 1118 let searchString = 1119 (result && (result.payload.suggestion || result.payload.query)) || 1120 this._lastSearchString; 1121 [url, openParams.postData] = lazy.UrlbarUtils.getSearchQueryUrl( 1122 oneOffParams.engine, 1123 searchString 1124 ); 1125 if (oneOffParams.openWhere == "tab") { 1126 this.window.gBrowser.tabContainer.addEventListener( 1127 "TabOpen", 1128 tabEvent => 1129 this._recordSearch( 1130 oneOffParams.engine, 1131 event, 1132 {}, 1133 tabEvent.target.linkedBrowser 1134 ), 1135 { once: true } 1136 ); 1137 } else { 1138 this._recordSearch(oneOffParams.engine, event); 1139 } 1140 1141 lazy.UrlbarUtils.addToFormHistory( 1142 this, 1143 searchString, 1144 oneOffParams.engine.name 1145 ).catch(console.error); 1146 } else { 1147 // Use the current value if we don't have a UrlbarResult e.g. because the 1148 // view is closed. 1149 url = this.untrimmedValue; 1150 openParams.postData = null; 1151 } 1152 1153 if (!url) { 1154 return; 1155 } 1156 1157 // When the user hits enter in a local search mode and there's no selected 1158 // result or one-off, don't do anything. 1159 if ( 1160 this.searchMode && 1161 !this.searchMode.engineName && 1162 !result && 1163 !oneOffParams 1164 ) { 1165 return; 1166 } 1167 1168 let where = oneOffParams?.openWhere || this._whereToOpen(event); 1169 if (selectedPrivateResult) { 1170 where = "window"; 1171 openParams.private = true; 1172 } 1173 openParams.allowInheritPrincipal = false; 1174 url = this._maybeCanonizeURL(event, url) || url.trim(); 1175 1176 let selectedResult = result || this.view.selectedResult; 1177 this.controller.engagementEvent.record(event, { 1178 element, 1179 selType, 1180 searchString: typedValue, 1181 result: selectedResult || this._resultForCurrentValue || null, 1182 }); 1183 1184 if (URL.canParse(url)) { 1185 // Annotate if the untrimmed value contained a scheme, to later potentially 1186 // be upgraded by schemeless HTTPS-First. 1187 openParams.schemelessInput = this.#getSchemelessInput( 1188 this.untrimmedValue 1189 ); 1190 this._loadURL(url, event, where, openParams); 1191 return; 1192 } 1193 1194 // This is not a URL and there's no selected element, because likely the 1195 // view is closed, or paste&go was used. 1196 // We must act consistently here, having or not an open view should not 1197 // make a difference if the search string is the same. 1198 1199 // If we have a result for the current value, we can just use it. 1200 if (!isComposing && this._resultForCurrentValue) { 1201 this.pickResult(this._resultForCurrentValue, event); 1202 return; 1203 } 1204 1205 // Otherwise, we must fetch the heuristic result for the current value. 1206 // TODO (Bug 1604927): If the urlbar results are restricted to a specific 1207 // engine, here we must search with that specific engine; indeed the 1208 // docshell wouldn't know about our engine restriction. 1209 // Also remember to invoke this._recordSearch, after replacing url with 1210 // the appropriate engine submission url. 1211 let browser = this.window.gBrowser.selectedBrowser; 1212 let lastLocationChange = browser.lastLocationChange; 1213 1214 // Increment rate denominator measuring how often Address Bar handleCommand fallback path is hit. 1215 Glean.urlbar.heuristicResultMissing.addToDenominator(1); 1216 1217 lazy.UrlbarUtils.getHeuristicResultFor(url, this) 1218 .then(newResult => { 1219 // Because this happens asynchronously, we must verify that the browser 1220 // location did not change in the meanwhile. 1221 if ( 1222 where != "current" || 1223 browser.lastLocationChange == lastLocationChange 1224 ) { 1225 this.pickResult(newResult, event, null, browser); 1226 } 1227 }) 1228 .catch(() => { 1229 if (url) { 1230 // Something went wrong, we should always have a heuristic result, 1231 // otherwise it means we're not able to search at all, maybe because 1232 // some parts of the profile are corrupt. 1233 // The urlbar should still allow to search or visit the typed string, 1234 // so that the user can look for help to resolve the problem. 1235 1236 // Increment rate numerator measuring how often Address Bar handleCommand fallback path is hit. 1237 Glean.urlbar.heuristicResultMissing.addToNumerator(1); 1238 1239 let flags = 1240 Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS | 1241 Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP; 1242 if (this.isPrivate) { 1243 flags |= Ci.nsIURIFixup.FIXUP_FLAG_PRIVATE_CONTEXT; 1244 } 1245 let { 1246 preferredURI: uri, 1247 postData, 1248 keywordAsSent, 1249 } = Services.uriFixup.getFixupURIInfo(url, flags); 1250 if ( 1251 where != "current" || 1252 browser.lastLocationChange == lastLocationChange 1253 ) { 1254 openParams.postData = postData; 1255 if (!keywordAsSent) { 1256 // `uri` is not a search engine url, so we annotate if the untrimmed 1257 // value contained a scheme, to potentially be later upgraded by 1258 // schemeless HTTPS-First. 1259 openParams.schemelessInput = this.#getSchemelessInput( 1260 this.untrimmedValue 1261 ); 1262 } 1263 this._loadURL(uri.spec, event, where, openParams, null, browser); 1264 } 1265 } 1266 }); 1267 // Don't add further handling here, the catch above is our last resort. 1268 } 1269 1270 handleRevert() { 1271 this.userTypedValue = null; 1272 // Nullify search mode before setURI so it won't try to restore it. 1273 this.searchMode = null; 1274 if (this.#isAddressbar) { 1275 this.setURI({ 1276 dueToTabSwitch: true, 1277 hideSearchTerms: true, 1278 }); 1279 } else { 1280 this.value = ""; 1281 } 1282 if (this.value && this.focused) { 1283 this.select(); 1284 } 1285 } 1286 1287 maybeHandleRevertFromPopup(anchorElement) { 1288 let state = this.getBrowserState(this.window.gBrowser.selectedBrowser); 1289 if (anchorElement?.closest("#urlbar") && state.persist?.shouldPersist) { 1290 this.handleRevert(); 1291 Glean.urlbarPersistedsearchterms.revertByPopupCount.add(1); 1292 } 1293 } 1294 1295 /** 1296 * Called by inputs that resemble search boxes, but actually hand input off 1297 * to the Urlbar. We use these fake inputs on the new tab page and 1298 * about:privatebrowsing. 1299 * 1300 * @param {string} searchString 1301 * The search string to use. 1302 * @param {nsISearchEngine} [searchEngine] 1303 * Optional. If included and the right prefs are set, we will enter search 1304 * mode when handing `searchString` from the fake input to the Urlbar. 1305 * @param {string} [newtabSessionId] 1306 * Optional. The id of the newtab session that handed off this search. 1307 */ 1308 handoff(searchString, searchEngine, newtabSessionId) { 1309 this._isHandoffSession = true; 1310 this._handoffSession = newtabSessionId; 1311 if (lazy.UrlbarPrefs.get("shouldHandOffToSearchMode") && searchEngine) { 1312 this.search(searchString, { 1313 searchEngine, 1314 searchModeEntry: "handoff", 1315 }); 1316 } else { 1317 this.search(searchString); 1318 } 1319 } 1320 1321 /** 1322 * Called when an element of the view is picked. 1323 * 1324 * @param {HTMLElement} element The element that was picked. 1325 * @param {Event} event The event that picked the element. 1326 */ 1327 pickElement(element, event) { 1328 let result = this.view.getResultFromElement(element); 1329 lazy.logger.debug( 1330 `pickElement ${element} with event ${event?.type}, result: ${result}` 1331 ); 1332 if (!result) { 1333 return; 1334 } 1335 this.pickResult(result, event, element); 1336 } 1337 1338 /** 1339 * Called when a result is picked. 1340 * 1341 * @param {UrlbarResult} result The result that was picked. 1342 * @param {Event} event The event that picked the result. 1343 * @param {HTMLElement} element the picked view element, if available. 1344 * @param {object} browser The browser to use for the load. 1345 */ 1346 // eslint-disable-next-line complexity 1347 pickResult( 1348 result, 1349 event, 1350 element = null, 1351 browser = this.window.gBrowser.selectedBrowser 1352 ) { 1353 if (element?.classList.contains("urlbarView-button-menu")) { 1354 this.view.openResultMenu(result, element); 1355 return; 1356 } 1357 1358 if (element?.dataset.command) { 1359 this.#pickMenuResult(result, event, element, browser); 1360 return; 1361 } 1362 1363 if ( 1364 result.providerName == lazy.UrlbarProviderGlobalActions.name && 1365 this.#providesSearchMode(result) 1366 ) { 1367 this.maybeConfirmSearchModeFromResult({ 1368 result, 1369 checkValue: false, 1370 }); 1371 return; 1372 } 1373 1374 // When a one-off is selected, we restyle heuristic results to look like 1375 // search results. In the unlikely event that they are clicked, instead of 1376 // picking the results as usual, we confirm search mode, same as if the user 1377 // had selected them and pressed the enter key. Restyling results in this 1378 // manner was agreed on as a compromise between consistent UX and 1379 // engineering effort. See review discussion at bug 1667766. 1380 if ( 1381 (this.searchMode?.isPreview && 1382 result.providerName == lazy.UrlbarProviderGlobalActions.name) || 1383 (result.heuristic && 1384 this.searchMode?.isPreview && 1385 this.view.oneOffSearchButtons?.selectedButton) 1386 ) { 1387 this.confirmSearchMode(); 1388 this.search(this.value); 1389 return; 1390 } 1391 1392 if ( 1393 result.type == lazy.UrlbarUtils.RESULT_TYPE.TIP && 1394 result.payload.type == "dismissalAcknowledgment" 1395 ) { 1396 // The user clicked the "Got it" button inside the dismissal 1397 // acknowledgment tip. Dismiss the tip. 1398 this.controller.engagementEvent.record(event, { 1399 result, 1400 element, 1401 searchString: this._lastSearchString, 1402 selType: "dismiss", 1403 }); 1404 this.view.onQueryResultRemoved(result.rowIndex); 1405 return; 1406 } 1407 1408 let resultUrl = element?.dataset.url; 1409 let originalUntrimmedValue = this.untrimmedValue; 1410 let isCanonized = this.setValueFromResult({ 1411 result, 1412 event, 1413 element, 1414 urlOverride: resultUrl, 1415 }); 1416 let where = this._whereToOpen(event); 1417 let openParams = { 1418 allowInheritPrincipal: false, 1419 globalHistoryOptions: { 1420 triggeringSource: this.#sapName, 1421 triggeringSearchEngine: result.payload?.engine, 1422 triggeringSponsoredURL: result.payload?.isSponsored 1423 ? result.payload.url 1424 : undefined, 1425 }, 1426 private: this.isPrivate, 1427 }; 1428 1429 if (resultUrl && where == "current") { 1430 // Open help links in a new tab. 1431 where = "tab"; 1432 } 1433 1434 if (!this.#providesSearchMode(result)) { 1435 this.view.close({ elementPicked: true }); 1436 } 1437 1438 if (isCanonized) { 1439 this.controller.engagementEvent.record(event, { 1440 result, 1441 element, 1442 selType: "canonized", 1443 searchString: this._lastSearchString, 1444 }); 1445 this._loadURL(this._untrimmedValue, event, where, openParams, browser); 1446 return; 1447 } 1448 1449 let { url, postData } = resultUrl 1450 ? { url: resultUrl, postData: null } 1451 : lazy.UrlbarUtils.getUrlFromResult(result, { element }); 1452 openParams.postData = postData; 1453 1454 switch (result.type) { 1455 case lazy.UrlbarUtils.RESULT_TYPE.URL: { 1456 if (result.heuristic) { 1457 // Bug 1578856: both the provider and the docshell run heuristics to 1458 // decide how to handle a non-url string, either fixing it to a url, or 1459 // searching for it. 1460 // Some preferences can control the docshell behavior, for example 1461 // if dns_first_for_single_words is true, the docshell looks up the word 1462 // against the dns server, and either loads it as an url or searches for 1463 // it, depending on the lookup result. The provider instead will always 1464 // return a fixed url in this case, because URIFixup is synchronous and 1465 // can't do a synchronous dns lookup. A possible long term solution 1466 // would involve sharing the docshell logic with the provider, along 1467 // with the dns lookup. 1468 // For now, in this specific case, we'll override the result's url 1469 // with the input value, and let it pass through to _loadURL(), and 1470 // finally to the docshell. 1471 // This also means that in some cases the heuristic result will show a 1472 // Visit entry, but the docshell will instead execute a search. It's a 1473 // rare case anyway, most likely to happen for enterprises customizing 1474 // the urifixup prefs. 1475 if ( 1476 lazy.UrlbarPrefs.get("browser.fixup.dns_first_for_single_words") && 1477 lazy.UrlbarUtils.looksLikeSingleWordHost(originalUntrimmedValue) 1478 ) { 1479 url = originalUntrimmedValue; 1480 } 1481 // Annotate if the untrimmed value contained a scheme, to later potentially 1482 // be upgraded by schemeless HTTPS-First. 1483 openParams.schemelessInput = this.#getSchemelessInput( 1484 originalUntrimmedValue 1485 ); 1486 } 1487 break; 1488 } 1489 case lazy.UrlbarUtils.RESULT_TYPE.KEYWORD: { 1490 // If this result comes from a bookmark keyword, let it inherit the 1491 // current document's principal, otherwise bookmarklets would break. 1492 openParams.allowInheritPrincipal = true; 1493 break; 1494 } 1495 case lazy.UrlbarUtils.RESULT_TYPE.TAB_SWITCH: { 1496 // Behaviour is reversed with SecondaryActions, default behaviour is to navigate 1497 // and button is provided to switch to tab. 1498 if ( 1499 this.hasAttribute("action-override") || 1500 (lazy.UrlbarPrefs.get("secondaryActions.switchToTab") && 1501 element?.dataset.action !== "tabswitch") 1502 ) { 1503 where = "current"; 1504 break; 1505 } 1506 1507 // Keep the searchMode for telemetry since handleRevert sets it to null. 1508 const searchMode = this.searchMode; 1509 this.handleRevert(); 1510 let prevTab = this.window.gBrowser.selectedTab; 1511 let loadOpts = { 1512 adoptIntoActiveWindow: lazy.UrlbarPrefs.get( 1513 "switchTabs.adoptIntoActiveWindow" 1514 ), 1515 }; 1516 1517 // We cache the search string because switching tab may clear it. 1518 let searchString = this._lastSearchString; 1519 this.controller.engagementEvent.record(event, { 1520 result, 1521 element, 1522 searchString, 1523 searchMode, 1524 selType: this.controller.engagementEvent.typeFromElement( 1525 result, 1526 element 1527 ), 1528 }); 1529 1530 let switched = this.window.switchToTabHavingURI( 1531 Services.io.newURI(url), 1532 true, 1533 loadOpts, 1534 lazy.UrlbarPrefs.get("switchTabs.searchAllContainers") && 1535 lazy.UrlbarProviderOpenTabs.isNonPrivateUserContextId( 1536 result.payload.userContextId 1537 ) 1538 ? result.payload.userContextId 1539 : null 1540 ); 1541 if (switched && prevTab.isEmpty) { 1542 this.window.gBrowser.removeTab(prevTab); 1543 } 1544 1545 if (switched && !this.isPrivate && !result.heuristic) { 1546 // We don't await for this, because a rejection should not interrupt 1547 // the load. Just reportError it. 1548 lazy.UrlbarUtils.addToInputHistory(url, searchString).catch( 1549 console.error 1550 ); 1551 } 1552 1553 // TODO (Bug 1865757): We should not show a "switchtotab" result for 1554 // tabs that are not currently open. Find out why tabs are not being 1555 // properly unregistered when they are being closed. 1556 if (!switched) { 1557 console.error(`Tried to switch to non-existent tab: ${url}`); 1558 lazy.UrlbarProviderOpenTabs.unregisterOpenTab( 1559 url, 1560 result.payload.userContextId, 1561 result.payload.tabGroup, 1562 this.isPrivate 1563 ); 1564 } 1565 1566 return; 1567 } 1568 case lazy.UrlbarUtils.RESULT_TYPE.SEARCH: { 1569 if (result.payload.providesSearchMode) { 1570 this.controller.engagementEvent.record(event, { 1571 result, 1572 element, 1573 searchString: this._lastSearchString, 1574 selType: this.controller.engagementEvent.typeFromElement( 1575 result, 1576 element 1577 ), 1578 }); 1579 this.maybeConfirmSearchModeFromResult({ 1580 result, 1581 checkValue: false, 1582 }); 1583 return; 1584 } 1585 1586 if ( 1587 !this.searchMode && 1588 result.heuristic && 1589 // If we asked the DNS earlier, avoid the post-facto check. 1590 !lazy.UrlbarPrefs.get("browser.fixup.dns_first_for_single_words") && 1591 // TODO (bug 1642623): for now there is no smart heuristic to skip the 1592 // DNS lookup, so any value above 0 will run it. 1593 lazy.UrlbarPrefs.get("dnsResolveSingleWordsAfterSearch") > 0 && 1594 this.window.gKeywordURIFixup && 1595 lazy.UrlbarUtils.looksLikeSingleWordHost(originalUntrimmedValue) 1596 ) { 1597 // When fixing a single word to a search, the docShell would also 1598 // query the DNS and if resolved ask the user whether they would 1599 // rather visit that as a host. On a positive answer, it adds the host 1600 // to the list that we use to make decisions. 1601 // Because we are directly asking for a search here, bypassing the 1602 // docShell, we need to do the same ourselves. 1603 // See also URIFixupChild.sys.mjs and keyword-uri-fixup. 1604 let fixupInfo = this._getURIFixupInfo(originalUntrimmedValue.trim()); 1605 if (fixupInfo) { 1606 this.window.gKeywordURIFixup.check( 1607 this.window.gBrowser.selectedBrowser, 1608 fixupInfo 1609 ); 1610 } 1611 } 1612 1613 if (result.payload.inPrivateWindow) { 1614 where = "window"; 1615 openParams.private = true; 1616 } 1617 1618 const actionDetails = { 1619 isSuggestion: !!result.payload.suggestion, 1620 isFormHistory: 1621 result.source == lazy.UrlbarUtils.RESULT_SOURCE.HISTORY, 1622 alias: result.payload.keyword, 1623 }; 1624 const engine = Services.search.getEngineByName(result.payload.engine); 1625 1626 if (where == "tab") { 1627 // The TabOpen event is fired synchronously so tabEvent.target 1628 // is guaranteed to be our new search tab. 1629 this.window.gBrowser.tabContainer.addEventListener( 1630 "TabOpen", 1631 tabEvent => 1632 this._recordSearch( 1633 engine, 1634 event, 1635 actionDetails, 1636 tabEvent.target.linkedBrowser 1637 ), 1638 { once: true } 1639 ); 1640 } else { 1641 this._recordSearch(engine, event, actionDetails); 1642 } 1643 1644 if (!result.payload.inPrivateWindow) { 1645 lazy.UrlbarUtils.addToFormHistory( 1646 this, 1647 result.payload.suggestion || result.payload.query, 1648 engine.name 1649 ).catch(console.error); 1650 } 1651 break; 1652 } 1653 case lazy.UrlbarUtils.RESULT_TYPE.TIP: { 1654 if (url) { 1655 break; 1656 } 1657 this.handleRevert(); 1658 this.controller.engagementEvent.record(event, { 1659 result, 1660 element, 1661 selType: "tip", 1662 searchString: this._lastSearchString, 1663 }); 1664 return; 1665 } 1666 case lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC: { 1667 if (!url) { 1668 // If we're not loading a URL, the engagement is done. First revert 1669 // and then record the engagement since providers expect the urlbar to 1670 // be reverted when they're notified of the engagement, but before 1671 // reverting, copy the search mode since it's nulled on revert. 1672 const { searchMode } = this; 1673 this.handleRevert(); 1674 this.controller.engagementEvent.record(event, { 1675 result, 1676 element, 1677 searchMode, 1678 searchString: this._lastSearchString, 1679 selType: this.controller.engagementEvent.typeFromElement( 1680 result, 1681 element 1682 ), 1683 }); 1684 return; 1685 } 1686 break; 1687 } 1688 case lazy.UrlbarUtils.RESULT_TYPE.OMNIBOX: { 1689 this.controller.engagementEvent.record(event, { 1690 result, 1691 element, 1692 selType: "extension", 1693 searchString: this._lastSearchString, 1694 }); 1695 1696 // The urlbar needs to revert to the loaded url when a command is 1697 // handled by the extension. 1698 this.handleRevert(); 1699 // We don't directly handle a load when an Omnibox API result is picked, 1700 // instead we forward the request to the WebExtension itself, because 1701 // the value may not even be a url. 1702 // We pass the keyword and content, that actually is the retrieved value 1703 // prefixed by the keyword. ExtensionSearchHandler uses this keyword 1704 // redundancy as a sanity check. 1705 lazy.ExtensionSearchHandler.handleInputEntered( 1706 result.payload.keyword, 1707 result.payload.content, 1708 where 1709 ); 1710 return; 1711 } 1712 case lazy.UrlbarUtils.RESULT_TYPE.RESTRICT: { 1713 this.handleRevert(); 1714 this.controller.engagementEvent.record(event, { 1715 result, 1716 element, 1717 searchString: this._lastSearchString, 1718 selType: this.controller.engagementEvent.typeFromElement( 1719 result, 1720 element 1721 ), 1722 }); 1723 this.maybeConfirmSearchModeFromResult({ 1724 result, 1725 checkValue: false, 1726 }); 1727 1728 return; 1729 } 1730 } 1731 1732 if (!url) { 1733 throw new Error(`Invalid url for result ${JSON.stringify(result)}`); 1734 } 1735 1736 // Record input history but only in non-private windows. 1737 if (!this.isPrivate) { 1738 let input; 1739 if (!result.heuristic) { 1740 input = this._lastSearchString; 1741 } else if (result.autofill?.type == "adaptive") { 1742 input = result.autofill.adaptiveHistoryInput; 1743 } 1744 // `input` may be an empty string, so do a strict comparison here. 1745 if (input !== undefined) { 1746 // We don't await for this, because a rejection should not interrupt 1747 // the load. Just reportError it. 1748 lazy.UrlbarUtils.addToInputHistory(url, input).catch(console.error); 1749 } 1750 } 1751 1752 this.controller.engagementEvent.startTrackingBounceEvent(browser, event, { 1753 result, 1754 element, 1755 searchString: this._lastSearchString, 1756 selType: this.controller.engagementEvent.typeFromElement(result, element), 1757 searchSource: this.getSearchSource(event), 1758 }); 1759 1760 this.controller.engagementEvent.record(event, { 1761 result, 1762 element, 1763 searchString: this._lastSearchString, 1764 selType: this.controller.engagementEvent.typeFromElement(result, element), 1765 searchSource: this.getSearchSource(event), 1766 }); 1767 1768 if (result.payload.sendAttributionRequest) { 1769 lazy.PartnerLinkAttribution.makeRequest({ 1770 targetURL: result.payload.url, 1771 source: this.#sapName, 1772 campaignID: Services.prefs.getStringPref( 1773 "browser.partnerlink.campaign.topsites" 1774 ), 1775 }); 1776 if (!this.isPrivate && result.providerName === "UrlbarProviderTopSites") { 1777 // The position is 1-based for telemetry 1778 const position = result.rowIndex + 1; 1779 Glean.contextualServicesTopsites.click[`urlbar_${position}`].add(1); 1780 } 1781 } 1782 1783 this._loadURL( 1784 url, 1785 event, 1786 where, 1787 openParams, 1788 { 1789 source: result.source, 1790 type: result.type, 1791 searchTerm: result.payload.suggestion ?? result.payload.query, 1792 }, 1793 browser 1794 ); 1795 } 1796 1797 /** 1798 * Called by the view when moving through results with the keyboard, and when 1799 * picking a result. This sets the input value to the value of the result and 1800 * invalidates the pageproxystate. It also sets the result that is associated 1801 * with the current input value. If you need to set this result but don't 1802 * want to also set the input value, then use setResultForCurrentValue. 1803 * 1804 * @param {object} options 1805 * Options. 1806 * @param {UrlbarResult} [options.result] 1807 * The result that was selected or picked, null if no result was selected. 1808 * @param {Event} [options.event] 1809 * The event that picked the result. 1810 * @param {string} [options.urlOverride] 1811 * Normally the URL is taken from `result.payload.url`, but if `urlOverride` 1812 * is specified, it's used instead. See `#getValueFromResult()`. 1813 * @param {Element} [options.element] 1814 * The element that was selected or picked, if available. For results that 1815 * have multiple selectable children, the value may be taken from a child 1816 * element rather than the result. See `#getValueFromResult()`. 1817 * @returns {boolean} 1818 * Whether the value has been canonized 1819 */ 1820 setValueFromResult({ 1821 result = null, 1822 event = null, 1823 urlOverride = null, 1824 element = null, 1825 } = {}) { 1826 // Usually this is set by a previous input event, but in certain cases, like 1827 // when opening Top Sites on a loaded page, it wouldn't happen. To avoid 1828 // confusing the user, we always enforce it when a result changes our value. 1829 this.setPageProxyState("invalid", true); 1830 1831 // A previous result may have previewed search mode. If we don't expect that 1832 // we might stay in a search mode of some kind, exit it now. 1833 if ( 1834 this.searchMode?.isPreview && 1835 !this.#providesSearchMode(result) && 1836 !this.view.oneOffSearchButtons?.selectedButton 1837 ) { 1838 this.searchMode = null; 1839 } 1840 1841 if (!result) { 1842 // This happens when there's no selection, for example when moving to the 1843 // one-offs search settings button, or to the input field when Top Sites 1844 // are shown; then we must reset the input value. 1845 // Note that for Top Sites the last search string would be empty, thus we 1846 // must restore the last text value. 1847 // Note that unselected autofill results will still arrive in this 1848 // function with a non-null `result`. They are handled below. 1849 this.value = this._lastSearchString || this._valueOnLastSearch; 1850 this.setResultForCurrentValue(result); 1851 return false; 1852 } 1853 1854 // We won't allow trimming when calling _setValue, since it makes too easy 1855 // for the user to wrongly transform `https` into `http`, for example by 1856 // picking a https://site/path_1 result and editing the path to path_2, 1857 // then we'd end up visiting http://site/path_2. 1858 // Trimming `http` would be ok, but there's other cases where it's unsafe, 1859 // like transforming a url into a search. 1860 // This choice also makes it easier to copy the full url of a result. 1861 1862 // We are supporting canonization of any result, in particular this allows 1863 // for single word search suggestions to be converted to a .com URL. 1864 // For autofilled results, the value to canonize is the user typed string, 1865 // not the autofilled value. 1866 let canonizedUrl = this._maybeCanonizeURL( 1867 event, 1868 result.autofill ? this._lastSearchString : this.value 1869 ); 1870 if (canonizedUrl) { 1871 this._setValue(canonizedUrl); 1872 1873 this.setResultForCurrentValue(result); 1874 return true; 1875 } 1876 1877 if (result.autofill) { 1878 this._autofillValue(result.autofill); 1879 } 1880 1881 if (this.#providesSearchMode(result)) { 1882 let enteredSearchMode; 1883 // Only preview search mode if the result is selected. 1884 if (this.view.resultIsSelected(result)) { 1885 // For ScotchBonnet, As Tab and Arrow Down/Up, Page Down/Up key are used 1886 // for selection of the urlbar results, keep the search mode as preview 1887 // mode if there are multiple results. 1888 // If ScotchBonnet is disabled, not starting a query means we will only 1889 // preview search mode. 1890 enteredSearchMode = this.maybeConfirmSearchModeFromResult({ 1891 result, 1892 checkValue: false, 1893 startQuery: 1894 lazy.UrlbarPrefs.get("scotchBonnet.enableOverride") && 1895 this.view.visibleResults.length == 1, 1896 }); 1897 } 1898 if (!enteredSearchMode) { 1899 this._setValue(this.#getValueFromResult(result), { 1900 actionType: this.#getActionTypeFromResult(result), 1901 }); 1902 this.searchMode = null; 1903 } 1904 this.setResultForCurrentValue(result); 1905 return false; 1906 } 1907 1908 if (!result.autofill) { 1909 let value = this.#getValueFromResult(result, { urlOverride, element }); 1910 this._setValue(value, { 1911 actionType: this.#getActionTypeFromResult(result), 1912 }); 1913 } 1914 1915 this.setResultForCurrentValue(result); 1916 1917 // Update placeholder selection and value to the current selected result to 1918 // prevent the on_selectionchange event to detect a "accent-character" 1919 // insertion. 1920 if (!result.autofill && this._autofillPlaceholder) { 1921 this._autofillPlaceholder.value = this.value; 1922 this._autofillPlaceholder.selectionStart = this.value.length; 1923 this._autofillPlaceholder.selectionEnd = this.value.length; 1924 } 1925 return false; 1926 } 1927 1928 /** 1929 * The input keeps track of the result associated with the current input 1930 * value. This result can be set by calling either setValueFromResult or this 1931 * method. Use this method when you need to set the result without also 1932 * setting the input value. This can be the case when either the selection is 1933 * cleared and no other result becomes selected, or when the result is the 1934 * heuristic and we don't want to modify the value the user is typing. 1935 * 1936 * @param {UrlbarResult} result 1937 * The result to associate with the current input value. 1938 */ 1939 setResultForCurrentValue(result) { 1940 this._resultForCurrentValue = result; 1941 } 1942 1943 /** 1944 * Called by the controller when the first result of a new search is received. 1945 * If it's an autofill result, then it may need to be autofilled, subject to a 1946 * few restrictions. 1947 * 1948 * @param {UrlbarResult} result 1949 * The first result. 1950 */ 1951 _autofillFirstResult(result) { 1952 if (!result.autofill) { 1953 return; 1954 } 1955 1956 let isPlaceholderSelected = 1957 this._autofillPlaceholder && 1958 this.selectionEnd == this._autofillPlaceholder.value.length && 1959 this.selectionStart == this._lastSearchString.length && 1960 this._autofillPlaceholder.value 1961 .toLocaleLowerCase() 1962 .startsWith(this._lastSearchString.toLocaleLowerCase()); 1963 1964 // Don't autofill if there's already a selection (with one caveat described 1965 // next) or the cursor isn't at the end of the input. But if there is a 1966 // selection and it's the autofill placeholder value, then do autofill. 1967 if ( 1968 !isPlaceholderSelected && 1969 !this._autofillIgnoresSelection && 1970 (this.selectionStart != this.selectionEnd || 1971 this.selectionEnd != this._lastSearchString.length) 1972 ) { 1973 return; 1974 } 1975 1976 this.setValueFromResult({ result }); 1977 } 1978 /** 1979 * Clears displayed autofill values and unsets the autofill placeholder. 1980 */ 1981 #clearAutofill() { 1982 if (!this._autofillPlaceholder) { 1983 return; 1984 } 1985 let currentSelectionStart = this.selectionStart; 1986 let currentSelectionEnd = this.selectionEnd; 1987 1988 // Overriding this value clears the selection. 1989 this.inputField.value = this.value.substring( 1990 0, 1991 this._autofillPlaceholder.selectionStart 1992 ); 1993 this._autofillPlaceholder = null; 1994 // Restore selection 1995 this.setSelectionRange(currentSelectionStart, currentSelectionEnd); 1996 } 1997 1998 /** 1999 * Invoked by the controller when the first result is received. 2000 * 2001 * @param {UrlbarResult} firstResult 2002 * The first result received. 2003 * @returns {boolean} 2004 * True if this method canceled the query and started a new one. False 2005 * otherwise. 2006 */ 2007 onFirstResult(firstResult) { 2008 // If the heuristic result has a keyword but isn't a keyword offer, we may 2009 // need to enter search mode. 2010 if ( 2011 firstResult.heuristic && 2012 firstResult.payload.keyword && 2013 !this.#providesSearchMode(firstResult) && 2014 this.maybeConfirmSearchModeFromResult({ 2015 result: firstResult, 2016 entry: "typed", 2017 checkValue: false, 2018 }) 2019 ) { 2020 return true; 2021 } 2022 2023 // To prevent selection flickering, we apply autofill on input through a 2024 // placeholder, without waiting for results. But, if the first result is 2025 // not an autofill one, the autofill prediction was wrong and we should 2026 // restore the original user typed string. 2027 if (firstResult.autofill) { 2028 this._autofillFirstResult(firstResult); 2029 } else if ( 2030 this._autofillPlaceholder && 2031 // Avoid clobbering added spaces (for token aliases, for example). 2032 !this.value.endsWith(" ") 2033 ) { 2034 this._autofillPlaceholder = null; 2035 this._setValue(this.userTypedValue); 2036 } 2037 2038 return false; 2039 } 2040 2041 /** 2042 * Starts a query based on the current input value. 2043 * 2044 * @param {object} [options] 2045 * Object options 2046 * @param {boolean} [options.allowAutofill] 2047 * Whether or not to allow providers to include autofill results. 2048 * @param {boolean} [options.autofillIgnoresSelection] 2049 * Normally we autofill only if the cursor is at the end of the string, 2050 * if this is set we'll autofill regardless of selection. 2051 * @param {string} [options.searchString] 2052 * The search string. If not given, the current input value is used. 2053 * Otherwise, the current input value must start with this value. 2054 * @param {boolean} [options.resetSearchState] 2055 * If this is the first search of a user interaction with the input, set 2056 * this to true (the default) so that search-related state from the previous 2057 * interaction doesn't interfere with the new interaction. Otherwise set it 2058 * to false so that state is maintained during a single interaction. The 2059 * intended use for this parameter is that it should be set to false when 2060 * this method is called due to input events. 2061 * @param {event} [options.event] 2062 * The user-generated event that triggered the query, if any. If given, we 2063 * will record engagement event telemetry for the query. 2064 */ 2065 startQuery({ 2066 allowAutofill, 2067 autofillIgnoresSelection = false, 2068 searchString, 2069 resetSearchState = true, 2070 event, 2071 } = {}) { 2072 if (!searchString) { 2073 searchString = 2074 this.getAttribute("pageproxystate") == "valid" ? "" : this.value; 2075 } else if (!this.value.startsWith(searchString)) { 2076 throw new Error("The current value doesn't start with the search string"); 2077 } 2078 2079 let queryContext = this.#makeQueryContext({ 2080 allowAutofill, 2081 event, 2082 searchString, 2083 }); 2084 2085 if (event) { 2086 this.controller.engagementEvent.start(event, queryContext, searchString); 2087 } 2088 2089 if (this._suppressStartQuery) { 2090 return; 2091 } 2092 2093 this._autofillIgnoresSelection = autofillIgnoresSelection; 2094 if (resetSearchState) { 2095 this._resetSearchState(); 2096 } 2097 2098 if (this.searchMode) { 2099 this.confirmSearchMode(); 2100 } 2101 2102 this._lastSearchString = searchString; 2103 this._valueOnLastSearch = this.value; 2104 2105 // TODO (Bug 1522902): This promise is necessary for tests, because some 2106 // tests are not listening for completion when starting a query through 2107 // other methods than startQuery (input events for example). 2108 this.lastQueryContextPromise = this.controller.startQuery(queryContext); 2109 } 2110 2111 /** 2112 * Sets the input's value, starts a search, and opens the view. 2113 * 2114 * @param {string} value 2115 * The input's value will be set to this value, and the search will 2116 * use it as its query. 2117 * @param {object} [options] 2118 * Object options 2119 * @param {nsISearchEngine} [options.searchEngine] 2120 * Search engine to use when the search is using a known alias. 2121 * @param {UrlbarUtils.SEARCH_MODE_ENTRY} [options.searchModeEntry] 2122 * If provided, we will record this parameter as the search mode entry point 2123 * in Telemetry. Consumers should provide this if they expect their call 2124 * to enter search mode. 2125 * @param {boolean} [options.focus] 2126 * If true, the urlbar will be focused. If false, the focus will remain 2127 * unchanged. 2128 * @param {boolean} [options.startQuery] 2129 * If true, start query to show urlbar result by fireing input event. If 2130 * false, not fire the event. 2131 */ 2132 search(value, options = {}) { 2133 let { searchEngine, searchModeEntry, startQuery = true } = options; 2134 if (options.focus ?? true) { 2135 this.focus(); 2136 } 2137 let trimmedValue = value.trim(); 2138 let end = trimmedValue.search(lazy.UrlUtils.REGEXP_SPACES); 2139 let firstToken = end == -1 ? trimmedValue : trimmedValue.substring(0, end); 2140 // Enter search mode if the string starts with a restriction token. 2141 let searchMode = this.searchModeForToken(firstToken); 2142 let firstTokenIsRestriction = !!searchMode; 2143 if (!searchMode && searchEngine) { 2144 searchMode = { engineName: searchEngine.name }; 2145 firstTokenIsRestriction = searchEngine.aliases.includes(firstToken); 2146 } 2147 2148 if (searchMode) { 2149 searchMode.entry = searchModeEntry; 2150 this.searchMode = searchMode; 2151 if (firstTokenIsRestriction) { 2152 // Remove the restriction token/alias from the string to be searched for 2153 // in search mode. 2154 value = value.replace(firstToken, ""); 2155 } 2156 if (lazy.UrlUtils.REGEXP_SPACES.test(value[0])) { 2157 // If there was a trailing space after the restriction token/alias, 2158 // remove it. 2159 value = value.slice(1); 2160 } 2161 } else if ( 2162 Object.values(lazy.UrlbarTokenizer.RESTRICT).includes(firstToken) 2163 ) { 2164 this.searchMode = null; 2165 // If the entire value is a restricted token, append a space. 2166 if (Object.values(lazy.UrlbarTokenizer.RESTRICT).includes(value)) { 2167 value += " "; 2168 } 2169 } 2170 this.inputField.value = value; 2171 // Avoid selecting the text if this method is called twice in a row. 2172 this.selectionStart = -1; 2173 2174 if (startQuery) { 2175 // Note: proper IME Composition handling depends on the fact this generates 2176 // an input event, rather than directly invoking the controller; everything 2177 // goes through _on_input, that will properly skip the search until the 2178 // composition is committed. _on_input also skips the search when it's the 2179 // same as the previous search, but we want to allow consecutive searches 2180 // with the same string. So clear _lastSearchString first. 2181 this._lastSearchString = ""; 2182 let event = new UIEvent("input", { 2183 bubbles: true, 2184 cancelable: false, 2185 view: this.window, 2186 detail: 0, 2187 }); 2188 this.inputField.dispatchEvent(event); 2189 } 2190 } 2191 2192 /** 2193 * Returns a search mode object if a token should enter search mode when 2194 * typed. This does not handle engine aliases. 2195 * 2196 * @param {Values<typeof lazy.UrlbarTokenizer.RESTRICT>} token 2197 * A restriction token to convert to search mode. 2198 * @returns {?object} 2199 * A search mode object. Null if search mode should not be entered. See 2200 * setSearchMode documentation for details. 2201 */ 2202 searchModeForToken(token) { 2203 if (token == lazy.UrlbarTokenizer.RESTRICT.SEARCH) { 2204 return { 2205 engineName: lazy.UrlbarSearchUtils.getDefaultEngine(this.isPrivate) 2206 ?.name, 2207 }; 2208 } 2209 2210 let mode = 2211 this.#isAddressbar && 2212 lazy.UrlbarUtils.LOCAL_SEARCH_MODES.find(m => m.restrict == token); 2213 if (mode) { 2214 // Return a copy so callers don't modify the object in LOCAL_SEARCH_MODES. 2215 return { ...mode }; 2216 } 2217 2218 return null; 2219 } 2220 2221 /** 2222 * Opens a search page if the value is non-empty, otherwise opens the 2223 * search engine homepage (searchform). 2224 * 2225 * @param {string} value 2226 * @param {object} options 2227 * @param {nsISearchEngine} options.searchEngine 2228 */ 2229 openEngineHomePage(value, { searchEngine }) { 2230 if (!searchEngine) { 2231 console.warn("No searchEngine parameter"); 2232 return; 2233 } 2234 2235 let trimmedValue = value.trim(); 2236 let url; 2237 if (trimmedValue) { 2238 url = searchEngine.getSubmission(trimmedValue, null).uri.spec; 2239 // TODO: record SAP telemetry, see Bug 1961789. 2240 } else { 2241 url = searchEngine.searchForm; 2242 lazy.BrowserSearchTelemetry.recordSearchForm(searchEngine, this.#sapName); 2243 } 2244 2245 this._lastSearchString = ""; 2246 if (this.#isAddressbar) { 2247 this.inputField.value = url; 2248 } 2249 this.selectionStart = -1; 2250 2251 this.window.openTrustedLinkIn(url, "current"); 2252 } 2253 2254 /** 2255 * Focus without the focus styles. 2256 * This is used by Activity Stream and about:privatebrowsing for search hand-off. 2257 */ 2258 setHiddenFocus() { 2259 this._hideFocus = true; 2260 if (this.focused) { 2261 this.removeAttribute("focused"); 2262 } else { 2263 this.focus(); 2264 } 2265 } 2266 2267 /** 2268 * Restore focus styles. 2269 * This is used by Activity Stream and about:privatebrowsing for search hand-off. 2270 * 2271 * @param {boolean} forceSuppressFocusBorder 2272 * Set true to suppress-focus-border attribute if this flag is true. 2273 */ 2274 removeHiddenFocus(forceSuppressFocusBorder = false) { 2275 this._hideFocus = false; 2276 if (this.focused) { 2277 this.toggleAttribute("focused", true); 2278 2279 if (forceSuppressFocusBorder) { 2280 this.toggleAttribute("suppress-focus-border", true); 2281 } 2282 } 2283 } 2284 2285 /** 2286 * Addressbar: Gets the search mode for a specific browser instance. 2287 * Searchbar: Gets the window-global search mode. 2288 * 2289 * @param {MozBrowser} browser 2290 * The search mode for this browser will be returned. 2291 * Pass the selected browser for the searchbar. 2292 * @param {boolean} [confirmedOnly] 2293 * Normally, if the browser has both preview and confirmed modes, preview 2294 * mode will be returned since it takes precedence. If this argument is 2295 * true, then only confirmed search mode will be returned, or null if 2296 * search mode hasn't been confirmed. 2297 * @returns {?object} 2298 * A search mode object or null if the browser/window is not in search mode. 2299 * See setSearchMode documentation. 2300 */ 2301 getSearchMode(browser, confirmedOnly = false) { 2302 let modes = this.#getSearchModesObject(browser); 2303 2304 // Return copies so that callers don't modify the stored values. 2305 if (!confirmedOnly && modes.preview) { 2306 return { ...modes.preview }; 2307 } 2308 if (modes.confirmed) { 2309 return { ...modes.confirmed }; 2310 } 2311 return null; 2312 } 2313 2314 /** 2315 * Addressbar: Sets the search mode for a specific browser instance. 2316 * Searchbar: Sets the window-global search mode. 2317 * If the given browser is selected, then this will also enter search mode. 2318 * 2319 * @param {object} searchMode 2320 * A search mode object. 2321 * @param {string} searchMode.engineName 2322 * The name of the search engine to restrict to. 2323 * @param {UrlbarUtils.RESULT_SOURCE} searchMode.source 2324 * A result source to restrict to. 2325 * @param {string} searchMode.entry 2326 * How search mode was entered. This is recorded in event telemetry. One of 2327 * the values in UrlbarUtils.SEARCH_MODE_ENTRY. 2328 * @param {boolean} [searchMode.isPreview] 2329 * If true, we will preview search mode. Search mode preview does not record 2330 * telemetry and has slighly different UI behavior. The preview is exited in 2331 * favor of full search mode when a query is executed. False should be 2332 * passed if the caller needs to enter search mode but expects it will not 2333 * be interacted with right away. Defaults to true. 2334 * @param {MozBrowser} browser 2335 * The browser for which to set search mode. 2336 * Pass the selected browser for the searchbar. 2337 */ 2338 async setSearchMode(searchMode, browser) { 2339 let currentSearchMode = this.getSearchMode(browser); 2340 let areSearchModesSame = 2341 (!currentSearchMode && !searchMode) || 2342 lazy.ObjectUtils.deepEqual(currentSearchMode, searchMode); 2343 2344 // Exit search mode if the passed-in engine is invalid or hidden. 2345 let engine; 2346 if (searchMode?.engineName) { 2347 if (!Services.search.isInitialized) { 2348 await Services.search.init(); 2349 } 2350 engine = Services.search.getEngineByName(searchMode.engineName); 2351 if (!engine || engine.hidden) { 2352 searchMode = null; 2353 } 2354 } 2355 2356 let { 2357 engineName, 2358 source, 2359 entry, 2360 restrictType, 2361 isPreview = true, 2362 } = searchMode || {}; 2363 2364 searchMode = null; 2365 2366 if (engineName) { 2367 searchMode = { 2368 engineName, 2369 isGeneralPurposeEngine: engine.isGeneralPurposeEngine, 2370 }; 2371 if (source) { 2372 searchMode.source = source; 2373 } else if (searchMode.isGeneralPurposeEngine) { 2374 // History results for general-purpose search engines are often not 2375 // useful, so we hide them in search mode. See bug 1658646 for 2376 // discussion. 2377 searchMode.source = lazy.UrlbarUtils.RESULT_SOURCE.SEARCH; 2378 } 2379 } else if (source) { 2380 let sourceName = lazy.UrlbarUtils.getResultSourceName(source); 2381 if (sourceName) { 2382 searchMode = { source }; 2383 } else { 2384 console.error(`Unrecognized source: ${source}`); 2385 } 2386 } 2387 2388 let modes = this.#getSearchModesObject(browser); 2389 2390 if (searchMode) { 2391 searchMode.isPreview = isPreview; 2392 if (lazy.UrlbarUtils.SEARCH_MODE_ENTRY.has(entry)) { 2393 searchMode.entry = entry; 2394 } else { 2395 // If we see this value showing up in telemetry, we should review 2396 // search mode's entry points. 2397 searchMode.entry = "other"; 2398 } 2399 2400 if (!searchMode.isPreview) { 2401 modes.confirmed = searchMode; 2402 delete modes.preview; 2403 } else { 2404 modes.preview = searchMode; 2405 } 2406 } else { 2407 delete modes.preview; 2408 delete modes.confirmed; 2409 } 2410 2411 if (restrictType) { 2412 searchMode.restrictType = restrictType; 2413 } 2414 2415 // Enter search mode if the browser is selected. 2416 if (browser == this.window.gBrowser.selectedBrowser) { 2417 this._updateSearchModeUI(searchMode); 2418 if (searchMode) { 2419 // Set userTypedValue to the query string so that it's properly restored 2420 // when switching back to the current tab and across sessions. 2421 this.userTypedValue = this.untrimmedValue; 2422 this.valueIsTyped = true; 2423 if (!searchMode.isPreview && !areSearchModesSame) { 2424 try { 2425 lazy.BrowserSearchTelemetry.recordSearchMode(searchMode); 2426 } catch (ex) { 2427 console.error(ex); 2428 } 2429 } 2430 } 2431 } 2432 } 2433 2434 /** 2435 * @typedef {object} SearchModesObject 2436 * 2437 * @property {object} [preview] preview search mode 2438 * @property {object} [confirmed] confirmed search mode 2439 */ 2440 2441 /** 2442 * @type {SearchModesObject|undefined} 2443 * 2444 * The (lazily initialized) search mode object for the searchbar. 2445 * This is needed because the searchbar has one search mode per window that 2446 * shouldn't change when switching tabs. For the address bar, the search mode 2447 * is stored per browser in #browserStates and this is always undefined. 2448 */ 2449 #searchbarSearchModes; 2450 2451 /** 2452 * Addressbar: Gets the search modes object for a specific browser instance. 2453 * Searchbar: Gets the window-global search modes object. 2454 * 2455 * @param {MozBrowser} browser 2456 * The browser to get the search modes object for. 2457 * Pass the selected browser for the searchbar. 2458 * @returns {SearchModesObject} 2459 */ 2460 #getSearchModesObject(browser) { 2461 if (!this.#isAddressbar) { 2462 // The passed browser doesn't matter here, but it does in setSearchMode. 2463 this.#searchbarSearchModes ??= {}; 2464 return this.#searchbarSearchModes; 2465 } 2466 2467 let state = this.getBrowserState(browser); 2468 state.searchModes ??= {}; 2469 return state.searchModes; 2470 } 2471 2472 /** 2473 * Restores the current browser search mode from a previously stored state. 2474 */ 2475 restoreSearchModeState() { 2476 this.searchMode = this.#getSearchModesObject( 2477 this.window.gBrowser.selectedBrowser 2478 ).confirmed; 2479 } 2480 2481 /** 2482 * Enters search mode with the default engine. 2483 */ 2484 searchModeShortcut() { 2485 // We restrict to search results when entering search mode from this 2486 // shortcut to honor historical behaviour. 2487 this.searchMode = { 2488 source: lazy.UrlbarUtils.RESULT_SOURCE.SEARCH, 2489 engineName: lazy.UrlbarSearchUtils.getDefaultEngine(this.isPrivate)?.name, 2490 entry: "shortcut", 2491 }; 2492 // The searchMode setter clears the input if pageproxystate is valid, so 2493 // we know at this point this.value will either be blank or the user's 2494 // typed string. 2495 this.search(this.value); 2496 this.select(); 2497 } 2498 2499 /** 2500 * Confirms the current search mode. 2501 */ 2502 confirmSearchMode() { 2503 let searchMode = this.searchMode; 2504 if (searchMode?.isPreview) { 2505 searchMode.isPreview = false; 2506 this.searchMode = searchMode; 2507 2508 // Unselect the one-off search button to ensure UI consistency. 2509 if (this.view.oneOffSearchButtons) { 2510 this.view.oneOffSearchButtons.selectedButton = null; 2511 } 2512 } 2513 } 2514 2515 // Getters and Setters below. 2516 2517 get editor() { 2518 return this.inputField.editor; 2519 } 2520 2521 get focused() { 2522 return this.document.activeElement == this.inputField; 2523 } 2524 2525 get goButton() { 2526 return this.querySelector(".urlbar-go-button"); 2527 } 2528 2529 get value() { 2530 return this.inputField.value; 2531 } 2532 2533 set value(val) { 2534 this._setValue(val, { allowTrim: true }); 2535 } 2536 2537 get untrimmedValue() { 2538 return this._untrimmedValue; 2539 } 2540 2541 get userTypedValue() { 2542 return this.#isAddressbar 2543 ? this.window.gBrowser.userTypedValue 2544 : this._userTypedValue; 2545 } 2546 2547 set userTypedValue(val) { 2548 if (this.#isAddressbar) { 2549 this.window.gBrowser.userTypedValue = val; 2550 } else { 2551 this._userTypedValue = val; 2552 } 2553 } 2554 2555 get lastSearchString() { 2556 return this._lastSearchString; 2557 } 2558 2559 get searchMode() { 2560 if (!this.window.gBrowser) { 2561 // This only happens before DOMContentLoaded. 2562 return null; 2563 } 2564 return this.getSearchMode(this.window.gBrowser.selectedBrowser); 2565 } 2566 2567 set searchMode(searchMode) { 2568 this.setSearchMode(searchMode, this.window.gBrowser.selectedBrowser); 2569 this.searchModeSwitcher?.onSearchModeChanged(); 2570 lazy.UrlbarSearchTermsPersistence.onSearchModeChanged(this.window); 2571 } 2572 2573 getBrowserState(browser) { 2574 let state = this.#browserStates.get(browser); 2575 if (!state) { 2576 state = {}; 2577 this.#browserStates.set(browser, state); 2578 } 2579 return state; 2580 } 2581 2582 async #updateLayoutBreakout() { 2583 if (!this.#allowBreakout) { 2584 return; 2585 } 2586 if (this.document.fullscreenElement) { 2587 // Toolbars are hidden in DOM fullscreen mode, so we can't get proper 2588 // layout information and need to retry after leaving that mode. 2589 this.window.addEventListener( 2590 "fullscreen", 2591 () => { 2592 this.#updateLayoutBreakout(); 2593 }, 2594 { once: true } 2595 ); 2596 return; 2597 } 2598 await this.#updateLayoutBreakoutDimensions(); 2599 } 2600 2601 startLayoutExtend() { 2602 if (!this.#allowBreakout || this.hasAttribute("breakout-extend")) { 2603 // Do not expand if the Urlbar does not support being expanded or it is 2604 // already expanded. 2605 return; 2606 } 2607 if (!this.view.isOpen) { 2608 return; 2609 } 2610 2611 this.#updateTextboxPosition(); 2612 2613 this.setAttribute("breakout-extend", "true"); 2614 2615 // Enable the animation only after the first extend call to ensure it 2616 // doesn't run when opening a new window. 2617 if (!this.hasAttribute("breakout-extend-animate")) { 2618 this.window.promiseDocumentFlushed(() => { 2619 this.window.requestAnimationFrame(() => { 2620 this.setAttribute("breakout-extend-animate", "true"); 2621 }); 2622 }); 2623 } 2624 } 2625 2626 endLayoutExtend() { 2627 // If reduce motion is enabled, we want to collapse the Urlbar here so the 2628 // user sees only sees two states: not expanded, and expanded with the view 2629 // open. 2630 if (!this.hasAttribute("breakout-extend") || this.view.isOpen) { 2631 return; 2632 } 2633 2634 this.removeAttribute("breakout-extend"); 2635 this.#updateTextboxPosition(); 2636 } 2637 2638 /** 2639 * Updates the user interface to indicate whether the URI in the address bar 2640 * is different than the loaded page, because it's being edited or because a 2641 * search result is currently selected and is displayed in the location bar. 2642 * 2643 * @param {string} state 2644 * The string "valid" indicates that the security indicators and other 2645 * related user interface elments should be shown because the URI in 2646 * the location bar matches the loaded page. The string "invalid" 2647 * indicates that the URI in the location bar is different than the 2648 * loaded page. 2649 * @param {boolean} [updatePopupNotifications] 2650 * Indicates whether we should update the PopupNotifications 2651 * visibility due to this change, otherwise avoid doing so as it is 2652 * being handled somewhere else. 2653 * @param {boolean} [forceUnifiedSearchButtonAvailable] 2654 * If this parameter is true, force to make Unified Search Button available. 2655 * Otherwise, the availability will be depedent on the proxy state. 2656 * Default value is false. 2657 */ 2658 setPageProxyState( 2659 state, 2660 updatePopupNotifications, 2661 forceUnifiedSearchButtonAvailable = false 2662 ) { 2663 let prevState = this.getAttribute("pageproxystate"); 2664 2665 this.setAttribute("pageproxystate", state); 2666 this._inputContainer.setAttribute("pageproxystate", state); 2667 this._identityBox?.setAttribute("pageproxystate", state); 2668 this.setUnifiedSearchButtonAvailability( 2669 forceUnifiedSearchButtonAvailable || state == "invalid" 2670 ); 2671 2672 if (state == "valid") { 2673 this._lastValidURLStr = this.value; 2674 } 2675 2676 if ( 2677 updatePopupNotifications && 2678 prevState != state && 2679 this.window.UpdatePopupNotificationsVisibility 2680 ) { 2681 this.window.UpdatePopupNotificationsVisibility(); 2682 } 2683 } 2684 2685 /** 2686 * When switching tabs quickly, TabSelect sometimes happens before 2687 * _adjustFocusAfterTabSwitch and due to the focus still being on the old 2688 * tab, we end up flickering the results pane briefly. 2689 */ 2690 afterTabSwitchFocusChange() { 2691 this._gotFocusChange = true; 2692 this._afterTabSelectAndFocusChange(); 2693 } 2694 2695 /** 2696 * Confirms search mode and starts a new search if appropriate for the given 2697 * result. See also _searchModeForResult. 2698 * 2699 * @param {object} options 2700 * Options object. 2701 * @param {string} [options.entry] 2702 * If provided, this will be recorded as the entry point into search mode. 2703 * See setSearchMode documentation for details. 2704 * @param {UrlbarResult} [options.result] 2705 * The result to confirm. Defaults to the currently selected result. 2706 * @param {boolean} [options.checkValue] 2707 * If true, the trimmed input value must equal the result's keyword in order 2708 * to enter search mode. 2709 * @param {boolean} [options.startQuery] 2710 * If true, start a query after entering search mode. Defaults to true. 2711 * @returns {boolean} 2712 * True if we entered search mode and false if not. 2713 */ 2714 maybeConfirmSearchModeFromResult({ 2715 entry, 2716 result = this._resultForCurrentValue, 2717 checkValue = true, 2718 startQuery = true, 2719 }) { 2720 if ( 2721 !result || 2722 (checkValue && 2723 this.value.trim() != result.payload.keyword?.trim() && 2724 this.value.trim() != result.payload.autofillKeyword?.trim()) 2725 ) { 2726 return false; 2727 } 2728 2729 let searchMode = this._searchModeForResult(result, entry); 2730 if (!searchMode) { 2731 return false; 2732 } 2733 2734 this.searchMode = searchMode; 2735 2736 let value = result.payload.query?.trimStart() || ""; 2737 this._setValue(value); 2738 2739 if (startQuery) { 2740 this.startQuery({ allowAutofill: false }); 2741 } 2742 2743 return true; 2744 } 2745 2746 observe(subject, topic, data) { 2747 switch (topic) { 2748 case lazy.SearchUtils.TOPIC_ENGINE_MODIFIED: { 2749 let engine = subject.QueryInterface(Ci.nsISearchEngine); 2750 switch (data) { 2751 case lazy.SearchUtils.MODIFIED_TYPE.CHANGED: 2752 case lazy.SearchUtils.MODIFIED_TYPE.REMOVED: { 2753 let searchMode = this.searchMode; 2754 if (searchMode?.engineName == engine.name) { 2755 // Exit search mode if the current search mode engine was removed. 2756 this.searchMode = searchMode; 2757 } 2758 break; 2759 } 2760 case lazy.SearchUtils.MODIFIED_TYPE.DEFAULT: 2761 if (!this.isPrivate) { 2762 this._updatePlaceholder(engine.name); 2763 } 2764 break; 2765 case lazy.SearchUtils.MODIFIED_TYPE.DEFAULT_PRIVATE: 2766 if (this.isPrivate) { 2767 this._updatePlaceholder(engine.name); 2768 } 2769 break; 2770 } 2771 break; 2772 } 2773 } 2774 } 2775 2776 /** 2777 * Get search source. 2778 * 2779 * @param {Event} event 2780 * The event that triggered this query. 2781 * @returns {string} 2782 * The source name. 2783 */ 2784 getSearchSource(event) { 2785 if (this.#isAddressbar) { 2786 if (this._isHandoffSession) { 2787 return "urlbar-handoff"; 2788 } 2789 2790 const isOneOff = 2791 this.view.oneOffSearchButtons?.eventTargetIsAOneOff(event); 2792 if (this.searchMode && !isOneOff) { 2793 // Without checking !isOneOff, we might record the string 2794 // oneoff_urlbar-searchmode in the SEARCH_COUNTS probe (in addition to 2795 // oneoff_urlbar and oneoff_searchbar). The extra information is not 2796 // necessary; the intent is the same regardless of whether the user is 2797 // in search mode when they do a key-modified click/enter on a one-off. 2798 return "urlbar-searchmode"; 2799 } 2800 2801 let state = this.getBrowserState(this.window.gBrowser.selectedBrowser); 2802 if (state.persist?.searchTerms && !isOneOff) { 2803 // Normally, we use state.persist.shouldPersist to check if search terms 2804 // persisted. However when the user modifies the search term, the boolean 2805 // will become false. Thus, we check the presence of the search terms to 2806 // know whether or not search terms ever persisted in the address bar. 2807 return "urlbar-persisted"; 2808 } 2809 } 2810 return this.#sapName; 2811 } 2812 2813 // Private methods below. 2814 2815 /* 2816 * Actions can have several buttons in the same result where not all 2817 * will provide a searchMode so check the currently selected button 2818 * in that case. 2819 */ 2820 #providesSearchMode(result) { 2821 if (!result) { 2822 return false; 2823 } 2824 if ( 2825 this.view.selectedElement && 2826 result.providerName == lazy.UrlbarProviderGlobalActions.name 2827 ) { 2828 return this.view.selectedElement.dataset.providesSearchmode == "true"; 2829 } 2830 return result.payload.providesSearchMode; 2831 } 2832 2833 _addObservers() { 2834 this._observer ??= { 2835 observe: this.observe.bind(this), 2836 QueryInterface: ChromeUtils.generateQI([ 2837 "nsIObserver", 2838 "nsISupportsWeakReference", 2839 ]), 2840 }; 2841 Services.obs.addObserver( 2842 this._observer, 2843 lazy.SearchUtils.TOPIC_ENGINE_MODIFIED, 2844 true 2845 ); 2846 } 2847 2848 _removeObservers() { 2849 if (this._observer) { 2850 Services.obs.removeObserver( 2851 this._observer, 2852 lazy.SearchUtils.TOPIC_ENGINE_MODIFIED 2853 ); 2854 this._observer = null; 2855 } 2856 } 2857 2858 _getURIFixupInfo(searchString) { 2859 let flags = 2860 Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS | 2861 Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP; 2862 if (this.isPrivate) { 2863 flags |= Ci.nsIURIFixup.FIXUP_FLAG_PRIVATE_CONTEXT; 2864 } 2865 try { 2866 return Services.uriFixup.getFixupURIInfo(searchString, flags); 2867 } catch (ex) { 2868 console.error( 2869 `An error occured while trying to fixup "${searchString}"`, 2870 ex 2871 ); 2872 } 2873 return null; 2874 } 2875 2876 _afterTabSelectAndFocusChange() { 2877 // We must have seen both events to proceed safely. 2878 if (!this._gotFocusChange || !this._gotTabSelect) { 2879 return; 2880 } 2881 this._gotFocusChange = this._gotTabSelect = false; 2882 2883 this.formatValue(); 2884 this._resetSearchState(); 2885 2886 // We don't use the original TabSelect event because caching it causes 2887 // leaks on MacOS. 2888 const event = new CustomEvent("tabswitch"); 2889 // If the urlbar is focused after a tab switch, record a potential 2890 // engagement event. When switching from a focused to a non-focused urlbar, 2891 // the blur event would record the abandonment. When switching from an 2892 // unfocused to a focused urlbar, there should be no search session ongoing, 2893 // so this will be a no-op. 2894 if (this.focused) { 2895 this.controller.engagementEvent.record(event, { 2896 searchString: this._lastSearchString, 2897 searchSource: this.getSearchSource(event), 2898 }); 2899 } 2900 2901 // Switching tabs doesn't always change urlbar focus, so we must try to 2902 // reopen here too, not just on focus. 2903 if (this.view.autoOpen({ event })) { 2904 return; 2905 } 2906 // The input may retain focus when switching tabs in which case we 2907 // need to close the view and search mode switcher popup explicitly. 2908 this.searchModeSwitcher.closePanel(); 2909 this.view.close(); 2910 } 2911 2912 #updateTextboxPosition() { 2913 if (!this.view.isOpen) { 2914 this.style.top = ""; 2915 return; 2916 } 2917 this.style.top = px( 2918 this.parentNode.getBoxQuads({ 2919 ignoreTransforms: true, 2920 flush: false, 2921 })[0].p1.y 2922 ); 2923 } 2924 2925 #updateTextboxPositionNextFrame() { 2926 if (!this.hasAttribute("breakout")) { 2927 return; 2928 } 2929 // Allow for any layout changes to take place (e.g. when the menubar becomes 2930 // inactive) before re-measuring to position the textbox 2931 this.window.requestAnimationFrame(() => { 2932 this.window.requestAnimationFrame(() => { 2933 this.#updateTextboxPosition(); 2934 }); 2935 }); 2936 } 2937 2938 #stopBreakout() { 2939 this.removeAttribute("breakout"); 2940 this.parentNode.removeAttribute("breakout"); 2941 this.style.top = ""; 2942 try { 2943 this.hidePopover(); 2944 } catch (ex) { 2945 // No big deal if not a popover already. 2946 } 2947 this._layoutBreakoutUpdateKey = {}; 2948 } 2949 2950 incrementBreakoutBlockerCount() { 2951 this.#breakoutBlockerCount++; 2952 if (this.#breakoutBlockerCount == 1) { 2953 this.#stopBreakout(); 2954 } 2955 } 2956 2957 decrementBreakoutBlockerCount() { 2958 if (this.#breakoutBlockerCount > 0) { 2959 this.#breakoutBlockerCount--; 2960 } 2961 if (this.#breakoutBlockerCount === 0) { 2962 this.#updateLayoutBreakout(); 2963 } 2964 } 2965 2966 async #updateLayoutBreakoutDimensions() { 2967 this.#stopBreakout(); 2968 2969 // When this method gets called a second time before the first call 2970 // finishes, we need to disregard the first one. 2971 let updateKey = {}; 2972 this._layoutBreakoutUpdateKey = updateKey; 2973 await this.window.promiseDocumentFlushed(() => {}); 2974 await new Promise(resolve => { 2975 this.window.requestAnimationFrame(() => { 2976 if (this._layoutBreakoutUpdateKey != updateKey || !this.isConnected) { 2977 return; 2978 } 2979 2980 this.parentNode.style.setProperty( 2981 "--urlbar-container-height", 2982 px(getBoundsWithoutFlushing(this.parentNode).height) 2983 ); 2984 this.style.setProperty( 2985 "--urlbar-height", 2986 px(getBoundsWithoutFlushing(this).height) 2987 ); 2988 2989 if (this.#breakoutBlockerCount) { 2990 return; 2991 } 2992 2993 this.setAttribute("breakout", "true"); 2994 this.parentNode.setAttribute("breakout", "true"); 2995 this.showPopover(); 2996 this.#updateTextboxPosition(); 2997 2998 resolve(); 2999 }); 3000 }); 3001 } 3002 3003 /** 3004 * Sets the input field value. 3005 * 3006 * @param {string} val The new value to set. 3007 * @param {object} [options] Options for setting. 3008 * @param {boolean} [options.allowTrim] Whether the value can be trimmed. 3009 * @param {string} [options.untrimmedValue] Override for this._untrimmedValue. 3010 * @param {boolean} [options.valueIsTyped] Override for this.valueIsTyped. 3011 * @param {string} [options.actionType] Value for the `actiontype` attribute. 3012 * 3013 * @returns {string} The set value. 3014 */ 3015 _setValue( 3016 val, 3017 { 3018 allowTrim = false, 3019 untrimmedValue = null, 3020 valueIsTyped = false, 3021 actionType = undefined, 3022 } = {} 3023 ) { 3024 // Don't expose internal about:reader URLs to the user. 3025 let originalUrl = lazy.ReaderMode.getOriginalUrlObjectForDisplay(val); 3026 if (originalUrl) { 3027 val = originalUrl.displaySpec; 3028 } 3029 this._untrimmedValue = untrimmedValue ?? val; 3030 this._protocolIsTrimmed = false; 3031 if (allowTrim) { 3032 let oldVal = val; 3033 val = this._trimValue(val); 3034 this._protocolIsTrimmed = 3035 oldVal.startsWith(lazy.BrowserUIUtils.trimURLProtocol) && 3036 !val.startsWith(lazy.BrowserUIUtils.trimURLProtocol); 3037 } 3038 3039 this.valueIsTyped = valueIsTyped; 3040 this._resultForCurrentValue = null; 3041 this.inputField.value = val; 3042 this.formatValue(); 3043 3044 if (actionType !== undefined) { 3045 this.setAttribute("actiontype", actionType); 3046 } else { 3047 this.removeAttribute("actiontype"); 3048 } 3049 3050 // Dispatch ValueChange event for accessibility. 3051 let event = this.document.createEvent("Events"); 3052 event.initEvent("ValueChange", true, true); 3053 this.inputField.dispatchEvent(event); 3054 3055 return val; 3056 } 3057 3058 /** 3059 * Extracts a input value from a UrlbarResult, used when filling the input 3060 * field on selecting a result. 3061 * 3062 * Some examples: 3063 * - If the result is a bookmark keyword or dynamic, the value will be 3064 * its `input` property. 3065 * - If the result is search, the value may be `keyword` combined with 3066 * `suggestion` or `query`. 3067 * - If the result is WebExtension Omnibox, the value will be extracted 3068 * from `content`. 3069 * - For results returning URLs the value may be `urlOverride` or `url`. 3070 * 3071 * @param {UrlbarResult} result 3072 * The result to extract the value from. 3073 * @param {object} options 3074 * Options object. 3075 * @param {string} [options.urlOverride] 3076 * For results normally returning a url string, this allows to override 3077 * it. A blank string may passed-in to clear the input. 3078 * @param {HTMLElement} [options.element] 3079 * The element that was selected or picked, if available. For results that 3080 * have multiple selectable children, the value may be taken from a child 3081 * element rather than the result. 3082 * @returns {string} The value. 3083 */ 3084 #getValueFromResult(result, { urlOverride = null, element = null } = {}) { 3085 switch (result.type) { 3086 case lazy.UrlbarUtils.RESULT_TYPE.KEYWORD: 3087 return result.payload.input; 3088 case lazy.UrlbarUtils.RESULT_TYPE.SEARCH: { 3089 let value = ""; 3090 if (result.payload.keyword) { 3091 value += result.payload.keyword + " "; 3092 } 3093 value += result.payload.suggestion || result.payload.query; 3094 return value; 3095 } 3096 case lazy.UrlbarUtils.RESULT_TYPE.OMNIBOX: 3097 return result.payload.content; 3098 case lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC: 3099 return ( 3100 element?.dataset.query || 3101 element?.dataset.url || 3102 result.payload.input || 3103 result.payload.query || 3104 "" 3105 ); 3106 case lazy.UrlbarUtils.RESULT_TYPE.RESTRICT: 3107 return result.payload.autofillKeyword + " "; 3108 case lazy.UrlbarUtils.RESULT_TYPE.TIP: { 3109 let value = element?.dataset.url || element?.dataset.input; 3110 if (value) { 3111 return value; 3112 } 3113 break; 3114 } 3115 } 3116 3117 // Always respect a set urlOverride property. 3118 if (urlOverride !== null) { 3119 // This returns null for the empty string, allowing callers to clear the 3120 // input by passing an empty string as urlOverride. 3121 let url = URL.parse(urlOverride); 3122 return url ? losslessDecodeURI(url.URI) : ""; 3123 } 3124 3125 let parsedUrl = URL.parse(result.payload.url); 3126 // If the url is not parsable, just return an empty string; 3127 if (!parsedUrl) { 3128 return ""; 3129 } 3130 3131 let url = losslessDecodeURI(parsedUrl.URI); 3132 // If the user didn't originally type a protocol, and we generated one, 3133 // trim the http protocol from the input value, as https-first may upgrade 3134 // it to https, breaking user expectations. 3135 let stripHttp = 3136 result.heuristic && 3137 result.payload.url.startsWith("http://") && 3138 this.userTypedValue && 3139 this.#getSchemelessInput(this.userTypedValue) == 3140 Ci.nsILoadInfo.SchemelessInputTypeSchemeless; 3141 if (!stripHttp) { 3142 return url; 3143 } 3144 // Attempt to trim the url. If doing so results in a string that is 3145 // interpreted as search (e.g. unknown single word host, or domain suffix), 3146 // use the unmodified url instead. Otherwise, if the user edits the url 3147 // and confirms the new value, we may transform the url into a search. 3148 let trimmedUrl = lazy.UrlbarUtils.stripPrefixAndTrim(url, { stripHttp })[0]; 3149 let isSearch = !!this._getURIFixupInfo(trimmedUrl)?.keywordAsSent; 3150 if (isSearch) { 3151 // Although https-first might not respect the shown protocol, converting 3152 // the result to a search would be more disruptive. 3153 return url; 3154 } 3155 return trimmedUrl; 3156 } 3157 3158 /** 3159 * Extracts from a result the value to use for the `actiontype` attribute. 3160 * 3161 * @param {UrlbarResult} result The UrlbarResult to consider. 3162 * 3163 * @returns {string} The `actiontype` value, or undefined. 3164 */ 3165 #getActionTypeFromResult(result) { 3166 switch (result.type) { 3167 case lazy.UrlbarUtils.RESULT_TYPE.TAB_SWITCH: 3168 return "switchtab"; 3169 case lazy.UrlbarUtils.RESULT_TYPE.OMNIBOX: 3170 return "extension"; 3171 default: 3172 return undefined; 3173 } 3174 } 3175 3176 /** 3177 * Resets some state so that searches from the user's previous interaction 3178 * with the input don't interfere with searches from a new interaction. 3179 */ 3180 _resetSearchState() { 3181 this._lastSearchString = this.value; 3182 this._autofillPlaceholder = null; 3183 } 3184 3185 /** 3186 * Autofills the autofill placeholder string if appropriate, and determines 3187 * whether autofill should be allowed for the new search started by an input 3188 * event. 3189 * 3190 * @param {string} value 3191 * The new search string. 3192 * @returns {boolean} 3193 * Whether autofill should be allowed in the new search. 3194 */ 3195 _maybeAutofillPlaceholder(value) { 3196 // We allow autofill in local but not remote search modes. 3197 let allowAutofill = 3198 this.selectionEnd == value.length && 3199 !this.searchMode?.engineName && 3200 this.searchMode?.source != lazy.UrlbarUtils.RESULT_SOURCE.SEARCH; 3201 3202 if (!allowAutofill) { 3203 this.#clearAutofill(); 3204 return false; 3205 } 3206 3207 // Determine whether we can autofill the placeholder. The placeholder is a 3208 // value that we autofill now, when the search starts and before we wait on 3209 // its first result, in order to prevent a flicker in the input caused by 3210 // the previous autofilled substring disappearing and reappearing when the 3211 // first result arrives. Of course we can only autofill the placeholder if 3212 // it starts with the new search string, and we shouldn't autofill anything 3213 // if the caret isn't at the end of the input. 3214 let canAutofillPlaceholder = false; 3215 if (this._autofillPlaceholder) { 3216 if (this._autofillPlaceholder.type == "adaptive") { 3217 canAutofillPlaceholder = 3218 value.length >= 3219 this._autofillPlaceholder.adaptiveHistoryInput.length && 3220 this._autofillPlaceholder.value 3221 .toLocaleLowerCase() 3222 .startsWith(value.toLocaleLowerCase()); 3223 } else { 3224 canAutofillPlaceholder = lazy.UrlbarUtils.canAutofillURL( 3225 this._autofillPlaceholder.value, 3226 value 3227 ); 3228 } 3229 } 3230 3231 if (!canAutofillPlaceholder) { 3232 this._autofillPlaceholder = null; 3233 } else if ( 3234 this._autofillPlaceholder && 3235 this.selectionEnd == this.value.length && 3236 this._enableAutofillPlaceholder 3237 ) { 3238 let autofillValue = 3239 value + this._autofillPlaceholder.value.substring(value.length); 3240 this._autofillValue({ 3241 value: autofillValue, 3242 selectionStart: value.length, 3243 selectionEnd: autofillValue.length, 3244 type: this._autofillPlaceholder.type, 3245 adaptiveHistoryInput: this._autofillPlaceholder.adaptiveHistoryInput, 3246 untrimmedValue: this._autofillPlaceholder.untrimmedValue, 3247 }); 3248 } 3249 3250 return true; 3251 } 3252 3253 /** 3254 * Invoked on overflow/underflow/scrollend events to update attributes 3255 * related to the input text directionality. Overflow fade masks use these 3256 * attributes to appear at the proper side of the urlbar. 3257 */ 3258 updateTextOverflow() { 3259 if (!this._overflowing) { 3260 this.removeAttribute("textoverflow"); 3261 return; 3262 } 3263 3264 let isRTL = 3265 this.getAttribute("domaindir") === "rtl" && 3266 lazy.UrlbarUtils.isTextDirectionRTL(this.value, this.window); 3267 3268 this.window.promiseDocumentFlushed(() => { 3269 // Check overflow again to ensure it didn't change in the meanwhile. 3270 let input = this.inputField; 3271 if (input && this._overflowing) { 3272 // Normally we overflow at the end side of the text direction, though 3273 // RTL domains may cause us to overflow at the opposite side. 3274 // The outcome differs depending on the input field contents and applied 3275 // formatting, and reports the final state of all the scrolling into an 3276 // attribute available to css rules. 3277 // Note it's also possible to scroll an unfocused input field using 3278 // SHIFT + mousewheel on Windows, or with just the mousewheel / touchpad 3279 // scroll (without modifiers) on Mac. 3280 let side = "both"; 3281 if (isRTL) { 3282 if (input.scrollLeft == 0) { 3283 side = "left"; 3284 } else if (input.scrollLeft == input.scrollLeftMin) { 3285 side = "right"; 3286 } 3287 } else if (input.scrollLeft == 0) { 3288 side = "right"; 3289 } else if (input.scrollLeft == input.scrollLeftMax) { 3290 side = "left"; 3291 } 3292 3293 this.window.requestAnimationFrame(() => { 3294 // And check once again, since we might have stopped overflowing 3295 // since the promiseDocumentFlushed callback fired. 3296 if (this._overflowing) { 3297 this.setAttribute("textoverflow", side); 3298 } 3299 }); 3300 } 3301 }); 3302 } 3303 3304 _updateUrlTooltip() { 3305 if (this.focused || !this._overflowing) { 3306 this.inputField.removeAttribute("title"); 3307 } else { 3308 this.inputField.setAttribute("title", this.untrimmedValue); 3309 } 3310 } 3311 3312 _getSelectedValueForClipboard() { 3313 let selectedVal = this.#selectedText; 3314 3315 // Handle multiple-range selection as a string for simplicity. 3316 if (this.editor.selection.rangeCount > 1) { 3317 return selectedVal; 3318 } 3319 3320 // If the selection doesn't start at the beginning or doesn't span the 3321 // full domain or the URL bar is modified or there is no text at all, 3322 // nothing else to do here. 3323 // TODO (Bug 1908360): the valueIsTyped usage here is confusing, as often 3324 // it doesn't really indicate a user typed a value, it's rather used as 3325 // a way to tell if the value was modified. 3326 if ( 3327 this.selectionStart > 0 || 3328 selectedVal == "" || 3329 (this.valueIsTyped && !this._protocolIsTrimmed) 3330 ) { 3331 return selectedVal; 3332 } 3333 3334 // The selection doesn't span the full domain if it doesn't contain a slash and is 3335 // followed by some character other than a slash. 3336 if (!selectedVal.includes("/")) { 3337 let remainder = this.value.replace(selectedVal, ""); 3338 if (remainder != "" && remainder[0] != "/") { 3339 return selectedVal; 3340 } 3341 } 3342 3343 let uri; 3344 if (this.getAttribute("pageproxystate") == "valid") { 3345 uri = this.#isOpenedPageInBlankTargetLoading 3346 ? this.window.gBrowser.selectedBrowser.browsingContext 3347 .nonWebControlledBlankURI 3348 : this.window.gBrowser.currentURI; 3349 } else { 3350 // The value could be: 3351 // 1. a trimmed url, set by selecting a result 3352 // 2. a search string set by selecting a result 3353 // 3. a url that was confirmed but didn't finish loading yet 3354 // If it's an url the untrimmedValue should resolve to a valid URI, 3355 // otherwise it's a search string that should be copied as-is. 3356 3357 // If the copied text is that autofilled value, return the url including 3358 // the protocol from its suggestion. 3359 let result = this._resultForCurrentValue; 3360 3361 if (result?.autofill?.value == selectedVal) { 3362 return result.payload.url; 3363 } 3364 3365 uri = URL.parse(this._untrimmedValue)?.URI; 3366 if (!uri) { 3367 return selectedVal; 3368 } 3369 } 3370 uri = this.makeURIReadable(uri); 3371 let displaySpec = uri.displaySpec; 3372 3373 // If the entire URL is selected, just use the actual loaded URI, 3374 // unless we want a decoded URI, or it's a data: or javascript: URI, 3375 // since those are hard to read when encoded. 3376 if ( 3377 this.value == selectedVal && 3378 !uri.schemeIs("javascript") && 3379 !uri.schemeIs("data") && 3380 !lazy.UrlbarPrefs.get("decodeURLsOnCopy") 3381 ) { 3382 return displaySpec; 3383 } 3384 3385 // Just the beginning of the URL is selected, or we want a decoded 3386 // url. First check for a trimmed value. 3387 3388 if ( 3389 !selectedVal.startsWith(lazy.BrowserUIUtils.trimURLProtocol) && 3390 // Note _trimValue may also trim a trailing slash, thus we can't just do 3391 // a straight string compare to tell if the protocol was trimmed. 3392 !displaySpec.startsWith(this._trimValue(displaySpec)) 3393 ) { 3394 selectedVal = lazy.BrowserUIUtils.trimURLProtocol + selectedVal; 3395 } 3396 3397 // If selection starts from the beginning and part or all of the URL 3398 // is selected, we check for decoded characters and encode them. 3399 // Unless decodeURLsOnCopy is set. Do not encode data: URIs. 3400 if (!lazy.UrlbarPrefs.get("decodeURLsOnCopy") && !uri.schemeIs("data")) { 3401 try { 3402 if (URL.canParse(selectedVal)) { 3403 // Use encodeURI instead of URL.href because we don't want 3404 // trailing slash. 3405 selectedVal = encodeURI(selectedVal); 3406 } 3407 } catch (ex) { 3408 // URL is invalid. Return original selected value. 3409 } 3410 } 3411 3412 return selectedVal; 3413 } 3414 3415 _toggleActionOverride(event) { 3416 if ( 3417 event.keyCode == KeyEvent.DOM_VK_SHIFT || 3418 event.keyCode == KeyEvent.DOM_VK_ALT || 3419 event.keyCode == 3420 (AppConstants.platform == "macosx" 3421 ? KeyEvent.DOM_VK_META 3422 : KeyEvent.DOM_VK_CONTROL) 3423 ) { 3424 if (event.type == "keydown") { 3425 this._actionOverrideKeyCount++; 3426 this.toggleAttribute("action-override", true); 3427 this.view.panel.setAttribute("action-override", true); 3428 } else if ( 3429 this._actionOverrideKeyCount && 3430 --this._actionOverrideKeyCount == 0 3431 ) { 3432 this._clearActionOverride(); 3433 } 3434 } 3435 } 3436 3437 _clearActionOverride() { 3438 this._actionOverrideKeyCount = 0; 3439 this.removeAttribute("action-override"); 3440 this.view.panel.removeAttribute("action-override"); 3441 } 3442 3443 /** 3444 * Records in telemetry that a search is being loaded, 3445 * updates an incremental total number of searches in a pref, 3446 * and informs ASRouter that a search has occurred via a trigger send 3447 * 3448 * @param {nsISearchEngine} engine 3449 * The engine to generate the query for. 3450 * @param {Event} event 3451 * The event that triggered this query. 3452 * @param {object} [searchActionDetails] 3453 * The details associated with this search query. 3454 * @param {boolean} [searchActionDetails.isSuggestion] 3455 * True if this query was initiated from a suggestion from the search engine. 3456 * @param {boolean} [searchActionDetails.alias] 3457 * True if this query was initiated via a search alias. 3458 * @param {boolean} [searchActionDetails.isFormHistory] 3459 * True if this query was initiated from a form history result. 3460 * @param {string} [searchActionDetails.url] 3461 * The url this query was triggered with. 3462 * @param {MozBrowser} [browser] 3463 * The browser where the search is being opened. 3464 * Defaults to the window's selected browser. 3465 */ 3466 _recordSearch( 3467 engine, 3468 event, 3469 searchActionDetails = {}, 3470 browser = this.window.gBrowser.selectedBrowser 3471 ) { 3472 const isOneOff = this.view.oneOffSearchButtons?.eventTargetIsAOneOff(event); 3473 const searchSource = this.getSearchSource(event); 3474 3475 // Record when the user uses the search bar to be 3476 // used for message targeting. This is arbitrarily capped 3477 // at 100, only to prevent the number from growing ifinitely. 3478 const totalSearches = Services.prefs.getIntPref( 3479 "browser.search.totalSearches" 3480 ); 3481 const totalSearchesCap = 100; 3482 if (totalSearches < totalSearchesCap) { 3483 Services.prefs.setIntPref( 3484 "browser.search.totalSearches", 3485 totalSearches + 1 3486 ); 3487 } 3488 3489 // Sending a trigger to ASRouter when a search happens 3490 lazy.ASRouter.sendTriggerMessage({ 3491 browser, 3492 id: "onSearch", 3493 context: { 3494 isSuggestion: searchActionDetails.isSuggestion || false, 3495 searchSource, 3496 isOneOff, 3497 }, 3498 }); 3499 3500 lazy.BrowserSearchTelemetry.recordSearch(browser, engine, searchSource, { 3501 ...searchActionDetails, 3502 isOneOff, 3503 newtabSessionId: this._handoffSession, 3504 }); 3505 } 3506 3507 /** 3508 * Shortens the given value, usually by removing http:// and trailing slashes. 3509 * 3510 * @param {string} val 3511 * The string to be trimmed if it appears to be URI 3512 * @returns {string} 3513 * The trimmed string 3514 */ 3515 _trimValue(val) { 3516 if (!this.#isAddressbar) { 3517 return val; 3518 } 3519 let trimmedValue = lazy.UrlbarPrefs.get("trimURLs") 3520 ? lazy.BrowserUIUtils.trimURL(val) 3521 : val; 3522 // Only trim value if the directionality doesn't change to RTL and we're not 3523 // showing a strikeout https protocol. 3524 return lazy.UrlbarUtils.isTextDirectionRTL(trimmedValue, this.window) || 3525 this.#lazy.valueFormatter.willShowFormattedMixedContentProtocol(val) 3526 ? val 3527 : trimmedValue; 3528 } 3529 3530 /** 3531 * Returns whether the passed-in event may represents a canonization request. 3532 * 3533 * @param {Event} event 3534 * An Event to examine. 3535 * @returns {boolean} 3536 * Whether the event is a KeyboardEvent that triggers canonization. 3537 */ 3538 #isCanonizeKeyboardEvent(event) { 3539 return ( 3540 KeyboardEvent.isInstance(event) && 3541 event.keyCode == KeyEvent.DOM_VK_RETURN && 3542 (AppConstants.platform == "macosx" ? event.metaKey : event.ctrlKey) && 3543 !event._disableCanonization && 3544 lazy.UrlbarPrefs.get("ctrlCanonizesURLs") 3545 ); 3546 } 3547 3548 /** 3549 * If appropriate, this prefixes a search string with 'www.' and suffixes it 3550 * with browser.fixup.alternate.suffix prior to navigating. 3551 * 3552 * @param {Event} event 3553 * The event that triggered this query. 3554 * @param {string} value 3555 * The search string that should be canonized. 3556 * @returns {string} 3557 * Returns the canonized URL if available and null otherwise. 3558 */ 3559 _maybeCanonizeURL(event, value) { 3560 // Only add the suffix when the URL bar value isn't already "URL-like", 3561 // and only if we get a keyboard event, to match user expectations. 3562 if ( 3563 this.sapName == "searchbar" || 3564 !this.#isCanonizeKeyboardEvent(event) || 3565 !/^\s*[^.:\/\s]+(?:\/.*|\s*)$/i.test(value) 3566 ) { 3567 return null; 3568 } 3569 3570 let suffix = Services.prefs.getCharPref("browser.fixup.alternate.suffix"); 3571 if (!suffix.endsWith("/")) { 3572 suffix += "/"; 3573 } 3574 3575 // trim leading/trailing spaces (bug 233205) 3576 value = value.trim(); 3577 3578 // Tack www. and suffix on. If user has appended directories, insert 3579 // suffix before them (bug 279035). Be careful not to get two slashes. 3580 let firstSlash = value.indexOf("/"); 3581 if (firstSlash >= 0) { 3582 value = 3583 value.substring(0, firstSlash) + 3584 suffix + 3585 value.substring(firstSlash + 1); 3586 } else { 3587 value = value + suffix; 3588 } 3589 3590 try { 3591 const info = Services.uriFixup.getFixupURIInfo( 3592 value, 3593 Ci.nsIURIFixup.FIXUP_FLAGS_MAKE_ALTERNATE_URI 3594 ); 3595 value = info.fixedURI.spec; 3596 } catch (ex) { 3597 console.error(`An error occured while trying to fixup "${value}"`, ex); 3598 } 3599 3600 this.value = value; 3601 return value; 3602 } 3603 3604 /** 3605 * Autofills a value into the input. The value will be autofilled regardless 3606 * of the input's current value. 3607 * 3608 * @param {object} options 3609 * The options object. 3610 * @param {string} options.value 3611 * The value to autofill. 3612 * @param {number} options.selectionStart 3613 * The new selectionStart. 3614 * @param {number} options.selectionEnd 3615 * The new selectionEnd. 3616 * @param {"origin" | "url" | "adaptive"} options.type 3617 * The autofill type, one of: "origin", "url", "adaptive" 3618 * @param {string} options.adaptiveHistoryInput 3619 * If the autofill type is "adaptive", this is the matching `input` value 3620 * from adaptive history. 3621 * @param {string} [options.untrimmedValue] 3622 * Untrimmed value including a protocol. 3623 */ 3624 _autofillValue({ 3625 value, 3626 selectionStart, 3627 selectionEnd, 3628 type, 3629 adaptiveHistoryInput, 3630 untrimmedValue, 3631 }) { 3632 // The autofilled value may be a URL that includes a scheme at the 3633 // beginning. Do not allow it to be trimmed. 3634 this._setValue(value, { untrimmedValue }); 3635 this.inputField.setSelectionRange(selectionStart, selectionEnd); 3636 this._autofillPlaceholder = { 3637 value, 3638 type, 3639 adaptiveHistoryInput, 3640 selectionStart, 3641 selectionEnd, 3642 untrimmedValue, 3643 }; 3644 } 3645 3646 /** 3647 * Called when a menu item from results menu is picked. 3648 * 3649 * @param {UrlbarResult} result The result that was picked. 3650 * @param {Event} event The event that picked the result. 3651 * @param {HTMLElement} element the picked view element, if available. 3652 * @param {object} browser The browser to use for the load. 3653 */ 3654 #pickMenuResult(result, event, element, browser) { 3655 this.controller.engagementEvent.record(event, { 3656 result, 3657 element, 3658 searchString: this._lastSearchString, 3659 selType: element.dataset.command, 3660 }); 3661 3662 if (element.dataset.command == "manage") { 3663 this.window.openPreferences("search-locationBar"); 3664 return; 3665 } 3666 3667 let url; 3668 if (element.dataset.command == "help") { 3669 url = result.payload.helpUrl; 3670 } 3671 url ||= element.dataset.url; 3672 3673 if (!url) { 3674 return; 3675 } 3676 3677 let where = this._whereToOpen(event); 3678 if (element.dataset.command == "help" && where == "current") { 3679 // Open help links in a new tab. 3680 where = "tab"; 3681 } 3682 3683 this.view.close({ elementPicked: true }); 3684 3685 this._loadURL( 3686 url, 3687 event, 3688 where, 3689 { 3690 allowInheritPrincipal: false, 3691 private: this.isPrivate, 3692 }, 3693 { 3694 source: result.source, 3695 type: result.type, 3696 }, 3697 browser 3698 ); 3699 } 3700 3701 /** 3702 * Loads the url in the appropriate place. 3703 * 3704 * @param {string} url 3705 * The URL to open. 3706 * @param {string} openUILinkWhere 3707 * Where we expect the result to be opened. 3708 * @param {object} params 3709 * The parameters related to how and where the result will be opened. 3710 * Further supported paramters are listed in _loadURL. 3711 * @param {object} [params.triggeringPrincipal] 3712 * The principal that the action was triggered from. 3713 * @param {object} [resultDetails] 3714 * Details of the selected result, if any. 3715 * Further supported details are listed in _loadURL. 3716 * @param {string} [resultDetails.searchTerm] 3717 * Search term of the result source, if any. 3718 * @param {object} browser the browser to use for the load. 3719 */ 3720 #prepareAddressbarLoad( 3721 url, 3722 openUILinkWhere, 3723 params, 3724 resultDetails = null, 3725 browser 3726 ) { 3727 if (!this.#isAddressbar) { 3728 throw new Error( 3729 "Can't prepare addressbar load when this isn't an addressbar input" 3730 ); 3731 } 3732 3733 // No point in setting these because we'll handleRevert() a few rows below. 3734 if (openUILinkWhere == "current") { 3735 // Make sure URL is formatted properly (don't show punycode). 3736 let formattedURL = url; 3737 try { 3738 formattedURL = losslessDecodeURI(new URL(url).URI); 3739 } catch {} 3740 3741 this.value = 3742 lazy.UrlbarPrefs.isPersistedSearchTermsEnabled() && 3743 resultDetails?.searchTerm 3744 ? resultDetails.searchTerm 3745 : formattedURL; 3746 browser.userTypedValue = this.value; 3747 } 3748 3749 // No point in setting this if we are loading in a new window. 3750 if ( 3751 openUILinkWhere != "window" && 3752 this.window.gInitialPages.includes(url) 3753 ) { 3754 browser.initialPageLoadedFromUserAction = url; 3755 } 3756 3757 try { 3758 lazy.UrlbarUtils.addToUrlbarHistory(url, this.window); 3759 } catch (ex) { 3760 // Things may go wrong when adding url to session history, 3761 // but don't let that interfere with the loading of the url. 3762 console.error(ex); 3763 } 3764 3765 // TODO: When bug 1498553 is resolved, we should be able to 3766 // remove the !triggeringPrincipal condition here. 3767 if ( 3768 !params.triggeringPrincipal || 3769 params.triggeringPrincipal.isSystemPrincipal 3770 ) { 3771 // Reset DOS mitigations for the basic auth prompt. 3772 delete browser.authPromptAbuseCounter; 3773 3774 // Reset temporary permissions on the current tab if the user reloads 3775 // the tab via the urlbar. 3776 if ( 3777 openUILinkWhere == "current" && 3778 browser.currentURI && 3779 url === browser.currentURI.spec 3780 ) { 3781 this.window.SitePermissions.clearTemporaryBlockPermissions(browser); 3782 } 3783 } 3784 3785 // Specifies that the URL load was initiated by the URL bar. 3786 params.initiatedByURLBar = true; 3787 } 3788 3789 /** 3790 * Loads the url in the appropriate place. 3791 * 3792 * @param {string} url 3793 * The URL to open. 3794 * @param {Event} event 3795 * The event that triggered to load the url. 3796 * @param {string} openUILinkWhere 3797 * Where we expect the result to be opened. 3798 * @param {object} params 3799 * The parameters related to how and where the result will be opened. 3800 * Further supported parameters are listed in utilityOverlay.js#openUILinkIn. 3801 * @param {object} [params.triggeringPrincipal] 3802 * The principal that the action was triggered from. 3803 * @param {nsIInputStream} [params.postData] 3804 * The POST data associated with a search submission. 3805 * @param {boolean} [params.allowInheritPrincipal] 3806 * Whether the principal can be inherited. 3807 * @param {nsILoadInfo.SchemelessInputType} [params.schemelessInput] 3808 * Whether the search/URL term was without an explicit scheme. 3809 * @param {object} [resultDetails] 3810 * Details of the selected result, if any. 3811 * @param {Values<typeof lazy.UrlbarUtils.RESULT_TYPE>} [resultDetails.type] 3812 * Details of the result type, if any. 3813 * @param {string} [resultDetails.searchTerm] 3814 * Search term of the result source, if any. 3815 * @param {Values<typeof lazy.UrlbarUtils.RESULT_SOURCE>} [resultDetails.source] 3816 * Details of the result source, if any. 3817 * @param {object} browser [optional] the browser to use for the load. 3818 */ 3819 _loadURL( 3820 url, 3821 event, 3822 openUILinkWhere, 3823 params, 3824 resultDetails = null, 3825 browser = this.window.gBrowser.selectedBrowser 3826 ) { 3827 if (this.#isAddressbar) { 3828 this.#prepareAddressbarLoad( 3829 url, 3830 openUILinkWhere, 3831 params, 3832 resultDetails, 3833 browser 3834 ); 3835 } 3836 3837 params.allowThirdPartyFixup = true; 3838 3839 if (openUILinkWhere == "current") { 3840 params.targetBrowser = browser; 3841 params.indicateErrorPageLoad = true; 3842 params.allowPinnedTabHostChange = true; 3843 params.allowPopups = url.startsWith("javascript:"); 3844 } else { 3845 params.initiatingDoc = this.window.document; 3846 } 3847 3848 if ( 3849 this._keyDownEnterDeferred && 3850 event?.keyCode === KeyEvent.DOM_VK_RETURN && 3851 openUILinkWhere === "current" 3852 ) { 3853 // In this case, we move the focus to the browser that loads the content 3854 // upon key up the enter key. 3855 // To do it, send avoidBrowserFocus flag to openTrustedLinkIn() to avoid 3856 // focusing on the browser in the function. And also, set loadedContent 3857 // flag that whether the content is loaded in the current tab by this enter 3858 // key. _keyDownEnterDeferred promise is processed at key up the enter, 3859 // focus on the browser passed by _keyDownEnterDeferred.resolve(). 3860 params.avoidBrowserFocus = true; 3861 this._keyDownEnterDeferred.loadedContent = true; 3862 this._keyDownEnterDeferred.resolve(browser); 3863 } 3864 3865 // Ensure the window gets the `private` feature if the current window 3866 // is private, unless the caller explicitly requested not to. 3867 if (this.isPrivate && !("private" in params)) { 3868 params.private = true; 3869 } 3870 3871 // Focus the content area before triggering loads, since if the load 3872 // occurs in a new tab, we want focus to be restored to the content 3873 // area when the current tab is re-selected. 3874 if (!params.avoidBrowserFocus) { 3875 browser.focus(); 3876 // Make sure the domain name stays visible for spoof protection and usability. 3877 this.inputField.setSelectionRange(0, 0); 3878 } 3879 3880 if (openUILinkWhere != "current" && this.sapName != "searchbar") { 3881 this.handleRevert(); 3882 } 3883 3884 // Notify about the start of navigation. 3885 this.#notifyStartNavigation(resultDetails); 3886 3887 try { 3888 this.window.openTrustedLinkIn(url, openUILinkWhere, params); 3889 } catch (ex) { 3890 // This load can throw an exception in certain cases, which means 3891 // we'll want to replace the URL with the loaded URL: 3892 if (ex.result != Cr.NS_ERROR_LOAD_SHOWED_ERRORPAGE) { 3893 this.handleRevert(); 3894 } 3895 } 3896 3897 // If we show the focus border after closing the view, it would appear to 3898 // flash since this._on_blur would remove it immediately after. 3899 this.view.close({ showFocusBorder: false }); 3900 } 3901 3902 /** 3903 * Determines where a URL/page should be opened. 3904 * 3905 * @param {Event} event the event triggering the opening. 3906 * @returns {"current" | "tabshifted" | "tab" | "save" | "window"} 3907 */ 3908 _whereToOpen(event) { 3909 let isKeyboardEvent = KeyboardEvent.isInstance(event); 3910 let reuseEmpty = isKeyboardEvent; 3911 let where = undefined; 3912 if ( 3913 isKeyboardEvent && 3914 (event.altKey || event.getModifierState("AltGraph")) 3915 ) { 3916 // We support using 'alt' to open in a tab, because ctrl/shift 3917 // might be used for canonizing URLs: 3918 where = event.shiftKey ? "tabshifted" : "tab"; 3919 } else if (this.#isCanonizeKeyboardEvent(event)) { 3920 // If we're allowing canonization, and this is a canonization key event, 3921 // open in current tab to avoid handling as new tab modifier. 3922 where = "current"; 3923 } else { 3924 where = lazy.BrowserUtils.whereToOpenLink(event, false, false); 3925 } 3926 let openInTabPref = 3927 this.#sapName == "searchbar" 3928 ? lazy.UrlbarPrefs.get("browser.search.openintab") 3929 : lazy.UrlbarPrefs.get("openintab"); 3930 if (openInTabPref) { 3931 if (where == "current") { 3932 where = "tab"; 3933 } else if (where == "tab") { 3934 where = "current"; 3935 } 3936 reuseEmpty = true; 3937 } 3938 if ( 3939 where == "tab" && 3940 reuseEmpty && 3941 this.window.gBrowser.selectedTab.isEmpty 3942 ) { 3943 where = "current"; 3944 } 3945 return where; 3946 } 3947 3948 _initCopyCutController() { 3949 if (this._copyCutController) { 3950 return; 3951 } 3952 this._copyCutController = new CopyCutController(this); 3953 this.inputField.controllers.insertControllerAt(0, this._copyCutController); 3954 } 3955 3956 /** 3957 * Searches the context menu for the location of a specific command. 3958 * 3959 * @param {string} menuItemCommand 3960 * The command to search for. 3961 * @returns {HTMLElement} 3962 * Html element that matches the command or 3963 * the last element if we could not find the command. 3964 */ 3965 #findMenuItemLocation(menuItemCommand) { 3966 let inputBox = this.querySelector("moz-input-box"); 3967 let contextMenu = inputBox.menupopup; 3968 let insertLocation = contextMenu.firstElementChild; 3969 // find the location of the command 3970 while ( 3971 insertLocation.nextElementSibling && 3972 insertLocation.getAttribute("cmd") != menuItemCommand 3973 ) { 3974 insertLocation = insertLocation.nextElementSibling; 3975 } 3976 3977 return insertLocation; 3978 } 3979 3980 /** 3981 * Strips known tracking query parameters/ link decorators. 3982 * 3983 * @returns {nsIURI} 3984 * The stripped URI or original URI, if nothing can be 3985 * stripped 3986 */ 3987 #stripURI() { 3988 let copyString = this._getSelectedValueForClipboard(); 3989 if (!copyString) { 3990 return null; 3991 } 3992 let strippedURI = null; 3993 3994 // Error check occurs during isClipboardURIValid 3995 let uri = Services.io.newURI(copyString); 3996 try { 3997 strippedURI = lazy.QueryStringStripper.stripForCopyOrShare(uri); 3998 } catch (e) { 3999 console.warn(`stripForCopyOrShare: ${e.message}`); 4000 return uri; 4001 } 4002 4003 if (strippedURI) { 4004 return this.makeURIReadable(strippedURI); 4005 } 4006 return uri; 4007 } 4008 4009 /** 4010 * Checks if the clipboard contains a valid URI 4011 * 4012 * @returns {true|false} 4013 */ 4014 #isClipboardURIValid() { 4015 let copyString = this._getSelectedValueForClipboard(); 4016 if (!copyString) { 4017 return false; 4018 } 4019 4020 return URL.canParse(copyString); 4021 } 4022 4023 /** 4024 * Checks if there is a query parameter that can be stripped 4025 * 4026 * @returns {true|false} 4027 */ 4028 #canStrip() { 4029 let copyString = this._getSelectedValueForClipboard(); 4030 if (!copyString) { 4031 return false; 4032 } 4033 // throws if the selected string is not a valid URI 4034 try { 4035 let uri = Services.io.newURI(copyString); 4036 return lazy.QueryStringStripper.canStripForShare(uri); 4037 } catch (e) { 4038 console.warn("canStrip failed!", e); 4039 return false; 4040 } 4041 } 4042 4043 /** 4044 * Restores the untrimmed value in the urlbar. 4045 * 4046 * @param {object} [options] 4047 * Options for untrimming. 4048 * @param {boolean} [options.moveCursorToStart] 4049 * Whether the cursor should be moved at position 0 after untrimming. 4050 * @param {boolean} [options.ignoreSelection] 4051 * Whether this should untrim, regardless of the current selection state. 4052 */ 4053 #maybeUntrimUrl({ moveCursorToStart = false, ignoreSelection = false } = {}) { 4054 // Check if we can untrim the current value. 4055 if ( 4056 !lazy.UrlbarPrefs.getScotchBonnetPref( 4057 "untrimOnUserInteraction.featureGate" 4058 ) || 4059 !this._protocolIsTrimmed || 4060 !this.focused || 4061 (!ignoreSelection && this.#allTextSelected) 4062 ) { 4063 return; 4064 } 4065 4066 let selectionStart = this.selectionStart; 4067 let selectionEnd = this.selectionEnd; 4068 4069 // Correct the selection taking the trimmed protocol into account. 4070 let offset = lazy.BrowserUIUtils.trimURLProtocol.length; 4071 4072 // In case of autofill, we may have to adjust its boundaries. 4073 if (this._autofillPlaceholder) { 4074 this._autofillPlaceholder.selectionStart += offset; 4075 this._autofillPlaceholder.selectionEnd += offset; 4076 } 4077 4078 if (moveCursorToStart) { 4079 this._setValue(this._untrimmedValue, { 4080 valueIsTyped: this.valueIsTyped, 4081 }); 4082 this.setSelectionRange(0, 0); 4083 return; 4084 } 4085 4086 if (selectionStart == selectionEnd) { 4087 // When cursor is at the end of the string, untrimming may 4088 // reintroduced a trailing slash and we want to move past it. 4089 if (selectionEnd == this.value.length) { 4090 offset += 1; 4091 } 4092 selectionStart = selectionEnd += offset; 4093 } else { 4094 // There's a selection, so we must calculate both the initial 4095 // protocol and the eventual trailing slash. 4096 if (selectionStart != 0) { 4097 selectionStart += offset; 4098 } else { 4099 // When selection starts at the beginning, the adjusted selection will 4100 // include the protocol only if the selected text includes the host. 4101 // The port is left out, as one may want to exclude it from the copy. 4102 let prePathMinusPort; 4103 try { 4104 let uri = Services.io.newURI(this._untrimmedValue); 4105 prePathMinusPort = [uri.userPass, uri.displayHost] 4106 .filter(Boolean) 4107 .join("@"); 4108 } catch (ex) { 4109 lazy.logger.error("Should only try to untrim valid URLs"); 4110 } 4111 if (!this.#selectedText.startsWith(prePathMinusPort)) { 4112 selectionStart += offset; 4113 } 4114 } 4115 if (selectionEnd == this.value.length) { 4116 offset += 1; 4117 } 4118 selectionEnd += offset; 4119 } 4120 4121 this._setValue(this._untrimmedValue, { 4122 valueIsTyped: this.valueIsTyped, 4123 }); 4124 4125 this.setSelectionRange(selectionStart, selectionEnd); 4126 } 4127 4128 // The strip-on-share feature will strip known tracking/decorational 4129 // query params from the URI and copy the stripped version to the clipboard. 4130 _initStripOnShare() { 4131 let contextMenu = this.querySelector("moz-input-box").menupopup; 4132 let insertLocation = this.#findMenuItemLocation("cmd_copy"); 4133 // set up the menu item 4134 let stripOnShare = this.document.createXULElement("menuitem"); 4135 this.document.l10n.setAttributes( 4136 stripOnShare, 4137 "text-action-copy-clean-link" 4138 ); 4139 stripOnShare.setAttribute("anonid", "strip-on-share"); 4140 stripOnShare.id = "strip-on-share"; 4141 4142 insertLocation.insertAdjacentElement("afterend", stripOnShare); 4143 4144 // Register listener that returns the stripped url or falls back 4145 // to the original url if nothing can be stripped. 4146 stripOnShare.addEventListener("command", () => { 4147 let strippedURI = this.#stripURI(); 4148 lazy.ClipboardHelper.copyString(strippedURI.displaySpec); 4149 }); 4150 4151 // Register a listener that hides the menu item if there is nothing to copy. 4152 contextMenu.addEventListener("popupshowing", () => { 4153 // feature is not enabled 4154 if (!lazy.QUERY_STRIPPING_STRIP_ON_SHARE) { 4155 stripOnShare.setAttribute("hidden", true); 4156 return; 4157 } 4158 let controller = 4159 this.document.commandDispatcher.getControllerForCommand("cmd_copy"); 4160 if ( 4161 !controller.isCommandEnabled("cmd_copy") || 4162 !this.#isClipboardURIValid() 4163 ) { 4164 stripOnShare.setAttribute("hidden", true); 4165 return; 4166 } 4167 stripOnShare.removeAttribute("hidden"); 4168 if (!this.#canStrip()) { 4169 stripOnShare.setAttribute("disabled", true); 4170 return; 4171 } 4172 stripOnShare.removeAttribute("disabled"); 4173 }); 4174 } 4175 4176 _initPasteAndGo() { 4177 let inputBox = this.querySelector("moz-input-box"); 4178 let contextMenu = inputBox.menupopup; 4179 let insertLocation = this.#findMenuItemLocation("cmd_paste"); 4180 if (!insertLocation) { 4181 return; 4182 } 4183 4184 let pasteAndGo = this.document.createXULElement("menuitem"); 4185 pasteAndGo.id = "paste-and-go"; 4186 let label = Services.strings 4187 .createBundle("chrome://browser/locale/browser.properties") 4188 .GetStringFromName("pasteAndGo.label"); 4189 pasteAndGo.setAttribute("label", label); 4190 pasteAndGo.setAttribute("anonid", "paste-and-go"); 4191 pasteAndGo.addEventListener("command", () => { 4192 this._suppressStartQuery = true; 4193 4194 this.select(); 4195 this.window.goDoCommand("cmd_paste"); 4196 this.setResultForCurrentValue(null); 4197 this.handleCommand(); 4198 this.controller.clearLastQueryContextCache(); 4199 4200 this._suppressStartQuery = false; 4201 }); 4202 4203 contextMenu.addEventListener("popupshowing", () => { 4204 // Close the results pane when the input field contextual menu is open, 4205 // because paste and go doesn't want a result selection. 4206 this.view.close(); 4207 4208 let controller = 4209 this.document.commandDispatcher.getControllerForCommand("cmd_paste"); 4210 let enabled = controller.isCommandEnabled("cmd_paste"); 4211 if (enabled) { 4212 pasteAndGo.removeAttribute("disabled"); 4213 } else { 4214 pasteAndGo.setAttribute("disabled", "true"); 4215 } 4216 }); 4217 4218 insertLocation.insertAdjacentElement("afterend", pasteAndGo); 4219 } 4220 4221 /** 4222 * This notifies observers that the user has entered or selected something in 4223 * the URL bar which will cause navigation. 4224 * 4225 * We use the observer service, so that we don't need to load extra facilities 4226 * if they aren't being used, e.g. WebNavigation. 4227 * 4228 * @param {UrlbarResult} result 4229 * Details of the result that was selected, if any. 4230 */ 4231 #notifyStartNavigation(result) { 4232 if (this.#isAddressbar) { 4233 Services.obs.notifyObservers({ result }, "urlbar-user-start-navigation"); 4234 } 4235 } 4236 4237 /** 4238 * Returns a search mode object if a result should enter search mode when 4239 * selected. 4240 * 4241 * @param {UrlbarResult} result 4242 * The result to check. 4243 * @param {string} [entry] 4244 * If provided, this will be recorded as the entry point into search mode. 4245 * See setSearchMode() documentation for details. 4246 * @returns {object} A search mode object. Null if search mode should not be 4247 * entered. See setSearchMode documentation for details. 4248 */ 4249 _searchModeForResult(result, entry = null) { 4250 // Search mode is determined by the result's keyword or engine. 4251 if ( 4252 !result.payload.keyword && 4253 !result.payload.engine && 4254 !this.view.selectedElement.dataset?.engine 4255 ) { 4256 return null; 4257 } 4258 4259 let searchMode = this.searchModeForToken(result.payload.keyword); 4260 // If result.originalEngine is set, then the user is Alt+Tabbing 4261 // through the one-offs, so the keyword doesn't match the engine. 4262 if ( 4263 !searchMode && 4264 result.payload.engine && 4265 (!result.payload.originalEngine || 4266 result.payload.engine == result.payload.originalEngine) 4267 ) { 4268 searchMode = { engineName: result.payload.engine }; 4269 } else if (this.view.selectedElement?.dataset.engine) { 4270 searchMode = { engineName: this.view.selectedElement.dataset.engine }; 4271 } 4272 4273 if (searchMode) { 4274 if (result.type == lazy.UrlbarUtils.RESULT_TYPE.RESTRICT) { 4275 searchMode.restrictType = "keyword"; 4276 } else if ( 4277 lazy.UrlbarTokenizer.SEARCH_MODE_RESTRICT.has(result.payload.keyword) 4278 ) { 4279 searchMode.restrictType = "symbol"; 4280 } 4281 if (entry) { 4282 searchMode.entry = entry; 4283 } else { 4284 switch (result.providerName) { 4285 case "UrlbarProviderTopSites": 4286 searchMode.entry = "topsites_urlbar"; 4287 break; 4288 case "UrlbarProviderTabToSearch": 4289 if (result.payload.dynamicType) { 4290 searchMode.entry = "tabtosearch_onboard"; 4291 } else { 4292 searchMode.entry = "tabtosearch"; 4293 } 4294 break; 4295 default: 4296 searchMode.entry = "keywordoffer"; 4297 break; 4298 } 4299 } 4300 } 4301 4302 return searchMode; 4303 } 4304 4305 /** 4306 * Updates the UI so that search mode is either entered or exited. 4307 * 4308 * @param {object} searchMode 4309 * See setSearchMode documentation. If null, then search mode is exited. 4310 */ 4311 _updateSearchModeUI(searchMode) { 4312 let { engineName, source, isGeneralPurposeEngine } = searchMode || {}; 4313 4314 // As an optimization, bail if the given search mode is null but search mode 4315 // is already inactive. Otherwise, browser_preferences_usage.js fails due to 4316 // accessing the browser.urlbar.placeholderName pref (via the call to 4317 // initPlaceHolder below) too many times. That test does not enter search mode, 4318 // but it triggers many calls to this method with a null search mode, via setURI. 4319 if (!engineName && !source && !this.hasAttribute("searchmode")) { 4320 return; 4321 } 4322 4323 if (this._searchModeIndicatorTitle) { 4324 this._searchModeIndicatorTitle.textContent = ""; 4325 this._searchModeIndicatorTitle.removeAttribute("data-l10n-id"); 4326 } 4327 4328 if (!engineName && !source) { 4329 this.removeAttribute("searchmode"); 4330 this.initPlaceHolder(true); 4331 return; 4332 } 4333 4334 if (this.#isAddressbar) { 4335 if (engineName) { 4336 // Set text content for the search mode indicator. 4337 this._searchModeIndicatorTitle.textContent = engineName; 4338 this.document.l10n.setAttributes( 4339 this.inputField, 4340 isGeneralPurposeEngine 4341 ? "urlbar-placeholder-search-mode-web-2" 4342 : "urlbar-placeholder-search-mode-other-engine", 4343 { name: engineName } 4344 ); 4345 } else if (source) { 4346 const messageIDs = { 4347 actions: "urlbar-placeholder-search-mode-other-actions", 4348 bookmarks: "urlbar-placeholder-search-mode-other-bookmarks", 4349 engine: "urlbar-placeholder-search-mode-other-engine", 4350 history: "urlbar-placeholder-search-mode-other-history", 4351 tabs: "urlbar-placeholder-search-mode-other-tabs", 4352 }; 4353 let sourceName = lazy.UrlbarUtils.getResultSourceName(source); 4354 let l10nID = `urlbar-search-mode-${sourceName}`; 4355 this.document.l10n.setAttributes( 4356 this._searchModeIndicatorTitle, 4357 l10nID 4358 ); 4359 this.document.l10n.setAttributes( 4360 this.inputField, 4361 messageIDs[sourceName] 4362 ); 4363 } 4364 } 4365 4366 this.toggleAttribute("searchmode", true); 4367 // Clear autofill. 4368 if (this._autofillPlaceholder && this.userTypedValue) { 4369 this.value = this.userTypedValue; 4370 } 4371 // Search mode should only be active when pageproxystate is invalid. 4372 if (this.getAttribute("pageproxystate") == "valid") { 4373 this.value = ""; 4374 this.setPageProxyState("invalid", true); 4375 } 4376 4377 this.searchModeSwitcher?.onSearchModeChanged(); 4378 } 4379 4380 /** 4381 * Handles persisted search terms logic for the current browser. This manages 4382 * state and updates the UI accordingly. 4383 * 4384 * @param {object} options 4385 * @param {object} options.state 4386 * The state object for the currently viewed browser. 4387 * @param {boolean} options.hideSearchTerms 4388 * True if we must hide the search terms and instead show the page URL. 4389 * @param {boolean} options.dueToTabSwitch 4390 * True if the browser was revealed again due to a tab switch. 4391 * @param {boolean} options.isSameDocument 4392 * True if the page load was same document. 4393 * @param {nsIURI} [options.uri] 4394 * The latest URI of the page. 4395 * @returns {boolean} 4396 * Whether search terms should persist. 4397 */ 4398 #handlePersistedSearchTerms({ 4399 state, 4400 hideSearchTerms, 4401 dueToTabSwitch, 4402 isSameDocument, 4403 uri, 4404 }) { 4405 if (!lazy.UrlbarPrefs.isPersistedSearchTermsEnabled()) { 4406 if (state.persist) { 4407 this.removeAttribute("persistsearchterms"); 4408 delete state.persist; 4409 } 4410 return false; 4411 } 4412 4413 // The first time the browser URI has been loaded to the input. If 4414 // persist is not defined, it is likely due to the tab being created in 4415 // the background or an existing tab moved to a new window and we have to 4416 // do the work for the first time. 4417 let firstView = (!isSameDocument && !dueToTabSwitch) || !state.persist; 4418 4419 let cachedUriDidChange = 4420 state.persist?.originalURI && 4421 (!this.window.gBrowser.selectedBrowser.originalURI || 4422 !state.persist.originalURI.equals( 4423 this.window.gBrowser.selectedBrowser.originalURI 4424 )); 4425 4426 // Capture the shouldPersist property if it exists before 4427 // setPersistenceState potentially modifies it. 4428 let wasPersisting = state.persist?.shouldPersist ?? false; 4429 4430 if (firstView || cachedUriDidChange) { 4431 lazy.UrlbarSearchTermsPersistence.setPersistenceState( 4432 state, 4433 this.window.gBrowser.selectedBrowser.originalURI 4434 ); 4435 } 4436 let shouldPersist = 4437 !hideSearchTerms && 4438 lazy.UrlbarSearchTermsPersistence.shouldPersist(state, { 4439 dueToTabSwitch, 4440 isSameDocument, 4441 uri: uri ?? this.window.gBrowser.currentURI, 4442 userTypedValue: this.userTypedValue, 4443 firstView, 4444 }); 4445 // When persisting, userTypedValue should have a value consistent with the 4446 // search terms to mimic a user typing the search terms. 4447 // When turning off persist, check if the userTypedValue needs to be 4448 // removed in order for the URL to return to the address bar. Single page 4449 // application SERPs will load secondary search pages (e.g. Maps, Images) 4450 // with the same document, which won't unset userTypedValue. 4451 if (shouldPersist) { 4452 this.userTypedValue = state.persist.searchTerms; 4453 } else if (wasPersisting && !shouldPersist) { 4454 this.userTypedValue = null; 4455 } 4456 4457 state.persist.shouldPersist = shouldPersist; 4458 this.toggleAttribute("persistsearchterms", state.persist.shouldPersist); 4459 4460 if (state.persist.shouldPersist && !isSameDocument) { 4461 Glean.urlbarPersistedsearchterms.viewCount.add(1); 4462 } 4463 4464 return shouldPersist; 4465 } 4466 4467 /** 4468 * Initializes the urlbar placeholder to the pre-saved engine name. We do this 4469 * via a preference, to avoid needing to synchronously init the search service. 4470 * 4471 * This should be called around the time of DOMContentLoaded, so that it is 4472 * initialized quickly before the user sees anything. 4473 * 4474 * Note: If the preference doesn't exist, we don't do anything as the default 4475 * placeholder is a string which doesn't have the engine name; however, this 4476 * can be overridden using the `force` parameter. 4477 * 4478 * @param {boolean} force If true and the preference doesn't exist, the 4479 * placeholder will be set to the default version 4480 * without an engine name ("Search or enter address"). 4481 */ 4482 initPlaceHolder(force = false) { 4483 if (!this.#isAddressbar) { 4484 return; 4485 } 4486 4487 let prefName = 4488 "browser.urlbar.placeholderName" + (this.isPrivate ? ".private" : ""); 4489 let engineName = Services.prefs.getStringPref(prefName, ""); 4490 if (engineName || force) { 4491 // We can do this directly, since we know we're at DOMContentLoaded. 4492 this._setPlaceholder(engineName || null); 4493 } 4494 } 4495 4496 /** 4497 * Asynchronously changes the urlbar placeholder to the name of the default 4498 * engine according to the search service when it is initialized. 4499 * 4500 * This should be called around the time of MozAfterPaint. Since the 4501 * placeholder was already initialized to the pre-saved engine name by 4502 * initPlaceHolder when this is called, the update is delayed to avoid 4503 * confusing the user. 4504 */ 4505 async delayedStartupInit() { 4506 // Only delay if requested, and we're not displaying text in the URL bar 4507 // currently. 4508 if (!this.value) { 4509 // Delays changing the URL Bar placeholder and Unified Search Button icon 4510 // until the user is not going to be seeing it, e.g. when there is a value 4511 // entered in the bar, or if there is a tab switch to a tab which has a url 4512 // loaded. We delay the update until the user is out of search mode since 4513 // an alternative placeholder is used in search mode. 4514 let updateListener = () => { 4515 if (this.value && !this.searchMode) { 4516 // By the time the user has switched, they may have changed the engine 4517 // again, so we need to call this function again but with the 4518 // new engine name. 4519 // No need to await for this to finish, we're in a listener here anyway. 4520 this.searchModeSwitcher.updateSearchIcon(); 4521 this._updatePlaceholderFromDefaultEngine(); 4522 this.inputField.removeEventListener("input", updateListener); 4523 this.window.gBrowser.tabContainer.removeEventListener( 4524 "TabSelect", 4525 updateListener 4526 ); 4527 } 4528 }; 4529 4530 this.inputField.addEventListener("input", updateListener); 4531 this.window.gBrowser.tabContainer.addEventListener( 4532 "TabSelect", 4533 updateListener 4534 ); 4535 } else { 4536 await this._updatePlaceholderFromDefaultEngine(); 4537 } 4538 4539 // If we haven't finished initializing, ensure the placeholder 4540 // preference is set for the next startup. 4541 if (this.#isAddressbar) { 4542 lazy.SearchUIUtils.updatePlaceholderNamePreference( 4543 await this._getDefaultSearchEngine(), 4544 this.isPrivate 4545 ); 4546 } 4547 } 4548 4549 /** 4550 * Set Unified Search Button availability. 4551 * 4552 * @param {boolean} available If true Unified Search Button will be available. 4553 */ 4554 setUnifiedSearchButtonAvailability(available) { 4555 this.toggleAttribute("unifiedsearchbutton-available", available); 4556 this.getBrowserState( 4557 this.window.gBrowser.selectedBrowser 4558 ).isUnifiedSearchButtonAvailable = available; 4559 } 4560 4561 /** 4562 * Returns a Promise that resolves with default search engine. 4563 * 4564 * @returns {Promise<nsISearchEngine>} 4565 */ 4566 _getDefaultSearchEngine() { 4567 return this.isPrivate 4568 ? Services.search.getDefaultPrivate() 4569 : Services.search.getDefault(); 4570 } 4571 4572 /** 4573 * This is a wrapper around '_updatePlaceholder' that uses the appropriate 4574 * default engine to get the engine name. 4575 */ 4576 async _updatePlaceholderFromDefaultEngine() { 4577 const defaultEngine = await this._getDefaultSearchEngine(); 4578 this._updatePlaceholder(defaultEngine.name); 4579 } 4580 4581 /** 4582 * Updates the URLBar placeholder for the specified engine, delaying the 4583 * update if required. 4584 * 4585 * Note: The engine name will only be displayed for application-provided 4586 * engines, as we know they should have short names. 4587 * 4588 * @param {string} engineName The search engine name to use for the update. 4589 */ 4590 _updatePlaceholder(engineName) { 4591 if (!engineName) { 4592 throw new Error("Expected an engineName to be specified"); 4593 } 4594 4595 if (this.searchMode || !this.#isAddressbar) { 4596 return; 4597 } 4598 4599 let engine = Services.search.getEngineByName(engineName); 4600 if (engine.isConfigEngine) { 4601 this._setPlaceholder(engineName); 4602 } else { 4603 // Display the default placeholder string. 4604 this._setPlaceholder(null); 4605 } 4606 } 4607 4608 /** 4609 * Sets the URLBar placeholder to either something based on the engine name, 4610 * or the default placeholder. 4611 * 4612 * @param {?string} engineName 4613 * The name of the engine or null to use the default placeholder. 4614 */ 4615 _setPlaceholder(engineName) { 4616 if (!this.#isAddressbar) { 4617 this.document.l10n.setAttributes(this.inputField, "searchbar-input"); 4618 return; 4619 } 4620 4621 let l10nId; 4622 if (lazy.UrlbarPrefs.get("keyword.enabled")) { 4623 l10nId = engineName 4624 ? "urlbar-placeholder-with-name" 4625 : "urlbar-placeholder"; 4626 } else { 4627 l10nId = "urlbar-placeholder-keyword-disabled"; 4628 } 4629 4630 this.document.l10n.setAttributes( 4631 this.inputField, 4632 l10nId, 4633 l10nId == "urlbar-placeholder-with-name" 4634 ? { name: engineName } 4635 : undefined 4636 ); 4637 } 4638 4639 /** 4640 * Determines if we should select all the text in the Urlbar based on the 4641 * Urlbar state, and whether the selection is empty. 4642 */ 4643 #maybeSelectAll() { 4644 if ( 4645 !this._preventClickSelectsAll && 4646 this.#compositionState != lazy.UrlbarUtils.COMPOSITION.COMPOSING && 4647 this.focused && 4648 this.inputField.selectionStart == this.inputField.selectionEnd 4649 ) { 4650 this.select(); 4651 } 4652 } 4653 4654 // Event handlers below. 4655 4656 _on_command(event) { 4657 // Something is executing a command, likely causing a focus change. This 4658 // should not be recorded as an abandonment. If the user is selecting a 4659 // result menu item or entering search mode from a one-off, then they are 4660 // in the same engagement and we should not discard. 4661 if ( 4662 !event.target.classList.contains("urlbarView-result-menuitem") && 4663 (!event.target.classList.contains("searchbar-engine-one-off-item") || 4664 this.searchMode?.entry != "oneoff") 4665 ) { 4666 this.controller.engagementEvent.discard(); 4667 } 4668 } 4669 4670 _on_blur(event) { 4671 lazy.logger.debug("Blur Event"); 4672 // We cannot count every blur events after a missed engagement as abandoment 4673 // because the user may have clicked on some view element that executes 4674 // a command causing a focus change. For example opening preferences from 4675 // the oneoff settings button. 4676 // For now we detect that case by discarding the event on command, but we 4677 // may want to figure out a more robust way to detect abandonment. 4678 this.controller.engagementEvent.record(event, { 4679 searchString: this._lastSearchString, 4680 searchSource: this.getSearchSource(event), 4681 }); 4682 4683 this.focusedViaMousedown = false; 4684 this._handoffSession = undefined; 4685 this._isHandoffSession = false; 4686 this.removeAttribute("focused"); 4687 4688 if (this._autofillPlaceholder && this.userTypedValue) { 4689 // If we were autofilling, remove the autofilled portion, by restoring 4690 // the value to the last typed one. 4691 this.value = this.userTypedValue; 4692 } else if ( 4693 this.value == this._untrimmedValue && 4694 !this.userTypedValue && 4695 !this.focused 4696 ) { 4697 // If the value was untrimmed by _on_focus and didn't change, trim it. 4698 this.value = this._untrimmedValue; 4699 } else { 4700 // We're not updating the value, so just format it. 4701 this.formatValue(); 4702 } 4703 4704 this._resetSearchState(); 4705 4706 // In certain cases, like holding an override key and confirming an entry, 4707 // we don't key a keyup event for the override key, thus we make this 4708 // additional cleanup on blur. 4709 this._clearActionOverride(); 4710 4711 // The extension input sessions depends more on blur than on the fact we 4712 // actually cancel a running query, so we do it here. 4713 if (lazy.ExtensionSearchHandler.hasActiveInputSession()) { 4714 lazy.ExtensionSearchHandler.handleInputCancelled(); 4715 } 4716 4717 // Respect the autohide preference for easier inspecting/debugging via 4718 // the browser toolbox. 4719 if (!lazy.UrlbarPrefs.get("ui.popup.disable_autohide")) { 4720 this.view.close(); 4721 } 4722 4723 // We may have hidden popup notifications, show them again if necessary. 4724 if ( 4725 this.getAttribute("pageproxystate") != "valid" && 4726 this.window.UpdatePopupNotificationsVisibility 4727 ) { 4728 this.window.UpdatePopupNotificationsVisibility(); 4729 } 4730 4731 // If user move the focus to another component while pressing Enter key, 4732 // then keyup at that component, as we can't get the event, clear the promise. 4733 if (this._keyDownEnterDeferred) { 4734 this._keyDownEnterDeferred.resolve(); 4735 this._keyDownEnterDeferred = null; 4736 } 4737 this._isKeyDownWithCtrl = false; 4738 this._isKeyDownWithMeta = false; 4739 this._isKeyDownWithMetaAndLeft = false; 4740 4741 Services.obs.notifyObservers(null, "urlbar-blur"); 4742 } 4743 4744 _on_click(event) { 4745 switch (event.target) { 4746 case this.inputField: 4747 case this._inputContainer: 4748 this.#maybeSelectAll(); 4749 this.#maybeUntrimUrl(); 4750 break; 4751 4752 case this._searchModeIndicatorClose: 4753 if (event.button != 2) { 4754 this.searchMode = null; 4755 if (this.view.oneOffSearchButtons) { 4756 this.view.oneOffSearchButtons.selectedButton = null; 4757 } 4758 if (this.view.isOpen) { 4759 this.startQuery({ 4760 event, 4761 }); 4762 } 4763 } 4764 break; 4765 4766 case this._revertButton: 4767 this.handleRevert(); 4768 this.select(); 4769 break; 4770 4771 case this.goButton: 4772 this.handleCommand(event); 4773 break; 4774 } 4775 } 4776 4777 _on_contextmenu(event) { 4778 this.#lazy.addSearchEngineHelper.refreshContextMenu(event); 4779 4780 // Context menu opened via keyboard shortcut. 4781 if (!event.button) { 4782 return; 4783 } 4784 4785 this.#maybeSelectAll(); 4786 } 4787 4788 _on_focus(event) { 4789 lazy.logger.debug("Focus Event"); 4790 if (!this._hideFocus) { 4791 this.toggleAttribute("focused", true); 4792 } 4793 4794 // If the value was trimmed, check whether we should untrim it. 4795 // This is necessary when a protocol was typed, but the whole url has 4796 // invalid parts, like the origin, then editing and confirming the trimmed 4797 // value would execute a search instead of visiting the typed url. 4798 if (this._protocolIsTrimmed) { 4799 let untrim = false; 4800 let fixedURI = this._getURIFixupInfo(this.value)?.preferredURI; 4801 if (fixedURI) { 4802 try { 4803 let expectedURI = Services.io.newURI(this._untrimmedValue); 4804 if ( 4805 lazy.UrlbarPrefs.getScotchBonnetPref("trimHttps") && 4806 this._untrimmedValue.startsWith("https://") 4807 ) { 4808 untrim = 4809 fixedURI.displaySpec.replace("http://", "https://") != 4810 expectedURI.displaySpec; // FIXME bug 1847723: Figure out a way to do this without manually messing with the fixed up URI. 4811 } else { 4812 untrim = fixedURI.displaySpec != expectedURI.displaySpec; 4813 } 4814 } catch (ex) { 4815 untrim = true; 4816 } 4817 } 4818 if (untrim) { 4819 this._setValue(this._untrimmedValue); 4820 } 4821 } 4822 4823 if (this.focusedViaMousedown) { 4824 this.view.autoOpen({ event }); 4825 } else { 4826 if (this._untrimOnFocusAfterKeydown) { 4827 // While the mousedown focus has more complex implications due to drag 4828 // and double-click select, we can untrim immediately when the urlbar is 4829 // focused by a keyboard shortcut. 4830 this.#maybeUntrimUrl({ ignoreSelection: true }); 4831 } 4832 4833 if (this.inputField.hasAttribute("refocused-by-panel")) { 4834 this.#maybeSelectAll(); 4835 } 4836 } 4837 4838 this._updateUrlTooltip(); 4839 this.formatValue(); 4840 4841 // Hide popup notifications, to reduce visual noise. 4842 if ( 4843 this.getAttribute("pageproxystate") != "valid" && 4844 this.window.UpdatePopupNotificationsVisibility 4845 ) { 4846 this.window.UpdatePopupNotificationsVisibility(); 4847 } 4848 4849 Services.obs.notifyObservers(null, "urlbar-focus"); 4850 } 4851 4852 _on_mouseover() { 4853 this._updateUrlTooltip(); 4854 } 4855 4856 _on_draggableregionleftmousedown() { 4857 if (!lazy.UrlbarPrefs.get("ui.popup.disable_autohide")) { 4858 this.view.close(); 4859 } 4860 } 4861 4862 _on_mousedown(event) { 4863 switch (event.currentTarget) { 4864 case this: { 4865 this._mousedownOnUrlbarDescendant = true; 4866 if ( 4867 event.composedTarget != this.inputField && 4868 event.composedTarget != this._inputContainer 4869 ) { 4870 break; 4871 } 4872 4873 this.focusedViaMousedown = !this.focused; 4874 this._preventClickSelectsAll = this.focused; 4875 4876 // Keep the focus status, since the attribute may be changed 4877 // upon calling this.focus(). 4878 const hasFocus = this.hasAttribute("focused"); 4879 if (event.composedTarget != this.inputField) { 4880 this.focus(); 4881 } 4882 4883 // The rest of this case only cares about left clicks. 4884 if (event.button != 0) { 4885 break; 4886 } 4887 4888 // Clear any previous selection unless we are focused, to ensure it 4889 // doesn't affect drag selection. 4890 if (this.focusedViaMousedown) { 4891 this.inputField.setSelectionRange(0, 0); 4892 } 4893 4894 // Do not suppress the focus border if we are already focused. If we 4895 // did, we'd hide the focus border briefly then show it again if the 4896 // user has Top Sites disabled, creating a flashing effect. 4897 this.view.autoOpen({ 4898 event, 4899 suppressFocusBorder: !hasFocus, 4900 }); 4901 break; 4902 } 4903 case this.window: 4904 if (this._mousedownOnUrlbarDescendant) { 4905 this._mousedownOnUrlbarDescendant = false; 4906 break; 4907 } 4908 // Don't close the view when clicking on a tab; we may want to keep the 4909 // view open on tab switch, and the TabSelect event arrived earlier. 4910 if (event.target.closest("tab")) { 4911 break; 4912 } 4913 4914 // Close the view when clicking on toolbars and other UI pieces that 4915 // might not automatically remove focus from the input. 4916 // Respect the autohide preference for easier inspecting/debugging via 4917 // the browser toolbox. 4918 if (!lazy.UrlbarPrefs.get("ui.popup.disable_autohide")) { 4919 if (this.view.isOpen && !this.hasAttribute("focused")) { 4920 // In this case, as blur event never happen from the inputField, we 4921 // record abandonment event explicitly. 4922 let blurEvent = new FocusEvent("blur", { 4923 relatedTarget: this.inputField, 4924 }); 4925 this.controller.engagementEvent.record(blurEvent, { 4926 searchString: this._lastSearchString, 4927 searchSource: this.getSearchSource(blurEvent), 4928 }); 4929 } 4930 4931 this.view.close(); 4932 } 4933 break; 4934 } 4935 } 4936 4937 _on_input(event) { 4938 if ( 4939 this._autofillPlaceholder && 4940 this.value === this.userTypedValue && 4941 (event.inputType === "deleteContentBackward" || 4942 event.inputType === "deleteContentForward") 4943 ) { 4944 // Take a telemetry if user deleted whole autofilled value. 4945 Glean.urlbar.autofillDeletion.add(1); 4946 } 4947 4948 let value = this.value; 4949 this.valueIsTyped = true; 4950 this._untrimmedValue = value; 4951 this._protocolIsTrimmed = false; 4952 this._resultForCurrentValue = null; 4953 4954 this.userTypedValue = value; 4955 // Unset userSelectionBehavior because the user is modifying the search 4956 // string, thus there's no valid selection. This is also used by the view 4957 // to set "aria-activedescendant", thus it should never get stale. 4958 this.controller.userSelectionBehavior = "none"; 4959 4960 let compositionState = this.#compositionState; 4961 let compositionClosedPopup = this.#compositionClosedPopup; 4962 4963 // Clear composition values if we're no more composing. 4964 if (this.#compositionState != lazy.UrlbarUtils.COMPOSITION.COMPOSING) { 4965 this.#compositionState = lazy.UrlbarUtils.COMPOSITION.NONE; 4966 this.#compositionClosedPopup = false; 4967 } 4968 4969 this.toggleAttribute("usertyping", value); 4970 this.removeAttribute("actiontype"); 4971 4972 if ( 4973 this.getAttribute("pageproxystate") == "valid" && 4974 this.value != this._lastValidURLStr 4975 ) { 4976 this.setPageProxyState("invalid", true); 4977 } 4978 4979 let state = this.getBrowserState(this.window.gBrowser.selectedBrowser); 4980 if ( 4981 state.persist?.shouldPersist && 4982 this.value !== state.persist.searchTerms 4983 ) { 4984 state.persist.shouldPersist = false; 4985 this.removeAttribute("persistsearchterms"); 4986 } 4987 4988 if (this.view.isOpen) { 4989 if (lazy.UrlbarPrefs.get("closeOtherPanelsOnOpen")) { 4990 // UrlbarView rolls up all popups when it opens, but we should 4991 // do the same for UrlbarInput when it's already open in case 4992 // a tab preview was opened 4993 this.window.docShell.treeOwner 4994 .QueryInterface(Ci.nsIInterfaceRequestor) 4995 .getInterface(Ci.nsIAppWindow) 4996 .rollupAllPopups(); 4997 } 4998 if (!value && !lazy.UrlbarPrefs.get("suggest.topsites")) { 4999 this.view.clear(); 5000 if (!this.searchMode || !this.view.oneOffSearchButtons?.hasView) { 5001 this.view.close(); 5002 return; 5003 } 5004 } 5005 } else { 5006 this.view.clear(); 5007 } 5008 5009 this.view.removeAccessibleFocus(); 5010 5011 // During composition with an IME, the following events happen in order: 5012 // 1. a compositionstart event 5013 // 2. some input events 5014 // 3. a compositionend event 5015 // 4. an input event 5016 5017 // We should do nothing during composition or if composition was canceled 5018 // and we didn't close the popup on composition start. 5019 if ( 5020 !lazy.UrlbarPrefs.get("keepPanelOpenDuringImeComposition") && 5021 (compositionState == lazy.UrlbarUtils.COMPOSITION.COMPOSING || 5022 (compositionState == lazy.UrlbarUtils.COMPOSITION.CANCELED && 5023 !compositionClosedPopup)) 5024 ) { 5025 return; 5026 } 5027 5028 // Autofill only when text is inserted (i.e., event.data is not empty) and 5029 // it's not due to pasting. 5030 const allowAutofill = 5031 (!lazy.UrlbarPrefs.get("keepPanelOpenDuringImeComposition") || 5032 compositionState !== lazy.UrlbarUtils.COMPOSITION.COMPOSING) && 5033 !!event.data && 5034 !lazy.UrlbarUtils.isPasteEvent(event) && 5035 this._maybeAutofillPlaceholder(value); 5036 5037 this.startQuery({ 5038 searchString: value, 5039 allowAutofill, 5040 resetSearchState: false, 5041 event, 5042 }); 5043 } 5044 5045 _on_selectionchange() { 5046 // Confirm placeholder as user text if it gets explicitly deselected. This 5047 // happens when the user wants to modify the autofilled text by either 5048 // clicking on it, or pressing HOME, END, RIGHT, … 5049 if ( 5050 this._autofillPlaceholder && 5051 this._autofillPlaceholder.value == this.value && 5052 (this._autofillPlaceholder.selectionStart != this.selectionStart || 5053 this._autofillPlaceholder.selectionEnd != this.selectionEnd) 5054 ) { 5055 this._autofillPlaceholder = null; 5056 this.userTypedValue = this.value; 5057 } 5058 } 5059 5060 _on_select() { 5061 // On certain user input, AutoCopyListener::OnSelectionChange() updates 5062 // the primary selection with user-selected text (when supported). 5063 // Selection::NotifySelectionListeners() then dispatches a "select" event 5064 // under similar conditions via TextInputListener::OnSelectionChange(). 5065 // This event is received here in order to replace the primary selection 5066 // from the editor with text having the adjustments of 5067 // _getSelectedValueForClipboard(), such as adding the scheme for the url. 5068 // 5069 // Other "select" events are also received, however, and must be excluded. 5070 if ( 5071 // _suppressPrimaryAdjustment is set during select(). Don't update 5072 // the primary selection because that is not the intent of user input, 5073 // which may be new tab or urlbar focus. 5074 this._suppressPrimaryAdjustment || 5075 // The check on isHandlingUserInput filters out async "select" events 5076 // from setSelectionRange(), which occur when autofill text is selected. 5077 !this.window.windowUtils.isHandlingUserInput || 5078 !Services.clipboard.isClipboardTypeSupported( 5079 Services.clipboard.kSelectionClipboard 5080 ) 5081 ) { 5082 return; 5083 } 5084 5085 let val = this._getSelectedValueForClipboard(); 5086 if (!val) { 5087 return; 5088 } 5089 5090 lazy.ClipboardHelper.copyStringToClipboard( 5091 val, 5092 Services.clipboard.kSelectionClipboard 5093 ); 5094 } 5095 5096 _on_overflow(event) { 5097 const targetIsPlaceholder = 5098 event.originalTarget.implementedPseudoElement == "::placeholder"; 5099 // We only care about the non-placeholder text. 5100 // This shouldn't be needed, see bug 1487036. 5101 if (targetIsPlaceholder) { 5102 return; 5103 } 5104 this._overflowing = true; 5105 this.updateTextOverflow(); 5106 } 5107 5108 _on_underflow(event) { 5109 const targetIsPlaceholder = 5110 event.originalTarget.implementedPseudoElement == "::placeholder"; 5111 // We only care about the non-placeholder text. 5112 // This shouldn't be needed, see bug 1487036. 5113 if (targetIsPlaceholder) { 5114 return; 5115 } 5116 this._overflowing = false; 5117 5118 this.updateTextOverflow(); 5119 5120 this._updateUrlTooltip(); 5121 } 5122 5123 _on_paste(event) { 5124 let originalPasteData = event.clipboardData.getData("text/plain"); 5125 if (!originalPasteData) { 5126 return; 5127 } 5128 5129 let oldValue = this.value; 5130 let oldStart = oldValue.substring(0, this.selectionStart); 5131 // If there is already non-whitespace content in the URL bar 5132 // preceding the pasted content, it's not necessary to check 5133 // protocols used by the pasted content: 5134 if (oldStart.trim()) { 5135 return; 5136 } 5137 let oldEnd = oldValue.substring(this.selectionEnd); 5138 5139 const pasteData = this.sanitizeTextFromClipboard(originalPasteData); 5140 5141 if (originalPasteData != pasteData) { 5142 // Unfortunately we're not allowed to set the bits being pasted 5143 // so cancel this event: 5144 event.preventDefault(); 5145 event.stopImmediatePropagation(); 5146 5147 const value = oldStart + pasteData + oldEnd; 5148 this._setValue(value, { valueIsTyped: true }); 5149 this.userTypedValue = value; 5150 5151 // Since we prevent the default paste event, we have to ensure the 5152 // pageproxystate is updated. The paste event replaces the actual current 5153 // page's URL with user-typed content, so we should set pageproxystate to 5154 // invalid. 5155 if (this.getAttribute("pageproxystate") == "valid") { 5156 this.setPageProxyState("invalid"); 5157 } 5158 this.toggleAttribute("usertyping", this._untrimmedValue); 5159 5160 // Fix up cursor/selection: 5161 let newCursorPos = oldStart.length + pasteData.length; 5162 this.inputField.setSelectionRange(newCursorPos, newCursorPos); 5163 5164 this.startQuery({ 5165 searchString: this.value, 5166 allowAutofill: false, 5167 resetSearchState: false, 5168 event, 5169 }); 5170 } 5171 } 5172 5173 /** 5174 * Sanitize and process data retrieved from the clipboard 5175 * 5176 * @param {string} clipboardData 5177 * The original data retrieved from the clipboard. 5178 * @returns {string} 5179 * The sanitized paste data, ready to use. 5180 */ 5181 sanitizeTextFromClipboard(clipboardData) { 5182 let fixedURI, keywordAsSent; 5183 try { 5184 ({ fixedURI, keywordAsSent } = Services.uriFixup.getFixupURIInfo( 5185 clipboardData, 5186 Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS | 5187 Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP 5188 )); 5189 } catch (e) {} 5190 5191 let pasteData; 5192 if (keywordAsSent) { 5193 // For performance reasons, we don't want to beautify a long string. 5194 if (clipboardData.length < 500) { 5195 // For only keywords, replace any white spaces including line break 5196 // with white space. 5197 pasteData = clipboardData.replace(/\s/g, " "); 5198 } else { 5199 pasteData = clipboardData; 5200 } 5201 } else if ( 5202 fixedURI?.scheme == "data" && 5203 !fixedURI.spec.match(/^data:.+;base64,/) 5204 ) { 5205 // For data url without base64, replace line break with white space. 5206 pasteData = clipboardData.replace(/[\r\n]/g, " "); 5207 } else { 5208 // For normal url or data url having basic64, or if fixup failed, just 5209 // remove line breaks. 5210 pasteData = clipboardData.replace(/[\r\n]/g, ""); 5211 } 5212 5213 return lazy.UrlbarUtils.stripUnsafeProtocolOnPaste(pasteData); 5214 } 5215 5216 /** 5217 * Generate a UrlbarQueryContext from the current context. 5218 * 5219 * @param {object} [options] 5220 * Optional params 5221 * @param {boolean} [options.allowAutofill] 5222 * Whether autofill is enabled. 5223 * @param {string} [options.searchString] 5224 * The string being searched. 5225 * @param {object} [options.event] 5226 * The event triggering the query. 5227 * @returns {UrlbarQueryContext} 5228 * The queryContext object. 5229 */ 5230 #makeQueryContext({ 5231 allowAutofill = true, 5232 searchString = null, 5233 event = null, 5234 } = {}) { 5235 // When we are in actions search mode we can show more results so 5236 // increase the limit. 5237 let maxResults = 5238 this.searchMode?.source != lazy.UrlbarUtils.RESULT_SOURCE.ACTIONS 5239 ? lazy.UrlbarPrefs.get("maxRichResults") 5240 : UNLIMITED_MAX_RESULTS; 5241 let options = { 5242 allowAutofill, 5243 isPrivate: this.isPrivate, 5244 sapName: this.sapName, 5245 maxResults, 5246 searchString, 5247 userContextId: parseInt( 5248 this.window.gBrowser.selectedBrowser.getAttribute("usercontextid") || 0 5249 ), 5250 tabGroup: this.window.gBrowser.selectedTab.group?.id ?? null, 5251 currentPage: this.window.gBrowser.currentURI.spec, 5252 prohibitRemoteResults: 5253 event && 5254 lazy.UrlbarUtils.isPasteEvent(event) && 5255 lazy.UrlbarPrefs.get("maxCharsForSearchSuggestions") < 5256 event.data?.length, 5257 }; 5258 5259 if (this.searchMode) { 5260 options.searchMode = this.searchMode; 5261 if (this.searchMode.source) { 5262 options.sources = [this.searchMode.source]; 5263 } 5264 } 5265 5266 return new lazy.UrlbarQueryContext(options); 5267 } 5268 5269 _on_scrollend() { 5270 this.updateTextOverflow(); 5271 } 5272 5273 _on_TabSelect() { 5274 // TabSelect may be activated by a keyboard shortcut and cause the urlbar 5275 // to take focus, in this case we should not untrim. 5276 this._untrimOnFocusAfterKeydown = false; 5277 this._gotTabSelect = true; 5278 this._afterTabSelectAndFocusChange(); 5279 } 5280 5281 _on_TabClose(event) { 5282 this.controller.engagementEvent.handleBounceEventTrigger( 5283 event.target.linkedBrowser 5284 ); 5285 5286 if (this.view.isOpen) { 5287 // Refresh results when a tab is closed while the results view is open. 5288 // This prevents switch-to-tab results from remaining in the results 5289 // list after their tab is closed. 5290 this.startQuery(); 5291 } 5292 } 5293 5294 _on_beforeinput(event) { 5295 if (event.data && this._keyDownEnterDeferred) { 5296 // Ignore char key input while processing enter key. 5297 event.preventDefault(); 5298 } 5299 } 5300 5301 _on_keydown(event) { 5302 if (event.currentTarget == this.window) { 5303 // It would be great if we could more easily detect the user focusing the 5304 // address bar through a keyboard shortcut, but F6 and TAB bypass are 5305 // not going through commands handling. 5306 // Also note we'll unset this on TabSelect, as it can focus the address 5307 // bar but we should not untrim in that case. 5308 this._untrimOnFocusAfterKeydown = !this.focused; 5309 return; 5310 } 5311 5312 // Repeated KeyboardEvents can easily cause subtle bugs in this logic, if 5313 // not properly handled, so let's first handle things that should not be 5314 // evaluated repeatedly. 5315 if (!event.repeat) { 5316 this.#allTextSelectedOnKeyDown = this.#allTextSelected; 5317 5318 if (event.keyCode === KeyEvent.DOM_VK_RETURN) { 5319 if (this._keyDownEnterDeferred) { 5320 this._keyDownEnterDeferred.reject(); 5321 } 5322 this._keyDownEnterDeferred = Promise.withResolvers(); 5323 event._disableCanonization = 5324 AppConstants.platform == "macosx" 5325 ? this._isKeyDownWithMeta 5326 : this._isKeyDownWithCtrl; 5327 } 5328 5329 // Now set the keydown trackers for the current event, anything that wants 5330 // to check the previous events should have happened before this point. 5331 // The previously value is persisted until keyup, as we check if the 5332 // modifiers were down, even if other keys are pressed in the meanwhile. 5333 if (event.ctrlKey && event.keyCode != KeyEvent.DOM_VK_CONTROL) { 5334 this._isKeyDownWithCtrl = true; 5335 } 5336 if (event.metaKey && event.keyCode != KeyEvent.DOM_VK_META) { 5337 this._isKeyDownWithMeta = true; 5338 } 5339 // This is used in keyup, so it can be set every time. 5340 this._isKeyDownWithMetaAndLeft = 5341 this._isKeyDownWithMeta && 5342 !event.shiftKey && 5343 event.keyCode == KeyEvent.DOM_VK_LEFT; 5344 5345 this._toggleActionOverride(event); 5346 } 5347 5348 // Due to event deferring, it's possible preventDefault() won't be invoked 5349 // soon enough to actually prevent some of the default behaviors, thus we 5350 // have to handle the event "twice". This first immediate call passes false 5351 // as second argument so that handleKeyNavigation will only simulate the 5352 // event handling, without actually executing actions. 5353 // TODO (Bug 1541806): improve this handling, maybe by delaying actions 5354 // instead of events. 5355 if (this.eventBufferer.shouldDeferEvent(event)) { 5356 this.controller.handleKeyNavigation(event, false); 5357 } 5358 this.eventBufferer.maybeDeferEvent(event, () => { 5359 this.controller.handleKeyNavigation(event); 5360 }); 5361 } 5362 5363 async _on_keyup(event) { 5364 if (event.currentTarget == this.window) { 5365 this._untrimOnFocusAfterKeydown = false; 5366 return; 5367 } 5368 5369 if (this.#allTextSelectedOnKeyDown) { 5370 let moveCursorToStart = this.#isHomeKeyUpEvent(event); 5371 // We must set the selection immediately because: 5372 // - on Mac Fn + Left is not handled properly as Home 5373 // - untrim depends on text not being fully selected. 5374 if (moveCursorToStart) { 5375 this.selectionStart = this.selectionEnd = 0; 5376 } 5377 this.#maybeUntrimUrl({ moveCursorToStart }); 5378 } 5379 if (event.keyCode === KeyEvent.DOM_VK_META) { 5380 this._isKeyDownWithMeta = false; 5381 this._isKeyDownWithMetaAndLeft = false; 5382 } 5383 if (event.keyCode === KeyEvent.DOM_VK_CONTROL) { 5384 this._isKeyDownWithCtrl = false; 5385 } 5386 5387 this._toggleActionOverride(event); 5388 5389 // Pressing Enter key while pressing Meta key, and next, even when releasing 5390 // Enter key before releasing Meta key, the keyup event is not fired. 5391 // Therefore, if Enter keydown is detecting, continue the post processing 5392 // for Enter key when any keyup event is detected. 5393 if (this._keyDownEnterDeferred) { 5394 if (this._keyDownEnterDeferred.loadedContent) { 5395 try { 5396 const loadingBrowser = await this._keyDownEnterDeferred.promise; 5397 // Ensure the selected browser didn't change in the meanwhile. 5398 if (this.window.gBrowser.selectedBrowser === loadingBrowser) { 5399 loadingBrowser.focus(); 5400 // Make sure the domain name stays visible for spoof protection and usability. 5401 this.inputField.setSelectionRange(0, 0); 5402 } 5403 } catch (ex) { 5404 // Not all the Enter actions in the urlbar will cause a navigation, then it 5405 // is normal for this to be rejected. 5406 // If _keyDownEnterDeferred was rejected on keydown, we don't nullify it here 5407 // to ensure not overwriting the new value created by keydown. 5408 } 5409 } else { 5410 // Discard the _keyDownEnterDeferred promise to receive any key inputs immediately. 5411 this._keyDownEnterDeferred.resolve(); 5412 } 5413 5414 this._keyDownEnterDeferred = null; 5415 } 5416 } 5417 5418 _on_compositionstart() { 5419 if (this.#compositionState == lazy.UrlbarUtils.COMPOSITION.COMPOSING) { 5420 throw new Error("Trying to start a nested composition?"); 5421 } 5422 this.#compositionState = lazy.UrlbarUtils.COMPOSITION.COMPOSING; 5423 5424 if (lazy.UrlbarPrefs.get("keepPanelOpenDuringImeComposition")) { 5425 return; 5426 } 5427 5428 // Close the view. This will also stop searching. 5429 if (this.view.isOpen) { 5430 // We're closing the view, but we want to retain search mode if the 5431 // selected result was previewing it. 5432 if (this.searchMode) { 5433 // If we entered search mode with an empty string, clear userTypedValue, 5434 // otherwise confirmSearchMode may try to set it as value. 5435 // This can happen for example if we entered search mode typing a 5436 // a partial engine domain and selecting a tab-to-search result. 5437 if (!this.value) { 5438 this.userTypedValue = null; 5439 } 5440 this.confirmSearchMode(); 5441 } 5442 this.#compositionClosedPopup = true; 5443 this.view.close(); 5444 } else { 5445 this.#compositionClosedPopup = false; 5446 } 5447 } 5448 5449 _on_compositionend(event) { 5450 if (this.#compositionState != lazy.UrlbarUtils.COMPOSITION.COMPOSING) { 5451 throw new Error("Trying to stop a non existing composition?"); 5452 } 5453 5454 if (!lazy.UrlbarPrefs.get("keepPanelOpenDuringImeComposition")) { 5455 // Clear the selection and the cached result, since they refer to the 5456 // state before this composition. A new input even will be generated 5457 // after this. 5458 this.view.clearSelection(); 5459 this._resultForCurrentValue = null; 5460 } 5461 5462 // We can't yet retrieve the committed value from the editor, since it isn't 5463 // completely committed yet. We'll handle it at the next input event. 5464 this.#compositionState = event.data 5465 ? lazy.UrlbarUtils.COMPOSITION.COMMIT 5466 : lazy.UrlbarUtils.COMPOSITION.CANCELED; 5467 } 5468 5469 _on_dragstart(event) { 5470 // Drag only if the gesture starts from the input field. 5471 let nodePosition = this.inputField.compareDocumentPosition( 5472 event.originalTarget 5473 ); 5474 if ( 5475 event.target != this.inputField && 5476 !(nodePosition & Node.DOCUMENT_POSITION_CONTAINED_BY) 5477 ) { 5478 return; 5479 } 5480 5481 // Don't cover potential drop targets on the toolbars or in content. 5482 this.view.close(); 5483 5484 // Only customize the drag data if the entire value is selected and it's a 5485 // loaded URI. Use default behavior otherwise. 5486 if ( 5487 !this.#allTextSelected || 5488 this.getAttribute("pageproxystate") != "valid" 5489 ) { 5490 return; 5491 } 5492 5493 let uri = this.makeURIReadable(this.window.gBrowser.currentURI); 5494 let href = uri.displaySpec; 5495 let title = this.window.gBrowser.contentTitle || href; 5496 5497 event.dataTransfer.setData("text/x-moz-url", `${href}\n${title}`); 5498 event.dataTransfer.setData("text/plain", href); 5499 event.dataTransfer.setData("text/html", `<a href="${href}">${title}</a>`); 5500 event.dataTransfer.effectAllowed = "copyLink"; 5501 event.stopPropagation(); 5502 } 5503 5504 /** 5505 * Handles dragover events for the input. 5506 * 5507 * @param {DragEvent} event 5508 */ 5509 _on_dragover(event) { 5510 if (!getDroppableData(event)) { 5511 event.dataTransfer.dropEffect = "none"; 5512 } 5513 } 5514 5515 /** 5516 * Handles dropping of data on the input. 5517 * 5518 * @param {DragEvent} event 5519 */ 5520 _on_drop(event) { 5521 let droppedData = getDroppableData(event); 5522 let droppedString = URL.isInstance(droppedData) 5523 ? droppedData.href 5524 : droppedData; 5525 if ( 5526 droppedString && 5527 droppedString !== this.window.gBrowser.currentURI.spec 5528 ) { 5529 this.value = droppedString; 5530 this.setPageProxyState("invalid"); 5531 this.focus(); 5532 if (this.#isAddressbar) { 5533 // If we're an address bar, we automatically open the dropped address or 5534 // submit the dropped string to the search engine. 5535 let principal = 5536 Services.droppedLinkHandler.getTriggeringPrincipal(event); 5537 // To simplify tracking of events, register an initial event for event 5538 // telemetry, to replace the missing input event. 5539 let queryContext = this.#makeQueryContext({ 5540 searchString: droppedString, 5541 }); 5542 this.controller.setLastQueryContextCache(queryContext); 5543 this.controller.engagementEvent.start(event, queryContext); 5544 this.handleNavigation({ triggeringPrincipal: principal }); 5545 // For safety reasons, in the drop case we don't want to immediately show 5546 // the dropped value, instead we want to keep showing the current page 5547 // url until an onLocationChange happens. 5548 // See the handling in `setURI` for further details. 5549 this.userTypedValue = null; 5550 this.setURI({ dueToTabSwitch: true }); 5551 } else { 5552 // If we're a search bar, allow for getting search suggestions, changing 5553 // the search engine, or modifying the search term before submitting. 5554 this.startQuery({ 5555 searchString: droppedString, 5556 event, 5557 }); 5558 } 5559 } 5560 } 5561 5562 _on_customizationstarting() { 5563 this.incrementBreakoutBlockerCount(); 5564 this.blur(); 5565 } 5566 5567 _on_aftercustomization() { 5568 this.decrementBreakoutBlockerCount(); 5569 this.#updateLayoutBreakout(); 5570 } 5571 5572 uiDensityChanged() { 5573 if (this.#breakoutBlockerCount) { 5574 return; 5575 } 5576 this.#updateLayoutBreakout(); 5577 } 5578 5579 _on_toolbarvisibilitychange() { 5580 this.#updateTextboxPositionNextFrame(); 5581 } 5582 5583 _on_DOMMenuBarActive() { 5584 this.#updateTextboxPositionNextFrame(); 5585 } 5586 5587 _on_DOMMenuBarInactive() { 5588 this.#updateTextboxPositionNextFrame(); 5589 } 5590 5591 #allTextSelectedOnKeyDown = false; 5592 get #allTextSelected() { 5593 return this.selectionStart == 0 && this.selectionEnd == this.value.length; 5594 } 5595 5596 /** 5597 * @param {string} value 5598 * A untrimmed address bar input. 5599 * @returns {nsILoadInfo.SchemelessInputType} 5600 * Returns `Ci.nsILoadInfo.SchemelessInputTypeSchemeless` if the input 5601 * doesn't start with a scheme relevant for schemeless HTTPS-First 5602 * (http://, https:// and file://). 5603 * Returns `Ci.nsILoadInfo.SchemelessInputTypeSchemeful` if it does have a scheme. 5604 */ 5605 #getSchemelessInput(value) { 5606 return ["http://", "https://", "file://"].every( 5607 scheme => !value.trim().startsWith(scheme) 5608 ) 5609 ? Ci.nsILoadInfo.SchemelessInputTypeSchemeless 5610 : Ci.nsILoadInfo.SchemelessInputTypeSchemeful; 5611 } 5612 5613 get #isOpenedPageInBlankTargetLoading() { 5614 return ( 5615 this.window.gBrowser.selectedBrowser.browsingContext.sessionHistory 5616 ?.count === 0 && 5617 this.window.gBrowser.selectedBrowser.browsingContext 5618 .nonWebControlledBlankURI 5619 ); 5620 } 5621 5622 // Search modes are per browser and are stored in the `searchModes` property of this map. 5623 // For a browser, search mode can be in preview mode, confirmed, or both. 5624 // Typically, search mode is entered in preview mode with a particular 5625 // source and is confirmed with the same source once a query starts. It's 5626 // also possible for a confirmed search mode to be replaced with a preview 5627 // mode with a different source, and in those cases, we need to re-confirm 5628 // search mode when preview mode is exited. In addition, only confirmed 5629 // search modes should be restored across sessions. We therefore need to 5630 // keep track of both the current confirmed and preview modes, per browser. 5631 // 5632 // For each browser with a search mode, this maps the browser to an object 5633 // like this: { preview, confirmed }. Both `preview` and `confirmed` are 5634 // search mode objects; see the setSearchMode documentation. Either one may 5635 // be undefined if that particular mode is not active for the browser. 5636 5637 /** 5638 * Tracks a state object per browser. 5639 */ 5640 #browserStates = new WeakMap(); 5641 5642 get #selectedText() { 5643 return this.editor.selection.toStringWithFormat( 5644 "text/plain", 5645 Ci.nsIDocumentEncoder.OutputPreformatted | 5646 Ci.nsIDocumentEncoder.OutputRaw, 5647 0 5648 ); 5649 } 5650 5651 /** 5652 * Check whether a key event has a similar effect as the Home key. 5653 * 5654 * @param {KeyboardEvent} event A Keyboard event 5655 * @returns {boolean} Whether the even will act like the Home key. 5656 */ 5657 #isHomeKeyUpEvent(event) { 5658 let isMac = AppConstants.platform === "macosx"; 5659 return ( 5660 // On MacOS this can be generated with Fn + Left. 5661 event.keyCode == KeyEvent.DOM_VK_HOME || 5662 // Windows and Linux also support Ctrl + Left. 5663 (!isMac && 5664 event.keyCode == KeyboardEvent.DOM_VK_LEFT && 5665 event.ctrlKey && 5666 !event.shiftKey) || 5667 // MacOS supports other combos to move cursor at the start of the line. 5668 // For example Ctrl + A. 5669 (isMac && 5670 event.keyCode == KeyboardEvent.DOM_VK_A && 5671 event.ctrlKey && 5672 !event.shiftKey) || 5673 // And also Cmd (Meta) + Left. 5674 // Unfortunately on MacOS it's not possible to detect combos with the meta 5675 // key during the keyup event, due to how the OS handles events. Thus we 5676 // record the combo on keydown, and check for it here. 5677 (isMac && 5678 event.keyCode == KeyEvent.DOM_VK_META && 5679 this._isKeyDownWithMetaAndLeft) 5680 ); 5681 } 5682 5683 #canHandleAsBlankPage(spec) { 5684 return this.window.isBlankPageURL(spec) || spec == "about:privatebrowsing"; 5685 } 5686 } 5687 5688 /** 5689 * Tries to extract droppable data from a DND event. 5690 * 5691 * @param {DragEvent} event The DND event to examine. 5692 * @returns {URL|string|null} 5693 * null if there's a security reason for which we should do nothing. 5694 * A URL object if it's a value we can load. 5695 * A string value otherwise. 5696 */ 5697 function getDroppableData(event) { 5698 let links; 5699 try { 5700 links = Services.droppedLinkHandler.dropLinks(event); 5701 } catch (ex) { 5702 // This is either an unexpected failure or a security exception; in either 5703 // case we should always return null. 5704 return null; 5705 } 5706 // The URL bar automatically handles inputs with newline characters, 5707 // so we can get away with treating text/x-moz-url flavours as text/plain. 5708 if (links[0]?.url) { 5709 event.preventDefault(); 5710 let href = links[0].url; 5711 if (lazy.UrlbarUtils.stripUnsafeProtocolOnPaste(href) != href) { 5712 // We may have stripped an unsafe protocol like javascript: and if so 5713 // there's no point in handling a partial drop. 5714 event.stopImmediatePropagation(); 5715 return null; 5716 } 5717 5718 // If this fails, checkLoadURIStrWithPrincipal would also fail, 5719 // as that's what it does with things that don't pass the IO 5720 // service's newURI constructor without fixup. It's conceivable we 5721 // may want to relax this check in the future (so e.g. www.foo.com 5722 // gets fixed up), but not right now. 5723 let url = URL.parse(href); 5724 if (url) { 5725 // If we succeed, try to pass security checks. If this works, return the 5726 // URL object. If the *security checks* fail, return null. 5727 try { 5728 let principal = 5729 Services.droppedLinkHandler.getTriggeringPrincipal(event); 5730 Services.scriptSecurityManager.checkLoadURIStrWithPrincipal( 5731 principal, 5732 url.href, 5733 Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL 5734 ); 5735 return url; 5736 } catch (ex) { 5737 return null; 5738 } 5739 } 5740 // We couldn't make a URL out of this. Continue on, and return text below. 5741 } 5742 // Handle as text. 5743 return event.dataTransfer.getData("text/plain"); 5744 } 5745 5746 /** 5747 * Decodes the given URI for displaying it in the address bar without losing 5748 * information, such that hitting Enter again will load the same URI. 5749 * 5750 * @param {nsIURI} aURI 5751 * The URI to decode 5752 * @returns {string} 5753 * The decoded URI 5754 */ 5755 function losslessDecodeURI(aURI) { 5756 let scheme = aURI.scheme; 5757 let value = aURI.displaySpec; 5758 5759 // Try to decode as UTF-8 if there's no encoding sequence that we would break. 5760 if (!/%25(?:3B|2F|3F|3A|40|26|3D|2B|24|2C|23)/i.test(value)) { 5761 let decodeASCIIOnly = !["https", "http", "file", "ftp"].includes(scheme); 5762 if (decodeASCIIOnly) { 5763 // This only decodes ascii characters (hex) 20-7e, except 25 (%). 5764 // This avoids both cases stipulated below (%-related issues, and \r, \n 5765 // and \t, which would be %0d, %0a and %09, respectively) as well as any 5766 // non-US-ascii characters. 5767 value = value.replace( 5768 /%(2[0-4]|2[6-9a-f]|[3-6][0-9a-f]|7[0-9a-e])/g, 5769 decodeURI 5770 ); 5771 } else { 5772 try { 5773 value = decodeURI(value) 5774 // decodeURI decodes %25 to %, which creates unintended encoding 5775 // sequences. Re-encode it, unless it's part of a sequence that 5776 // survived decodeURI, i.e. one for: 5777 // ';', '/', '?', ':', '@', '&', '=', '+', '$', ',', '#' 5778 // (RFC 3987 section 3.2) 5779 .replace( 5780 /%(?!3B|2F|3F|3A|40|26|3D|2B|24|2C|23)/gi, 5781 encodeURIComponent 5782 ); 5783 } catch (e) {} 5784 } 5785 } 5786 5787 // IMPORTANT: The following regular expressions are Unicode-aware due to /v. 5788 // Avoid matching high or low surrogate pairs directly, always work with 5789 // full Unicode scalar values. 5790 5791 // Encode potentially invisible characters: 5792 // U+0000-001F: C0/C1 control characters 5793 // U+007F-009F: commands 5794 // U+00A0, U+1680, U+2000-200A, U+202F, U+205F, U+3000: other spaces 5795 // U+2028-2029: line and paragraph separators 5796 // U+2800: braille empty pattern 5797 // U+FFFC: object replacement character 5798 // Encode any trailing whitespace that may be part of a pasted URL, so that it 5799 // doesn't get eaten away by the location bar (bug 410726). 5800 // Encode all adjacent space chars (U+0020), to prevent spoofing attempts 5801 // where they would push part of the URL to overflow the location bar 5802 // (bug 1395508). A single space, or the last space if the are many, is 5803 // preserved to maintain readability of certain urls if it's not followed by a 5804 // control or separator character. We only do this for the common space, 5805 // because others may be eaten when copied to the clipboard,so it's safer to 5806 // preserve them encoded. 5807 value = value.replace( 5808 // eslint-disable-next-line no-control-regex 5809 /[[\p{Separator}--\u{0020}]\p{Control}\u{2800}\u{FFFC}]|\u{0020}(?=[\p{Other}\p{Separator}])|\s$/gv, 5810 encodeURIComponent 5811 ); 5812 5813 // Encode characters that are ignorable, can't be rendered usefully, or may 5814 // confuse users. 5815 // 5816 // Default ignorable characters; ZWNJ (U+200C) and ZWJ (U+200D) are excluded 5817 // per bug 582186: 5818 // U+00AD, U+034F, U+06DD, U+070F, U+115F-1160, U+17B4, U+17B5, U+180B-180E, 5819 // U+2060, U+FEFF, U+200B, U+2060-206F, U+3164, U+FE00-FE0F, U+FFA0, 5820 // U+FFF0-FFFB, U+1D173-1D17A, U+E0000-E0FFF 5821 // Bidi control characters (RFC 3987 sections 3.2 and 4.1 paragraph 6): 5822 // U+061C, U+200E, U+200F, U+202A-202E, U+2066-2069 5823 // Other format characters in the Cf category that are unlikely to be rendered 5824 // usefully: 5825 // U+0600-0605, U+08E2, U+110BD, U+110CD, U+13430-13438, U+1BCA0-1BCA3 5826 // Mimicking UI parts: 5827 // U+1F50F-1F513, U+1F6E1 5828 // Unassigned codepoints, sometimes shown as empty glyphs. 5829 value = value.replace( 5830 // eslint-disable-next-line no-misleading-character-class 5831 /[[\p{Format}--[\u{200C}\u{200D}]]\u{034F}\u{115F}\u{1160}\u{17B4}\u{17B5}\u{180B}-\u{180D}\u{3164}\u{FE00}-\u{FE0F}\u{FFA0}\u{FFF0}-\u{FFFB}\p{Unassigned}\p{Private_Use}\u{E0000}-\u{E0FFF}\u{1F50F}-\u{1F513}\u{1F6E1}]/gv, 5832 encodeURIComponent 5833 ); 5834 return value; 5835 } 5836 5837 /** 5838 * Handles copy and cut commands for the urlbar. 5839 */ 5840 class CopyCutController { 5841 /** 5842 * @param {UrlbarInput} urlbar 5843 * The UrlbarInput instance to use this controller for. 5844 */ 5845 constructor(urlbar) { 5846 this.urlbar = urlbar; 5847 } 5848 5849 /** 5850 * @param {string} command 5851 * The name of the command to handle. 5852 */ 5853 doCommand(command) { 5854 let urlbar = this.urlbar; 5855 let val = urlbar._getSelectedValueForClipboard(); 5856 if (!val) { 5857 return; 5858 } 5859 5860 if (command == "cmd_cut" && this.isCommandEnabled(command)) { 5861 let start = urlbar.selectionStart; 5862 let end = urlbar.selectionEnd; 5863 urlbar.inputField.value = 5864 urlbar.inputField.value.substring(0, start) + 5865 urlbar.inputField.value.substring(end); 5866 urlbar.inputField.setSelectionRange(start, start); 5867 5868 let event = new UIEvent("input", { 5869 bubbles: true, 5870 cancelable: false, 5871 view: urlbar.window, 5872 detail: 0, 5873 }); 5874 urlbar.inputField.dispatchEvent(event); 5875 } 5876 5877 lazy.ClipboardHelper.copyString(val); 5878 } 5879 5880 /** 5881 * @param {string} command 5882 * The name of the command to check. 5883 * @returns {boolean} 5884 * Whether the command is handled by this controller. 5885 */ 5886 supportsCommand(command) { 5887 switch (command) { 5888 case "cmd_copy": 5889 case "cmd_cut": 5890 return true; 5891 } 5892 return false; 5893 } 5894 5895 /** 5896 * @param {string} command 5897 * The name of the command to check. 5898 * @returns {boolean} 5899 * Whether the command should be enabled. 5900 */ 5901 isCommandEnabled(command) { 5902 return ( 5903 this.supportsCommand(command) && 5904 (command != "cmd_cut" || !this.urlbar.readOnly) && 5905 this.urlbar.selectionStart < this.urlbar.selectionEnd 5906 ); 5907 } 5908 5909 onEvent() {} 5910 } 5911 5912 /** 5913 * Manages the Add Search Engine contextual menu entries. 5914 * 5915 * Note: setEnginesFromBrowser must be invoked from the outside when the 5916 * page provided engines list changes. 5917 * refreshContextMenu must be invoked when the context menu is opened. 5918 */ 5919 class AddSearchEngineHelper { 5920 /** 5921 * @type {UrlbarSearchOneOffs} 5922 */ 5923 shortcutButtons; 5924 5925 /** 5926 * @param {UrlbarInput} input The parent UrlbarInput. 5927 */ 5928 constructor(input) { 5929 this.input = input; 5930 this.shortcutButtons = input.view.oneOffSearchButtons; 5931 } 5932 5933 /** 5934 * If there's more than this number of engines, the context menu offers 5935 * them in a submenu. 5936 * 5937 * @returns {number} 5938 */ 5939 get maxInlineEngines() { 5940 return this.shortcutButtons._maxInlineAddEngines; 5941 } 5942 5943 /** 5944 * Invoked by OpenSearchManager when the list of available engines changes. 5945 * 5946 * @param {object} browser The current browser. 5947 * @param {object} engines The updated list of available engines. 5948 */ 5949 setEnginesFromBrowser(browser, engines) { 5950 this.browsingContext = browser.browsingContext; 5951 // Make a copy of the array for state comparison. 5952 engines = engines.slice(); 5953 if (!this._sameEngines(this.engines, engines)) { 5954 this.engines = engines; 5955 this.shortcutButtons?.updateWebEngines(); 5956 } 5957 } 5958 5959 _sameEngines(engines1, engines2) { 5960 if (engines1?.length != engines2?.length) { 5961 return false; 5962 } 5963 return lazy.ObjectUtils.deepEqual( 5964 engines1.map(e => e.title), 5965 engines2.map(e => e.title) 5966 ); 5967 } 5968 5969 _createMenuitem(engine, index) { 5970 let elt = this.input.document.createXULElement("menuitem"); 5971 elt.setAttribute("anonid", `add-engine-${index}`); 5972 elt.classList.add("menuitem-iconic"); 5973 elt.classList.add("context-menu-add-engine"); 5974 this.input.document.l10n.setAttributes(elt, "search-one-offs-add-engine", { 5975 engineName: engine.title, 5976 }); 5977 elt.setAttribute("uri", engine.uri); 5978 if (engine.icon) { 5979 elt.setAttribute("image", engine.icon); 5980 } else { 5981 elt.removeAttribute("image"); 5982 } 5983 elt.addEventListener("command", this._onCommand.bind(this)); 5984 return elt; 5985 } 5986 5987 _createMenu(engine) { 5988 let elt = this.input.document.createXULElement("menu"); 5989 elt.setAttribute("anonid", "add-engine-menu"); 5990 elt.classList.add("menu-iconic"); 5991 elt.classList.add("context-menu-add-engine"); 5992 this.input.document.l10n.setAttributes( 5993 elt, 5994 "search-one-offs-add-engine-menu" 5995 ); 5996 if (engine.icon) { 5997 elt.setAttribute("image", ChromeUtils.encodeURIForSrcset(engine.icon)); 5998 } 5999 let popup = this.input.document.createXULElement("menupopup"); 6000 elt.appendChild(popup); 6001 return elt; 6002 } 6003 6004 refreshContextMenu() { 6005 let engines = this.engines; 6006 let contextMenu = this.input.querySelector("moz-input-box").menupopup; 6007 6008 // Certain operations, like customization, destroy and recreate widgets, 6009 // so we cannot rely on cached elements. 6010 if (!contextMenu.querySelector(".menuseparator-add-engine")) { 6011 this.contextSeparator = 6012 this.input.document.createXULElement("menuseparator"); 6013 this.contextSeparator.setAttribute("anonid", "add-engine-separator"); 6014 this.contextSeparator.classList.add("menuseparator-add-engine"); 6015 this.contextSeparator.collapsed = true; 6016 contextMenu.appendChild(this.contextSeparator); 6017 } 6018 6019 this.contextSeparator.collapsed = !engines.length; 6020 let curElt = this.contextSeparator; 6021 // Remove the previous items, if any. 6022 for (let elt = curElt.nextElementSibling; elt; ) { 6023 let nextElementSibling = elt.nextElementSibling; 6024 elt.remove(); 6025 elt = nextElementSibling; 6026 } 6027 6028 // If the page provides too many engines, we only show a single menu entry 6029 // with engines in a submenu. 6030 if (engines.length > this.maxInlineEngines) { 6031 // Set the menu button's image to the image of the first engine. The 6032 // offered engines may have differing images, so there's no perfect 6033 // choice here. 6034 let elt = this._createMenu(engines[0]); 6035 this.contextSeparator.insertAdjacentElement("afterend", elt); 6036 curElt = elt.lastElementChild; 6037 } 6038 6039 // Insert the engines, either in the contextual menu or the sub menu. 6040 for (let i = 0; i < engines.length; ++i) { 6041 let elt = this._createMenuitem(engines[i], i); 6042 if (curElt.localName == "menupopup") { 6043 curElt.appendChild(elt); 6044 } else { 6045 curElt.insertAdjacentElement("afterend", elt); 6046 } 6047 curElt = elt; 6048 } 6049 } 6050 6051 async _onCommand(event) { 6052 let added = await lazy.SearchUIUtils.addOpenSearchEngine( 6053 event.target.getAttribute("uri"), 6054 event.target.getAttribute("image"), 6055 this.browsingContext 6056 ).catch(console.error); 6057 if (added) { 6058 // Remove the offered engine from the list. The browser updated the 6059 // engines list at this point, so we just have to refresh the menu.) 6060 this.refreshContextMenu(); 6061 } 6062 } 6063 } 6064 6065 customElements.define("moz-urlbar", UrlbarInput);