mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-11-11 05:39:41 +02:00
Differential Revision: https://phabricator.services.mozilla.com/D29430 --HG-- extra : moz-landing-system : lando
523 lines
17 KiB
JavaScript
523 lines
17 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/. */
|
|
|
|
"use strict";
|
|
|
|
/**
|
|
* This module exports the UrlbarUtils singleton, which contains constants and
|
|
* helper functions that are useful to all components of the urlbar.
|
|
*/
|
|
|
|
var EXPORTED_SYMBOLS = [
|
|
"UrlbarMuxer",
|
|
"UrlbarProvider",
|
|
"UrlbarQueryContext",
|
|
"UrlbarUtils",
|
|
];
|
|
|
|
const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
XPCOMUtils.defineLazyModuleGetters(this, {
|
|
BrowserUtils: "resource://gre/modules/BrowserUtils.jsm",
|
|
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
|
|
PlacesUIUtils: "resource:///modules/PlacesUIUtils.jsm",
|
|
PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
|
|
Services: "resource://gre/modules/Services.jsm",
|
|
UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
|
|
});
|
|
|
|
var UrlbarUtils = {
|
|
// Values for browser.urlbar.insertMethod
|
|
INSERTMETHOD: {
|
|
// Just append new results.
|
|
APPEND: 0,
|
|
// Merge previous and current results if search strings are related.
|
|
MERGE_RELATED: 1,
|
|
// Always merge previous and current results.
|
|
MERGE: 2,
|
|
},
|
|
|
|
// Extensions are allowed to add suggestions if they have registered a keyword
|
|
// with the omnibox API. This is the maximum number of suggestions an extension
|
|
// is allowed to add for a given search string.
|
|
// This value includes the heuristic result.
|
|
MAXIMUM_ALLOWED_EXTENSION_MATCHES: 6,
|
|
|
|
// This is used by UnifiedComplete, the new implementation will use
|
|
// PROVIDER_TYPE and RESULT_TYPE
|
|
RESULT_GROUP: {
|
|
HEURISTIC: "heuristic",
|
|
GENERAL: "general",
|
|
SUGGESTION: "suggestion",
|
|
EXTENSION: "extension",
|
|
},
|
|
|
|
// Defines provider types.
|
|
PROVIDER_TYPE: {
|
|
// Should be executed immediately, because it returns heuristic results
|
|
// that must be handled to the user asap.
|
|
IMMEDIATE: 1,
|
|
// Can be delayed, contains results coming from the session or the profile.
|
|
PROFILE: 2,
|
|
// Can be delayed, contains results coming from the network.
|
|
NETWORK: 3,
|
|
// Can be delayed, contains results coming from unknown sources.
|
|
EXTENSION: 4,
|
|
},
|
|
|
|
// Defines UrlbarResult types.
|
|
// If you add new result types, consider checking if consumers of
|
|
// "urlbar-user-start-navigation" need update as well.
|
|
RESULT_TYPE: {
|
|
// An open tab.
|
|
// Payload: { icon, url, userContextId }
|
|
TAB_SWITCH: 1,
|
|
// A search suggestion or engine.
|
|
// Payload: { icon, suggestion, keyword, query }
|
|
SEARCH: 2,
|
|
// A common url/title tuple, may be a bookmark with tags.
|
|
// Payload: { icon, url, title, tags }
|
|
URL: 3,
|
|
// A bookmark keyword.
|
|
// Payload: { icon, url, keyword, postData }
|
|
KEYWORD: 4,
|
|
// A WebExtension Omnibox result.
|
|
// Payload: { icon, keyword, title, content }
|
|
OMNIBOX: 5,
|
|
// A tab from another synced device.
|
|
// Payload: { url, icon, device, title }
|
|
REMOTE_TAB: 6,
|
|
},
|
|
|
|
// This defines the source of results returned by a provider. Each provider
|
|
// can return results from more than one source. This is used by the
|
|
// ProvidersManager to decide which providers must be queried and which
|
|
// results can be returned.
|
|
// If you add new source types, consider checking if consumers of
|
|
// "urlbar-user-start-navigation" need update as well.
|
|
RESULT_SOURCE: {
|
|
BOOKMARKS: 1,
|
|
HISTORY: 2,
|
|
SEARCH: 3,
|
|
TABS: 4,
|
|
OTHER_LOCAL: 5,
|
|
OTHER_NETWORK: 6,
|
|
},
|
|
|
|
// This defines icon locations that are common used in the UI.
|
|
ICON: {
|
|
// DEFAULT is defined lazily so it doesn't eagerly initialize PlacesUtils.
|
|
SEARCH_GLASS: "chrome://browser/skin/search-glass.svg",
|
|
},
|
|
|
|
// The number of results by which Page Up/Down move the selection.
|
|
PAGE_UP_DOWN_DELTA: 5,
|
|
|
|
// IME composition states.
|
|
COMPOSITION: {
|
|
NONE: 1,
|
|
COMPOSING: 2,
|
|
COMMIT: 3,
|
|
CANCELED: 4,
|
|
},
|
|
|
|
// This defines possible reasons for canceling a query.
|
|
CANCEL_REASON: {
|
|
// 1 is intentionally left in case we want a none/undefined/other later.
|
|
BLUR: 2,
|
|
},
|
|
|
|
// Limit the length of titles and URLs we display so layout doesn't spend too
|
|
// much time building text runs.
|
|
MAX_TEXT_LENGTH: 255,
|
|
|
|
/**
|
|
* Adds a url to history as long as it isn't in a private browsing window,
|
|
* and it is valid.
|
|
*
|
|
* @param {string} url The url to add to history.
|
|
* @param {nsIDomWindow} window The window from where the url is being added.
|
|
*/
|
|
addToUrlbarHistory(url, window) {
|
|
if (!PrivateBrowsingUtils.isWindowPrivate(window) &&
|
|
url &&
|
|
!url.includes(" ") &&
|
|
!/[\x00-\x1F]/.test(url)) // eslint-disable-line no-control-regex
|
|
PlacesUIUtils.markPageAsTyped(url);
|
|
},
|
|
|
|
/**
|
|
* Given a string, will generate a more appropriate urlbar value if a Places
|
|
* keyword or a search alias is found at the beginning of it.
|
|
*
|
|
* @param {string} url
|
|
* A string that may begin with a keyword or an alias.
|
|
*
|
|
* @returns {Promise}
|
|
* @resolves { url, postData, mayInheritPrincipal }. If it's not possible
|
|
* to discern a keyword or an alias, url will be the input string.
|
|
*/
|
|
async getShortcutOrURIAndPostData(url) {
|
|
let mayInheritPrincipal = false;
|
|
let postData = null;
|
|
// Split on the first whitespace.
|
|
let [keyword, param = ""] = url.trim().split(/\s(.+)/, 2);
|
|
|
|
if (!keyword) {
|
|
return { url, postData, mayInheritPrincipal };
|
|
}
|
|
|
|
await Services.search.init();
|
|
let engine = Services.search.getEngineByAlias(keyword);
|
|
if (engine) {
|
|
let submission = engine.getSubmission(param, null, "keyword");
|
|
return { url: submission.uri.spec,
|
|
postData: submission.postData,
|
|
mayInheritPrincipal };
|
|
}
|
|
|
|
// A corrupt Places database could make this throw, breaking navigation
|
|
// from the location bar.
|
|
let entry = null;
|
|
try {
|
|
entry = await PlacesUtils.keywords.fetch(keyword);
|
|
} catch (ex) {
|
|
Cu.reportError(`Unable to fetch Places keyword "${keyword}": ${ex}`);
|
|
}
|
|
if (!entry || !entry.url) {
|
|
// This is not a Places keyword.
|
|
return { url, postData, mayInheritPrincipal };
|
|
}
|
|
|
|
try {
|
|
[url, postData] =
|
|
await BrowserUtils.parseUrlAndPostData(entry.url.href,
|
|
entry.postData,
|
|
param);
|
|
if (postData) {
|
|
postData = this.getPostDataStream(postData);
|
|
}
|
|
|
|
// Since this URL came from a bookmark, it's safe to let it inherit the
|
|
// current document's principal.
|
|
mayInheritPrincipal = true;
|
|
} catch (ex) {
|
|
// It was not possible to bind the param, just use the original url value.
|
|
}
|
|
|
|
return { url, postData, mayInheritPrincipal };
|
|
},
|
|
|
|
/**
|
|
* Returns an input stream wrapper for the given post data.
|
|
*
|
|
* @param {string} postDataString The string to wrap.
|
|
* @param {string} [type] The encoding type.
|
|
* @returns {nsIInputStream} An input stream of the wrapped post data.
|
|
*/
|
|
getPostDataStream(postDataString,
|
|
type = "application/x-www-form-urlencoded") {
|
|
let dataStream = Cc["@mozilla.org/io/string-input-stream;1"]
|
|
.createInstance(Ci.nsIStringInputStream);
|
|
dataStream.data = postDataString;
|
|
|
|
let mimeStream = Cc["@mozilla.org/network/mime-input-stream;1"]
|
|
.createInstance(Ci.nsIMIMEInputStream);
|
|
mimeStream.addHeader("Content-Type", type);
|
|
mimeStream.setData(dataStream);
|
|
return mimeStream.QueryInterface(Ci.nsIInputStream);
|
|
},
|
|
|
|
/**
|
|
* Returns a list of all the token substring matches in a string. Matching is
|
|
* case insensitive. Each match in the returned list is a tuple: [matchIndex,
|
|
* matchLength]. matchIndex is the index in the string of the match, and
|
|
* matchLength is the length of the match.
|
|
*
|
|
* @param {array} tokens The tokens to search for.
|
|
* @param {string} str The string to match against.
|
|
* @returns {array} An array: [
|
|
* [matchIndex_0, matchLength_0],
|
|
* [matchIndex_1, matchLength_1],
|
|
* ...
|
|
* [matchIndex_n, matchLength_n]
|
|
* ].
|
|
* The array is sorted by match indexes ascending.
|
|
*/
|
|
getTokenMatches(tokens, str) {
|
|
str = str.toLocaleLowerCase();
|
|
// To generate non-overlapping ranges, we start from a 0-filled array with
|
|
// the same length of the string, and use it as a collision marker, setting
|
|
// 1 where a token matches.
|
|
let hits = new Array(str.length).fill(0);
|
|
for (let { lowerCaseValue } of tokens) {
|
|
// Ideally we should never hit the empty token case, but just in case
|
|
// the `needle` check protects us from an infinite loop.
|
|
for (let index = 0, needle = lowerCaseValue; index >= 0 && needle;) {
|
|
index = str.indexOf(needle, index);
|
|
if (index >= 0) {
|
|
hits.fill(1, index, index + needle.length);
|
|
index += needle.length;
|
|
}
|
|
}
|
|
}
|
|
// Starting from the collision array, generate [start, len] tuples
|
|
// representing the ranges to be highlighted.
|
|
let ranges = [];
|
|
for (let index = hits.indexOf(1); index >= 0 && index < hits.length;) {
|
|
let len = 0;
|
|
for (let j = index; j < hits.length && hits[j]; ++j, ++len);
|
|
ranges.push([index, len]);
|
|
// Move to the next 1.
|
|
index = hits.indexOf(1, index + len);
|
|
}
|
|
return ranges;
|
|
},
|
|
|
|
/**
|
|
* Extracts an url from a result, if possible.
|
|
* @param {UrlbarResult} result The result to extract from.
|
|
* @returns {object} a {url, postData} object, or null if a url can't be built
|
|
* from this result.
|
|
*/
|
|
getUrlFromResult(result) {
|
|
switch (result.type) {
|
|
case UrlbarUtils.RESULT_TYPE.URL:
|
|
case UrlbarUtils.RESULT_TYPE.REMOTE_TAB:
|
|
case UrlbarUtils.RESULT_TYPE.TAB_SWITCH:
|
|
return {url: result.payload.url, postData: null};
|
|
case UrlbarUtils.RESULT_TYPE.KEYWORD:
|
|
return {
|
|
url: result.payload.url,
|
|
postData: result.payload.postData ?
|
|
this.getPostDataStream(result.payload.postData) : null,
|
|
};
|
|
case UrlbarUtils.RESULT_TYPE.SEARCH: {
|
|
const engine = Services.search.getEngineByName(result.payload.engine);
|
|
let [url, postData] = this.getSearchQueryUrl(
|
|
engine, result.payload.suggestion || result.payload.query);
|
|
return {url, postData};
|
|
}
|
|
}
|
|
return {url: null, postData: null};
|
|
},
|
|
|
|
/**
|
|
* Get the url to load for the search query.
|
|
*
|
|
* @param {nsISearchEngine} engine
|
|
* The engine to generate the query for.
|
|
* @param {string} query
|
|
* The query string to search for.
|
|
* @returns {array}
|
|
* Returns an array containing the query url (string) and the
|
|
* post data (object).
|
|
*/
|
|
getSearchQueryUrl(engine, query) {
|
|
let submission = engine.getSubmission(query, null, "keyword");
|
|
return [submission.uri.spec, submission.postData];
|
|
},
|
|
|
|
/**
|
|
* Tries to initiate a speculative connection to a given url.
|
|
* @param {nsISearchEngine|nsIURI|URL|string} urlOrEngine entity to initiate
|
|
* a speculative connection for.
|
|
* @param {window} window the window from where the connection is initialized.
|
|
* @note This is not infallible, if a speculative connection cannot be
|
|
* initialized, it will be a no-op.
|
|
*/
|
|
setupSpeculativeConnection(urlOrEngine, window) {
|
|
if (!UrlbarPrefs.get("speculativeConnect.enabled")) {
|
|
return;
|
|
}
|
|
if (urlOrEngine instanceof Ci.nsISearchEngine) {
|
|
try {
|
|
urlOrEngine.speculativeConnect({
|
|
window,
|
|
originAttributes: window.gBrowser.contentPrincipal.originAttributes,
|
|
});
|
|
} catch (ex) {
|
|
// Can't setup speculative connection for this url, just ignore it.
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (urlOrEngine instanceof URL) {
|
|
urlOrEngine = urlOrEngine.href;
|
|
}
|
|
|
|
try {
|
|
let uri = urlOrEngine instanceof Ci.nsIURI ? urlOrEngine
|
|
: Services.io.newURI(urlOrEngine);
|
|
Services.io.speculativeConnect(uri, window.gBrowser.contentPrincipal, null);
|
|
} catch (ex) {
|
|
// Can't setup speculative connection for this url, just ignore it.
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Used to filter out the javascript protocol from URIs, since we don't
|
|
* support LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL for those.
|
|
* @param {string} pasteData The data to check for javacript protocol.
|
|
* @returns {string} The modified paste data.
|
|
*/
|
|
stripUnsafeProtocolOnPaste(pasteData) {
|
|
while (true) {
|
|
let scheme = "";
|
|
try {
|
|
scheme = Services.io.extractScheme(pasteData);
|
|
} catch (ex) {
|
|
// If it throws, this is not a javascript scheme.
|
|
}
|
|
if (scheme != "javascript") {
|
|
break;
|
|
}
|
|
|
|
pasteData = pasteData.substring(pasteData.indexOf(":") + 1);
|
|
}
|
|
return pasteData;
|
|
},
|
|
};
|
|
|
|
XPCOMUtils.defineLazyGetter(UrlbarUtils.ICON, "DEFAULT", () => {
|
|
return PlacesUtils.favicons.defaultFavicon.spec;
|
|
});
|
|
|
|
/**
|
|
* UrlbarQueryContext defines a user's autocomplete input from within the urlbar.
|
|
* It supplements it with details of how the search results should be obtained
|
|
* and what they consist of.
|
|
*/
|
|
class UrlbarQueryContext {
|
|
/**
|
|
* Constructs the UrlbarQueryContext instance.
|
|
*
|
|
* @param {object} options
|
|
* The initial options for UrlbarQueryContext.
|
|
* @param {string} options.searchString
|
|
* The string the user entered in autocomplete. Could be the empty string
|
|
* in the case of the user opening the popup via the mouse.
|
|
* @param {boolean} options.isPrivate
|
|
* Set to true if this query was started from a private browsing window.
|
|
* @param {number} options.maxResults
|
|
* The maximum number of results that will be displayed for this query.
|
|
* @param {boolean} options.allowAutofill
|
|
* Whether or not to allow providers to include autofill results.
|
|
* @param {number} options.userContextId
|
|
* The container id where this context was generated, if any.
|
|
*/
|
|
constructor(options = {}) {
|
|
this._checkRequiredOptions(options, [
|
|
"allowAutofill",
|
|
"isPrivate",
|
|
"maxResults",
|
|
"searchString",
|
|
]);
|
|
|
|
if (isNaN(parseInt(options.maxResults))) {
|
|
throw new Error(`Invalid maxResults property provided to UrlbarQueryContext`);
|
|
}
|
|
|
|
if (options.providers &&
|
|
(!Array.isArray(options.providers) || !options.providers.length)) {
|
|
throw new Error(`Invalid providers list`);
|
|
}
|
|
|
|
if (options.sources &&
|
|
(!Array.isArray(options.sources) || !options.sources.length)) {
|
|
throw new Error(`Invalid sources list`);
|
|
}
|
|
|
|
this.userContextId = options.userContextId;
|
|
}
|
|
|
|
/**
|
|
* Checks the required options, saving them as it goes.
|
|
*
|
|
* @param {object} options The options object to check.
|
|
* @param {array} optionNames The names of the options to check for.
|
|
* @throws {Error} Throws if there is a missing option.
|
|
*/
|
|
_checkRequiredOptions(options, optionNames) {
|
|
for (let optionName of optionNames) {
|
|
if (!(optionName in options)) {
|
|
throw new Error(`Missing or empty ${optionName} provided to UrlbarQueryContext`);
|
|
}
|
|
this[optionName] = options[optionName];
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Base class for a muxer.
|
|
* The muxer scope is to sort a given list of results.
|
|
*/
|
|
class UrlbarMuxer {
|
|
/**
|
|
* Unique name for the muxer, used by the context to sort results.
|
|
* Not using a unique name will cause the newest registration to win.
|
|
* @abstract
|
|
*/
|
|
get name() {
|
|
return "UrlbarMuxerBase";
|
|
}
|
|
/**
|
|
* Sorts queryContext results in-place.
|
|
* @param {UrlbarQueryContext} queryContext the context to sort results for.
|
|
* @abstract
|
|
*/
|
|
sort(queryContext) {
|
|
throw new Error("Trying to access the base class, must be overridden");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Base class for a provider.
|
|
* The provider scope is to query a datasource and return results from it.
|
|
*/
|
|
class UrlbarProvider {
|
|
/**
|
|
* Unique name for the provider, used by the context to filter on providers.
|
|
* Not using a unique name will cause the newest registration to win.
|
|
* @abstract
|
|
*/
|
|
get name() {
|
|
return "UrlbarProviderBase";
|
|
}
|
|
/**
|
|
* The type of the provider, must be one of UrlbarUtils.PROVIDER_TYPE.
|
|
* @abstract
|
|
*/
|
|
get type() {
|
|
throw new Error("Trying to access the base class, must be overridden");
|
|
}
|
|
/**
|
|
* List of UrlbarUtils.RESULT_SOURCE, representing the data sources used by
|
|
* the provider.
|
|
* @abstract
|
|
*/
|
|
get sources() {
|
|
throw new Error("Trying to access the base class, must be overridden");
|
|
}
|
|
/**
|
|
* Starts querying.
|
|
* @param {UrlbarQueryContext} queryContext The query context object
|
|
* @param {function} addCallback Callback invoked by the provider to add a new
|
|
* result. A UrlbarResult should be passed to it.
|
|
* @note Extended classes should return a Promise resolved when the provider
|
|
* is done searching AND returning results.
|
|
* @abstract
|
|
*/
|
|
startQuery(queryContext, addCallback) {
|
|
throw new Error("Trying to access the base class, must be overridden");
|
|
}
|
|
/**
|
|
* Cancels a running query,
|
|
* @param {UrlbarQueryContext} queryContext the query context object to cancel
|
|
* query for.
|
|
* @abstract
|
|
*/
|
|
cancelQuery(queryContext) {
|
|
throw new Error("Trying to access the base class, must be overridden");
|
|
}
|
|
}
|