Bug 1857669 - Install libavcodec/libavutil for Selenium tests r=releng-reviewers...
[gecko.git] / docshell / base / URIFixup.sys.mjs
blobe68652c5c0021bdd87f054e97ba53a29aa893f46
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/. */
7 /**
8  * This component handles fixing up URIs, by correcting obvious typos and adding
9  * missing schemes.
10  * URI references:
11  *   http://www.faqs.org/rfcs/rfc1738.html
12  *   http://www.faqs.org/rfcs/rfc2396.html
13  */
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";
23 const lazy = {};
25 XPCOMUtils.defineLazyServiceGetter(
26   lazy,
27   "externalProtocolService",
28   "@mozilla.org/uriloader/external-protocol-service;1",
29   "nsIExternalProtocolService"
32 XPCOMUtils.defineLazyServiceGetter(
33   lazy,
34   "defaultProtocolHandler",
35   "@mozilla.org/network/protocol;1?name=default",
36   "nsIProtocolHandler"
39 XPCOMUtils.defineLazyServiceGetter(
40   lazy,
41   "fileProtocolHandler",
42   "@mozilla.org/network/protocol;1?name=file",
43   "nsIFileProtocolHandler"
46 XPCOMUtils.defineLazyServiceGetter(
47   lazy,
48   "handlerService",
49   "@mozilla.org/uriloader/handler-service;1",
50   "nsIHandlerService"
53 XPCOMUtils.defineLazyPreferenceGetter(
54   lazy,
55   "fixupSchemeTypos",
56   "browser.fixup.typo.scheme",
57   true
59 XPCOMUtils.defineLazyPreferenceGetter(
60   lazy,
61   "dnsFirstForSingleWords",
62   "browser.fixup.dns_first_for_single_words",
63   false
65 XPCOMUtils.defineLazyPreferenceGetter(
66   lazy,
67   "keywordEnabled",
68   "keyword.enabled",
69   true
71 XPCOMUtils.defineLazyPreferenceGetter(
72   lazy,
73   "alternateProtocol",
74   "browser.fixup.alternate.protocol",
75   "https"
78 XPCOMUtils.defineLazyPreferenceGetter(
79   lazy,
80   "dnsResolveFullyQualifiedNames",
81   "browser.urlbar.dnsResolveFullyQualifiedNames",
82   true
85 const {
86   FIXUP_FLAG_NONE,
87   FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP,
88   FIXUP_FLAGS_MAKE_ALTERNATE_URI,
89   FIXUP_FLAG_PRIVATE_CONTEXT,
90   FIXUP_FLAG_FIX_SCHEME_TYPOS,
91 } = Ci.nsIURIFixup;
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(
99   lazy,
100   "userPasswordRegex",
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.
124 // Note:
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(
131   lazy,
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(
144   lazy,
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(
153   lazy,
154   "IPv4LikeRegex",
155   () => /^(?:[a-z+.-]+:\/*(?!\/))?(?:\d{1,3}\.){2,3}\d{1,3}(?::\d+|\/)?/i
157 ChromeUtils.defineLazyGetter(
158   lazy,
159   "IPv6LikeRegex",
160   () =>
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(
168     Services.prefs
169       .getChildList(branch)
170       .filter(p => Services.prefs.getBoolPref(p, false))
171       .map(p => p.substring(branch.length))
172   );
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)) {
178         domains.add(domain);
179       } else {
180         domains.delete(domain);
181       }
182     },
183     QueryInterface: ChromeUtils.generateQI([
184       "nsIObserver",
185       "nsISupportsWeakReference",
186     ]),
187   };
188   Services.prefs.addObserver(branch, domains._observer, true);
189   return domains;
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:
197 //   "two" => ["two"]
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);
211     if (lastPart) {
212       let entries = suffixes.get(lastPart);
213       if (!entries) {
214         entries = new Set();
215         suffixes.set(lastPart, entries);
216       }
217       entries.add(suffix);
218     }
219   }
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)) {
227         // Add the suffix.
228         if (!entries) {
229           entries = new Set();
230           suffixes.set(lastPart, entries);
231         }
232         entries.add(suffix);
233       } else if (entries) {
234         // Remove the suffix.
235         entries.delete(suffix);
236         if (!entries.size) {
237           suffixes.delete(lastPart);
238         }
239       }
240     },
241     QueryInterface: ChromeUtils.generateQI([
242       "nsIObserver",
243       "nsISupportsWeakReference",
244     ]),
245   };
246   Services.prefs.addObserver(branch, suffixes._observer, true);
247   return suffixes;
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()}__`
258     );
261 URIFixup.prototype = {
262   get FIXUP_FLAG_NONE() {
263     return FIXUP_FLAG_NONE;
264   },
265   get FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP() {
266     return FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
267   },
268   get FIXUP_FLAGS_MAKE_ALTERNATE_URI() {
269     return FIXUP_FLAGS_MAKE_ALTERNATE_URI;
270   },
271   get FIXUP_FLAG_PRIVATE_CONTEXT() {
272     return FIXUP_FLAG_PRIVATE_CONTEXT;
273   },
274   get FIXUP_FLAG_FIX_SCHEME_TYPOS() {
275     return FIXUP_FLAG_FIX_SCHEME_TYPOS;
276   },
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, "");
285     if (!uriString) {
286       throw new Components.Exception(
287         "Should pass a non-null uri",
288         Cr.NS_ERROR_FAILURE
289       );
290     }
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;
303       return info;
304     }
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);
310       if (fileURI) {
311         info.preferredURI = info.fixedURI = fileURI;
312         info.fixupChangedProtocol = true;
313         return info;
314       }
315     }
317     const isCommonProtocol = COMMON_PROTOCOLS.includes(scheme);
319     let canHandleProtocol =
320       scheme &&
321       (isCommonProtocol ||
322         Services.io.getProtocolHandler(scheme) != lazy.defaultProtocolHandler ||
323         this._isKnownExternalProtocol(scheme));
325     if (
326       canHandleProtocol ||
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))
332     ) {
333       // Just try to create an URL out of it.
334       try {
335         info.fixedURI = Services.io.newURI(uriString);
336       } catch (ex) {
337         if (ex.result != Cr.NS_ERROR_MALFORMED_URI) {
338           throw ex;
339         }
340       }
341     }
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.
349     if (
350       info.fixedURI &&
351       lazy.keywordEnabled &&
352       fixupFlags & FIXUP_FLAG_FIX_SCHEME_TYPOS &&
353       scheme &&
354       !canHandleProtocol
355     ) {
356       tryKeywordFixupForURIInfo(uriString, info, isPrivateContext);
357     }
359     if (info.fixedURI) {
360       if (!info.preferredURI) {
361         maybeSetAlternateFixedURI(info, fixupFlags);
362         info.preferredURI = info.fixedURI;
363       }
364       fixupConsecutiveDotsHost(info);
365       return info;
366     }
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(/^:?\/\//, "");
377     }
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);
393           return info;
394         }
395       }
396     }
398     // Handle "www.<something>" as a URI.
399     const asciiHost = info.fixedURI?.asciiHost;
400     if (
401       asciiHost?.length > 4 &&
402       asciiHost?.startsWith("www.") &&
403       asciiHost?.lastIndexOf(".") == 3
404     ) {
405       return info;
406     }
408     // Memoize the public suffix check, since it may be expensive and should
409     // only run once when necessary.
410     let suffixInfo;
411     function checkSuffix(info) {
412       if (!suffixInfo) {
413         suffixInfo = checkAndFixPublicSuffix(info);
414       }
415       return suffixInfo;
416     }
418     // See if it is a keyword and whether a keyword must be fixed up.
419     if (
420       lazy.keywordEnabled &&
421       fixupFlags & FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP &&
422       !inputHadDuffProtocol &&
423       !checkSuffix(info).suffix &&
424       keywordURIFixup(uriString, info, isPrivateContext)
425     ) {
426       fixupConsecutiveDotsHost(info);
427       return info;
428     }
430     if (
431       info.fixedURI &&
432       (!info.fixupChangedProtocol || !checkSuffix(info).hasUnknownSuffix)
433     ) {
434       fixupConsecutiveDotsHost(info);
435       return info;
436     }
438     // If we still haven't been able to construct a valid URI, try to force a
439     // keyword match.
440     if (lazy.keywordEnabled && fixupFlags & FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP) {
441       tryKeywordFixupForURIInfo(info.originalInput, info, isPrivateContext);
442     }
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
449       );
450     }
452     fixupConsecutiveDotsHost(info);
453     return info;
454   },
456   webNavigationFlagsToFixupFlags(href, navigationFlags) {
457     try {
458       Services.io.newURI(href);
459       // Remove LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP for valid uris.
460       navigationFlags &=
461         ~Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP;
462     } catch (ex) {}
464     let fixupFlags = FIXUP_FLAG_NONE;
465     if (
466       navigationFlags & Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP
467     ) {
468       fixupFlags |= FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
469     }
470     if (navigationFlags & Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS) {
471       fixupFlags |= FIXUP_FLAG_FIX_SCHEME_TYPOS;
472     }
473     return fixupFlags;
474   },
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
480       // parent process.
481       throw new Components.Exception(
482         "Can't invoke URIFixup in the content process",
483         Cr.NS_ERROR_NOT_AVAILABLE
484       );
485     }
486     let info = new URIFixupInfo(keyword);
488     // Strip leading "?" and leading/trailing spaces from aKeyword
489     if (keyword.startsWith("?")) {
490       keyword = keyword.substring(1);
491     }
492     keyword = keyword.trim();
494     if (!Services.search.hasSuccessfullyInitialized) {
495       return info;
496     }
498     // Try falling back to the search service's default search engine
499     // We must use an appropriate search engine depending on the private
500     // context.
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";
510     }
511     let submission = engine.getSubmission(keyword, responseType, "keyword");
512     if (
513       !submission ||
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")
517     ) {
518       throw new Components.Exception(
519         "Invalid search submission uri",
520         Cr.NS_ERROR_NOT_AVAILABLE
521       );
522     }
523     let submissionPostDataStream = submission.postData;
524     if (submissionPostDataStream) {
525       info.postData = submissionPostDataStream;
526     }
528     info.keywordProviderName = engine.name;
529     info.keywordAsSent = keyword;
530     info.preferredURI = submission.uri;
531     return info;
532   },
534   forceHttpFixup(uriString) {
535     if (!uriString) {
536       throw new Components.Exception(
537         "Should pass a non-null uri",
538         Cr.NS_ERROR_FAILURE
539       );
540     }
542     let info = new URIFixupInfo(uriString);
543     let { scheme, fixedSchemeUriString, fixupChangedProtocol } = extractScheme(
544       uriString,
545       FIXUP_FLAG_FIX_SCHEME_TYPOS
546     );
548     if (scheme != "http" && scheme != "https") {
549       throw new Components.Exception(
550         "Scheme should be either http or https",
551         Cr.NS_ERROR_FAILURE
552       );
553     }
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;
563     }
565     return info;
566   },
568   checkHost(uri, listener, originAttributes) {
569     let { displayHost, asciiHost } = uri;
570     if (!displayHost) {
571       throw new Components.Exception(
572         "URI must have displayHost",
573         Cr.NS_ERROR_FAILURE
574       );
575     }
576     if (!asciiHost) {
577       throw new Components.Exception(
578         "URI must have asciiHost",
579         Cr.NS_ERROR_FAILURE
580       );
581     }
583     let isIPv4Address = host => {
584       let parts = host.split(".");
585       if (parts.length != 4) {
586         return false;
587       }
588       return parts.every(part => {
589         let n = parseInt(part, 10);
590         return n >= 0 && n <= 255;
591       });
592     };
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)) {
604       return;
605     }
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(".")) {
612       lookupName += ".";
613     }
615     Services.obs.notifyObservers(null, "uri-fixup-check-dns");
616     Services.dns.asyncResolve(
617       lookupName,
618       Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
619       0,
620       null,
621       listener,
622       Services.tm.mainThread,
623       originAttributes
624     );
625   },
627   isDomainKnown,
629   _isKnownExternalProtocol(scheme) {
630     if (this._trustExternalProtocolService) {
631       return lazy.externalProtocolService.externalProtocolHandlerExists(scheme);
632     }
634     try {
635       // nsIExternalProtocolService.getProtocolHandlerInfo() on Android throws
636       // error due to not implemented.
637       return lazy.handlerService.exists(
638         lazy.externalProtocolService.getProtocolHandlerInfo(scheme)
639       );
640     } catch (e) {
641       return false;
642     }
643   },
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;
656   },
657   get consumer() {
658     return this._consumer || null;
659   },
661   set preferredURI(uri) {
662     this._preferredURI = uri;
663   },
664   get preferredURI() {
665     return this._preferredURI || null;
666   },
668   set fixedURI(uri) {
669     this._fixedURI = uri;
670   },
671   get fixedURI() {
672     return this._fixedURI || null;
673   },
675   set keywordProviderName(name) {
676     this._keywordProviderName = name;
677   },
678   get keywordProviderName() {
679     return this._keywordProviderName || "";
680   },
682   set keywordAsSent(keyword) {
683     this._keywordAsSent = keyword;
684   },
685   get keywordAsSent() {
686     return this._keywordAsSent || "";
687   },
689   set fixupChangedProtocol(changed) {
690     this._fixupChangedProtocol = changed;
691   },
692   get fixupChangedProtocol() {
693     return !!this._fixupChangedProtocol;
694   },
696   set fixupCreatedAlternateURI(changed) {
697     this._fixupCreatedAlternateURI = changed;
698   },
699   get fixupCreatedAlternateURI() {
700     return !!this._fixupCreatedAlternateURI;
701   },
703   set originalInput(input) {
704     this._originalInput = input;
705   },
706   get originalInput() {
707     return this._originalInput || "";
708   },
710   set postData(postData) {
711     this._postData = postData;
712   },
713   get postData() {
714     return this._postData || null;
715   },
717   classID: Components.ID("{33d75835-722f-42c0-89cc-44f328e56a86}"),
718   QueryInterface: ChromeUtils.generateQI(["nsIURIFixupInfo"]),
721 // Helpers
724  * Implementation of isDomainKnown, so we don't have to go through the
725  * service.
726  * @param {string} asciiHost
727  * @returns {boolean} whether the domain is known
728  */
729 function isDomainKnown(asciiHost) {
730   if (lazy.dnsFirstForSingleWords) {
731     return true;
732   }
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(".");
741   }
742   if (lazy.knownDomains.has(asciiHost.toLowerCase())) {
743     return true;
744   }
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) {
748     return false;
749   }
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);
755   if (suffixes) {
756     return Array.from(suffixes).some(s => asciiHost.endsWith(s));
757   }
758   return false;
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.
770  */
771 function checkAndFixPublicSuffix(info) {
772   let uri = info.fixedURI;
773   let asciiHost = uri?.asciiHost;
774   if (
775     !asciiHost ||
776     !asciiHost.includes(".") ||
777     asciiHost.endsWith(".") ||
778     isDomainKnown(asciiHost)
779   ) {
780     return { suffix: "", hasUnknownSuffix: false };
781   }
783   // Quick bailouts for most common cases, according to Alexa Top 1 million.
784   if (
785     /^\w/.test(asciiHost) &&
786     (asciiHost.endsWith(".com") ||
787       asciiHost.endsWith(".net") ||
788       asciiHost.endsWith(".org") ||
789       asciiHost.endsWith(".ru") ||
790       asciiHost.endsWith(".de"))
791   ) {
792     return {
793       suffix: asciiHost.substring(asciiHost.lastIndexOf(".") + 1),
794       hasUnknownSuffix: false,
795     };
796   }
797   try {
798     let suffix = Services.eTLD.getKnownPublicSuffix(uri);
799     if (suffix) {
800       return { suffix, hasUnknownSuffix: false };
801     }
802   } catch (ex) {
803     return { suffix: "", hasUnknownSuffix: false };
804   }
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 };
810   }
811   for (let [typo, fixed] of [
812     ["ocm", "com"],
813     ["con", "com"],
814     ["cmo", "com"],
815     ["xom", "com"],
816     ["vom", "com"],
817     ["cpm", "com"],
818     ["com'", "com"],
819     ["ent", "net"],
820     ["ner", "net"],
821     ["nte", "net"],
822     ["met", "net"],
823     ["rog", "org"],
824     ["ogr", "org"],
825     ["prg", "org"],
826     ["orh", "org"],
827   ]) {
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;
834       }
835       return { suffix: fixed, hasUnknownSuffix: false };
836     }
837   }
838   return { suffix: "", hasUnknownSuffix: true };
841 function tryKeywordFixupForURIInfo(uriString, fixupInfo, isPrivateContext) {
842   try {
843     let keywordInfo = Services.uriFixup.keywordToURI(
844       uriString,
845       isPrivateContext
846     );
847     fixupInfo.keywordProviderName = keywordInfo.keywordProviderName;
848     fixupInfo.keywordAsSent = keywordInfo.keywordAsSent;
849     fixupInfo.preferredURI = keywordInfo.preferredURI;
850     return true;
851   } catch (ex) {}
852   return false;
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
862  */
863 function maybeSetAlternateFixedURI(info, fixupFlags) {
864   let uri = info.fixedURI;
865   if (
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
870     uri.userPass ||
871     // Don't fix up hosts with ports
872     uri.port != -1
873   ) {
874     return false;
875   }
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") {
882     return false;
883   }
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) {
890     return false;
891   }
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.
901  */
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.
908     attemptFixup =
909       uriString.includes("\\") ||
910       (uriString[1] == ":" && (uriString.length == 2 || uriString[2] == "/"));
911     if (uriString[1] == ":" && uriString[2] == "/") {
912       path = uriString.replace(/\//g, "\\");
913     }
914   } else {
915     // UNIX: Check if it starts with "/" or "~".
916     attemptFixup = /^[~/]/.test(uriString);
917   }
918   if (attemptFixup) {
919     try {
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)
926       );
927     } catch (ex) {
928       // Not a file uri.
929     }
930   }
931   return null;
935  * Tries to fixup a string to an nsIURI by adding the default protocol.
937  * Should fix things like:
938  *    no-scheme.com
939  *    ftp.no-scheme.com
940  *    ftp4.no-scheme.com
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.
947  */
948 function fixupURIProtocol(uriString) {
949   let schemePos = uriString.indexOf("://");
950   if (schemePos == -1 || schemePos > uriString.search(/[:\/]/)) {
951     uriString = "http://" + uriString;
952   }
953   try {
954     return Services.io.newURI(uriString);
955   } catch (ex) {
956     // We generated an invalid uri.
957   }
958   return null;
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.
968  */
969 function keywordURIFixup(uriString, fixupInfo, isPrivateContext) {
970   // Here is a few examples of strings that should be searched:
971   // "what is mozilla"
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"
985   // "blah.com."
987   // We do keyword lookups if the input starts with a question mark.
988   if (uriString.startsWith("?")) {
989     return tryKeywordFixupForURIInfo(
990       fixupInfo.originalInput,
991       fixupInfo,
992       isPrivateContext
993     );
994   }
996   // Check for IPs.
997   const userPassword = lazy.userPasswordRegex.exec(uriString);
998   const ipString = userPassword
999     ? uriString.replace(userPassword[2], "")
1000     : uriString;
1001   if (lazy.IPv4LikeRegex.test(ipString) || lazy.IPv6LikeRegex.test(ipString)) {
1002     return false;
1003   }
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;
1010   if (
1011     asciiHost &&
1012     (isDomainKnown(asciiHost) ||
1013       (asciiHost.endsWith(".") &&
1014         asciiHost.indexOf(".") != asciiHost.length - 1))
1015   ) {
1016     return false;
1017   }
1019   // Avoid keyword lookup if the url seems to have password.
1020   if (fixupInfo.fixedURI?.password) {
1021     return false;
1022   }
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.
1027   if (
1028     !isURILike(uriString, fixupInfo.fixedURI?.displayHost) ||
1029     (fixupInfo.fixedURI?.userPass && fixupInfo.fixedURI?.pathQueryRef === "/")
1030   ) {
1031     return tryKeywordFixupForURIInfo(
1032       fixupInfo.originalInput,
1033       fixupInfo,
1034       isPrivateContext
1035     );
1036   }
1038   return false;
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
1046  * @returns {object}
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
1050  */
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;
1059   if (
1060     !matches ||
1061     (!hasColon && !hasSlash2) ||
1062     (!hasColon && !isFixupSchemeTypos)
1063   ) {
1064     return {
1065       scheme: "",
1066       fixedSchemeUriString: uriString,
1067       fixupChangedProtocol: false,
1068     };
1069   }
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}`;
1078   }
1080   let fixupChangedProtocol = false;
1082   if (isFixupSchemeTypos) {
1083     // Fix up common scheme typos.
1084     // TODO: Use levenshtein distance here?
1085     fixupChangedProtocol = [
1086       ["ttp", "http"],
1087       ["htp", "http"],
1088       ["ttps", "https"],
1089       ["tps", "https"],
1090       ["ps", "https"],
1091       ["htps", "https"],
1092       ["ile", "file"],
1093       ["le", "file"],
1094     ].some(([typo, fixed]) => {
1095       if (scheme === typo) {
1096         scheme = fixed;
1097         fixedSchemeUriString =
1098           scheme + fixedSchemeUriString.substring(typo.length);
1099         return true;
1100       }
1101       return false;
1102     });
1103   }
1105   return {
1106     scheme,
1107     fixedSchemeUriString,
1108     fixupChangedProtocol,
1109   };
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
1121  */
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",
1133       Cr.NS_ERROR_FAILURE
1134     );
1135   }
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
1142     );
1143   }
1144   return {
1145     preferredURI: Services.io.newURI("view-source:" + info.preferredURI.spec),
1146     postData: info.postData,
1147   };
1151  * Fixup the host of fixedURI if it contains consecutive dots.
1152  * @param {URIFixupInfo} info an URIInfo object
1153  */
1154 function fixupConsecutiveDotsHost(fixupInfo) {
1155   const uri = fixupInfo.fixedURI;
1157   try {
1158     if (!uri?.host.includes("..")) {
1159       return;
1160     }
1161   } catch (e) {
1162     return;
1163   }
1165   try {
1166     const isPreferredEqualsToFixed = fixupInfo.preferredURI?.equals(uri);
1168     fixupInfo.fixedURI = uri
1169       .mutate()
1170       .setHost(uri.host.replace(/\.+/g, "."))
1171       .finalize();
1173     if (isPreferredEqualsToFixed) {
1174       fixupInfo.preferredURI = fixupInfo.fixedURI;
1175     }
1176   } catch (e) {
1177     if (e.result !== Cr.NS_ERROR_MALFORMED_URI) {
1178       throw e;
1179     }
1180   }
1184  * Return whether or not given string is uri like.
1185  * This function returns true like following strings.
1186  * - ":8080"
1187  * - "localhost:8080" (if given host is "localhost")
1188  * - "/foo?bar"
1189  * - "/foo#bar"
1190  * @param {string} uriString.
1191  * @param {string} host.
1192  * @param {boolean} true if uri like.
1193  */
1194 function isURILike(uriString, host) {
1195   const indexOfSlash = uriString.indexOf("/");
1196   if (
1197     indexOfSlash >= 0 &&
1198     (indexOfSlash < uriString.indexOf("?", indexOfSlash) ||
1199       indexOfSlash < uriString.indexOf("#", indexOfSlash))
1200   ) {
1201     return true;
1202   }
1204   if (uriString.startsWith(host)) {
1205     uriString = uriString.substring(host.length);
1206   }
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
1215  * the hostname.
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.
1225  */
1226 function maybeAddPrefixAndSuffix(oldHost) {
1227   let prefix = Services.prefs.getCharPref(
1228     "browser.fixup.alternate.prefix",
1229     "www."
1230   );
1231   let suffix = Services.prefs.getCharPref(
1232     "browser.fixup.alternate.suffix",
1233     ".com"
1234   );
1235   let newHost = "";
1236   let numDots = (oldHost.match(/\./g) || []).length;
1237   if (numDots == 0) {
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;
1244     }
1245   }
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.
1259  * @return {boolean}
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.
1263  */
1264 function updateHostAndScheme(info, newHost) {
1265   let oldHost = info.fixedURI.host;
1266   let oldScheme = info.fixedURI.scheme;
1267   try {
1268     info.fixedURI = info.fixedURI
1269       .mutate()
1270       .setScheme(lazy.alternateProtocol)
1271       .setHost(newHost)
1272       .finalize();
1273   } catch (ex) {
1274     if (ex.result != Cr.NS_ERROR_MALFORMED_URI) {
1275       throw ex;
1276     }
1277     return false;
1278   }
1279   if (oldScheme != info.fixedURI.scheme) {
1280     info.fixupChangedProtocol = true;
1281   }
1282   if (oldHost != info.fixedURI.host) {
1283     info.fixupCreatedAlternateURI = true;
1284   }
1285   return true;