1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
2 * vim: sw=2 ts=2 sts=2 expandtab
3 * This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
8 * This component handles fixing up URIs, by correcting obvious typos and adding
11 * http://www.faqs.org/rfcs/rfc1738.html
12 * http://www.faqs.org/rfcs/rfc2396.html
15 // TODO (Bug 1641220) getFixupURIInfo has a complex logic, that likely could be
16 // simplified, but the risk of regressing its behavior is high.
17 /* eslint complexity: ["error", 43] */
19 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
21 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
25 XPCOMUtils.defineLazyServiceGetter(
27 "externalProtocolService",
28 "@mozilla.org/uriloader/external-protocol-service;1",
29 "nsIExternalProtocolService"
32 XPCOMUtils.defineLazyServiceGetter(
34 "defaultProtocolHandler",
35 "@mozilla.org/network/protocol;1?name=default",
39 XPCOMUtils.defineLazyServiceGetter(
41 "fileProtocolHandler",
42 "@mozilla.org/network/protocol;1?name=file",
43 "nsIFileProtocolHandler"
46 XPCOMUtils.defineLazyServiceGetter(
49 "@mozilla.org/uriloader/handler-service;1",
53 XPCOMUtils.defineLazyPreferenceGetter(
56 "browser.fixup.typo.scheme",
59 XPCOMUtils.defineLazyPreferenceGetter(
61 "dnsFirstForSingleWords",
62 "browser.fixup.dns_first_for_single_words",
65 XPCOMUtils.defineLazyPreferenceGetter(
71 XPCOMUtils.defineLazyPreferenceGetter(
74 "browser.fixup.alternate.protocol",
78 XPCOMUtils.defineLazyPreferenceGetter(
80 "dnsResolveFullyQualifiedNames",
81 "browser.urlbar.dnsResolveFullyQualifiedNames",
87 FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP,
88 FIXUP_FLAGS_MAKE_ALTERNATE_URI,
89 FIXUP_FLAG_PRIVATE_CONTEXT,
90 FIXUP_FLAG_FIX_SCHEME_TYPOS,
93 const COMMON_PROTOCOLS = ["http", "https", "file"];
95 // Regex used to identify user:password tokens in url strings.
96 // This is not a strict valid characters check, because we try to fixup this
97 // part of the url too.
98 ChromeUtils.defineLazyGetter(
101 () => /^([a-z+.-]+:\/{0,3})*([^\/@]+@).+/i
104 // Regex used to identify the string that starts with port expression.
105 ChromeUtils.defineLazyGetter(lazy, "portRegex", () => /^:\d{1,5}([?#/]|$)/);
107 // Regex used to identify numbers.
108 ChromeUtils.defineLazyGetter(lazy, "numberRegex", () => /^[0-9]+(\.[0-9]+)?$/);
110 // Regex used to identify tab separated content (having at least 2 tabs).
111 ChromeUtils.defineLazyGetter(lazy, "maxOneTabRegex", () => /^[^\t]*\t?[^\t]*$/);
113 // Regex used to test if a string with a protocol might instead be a url
114 // without a protocol but with a port:
116 // <hostname>:<port> or
117 // <hostname>:<port>/
119 // Where <hostname> is a string of alphanumeric characters and dashes
120 // separated by dots.
121 // and <port> is a 5 or less digits. This actually breaks the rfc2396
122 // definition of a scheme which allows dots in schemes.
125 // People expecting this to work with
126 // <user>:<password>@<host>:<port>/<url-path> will be disappointed!
128 // Note: Parser could be a lot tighter, tossing out silly hostnames
129 // such as those containing consecutive dots and so on.
130 ChromeUtils.defineLazyGetter(
132 "possiblyHostPortRegex",
133 () => /^[a-z0-9-]+(\.[a-z0-9-]+)*:[0-9]{1,5}([/?#]|$)/i
136 // Regex used to strip newlines.
137 ChromeUtils.defineLazyGetter(lazy, "newLinesRegex", () => /[\r\n]/g);
139 // Regex used to match a possible protocol.
140 // This resembles the logic in Services.io.extractScheme, thus \t is admitted
141 // and stripped later. We don't use Services.io.extractScheme because of
142 // performance bottleneck caused by crossing XPConnect.
143 ChromeUtils.defineLazyGetter(
145 "possibleProtocolRegex",
146 () => /^([a-z][a-z0-9.+\t-]*)(:|;)?(\/\/)?/i
149 // Regex used to match IPs. Note that these are not made to validate IPs, but
150 // just to detect strings that look like an IP. They also skip protocol.
151 // For IPv4 this also accepts a shorthand format with just 2 dots.
152 ChromeUtils.defineLazyGetter(
155 () => /^(?:[a-z+.-]+:\/*(?!\/))?(?:\d{1,3}\.){2,3}\d{1,3}(?::\d+|\/)?/i
157 ChromeUtils.defineLazyGetter(
161 /^(?:[a-z+.-]+:\/*(?!\/))?\[(?:[0-9a-f]{0,4}:){0,7}[0-9a-f]{0,4}\]?(?::\d+|\/)?/i
164 // Cache of known domains.
165 ChromeUtils.defineLazyGetter(lazy, "knownDomains", () => {
166 const branch = "browser.fixup.domainwhitelist.";
167 let domains = new Set(
169 .getChildList(branch)
170 .filter(p => Services.prefs.getBoolPref(p, false))
171 .map(p => p.substring(branch.length))
173 // Hold onto the observer to avoid it being GC-ed.
174 domains._observer = {
175 observe(subject, topic, data) {
176 let domain = data.substring(branch.length);
177 if (Services.prefs.getBoolPref(data, false)) {
180 domains.delete(domain);
183 QueryInterface: ChromeUtils.generateQI([
185 "nsISupportsWeakReference",
188 Services.prefs.addObserver(branch, domains._observer, true);
192 // Cache of known suffixes.
193 // This works differently from the known domains, because when we examine a
194 // domain we can't tell how many dot-separated parts constitute the suffix.
195 // We create a Map keyed by the last dotted part, containing a Set of
196 // all the suffixes ending with that part:
198 // "three" => ["some.three", "three"]
199 // When searching we can restrict the linear scan based on the last part.
200 // The ideal structure for this would be a Directed Acyclic Word Graph, but
201 // since we expect this list to be small it's not worth the complication.
202 ChromeUtils.defineLazyGetter(lazy, "knownSuffixes", () => {
203 const branch = "browser.fixup.domainsuffixwhitelist.";
204 let suffixes = new Map();
205 let prefs = Services.prefs
206 .getChildList(branch)
207 .filter(p => Services.prefs.getBoolPref(p, false));
208 for (let pref of prefs) {
209 let suffix = pref.substring(branch.length);
210 let lastPart = suffix.substr(suffix.lastIndexOf(".") + 1);
212 let entries = suffixes.get(lastPart);
215 suffixes.set(lastPart, entries);
220 // Hold onto the observer to avoid it being GC-ed.
221 suffixes._observer = {
222 observe(subject, topic, data) {
223 let suffix = data.substring(branch.length);
224 let lastPart = suffix.substr(suffix.lastIndexOf(".") + 1);
225 let entries = suffixes.get(lastPart);
226 if (Services.prefs.getBoolPref(data, false)) {
230 suffixes.set(lastPart, entries);
233 } else if (entries) {
234 // Remove the suffix.
235 entries.delete(suffix);
237 suffixes.delete(lastPart);
241 QueryInterface: ChromeUtils.generateQI([
243 "nsISupportsWeakReference",
246 Services.prefs.addObserver(branch, suffixes._observer, true);
250 export function URIFixup() {
251 // There are cases that nsIExternalProtocolService.externalProtocolHandlerExists() does
252 // not work well and returns always true due to flatpak. In this case, in order to
253 // fallback to nsIHandlerService.exits(), we test whether can trust
254 // nsIExternalProtocolService here.
255 this._trustExternalProtocolService =
256 !lazy.externalProtocolService.externalProtocolHandlerExists(
257 `__dummy${Date.now()}__`
261 URIFixup.prototype = {
262 get FIXUP_FLAG_NONE() {
263 return FIXUP_FLAG_NONE;
265 get FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP() {
266 return FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
268 get FIXUP_FLAGS_MAKE_ALTERNATE_URI() {
269 return FIXUP_FLAGS_MAKE_ALTERNATE_URI;
271 get FIXUP_FLAG_PRIVATE_CONTEXT() {
272 return FIXUP_FLAG_PRIVATE_CONTEXT;
274 get FIXUP_FLAG_FIX_SCHEME_TYPOS() {
275 return FIXUP_FLAG_FIX_SCHEME_TYPOS;
278 getFixupURIInfo(uriString, fixupFlags = FIXUP_FLAG_NONE) {
279 let isPrivateContext = fixupFlags & FIXUP_FLAG_PRIVATE_CONTEXT;
281 // Eliminate embedded newlines, which single-line text fields now allow,
282 // and cleanup the empty spaces and tabs that might be on each end.
283 uriString = uriString.trim().replace(lazy.newLinesRegex, "");
286 throw new Components.Exception(
287 "Should pass a non-null uri",
292 let info = new URIFixupInfo(uriString);
294 const { scheme, fixedSchemeUriString, fixupChangedProtocol } =
295 extractScheme(uriString, fixupFlags);
296 uriString = fixedSchemeUriString;
297 info.fixupChangedProtocol = fixupChangedProtocol;
299 if (scheme == "view-source") {
300 let { preferredURI, postData } = fixupViewSource(uriString, fixupFlags);
301 info.preferredURI = info.fixedURI = preferredURI;
302 info.postData = postData;
306 if (scheme.length < 2) {
307 // Check if it is a file path. We skip most schemes because the only case
308 // where a file path may look like having a scheme is "X:" on Windows.
309 let fileURI = fileURIFixup(uriString);
311 info.preferredURI = info.fixedURI = fileURI;
312 info.fixupChangedProtocol = true;
317 const isCommonProtocol = COMMON_PROTOCOLS.includes(scheme);
319 let canHandleProtocol =
322 Services.io.getProtocolHandler(scheme) != lazy.defaultProtocolHandler ||
323 this._isKnownExternalProtocol(scheme));
327 // If it's an unknown handler and the given URL looks like host:port or
328 // has a user:password we can't pass it to the external protocol handler.
329 // We'll instead try fixing it with http later.
330 (!lazy.possiblyHostPortRegex.test(uriString) &&
331 !lazy.userPasswordRegex.test(uriString))
333 // Just try to create an URL out of it.
335 info.fixedURI = Services.io.newURI(uriString);
337 if (ex.result != Cr.NS_ERROR_MALFORMED_URI) {
343 // We're dealing with a theoretically valid URI but we have no idea how to
344 // load it. (e.g. "christmas:humbug")
345 // It's more likely the user wants to search, and so we chuck this over to
346 // their preferred search provider.
347 // TODO (Bug 1588118): Should check FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP
348 // instead of FIXUP_FLAG_FIX_SCHEME_TYPOS.
351 lazy.keywordEnabled &&
352 fixupFlags & FIXUP_FLAG_FIX_SCHEME_TYPOS &&
356 tryKeywordFixupForURIInfo(uriString, info, isPrivateContext);
360 if (!info.preferredURI) {
361 maybeSetAlternateFixedURI(info, fixupFlags);
362 info.preferredURI = info.fixedURI;
364 fixupConsecutiveDotsHost(info);
368 // Fix up protocol string before calling KeywordURIFixup, because
369 // it cares about the hostname of such URIs.
370 // Prune duff protocol schemes:
371 // ://totallybroken.url.com
372 // //shorthand.url.com
373 let inputHadDuffProtocol =
374 uriString.startsWith("://") || uriString.startsWith("//");
375 if (inputHadDuffProtocol) {
376 uriString = uriString.replace(/^:?\/\//, "");
379 // Avoid fixing up content that looks like tab-separated values.
380 // Assume that 1 tab is accidental, but more than 1 implies this is
381 // supposed to be tab-separated content.
382 if (!isCommonProtocol && lazy.maxOneTabRegex.test(uriString)) {
383 let uriWithProtocol = fixupURIProtocol(uriString);
384 if (uriWithProtocol) {
385 info.fixedURI = uriWithProtocol;
386 info.fixupChangedProtocol = true;
387 maybeSetAlternateFixedURI(info, fixupFlags);
388 info.preferredURI = info.fixedURI;
389 // Check if it's a forced visit. The user can enforce a visit by
390 // appending a slash, but the string must be in a valid uri format.
391 if (uriString.endsWith("/")) {
392 fixupConsecutiveDotsHost(info);
398 // Handle "www.<something>" as a URI.
399 const asciiHost = info.fixedURI?.asciiHost;
401 asciiHost?.length > 4 &&
402 asciiHost?.startsWith("www.") &&
403 asciiHost?.lastIndexOf(".") == 3
408 // Memoize the public suffix check, since it may be expensive and should
409 // only run once when necessary.
411 function checkSuffix(info) {
413 suffixInfo = checkAndFixPublicSuffix(info);
418 // See if it is a keyword and whether a keyword must be fixed up.
420 lazy.keywordEnabled &&
421 fixupFlags & FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP &&
422 !inputHadDuffProtocol &&
423 !checkSuffix(info).suffix &&
424 keywordURIFixup(uriString, info, isPrivateContext)
426 fixupConsecutiveDotsHost(info);
432 (!info.fixupChangedProtocol || !checkSuffix(info).hasUnknownSuffix)
434 fixupConsecutiveDotsHost(info);
438 // If we still haven't been able to construct a valid URI, try to force a
440 if (lazy.keywordEnabled && fixupFlags & FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP) {
441 tryKeywordFixupForURIInfo(info.originalInput, info, isPrivateContext);
444 if (!info.preferredURI) {
445 // We couldn't salvage anything.
446 throw new Components.Exception(
447 "Couldn't build a valid uri",
448 Cr.NS_ERROR_MALFORMED_URI
452 fixupConsecutiveDotsHost(info);
456 webNavigationFlagsToFixupFlags(href, navigationFlags) {
458 Services.io.newURI(href);
459 // Remove LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP for valid uris.
461 ~Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP;
464 let fixupFlags = FIXUP_FLAG_NONE;
466 navigationFlags & Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP
468 fixupFlags |= FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
470 if (navigationFlags & Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS) {
471 fixupFlags |= FIXUP_FLAG_FIX_SCHEME_TYPOS;
476 keywordToURI(keyword, isPrivateContext) {
477 if (Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT) {
478 // There's no search service in the content process, thus all the calls
479 // from it that care about keywords conversion should go through the
481 throw new Components.Exception(
482 "Can't invoke URIFixup in the content process",
483 Cr.NS_ERROR_NOT_AVAILABLE
486 let info = new URIFixupInfo(keyword);
488 // Strip leading "?" and leading/trailing spaces from aKeyword
489 if (keyword.startsWith("?")) {
490 keyword = keyword.substring(1);
492 keyword = keyword.trim();
494 if (!Services.search.hasSuccessfullyInitialized) {
498 // Try falling back to the search service's default search engine
499 // We must use an appropriate search engine depending on the private
501 let engine = isPrivateContext
502 ? Services.search.defaultPrivateEngine
503 : Services.search.defaultEngine;
505 // We allow default search plugins to specify alternate parameters that are
506 // specific to keyword searches.
507 let responseType = null;
508 if (engine.supportsResponseType("application/x-moz-keywordsearch")) {
509 responseType = "application/x-moz-keywordsearch";
511 let submission = engine.getSubmission(keyword, responseType, "keyword");
514 // For security reasons (avoid redirecting to file, data, or other unsafe
515 // protocols) we only allow fixup to http/https search engines.
516 !submission.uri.scheme.startsWith("http")
518 throw new Components.Exception(
519 "Invalid search submission uri",
520 Cr.NS_ERROR_NOT_AVAILABLE
523 let submissionPostDataStream = submission.postData;
524 if (submissionPostDataStream) {
525 info.postData = submissionPostDataStream;
528 info.keywordProviderName = engine.name;
529 info.keywordAsSent = keyword;
530 info.preferredURI = submission.uri;
534 forceHttpFixup(uriString) {
536 throw new Components.Exception(
537 "Should pass a non-null uri",
542 let info = new URIFixupInfo(uriString);
543 let { scheme, fixedSchemeUriString, fixupChangedProtocol } = extractScheme(
545 FIXUP_FLAG_FIX_SCHEME_TYPOS
548 if (scheme != "http" && scheme != "https") {
549 throw new Components.Exception(
550 "Scheme should be either http or https",
555 info.fixupChangedProtocol = fixupChangedProtocol;
556 info.fixedURI = Services.io.newURI(fixedSchemeUriString);
558 let host = info.fixedURI.host;
559 if (host != "http" && host != "https" && host != "localhost") {
560 let modifiedHostname = maybeAddPrefixAndSuffix(host);
561 updateHostAndScheme(info, modifiedHostname);
562 info.preferredURI = info.fixedURI;
568 checkHost(uri, listener, originAttributes) {
569 let { displayHost, asciiHost } = uri;
571 throw new Components.Exception(
572 "URI must have displayHost",
577 throw new Components.Exception(
578 "URI must have asciiHost",
583 let isIPv4Address = host => {
584 let parts = host.split(".");
585 if (parts.length != 4) {
588 return parts.every(part => {
589 let n = parseInt(part, 10);
590 return n >= 0 && n <= 255;
594 // Avoid showing fixup information if we're suggesting an IP. Note that
595 // decimal representations of IPs are normalized to a 'regular'
596 // dot-separated IP address by network code, but that only happens for
597 // numbers that don't overflow. Longer numbers do not get normalized,
598 // but still work to access IP addresses. So for instance,
599 // 1097347366913 (ff7f000001) gets resolved by using the final bytes,
600 // making it the same as 7f000001, which is 127.0.0.1 aka localhost.
601 // While 2130706433 would get normalized by network, 1097347366913
602 // does not, and we have to deal with both cases here:
603 if (isIPv4Address(asciiHost) || /^(?:\d+|0x[a-f0-9]+)$/i.test(asciiHost)) {
607 // For dotless hostnames, we want to ensure this ends with a '.' but don't
608 // want the . showing up in the UI if we end up notifying the user, so we
609 // use a separate variable.
610 let lookupName = displayHost;
611 if (lazy.dnsResolveFullyQualifiedNames && !lookupName.includes(".")) {
615 Services.obs.notifyObservers(null, "uri-fixup-check-dns");
616 Services.dns.asyncResolve(
618 Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
622 Services.tm.mainThread,
629 _isKnownExternalProtocol(scheme) {
630 if (this._trustExternalProtocolService) {
631 return lazy.externalProtocolService.externalProtocolHandlerExists(scheme);
635 // nsIExternalProtocolService.getProtocolHandlerInfo() on Android throws
636 // error due to not implemented.
637 return lazy.handlerService.exists(
638 lazy.externalProtocolService.getProtocolHandlerInfo(scheme)
645 classID: Components.ID("{c6cf88b7-452e-47eb-bdc9-86e3561648ef}"),
646 QueryInterface: ChromeUtils.generateQI(["nsIURIFixup"]),
649 export function URIFixupInfo(originalInput = "") {
650 this._originalInput = originalInput;
653 URIFixupInfo.prototype = {
654 set consumer(consumer) {
655 this._consumer = consumer || null;
658 return this._consumer || null;
661 set preferredURI(uri) {
662 this._preferredURI = uri;
665 return this._preferredURI || null;
669 this._fixedURI = uri;
672 return this._fixedURI || null;
675 set keywordProviderName(name) {
676 this._keywordProviderName = name;
678 get keywordProviderName() {
679 return this._keywordProviderName || "";
682 set keywordAsSent(keyword) {
683 this._keywordAsSent = keyword;
685 get keywordAsSent() {
686 return this._keywordAsSent || "";
689 set fixupChangedProtocol(changed) {
690 this._fixupChangedProtocol = changed;
692 get fixupChangedProtocol() {
693 return !!this._fixupChangedProtocol;
696 set fixupCreatedAlternateURI(changed) {
697 this._fixupCreatedAlternateURI = changed;
699 get fixupCreatedAlternateURI() {
700 return !!this._fixupCreatedAlternateURI;
703 set originalInput(input) {
704 this._originalInput = input;
706 get originalInput() {
707 return this._originalInput || "";
710 set postData(postData) {
711 this._postData = postData;
714 return this._postData || null;
717 classID: Components.ID("{33d75835-722f-42c0-89cc-44f328e56a86}"),
718 QueryInterface: ChromeUtils.generateQI(["nsIURIFixupInfo"]),
724 * Implementation of isDomainKnown, so we don't have to go through the
726 * @param {string} asciiHost
727 * @returns {boolean} whether the domain is known
729 function isDomainKnown(asciiHost) {
730 if (lazy.dnsFirstForSingleWords) {
733 // Check if this domain is known as an actual
734 // domain (which will prevent a keyword query)
735 // Note that any processing of the host here should stay in sync with
736 // code in the front-end(s) that set the pref.
737 let lastDotIndex = asciiHost.lastIndexOf(".");
738 if (lastDotIndex == asciiHost.length - 1) {
739 asciiHost = asciiHost.substring(0, asciiHost.length - 1);
740 lastDotIndex = asciiHost.lastIndexOf(".");
742 if (lazy.knownDomains.has(asciiHost.toLowerCase())) {
745 // If there's no dot or only a leading dot we are done, otherwise we'll check
746 // against the known suffixes.
747 if (lastDotIndex <= 0) {
750 // Don't use getPublicSuffix here, since the suffix is not in the PSL,
751 // thus it couldn't tell if the suffix is made up of one or multiple
752 // dot-separated parts.
753 let lastPart = asciiHost.substr(lastDotIndex + 1);
754 let suffixes = lazy.knownSuffixes.get(lastPart);
756 return Array.from(suffixes).some(s => asciiHost.endsWith(s));
762 * Checks the suffix of info.fixedURI against the Public Suffix List.
763 * If the suffix is unknown due to a typo this will try to fix it up.
764 * @param {URIFixupInfo} info about the uri to check.
765 * @note this may modify the public suffix of info.fixedURI.
766 * @returns {object} result The lookup result.
767 * @returns {string} result.suffix The public suffix if one can be identified.
768 * @returns {boolean} result.hasUnknownSuffix True when the suffix is not in the
769 * Public Suffix List and it's not in knownSuffixes. False in the other cases.
771 function checkAndFixPublicSuffix(info) {
772 let uri = info.fixedURI;
773 let asciiHost = uri?.asciiHost;
776 !asciiHost.includes(".") ||
777 asciiHost.endsWith(".") ||
778 isDomainKnown(asciiHost)
780 return { suffix: "", hasUnknownSuffix: false };
783 // Quick bailouts for most common cases, according to Alexa Top 1 million.
785 /^\w/.test(asciiHost) &&
786 (asciiHost.endsWith(".com") ||
787 asciiHost.endsWith(".net") ||
788 asciiHost.endsWith(".org") ||
789 asciiHost.endsWith(".ru") ||
790 asciiHost.endsWith(".de"))
793 suffix: asciiHost.substring(asciiHost.lastIndexOf(".") + 1),
794 hasUnknownSuffix: false,
798 let suffix = Services.eTLD.getKnownPublicSuffix(uri);
800 return { suffix, hasUnknownSuffix: false };
803 return { suffix: "", hasUnknownSuffix: false };
805 // Suffix is unknown, try to fix most common 3 chars TLDs typos.
806 // .com is the most commonly mistyped tld, so it has more cases.
807 let suffix = Services.eTLD.getPublicSuffix(uri);
808 if (!suffix || lazy.numberRegex.test(suffix)) {
809 return { suffix: "", hasUnknownSuffix: false };
811 for (let [typo, fixed] of [
828 if (suffix == typo) {
829 let host = uri.host.substring(0, uri.host.length - typo.length) + fixed;
830 let updatePreferredURI = info.preferredURI == info.fixedURI;
831 info.fixedURI = uri.mutate().setHost(host).finalize();
832 if (updatePreferredURI) {
833 info.preferredURI = info.fixedURI;
835 return { suffix: fixed, hasUnknownSuffix: false };
838 return { suffix: "", hasUnknownSuffix: true };
841 function tryKeywordFixupForURIInfo(uriString, fixupInfo, isPrivateContext) {
843 let keywordInfo = Services.uriFixup.keywordToURI(
847 fixupInfo.keywordProviderName = keywordInfo.keywordProviderName;
848 fixupInfo.keywordAsSent = keywordInfo.keywordAsSent;
849 fixupInfo.preferredURI = keywordInfo.preferredURI;
856 * This generates an alternate fixedURI, by adding a prefix and a suffix to
857 * the fixedURI host, if and only if the protocol is http. It should _never_
858 * modify URIs with other protocols.
859 * @param {URIFixupInfo} info an URIInfo object
860 * @param {integer} fixupFlags the fixup flags
861 * @returns {boolean} Whether an alternate uri was generated
863 function maybeSetAlternateFixedURI(info, fixupFlags) {
864 let uri = info.fixedURI;
866 !(fixupFlags & FIXUP_FLAGS_MAKE_ALTERNATE_URI) ||
867 // Code only works for http. Not for any other protocol including https!
868 !uri.schemeIs("http") ||
869 // Security - URLs with user / password info should NOT be fixed up
871 // Don't fix up hosts with ports
877 let oldHost = uri.host;
878 // Don't create an alternate uri for localhost, because it would be confusing.
879 // Ditto for 'http' and 'https' as these are frequently the result of typos, e.g.
880 // 'https//foo' (note missing : ).
881 if (oldHost == "localhost" || oldHost == "http" || oldHost == "https") {
885 // Get the prefix and suffix to stick onto the new hostname. By default these
886 // are www. & .com but they could be any other value, e.g. www. & .org
887 let newHost = maybeAddPrefixAndSuffix(oldHost);
889 if (newHost == oldHost) {
893 return updateHostAndScheme(info, newHost);
897 * Try to fixup a file URI.
898 * @param {string} uriString The file URI to fix.
899 * @returns {nsIURI} a fixed uri or null.
900 * @note FileURIFixup only returns a URI if it has to add the file: protocol.
902 function fileURIFixup(uriString) {
903 let attemptFixup = false;
904 let path = uriString;
905 if (AppConstants.platform == "win") {
906 // Check for "\"" in the url-string, just a drive (e.g. C:),
907 // or 'A:/...' where the "protocol" is also a single letter.
909 uriString.includes("\\") ||
910 (uriString[1] == ":" && (uriString.length == 2 || uriString[2] == "/"));
911 if (uriString[1] == ":" && uriString[2] == "/") {
912 path = uriString.replace(/\//g, "\\");
915 // UNIX: Check if it starts with "/" or "~".
916 attemptFixup = /^[~/]/.test(uriString);
920 // Test if this is a valid path by trying to create a local file
921 // object. The URL of that is returned if successful.
922 let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
923 file.initWithPath(path);
924 return Services.io.newURI(
925 lazy.fileProtocolHandler.getURLSpecFromActualFile(file)
935 * Tries to fixup a string to an nsIURI by adding the default protocol.
937 * Should fix things like:
941 * no-scheme.com/query?foo=http://www.foo.com
942 * user:pass@no-scheme.com
944 * @param {string} uriString The string to fixup.
945 * @returns {nsIURI} an nsIURI built adding the default protocol to the string,
946 * or null if fixing was not possible.
948 function fixupURIProtocol(uriString) {
949 let schemePos = uriString.indexOf("://");
950 if (schemePos == -1 || schemePos > uriString.search(/[:\/]/)) {
951 uriString = "http://" + uriString;
954 return Services.io.newURI(uriString);
956 // We generated an invalid uri.
962 * Tries to fixup a string to a search url.
963 * @param {string} uriString the string to fixup.
964 * @param {URIFixupInfo} fixupInfo The fixup info object, modified in-place.
965 * @param {boolean} isPrivateContext Whether this happens in a private context.
966 * @param {nsIInputStream} postData optional POST data for the search
967 * @returns {boolean} Whether the keyword fixup was succesful.
969 function keywordURIFixup(uriString, fixupInfo, isPrivateContext) {
970 // Here is a few examples of strings that should be searched:
972 // "what is mozilla?"
973 // "docshell site:mozilla.org" - has a space in the origin part
974 // "?site:mozilla.org - anything that begins with a question mark
975 // "mozilla'.org" - Things that have a quote before the first dot/colon
976 // "mozilla/test" - unknown host
977 // ".mozilla", "mozilla." - starts or ends with a dot ()
978 // "user@nonQualifiedHost"
980 // These other strings should not be searched, because they could be URIs:
981 // "www.blah.com" - Domain with a standard or known suffix
982 // "knowndomain" - known domain
983 // "nonQualifiedHost:8888?something" - has a port
984 // "user:pass@nonQualifiedHost"
987 // We do keyword lookups if the input starts with a question mark.
988 if (uriString.startsWith("?")) {
989 return tryKeywordFixupForURIInfo(
990 fixupInfo.originalInput,
997 const userPassword = lazy.userPasswordRegex.exec(uriString);
998 const ipString = userPassword
999 ? uriString.replace(userPassword[2], "")
1001 if (lazy.IPv4LikeRegex.test(ipString) || lazy.IPv6LikeRegex.test(ipString)) {
1005 // Avoid keyword lookup if we can identify a host and it's known, or ends
1006 // with a dot and has some path.
1007 // Note that if dnsFirstForSingleWords is true isDomainKnown will always
1008 // return true, so we can avoid checking dnsFirstForSingleWords after this.
1009 let asciiHost = fixupInfo.fixedURI?.asciiHost;
1012 (isDomainKnown(asciiHost) ||
1013 (asciiHost.endsWith(".") &&
1014 asciiHost.indexOf(".") != asciiHost.length - 1))
1019 // Avoid keyword lookup if the url seems to have password.
1020 if (fixupInfo.fixedURI?.password) {
1024 // Even if the host is unknown, avoid keyword lookup if the string has
1025 // uri-like characteristics, unless it looks like "user@unknownHost".
1026 // Note we already excluded passwords at this point.
1028 !isURILike(uriString, fixupInfo.fixedURI?.displayHost) ||
1029 (fixupInfo.fixedURI?.userPass && fixupInfo.fixedURI?.pathQueryRef === "/")
1031 return tryKeywordFixupForURIInfo(
1032 fixupInfo.originalInput,
1042 * Mimics the logic in Services.io.extractScheme, but avoids crossing XPConnect.
1043 * This also tries to fixup the scheme if it was clearly mistyped.
1044 * @param {string} uriString the string to examine
1045 * @param {integer} fixupFlags The original fixup flags
1047 * scheme: a typo fixed scheme or empty string if one could not be identified
1048 * fixedSchemeUriString: uri string with a typo fixed scheme
1049 * fixupChangedProtocol: true if the scheme is fixed up
1051 function extractScheme(uriString, fixupFlags = FIXUP_FLAG_NONE) {
1052 const matches = uriString.match(lazy.possibleProtocolRegex);
1053 const hasColon = matches?.[2] === ":";
1054 const hasSlash2 = matches?.[3] === "//";
1056 const isFixupSchemeTypos =
1057 lazy.fixupSchemeTypos && fixupFlags & FIXUP_FLAG_FIX_SCHEME_TYPOS;
1061 (!hasColon && !hasSlash2) ||
1062 (!hasColon && !isFixupSchemeTypos)
1066 fixedSchemeUriString: uriString,
1067 fixupChangedProtocol: false,
1071 let scheme = matches[1].replace("\t", "").toLowerCase();
1072 let fixedSchemeUriString = uriString;
1074 if (isFixupSchemeTypos && hasSlash2) {
1075 // Fix up typos for string that user would have intented as protocol.
1076 const afterProtocol = uriString.substring(matches[0].length);
1077 fixedSchemeUriString = `${scheme}://${afterProtocol}`;
1080 let fixupChangedProtocol = false;
1082 if (isFixupSchemeTypos) {
1083 // Fix up common scheme typos.
1084 // TODO: Use levenshtein distance here?
1085 fixupChangedProtocol = [
1094 ].some(([typo, fixed]) => {
1095 if (scheme === typo) {
1097 fixedSchemeUriString =
1098 scheme + fixedSchemeUriString.substring(typo.length);
1107 fixedSchemeUriString,
1108 fixupChangedProtocol,
1113 * View-source is a pseudo scheme. We're interested in fixing up the stuff
1114 * after it. The easiest way to do that is to call this method again with
1115 * the "view-source:" lopped off and then prepend it again afterwards.
1116 * @param {string} uriString The original string to fixup
1117 * @param {integer} fixupFlags The original fixup flags
1118 * @param {nsIInputStream} postData Optional POST data for the search
1119 * @returns {object} {preferredURI, postData} The fixed URI and relative postData
1120 * @throws if it's not possible to fixup the url
1122 function fixupViewSource(uriString, fixupFlags) {
1123 // We disable keyword lookup and alternate URIs so that small typos don't
1124 // cause us to look at very different domains.
1125 let newFixupFlags = fixupFlags & ~FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
1126 let innerURIString = uriString.substring(12).trim();
1128 // Prevent recursion.
1129 const { scheme: innerScheme } = extractScheme(innerURIString);
1130 if (innerScheme == "view-source") {
1131 throw new Components.Exception(
1132 "Prevent view-source recursion",
1137 let info = Services.uriFixup.getFixupURIInfo(innerURIString, newFixupFlags);
1138 if (!info.preferredURI) {
1139 throw new Components.Exception(
1140 "Couldn't build a valid uri",
1141 Cr.NS_ERROR_MALFORMED_URI
1145 preferredURI: Services.io.newURI("view-source:" + info.preferredURI.spec),
1146 postData: info.postData,
1151 * Fixup the host of fixedURI if it contains consecutive dots.
1152 * @param {URIFixupInfo} info an URIInfo object
1154 function fixupConsecutiveDotsHost(fixupInfo) {
1155 const uri = fixupInfo.fixedURI;
1158 if (!uri?.host.includes("..")) {
1166 const isPreferredEqualsToFixed = fixupInfo.preferredURI?.equals(uri);
1168 fixupInfo.fixedURI = uri
1170 .setHost(uri.host.replace(/\.+/g, "."))
1173 if (isPreferredEqualsToFixed) {
1174 fixupInfo.preferredURI = fixupInfo.fixedURI;
1177 if (e.result !== Cr.NS_ERROR_MALFORMED_URI) {
1184 * Return whether or not given string is uri like.
1185 * This function returns true like following strings.
1187 * - "localhost:8080" (if given host is "localhost")
1190 * @param {string} uriString.
1191 * @param {string} host.
1192 * @param {boolean} true if uri like.
1194 function isURILike(uriString, host) {
1195 const indexOfSlash = uriString.indexOf("/");
1197 indexOfSlash >= 0 &&
1198 (indexOfSlash < uriString.indexOf("?", indexOfSlash) ||
1199 indexOfSlash < uriString.indexOf("#", indexOfSlash))
1204 if (uriString.startsWith(host)) {
1205 uriString = uriString.substring(host.length);
1208 return lazy.portRegex.test(uriString);
1212 * Add prefix and suffix to a hostname if both are missing.
1214 * If the host does not start with the prefix, add the prefix to
1217 * By default the prefix and suffix are www. and .com but they could
1218 * be any value e.g. www. and .org as they use the preferences
1219 * "browser.fixup.alternate.prefix" and "browser.fixup.alternative.suffix"
1221 * If no changes were made, it returns an empty string.
1223 * @param {string} oldHost.
1224 * @return {String} Fixed up hostname or an empty string.
1226 function maybeAddPrefixAndSuffix(oldHost) {
1227 let prefix = Services.prefs.getCharPref(
1228 "browser.fixup.alternate.prefix",
1231 let suffix = Services.prefs.getCharPref(
1232 "browser.fixup.alternate.suffix",
1236 let numDots = (oldHost.match(/\./g) || []).length;
1238 newHost = prefix + oldHost + suffix;
1239 } else if (numDots == 1) {
1240 if (prefix && oldHost == prefix) {
1241 newHost = oldHost + suffix;
1242 } else if (suffix && !oldHost.startsWith(prefix)) {
1243 newHost = prefix + oldHost;
1246 return newHost ? newHost : oldHost;
1250 * Given an instance of URIFixupInfo, update its fixedURI.
1252 * First, change the protocol to the one stored in
1253 * "browser.fixup.alternate.protocol".
1255 * Then, try to update fixedURI's host to newHost.
1257 * @param {URIFixupInfo} info.
1258 * @param {string} newHost.
1260 * True, if info was updated without any errors.
1261 * False, if NS_ERROR_MALFORMED_URI error.
1262 * @throws If a non-NS_ERROR_MALFORMED_URI error occurs.
1264 function updateHostAndScheme(info, newHost) {
1265 let oldHost = info.fixedURI.host;
1266 let oldScheme = info.fixedURI.scheme;
1268 info.fixedURI = info.fixedURI
1270 .setScheme(lazy.alternateProtocol)
1274 if (ex.result != Cr.NS_ERROR_MALFORMED_URI) {
1279 if (oldScheme != info.fixedURI.scheme) {
1280 info.fixupChangedProtocol = true;
1282 if (oldHost != info.fixedURI.host) {
1283 info.fixupCreatedAlternateURI = true;