forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			4334 lines
		
	
	
	
		
			144 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			4334 lines
		
	
	
	
		
			144 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* This Source Code Form is subject to the terms of the Mozilla Public
 | |
|  * License, v. 2.0. If a copy of the MPL was not distributed with this
 | |
|  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 | |
| 
 | |
| import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
 | |
| 
 | |
| import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
 | |
| 
 | |
| const lazy = {};
 | |
| 
 | |
| ChromeUtils.defineESModuleGetters(lazy, {
 | |
|   BrowserSearchTelemetry: "resource:///modules/BrowserSearchTelemetry.sys.mjs",
 | |
|   BrowserUIUtils: "resource:///modules/BrowserUIUtils.sys.mjs",
 | |
|   ExtensionSearchHandler:
 | |
|     "resource://gre/modules/ExtensionSearchHandler.sys.mjs",
 | |
|   ObjectUtils: "resource://gre/modules/ObjectUtils.sys.mjs",
 | |
|   PartnerLinkAttribution: "resource:///modules/PartnerLinkAttribution.sys.mjs",
 | |
|   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
 | |
|   PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs",
 | |
|   ReaderMode: "resource://gre/modules/ReaderMode.sys.mjs",
 | |
|   SearchUIUtils: "resource:///modules/SearchUIUtils.sys.mjs",
 | |
|   SearchUtils: "resource://gre/modules/SearchUtils.sys.mjs",
 | |
|   UrlbarController: "resource:///modules/UrlbarController.sys.mjs",
 | |
|   UrlbarEventBufferer: "resource:///modules/UrlbarEventBufferer.sys.mjs",
 | |
|   UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
 | |
|   UrlbarQueryContext: "resource:///modules/UrlbarUtils.sys.mjs",
 | |
|   UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.sys.mjs",
 | |
|   UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.sys.mjs",
 | |
|   UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs",
 | |
|   UrlbarValueFormatter: "resource:///modules/UrlbarValueFormatter.sys.mjs",
 | |
|   UrlbarView: "resource:///modules/UrlbarView.sys.mjs",
 | |
| });
 | |
| 
 | |
| XPCOMUtils.defineLazyServiceGetter(
 | |
|   lazy,
 | |
|   "ClipboardHelper",
 | |
|   "@mozilla.org/widget/clipboardhelper;1",
 | |
|   "nsIClipboardHelper"
 | |
| );
 | |
| 
 | |
| XPCOMUtils.defineLazyServiceGetter(
 | |
|   lazy,
 | |
|   "QueryStringStripper",
 | |
|   "@mozilla.org/url-query-string-stripper;1",
 | |
|   "nsIURLQueryStringStripper"
 | |
| );
 | |
| 
 | |
| XPCOMUtils.defineLazyPreferenceGetter(
 | |
|   lazy,
 | |
|   "QUERY_STRIPPING_STRIP_ON_SHARE",
 | |
|   "privacy.query_stripping.strip_on_share.enabled",
 | |
|   false
 | |
| );
 | |
| 
 | |
| const DEFAULT_FORM_HISTORY_NAME = "searchbar-history";
 | |
| const SEARCH_BUTTON_ID = "urlbar-search-button";
 | |
| 
 | |
| // The scalar category of TopSites click for Contextual Services
 | |
| const SCALAR_CATEGORY_TOPSITES = "contextual.services.topsites.click";
 | |
| 
 | |
| let getBoundsWithoutFlushing = element =>
 | |
|   element.ownerGlobal.windowUtils.getBoundsWithoutFlushing(element);
 | |
| let px = number => number.toFixed(2) + "px";
 | |
| 
 | |
| /**
 | |
|  * Implements the text input part of the address bar UI.
 | |
|  */
 | |
| export class UrlbarInput {
 | |
|   /**
 | |
|    * @param {object} options
 | |
|    *   The initial options for UrlbarInput.
 | |
|    * @param {object} options.textbox
 | |
|    *   The container element.
 | |
|    */
 | |
|   constructor(options = {}) {
 | |
|     this.textbox = options.textbox;
 | |
| 
 | |
|     this.window = this.textbox.ownerGlobal;
 | |
|     this.isPrivate = lazy.PrivateBrowsingUtils.isWindowPrivate(this.window);
 | |
|     this.document = this.window.document;
 | |
| 
 | |
|     // Create the panel to contain results.
 | |
|     this.textbox.appendChild(
 | |
|       this.window.MozXULElement.parseXULToFragment(`
 | |
|         <vbox class="urlbarView"
 | |
|               role="group"
 | |
|               tooltip="aHTMLTooltip">
 | |
|           <html:div class="urlbarView-body-outer">
 | |
|             <html:div class="urlbarView-body-inner">
 | |
|               <html:div id="urlbar-results"
 | |
|                         class="urlbarView-results"
 | |
|                         role="listbox"/>
 | |
|             </html:div>
 | |
|           </html:div>
 | |
|           <menupopup class="urlbarView-result-menu"
 | |
|                      consumeoutsideclicks="false"/>
 | |
|           <hbox class="search-one-offs"
 | |
|                 includecurrentengine="true"
 | |
|                 disabletab="true"/>
 | |
|         </vbox>
 | |
|       `)
 | |
|     );
 | |
|     this.panel = this.textbox.querySelector(".urlbarView");
 | |
| 
 | |
|     this.searchButton = lazy.UrlbarPrefs.get("experimental.searchButton");
 | |
|     if (this.searchButton) {
 | |
|       this.textbox.classList.add("searchButton");
 | |
|     }
 | |
| 
 | |
|     this.controller = new lazy.UrlbarController({
 | |
|       input: this,
 | |
|       eventTelemetryCategory: options.eventTelemetryCategory,
 | |
|     });
 | |
|     this.view = new lazy.UrlbarView(this);
 | |
|     this.valueIsTyped = false;
 | |
|     this.formHistoryName = DEFAULT_FORM_HISTORY_NAME;
 | |
|     this.lastQueryContextPromise = Promise.resolve();
 | |
|     this._actionOverrideKeyCount = 0;
 | |
|     this._autofillPlaceholder = null;
 | |
|     this._lastSearchString = "";
 | |
|     this._lastValidURLStr = "";
 | |
|     this._valueOnLastSearch = "";
 | |
|     this._resultForCurrentValue = null;
 | |
|     this._suppressStartQuery = false;
 | |
|     this._suppressPrimaryAdjustment = false;
 | |
|     this._untrimmedValue = "";
 | |
| 
 | |
|     // Search modes are per browser and are stored in this map.  For a
 | |
|     // browser, search mode can be in preview mode, confirmed, or both.
 | |
|     // Typically, search mode is entered in preview mode with a particular
 | |
|     // source and is confirmed with the same source once a query starts.  It's
 | |
|     // also possible for a confirmed search mode to be replaced with a preview
 | |
|     // mode with a different source, and in those cases, we need to re-confirm
 | |
|     // search mode when preview mode is exited. In addition, only confirmed
 | |
|     // search modes should be restored across sessions. We therefore need to
 | |
|     // keep track of both the current confirmed and preview modes, per browser.
 | |
|     //
 | |
|     // For each browser with a search mode, this maps the browser to an object
 | |
|     // like this: { preview, confirmed }.  Both `preview` and `confirmed` are
 | |
|     // search mode objects; see the setSearchMode documentation.  Either one may
 | |
|     // be undefined if that particular mode is not active for the browser.
 | |
|     this._searchModesByBrowser = new WeakMap();
 | |
| 
 | |
|     this.QueryInterface = ChromeUtils.generateQI([
 | |
|       "nsIObserver",
 | |
|       "nsISupportsWeakReference",
 | |
|     ]);
 | |
|     this._addObservers();
 | |
| 
 | |
|     // This exists only for tests.
 | |
|     this._enableAutofillPlaceholder = true;
 | |
| 
 | |
|     // Forward certain methods and properties.
 | |
|     const CONTAINER_METHODS = [
 | |
|       "getAttribute",
 | |
|       "hasAttribute",
 | |
|       "querySelector",
 | |
|       "setAttribute",
 | |
|       "removeAttribute",
 | |
|       "toggleAttribute",
 | |
|     ];
 | |
|     const INPUT_METHODS = ["addEventListener", "blur", "removeEventListener"];
 | |
|     const READ_WRITE_PROPERTIES = [
 | |
|       "placeholder",
 | |
|       "readOnly",
 | |
|       "selectionStart",
 | |
|       "selectionEnd",
 | |
|     ];
 | |
| 
 | |
|     for (let method of CONTAINER_METHODS) {
 | |
|       this[method] = (...args) => {
 | |
|         return this.textbox[method](...args);
 | |
|       };
 | |
|     }
 | |
| 
 | |
|     for (let method of INPUT_METHODS) {
 | |
|       this[method] = (...args) => {
 | |
|         return this.inputField[method](...args);
 | |
|       };
 | |
|     }
 | |
| 
 | |
|     for (let property of READ_WRITE_PROPERTIES) {
 | |
|       Object.defineProperty(this, property, {
 | |
|         enumerable: true,
 | |
|         get() {
 | |
|           return this.inputField[property];
 | |
|         },
 | |
|         set(val) {
 | |
|           this.inputField[property] = val;
 | |
|         },
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     this.inputField = this.querySelector("#urlbar-input");
 | |
|     this._inputContainer = this.querySelector("#urlbar-input-container");
 | |
|     this._identityBox = this.querySelector("#identity-box");
 | |
|     this._searchModeIndicator = this.querySelector(
 | |
|       "#urlbar-search-mode-indicator"
 | |
|     );
 | |
|     this._searchModeIndicatorTitle = this._searchModeIndicator.querySelector(
 | |
|       "#urlbar-search-mode-indicator-title"
 | |
|     );
 | |
|     this._searchModeIndicatorClose = this._searchModeIndicator.querySelector(
 | |
|       "#urlbar-search-mode-indicator-close"
 | |
|     );
 | |
|     this._searchModeLabel = this.querySelector("#urlbar-label-search-mode");
 | |
|     this._toolbar = this.textbox.closest("toolbar");
 | |
| 
 | |
|     ChromeUtils.defineLazyGetter(this, "valueFormatter", () => {
 | |
|       return new lazy.UrlbarValueFormatter(this);
 | |
|     });
 | |
| 
 | |
|     ChromeUtils.defineLazyGetter(this, "addSearchEngineHelper", () => {
 | |
|       return new AddSearchEngineHelper(this);
 | |
|     });
 | |
| 
 | |
|     // If the toolbar is not visible in this window or the urlbar is readonly,
 | |
|     // we'll stop here, so that most properties of the input object are valid,
 | |
|     // but we won't handle events.
 | |
|     if (!this.window.toolbar.visible || this.readOnly) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // The event bufferer can be used to defer events that may affect users
 | |
|     // muscle memory; for example quickly pressing DOWN+ENTER should end up
 | |
|     // on a predictable result, regardless of the search status. The event
 | |
|     // bufferer will invoke the handling code at the right time.
 | |
|     this.eventBufferer = new lazy.UrlbarEventBufferer(this);
 | |
| 
 | |
|     this._inputFieldEvents = [
 | |
|       "compositionstart",
 | |
|       "compositionend",
 | |
|       "contextmenu",
 | |
|       "dragover",
 | |
|       "dragstart",
 | |
|       "drop",
 | |
|       "focus",
 | |
|       "blur",
 | |
|       "input",
 | |
|       "beforeinput",
 | |
|       "keydown",
 | |
|       "keyup",
 | |
|       "mouseover",
 | |
|       "overflow",
 | |
|       "underflow",
 | |
|       "paste",
 | |
|       "scrollend",
 | |
|       "select",
 | |
|       "selectionchange",
 | |
|     ];
 | |
|     for (let name of this._inputFieldEvents) {
 | |
|       this.addEventListener(name, this);
 | |
|     }
 | |
| 
 | |
|     this.window.addEventListener("mousedown", this);
 | |
|     if (AppConstants.platform == "win") {
 | |
|       this.window.addEventListener("draggableregionleftmousedown", this);
 | |
|     }
 | |
|     this.textbox.addEventListener("mousedown", this);
 | |
| 
 | |
|     // This listener handles clicks from our children too, included the search mode
 | |
|     // indicator close button.
 | |
|     this._inputContainer.addEventListener("click", this);
 | |
| 
 | |
|     // This is used to detect commands launched from the panel, to avoid
 | |
|     // recording abandonment events when the command causes a blur event.
 | |
|     this.view.panel.addEventListener("command", this, true);
 | |
| 
 | |
|     this.window.gBrowser.tabContainer.addEventListener("TabSelect", this);
 | |
| 
 | |
|     this.window.addEventListener("customizationstarting", this);
 | |
|     this.window.addEventListener("aftercustomization", this);
 | |
| 
 | |
|     this.updateLayoutBreakout();
 | |
| 
 | |
|     this._initCopyCutController();
 | |
|     this._initPasteAndGo();
 | |
|     this._initStripOnShare();
 | |
| 
 | |
|     // Tracks IME composition.
 | |
|     this._compositionState = lazy.UrlbarUtils.COMPOSITION.NONE;
 | |
|     this._compositionClosedPopup = false;
 | |
| 
 | |
|     this.editor.newlineHandling =
 | |
|       Ci.nsIEditor.eNewlinesStripSurroundingWhitespace;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Applies styling to the text in the urlbar input, depending on the text.
 | |
|    */
 | |
|   formatValue() {
 | |
|     // The editor may not exist if the toolbar is not visible.
 | |
|     if (this.editor) {
 | |
|       this.valueFormatter.update();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   focus() {
 | |
|     let beforeFocus = new CustomEvent("beforefocus", {
 | |
|       bubbles: true,
 | |
|       cancelable: true,
 | |
|     });
 | |
|     this.inputField.dispatchEvent(beforeFocus);
 | |
|     if (beforeFocus.defaultPrevented) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     this.inputField.focus();
 | |
|   }
 | |
| 
 | |
|   select() {
 | |
|     let beforeSelect = new CustomEvent("beforeselect", {
 | |
|       bubbles: true,
 | |
|       cancelable: true,
 | |
|     });
 | |
|     this.inputField.dispatchEvent(beforeSelect);
 | |
|     if (beforeSelect.defaultPrevented) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // See _on_select().  HTMLInputElement.select() dispatches a "select"
 | |
|     // event but does not set the primary selection.
 | |
|     this._suppressPrimaryAdjustment = true;
 | |
|     this.inputField.select();
 | |
|     this._suppressPrimaryAdjustment = false;
 | |
|   }
 | |
| 
 | |
|   setSelectionRange(selectionStart, selectionEnd) {
 | |
|     this.focus();
 | |
| 
 | |
|     let beforeSelect = new CustomEvent("beforeselect", {
 | |
|       bubbles: true,
 | |
|       cancelable: true,
 | |
|     });
 | |
|     this.inputField.dispatchEvent(beforeSelect);
 | |
|     if (beforeSelect.defaultPrevented) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // See _on_select().  HTMLInputElement.select() dispatches a "select"
 | |
|     // event but does not set the primary selection.
 | |
|     this._suppressPrimaryAdjustment = true;
 | |
|     this.inputField.setSelectionRange(selectionStart, selectionEnd);
 | |
|     this._suppressPrimaryAdjustment = false;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Sets the URI to display in the location bar.
 | |
|    *
 | |
|    * @param {nsIURI} [uri]
 | |
|    *        If this is unspecified, the current URI will be used.
 | |
|    * @param {boolean} [dueToTabSwitch]
 | |
|    *        True if this is being called due to switching tabs and false
 | |
|    *        otherwise.
 | |
|    * @param {boolean} [dueToSessionRestore]
 | |
|    *        True if this is being called due to session restore and false
 | |
|    *        otherwise.
 | |
|    * @param {boolean} [dontShowSearchTerms]
 | |
|    *        True if userTypedValue should not be overidden by search terms
 | |
|    *        and false otherwise.
 | |
|    * @param {boolean} [isSameDocument]
 | |
|    *        True if the caller of setURI loaded a new document and false
 | |
|    *        otherwise (e.g. the location change was from an anchor scroll
 | |
|    *        or a pushState event).
 | |
|    */
 | |
|   setURI(
 | |
|     uri = null,
 | |
|     dueToTabSwitch = false,
 | |
|     dueToSessionRestore = false,
 | |
|     dontShowSearchTerms = false,
 | |
|     isSameDocument = false
 | |
|   ) {
 | |
|     if (!this.window.gBrowser.userTypedValue) {
 | |
|       this.window.gBrowser.selectedBrowser.searchTerms = "";
 | |
|       if (
 | |
|         !dontShowSearchTerms &&
 | |
|         lazy.UrlbarPrefs.isPersistedSearchTermsEnabled()
 | |
|       ) {
 | |
|         this.window.gBrowser.selectedBrowser.searchTerms =
 | |
|           lazy.UrlbarSearchUtils.getSearchTermIfDefaultSerpUri(
 | |
|             this.window.gBrowser.selectedBrowser.originalURI ?? uri
 | |
|           );
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     let value = this.window.gBrowser.userTypedValue;
 | |
|     let valid = false;
 | |
| 
 | |
|     // If `value` is null or if it's an empty string and we're switching tabs
 | |
|     // or the userTypedValue equals the search terms, set value to either
 | |
|     // search terms or the browser's current URI. When a user empties the input,
 | |
|     // switches tabs, and switches back, we want the URI to become visible again
 | |
|     // so the user knows what URI they're viewing.
 | |
|     // An exception to this is made in case of an auth request from a different
 | |
|     // base domain. To avoid auth prompt spoofing we already display the url of
 | |
|     // the cross domain resource, although the page is not loaded yet.
 | |
|     // This url will be set/unset by PromptParent. See bug 791594 for reference.
 | |
|     if (
 | |
|       value === null ||
 | |
|       (!value && dueToTabSwitch) ||
 | |
|       (value && value === this.window.gBrowser.selectedBrowser.searchTerms)
 | |
|     ) {
 | |
|       if (this.window.gBrowser.selectedBrowser.searchTerms) {
 | |
|         value = this.window.gBrowser.selectedBrowser.searchTerms;
 | |
|         valid = !dueToSessionRestore;
 | |
|         if (!isSameDocument) {
 | |
|           Services.telemetry.scalarAdd(
 | |
|             "urlbar.persistedsearchterms.view_count",
 | |
|             1
 | |
|           );
 | |
|         }
 | |
|       } else {
 | |
|         uri =
 | |
|           this.window.gBrowser.selectedBrowser.currentAuthPromptURI ||
 | |
|           uri ||
 | |
|           (this.window.gBrowser.selectedBrowser.browsingContext.sessionHistory
 | |
|             ?.count === 0 &&
 | |
|             this.window.gBrowser.selectedBrowser.browsingContext
 | |
|               .nonWebControlledBlankURI) ||
 | |
|           this.window.gBrowser.currentURI;
 | |
|         // Strip off usernames and passwords for the location bar
 | |
|         try {
 | |
|           uri = Services.io.createExposableURI(uri);
 | |
|         } catch (e) {}
 | |
| 
 | |
|         // Replace initial page URIs with an empty string
 | |
|         // only if there's no opener (bug 370555).
 | |
|         if (
 | |
|           this.window.isInitialPage(uri) &&
 | |
|           lazy.BrowserUIUtils.checkEmptyPageOrigin(
 | |
|             this.window.gBrowser.selectedBrowser,
 | |
|             uri
 | |
|           )
 | |
|         ) {
 | |
|           value = "";
 | |
|         } else {
 | |
|           // We should deal with losslessDecodeURI throwing for exotic URIs
 | |
|           try {
 | |
|             value = losslessDecodeURI(uri);
 | |
|           } catch (ex) {
 | |
|             value = "about:blank";
 | |
|           }
 | |
|         }
 | |
|         // If we update the URI while restoring a session, set the proxyState to
 | |
|         // invalid, because we don't have a valid security state to show via site
 | |
|         // identity yet. See Bug 1746383.
 | |
|         valid =
 | |
|           !dueToSessionRestore &&
 | |
|           (!this.window.isBlankPageURL(uri.spec) ||
 | |
|             uri.schemeIs("moz-extension"));
 | |
|       }
 | |
|     } else if (
 | |
|       this.window.isInitialPage(value) &&
 | |
|       lazy.BrowserUIUtils.checkEmptyPageOrigin(
 | |
|         this.window.gBrowser.selectedBrowser
 | |
|       )
 | |
|     ) {
 | |
|       value = "";
 | |
|       valid = true;
 | |
|     }
 | |
| 
 | |
|     const previousUntrimmedValue = this.untrimmedValue;
 | |
|     const previousSelectionStart = this.selectionStart;
 | |
|     const previousSelectionEnd = this.selectionEnd;
 | |
| 
 | |
|     this.value = value;
 | |
|     this.valueIsTyped = !valid;
 | |
|     this.toggleAttribute("usertyping", !valid && value);
 | |
| 
 | |
|     if (this.focused && value != previousUntrimmedValue) {
 | |
|       if (
 | |
|         previousSelectionStart != previousSelectionEnd &&
 | |
|         value.substring(previousSelectionStart, previousSelectionEnd) ===
 | |
|           previousUntrimmedValue.substring(
 | |
|             previousSelectionStart,
 | |
|             previousSelectionEnd
 | |
|           )
 | |
|       ) {
 | |
|         // If the same text is in the same place as the previously selected text,
 | |
|         // the selection is kept.
 | |
|         this.inputField.setSelectionRange(
 | |
|           previousSelectionStart,
 | |
|           previousSelectionEnd
 | |
|         );
 | |
|       } else if (
 | |
|         previousSelectionEnd &&
 | |
|         (previousUntrimmedValue.length === previousSelectionEnd ||
 | |
|           value.length <= previousSelectionEnd)
 | |
|       ) {
 | |
|         // If the previous end caret is not 0 and the caret is at the end of the
 | |
|         // input or its position is beyond the end of the new value, keep the
 | |
|         // position at the end.
 | |
|         this.inputField.setSelectionRange(value.length, value.length);
 | |
|       } else {
 | |
|         // Otherwise clear selection and set the caret position to the previous
 | |
|         // caret end position.
 | |
|         this.inputField.setSelectionRange(
 | |
|           previousSelectionEnd,
 | |
|           previousSelectionEnd
 | |
|         );
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // The proxystate must be set before setting search mode below because
 | |
|     // search mode depends on it.
 | |
|     this.setPageProxyState(valid ? "valid" : "invalid", dueToTabSwitch);
 | |
| 
 | |
|     // If we're switching tabs, restore the tab's search mode.  Otherwise, if
 | |
|     // the URI is valid, exit search mode.  This must happen after setting
 | |
|     // proxystate above because search mode depends on it.
 | |
|     if (dueToTabSwitch && !valid) {
 | |
|       this.restoreSearchModeState();
 | |
|     } else if (valid) {
 | |
|       this.searchMode = null;
 | |
|     }
 | |
| 
 | |
|     // Dispatch URIUpdate event to synchronize the tab status when switching.
 | |
|     let event = new CustomEvent("SetURI", { bubbles: true });
 | |
|     this.inputField.dispatchEvent(event);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Converts an internal URI (e.g. a URI with a username or password) into one
 | |
|    * which we can expose to the user.
 | |
|    *
 | |
|    * @param {nsIURI} uri
 | |
|    *   The URI to be converted
 | |
|    * @returns {nsIURI}
 | |
|    *   The converted, exposable URI
 | |
|    */
 | |
|   makeURIReadable(uri) {
 | |
|     // Avoid copying 'about:reader?url=', and always provide the original URI:
 | |
|     // Reader mode ensures we call createExposableURI itself.
 | |
|     let readerStrippedURI = lazy.ReaderMode.getOriginalUrlObjectForDisplay(
 | |
|       uri.displaySpec
 | |
|     );
 | |
|     if (readerStrippedURI) {
 | |
|       return readerStrippedURI;
 | |
|     }
 | |
| 
 | |
|     try {
 | |
|       return Services.io.createExposableURI(uri);
 | |
|     } catch (ex) {}
 | |
| 
 | |
|     return uri;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Passes DOM events to the _on_<event type> methods.
 | |
|    *
 | |
|    * @param {Event} event The event to handle.
 | |
|    */
 | |
|   handleEvent(event) {
 | |
|     let methodName = "_on_" + event.type;
 | |
|     if (methodName in this) {
 | |
|       this[methodName](event);
 | |
|     } else {
 | |
|       throw new Error("Unrecognized UrlbarInput event: " + event.type);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Handles an event which might open text or a URL. If the event requires
 | |
|    * doing so, handleCommand forwards it to handleNavigation.
 | |
|    *
 | |
|    * @param {Event} [event] The event triggering the open.
 | |
|    */
 | |
|   handleCommand(event = null) {
 | |
|     let isMouseEvent = this.window.MouseEvent.isInstance(event);
 | |
|     if (isMouseEvent && event.button == 2) {
 | |
|       // Do nothing for right clicks.
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // Determine whether to use the selected one-off search button.  In
 | |
|     // one-off search buttons parlance, "selected" means that the button
 | |
|     // has been navigated to via the keyboard.  So we want to use it if
 | |
|     // the triggering event is not a mouse click -- i.e., it's a Return
 | |
|     // key -- or if the one-off was mouse-clicked.
 | |
|     if (this.view.isOpen) {
 | |
|       let selectedOneOff = this.view.oneOffSearchButtons.selectedButton;
 | |
|       if (selectedOneOff && (!isMouseEvent || event.target == selectedOneOff)) {
 | |
|         this.view.oneOffSearchButtons.handleSearchCommand(event, {
 | |
|           engineName: selectedOneOff.engine?.name,
 | |
|           source: selectedOneOff.source,
 | |
|           entry: "oneoff",
 | |
|         });
 | |
|         return;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     this.handleNavigation({ event });
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @typedef {object} HandleNavigationOneOffParams
 | |
|    *
 | |
|    * @property {string} openWhere
 | |
|    *   Where we expect the result to be opened.
 | |
|    * @property {object} openParams
 | |
|    *   The parameters related to where the result will be opened.
 | |
|    * @property {Node} engine
 | |
|    *   The selected one-off's engine.
 | |
|    */
 | |
| 
 | |
|   /**
 | |
|    * Handles an event which would cause a URL or text to be opened.
 | |
|    *
 | |
|    * @param {object} [options]
 | |
|    *   Options for the navigation.
 | |
|    * @param {Event} [options.event]
 | |
|    *   The event triggering the open.
 | |
|    * @param {HandleNavigationOneOffParams} [options.oneOffParams]
 | |
|    *   Optional. Pass if this navigation was triggered by a one-off. Practically
 | |
|    *   speaking, UrlbarSearchOneOffs passes this when the user holds certain key
 | |
|    *   modifiers while picking a one-off. In those cases, we do an immediate
 | |
|    *   search using the one-off's engine instead of entering search mode.
 | |
|    * @param {object} [options.triggeringPrincipal]
 | |
|    *   The principal that the action was triggered from.
 | |
|    */
 | |
|   handleNavigation({ event, oneOffParams, triggeringPrincipal }) {
 | |
|     let element = this.view.selectedElement;
 | |
|     let result = this.view.getResultFromElement(element);
 | |
|     let openParams = oneOffParams?.openParams || {};
 | |
| 
 | |
|     // If the value was submitted during composition, the result may not have
 | |
|     // been updated yet, because the input event happens after composition end.
 | |
|     // We can't trust element nor _resultForCurrentValue targets in that case,
 | |
|     // so we always generate a new heuristic to load.
 | |
|     let isComposing = this.editor.composing;
 | |
| 
 | |
|     // Use the selected element if we have one; this is usually the case
 | |
|     // when the view is open.
 | |
|     let selectedPrivateResult =
 | |
|       result &&
 | |
|       result.type == lazy.UrlbarUtils.RESULT_TYPE.SEARCH &&
 | |
|       result.payload.inPrivateWindow;
 | |
|     let selectedPrivateEngineResult =
 | |
|       selectedPrivateResult && result.payload.isPrivateEngine;
 | |
|     if (
 | |
|       !isComposing &&
 | |
|       element &&
 | |
|       (!oneOffParams?.engine || selectedPrivateEngineResult)
 | |
|     ) {
 | |
|       this.pickElement(element, event);
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // Use the hidden heuristic if it exists and there's no selection.
 | |
|     if (
 | |
|       lazy.UrlbarPrefs.get("experimental.hideHeuristic") &&
 | |
|       !element &&
 | |
|       !isComposing &&
 | |
|       !oneOffParams?.engine &&
 | |
|       this._resultForCurrentValue?.heuristic
 | |
|     ) {
 | |
|       this.pickResult(this._resultForCurrentValue, event);
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // We don't select a heuristic result when we're autofilling a token alias,
 | |
|     // but we want pressing Enter to behave like the first result was selected.
 | |
|     if (!result && this.value.startsWith("@")) {
 | |
|       let tokenAliasResult = this.view.getResultAtIndex(0);
 | |
|       if (tokenAliasResult?.autofill && tokenAliasResult?.payload.keyword) {
 | |
|         this.pickResult(tokenAliasResult, event);
 | |
|         return;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     let url;
 | |
|     let selType = this.controller.engagementEvent.typeFromElement(
 | |
|       result,
 | |
|       element
 | |
|     );
 | |
|     let typedValue = this.value;
 | |
|     if (oneOffParams?.engine) {
 | |
|       selType = "oneoff";
 | |
|       typedValue = this._lastSearchString;
 | |
|       // If there's a selected one-off button then load a search using
 | |
|       // the button's engine.
 | |
|       result = this._resultForCurrentValue;
 | |
| 
 | |
|       let searchString =
 | |
|         (result && (result.payload.suggestion || result.payload.query)) ||
 | |
|         this._lastSearchString;
 | |
|       [url, openParams.postData] = lazy.UrlbarUtils.getSearchQueryUrl(
 | |
|         oneOffParams.engine,
 | |
|         searchString
 | |
|       );
 | |
|       this._recordSearch(oneOffParams.engine, event, { url });
 | |
| 
 | |
|       lazy.UrlbarUtils.addToFormHistory(
 | |
|         this,
 | |
|         searchString,
 | |
|         oneOffParams.engine.name
 | |
|       ).catch(console.error);
 | |
|     } else {
 | |
|       // Use the current value if we don't have a UrlbarResult e.g. because the
 | |
|       // view is closed.
 | |
|       url = this.untrimmedValue;
 | |
|       openParams.postData = null;
 | |
|     }
 | |
| 
 | |
|     if (!url) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // When the user hits enter in a local search mode and there's no selected
 | |
|     // result or one-off, don't do anything.
 | |
|     if (
 | |
|       this.searchMode &&
 | |
|       !this.searchMode.engineName &&
 | |
|       !result &&
 | |
|       !oneOffParams
 | |
|     ) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let selectedResult = result || this.view.selectedResult;
 | |
|     this.controller.recordSelectedResult(event, selectedResult);
 | |
| 
 | |
|     let where = oneOffParams?.openWhere || this._whereToOpen(event);
 | |
|     if (selectedPrivateResult) {
 | |
|       where = "window";
 | |
|       openParams.private = true;
 | |
|     }
 | |
|     openParams.allowInheritPrincipal = false;
 | |
|     url = this._maybeCanonizeURL(event, url) || url.trim();
 | |
| 
 | |
|     this.controller.engagementEvent.record(event, {
 | |
|       element,
 | |
|       selType,
 | |
|       searchString: typedValue,
 | |
|       result: selectedResult,
 | |
|     });
 | |
| 
 | |
|     let isValidUrl = false;
 | |
|     try {
 | |
|       new URL(url);
 | |
|       isValidUrl = true;
 | |
|     } catch (ex) {}
 | |
|     if (isValidUrl) {
 | |
|       this._loadURL(url, event, where, openParams);
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // This is not a URL and there's no selected element, because likely the
 | |
|     // view is closed, or paste&go was used.
 | |
|     // We must act consistently here, having or not an open view should not
 | |
|     // make a difference if the search string is the same.
 | |
| 
 | |
|     // If we have a result for the current value, we can just use it.
 | |
|     if (!isComposing && this._resultForCurrentValue) {
 | |
|       this.pickResult(this._resultForCurrentValue, event);
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // Otherwise, we must fetch the heuristic result for the current value.
 | |
|     // TODO (Bug 1604927): If the urlbar results are restricted to a specific
 | |
|     // engine, here we must search with that specific engine; indeed the
 | |
|     // docshell wouldn't know about our engine restriction.
 | |
|     // Also remember to invoke this._recordSearch, after replacing url with
 | |
|     // the appropriate engine submission url.
 | |
|     let browser = this.window.gBrowser.selectedBrowser;
 | |
|     let lastLocationChange = browser.lastLocationChange;
 | |
|     lazy.UrlbarUtils.getHeuristicResultFor(url, this.window)
 | |
|       .then(newResult => {
 | |
|         // Because this happens asynchronously, we must verify that the browser
 | |
|         // location did not change in the meanwhile.
 | |
|         if (
 | |
|           where != "current" ||
 | |
|           browser.lastLocationChange == lastLocationChange
 | |
|         ) {
 | |
|           this.pickResult(newResult, event, null, browser);
 | |
|         }
 | |
|       })
 | |
|       .catch(ex => {
 | |
|         if (url) {
 | |
|           // Something went wrong, we should always have a heuristic result,
 | |
|           // otherwise it means we're not able to search at all, maybe because
 | |
|           // some parts of the profile are corrupt.
 | |
|           // The urlbar should still allow to search or visit the typed string,
 | |
|           // so that the user can look for help to resolve the problem.
 | |
|           let flags =
 | |
|             Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS |
 | |
|             Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
 | |
|           if (this.isPrivate) {
 | |
|             flags |= Ci.nsIURIFixup.FIXUP_FLAG_PRIVATE_CONTEXT;
 | |
|           }
 | |
|           let { preferredURI: uri, postData } =
 | |
|             Services.uriFixup.getFixupURIInfo(url, flags);
 | |
|           if (
 | |
|             where != "current" ||
 | |
|             browser.lastLocationChange == lastLocationChange
 | |
|           ) {
 | |
|             openParams.postData = postData;
 | |
|             this._loadURL(uri.spec, event, where, openParams, null, browser);
 | |
|           }
 | |
|         }
 | |
|       });
 | |
|     // Don't add further handling here, the catch above is our last resort.
 | |
|   }
 | |
| 
 | |
|   handleRevert(dontShowSearchTerms = false) {
 | |
|     this.window.gBrowser.userTypedValue = null;
 | |
|     // Nullify search mode before setURI so it won't try to restore it.
 | |
|     this.searchMode = null;
 | |
|     this.setURI(null, true, false, dontShowSearchTerms);
 | |
|     if (this.value && this.focused) {
 | |
|       this.select();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   maybeHandleRevertFromPopup(anchorElement) {
 | |
|     if (
 | |
|       anchorElement?.closest("#urlbar") &&
 | |
|       this.window.gBrowser.selectedBrowser.searchTerms
 | |
|     ) {
 | |
|       // The Persist Search Tip can be open while a PopupNotification is queued
 | |
|       // to appear, so ensure that the tip is closed.
 | |
|       this.view.close();
 | |
| 
 | |
|       this.handleRevert(true);
 | |
|       Services.telemetry.scalarAdd(
 | |
|         "urlbar.persistedsearchterms.revert_by_popup_count",
 | |
|         1
 | |
|       );
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Called by inputs that resemble search boxes, but actually hand input off
 | |
|    * to the Urlbar. We use these fake inputs on the new tab page and
 | |
|    * about:privatebrowsing.
 | |
|    *
 | |
|    * @param {string} searchString
 | |
|    *   The search string to use.
 | |
|    * @param {nsISearchEngine} [searchEngine]
 | |
|    *   Optional. If included and the right prefs are set, we will enter search
 | |
|    *   mode when handing `searchString` from the fake input to the Urlbar.
 | |
|    * @param {string} newtabSessionId
 | |
|    *   Optional. The id of the newtab session that handed off this search.
 | |
|    *
 | |
|    */
 | |
|   handoff(searchString, searchEngine, newtabSessionId) {
 | |
|     this._isHandoffSession = true;
 | |
|     this._handoffSession = newtabSessionId;
 | |
|     if (lazy.UrlbarPrefs.get("shouldHandOffToSearchMode") && searchEngine) {
 | |
|       this.search(searchString, {
 | |
|         searchEngine,
 | |
|         searchModeEntry: "handoff",
 | |
|       });
 | |
|     } else {
 | |
|       this.search(searchString);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Called when an element of the view is picked.
 | |
|    *
 | |
|    * @param {Element} element The element that was picked.
 | |
|    * @param {Event} event The event that picked the element.
 | |
|    */
 | |
|   pickElement(element, event) {
 | |
|     let result = this.view.getResultFromElement(element);
 | |
|     if (!result) {
 | |
|       return;
 | |
|     }
 | |
|     this.pickResult(result, event, element);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Called when a result is picked.
 | |
|    *
 | |
|    * @param {UrlbarResult} result The result that was picked.
 | |
|    * @param {Event} event The event that picked the result.
 | |
|    * @param {DOMElement} element the picked view element, if available.
 | |
|    * @param {object} browser The browser to use for the load.
 | |
|    */
 | |
|   // eslint-disable-next-line complexity
 | |
|   pickResult(
 | |
|     result,
 | |
|     event,
 | |
|     element = null,
 | |
|     browser = this.window.gBrowser.selectedBrowser
 | |
|   ) {
 | |
|     if (element?.classList.contains("urlbarView-button-menu")) {
 | |
|       this.view.openResultMenu(result, element);
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let urlOverride;
 | |
|     if (element?.dataset.command) {
 | |
|       this.controller.engagementEvent.record(event, {
 | |
|         result,
 | |
|         element,
 | |
|         searchString: this._lastSearchString,
 | |
|         selType:
 | |
|           element.dataset.command == "help" &&
 | |
|           result.type == lazy.UrlbarUtils.RESULT_TYPE.TIP
 | |
|             ? "tiphelp"
 | |
|             : element.dataset.command,
 | |
|       });
 | |
|       if (element.dataset.command == "help") {
 | |
|         urlOverride = result.payload.helpUrl;
 | |
|       }
 | |
|       urlOverride ||= element.dataset.url;
 | |
|       if (!urlOverride) {
 | |
|         return;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // When a one-off is selected, we restyle heuristic results to look like
 | |
|     // search results. In the unlikely event that they are clicked, instead of
 | |
|     // picking the results as usual, we confirm search mode, same as if the user
 | |
|     // had selected them and pressed the enter key. Restyling results in this
 | |
|     // manner was agreed on as a compromise between consistent UX and
 | |
|     // engineering effort. See review discussion at bug 1667766.
 | |
|     if (
 | |
|       result.heuristic &&
 | |
|       this.searchMode?.isPreview &&
 | |
|       this.view.oneOffSearchButtons.selectedButton
 | |
|     ) {
 | |
|       this.confirmSearchMode();
 | |
|       this.search(this.value);
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (
 | |
|       result.type == lazy.UrlbarUtils.RESULT_TYPE.TIP &&
 | |
|       result.payload.type == "dismissalAcknowledgment"
 | |
|     ) {
 | |
|       // The user clicked the "Got it" button inside the dismissal
 | |
|       // acknowledgment tip. Dismiss the tip.
 | |
|       this.controller.engagementEvent.record(event, {
 | |
|         result,
 | |
|         element,
 | |
|         searchString: this._lastSearchString,
 | |
|         selType: "dismiss",
 | |
|       });
 | |
|       this.view.onQueryResultRemoved(result.rowIndex);
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     urlOverride ||= element?.dataset.url;
 | |
|     let originalUntrimmedValue = this.untrimmedValue;
 | |
|     let isCanonized = this.setValueFromResult({ result, event, urlOverride });
 | |
|     let where = this._whereToOpen(event);
 | |
|     let openParams = {
 | |
|       allowInheritPrincipal: false,
 | |
|       globalHistoryOptions: {
 | |
|         triggeringSearchEngine: result.payload?.engine,
 | |
|         triggeringSponsoredURL: result.payload?.isSponsored
 | |
|           ? result.payload.url
 | |
|           : undefined,
 | |
|       },
 | |
|       private: this.isPrivate,
 | |
|     };
 | |
| 
 | |
|     if (
 | |
|       urlOverride &&
 | |
|       result.type != lazy.UrlbarUtils.RESULT_TYPE.TIP &&
 | |
|       where == "current"
 | |
|     ) {
 | |
|       // Open non-tip help links in a new tab unless the user held a modifier.
 | |
|       // TODO (bug 1696232): Do this for tip help links, too.
 | |
|       where = "tab";
 | |
|     }
 | |
| 
 | |
|     if (!result.payload.providesSearchMode) {
 | |
|       this.view.close({ elementPicked: true });
 | |
|     }
 | |
| 
 | |
|     this.controller.recordSelectedResult(event, result);
 | |
| 
 | |
|     if (isCanonized) {
 | |
|       this.controller.engagementEvent.record(event, {
 | |
|         result,
 | |
|         element,
 | |
|         selType: "canonized",
 | |
|         searchString: this._lastSearchString,
 | |
|       });
 | |
|       this._loadURL(this.value, event, where, openParams, browser);
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let { url, postData } = urlOverride
 | |
|       ? { url: urlOverride, postData: null }
 | |
|       : lazy.UrlbarUtils.getUrlFromResult(result);
 | |
|     openParams.postData = postData;
 | |
| 
 | |
|     switch (result.type) {
 | |
|       case lazy.UrlbarUtils.RESULT_TYPE.URL: {
 | |
|         // Bug 1578856: both the provider and the docshell run heuristics to
 | |
|         // decide how to handle a non-url string, either fixing it to a url, or
 | |
|         // searching for it.
 | |
|         // Some preferences can control the docshell behavior, for example
 | |
|         // if dns_first_for_single_words is true, the docshell looks up the word
 | |
|         // against the dns server, and either loads it as an url or searches for
 | |
|         // it, depending on the lookup result. The provider instead will always
 | |
|         // return a fixed url in this case, because URIFixup is synchronous and
 | |
|         // can't do a synchronous dns lookup. A possible long term solution
 | |
|         // would involve sharing the docshell logic with the provider, along
 | |
|         // with the dns lookup.
 | |
|         // For now, in this specific case, we'll override the result's url
 | |
|         // with the input value, and let it pass through to _loadURL(), and
 | |
|         // finally to the docshell.
 | |
|         // This also means that in some cases the heuristic result will show a
 | |
|         // Visit entry, but the docshell will instead execute a search. It's a
 | |
|         // rare case anyway, most likely to happen for enterprises customizing
 | |
|         // the urifixup prefs.
 | |
|         if (
 | |
|           result.heuristic &&
 | |
|           lazy.UrlbarPrefs.get("browser.fixup.dns_first_for_single_words") &&
 | |
|           lazy.UrlbarUtils.looksLikeSingleWordHost(originalUntrimmedValue)
 | |
|         ) {
 | |
|           url = originalUntrimmedValue;
 | |
|         }
 | |
|         break;
 | |
|       }
 | |
|       case lazy.UrlbarUtils.RESULT_TYPE.KEYWORD: {
 | |
|         // If this result comes from a bookmark keyword, let it inherit the
 | |
|         // current document's principal, otherwise bookmarklets would break.
 | |
|         openParams.allowInheritPrincipal = true;
 | |
|         break;
 | |
|       }
 | |
|       case lazy.UrlbarUtils.RESULT_TYPE.TAB_SWITCH: {
 | |
|         if (this.hasAttribute("actionoverride")) {
 | |
|           where = "current";
 | |
|           break;
 | |
|         }
 | |
| 
 | |
|         // Keep the searchMode for telemetry since handleRevert sets it to null.
 | |
|         const searchMode = this.searchMode;
 | |
|         this.handleRevert();
 | |
|         let prevTab = this.window.gBrowser.selectedTab;
 | |
|         let loadOpts = {
 | |
|           adoptIntoActiveWindow: lazy.UrlbarPrefs.get(
 | |
|             "switchTabs.adoptIntoActiveWindow"
 | |
|           ),
 | |
|         };
 | |
| 
 | |
|         // We cache the search string because switching tab may clear it.
 | |
|         let searchString = this._lastSearchString;
 | |
|         this.controller.engagementEvent.record(event, {
 | |
|           result,
 | |
|           element,
 | |
|           searchString,
 | |
|           searchMode,
 | |
|           selType: "tabswitch",
 | |
|         });
 | |
| 
 | |
|         let switched = this.window.switchToTabHavingURI(
 | |
|           Services.io.newURI(url),
 | |
|           false,
 | |
|           loadOpts
 | |
|         );
 | |
|         if (switched && prevTab.isEmpty) {
 | |
|           this.window.gBrowser.removeTab(prevTab);
 | |
|         }
 | |
| 
 | |
|         if (switched && !this.isPrivate && !result.heuristic) {
 | |
|           // We don't await for this, because a rejection should not interrupt
 | |
|           // the load. Just reportError it.
 | |
|           lazy.UrlbarUtils.addToInputHistory(url, searchString).catch(
 | |
|             console.error
 | |
|           );
 | |
|         }
 | |
| 
 | |
|         return;
 | |
|       }
 | |
|       case lazy.UrlbarUtils.RESULT_TYPE.SEARCH: {
 | |
|         if (result.payload.providesSearchMode) {
 | |
|           this.controller.engagementEvent.record(event, {
 | |
|             result,
 | |
|             element,
 | |
|             searchString: this._lastSearchString,
 | |
|             selType: this.controller.engagementEvent.typeFromElement(
 | |
|               result,
 | |
|               element
 | |
|             ),
 | |
|           });
 | |
|           this.maybeConfirmSearchModeFromResult({
 | |
|             result,
 | |
|             checkValue: false,
 | |
|           });
 | |
|           return;
 | |
|         }
 | |
| 
 | |
|         if (
 | |
|           !this.searchMode &&
 | |
|           result.heuristic &&
 | |
|           // If we asked the DNS earlier, avoid the post-facto check.
 | |
|           !lazy.UrlbarPrefs.get("browser.fixup.dns_first_for_single_words") &&
 | |
|           // TODO (bug 1642623): for now there is no smart heuristic to skip the
 | |
|           // DNS lookup, so any value above 0 will run it.
 | |
|           lazy.UrlbarPrefs.get("dnsResolveSingleWordsAfterSearch") > 0 &&
 | |
|           this.window.gKeywordURIFixup &&
 | |
|           lazy.UrlbarUtils.looksLikeSingleWordHost(originalUntrimmedValue)
 | |
|         ) {
 | |
|           // When fixing a single word to a search, the docShell would also
 | |
|           // query the DNS and if resolved ask the user whether they would
 | |
|           // rather visit that as a host. On a positive answer, it adds the host
 | |
|           // to the list that we use to make decisions.
 | |
|           // Because we are directly asking for a search here, bypassing the
 | |
|           // docShell, we need to do the same ourselves.
 | |
|           // See also URIFixupChild.jsm and keyword-uri-fixup.
 | |
|           let fixupInfo = this._getURIFixupInfo(originalUntrimmedValue.trim());
 | |
|           if (fixupInfo) {
 | |
|             this.window.gKeywordURIFixup.check(
 | |
|               this.window.gBrowser.selectedBrowser,
 | |
|               fixupInfo
 | |
|             );
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         if (result.payload.inPrivateWindow) {
 | |
|           where = "window";
 | |
|           openParams.private = true;
 | |
|         }
 | |
| 
 | |
|         const actionDetails = {
 | |
|           isSuggestion: !!result.payload.suggestion,
 | |
|           isFormHistory:
 | |
|             result.source == lazy.UrlbarUtils.RESULT_SOURCE.HISTORY,
 | |
|           alias: result.payload.keyword,
 | |
|           url,
 | |
|         };
 | |
|         const engine = Services.search.getEngineByName(result.payload.engine);
 | |
|         this._recordSearch(engine, event, actionDetails);
 | |
| 
 | |
|         if (!result.payload.inPrivateWindow) {
 | |
|           lazy.UrlbarUtils.addToFormHistory(
 | |
|             this,
 | |
|             result.payload.suggestion || result.payload.query,
 | |
|             engine.name
 | |
|           ).catch(console.error);
 | |
|         }
 | |
|         break;
 | |
|       }
 | |
|       case lazy.UrlbarUtils.RESULT_TYPE.TIP: {
 | |
|         let scalarName =
 | |
|           element.classList.contains("urlbarView-button-help") ||
 | |
|           element.dataset.command == "help"
 | |
|             ? `${result.payload.type}-help`
 | |
|             : `${result.payload.type}-picked`;
 | |
|         Services.telemetry.keyedScalarAdd("urlbar.tips", scalarName, 1);
 | |
|         if (url) {
 | |
|           break;
 | |
|         }
 | |
|         this.handleRevert();
 | |
|         this.controller.engagementEvent.record(event, {
 | |
|           result,
 | |
|           element,
 | |
|           selType: "tip",
 | |
|           searchString: this._lastSearchString,
 | |
|         });
 | |
|         return;
 | |
|       }
 | |
|       case lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC: {
 | |
|         if (url) {
 | |
|           break;
 | |
|         }
 | |
|         url = result.payload.url;
 | |
|         // Keep the searchMode for telemetry since handleRevert sets it to null.
 | |
|         const searchMode = this.searchMode;
 | |
|         // Do not revert the Urlbar if we're going to navigate. We want the URL
 | |
|         // populated so we can navigate to it.
 | |
|         if (!url || !result.payload.shouldNavigate) {
 | |
|           this.handleRevert();
 | |
|         }
 | |
|         // If we won't be navigating, this is the end of the engagement.
 | |
|         if (!url || !result.payload.shouldNavigate) {
 | |
|           this.controller.engagementEvent.record(event, {
 | |
|             result,
 | |
|             element,
 | |
|             searchMode,
 | |
|             searchString: this._lastSearchString,
 | |
|             selType: this.controller.engagementEvent.typeFromElement(
 | |
|               result,
 | |
|               element
 | |
|             ),
 | |
|           });
 | |
|           return;
 | |
|         }
 | |
|         break;
 | |
|       }
 | |
|       case lazy.UrlbarUtils.RESULT_TYPE.OMNIBOX: {
 | |
|         this.controller.engagementEvent.record(event, {
 | |
|           result,
 | |
|           element,
 | |
|           selType: "extension",
 | |
|           searchString: this._lastSearchString,
 | |
|         });
 | |
| 
 | |
|         // The urlbar needs to revert to the loaded url when a command is
 | |
|         // handled by the extension.
 | |
|         this.handleRevert();
 | |
|         // We don't directly handle a load when an Omnibox API result is picked,
 | |
|         // instead we forward the request to the WebExtension itself, because
 | |
|         // the value may not even be a url.
 | |
|         // We pass the keyword and content, that actually is the retrieved value
 | |
|         // prefixed by the keyword. ExtensionSearchHandler uses this keyword
 | |
|         // redundancy as a sanity check.
 | |
|         lazy.ExtensionSearchHandler.handleInputEntered(
 | |
|           result.payload.keyword,
 | |
|           result.payload.content,
 | |
|           where
 | |
|         );
 | |
|         return;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (!url) {
 | |
|       throw new Error(`Invalid url for result ${JSON.stringify(result)}`);
 | |
|     }
 | |
| 
 | |
|     // Record input history but only in non-private windows.
 | |
|     if (!this.isPrivate) {
 | |
|       let input;
 | |
|       if (!result.heuristic) {
 | |
|         input = this._lastSearchString;
 | |
|       } else if (result.autofill?.type == "adaptive") {
 | |
|         input = result.autofill.adaptiveHistoryInput;
 | |
|       }
 | |
|       // `input` may be an empty string, so do a strict comparison here.
 | |
|       if (input !== undefined) {
 | |
|         // We don't await for this, because a rejection should not interrupt
 | |
|         // the load. Just reportError it.
 | |
|         lazy.UrlbarUtils.addToInputHistory(url, input).catch(console.error);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     this.controller.engagementEvent.record(event, {
 | |
|       result,
 | |
|       element,
 | |
|       searchString: this._lastSearchString,
 | |
|       selType: this.controller.engagementEvent.typeFromElement(result, element),
 | |
|       searchSource: this.getSearchSource(event),
 | |
|     });
 | |
| 
 | |
|     if (result.payload.sendAttributionRequest) {
 | |
|       lazy.PartnerLinkAttribution.makeRequest({
 | |
|         targetURL: result.payload.url,
 | |
|         source: "urlbar",
 | |
|         campaignID: Services.prefs.getStringPref(
 | |
|           "browser.partnerlink.campaign.topsites"
 | |
|         ),
 | |
|       });
 | |
|       if (!this.isPrivate && result.providerName === "UrlbarProviderTopSites") {
 | |
|         // The position is 1-based for telemetry
 | |
|         const position = result.rowIndex + 1;
 | |
|         Services.telemetry.keyedScalarAdd(
 | |
|           SCALAR_CATEGORY_TOPSITES,
 | |
|           `urlbar_${position}`,
 | |
|           1
 | |
|         );
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     this._loadURL(
 | |
|       url,
 | |
|       event,
 | |
|       where,
 | |
|       openParams,
 | |
|       {
 | |
|         source: result.source,
 | |
|         type: result.type,
 | |
|         searchTerm: result.payload.suggestion ?? result.payload.query,
 | |
|       },
 | |
|       browser
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Called by the view when moving through results with the keyboard, and when
 | |
|    * picking a result.  This sets the input value to the value of the result and
 | |
|    * invalidates the pageproxystate.  It also sets the result that is associated
 | |
|    * with the current input value.  If you need to set this result but don't
 | |
|    * want to also set the input value, then use setResultForCurrentValue.
 | |
|    *
 | |
|    * @param {object} options
 | |
|    *   Options.
 | |
|    * @param {UrlbarResult} [options.result]
 | |
|    *   The result that was selected or picked, null if no result was selected.
 | |
|    * @param {Event} [options.event]
 | |
|    *   The event that picked the result.
 | |
|    * @param {string} [options.urlOverride]
 | |
|    *   Normally the URL is taken from `result.payload.url`, but if `urlOverride`
 | |
|    *   is specified, it's used instead.
 | |
|    * @returns {boolean}
 | |
|    *   Whether the value has been canonized
 | |
|    */
 | |
|   setValueFromResult({ result = null, event = null, urlOverride = null } = {}) {
 | |
|     // Usually this is set by a previous input event, but in certain cases, like
 | |
|     // when opening Top Sites on a loaded page, it wouldn't happen. To avoid
 | |
|     // confusing the user, we always enforce it when a result changes our value.
 | |
|     this.setPageProxyState("invalid", true);
 | |
| 
 | |
|     // A previous result may have previewed search mode. If we don't expect that
 | |
|     // we might stay in a search mode of some kind, exit it now.
 | |
|     if (
 | |
|       this.searchMode?.isPreview &&
 | |
|       !result?.payload.providesSearchMode &&
 | |
|       !this.view.oneOffSearchButtons.selectedButton
 | |
|     ) {
 | |
|       this.searchMode = null;
 | |
|     }
 | |
| 
 | |
|     if (!result) {
 | |
|       // This happens when there's no selection, for example when moving to the
 | |
|       // one-offs search settings button, or to the input field when Top Sites
 | |
|       // are shown; then we must reset the input value.
 | |
|       // Note that for Top Sites the last search string would be empty, thus we
 | |
|       // must restore the last text value.
 | |
|       // Note that unselected autofill results will still arrive in this
 | |
|       // function with a non-null `result`. They are handled below.
 | |
|       this.value = this._lastSearchString || this._valueOnLastSearch;
 | |
|       this.setResultForCurrentValue(result);
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     // The value setter clobbers the actiontype attribute, so we need this
 | |
|     // helper to restore it afterwards.
 | |
|     const setValueAndRestoreActionType = (value, allowTrim) => {
 | |
|       this._setValue(value, allowTrim);
 | |
| 
 | |
|       switch (result.type) {
 | |
|         case lazy.UrlbarUtils.RESULT_TYPE.TAB_SWITCH:
 | |
|           this.setAttribute("actiontype", "switchtab");
 | |
|           break;
 | |
|         case lazy.UrlbarUtils.RESULT_TYPE.OMNIBOX:
 | |
|           this.setAttribute("actiontype", "extension");
 | |
|           break;
 | |
|       }
 | |
|     };
 | |
| 
 | |
|     // For autofilled results, the value that should be canonized is not the
 | |
|     // autofilled value but the value that the user typed.
 | |
|     let canonizedUrl = this._maybeCanonizeURL(
 | |
|       event,
 | |
|       result.autofill ? this._lastSearchString : this.value
 | |
|     );
 | |
|     if (canonizedUrl) {
 | |
|       setValueAndRestoreActionType(canonizedUrl, true);
 | |
|       this.setResultForCurrentValue(result);
 | |
|       return true;
 | |
|     }
 | |
| 
 | |
|     if (result.autofill) {
 | |
|       this._autofillValue(result.autofill);
 | |
|     }
 | |
| 
 | |
|     if (result.payload.providesSearchMode) {
 | |
|       let enteredSearchMode;
 | |
|       // Only preview search mode if the result is selected.
 | |
|       if (this.view.resultIsSelected(result)) {
 | |
|         // Not starting a query means we will only preview search mode.
 | |
|         enteredSearchMode = this.maybeConfirmSearchModeFromResult({
 | |
|           result,
 | |
|           checkValue: false,
 | |
|           startQuery: false,
 | |
|         });
 | |
|       }
 | |
|       if (!enteredSearchMode) {
 | |
|         setValueAndRestoreActionType(this._getValueFromResult(result), true);
 | |
|         this.searchMode = null;
 | |
|       }
 | |
|       this.setResultForCurrentValue(result);
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     // If the url is trimmed but it's invalid (for example it has an unknown
 | |
|     // single word host, or an unknown domain suffix), trimming
 | |
|     // it would end up executing a search instead of visiting it.
 | |
|     let allowTrim = true;
 | |
|     if (
 | |
|       (urlOverride || result.type == lazy.UrlbarUtils.RESULT_TYPE.URL) &&
 | |
|       lazy.UrlbarPrefs.get("trimURLs")
 | |
|     ) {
 | |
|       let url = urlOverride || result.payload.url;
 | |
|       if (url.startsWith(lazy.BrowserUIUtils.trimURLProtocol)) {
 | |
|         let fixupInfo = this._getURIFixupInfo(lazy.BrowserUIUtils.trimURL(url));
 | |
|         if (fixupInfo?.keywordAsSent) {
 | |
|           allowTrim = false;
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (!result.autofill) {
 | |
|       setValueAndRestoreActionType(
 | |
|         this._getValueFromResult(result, urlOverride),
 | |
|         allowTrim
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     this.setResultForCurrentValue(result);
 | |
| 
 | |
|     // Update placeholder selection and value to the current selected result to
 | |
|     // prevent the on_selectionchange event to detect a "accent-character"
 | |
|     // insertion.
 | |
|     if (!result.autofill && this._autofillPlaceholder) {
 | |
|       this._autofillPlaceholder.value = this.value;
 | |
|       this._autofillPlaceholder.selectionStart = this.value.length;
 | |
|       this._autofillPlaceholder.selectionEnd = this.value.length;
 | |
|     }
 | |
|     return false;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * The input keeps track of the result associated with the current input
 | |
|    * value.  This result can be set by calling either setValueFromResult or this
 | |
|    * method.  Use this method when you need to set the result without also
 | |
|    * setting the input value.  This can be the case when either the selection is
 | |
|    * cleared and no other result becomes selected, or when the result is the
 | |
|    * heuristic and we don't want to modify the value the user is typing.
 | |
|    *
 | |
|    * @param {UrlbarResult} result
 | |
|    *   The result to associate with the current input value.
 | |
|    */
 | |
|   setResultForCurrentValue(result) {
 | |
|     this._resultForCurrentValue = result;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Called by the controller when the first result of a new search is received.
 | |
|    * If it's an autofill result, then it may need to be autofilled, subject to a
 | |
|    * few restrictions.
 | |
|    *
 | |
|    * @param {UrlbarResult} result
 | |
|    *   The first result.
 | |
|    */
 | |
|   _autofillFirstResult(result) {
 | |
|     if (!result.autofill) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let isPlaceholderSelected =
 | |
|       this._autofillPlaceholder &&
 | |
|       this.selectionEnd == this._autofillPlaceholder.value.length &&
 | |
|       this.selectionStart == this._lastSearchString.length &&
 | |
|       this._autofillPlaceholder.value
 | |
|         .toLocaleLowerCase()
 | |
|         .startsWith(this._lastSearchString.toLocaleLowerCase());
 | |
| 
 | |
|     // Don't autofill if there's already a selection (with one caveat described
 | |
|     // next) or the cursor isn't at the end of the input.  But if there is a
 | |
|     // selection and it's the autofill placeholder value, then do autofill.
 | |
|     if (
 | |
|       !isPlaceholderSelected &&
 | |
|       !this._autofillIgnoresSelection &&
 | |
|       (this.selectionStart != this.selectionEnd ||
 | |
|         this.selectionEnd != this._lastSearchString.length)
 | |
|     ) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     this.setValueFromResult({ result });
 | |
|   }
 | |
|   /**
 | |
|    * Clears displayed autofill values and unsets the autofill placeholder.
 | |
|    */
 | |
|   #clearAutofill() {
 | |
|     if (!this._autofillPlaceholder) {
 | |
|       return;
 | |
|     }
 | |
|     let currentSelectionStart = this.selectionStart;
 | |
|     let currentSelectionEnd = this.selectionEnd;
 | |
| 
 | |
|     // Overriding this value clears the selection.
 | |
|     this.inputField.value = this.value.substring(
 | |
|       0,
 | |
|       this._autofillPlaceholder.selectionStart
 | |
|     );
 | |
|     this._autofillPlaceholder = null;
 | |
|     // Restore selection
 | |
|     this.setSelectionRange(currentSelectionStart, currentSelectionEnd);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Invoked by the controller when the first result is received.
 | |
|    *
 | |
|    * @param {UrlbarResult} firstResult
 | |
|    *   The first result received.
 | |
|    * @returns {boolean}
 | |
|    *   True if this method canceled the query and started a new one.  False
 | |
|    *   otherwise.
 | |
|    */
 | |
|   onFirstResult(firstResult) {
 | |
|     // If the heuristic result has a keyword but isn't a keyword offer, we may
 | |
|     // need to enter search mode.
 | |
|     if (
 | |
|       firstResult.heuristic &&
 | |
|       firstResult.payload.keyword &&
 | |
|       !firstResult.payload.providesSearchMode &&
 | |
|       this.maybeConfirmSearchModeFromResult({
 | |
|         result: firstResult,
 | |
|         entry: "typed",
 | |
|         checkValue: false,
 | |
|       })
 | |
|     ) {
 | |
|       return true;
 | |
|     }
 | |
| 
 | |
|     // To prevent selection flickering, we apply autofill on input through a
 | |
|     // placeholder, without waiting for results. But, if the first result is
 | |
|     // not an autofill one, the autofill prediction was wrong and we should
 | |
|     // restore the original user typed string.
 | |
|     if (firstResult.autofill) {
 | |
|       this._autofillFirstResult(firstResult);
 | |
|     } else if (
 | |
|       this._autofillPlaceholder &&
 | |
|       // Avoid clobbering added spaces (for token aliases, for example).
 | |
|       !this.value.endsWith(" ")
 | |
|     ) {
 | |
|       this._autofillPlaceholder = null;
 | |
|       this._setValue(this.window.gBrowser.userTypedValue, false);
 | |
|     }
 | |
| 
 | |
|     return false;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Starts a query based on the current input value.
 | |
|    *
 | |
|    * @param {object} [options]
 | |
|    *   Object options
 | |
|    * @param {boolean} [options.allowAutofill]
 | |
|    *   Whether or not to allow providers to include autofill results.
 | |
|    * @param {boolean} [options.autofillIgnoresSelection]
 | |
|    *   Normally we autofill only if the cursor is at the end of the string,
 | |
|    *   if this is set we'll autofill regardless of selection.
 | |
|    * @param {string} [options.searchString]
 | |
|    *   The search string.  If not given, the current input value is used.
 | |
|    *   Otherwise, the current input value must start with this value.
 | |
|    * @param {boolean} [options.resetSearchState]
 | |
|    *   If this is the first search of a user interaction with the input, set
 | |
|    *   this to true (the default) so that search-related state from the previous
 | |
|    *   interaction doesn't interfere with the new interaction.  Otherwise set it
 | |
|    *   to false so that state is maintained during a single interaction.  The
 | |
|    *   intended use for this parameter is that it should be set to false when
 | |
|    *   this method is called due to input events.
 | |
|    * @param {event} [options.event]
 | |
|    *   The user-generated event that triggered the query, if any.  If given, we
 | |
|    *   will record engagement event telemetry for the query.
 | |
|    */
 | |
|   startQuery({
 | |
|     allowAutofill = true,
 | |
|     autofillIgnoresSelection = false,
 | |
|     searchString = null,
 | |
|     resetSearchState = true,
 | |
|     event = null,
 | |
|   } = {}) {
 | |
|     if (!searchString) {
 | |
|       searchString =
 | |
|         this.getAttribute("pageproxystate") == "valid" ? "" : this.value;
 | |
|     } else if (!this.value.startsWith(searchString)) {
 | |
|       throw new Error("The current value doesn't start with the search string");
 | |
|     }
 | |
| 
 | |
|     if (event) {
 | |
|       this.controller.engagementEvent.start(event, searchString);
 | |
|     }
 | |
| 
 | |
|     if (this._suppressStartQuery) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     this._autofillIgnoresSelection = autofillIgnoresSelection;
 | |
|     if (resetSearchState) {
 | |
|       this._resetSearchState();
 | |
|     }
 | |
| 
 | |
|     this._lastSearchString = searchString;
 | |
|     this._valueOnLastSearch = this.value;
 | |
| 
 | |
|     let options = {
 | |
|       allowAutofill,
 | |
|       isPrivate: this.isPrivate,
 | |
|       maxResults: lazy.UrlbarPrefs.get("maxRichResults"),
 | |
|       searchString,
 | |
|       userContextId:
 | |
|         this.window.gBrowser.selectedBrowser.getAttribute("usercontextid"),
 | |
|       currentPage: this.window.gBrowser.currentURI.spec,
 | |
|       formHistoryName: this.formHistoryName,
 | |
|       prohibitRemoteResults:
 | |
|         event &&
 | |
|         lazy.UrlbarUtils.isPasteEvent(event) &&
 | |
|         lazy.UrlbarPrefs.get("maxCharsForSearchSuggestions") <
 | |
|           event.data?.length,
 | |
|     };
 | |
| 
 | |
|     if (this.searchMode) {
 | |
|       this.confirmSearchMode();
 | |
|       options.searchMode = this.searchMode;
 | |
|       if (this.searchMode.source) {
 | |
|         options.sources = [this.searchMode.source];
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // TODO (Bug 1522902): This promise is necessary for tests, because some
 | |
|     // tests are not listening for completion when starting a query through
 | |
|     // other methods than startQuery (input events for example).
 | |
|     this.lastQueryContextPromise = this.controller.startQuery(
 | |
|       new lazy.UrlbarQueryContext(options)
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Sets the input's value, starts a search, and opens the view.
 | |
|    *
 | |
|    * @param {string} value
 | |
|    *   The input's value will be set to this value, and the search will
 | |
|    *   use it as its query.
 | |
|    * @param {object} [options]
 | |
|    *   Object options
 | |
|    * @param {nsISearchEngine} [options.searchEngine]
 | |
|    *   Search engine to use when the search is using a known alias.
 | |
|    * @param {UrlbarUtils.SEARCH_MODE_ENTRY} [options.searchModeEntry]
 | |
|    *   If provided, we will record this parameter as the search mode entry point
 | |
|    *   in Telemetry. Consumers should provide this if they expect their call
 | |
|    *   to enter search mode.
 | |
|    * @param {boolean} [options.focus]
 | |
|    *   If true, the urlbar will be focused.  If false, the focus will remain
 | |
|    *   unchanged.
 | |
|    */
 | |
|   search(value, { searchEngine, searchModeEntry, focus = true } = {}) {
 | |
|     if (focus) {
 | |
|       this.focus();
 | |
|     }
 | |
|     let trimmedValue = value.trim();
 | |
|     let end = trimmedValue.search(lazy.UrlbarTokenizer.REGEXP_SPACES);
 | |
|     let firstToken = end == -1 ? trimmedValue : trimmedValue.substring(0, end);
 | |
|     // Enter search mode if the string starts with a restriction token.
 | |
|     let searchMode = lazy.UrlbarUtils.searchModeForToken(firstToken);
 | |
|     let firstTokenIsRestriction = !!searchMode;
 | |
|     if (!searchMode && searchEngine) {
 | |
|       searchMode = { engineName: searchEngine.name };
 | |
|       firstTokenIsRestriction = searchEngine.aliases.includes(firstToken);
 | |
|     }
 | |
| 
 | |
|     if (searchMode) {
 | |
|       searchMode.entry = searchModeEntry;
 | |
|       this.searchMode = searchMode;
 | |
|       if (firstTokenIsRestriction) {
 | |
|         // Remove the restriction token/alias from the string to be searched for
 | |
|         // in search mode.
 | |
|         value = value.replace(firstToken, "");
 | |
|       }
 | |
|       if (lazy.UrlbarTokenizer.REGEXP_SPACES.test(value[0])) {
 | |
|         // If there was a trailing space after the restriction token/alias,
 | |
|         // remove it.
 | |
|         value = value.slice(1);
 | |
|       }
 | |
|       this._revertOnBlurValue = value;
 | |
|     } else if (
 | |
|       Object.values(lazy.UrlbarTokenizer.RESTRICT).includes(firstToken)
 | |
|     ) {
 | |
|       this.searchMode = null;
 | |
|       // If the entire value is a restricted token, append a space.
 | |
|       if (Object.values(lazy.UrlbarTokenizer.RESTRICT).includes(value)) {
 | |
|         value += " ";
 | |
|       }
 | |
|       this._revertOnBlurValue = value;
 | |
|     }
 | |
|     this.inputField.value = value;
 | |
|     // Avoid selecting the text if this method is called twice in a row.
 | |
|     this.selectionStart = -1;
 | |
| 
 | |
|     // Note: proper IME Composition handling depends on the fact this generates
 | |
|     // an input event, rather than directly invoking the controller; everything
 | |
|     // goes through _on_input, that will properly skip the search until the
 | |
|     // composition is committed. _on_input also skips the search when it's the
 | |
|     // same as the previous search, but we want to allow consecutive searches
 | |
|     // with the same string. So clear _lastSearchString first.
 | |
|     this._lastSearchString = "";
 | |
|     let event = new UIEvent("input", {
 | |
|       bubbles: true,
 | |
|       cancelable: false,
 | |
|       view: this.window,
 | |
|       detail: 0,
 | |
|     });
 | |
|     this.inputField.dispatchEvent(event);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Focus without the focus styles.
 | |
|    * This is used by Activity Stream and about:privatebrowsing for search hand-off.
 | |
|    */
 | |
|   setHiddenFocus() {
 | |
|     this._hideFocus = true;
 | |
|     if (this.focused) {
 | |
|       this.removeAttribute("focused");
 | |
|     } else {
 | |
|       this.focus();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Restore focus styles.
 | |
|    * This is used by Activity Stream and about:privatebrowsing for search hand-off.
 | |
|    *
 | |
|    * @param {Browser} forceSuppressFocusBorder
 | |
|    *   Set true to suppress-focus-border attribute if this flag is true.
 | |
|    */
 | |
|   removeHiddenFocus(forceSuppressFocusBorder = false) {
 | |
|     this._hideFocus = false;
 | |
|     if (this.focused) {
 | |
|       this.setAttribute("focused", "true");
 | |
| 
 | |
|       if (forceSuppressFocusBorder) {
 | |
|         this.toggleAttribute("suppress-focus-border", true);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Gets the search mode for a specific browser instance.
 | |
|    *
 | |
|    * @param {Browser} browser
 | |
|    *   The search mode for this browser will be returned.
 | |
|    * @param {boolean} [confirmedOnly]
 | |
|    *   Normally, if the browser has both preview and confirmed modes, preview
 | |
|    *   mode will be returned since it takes precedence.  If this argument is
 | |
|    *   true, then only confirmed search mode will be returned, or null if
 | |
|    *   search mode hasn't been confirmed.
 | |
|    * @returns {object}
 | |
|    *   A search mode object.  See setSearchMode documentation.  If the browser
 | |
|    *   is not in search mode, then null is returned.
 | |
|    */
 | |
|   getSearchMode(browser, confirmedOnly = false) {
 | |
|     let modes = this._searchModesByBrowser.get(browser);
 | |
| 
 | |
|     // Return copies so that callers don't modify the stored values.
 | |
|     if (!confirmedOnly && modes?.preview) {
 | |
|       return { ...modes.preview };
 | |
|     }
 | |
|     if (modes?.confirmed) {
 | |
|       return { ...modes.confirmed };
 | |
|     }
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Sets search mode for a specific browser instance.  If the given browser is
 | |
|    * selected, then this will also enter search mode.
 | |
|    *
 | |
|    * @param {object} searchMode
 | |
|    *   A search mode object.
 | |
|    * @param {string} searchMode.engineName
 | |
|    *   The name of the search engine to restrict to.
 | |
|    * @param {UrlbarUtils.RESULT_SOURCE} searchMode.source
 | |
|    *   A result source to restrict to.
 | |
|    * @param {string} searchMode.entry
 | |
|    *   How search mode was entered. This is recorded in event telemetry. One of
 | |
|    *   the values in UrlbarUtils.SEARCH_MODE_ENTRY.
 | |
|    * @param {boolean} [searchMode.isPreview]
 | |
|    *   If true, we will preview search mode. Search mode preview does not record
 | |
|    *   telemetry and has slighly different UI behavior. The preview is exited in
 | |
|    *   favor of full search mode when a query is executed. False should be
 | |
|    *   passed if the caller needs to enter search mode but expects it will not
 | |
|    *   be interacted with right away. Defaults to true.
 | |
|    * @param {Browser} browser
 | |
|    *   The browser for which to set search mode.
 | |
|    */
 | |
|   async setSearchMode(searchMode, browser) {
 | |
|     let currentSearchMode = this.getSearchMode(browser);
 | |
|     let areSearchModesSame =
 | |
|       (!currentSearchMode && !searchMode) ||
 | |
|       lazy.ObjectUtils.deepEqual(currentSearchMode, searchMode);
 | |
| 
 | |
|     // Exit search mode if the passed-in engine is invalid or hidden.
 | |
|     let engine;
 | |
|     if (searchMode?.engineName) {
 | |
|       if (!Services.search.isInitialized) {
 | |
|         await Services.search.init();
 | |
|       }
 | |
|       engine = Services.search.getEngineByName(searchMode.engineName);
 | |
|       if (!engine || engine.hidden) {
 | |
|         searchMode = null;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     let { engineName, source, entry, isPreview = true } = searchMode || {};
 | |
| 
 | |
|     searchMode = null;
 | |
| 
 | |
|     if (engineName) {
 | |
|       searchMode = {
 | |
|         engineName,
 | |
|         isGeneralPurposeEngine: engine.isGeneralPurposeEngine,
 | |
|       };
 | |
|       if (source) {
 | |
|         searchMode.source = source;
 | |
|       } else if (searchMode.isGeneralPurposeEngine) {
 | |
|         // History results for general-purpose search engines are often not
 | |
|         // useful, so we hide them in search mode. See bug 1658646 for
 | |
|         // discussion.
 | |
|         searchMode.source = lazy.UrlbarUtils.RESULT_SOURCE.SEARCH;
 | |
|       }
 | |
|     } else if (source) {
 | |
|       let sourceName = lazy.UrlbarUtils.getResultSourceName(source);
 | |
|       if (sourceName) {
 | |
|         searchMode = { source };
 | |
|       } else {
 | |
|         console.error(`Unrecognized source: ${source}`);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (searchMode) {
 | |
|       searchMode.isPreview = isPreview;
 | |
|       if (lazy.UrlbarUtils.SEARCH_MODE_ENTRY.has(entry)) {
 | |
|         searchMode.entry = entry;
 | |
|       } else {
 | |
|         // If we see this value showing up in telemetry, we should review
 | |
|         // search mode's entry points.
 | |
|         searchMode.entry = "other";
 | |
|       }
 | |
| 
 | |
|       // Add the search mode to the map.
 | |
|       if (!searchMode.isPreview) {
 | |
|         this._searchModesByBrowser.set(browser, {
 | |
|           confirmed: searchMode,
 | |
|         });
 | |
|       } else {
 | |
|         let modes = this._searchModesByBrowser.get(browser) || {};
 | |
|         modes.preview = searchMode;
 | |
|         this._searchModesByBrowser.set(browser, modes);
 | |
|       }
 | |
|     } else {
 | |
|       this._searchModesByBrowser.delete(browser);
 | |
|     }
 | |
| 
 | |
|     // Enter search mode if the browser is selected.
 | |
|     if (browser == this.window.gBrowser.selectedBrowser) {
 | |
|       this._updateSearchModeUI(searchMode);
 | |
|       if (searchMode) {
 | |
|         // Set userTypedValue to the query string so that it's properly restored
 | |
|         // when switching back to the current tab and across sessions.
 | |
|         this.window.gBrowser.userTypedValue = this.untrimmedValue;
 | |
|         this.valueIsTyped = true;
 | |
|         if (!searchMode.isPreview && !areSearchModesSame) {
 | |
|           try {
 | |
|             lazy.BrowserSearchTelemetry.recordSearchMode(searchMode);
 | |
|           } catch (ex) {
 | |
|             console.error(ex);
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Restores the current browser search mode from a previously stored state.
 | |
|    */
 | |
|   restoreSearchModeState() {
 | |
|     let modes = this._searchModesByBrowser.get(
 | |
|       this.window.gBrowser.selectedBrowser
 | |
|     );
 | |
|     this.searchMode = modes?.confirmed;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Enters search mode with the default engine.
 | |
|    */
 | |
|   searchModeShortcut() {
 | |
|     // We restrict to search results when entering search mode from this
 | |
|     // shortcut to honor historical behaviour.
 | |
|     this.searchMode = {
 | |
|       source: lazy.UrlbarUtils.RESULT_SOURCE.SEARCH,
 | |
|       engineName: lazy.UrlbarSearchUtils.getDefaultEngine(this.isPrivate)?.name,
 | |
|       entry: "shortcut",
 | |
|     };
 | |
|     // The searchMode setter clears the input if pageproxystate is valid, so
 | |
|     // we know at this point this.value will either be blank or the user's
 | |
|     // typed string.
 | |
|     this.search(this.value);
 | |
|     this.select();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Confirms the current search mode.
 | |
|    */
 | |
|   confirmSearchMode() {
 | |
|     let searchMode = this.searchMode;
 | |
|     if (searchMode?.isPreview) {
 | |
|       searchMode.isPreview = false;
 | |
|       this.searchMode = searchMode;
 | |
| 
 | |
|       // Unselect the one-off search button to ensure UI consistency.
 | |
|       this.view.oneOffSearchButtons.selectedButton = null;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // Getters and Setters below.
 | |
| 
 | |
|   get editor() {
 | |
|     return this.inputField.editor;
 | |
|   }
 | |
| 
 | |
|   get focused() {
 | |
|     return this.document.activeElement == this.inputField;
 | |
|   }
 | |
| 
 | |
|   get goButton() {
 | |
|     return this.querySelector("#urlbar-go-button");
 | |
|   }
 | |
| 
 | |
|   get value() {
 | |
|     return this.inputField.value;
 | |
|   }
 | |
| 
 | |
|   get untrimmedValue() {
 | |
|     return this._untrimmedValue;
 | |
|   }
 | |
| 
 | |
|   set value(val) {
 | |
|     this._setValue(val, true);
 | |
|   }
 | |
| 
 | |
|   get lastSearchString() {
 | |
|     return this._lastSearchString;
 | |
|   }
 | |
| 
 | |
|   get searchMode() {
 | |
|     return this.getSearchMode(this.window.gBrowser.selectedBrowser);
 | |
|   }
 | |
| 
 | |
|   set searchMode(searchMode) {
 | |
|     this.setSearchMode(searchMode, this.window.gBrowser.selectedBrowser);
 | |
|   }
 | |
| 
 | |
|   async updateLayoutBreakout() {
 | |
|     if (!this._toolbar) {
 | |
|       // Expanding requires a parent toolbar.
 | |
|       return;
 | |
|     }
 | |
|     if (this.document.fullscreenElement) {
 | |
|       // Toolbars are hidden in DOM fullscreen mode, so we can't get proper
 | |
|       // layout information and need to retry after leaving that mode.
 | |
|       this.window.addEventListener(
 | |
|         "fullscreen",
 | |
|         () => {
 | |
|           this.updateLayoutBreakout();
 | |
|         },
 | |
|         { once: true }
 | |
|       );
 | |
|       return;
 | |
|     }
 | |
|     await this._updateLayoutBreakoutDimensions();
 | |
|   }
 | |
| 
 | |
|   startLayoutExtend() {
 | |
|     // Do not expand if:
 | |
|     // The Urlbar does not support being expanded or it is already expanded
 | |
|     if (
 | |
|       !this.hasAttribute("breakout") ||
 | |
|       this.hasAttribute("breakout-extend")
 | |
|     ) {
 | |
|       return;
 | |
|     }
 | |
|     if (!this.view.isOpen) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (Cu.isInAutomation) {
 | |
|       if (lazy.UrlbarPrefs.get("disableExtendForTests")) {
 | |
|         this.setAttribute("breakout-extend-disabled", "true");
 | |
|         return;
 | |
|       }
 | |
|       this.removeAttribute("breakout-extend-disabled");
 | |
|     }
 | |
| 
 | |
|     this._toolbar.setAttribute("urlbar-exceeds-toolbar-bounds", "true");
 | |
|     this.setAttribute("breakout-extend", "true");
 | |
| 
 | |
|     // Enable the animation only after the first extend call to ensure it
 | |
|     // doesn't run when opening a new window.
 | |
|     if (!this.hasAttribute("breakout-extend-animate")) {
 | |
|       this.window.promiseDocumentFlushed(() => {
 | |
|         this.window.requestAnimationFrame(() => {
 | |
|           this.setAttribute("breakout-extend-animate", "true");
 | |
|         });
 | |
|       });
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   endLayoutExtend() {
 | |
|     // If reduce motion is enabled, we want to collapse the Urlbar here so the
 | |
|     // user sees only sees two states: not expanded, and expanded with the view
 | |
|     // open.
 | |
|     if (!this.hasAttribute("breakout-extend") || this.view.isOpen) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     this.removeAttribute("breakout-extend");
 | |
|     this._toolbar.removeAttribute("urlbar-exceeds-toolbar-bounds");
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Updates the user interface to indicate whether the URI in the address bar
 | |
|    * is different than the loaded page, because it's being edited or because a
 | |
|    * search result is currently selected and is displayed in the location bar.
 | |
|    *
 | |
|    * @param {string} state
 | |
|    *        The string "valid" indicates that the security indicators and other
 | |
|    *        related user interface elments should be shown because the URI in
 | |
|    *        the location bar matches the loaded page. The string "invalid"
 | |
|    *        indicates that the URI in the location bar is different than the
 | |
|    *        loaded page.
 | |
|    * @param {boolean} [updatePopupNotifications]
 | |
|    *        Indicates whether we should update the PopupNotifications
 | |
|    *        visibility due to this change, otherwise avoid doing so as it is
 | |
|    *        being handled somewhere else.
 | |
|    */
 | |
|   setPageProxyState(state, updatePopupNotifications) {
 | |
|     let prevState = this.getAttribute("pageproxystate");
 | |
| 
 | |
|     this.setAttribute("pageproxystate", state);
 | |
|     this._inputContainer.setAttribute("pageproxystate", state);
 | |
|     this._identityBox.setAttribute("pageproxystate", state);
 | |
| 
 | |
|     if (state == "valid") {
 | |
|       this._lastValidURLStr = this.value;
 | |
|     }
 | |
| 
 | |
|     if (
 | |
|       updatePopupNotifications &&
 | |
|       prevState != state &&
 | |
|       this.window.UpdatePopupNotificationsVisibility
 | |
|     ) {
 | |
|       this.window.UpdatePopupNotificationsVisibility();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * When switching tabs quickly, TabSelect sometimes happens before
 | |
|    * _adjustFocusAfterTabSwitch and due to the focus still being on the old
 | |
|    * tab, we end up flickering the results pane briefly.
 | |
|    */
 | |
|   afterTabSwitchFocusChange() {
 | |
|     this._gotFocusChange = true;
 | |
|     this._afterTabSelectAndFocusChange();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Confirms search mode and starts a new search if appropriate for the given
 | |
|    * result.  See also _searchModeForResult.
 | |
|    *
 | |
|    * @param {object} options
 | |
|    *   Options object.
 | |
|    * @param {string} options.entry
 | |
|    *   The search mode entry point. See setSearchMode documentation for details.
 | |
|    * @param {UrlbarResult} [options.result]
 | |
|    *   The result to confirm. Defaults to the currently selected result.
 | |
|    * @param {boolean} [options.checkValue]
 | |
|    *   If true, the trimmed input value must equal the result's keyword in order
 | |
|    *   to enter search mode.
 | |
|    * @param {boolean} [options.startQuery]
 | |
|    *   If true, start a query after entering search mode. Defaults to true.
 | |
|    * @returns {boolean}
 | |
|    *   True if we entered search mode and false if not.
 | |
|    */
 | |
|   maybeConfirmSearchModeFromResult({
 | |
|     entry,
 | |
|     result = this._resultForCurrentValue,
 | |
|     checkValue = true,
 | |
|     startQuery = true,
 | |
|   }) {
 | |
|     if (
 | |
|       !result ||
 | |
|       (checkValue && this.value.trim() != result.payload.keyword?.trim())
 | |
|     ) {
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     let searchMode = this._searchModeForResult(result, entry);
 | |
|     if (!searchMode) {
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     this.searchMode = searchMode;
 | |
| 
 | |
|     let value = result.payload.query?.trimStart() || "";
 | |
|     this._setValue(value, false);
 | |
| 
 | |
|     if (startQuery) {
 | |
|       this.startQuery({ allowAutofill: false });
 | |
|     }
 | |
| 
 | |
|     return true;
 | |
|   }
 | |
| 
 | |
|   observe(subject, topic, data) {
 | |
|     switch (topic) {
 | |
|       case lazy.SearchUtils.TOPIC_ENGINE_MODIFIED: {
 | |
|         switch (data) {
 | |
|           case lazy.SearchUtils.MODIFIED_TYPE.CHANGED:
 | |
|           case lazy.SearchUtils.MODIFIED_TYPE.REMOVED: {
 | |
|             let searchMode = this.searchMode;
 | |
|             let engine = subject.QueryInterface(Ci.nsISearchEngine);
 | |
|             if (searchMode?.engineName == engine.name) {
 | |
|               // Exit search mode if the current search mode engine was removed.
 | |
|               this.searchMode = searchMode;
 | |
|             }
 | |
|             break;
 | |
|           }
 | |
|         }
 | |
|         break;
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Get search source.
 | |
|    *
 | |
|    * @param {Event} event
 | |
|    *   The event that triggered this query.
 | |
|    * @returns {string}
 | |
|    *   The source name.
 | |
|    */
 | |
|   getSearchSource(event) {
 | |
|     if (this._isHandoffSession) {
 | |
|       return "urlbar-handoff";
 | |
|     }
 | |
| 
 | |
|     const isOneOff = this.view.oneOffSearchButtons.eventTargetIsAOneOff(event);
 | |
|     if (this.searchMode && !isOneOff) {
 | |
|       // Without checking !isOneOff, we might record the string
 | |
|       // oneoff_urlbar-searchmode in the SEARCH_COUNTS probe (in addition to
 | |
|       // oneoff_urlbar and oneoff_searchbar). The extra information is not
 | |
|       // necessary; the intent is the same regardless of whether the user is
 | |
|       // in search mode when they do a key-modified click/enter on a one-off.
 | |
|       return "urlbar-searchmode";
 | |
|     }
 | |
| 
 | |
|     if (this.window.gBrowser.selectedBrowser.searchTerms && !isOneOff) {
 | |
|       return "urlbar-persisted";
 | |
|     }
 | |
| 
 | |
|     return "urlbar";
 | |
|   }
 | |
| 
 | |
|   // Private methods below.
 | |
| 
 | |
|   _addObservers() {
 | |
|     Services.obs.addObserver(
 | |
|       this,
 | |
|       lazy.SearchUtils.TOPIC_ENGINE_MODIFIED,
 | |
|       true
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   _getURIFixupInfo(searchString) {
 | |
|     let flags =
 | |
|       Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS |
 | |
|       Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
 | |
|     if (this.isPrivate) {
 | |
|       flags |= Ci.nsIURIFixup.FIXUP_FLAG_PRIVATE_CONTEXT;
 | |
|     }
 | |
|     try {
 | |
|       return Services.uriFixup.getFixupURIInfo(searchString, flags);
 | |
|     } catch (ex) {
 | |
|       console.error(
 | |
|         `An error occured while trying to fixup "${searchString}"`,
 | |
|         ex
 | |
|       );
 | |
|     }
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   _afterTabSelectAndFocusChange() {
 | |
|     // We must have seen both events to proceed safely.
 | |
|     if (!this._gotFocusChange || !this._gotTabSelect) {
 | |
|       return;
 | |
|     }
 | |
|     this._gotFocusChange = this._gotTabSelect = false;
 | |
| 
 | |
|     this.formatValue();
 | |
|     this._resetSearchState();
 | |
| 
 | |
|     // Switching tabs doesn't always change urlbar focus, so we must try to
 | |
|     // reopen here too, not just on focus.
 | |
|     // We don't use the original TabSelect event because caching it causes
 | |
|     // leaks on MacOS.
 | |
|     if (this.view.autoOpen({ event: new CustomEvent("tabswitch") })) {
 | |
|       return;
 | |
|     }
 | |
|     // The input may retain focus when switching tabs in which case we
 | |
|     // need to close the view explicitly.
 | |
|     this.view.close();
 | |
|   }
 | |
| 
 | |
|   async _updateLayoutBreakoutDimensions() {
 | |
|     // When this method gets called a second time before the first call
 | |
|     // finishes, we need to disregard the first one.
 | |
|     let updateKey = {};
 | |
|     this._layoutBreakoutUpdateKey = updateKey;
 | |
| 
 | |
|     this.removeAttribute("breakout");
 | |
|     this.textbox.parentNode.removeAttribute("breakout");
 | |
| 
 | |
|     await this.window.promiseDocumentFlushed(() => {});
 | |
|     await new Promise(resolve => {
 | |
|       this.window.requestAnimationFrame(() => {
 | |
|         if (this._layoutBreakoutUpdateKey != updateKey) {
 | |
|           return;
 | |
|         }
 | |
| 
 | |
|         this.textbox.parentNode.style.setProperty(
 | |
|           "--urlbar-container-height",
 | |
|           px(getBoundsWithoutFlushing(this.textbox.parentNode).height)
 | |
|         );
 | |
|         this.textbox.style.setProperty(
 | |
|           "--urlbar-height",
 | |
|           px(getBoundsWithoutFlushing(this.textbox).height)
 | |
|         );
 | |
|         this.textbox.style.setProperty(
 | |
|           "--urlbar-toolbar-height",
 | |
|           px(getBoundsWithoutFlushing(this._toolbar).height)
 | |
|         );
 | |
| 
 | |
|         this.setAttribute("breakout", "true");
 | |
|         this.textbox.parentNode.setAttribute("breakout", "true");
 | |
| 
 | |
|         resolve();
 | |
|       });
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   _setValue(val, allowTrim) {
 | |
|     // Don't expose internal about:reader URLs to the user.
 | |
|     let originalUrl = lazy.ReaderMode.getOriginalUrlObjectForDisplay(val);
 | |
|     if (originalUrl) {
 | |
|       val = originalUrl.displaySpec;
 | |
|     }
 | |
|     this._untrimmedValue = val;
 | |
| 
 | |
|     if (allowTrim) {
 | |
|       val = this._trimValue(val);
 | |
|     }
 | |
| 
 | |
|     this.valueIsTyped = false;
 | |
|     this._resultForCurrentValue = null;
 | |
|     this.inputField.value = val;
 | |
|     this.formatValue();
 | |
|     this.removeAttribute("actiontype");
 | |
| 
 | |
|     // Dispatch ValueChange event for accessibility.
 | |
|     let event = this.document.createEvent("Events");
 | |
|     event.initEvent("ValueChange", true, true);
 | |
|     this.inputField.dispatchEvent(event);
 | |
| 
 | |
|     return val;
 | |
|   }
 | |
| 
 | |
|   _getValueFromResult(result, urlOverride = null) {
 | |
|     switch (result.type) {
 | |
|       case lazy.UrlbarUtils.RESULT_TYPE.KEYWORD:
 | |
|         return result.payload.input;
 | |
|       case lazy.UrlbarUtils.RESULT_TYPE.SEARCH: {
 | |
|         let value = "";
 | |
|         if (result.payload.keyword) {
 | |
|           value += result.payload.keyword + " ";
 | |
|         }
 | |
|         value += result.payload.suggestion || result.payload.query;
 | |
|         return value;
 | |
|       }
 | |
|       case lazy.UrlbarUtils.RESULT_TYPE.OMNIBOX:
 | |
|         return result.payload.content;
 | |
|       case lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC:
 | |
|         return result.payload.input || "";
 | |
|     }
 | |
| 
 | |
|     if (urlOverride === "") {
 | |
|       // Allow callers to clear the input.
 | |
|       return "";
 | |
|     }
 | |
| 
 | |
|     try {
 | |
|       let uri = Services.io.newURI(urlOverride || result.payload.url);
 | |
|       if (uri) {
 | |
|         return losslessDecodeURI(uri);
 | |
|       }
 | |
|     } catch (ex) {}
 | |
| 
 | |
|     return "";
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Resets some state so that searches from the user's previous interaction
 | |
|    * with the input don't interfere with searches from a new interaction.
 | |
|    */
 | |
|   _resetSearchState() {
 | |
|     this._lastSearchString = this.value;
 | |
|     this._autofillPlaceholder = null;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Autofills the autofill placeholder string if appropriate, and determines
 | |
|    * whether autofill should be allowed for the new search started by an input
 | |
|    * event.
 | |
|    *
 | |
|    * @param {string} value
 | |
|    *   The new search string.
 | |
|    * @returns {boolean}
 | |
|    *   Whether autofill should be allowed in the new search.
 | |
|    */
 | |
|   _maybeAutofillPlaceholder(value) {
 | |
|     // We allow autofill in local but not remote search modes.
 | |
|     let allowAutofill =
 | |
|       this.selectionEnd == value.length &&
 | |
|       !this.searchMode?.engineName &&
 | |
|       this.searchMode?.source != lazy.UrlbarUtils.RESULT_SOURCE.SEARCH;
 | |
| 
 | |
|     if (!allowAutofill) {
 | |
|       this.#clearAutofill();
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     // Determine whether we can autofill the placeholder.  The placeholder is a
 | |
|     // value that we autofill now, when the search starts and before we wait on
 | |
|     // its first result, in order to prevent a flicker in the input caused by
 | |
|     // the previous autofilled substring disappearing and reappearing when the
 | |
|     // first result arrives.  Of course we can only autofill the placeholder if
 | |
|     // it starts with the new search string, and we shouldn't autofill anything
 | |
|     // if the caret isn't at the end of the input.
 | |
|     let canAutofillPlaceholder = false;
 | |
|     if (this._autofillPlaceholder) {
 | |
|       if (this._autofillPlaceholder.type == "adaptive") {
 | |
|         canAutofillPlaceholder =
 | |
|           value.length >=
 | |
|             this._autofillPlaceholder.adaptiveHistoryInput.length &&
 | |
|           this._autofillPlaceholder.value
 | |
|             .toLocaleLowerCase()
 | |
|             .startsWith(value.toLocaleLowerCase());
 | |
|       } else {
 | |
|         canAutofillPlaceholder = lazy.UrlbarUtils.canAutofillURL(
 | |
|           this._autofillPlaceholder.value,
 | |
|           value
 | |
|         );
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (!canAutofillPlaceholder) {
 | |
|       this._autofillPlaceholder = null;
 | |
|     } else if (
 | |
|       this._autofillPlaceholder &&
 | |
|       this.selectionEnd == this.value.length &&
 | |
|       this._enableAutofillPlaceholder
 | |
|     ) {
 | |
|       let autofillValue =
 | |
|         value + this._autofillPlaceholder.value.substring(value.length);
 | |
|       this._autofillValue({
 | |
|         value: autofillValue,
 | |
|         selectionStart: value.length,
 | |
|         selectionEnd: autofillValue.length,
 | |
|         type: this._autofillPlaceholder.type,
 | |
|         adaptiveHistoryInput: this._autofillPlaceholder.adaptiveHistoryInput,
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     return true;
 | |
|   }
 | |
| 
 | |
|   _checkForRtlText(value) {
 | |
|     let directionality = this.window.windowUtils.getDirectionFromText(value);
 | |
|     if (directionality == this.window.windowUtils.DIRECTION_RTL) {
 | |
|       return true;
 | |
|     }
 | |
|     return false;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Invoked on overflow/underflow/scrollend events to update attributes
 | |
|    * related to the input text directionality. Overflow fade masks use these
 | |
|    * attributes to appear at the proper side of the urlbar.
 | |
|    */
 | |
|   updateTextOverflow() {
 | |
|     if (!this._overflowing) {
 | |
|       this.removeAttribute("textoverflow");
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let isRTL =
 | |
|       this.getAttribute("domaindir") === "rtl" &&
 | |
|       this._checkForRtlText(this.value);
 | |
| 
 | |
|     this.window.promiseDocumentFlushed(() => {
 | |
|       // Check overflow again to ensure it didn't change in the meanwhile.
 | |
|       let input = this.inputField;
 | |
|       if (input && this._overflowing) {
 | |
|         // Normally we would overflow at the final side of text direction,
 | |
|         // though RTL domains may cause us to overflow at the opposite side.
 | |
|         // This happens dynamically as a consequence of the input field contents
 | |
|         // and the call to _ensureFormattedHostVisible, this code only reports
 | |
|         // the final state of all that scrolling into an attribute, because
 | |
|         // there's no other way to capture this in css.
 | |
|         // Note it's also possible to scroll an unfocused input field using
 | |
|         // SHIFT + mousewheel on Windows, or with just the mousewheel / touchpad
 | |
|         // scroll (without modifiers) on Mac.
 | |
|         let side = "both";
 | |
|         if (isRTL) {
 | |
|           if (input.scrollLeft == 0) {
 | |
|             side = "left";
 | |
|           } else if (input.scrollLeft == input.scrollLeftMin) {
 | |
|             side = "right";
 | |
|           }
 | |
|         } else if (input.scrollLeft == 0) {
 | |
|           side = "right";
 | |
|         } else if (input.scrollLeft == input.scrollLeftMax) {
 | |
|           side = "left";
 | |
|         }
 | |
| 
 | |
|         this.window.requestAnimationFrame(() => {
 | |
|           // And check once again, since we might have stopped overflowing
 | |
|           // since the promiseDocumentFlushed callback fired.
 | |
|           if (this._overflowing) {
 | |
|             this.setAttribute("textoverflow", side);
 | |
|           }
 | |
|         });
 | |
|       }
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   _updateUrlTooltip() {
 | |
|     if (this.focused || !this._overflowing) {
 | |
|       this.inputField.removeAttribute("title");
 | |
|     } else {
 | |
|       this.inputField.setAttribute("title", this.untrimmedValue);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   _getSelectedValueForClipboard() {
 | |
|     let selection = this.editor.selection;
 | |
|     const flags =
 | |
|       Ci.nsIDocumentEncoder.OutputPreformatted |
 | |
|       Ci.nsIDocumentEncoder.OutputRaw;
 | |
|     let selectedVal = selection.toStringWithFormat("text/plain", flags, 0);
 | |
| 
 | |
|     // Handle multiple-range selection as a string for simplicity.
 | |
|     if (selection.rangeCount > 1) {
 | |
|       return selectedVal;
 | |
|     }
 | |
| 
 | |
|     // If the selection doesn't start at the beginning or doesn't span the
 | |
|     // full domain or the URL bar is modified or there is no text at all,
 | |
|     // nothing else to do here.
 | |
|     if (this.selectionStart > 0 || this.valueIsTyped || selectedVal == "") {
 | |
|       return selectedVal;
 | |
|     }
 | |
| 
 | |
|     // The selection doesn't span the full domain if it doesn't contain a slash and is
 | |
|     // followed by some character other than a slash.
 | |
|     if (!selectedVal.includes("/")) {
 | |
|       let remainder = this.value.replace(selectedVal, "");
 | |
|       if (remainder != "" && remainder[0] != "/") {
 | |
|         return selectedVal;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     let uri;
 | |
|     if (this.getAttribute("pageproxystate") == "valid") {
 | |
|       uri = this.window.gBrowser.currentURI;
 | |
|     } else {
 | |
|       // The value could be:
 | |
|       // 1. a trimmed url, set by selecting a result
 | |
|       // 2. a search string set by selecting a result
 | |
|       // 3. a url that was confirmed but didn't finish loading yet
 | |
|       // If it's an url the untrimmedValue should resolve to a valid URI,
 | |
|       // otherwise it's a search string that should be copied as-is.
 | |
| 
 | |
|       // If the copied text is that autofilled value, return the url including
 | |
|       // the protocol from its suggestion.
 | |
|       let result = this.view.getResultAtIndex(0);
 | |
|       if (result?.autofill?.value == selectedVal) {
 | |
|         return result.payload.url;
 | |
|       }
 | |
| 
 | |
|       try {
 | |
|         uri = Services.io.newURI(this._untrimmedValue);
 | |
|       } catch (ex) {
 | |
|         return selectedVal;
 | |
|       }
 | |
|     }
 | |
|     uri = this.makeURIReadable(uri);
 | |
|     let displaySpec = uri.displaySpec;
 | |
| 
 | |
|     // If the entire URL is selected, just use the actual loaded URI,
 | |
|     // unless we want a decoded URI, or it's a data: or javascript: URI,
 | |
|     // since those are hard to read when encoded.
 | |
|     if (
 | |
|       this.value == selectedVal &&
 | |
|       !uri.schemeIs("javascript") &&
 | |
|       !uri.schemeIs("data") &&
 | |
|       !lazy.UrlbarPrefs.get("decodeURLsOnCopy")
 | |
|     ) {
 | |
|       return displaySpec;
 | |
|     }
 | |
| 
 | |
|     // Just the beginning of the URL is selected, or we want a decoded
 | |
|     // url. First check for a trimmed value.
 | |
| 
 | |
|     if (
 | |
|       !selectedVal.startsWith(lazy.BrowserUIUtils.trimURLProtocol) &&
 | |
|       // Note _trimValue may also trim a trailing slash, thus we can't just do
 | |
|       // a straight string compare to tell if the protocol was trimmed.
 | |
|       !displaySpec.startsWith(this._trimValue(displaySpec))
 | |
|     ) {
 | |
|       selectedVal = lazy.BrowserUIUtils.trimURLProtocol + selectedVal;
 | |
|     }
 | |
| 
 | |
|     // If selection starts from the beginning and part or all of the URL
 | |
|     // is selected, we check for decoded characters and encode them.
 | |
|     // Unless decodeURLsOnCopy is set. Do not encode data: URIs.
 | |
|     if (!lazy.UrlbarPrefs.get("decodeURLsOnCopy") && !uri.schemeIs("data")) {
 | |
|       try {
 | |
|         new URL(selectedVal);
 | |
|         // Use encodeURI instead of URL.href because we don't want
 | |
|         // trailing slash.
 | |
|         selectedVal = encodeURI(selectedVal);
 | |
|       } catch (ex) {
 | |
|         // URL is invalid. Return original selected value.
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     return selectedVal;
 | |
|   }
 | |
| 
 | |
|   _toggleActionOverride(event) {
 | |
|     // Ignore repeated KeyboardEvents.
 | |
|     if (event.repeat) {
 | |
|       return;
 | |
|     }
 | |
|     if (
 | |
|       event.keyCode == KeyEvent.DOM_VK_SHIFT ||
 | |
|       event.keyCode == KeyEvent.DOM_VK_ALT ||
 | |
|       event.keyCode ==
 | |
|         (AppConstants.platform == "macosx"
 | |
|           ? KeyEvent.DOM_VK_META
 | |
|           : KeyEvent.DOM_VK_CONTROL)
 | |
|     ) {
 | |
|       if (event.type == "keydown") {
 | |
|         this._actionOverrideKeyCount++;
 | |
|         this.setAttribute("actionoverride", "true");
 | |
|         this.view.panel.setAttribute("actionoverride", "true");
 | |
|       } else if (
 | |
|         this._actionOverrideKeyCount &&
 | |
|         --this._actionOverrideKeyCount == 0
 | |
|       ) {
 | |
|         this._clearActionOverride();
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   _clearActionOverride() {
 | |
|     this._actionOverrideKeyCount = 0;
 | |
|     this.removeAttribute("actionoverride");
 | |
|     this.view.panel.removeAttribute("actionoverride");
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Get the url to load for the search query and records in telemetry that it
 | |
|    * is being loaded.
 | |
|    *
 | |
|    * @param {nsISearchEngine} engine
 | |
|    *   The engine to generate the query for.
 | |
|    * @param {Event} event
 | |
|    *   The event that triggered this query.
 | |
|    * @param {object} searchActionDetails
 | |
|    *   The details associated with this search query.
 | |
|    * @param {boolean} searchActionDetails.isSuggestion
 | |
|    *   True if this query was initiated from a suggestion from the search engine.
 | |
|    * @param {boolean} searchActionDetails.alias
 | |
|    *   True if this query was initiated via a search alias.
 | |
|    * @param {boolean} searchActionDetails.isFormHistory
 | |
|    *   True if this query was initiated from a form history result.
 | |
|    * @param {string} searchActionDetails.url
 | |
|    *   The url this query was triggered with.
 | |
|    */
 | |
|   _recordSearch(engine, event, searchActionDetails = {}) {
 | |
|     const isOneOff = this.view.oneOffSearchButtons.eventTargetIsAOneOff(event);
 | |
| 
 | |
|     lazy.BrowserSearchTelemetry.recordSearch(
 | |
|       this.window.gBrowser.selectedBrowser,
 | |
|       engine,
 | |
|       this.getSearchSource(event),
 | |
|       {
 | |
|         ...searchActionDetails,
 | |
|         isOneOff,
 | |
|         newtabSessionId: this._handoffSession,
 | |
|       }
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Shortens the given value, usually by removing http:// and trailing slashes.
 | |
|    *
 | |
|    * @param {string} val
 | |
|    *   The string to be trimmed if it appears to be URI
 | |
|    * @returns {string}
 | |
|    *   The trimmed string
 | |
|    */
 | |
|   _trimValue(val) {
 | |
|     let trimmedValue = lazy.UrlbarPrefs.get("trimURLs")
 | |
|       ? lazy.BrowserUIUtils.trimURL(val)
 | |
|       : val;
 | |
|     // Only trim value if the directionality doesn't change to RTL.
 | |
|     return this.window.windowUtils.getDirectionFromText(trimmedValue) ==
 | |
|       this.window.windowUtils.DIRECTION_RTL
 | |
|       ? val
 | |
|       : trimmedValue;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * If appropriate, this prefixes a search string with 'www.' and suffixes it
 | |
|    * with browser.fixup.alternate.suffix prior to navigating.
 | |
|    *
 | |
|    * @param {Event} event
 | |
|    *   The event that triggered this query.
 | |
|    * @param {string} value
 | |
|    *   The search string that should be canonized.
 | |
|    * @returns {string}
 | |
|    *   Returns the canonized URL if available and null otherwise.
 | |
|    */
 | |
|   _maybeCanonizeURL(event, value) {
 | |
|     // Only add the suffix when the URL bar value isn't already "URL-like",
 | |
|     // and only if we get a keyboard event, to match user expectations.
 | |
|     if (
 | |
|       !KeyboardEvent.isInstance(event) ||
 | |
|       event._disableCanonization ||
 | |
|       !event.ctrlKey ||
 | |
|       !lazy.UrlbarPrefs.get("ctrlCanonizesURLs") ||
 | |
|       !/^\s*[^.:\/\s]+(?:\/.*|\s*)$/i.test(value)
 | |
|     ) {
 | |
|       return null;
 | |
|     }
 | |
| 
 | |
|     let suffix = Services.prefs.getCharPref("browser.fixup.alternate.suffix");
 | |
|     if (!suffix.endsWith("/")) {
 | |
|       suffix += "/";
 | |
|     }
 | |
| 
 | |
|     // trim leading/trailing spaces (bug 233205)
 | |
|     value = value.trim();
 | |
| 
 | |
|     // Tack www. and suffix on.  If user has appended directories, insert
 | |
|     // suffix before them (bug 279035).  Be careful not to get two slashes.
 | |
|     let firstSlash = value.indexOf("/");
 | |
|     if (firstSlash >= 0) {
 | |
|       value =
 | |
|         value.substring(0, firstSlash) +
 | |
|         suffix +
 | |
|         value.substring(firstSlash + 1);
 | |
|     } else {
 | |
|       value = value + suffix;
 | |
|     }
 | |
| 
 | |
|     try {
 | |
|       const info = Services.uriFixup.getFixupURIInfo(
 | |
|         value,
 | |
|         Ci.nsIURIFixup.FIXUP_FLAG_FORCE_ALTERNATE_URI
 | |
|       );
 | |
|       value = info.fixedURI.spec;
 | |
|     } catch (ex) {
 | |
|       console.error(`An error occured while trying to fixup "${value}"`, ex);
 | |
|     }
 | |
| 
 | |
|     this.value = value;
 | |
|     return value;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Autofills a value into the input.  The value will be autofilled regardless
 | |
|    * of the input's current value.
 | |
|    *
 | |
|    * @param {object} options
 | |
|    *   The options object.
 | |
|    * @param {string} options.value
 | |
|    *   The value to autofill.
 | |
|    * @param {integer} options.selectionStart
 | |
|    *   The new selectionStart.
 | |
|    * @param {integer} options.selectionEnd
 | |
|    *   The new selectionEnd.
 | |
|    * @param {"origin" | "url" | "adaptive"} options.type
 | |
|    *   The autofill type, one of: "origin", "url", "adaptive"
 | |
|    * @param {string} options.adaptiveHistoryInput
 | |
|    *   If the autofill type is "adaptive", this is the matching `input` value
 | |
|    *   from adaptive history.
 | |
|    */
 | |
|   _autofillValue({
 | |
|     value,
 | |
|     selectionStart,
 | |
|     selectionEnd,
 | |
|     type,
 | |
|     adaptiveHistoryInput,
 | |
|   }) {
 | |
|     // The autofilled value may be a URL that includes a scheme at the
 | |
|     // beginning.  Do not allow it to be trimmed.
 | |
|     this._setValue(value, false);
 | |
|     this.inputField.setSelectionRange(selectionStart, selectionEnd);
 | |
|     this._autofillPlaceholder = {
 | |
|       value,
 | |
|       type,
 | |
|       adaptiveHistoryInput,
 | |
|       selectionStart,
 | |
|       selectionEnd,
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Loads the url in the appropriate place.
 | |
|    *
 | |
|    * @param {string} url
 | |
|    *   The URL to open.
 | |
|    * @param {Event} event
 | |
|    *   The event that triggered to load the url.
 | |
|    * @param {string} openUILinkWhere
 | |
|    *   Where we expect the result to be opened.
 | |
|    * @param {object} params
 | |
|    *   The parameters related to how and where the result will be opened.
 | |
|    *   Further supported paramters are listed in utilityOverlay.js#openUILinkIn.
 | |
|    * @param {object} params.triggeringPrincipal
 | |
|    *   The principal that the action was triggered from.
 | |
|    * @param {nsIInputStream} [params.postData]
 | |
|    *   The POST data associated with a search submission.
 | |
|    * @param {boolean} [params.allowInheritPrincipal]
 | |
|    *   Whether the principal can be inherited.
 | |
|    * @param {object} [resultDetails]
 | |
|    *   Details of the selected result, if any.
 | |
|    * @param {UrlbarUtils.RESULT_TYPE} [resultDetails.type]
 | |
|    *   Details of the result type, if any.
 | |
|    * @param {string} [resultDetails.searchTerm]
 | |
|    *   Search term of the result source, if any.
 | |
|    * @param {UrlbarUtils.RESULT_SOURCE} [resultDetails.source]
 | |
|    *   Details of the result source, if any.
 | |
|    * @param {object} browser [optional] the browser to use for the load.
 | |
|    */
 | |
|   _loadURL(
 | |
|     url,
 | |
|     event,
 | |
|     openUILinkWhere,
 | |
|     params,
 | |
|     resultDetails = null,
 | |
|     browser = this.window.gBrowser.selectedBrowser
 | |
|   ) {
 | |
|     // No point in setting these because we'll handleRevert() a few rows below.
 | |
|     if (openUILinkWhere == "current") {
 | |
|       this.value =
 | |
|         lazy.UrlbarPrefs.get("showSearchTermsFeatureGate") &&
 | |
|         lazy.UrlbarPrefs.get("showSearchTerms.enabled") &&
 | |
|         resultDetails?.searchTerm
 | |
|           ? resultDetails.searchTerm
 | |
|           : url;
 | |
|       browser.userTypedValue = this.value;
 | |
|     }
 | |
| 
 | |
|     // No point in setting this if we are loading in a new window.
 | |
|     if (
 | |
|       openUILinkWhere != "window" &&
 | |
|       this.window.gInitialPages.includes(url)
 | |
|     ) {
 | |
|       browser.initialPageLoadedFromUserAction = url;
 | |
|     }
 | |
| 
 | |
|     try {
 | |
|       lazy.UrlbarUtils.addToUrlbarHistory(url, this.window);
 | |
|     } catch (ex) {
 | |
|       // Things may go wrong when adding url to session history,
 | |
|       // but don't let that interfere with the loading of the url.
 | |
|       console.error(ex);
 | |
|     }
 | |
| 
 | |
|     // TODO: When bug 1498553 is resolved, we should be able to
 | |
|     // remove the !triggeringPrincipal condition here.
 | |
|     if (
 | |
|       !params.triggeringPrincipal ||
 | |
|       params.triggeringPrincipal.isSystemPrincipal
 | |
|     ) {
 | |
|       // Reset DOS mitigations for the basic auth prompt.
 | |
|       delete browser.authPromptAbuseCounter;
 | |
| 
 | |
|       // Reset temporary permissions on the current tab if the user reloads
 | |
|       // the tab via the urlbar.
 | |
|       if (
 | |
|         openUILinkWhere == "current" &&
 | |
|         browser.currentURI &&
 | |
|         url === browser.currentURI.spec
 | |
|       ) {
 | |
|         this.window.SitePermissions.clearTemporaryBlockPermissions(browser);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     params.allowThirdPartyFixup = true;
 | |
| 
 | |
|     if (openUILinkWhere == "current") {
 | |
|       params.targetBrowser = browser;
 | |
|       params.indicateErrorPageLoad = true;
 | |
|       params.allowPinnedTabHostChange = true;
 | |
|       params.allowPopups = url.startsWith("javascript:");
 | |
|     } else {
 | |
|       params.initiatingDoc = this.window.document;
 | |
|     }
 | |
| 
 | |
|     if (
 | |
|       this._keyDownEnterDeferred &&
 | |
|       event?.keyCode === KeyEvent.DOM_VK_RETURN &&
 | |
|       openUILinkWhere === "current"
 | |
|     ) {
 | |
|       // In this case, we move the focus to the browser that loads the content
 | |
|       // upon key up the enter key.
 | |
|       // To do it, send avoidBrowserFocus flag to openTrustedLinkIn() to avoid
 | |
|       // focusing on the browser in the function. And also, set loadedContent
 | |
|       // flag that whether the content is loaded in the current tab by this enter
 | |
|       // key. _keyDownEnterDeferred promise is processed at key up the enter,
 | |
|       // focus on the browser passed by _keyDownEnterDeferred.resolve().
 | |
|       params.avoidBrowserFocus = true;
 | |
|       this._keyDownEnterDeferred.loadedContent = true;
 | |
|       this._keyDownEnterDeferred.resolve(browser);
 | |
|     }
 | |
| 
 | |
|     // Ensure the window gets the `private` feature if the current window
 | |
|     // is private, unless the caller explicitly requested not to.
 | |
|     if (this.isPrivate && !("private" in params)) {
 | |
|       params.private = true;
 | |
|     }
 | |
| 
 | |
|     // Focus the content area before triggering loads, since if the load
 | |
|     // occurs in a new tab, we want focus to be restored to the content
 | |
|     // area when the current tab is re-selected.
 | |
|     if (!params.avoidBrowserFocus) {
 | |
|       browser.focus();
 | |
|       // Make sure the domain name stays visible for spoof protection and usability.
 | |
|       this.inputField.setSelectionRange(0, 0);
 | |
|     }
 | |
| 
 | |
|     if (openUILinkWhere != "current") {
 | |
|       this.handleRevert();
 | |
|     }
 | |
| 
 | |
|     // Notify about the start of navigation.
 | |
|     this._notifyStartNavigation(resultDetails);
 | |
| 
 | |
|     try {
 | |
|       this.window.openTrustedLinkIn(url, openUILinkWhere, params);
 | |
|     } catch (ex) {
 | |
|       // This load can throw an exception in certain cases, which means
 | |
|       // we'll want to replace the URL with the loaded URL:
 | |
|       if (ex.result != Cr.NS_ERROR_LOAD_SHOWED_ERRORPAGE) {
 | |
|         this.handleRevert();
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // If we show the focus border after closing the view, it would appear to
 | |
|     // flash since this._on_blur would remove it immediately after.
 | |
|     this.view.close({ showFocusBorder: false });
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Determines where a URL/page should be opened.
 | |
|    *
 | |
|    * @param {Event} event the event triggering the opening.
 | |
|    * @returns {"current" | "tabshifted" | "tab" | "save" | "window"}
 | |
|    */
 | |
|   _whereToOpen(event) {
 | |
|     let isKeyboardEvent = KeyboardEvent.isInstance(event);
 | |
|     let reuseEmpty = isKeyboardEvent;
 | |
|     let where = undefined;
 | |
|     if (
 | |
|       isKeyboardEvent &&
 | |
|       (event.altKey || event.getModifierState("AltGraph"))
 | |
|     ) {
 | |
|       // We support using 'alt' to open in a tab, because ctrl/shift
 | |
|       // might be used for canonizing URLs:
 | |
|       where = event.shiftKey ? "tabshifted" : "tab";
 | |
|     } else if (
 | |
|       isKeyboardEvent &&
 | |
|       event.ctrlKey &&
 | |
|       lazy.UrlbarPrefs.get("ctrlCanonizesURLs")
 | |
|     ) {
 | |
|       // If we're allowing canonization, and this is a key event with ctrl
 | |
|       // pressed, open in current tab to allow ctrl-enter to canonize URL.
 | |
|       where = "current";
 | |
|     } else {
 | |
|       where = this.window.whereToOpenLink(event, false, false);
 | |
|     }
 | |
|     if (lazy.UrlbarPrefs.get("openintab")) {
 | |
|       if (where == "current") {
 | |
|         where = "tab";
 | |
|       } else if (where == "tab") {
 | |
|         where = "current";
 | |
|       }
 | |
|       reuseEmpty = true;
 | |
|     }
 | |
|     if (
 | |
|       where == "tab" &&
 | |
|       reuseEmpty &&
 | |
|       this.window.gBrowser.selectedTab.isEmpty
 | |
|     ) {
 | |
|       where = "current";
 | |
|     }
 | |
|     return where;
 | |
|   }
 | |
| 
 | |
|   _initCopyCutController() {
 | |
|     this._copyCutController = new CopyCutController(this);
 | |
|     this.inputField.controllers.insertControllerAt(0, this._copyCutController);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Searches the context menu for the location of a specific command.
 | |
|    *
 | |
|    * @param {string} menuItemCommand
 | |
|    *    The command to search for.
 | |
|    * @returns {string}
 | |
|    *    Html element that matches the command or
 | |
|    *    the last element if we could not find the command.
 | |
|    */
 | |
|   #findMenuItemLocation(menuItemCommand) {
 | |
|     let inputBox = this.querySelector("moz-input-box");
 | |
|     let contextMenu = inputBox.menupopup;
 | |
|     let insertLocation = contextMenu.firstElementChild;
 | |
|     // find the location of the command
 | |
|     while (
 | |
|       insertLocation.nextElementSibling &&
 | |
|       insertLocation.getAttribute("cmd") != menuItemCommand
 | |
|     ) {
 | |
|       insertLocation = insertLocation.nextElementSibling;
 | |
|     }
 | |
| 
 | |
|     return insertLocation;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Strips known tracking query parameters/ link decorators.
 | |
|    *
 | |
|    * @returns {nsIURI|null}
 | |
|    *   The stripped URI or null
 | |
|    */
 | |
|   #stripURI() {
 | |
|     let copyString = this._getSelectedValueForClipboard();
 | |
|     if (!copyString) {
 | |
|       return null;
 | |
|     }
 | |
|     let strippedURI = null;
 | |
|     // throws if the selected string is not a valid URI
 | |
|     try {
 | |
|       let uri = Services.io.newURI(copyString);
 | |
|       strippedURI = lazy.QueryStringStripper.stripForCopyOrShare(uri);
 | |
|     } catch (e) {
 | |
|       console.debug(`stripURI: ${e.message}`);
 | |
|       return null;
 | |
|     }
 | |
|     if (strippedURI) {
 | |
|       return this.makeURIReadable(strippedURI);
 | |
|     }
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   // The strip-on-share feature will strip known tracking/decorational
 | |
|   // query params from the URI and copy the stripped version to the clipboard.
 | |
|   _initStripOnShare() {
 | |
|     let contextMenu = this.querySelector("moz-input-box").menupopup;
 | |
|     let insertLocation = this.#findMenuItemLocation("cmd_copy");
 | |
|     if (!insertLocation.getAttribute("cmd") == "cmd_copy") {
 | |
|       return;
 | |
|     }
 | |
|     // set up the menu item
 | |
|     let stripOnShare = this.document.createXULElement("menuitem");
 | |
|     this.document.l10n.setAttributes(
 | |
|       stripOnShare,
 | |
|       "text-action-strip-on-share"
 | |
|     );
 | |
|     stripOnShare.setAttribute("anonid", "strip-on-share");
 | |
|     stripOnShare.id = "strip-on-share";
 | |
| 
 | |
|     insertLocation.insertAdjacentElement("afterend", stripOnShare);
 | |
| 
 | |
|     // register listener that returns the stripped version of the url
 | |
|     stripOnShare.addEventListener("command", () => {
 | |
|       let strippedURI = this.#stripURI();
 | |
|       if (!strippedURI) {
 | |
|         // If there is nothing to strip the menu item should not have been visible.
 | |
|         // We might end up here if there was an unexpected URI change.
 | |
|         console.warn("StripOnShare: Unexpected null value.");
 | |
|         return;
 | |
|       }
 | |
|       lazy.ClipboardHelper.copyString(strippedURI.displaySpec);
 | |
|     });
 | |
| 
 | |
|     // register a listener that hides the menu item if there is nothing to copy or nothing to strip.
 | |
|     contextMenu.addEventListener("popupshowing", () => {
 | |
|       // feature is not enabled
 | |
|       if (!lazy.QUERY_STRIPPING_STRIP_ON_SHARE) {
 | |
|         stripOnShare.setAttribute("hidden", true);
 | |
|         return;
 | |
|       }
 | |
|       let controller =
 | |
|         this.document.commandDispatcher.getControllerForCommand("cmd_copy");
 | |
|       // url bar is empty
 | |
|       if (!controller.isCommandEnabled("cmd_copy")) {
 | |
|         stripOnShare.setAttribute("hidden", true);
 | |
|         return;
 | |
|       }
 | |
|       // nothing to strip/selection is not a valid url
 | |
|       if (!this.#stripURI()) {
 | |
|         stripOnShare.setAttribute("hidden", true);
 | |
|         return;
 | |
|       }
 | |
|       stripOnShare.setAttribute("hidden", false);
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   _initPasteAndGo() {
 | |
|     let inputBox = this.querySelector("moz-input-box");
 | |
|     let contextMenu = inputBox.menupopup;
 | |
|     let insertLocation = this.#findMenuItemLocation("cmd_paste");
 | |
|     if (!insertLocation) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let pasteAndGo = this.document.createXULElement("menuitem");
 | |
|     pasteAndGo.id = "paste-and-go";
 | |
|     let label = Services.strings
 | |
|       .createBundle("chrome://browser/locale/browser.properties")
 | |
|       .GetStringFromName("pasteAndGo.label");
 | |
|     pasteAndGo.setAttribute("label", label);
 | |
|     pasteAndGo.setAttribute("anonid", "paste-and-go");
 | |
|     pasteAndGo.addEventListener("command", () => {
 | |
|       this._suppressStartQuery = true;
 | |
| 
 | |
|       this.select();
 | |
|       this.window.goDoCommand("cmd_paste");
 | |
|       this.setResultForCurrentValue(null);
 | |
|       this.controller.clearLastQueryContextCache();
 | |
|       this.handleCommand();
 | |
| 
 | |
|       this._suppressStartQuery = false;
 | |
|     });
 | |
| 
 | |
|     contextMenu.addEventListener("popupshowing", () => {
 | |
|       // Close the results pane when the input field contextual menu is open,
 | |
|       // because paste and go doesn't want a result selection.
 | |
|       this.view.close();
 | |
| 
 | |
|       let controller =
 | |
|         this.document.commandDispatcher.getControllerForCommand("cmd_paste");
 | |
|       let enabled = controller.isCommandEnabled("cmd_paste");
 | |
|       if (enabled) {
 | |
|         pasteAndGo.removeAttribute("disabled");
 | |
|       } else {
 | |
|         pasteAndGo.setAttribute("disabled", "true");
 | |
|       }
 | |
|     });
 | |
| 
 | |
|     insertLocation.insertAdjacentElement("afterend", pasteAndGo);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * This notifies observers that the user has entered or selected something in
 | |
|    * the URL bar which will cause navigation.
 | |
|    *
 | |
|    * We use the observer service, so that we don't need to load extra facilities
 | |
|    * if they aren't being used, e.g. WebNavigation.
 | |
|    *
 | |
|    * @param {UrlbarResult} result
 | |
|    *   Details of the result that was selected, if any.
 | |
|    */
 | |
|   _notifyStartNavigation(result) {
 | |
|     Services.obs.notifyObservers({ result }, "urlbar-user-start-navigation");
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Returns a search mode object if a result should enter search mode when
 | |
|    * selected.
 | |
|    *
 | |
|    * @param {UrlbarResult} result
 | |
|    *   The result to check.
 | |
|    * @param {string} [entry]
 | |
|    *   If provided, this will be recorded as the entry point into search mode.
 | |
|    *   See setSearchMode() documentation for details.
 | |
|    * @returns {object} A search mode object. Null if search mode should not be
 | |
|    *   entered. See setSearchMode documentation for details.
 | |
|    */
 | |
|   _searchModeForResult(result, entry = null) {
 | |
|     // Search mode is determined by the result's keyword or engine.
 | |
|     if (!result.payload.keyword && !result.payload.engine) {
 | |
|       return null;
 | |
|     }
 | |
| 
 | |
|     let searchMode = lazy.UrlbarUtils.searchModeForToken(
 | |
|       result.payload.keyword
 | |
|     );
 | |
|     // If result.originalEngine is set, then the user is Alt+Tabbing
 | |
|     // through the one-offs, so the keyword doesn't match the engine.
 | |
|     if (
 | |
|       !searchMode &&
 | |
|       result.payload.engine &&
 | |
|       (!result.payload.originalEngine ||
 | |
|         result.payload.engine == result.payload.originalEngine)
 | |
|     ) {
 | |
|       searchMode = { engineName: result.payload.engine };
 | |
|     }
 | |
| 
 | |
|     if (searchMode) {
 | |
|       if (entry) {
 | |
|         searchMode.entry = entry;
 | |
|       } else {
 | |
|         switch (result.providerName) {
 | |
|           case "UrlbarProviderTopSites":
 | |
|             searchMode.entry = "topsites_urlbar";
 | |
|             break;
 | |
|           case "TabToSearch":
 | |
|             if (result.payload.dynamicType) {
 | |
|               searchMode.entry = "tabtosearch_onboard";
 | |
|             } else {
 | |
|               searchMode.entry = "tabtosearch";
 | |
|             }
 | |
|             break;
 | |
|           default:
 | |
|             searchMode.entry = "keywordoffer";
 | |
|             break;
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     return searchMode;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Updates the UI so that search mode is either entered or exited.
 | |
|    *
 | |
|    * @param {object} searchMode
 | |
|    *   See setSearchMode documentation.  If null, then search mode is exited.
 | |
|    */
 | |
|   _updateSearchModeUI(searchMode) {
 | |
|     let { engineName, source, isGeneralPurposeEngine } = searchMode || {};
 | |
| 
 | |
|     // As an optimization, bail if the given search mode is null but search mode
 | |
|     // is already inactive.  Otherwise browser_preferences_usage.js fails due to
 | |
|     // accessing the browser.urlbar.placeholderName pref (via the call to
 | |
|     // BrowserSearch.initPlaceHolder below) too many times.  That test does not
 | |
|     // enter search mode, but it triggers many calls to this method with a null
 | |
|     // search mode, via setURI.
 | |
|     if (!engineName && !source && !this.hasAttribute("searchmode")) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     this._searchModeIndicatorTitle.textContent = "";
 | |
|     this._searchModeLabel.textContent = "";
 | |
|     this._searchModeIndicatorTitle.removeAttribute("data-l10n-id");
 | |
|     this._searchModeLabel.removeAttribute("data-l10n-id");
 | |
|     this.removeAttribute("searchmodesource");
 | |
| 
 | |
|     if (!engineName && !source) {
 | |
|       try {
 | |
|         // This will throw before DOMContentLoaded in
 | |
|         // PrivateBrowsingUtils.privacyContextFromWindow because
 | |
|         // aWindow.docShell is null.
 | |
|         this.window.BrowserSearch.initPlaceHolder(true);
 | |
|       } catch (ex) {}
 | |
|       this.removeAttribute("searchmode");
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (engineName) {
 | |
|       // Set text content for the search mode indicator.
 | |
|       this._searchModeIndicatorTitle.textContent = engineName;
 | |
|       this._searchModeLabel.textContent = engineName;
 | |
|       this.document.l10n.setAttributes(
 | |
|         this.inputField,
 | |
|         isGeneralPurposeEngine
 | |
|           ? "urlbar-placeholder-search-mode-web-2"
 | |
|           : "urlbar-placeholder-search-mode-other-engine",
 | |
|         { name: engineName }
 | |
|       );
 | |
|     } else if (source) {
 | |
|       let sourceName = lazy.UrlbarUtils.getResultSourceName(source);
 | |
|       let l10nID = `urlbar-search-mode-${sourceName}`;
 | |
|       this.document.l10n.setAttributes(this._searchModeIndicatorTitle, l10nID);
 | |
|       this.document.l10n.setAttributes(this._searchModeLabel, l10nID);
 | |
|       this.document.l10n.setAttributes(
 | |
|         this.inputField,
 | |
|         `urlbar-placeholder-search-mode-other-${sourceName}`
 | |
|       );
 | |
|       this.setAttribute("searchmodesource", sourceName);
 | |
|     }
 | |
| 
 | |
|     this.toggleAttribute("searchmode", true);
 | |
|     // Clear autofill.
 | |
|     if (this._autofillPlaceholder && this.window.gBrowser.userTypedValue) {
 | |
|       this.value = this.window.gBrowser.userTypedValue;
 | |
|     }
 | |
|     // Search mode should only be active when pageproxystate is invalid.
 | |
|     if (this.getAttribute("pageproxystate") == "valid") {
 | |
|       this.value = "";
 | |
|       this.setPageProxyState("invalid", true);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Determines if we should select all the text in the Urlbar based on the
 | |
|    *  Urlbar state, and whether the selection is empty.
 | |
|    */
 | |
|   _maybeSelectAll() {
 | |
|     if (
 | |
|       !this._preventClickSelectsAll &&
 | |
|       this._compositionState != lazy.UrlbarUtils.COMPOSITION.COMPOSING &&
 | |
|       this.document.activeElement == this.inputField &&
 | |
|       this.inputField.selectionStart == this.inputField.selectionEnd
 | |
|     ) {
 | |
|       this.select();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // Event handlers below.
 | |
| 
 | |
|   _on_command(event) {
 | |
|     // Something is executing a command, likely causing a focus change. This
 | |
|     // should not be recorded as an abandonment. If the user is selecting a
 | |
|     // result menu item or entering search mode from a one-off, then they are
 | |
|     // in the same engagement and we should not discard.
 | |
|     if (
 | |
|       !event.target.classList.contains("urlbarView-result-menuitem") &&
 | |
|       (!event.target.classList.contains("searchbar-engine-one-off-item") ||
 | |
|         this.searchMode?.entry != "oneoff")
 | |
|     ) {
 | |
|       this.controller.engagementEvent.discard();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   _on_blur(event) {
 | |
|     // We cannot count every blur events after a missed engagement as abandoment
 | |
|     // because the user may have clicked on some view element that executes
 | |
|     // a command causing a focus change. For example opening preferences from
 | |
|     // the oneoff settings button.
 | |
|     // For now we detect that case by discarding the event on command, but we
 | |
|     // may want to figure out a more robust way to detect abandonment.
 | |
|     this.controller.engagementEvent.record(event, {
 | |
|       searchString: this._lastSearchString,
 | |
|       searchSource: this.getSearchSource(event),
 | |
|     });
 | |
| 
 | |
|     this.focusedViaMousedown = false;
 | |
|     this._handoffSession = undefined;
 | |
|     this._isHandoffSession = false;
 | |
|     this.removeAttribute("focused");
 | |
| 
 | |
|     if (this._revertOnBlurValue == this.value) {
 | |
|       this.handleRevert();
 | |
|     } else if (
 | |
|       this._autofillPlaceholder &&
 | |
|       this.window.gBrowser.userTypedValue
 | |
|     ) {
 | |
|       // If we were autofilling, remove the autofilled portion, by restoring
 | |
|       // the value to the last typed one.
 | |
|       this.value = this.window.gBrowser.userTypedValue;
 | |
|     } else if (this.value == this._focusUntrimmedValue) {
 | |
|       // If the value was untrimmed by _on_focus and didn't change, trim it.
 | |
|       this.value = this._focusUntrimmedValue;
 | |
|     } else {
 | |
|       // We're not updating the value, so just format it.
 | |
|       this.formatValue();
 | |
|     }
 | |
|     this._focusUntrimmedValue = null;
 | |
|     this._revertOnBlurValue = null;
 | |
| 
 | |
|     this._resetSearchState();
 | |
| 
 | |
|     // In certain cases, like holding an override key and confirming an entry,
 | |
|     // we don't key a keyup event for the override key, thus we make this
 | |
|     // additional cleanup on blur.
 | |
|     this._clearActionOverride();
 | |
| 
 | |
|     // The extension input sessions depends more on blur than on the fact we
 | |
|     // actually cancel a running query, so we do it here.
 | |
|     if (lazy.ExtensionSearchHandler.hasActiveInputSession()) {
 | |
|       lazy.ExtensionSearchHandler.handleInputCancelled();
 | |
|     }
 | |
| 
 | |
|     // Respect the autohide preference for easier inspecting/debugging via
 | |
|     // the browser toolbox.
 | |
|     if (!lazy.UrlbarPrefs.get("ui.popup.disable_autohide")) {
 | |
|       this.view.close();
 | |
|     }
 | |
| 
 | |
|     // If there were search terms shown in the URL bar and the user
 | |
|     // didn't end up modifying the userTypedValue while it was
 | |
|     // focused, change back to a valid pageproxystate.
 | |
|     if (
 | |
|       this.window.gBrowser.selectedBrowser.searchTerms &&
 | |
|       this.window.gBrowser.userTypedValue == null
 | |
|     ) {
 | |
|       this.setPageProxyState("valid", true);
 | |
|     }
 | |
| 
 | |
|     // We may have hidden popup notifications, show them again if necessary.
 | |
|     if (
 | |
|       this.getAttribute("pageproxystate") != "valid" &&
 | |
|       this.window.UpdatePopupNotificationsVisibility
 | |
|     ) {
 | |
|       this.window.UpdatePopupNotificationsVisibility();
 | |
|     }
 | |
| 
 | |
|     // If user move the focus to another component while pressing Enter key,
 | |
|     // then keyup at that component, as we can't get the event, clear the promise.
 | |
|     if (this._keyDownEnterDeferred) {
 | |
|       this._keyDownEnterDeferred.resolve();
 | |
|       this._keyDownEnterDeferred = null;
 | |
|     }
 | |
|     this._isKeyDownWithCtrl = false;
 | |
| 
 | |
|     Services.obs.notifyObservers(null, "urlbar-blur");
 | |
|   }
 | |
| 
 | |
|   _on_click(event) {
 | |
|     if (
 | |
|       event.target == this.inputField ||
 | |
|       event.target == this._inputContainer ||
 | |
|       event.target.id == SEARCH_BUTTON_ID
 | |
|     ) {
 | |
|       this._maybeSelectAll();
 | |
|     }
 | |
| 
 | |
|     if (event.target == this._searchModeIndicatorClose && event.button != 2) {
 | |
|       this.searchMode = null;
 | |
|       this.view.oneOffSearchButtons.selectedButton = null;
 | |
|       if (this.view.isOpen) {
 | |
|         this.startQuery({
 | |
|           event,
 | |
|         });
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   _on_contextmenu(event) {
 | |
|     this.addSearchEngineHelper.refreshContextMenu(event);
 | |
| 
 | |
|     // Context menu opened via keyboard shortcut.
 | |
|     if (!event.button) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     this._maybeSelectAll();
 | |
|   }
 | |
| 
 | |
|   _on_focus(event) {
 | |
|     if (!this._hideFocus) {
 | |
|       this.setAttribute("focused", "true");
 | |
|     }
 | |
| 
 | |
|     // When the search term matches the SERP, the URL bar is in a valid
 | |
|     // pageproxystate. In order to only show the search icon, switch to
 | |
|     // an invalid pageproxystate.
 | |
|     if (this.window.gBrowser.selectedBrowser.searchTerms) {
 | |
|       this.setPageProxyState("invalid", true);
 | |
|     }
 | |
| 
 | |
|     // If the value was trimmed, check whether we should untrim it.
 | |
|     // This is necessary when a protocol was typed, but the whole url has
 | |
|     // invalid parts, like the origin, then editing and confirming the trimmed
 | |
|     // value would execute a search instead of visiting the typed url.
 | |
|     if (this.value != this._untrimmedValue) {
 | |
|       let untrim = false;
 | |
|       let fixedURI = this._getURIFixupInfo(this.value)?.preferredURI;
 | |
|       if (fixedURI) {
 | |
|         try {
 | |
|           let expectedURI = Services.io.newURI(this._untrimmedValue);
 | |
|           if (
 | |
|             lazy.UrlbarPrefs.get("trimHttps") &&
 | |
|             this._untrimmedValue.startsWith("https://")
 | |
|           ) {
 | |
|             untrim =
 | |
|               fixedURI.displaySpec.replace("http://", "https://") !=
 | |
|               expectedURI.displaySpec; // FIXME bug 1847723: Figure out a way to do this without manually messing with the fixed up URI.
 | |
|           } else {
 | |
|             untrim = fixedURI.displaySpec != expectedURI.displaySpec;
 | |
|           }
 | |
|         } catch (ex) {
 | |
|           untrim = true;
 | |
|         }
 | |
|       }
 | |
|       if (untrim) {
 | |
|         this._focusUntrimmedValue = this._untrimmedValue;
 | |
|         this._setValue(this._focusUntrimmedValue, false);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (this.focusedViaMousedown) {
 | |
|       this.view.autoOpen({ event });
 | |
|     } else if (this.inputField.hasAttribute("refocused-by-panel")) {
 | |
|       this._maybeSelectAll();
 | |
|     }
 | |
| 
 | |
|     this._updateUrlTooltip();
 | |
|     this.formatValue();
 | |
| 
 | |
|     // Hide popup notifications, to reduce visual noise.
 | |
|     if (
 | |
|       this.getAttribute("pageproxystate") != "valid" &&
 | |
|       this.window.UpdatePopupNotificationsVisibility
 | |
|     ) {
 | |
|       this.window.UpdatePopupNotificationsVisibility();
 | |
|     }
 | |
| 
 | |
|     Services.obs.notifyObservers(null, "urlbar-focus");
 | |
|   }
 | |
| 
 | |
|   _on_mouseover(event) {
 | |
|     this._updateUrlTooltip();
 | |
|   }
 | |
| 
 | |
|   _on_draggableregionleftmousedown(event) {
 | |
|     if (!lazy.UrlbarPrefs.get("ui.popup.disable_autohide")) {
 | |
|       this.view.close();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   _on_mousedown(event) {
 | |
|     switch (event.currentTarget) {
 | |
|       case this.textbox:
 | |
|         this._mousedownOnUrlbarDescendant = true;
 | |
| 
 | |
|         if (
 | |
|           event.target != this.inputField &&
 | |
|           event.target != this._inputContainer &&
 | |
|           event.target.id != SEARCH_BUTTON_ID
 | |
|         ) {
 | |
|           break;
 | |
|         }
 | |
| 
 | |
|         this.focusedViaMousedown = !this.focused;
 | |
|         this._preventClickSelectsAll = this.focused;
 | |
| 
 | |
|         // Keep the focus status, since the attribute may be changed
 | |
|         // upon calling this.focus().
 | |
|         const hasFocus = this.hasAttribute("focused");
 | |
|         if (event.target != this.inputField) {
 | |
|           this.focus();
 | |
|         }
 | |
| 
 | |
|         // The rest of this case only cares about left clicks.
 | |
|         if (event.button != 0) {
 | |
|           break;
 | |
|         }
 | |
| 
 | |
|         // Clear any previous selection unless we are focused, to ensure it
 | |
|         // doesn't affect drag selection.
 | |
|         if (this.focusedViaMousedown) {
 | |
|           this.inputField.setSelectionRange(0, 0);
 | |
|         }
 | |
| 
 | |
|         if (event.target.id == SEARCH_BUTTON_ID) {
 | |
|           this._preventClickSelectsAll = true;
 | |
|           this.search(lazy.UrlbarTokenizer.RESTRICT.SEARCH);
 | |
|         } else {
 | |
|           // Do not suppress the focus border if we are already focused. If we
 | |
|           // did, we'd hide the focus border briefly then show it again if the
 | |
|           // user has Top Sites disabled, creating a flashing effect.
 | |
|           this.view.autoOpen({
 | |
|             event,
 | |
|             suppressFocusBorder: !hasFocus,
 | |
|           });
 | |
|         }
 | |
|         break;
 | |
|       case this.window:
 | |
|         if (this._mousedownOnUrlbarDescendant) {
 | |
|           this._mousedownOnUrlbarDescendant = false;
 | |
|           break;
 | |
|         }
 | |
|         // Don't close the view when clicking on a tab; we may want to keep the
 | |
|         // view open on tab switch, and the TabSelect event arrived earlier.
 | |
|         if (event.target.closest("tab")) {
 | |
|           break;
 | |
|         }
 | |
| 
 | |
|         // Close the view when clicking on toolbars and other UI pieces that
 | |
|         // might not automatically remove focus from the input.
 | |
|         // Respect the autohide preference for easier inspecting/debugging via
 | |
|         // the browser toolbox.
 | |
|         if (!lazy.UrlbarPrefs.get("ui.popup.disable_autohide")) {
 | |
|           if (this.view.isOpen && !this.hasAttribute("focused")) {
 | |
|             // In this case, as blur event never happen from the inputField, we
 | |
|             // record abandonment event explicitly.
 | |
|             let blurEvent = new FocusEvent("blur", {
 | |
|               relatedTarget: this.inputField,
 | |
|             });
 | |
|             this.controller.engagementEvent.record(blurEvent, {
 | |
|               searchString: this._lastSearchString,
 | |
|               searchSource: this.getSearchSource(blurEvent),
 | |
|             });
 | |
|           }
 | |
| 
 | |
|           this.view.close();
 | |
|         }
 | |
|         break;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   _on_input(event) {
 | |
|     if (
 | |
|       this._autofillPlaceholder &&
 | |
|       this.value === this.window.gBrowser.userTypedValue &&
 | |
|       (event.inputType === "deleteContentBackward" ||
 | |
|         event.inputType === "deleteContentForward")
 | |
|     ) {
 | |
|       // Take a telemetry if user deleted whole autofilled value.
 | |
|       Services.telemetry.scalarAdd("urlbar.autofill_deletion", 1);
 | |
|     }
 | |
| 
 | |
|     let value = this.value;
 | |
|     this.valueIsTyped = true;
 | |
|     this._untrimmedValue = value;
 | |
|     this._resultForCurrentValue = null;
 | |
| 
 | |
|     this.window.gBrowser.userTypedValue = value;
 | |
|     // Unset userSelectionBehavior because the user is modifying the search
 | |
|     // string, thus there's no valid selection. This is also used by the view
 | |
|     // to set "aria-activedescendant", thus it should never get stale.
 | |
|     this.controller.userSelectionBehavior = "none";
 | |
| 
 | |
|     let compositionState = this._compositionState;
 | |
|     let compositionClosedPopup = this._compositionClosedPopup;
 | |
| 
 | |
|     // Clear composition values if we're no more composing.
 | |
|     if (this._compositionState != lazy.UrlbarUtils.COMPOSITION.COMPOSING) {
 | |
|       this._compositionState = lazy.UrlbarUtils.COMPOSITION.NONE;
 | |
|       this._compositionClosedPopup = false;
 | |
|     }
 | |
| 
 | |
|     this.toggleAttribute("usertyping", value);
 | |
|     this.removeAttribute("actiontype");
 | |
| 
 | |
|     if (
 | |
|       this.getAttribute("pageproxystate") == "valid" &&
 | |
|       this.value != this._lastValidURLStr
 | |
|     ) {
 | |
|       this.setPageProxyState("invalid", true);
 | |
|     }
 | |
| 
 | |
|     if (!this.view.isOpen) {
 | |
|       this.view.clear();
 | |
|     } else if (!value && !lazy.UrlbarPrefs.get("suggest.topsites")) {
 | |
|       this.view.clear();
 | |
|       if (!this.searchMode || !this.view.oneOffSearchButtons.hasView) {
 | |
|         this.view.close();
 | |
|         return;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     this.view.removeAccessibleFocus();
 | |
| 
 | |
|     // During composition with an IME, the following events happen in order:
 | |
|     // 1. a compositionstart event
 | |
|     // 2. some input events
 | |
|     // 3. a compositionend event
 | |
|     // 4. an input event
 | |
| 
 | |
|     // We should do nothing during composition or if composition was canceled
 | |
|     // and we didn't close the popup on composition start.
 | |
|     if (
 | |
|       !lazy.UrlbarPrefs.get("keepPanelOpenDuringImeComposition") &&
 | |
|       (compositionState == lazy.UrlbarUtils.COMPOSITION.COMPOSING ||
 | |
|         (compositionState == lazy.UrlbarUtils.COMPOSITION.CANCELED &&
 | |
|           !compositionClosedPopup))
 | |
|     ) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // Autofill only when text is inserted (i.e., event.data is not empty) and
 | |
|     // it's not due to pasting.
 | |
|     const allowAutofill =
 | |
|       (!lazy.UrlbarPrefs.get("keepPanelOpenDuringImeComposition") ||
 | |
|         compositionState !== lazy.UrlbarUtils.COMPOSITION.COMPOSING) &&
 | |
|       !!event.data &&
 | |
|       !lazy.UrlbarUtils.isPasteEvent(event) &&
 | |
|       this._maybeAutofillPlaceholder(value);
 | |
| 
 | |
|     this.startQuery({
 | |
|       searchString: value,
 | |
|       allowAutofill,
 | |
|       resetSearchState: false,
 | |
|       event,
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   _on_selectionchange(event) {
 | |
|     // Confirm placeholder as user text if it gets explicitly deselected. This
 | |
|     // happens when the user wants to modify the autofilled text by either
 | |
|     // clicking on it, or pressing HOME, END, RIGHT, …
 | |
|     if (
 | |
|       this._autofillPlaceholder &&
 | |
|       this._autofillPlaceholder.value == this.value &&
 | |
|       (this._autofillPlaceholder.selectionStart != this.selectionStart ||
 | |
|         this._autofillPlaceholder.selectionEnd != this.selectionEnd)
 | |
|     ) {
 | |
|       this._autofillPlaceholder = null;
 | |
|       this.window.gBrowser.userTypedValue = this.value;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   _on_select(event) {
 | |
|     // On certain user input, AutoCopyListener::OnSelectionChange() updates
 | |
|     // the primary selection with user-selected text (when supported).
 | |
|     // Selection::NotifySelectionListeners() then dispatches a "select" event
 | |
|     // under similar conditions via TextInputListener::OnSelectionChange().
 | |
|     // This event is received here in order to replace the primary selection
 | |
|     // from the editor with text having the adjustments of
 | |
|     // _getSelectedValueForClipboard(), such as adding the scheme for the url.
 | |
|     //
 | |
|     // Other "select" events are also received, however, and must be excluded.
 | |
|     if (
 | |
|       // _suppressPrimaryAdjustment is set during select().  Don't update
 | |
|       // the primary selection because that is not the intent of user input,
 | |
|       // which may be new tab or urlbar focus.
 | |
|       this._suppressPrimaryAdjustment ||
 | |
|       // The check on isHandlingUserInput filters out async "select" events
 | |
|       // from setSelectionRange(), which occur when autofill text is selected.
 | |
|       !this.window.windowUtils.isHandlingUserInput ||
 | |
|       !Services.clipboard.isClipboardTypeSupported(
 | |
|         Services.clipboard.kSelectionClipboard
 | |
|       )
 | |
|     ) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let val = this._getSelectedValueForClipboard();
 | |
|     if (!val) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     lazy.ClipboardHelper.copyStringToClipboard(
 | |
|       val,
 | |
|       Services.clipboard.kSelectionClipboard
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   _on_overflow(event) {
 | |
|     const targetIsPlaceholder =
 | |
|       event.originalTarget.implementedPseudoElement == "::placeholder";
 | |
|     // We only care about the non-placeholder text.
 | |
|     // This shouldn't be needed, see bug 1487036.
 | |
|     if (targetIsPlaceholder) {
 | |
|       return;
 | |
|     }
 | |
|     this._overflowing = true;
 | |
|     this.updateTextOverflow();
 | |
|   }
 | |
| 
 | |
|   _on_underflow(event) {
 | |
|     const targetIsPlaceholder =
 | |
|       event.originalTarget.implementedPseudoElement == "::placeholder";
 | |
|     // We only care about the non-placeholder text.
 | |
|     // This shouldn't be needed, see bug 1487036.
 | |
|     if (targetIsPlaceholder) {
 | |
|       return;
 | |
|     }
 | |
|     this._overflowing = false;
 | |
| 
 | |
|     this.updateTextOverflow();
 | |
| 
 | |
|     this._updateUrlTooltip();
 | |
|   }
 | |
| 
 | |
|   _on_paste(event) {
 | |
|     let originalPasteData = event.clipboardData.getData("text/plain");
 | |
|     if (!originalPasteData) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let oldValue = this.value;
 | |
|     let oldStart = oldValue.substring(0, this.selectionStart);
 | |
|     // If there is already non-whitespace content in the URL bar
 | |
|     // preceding the pasted content, it's not necessary to check
 | |
|     // protocols used by the pasted content:
 | |
|     if (oldStart.trim()) {
 | |
|       return;
 | |
|     }
 | |
|     let oldEnd = oldValue.substring(this.selectionEnd);
 | |
| 
 | |
|     const pasteData = this.sanitizeTextFromClipboard(originalPasteData);
 | |
| 
 | |
|     if (originalPasteData != pasteData) {
 | |
|       // Unfortunately we're not allowed to set the bits being pasted
 | |
|       // so cancel this event:
 | |
|       event.preventDefault();
 | |
|       event.stopImmediatePropagation();
 | |
| 
 | |
|       const value = oldStart + pasteData + oldEnd;
 | |
|       this._setValue(value);
 | |
|       this.window.gBrowser.userTypedValue = value;
 | |
| 
 | |
|       this.toggleAttribute("usertyping", this._untrimmedValue);
 | |
| 
 | |
|       // Fix up cursor/selection:
 | |
|       let newCursorPos = oldStart.length + pasteData.length;
 | |
|       this.inputField.setSelectionRange(newCursorPos, newCursorPos);
 | |
| 
 | |
|       this.startQuery({
 | |
|         searchString: this.value,
 | |
|         allowAutofill: false,
 | |
|         resetSearchState: false,
 | |
|         event,
 | |
|       });
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Sanitize and process data retrieved from the clipboard
 | |
|    *
 | |
|    * @param {string} clipboardData
 | |
|    *   The original data retrieved from the clipboard.
 | |
|    * @returns {string}
 | |
|    *   The sanitized paste data, ready to use.
 | |
|    */
 | |
|   sanitizeTextFromClipboard(clipboardData) {
 | |
|     let fixedURI, keywordAsSent;
 | |
|     try {
 | |
|       ({ fixedURI, keywordAsSent } = Services.uriFixup.getFixupURIInfo(
 | |
|         clipboardData,
 | |
|         Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS |
 | |
|           Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP
 | |
|       ));
 | |
|     } catch (e) {}
 | |
| 
 | |
|     let pasteData;
 | |
|     if (keywordAsSent) {
 | |
|       // For performance reasons, we don't want to beautify a long string.
 | |
|       if (clipboardData.length < 500) {
 | |
|         // For only keywords, replace any white spaces including line break
 | |
|         // with white space.
 | |
|         pasteData = clipboardData.replace(/\s/g, " ");
 | |
|       } else {
 | |
|         pasteData = clipboardData;
 | |
|       }
 | |
|     } else if (
 | |
|       fixedURI?.scheme == "data" &&
 | |
|       !fixedURI.spec.match(/^data:.+;base64,/)
 | |
|     ) {
 | |
|       // For data url without base64, replace line break with white space.
 | |
|       pasteData = clipboardData.replace(/[\r\n]/g, " ");
 | |
|     } else {
 | |
|       // For normal url or data url having basic64, or if fixup failed, just
 | |
|       // remove line breaks.
 | |
|       pasteData = clipboardData.replace(/[\r\n]/g, "");
 | |
|     }
 | |
| 
 | |
|     return lazy.UrlbarUtils.stripUnsafeProtocolOnPaste(pasteData);
 | |
|   }
 | |
| 
 | |
|   _on_scrollend(event) {
 | |
|     this.updateTextOverflow();
 | |
|   }
 | |
| 
 | |
|   _on_TabSelect(event) {
 | |
|     this._gotTabSelect = true;
 | |
|     this._afterTabSelectAndFocusChange();
 | |
|   }
 | |
| 
 | |
|   _on_beforeinput(event) {
 | |
|     if (event.data && this._keyDownEnterDeferred) {
 | |
|       // Ignore char key input while processing enter key.
 | |
|       event.preventDefault();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   _on_keydown(event) {
 | |
|     if (event.keyCode === KeyEvent.DOM_VK_RETURN) {
 | |
|       if (this._keyDownEnterDeferred) {
 | |
|         this._keyDownEnterDeferred.reject();
 | |
|       }
 | |
|       this._keyDownEnterDeferred = lazy.PromiseUtils.defer();
 | |
|       event._disableCanonization = this._isKeyDownWithCtrl;
 | |
|     } else if (event.keyCode !== KeyEvent.DOM_VK_CONTROL && event.ctrlKey) {
 | |
|       this._isKeyDownWithCtrl = true;
 | |
|     }
 | |
| 
 | |
|     // Due to event deferring, it's possible preventDefault() won't be invoked
 | |
|     // soon enough to actually prevent some of the default behaviors, thus we
 | |
|     // have to handle the event "twice". This first immediate call passes false
 | |
|     // as second argument so that handleKeyNavigation will only simulate the
 | |
|     // event handling, without actually executing actions.
 | |
|     // TODO (Bug 1541806): improve this handling, maybe by delaying actions
 | |
|     // instead of events.
 | |
|     if (this.eventBufferer.shouldDeferEvent(event)) {
 | |
|       this.controller.handleKeyNavigation(event, false);
 | |
|     }
 | |
|     this._toggleActionOverride(event);
 | |
|     this.eventBufferer.maybeDeferEvent(event, () => {
 | |
|       this.controller.handleKeyNavigation(event);
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   async _on_keyup(event) {
 | |
|     if (event.keyCode === KeyEvent.DOM_VK_CONTROL) {
 | |
|       this._isKeyDownWithCtrl = false;
 | |
|     }
 | |
| 
 | |
|     this._toggleActionOverride(event);
 | |
| 
 | |
|     // Pressing Enter key while pressing Meta key, and next, even when releasing
 | |
|     // Enter key before releasing Meta key, the keyup event is not fired.
 | |
|     // Therefore, if Enter keydown is detecting, continue the post processing
 | |
|     // for Enter key when any keyup event is detected.
 | |
|     if (this._keyDownEnterDeferred) {
 | |
|       if (this._keyDownEnterDeferred.loadedContent) {
 | |
|         try {
 | |
|           const loadingBrowser = await this._keyDownEnterDeferred.promise;
 | |
|           // Ensure the selected browser didn't change in the meanwhile.
 | |
|           if (this.window.gBrowser.selectedBrowser === loadingBrowser) {
 | |
|             loadingBrowser.focus();
 | |
|             // Make sure the domain name stays visible for spoof protection and usability.
 | |
|             this.inputField.setSelectionRange(0, 0);
 | |
|           }
 | |
|         } catch (ex) {
 | |
|           // Not all the Enter actions in the urlbar will cause a navigation, then it
 | |
|           // is normal for this to be rejected.
 | |
|           // If _keyDownEnterDeferred was rejected on keydown, we don't nullify it here
 | |
|           // to ensure not overwriting the new value created by keydown.
 | |
|         }
 | |
|       } else {
 | |
|         // Discard the _keyDownEnterDeferred promise to receive any key inputs immediately.
 | |
|         this._keyDownEnterDeferred.resolve();
 | |
|       }
 | |
| 
 | |
|       this._keyDownEnterDeferred = null;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   _on_compositionstart(event) {
 | |
|     if (this._compositionState == lazy.UrlbarUtils.COMPOSITION.COMPOSING) {
 | |
|       throw new Error("Trying to start a nested composition?");
 | |
|     }
 | |
|     this._compositionState = lazy.UrlbarUtils.COMPOSITION.COMPOSING;
 | |
| 
 | |
|     if (lazy.UrlbarPrefs.get("keepPanelOpenDuringImeComposition")) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // Close the view. This will also stop searching.
 | |
|     if (this.view.isOpen) {
 | |
|       // We're closing the view, but we want to retain search mode if the
 | |
|       // selected result was previewing it.
 | |
|       if (this.searchMode) {
 | |
|         // If we entered search mode with an empty string, clear userTypedValue,
 | |
|         // otherwise confirmSearchMode may try to set it as value.
 | |
|         // This can happen for example if we entered search mode typing a
 | |
|         // a partial engine domain and selecting a tab-to-search result.
 | |
|         if (!this.value) {
 | |
|           this.window.gBrowser.userTypedValue = null;
 | |
|         }
 | |
|         this.confirmSearchMode();
 | |
|       }
 | |
|       this._compositionClosedPopup = true;
 | |
|       this.view.close();
 | |
|     } else {
 | |
|       this._compositionClosedPopup = false;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   _on_compositionend(event) {
 | |
|     if (this._compositionState != lazy.UrlbarUtils.COMPOSITION.COMPOSING) {
 | |
|       throw new Error("Trying to stop a non existing composition?");
 | |
|     }
 | |
| 
 | |
|     if (!lazy.UrlbarPrefs.get("keepPanelOpenDuringImeComposition")) {
 | |
|       // Clear the selection and the cached result, since they refer to the
 | |
|       // state before this composition. A new input even will be generated
 | |
|       // after this.
 | |
|       this.view.clearSelection();
 | |
|       this._resultForCurrentValue = null;
 | |
|     }
 | |
| 
 | |
|     // We can't yet retrieve the committed value from the editor, since it isn't
 | |
|     // completely committed yet. We'll handle it at the next input event.
 | |
|     this._compositionState = event.data
 | |
|       ? lazy.UrlbarUtils.COMPOSITION.COMMIT
 | |
|       : lazy.UrlbarUtils.COMPOSITION.CANCELED;
 | |
|   }
 | |
| 
 | |
|   _on_dragstart(event) {
 | |
|     // Drag only if the gesture starts from the input field.
 | |
|     let nodePosition = this.inputField.compareDocumentPosition(
 | |
|       event.originalTarget
 | |
|     );
 | |
|     if (
 | |
|       event.target != this.inputField &&
 | |
|       !(nodePosition & Node.DOCUMENT_POSITION_CONTAINED_BY)
 | |
|     ) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // Don't cover potential drop targets on the toolbars or in content.
 | |
|     this.view.close();
 | |
| 
 | |
|     // Only customize the drag data if the entire value is selected and it's a
 | |
|     // loaded URI. Use default behavior otherwise.
 | |
|     if (
 | |
|       this.selectionStart != 0 ||
 | |
|       this.selectionEnd != this.inputField.textLength ||
 | |
|       this.getAttribute("pageproxystate") != "valid"
 | |
|     ) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let uri = this.makeURIReadable(this.window.gBrowser.currentURI);
 | |
|     let href = uri.displaySpec;
 | |
|     let title = this.window.gBrowser.contentTitle || href;
 | |
| 
 | |
|     event.dataTransfer.setData("text/x-moz-url", `${href}\n${title}`);
 | |
|     event.dataTransfer.setData("text/plain", href);
 | |
|     event.dataTransfer.setData("text/html", `<a href="${href}">${title}</a>`);
 | |
|     event.dataTransfer.effectAllowed = "copyLink";
 | |
|     event.stopPropagation();
 | |
|   }
 | |
| 
 | |
|   _on_dragover(event) {
 | |
|     if (!getDroppableData(event)) {
 | |
|       event.dataTransfer.dropEffect = "none";
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   _on_drop(event) {
 | |
|     let droppedItem = getDroppableData(event);
 | |
|     let droppedURL = URL.isInstance(droppedItem)
 | |
|       ? droppedItem.href
 | |
|       : droppedItem;
 | |
|     if (droppedURL && droppedURL !== this.window.gBrowser.currentURI.spec) {
 | |
|       let principal = Services.droppedLinkHandler.getTriggeringPrincipal(event);
 | |
|       this.value = droppedURL;
 | |
|       this.setPageProxyState("invalid");
 | |
|       this.focus();
 | |
|       // To simplify tracking of events, register an initial event for event
 | |
|       // telemetry, to replace the missing input event.
 | |
|       this.controller.engagementEvent.start(event);
 | |
|       this.controller.clearLastQueryContextCache();
 | |
|       this.handleNavigation({ triggeringPrincipal: principal });
 | |
|       // For safety reasons, in the drop case we don't want to immediately show
 | |
|       // the the dropped value, instead we want to keep showing the current page
 | |
|       // url until an onLocationChange happens.
 | |
|       // See the handling in `setURI` for further details.
 | |
|       this.window.gBrowser.userTypedValue = null;
 | |
|       this.setURI(null, true);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   _on_customizationstarting() {
 | |
|     this.blur();
 | |
| 
 | |
|     this.inputField.controllers.removeController(this._copyCutController);
 | |
|     delete this._copyCutController;
 | |
|   }
 | |
| 
 | |
|   _on_aftercustomization() {
 | |
|     this._initCopyCutController();
 | |
|     this._initPasteAndGo();
 | |
|     this._initStripOnShare();
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Tries to extract droppable data from a DND event.
 | |
|  *
 | |
|  * @param {Event} event The DND event to examine.
 | |
|  * @returns {URL|string|null}
 | |
|  *          null if there's a security reason for which we should do nothing.
 | |
|  *          A URL object if it's a value we can load.
 | |
|  *          A string value otherwise.
 | |
|  */
 | |
| function getDroppableData(event) {
 | |
|   let links;
 | |
|   try {
 | |
|     links = Services.droppedLinkHandler.dropLinks(event);
 | |
|   } catch (ex) {
 | |
|     // This is either an unexpected failure or a security exception; in either
 | |
|     // case we should always return null.
 | |
|     return null;
 | |
|   }
 | |
|   // The URL bar automatically handles inputs with newline characters,
 | |
|   // so we can get away with treating text/x-moz-url flavours as text/plain.
 | |
|   if (links.length && links[0].url) {
 | |
|     event.preventDefault();
 | |
|     let href = links[0].url;
 | |
|     if (lazy.UrlbarUtils.stripUnsafeProtocolOnPaste(href) != href) {
 | |
|       // We may have stripped an unsafe protocol like javascript: and if so
 | |
|       // there's no point in handling a partial drop.
 | |
|       event.stopImmediatePropagation();
 | |
|       return null;
 | |
|     }
 | |
| 
 | |
|     try {
 | |
|       // If this throws, checkLoadURStrWithPrincipal would also throw,
 | |
|       // as that's what it does with things that don't pass the IO
 | |
|       // service's newURI constructor without fixup. It's conceivable we
 | |
|       // may want to relax this check in the future (so e.g. www.foo.com
 | |
|       // gets fixed up), but not right now.
 | |
|       let url = new URL(href);
 | |
|       // If we succeed, try to pass security checks. If this works, return the
 | |
|       // URL object. If the *security checks* fail, return null.
 | |
|       try {
 | |
|         let principal =
 | |
|           Services.droppedLinkHandler.getTriggeringPrincipal(event);
 | |
|         Services.scriptSecurityManager.checkLoadURIStrWithPrincipal(
 | |
|           principal,
 | |
|           url.href,
 | |
|           Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL
 | |
|         );
 | |
|         return url;
 | |
|       } catch (ex) {
 | |
|         return null;
 | |
|       }
 | |
|     } catch (ex) {
 | |
|       // We couldn't make a URL out of this. Continue on, and return text below.
 | |
|     }
 | |
|   }
 | |
|   // Handle as text.
 | |
|   return event.dataTransfer.getData("text/plain");
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Decodes the given URI for displaying it in the address bar without losing
 | |
|  * information, such that hitting Enter again will load the same URI.
 | |
|  *
 | |
|  * @param {nsIURI} aURI
 | |
|  *   The URI to decode
 | |
|  * @returns {string}
 | |
|  *   The decoded URI
 | |
|  */
 | |
| function losslessDecodeURI(aURI) {
 | |
|   let scheme = aURI.scheme;
 | |
|   let value = aURI.displaySpec;
 | |
| 
 | |
|   // Try to decode as UTF-8 if there's no encoding sequence that we would break.
 | |
|   if (!/%25(?:3B|2F|3F|3A|40|26|3D|2B|24|2C|23)/i.test(value)) {
 | |
|     let decodeASCIIOnly = !["https", "http", "file", "ftp"].includes(scheme);
 | |
|     if (decodeASCIIOnly) {
 | |
|       // This only decodes ascii characters (hex) 20-7e, except 25 (%).
 | |
|       // This avoids both cases stipulated below (%-related issues, and \r, \n
 | |
|       // and \t, which would be %0d, %0a and %09, respectively) as well as any
 | |
|       // non-US-ascii characters.
 | |
|       value = value.replace(
 | |
|         /%(2[0-4]|2[6-9a-f]|[3-6][0-9a-f]|7[0-9a-e])/g,
 | |
|         decodeURI
 | |
|       );
 | |
|     } else {
 | |
|       try {
 | |
|         value = decodeURI(value)
 | |
|           // decodeURI decodes %25 to %, which creates unintended encoding
 | |
|           // sequences. Re-encode it, unless it's part of a sequence that
 | |
|           // survived decodeURI, i.e. one for:
 | |
|           // ';', '/', '?', ':', '@', '&', '=', '+', '$', ',', '#'
 | |
|           // (RFC 3987 section 3.2)
 | |
|           .replace(
 | |
|             /%(?!3B|2F|3F|3A|40|26|3D|2B|24|2C|23)/gi,
 | |
|             encodeURIComponent
 | |
|           );
 | |
|       } catch (e) {}
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // Encode potentially invisible characters:
 | |
|   //   U+0000-001F: C0/C1 control characters
 | |
|   //   U+007F-009F: commands
 | |
|   //   U+00A0, U+1680, U+2000-200A, U+202F, U+205F, U+3000: other spaces
 | |
|   //   U+2028-2029: line and paragraph separators
 | |
|   //   U+2800: braille empty pattern
 | |
|   //   U+FFFC: object replacement character
 | |
|   // Encode any trailing whitespace that may be part of a pasted URL, so that it
 | |
|   // doesn't get eaten away by the location bar (bug 410726).
 | |
|   // Encode all adjacent space chars (U+0020), to prevent spoofing attempts
 | |
|   // where they would push part of the URL to overflow the location bar
 | |
|   // (bug 1395508). A single space, or the last space if the are many, is
 | |
|   // preserved to maintain readability of certain urls. We only do this for the
 | |
|   // common space, because others may be eaten when copied to the clipboard, so
 | |
|   // it's safer to preserve them encoded.
 | |
|   value = value.replace(
 | |
|     // eslint-disable-next-line no-control-regex
 | |
|     /[\u0000-\u001f\u007f-\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u2800\u3000\ufffc]|[\r\n\t]|\u0020(?=\u0020)|\s$/g,
 | |
|     encodeURIComponent
 | |
|   );
 | |
| 
 | |
|   // Encode characters that are ignorable, can't be rendered usefully, or may
 | |
|   // confuse users.
 | |
|   //
 | |
|   // Default ignorable characters; ZWNJ (U+200C) and ZWJ (U+200D) are excluded
 | |
|   // per bug 582186:
 | |
|   //   U+00AD, U+034F, U+06DD, U+070F, U+115F-1160, U+17B4, U+17B5, U+180B-180E,
 | |
|   //   U+2060, U+FEFF, U+200B, U+2060-206F, U+3164, U+FE00-FE0F, U+FFA0,
 | |
|   //   U+FFF0-FFFB, U+1D173-1D17A (U+D834 + DD73-DD7A),
 | |
|   //   U+E0000-E0FFF (U+DB40-DB43 + U+DC00-DFFF)
 | |
|   // Bidi control characters (RFC 3987 sections 3.2 and 4.1 paragraph 6):
 | |
|   //   U+061C, U+200E, U+200F, U+202A-202E, U+2066-2069
 | |
|   // Other format characters in the Cf category that are unlikely to be rendered
 | |
|   // usefully:
 | |
|   //   U+0600-0605, U+08E2, U+110BD (U+D804 + U+DCBD),
 | |
|   //   U+110CD (U+D804 + U+DCCD), U+13430-13438 (U+D80D + U+DC30-DC38),
 | |
|   //   U+1BCA0-1BCA3 (U+D82F + U+DCA0-DCA3)
 | |
|   // Mimicking UI parts:
 | |
|   //   U+1F50F-1F513 (U+D83D + U+DD0F-DD13), U+1F6E1 (U+D83D + U+DEE1)
 | |
|   value = value.replace(
 | |
|     // eslint-disable-next-line no-misleading-character-class
 | |
|     /[\u00ad\u034f\u061c\u06dd\u070f\u115f\u1160\u17b4\u17b5\u180b-\u180e\u200b\u200e\u200f\u202a-\u202e\u2060-\u206f\u3164\u0600-\u0605\u08e2\ufe00-\ufe0f\ufeff\uffa0\ufff0-\ufffb]|\ud804[\udcbd\udccd]|\ud80d[\udc30-\udc38]|\ud82f[\udca0-\udca3]|\ud834[\udd73-\udd7a]|[\udb40-\udb43][\udc00-\udfff]|\ud83d[\udd0f-\udd13\udee1]/g,
 | |
|     encodeURIComponent
 | |
|   );
 | |
|   return value;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Handles copy and cut commands for the urlbar.
 | |
|  */
 | |
| class CopyCutController {
 | |
|   /**
 | |
|    * @param {UrlbarInput} urlbar
 | |
|    *   The UrlbarInput instance to use this controller for.
 | |
|    */
 | |
|   constructor(urlbar) {
 | |
|     this.urlbar = urlbar;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @param {string} command
 | |
|    *   The name of the command to handle.
 | |
|    */
 | |
|   doCommand(command) {
 | |
|     let urlbar = this.urlbar;
 | |
|     let val = urlbar._getSelectedValueForClipboard();
 | |
|     if (!val) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (command == "cmd_cut" && this.isCommandEnabled(command)) {
 | |
|       let start = urlbar.selectionStart;
 | |
|       let end = urlbar.selectionEnd;
 | |
|       urlbar.inputField.value =
 | |
|         urlbar.inputField.value.substring(0, start) +
 | |
|         urlbar.inputField.value.substring(end);
 | |
|       urlbar.inputField.setSelectionRange(start, start);
 | |
| 
 | |
|       let event = new UIEvent("input", {
 | |
|         bubbles: true,
 | |
|         cancelable: false,
 | |
|         view: urlbar.window,
 | |
|         detail: 0,
 | |
|       });
 | |
|       urlbar.inputField.dispatchEvent(event);
 | |
|     }
 | |
| 
 | |
|     lazy.ClipboardHelper.copyString(val);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @param {string} command
 | |
|    *   The name of the command to check.
 | |
|    * @returns {boolean}
 | |
|    *   Whether the command is handled by this controller.
 | |
|    */
 | |
|   supportsCommand(command) {
 | |
|     switch (command) {
 | |
|       case "cmd_copy":
 | |
|       case "cmd_cut":
 | |
|         return true;
 | |
|     }
 | |
|     return false;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @param {string} command
 | |
|    *   The name of the command to check.
 | |
|    * @returns {boolean}
 | |
|    *   Whether the command should be enabled.
 | |
|    */
 | |
|   isCommandEnabled(command) {
 | |
|     return (
 | |
|       this.supportsCommand(command) &&
 | |
|       (command != "cmd_cut" || !this.urlbar.readOnly) &&
 | |
|       this.urlbar.selectionStart < this.urlbar.selectionEnd
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   onEvent() {}
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Manages the Add Search Engine contextual menu entries.
 | |
|  *
 | |
|  * Note: setEnginesFromBrowser must be invoked from the outside when the
 | |
|  *       page provided engines list changes.
 | |
|  *       refreshContextMenu must be invoked when the context menu is opened.
 | |
|  */
 | |
| class AddSearchEngineHelper {
 | |
|   /**
 | |
|    * @type {UrlbarSearchOneOffs}
 | |
|    */
 | |
|   shortcutButtons;
 | |
| 
 | |
|   /**
 | |
|    * @param {UrlbarInput} input The parent UrlbarInput.
 | |
|    */
 | |
|   constructor(input) {
 | |
|     this.input = input;
 | |
|     this.shortcutButtons = input.view.oneOffSearchButtons;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * If there's more than this number of engines, the context menu offers
 | |
|    * them in a submenu.
 | |
|    *
 | |
|    * @returns {number}
 | |
|    */
 | |
|   get maxInlineEngines() {
 | |
|     return this.shortcutButtons._maxInlineAddEngines;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Invoked by browser when the list of available engines changes.
 | |
|    *
 | |
|    * @param {object} browser The invoking browser.
 | |
|    */
 | |
|   setEnginesFromBrowser(browser) {
 | |
|     this.browsingContext = browser.browsingContext;
 | |
|     // Make a copy of the array for state comparison.
 | |
|     let engines = browser.engines?.slice() || [];
 | |
|     if (!this._sameEngines(this.engines, engines)) {
 | |
|       this.engines = engines;
 | |
|       this.shortcutButtons.updateWebEngines(engines);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   _sameEngines(engines1, engines2) {
 | |
|     if (engines1?.length != engines2?.length) {
 | |
|       return false;
 | |
|     }
 | |
|     return lazy.ObjectUtils.deepEqual(
 | |
|       engines1.map(e => e.title),
 | |
|       engines2.map(e => e.title)
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   _createMenuitem(engine, index) {
 | |
|     let elt = this.input.document.createXULElement("menuitem");
 | |
|     elt.setAttribute("anonid", `add-engine-${index}`);
 | |
|     elt.classList.add("menuitem-iconic");
 | |
|     elt.classList.add("context-menu-add-engine");
 | |
|     this.input.document.l10n.setAttributes(elt, "search-one-offs-add-engine", {
 | |
|       engineName: engine.title,
 | |
|     });
 | |
|     elt.setAttribute("uri", engine.uri);
 | |
|     if (engine.icon) {
 | |
|       elt.setAttribute("image", engine.icon);
 | |
|     } else {
 | |
|       elt.removeAttribute("image", engine.icon);
 | |
|     }
 | |
|     elt.addEventListener("command", this._onCommand.bind(this));
 | |
|     return elt;
 | |
|   }
 | |
| 
 | |
|   _createMenu(engine) {
 | |
|     let elt = this.input.document.createXULElement("menu");
 | |
|     elt.setAttribute("anonid", "add-engine-menu");
 | |
|     elt.classList.add("menu-iconic");
 | |
|     elt.classList.add("context-menu-add-engine");
 | |
|     this.input.document.l10n.setAttributes(
 | |
|       elt,
 | |
|       "search-one-offs-add-engine-menu"
 | |
|     );
 | |
|     if (engine.icon) {
 | |
|       elt.setAttribute("image", engine.icon);
 | |
|     }
 | |
|     let popup = this.input.document.createXULElement("menupopup");
 | |
|     elt.appendChild(popup);
 | |
|     return elt;
 | |
|   }
 | |
| 
 | |
|   refreshContextMenu() {
 | |
|     let engines = this.engines;
 | |
| 
 | |
|     // Certain operations, like customization, destroy and recreate widgets,
 | |
|     // so we cannot rely on cached elements.
 | |
|     if (!this.input.querySelector(".menuseparator-add-engine")) {
 | |
|       this.contextSeparator =
 | |
|         this.input.document.createXULElement("menuseparator");
 | |
|       this.contextSeparator.setAttribute("anonid", "add-engine-separator");
 | |
|       this.contextSeparator.classList.add("menuseparator-add-engine");
 | |
|       this.contextSeparator.collapsed = true;
 | |
|       let contextMenu = this.input.querySelector("moz-input-box").menupopup;
 | |
|       contextMenu.appendChild(this.contextSeparator);
 | |
|     }
 | |
| 
 | |
|     this.contextSeparator.collapsed = !engines.length;
 | |
|     let curElt = this.contextSeparator;
 | |
|     // Remove the previous items, if any.
 | |
|     for (let elt = curElt.nextElementSibling; elt; ) {
 | |
|       let nextElementSibling = elt.nextElementSibling;
 | |
|       elt.remove();
 | |
|       elt = nextElementSibling;
 | |
|     }
 | |
| 
 | |
|     // If the page provides too many engines, we only show a single menu entry
 | |
|     // with engines in a submenu.
 | |
|     if (engines.length > this.maxInlineEngines) {
 | |
|       // Set the menu button's image to the image of the first engine.  The
 | |
|       // offered engines may have differing images, so there's no perfect
 | |
|       // choice here.
 | |
|       let elt = this._createMenu(engines[0]);
 | |
|       this.contextSeparator.insertAdjacentElement("afterend", elt);
 | |
|       curElt = elt.lastElementChild;
 | |
|     }
 | |
| 
 | |
|     // Insert the engines, either in the contextual menu or the sub menu.
 | |
|     for (let i = 0; i < engines.length; ++i) {
 | |
|       let elt = this._createMenuitem(engines[i], i);
 | |
|       if (curElt.localName == "menupopup") {
 | |
|         curElt.appendChild(elt);
 | |
|       } else {
 | |
|         curElt.insertAdjacentElement("afterend", elt);
 | |
|       }
 | |
|       curElt = elt;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   async _onCommand(event) {
 | |
|     let added = await lazy.SearchUIUtils.addOpenSearchEngine(
 | |
|       event.target.getAttribute("uri"),
 | |
|       event.target.getAttribute("image"),
 | |
|       this.browsingContext
 | |
|     ).catch(console.error);
 | |
|     if (added) {
 | |
|       // Remove the offered engine from the list. The browser updated the
 | |
|       // engines list at this point, so we just have to refresh the menu.)
 | |
|       this.refreshContextMenu();
 | |
|     }
 | |
|   }
 | |
| }
 | 
