Bug 1861709 replace AudioCallbackDriver::ThreadRunning() assertions that mean to...
[gecko.git] / docshell / base / URIFixup.sys.mjs
blobc42fb2efa8df023cd1f7583152867ed20d2ed946
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         info.wasSchemelessInput = true;
388         maybeSetAlternateFixedURI(info, fixupFlags);
389         info.preferredURI = info.fixedURI;
390         // Check if it's a forced visit. The user can enforce a visit by
391         // appending a slash, but the string must be in a valid uri format.
392         if (uriString.endsWith("/")) {
393           fixupConsecutiveDotsHost(info);
394           return info;
395         }
396       }
397     }
399     // Handle "www.<something>" as a URI.
400     const asciiHost = info.fixedURI?.asciiHost;
401     if (
402       asciiHost?.length > 4 &&
403       asciiHost?.startsWith("www.") &&
404       asciiHost?.lastIndexOf(".") == 3
405     ) {
406       return info;
407     }
409     // Memoize the public suffix check, since it may be expensive and should
410     // only run once when necessary.
411     let suffixInfo;
412     function checkSuffix(info) {
413       if (!suffixInfo) {
414         suffixInfo = checkAndFixPublicSuffix(info);
415       }
416       return suffixInfo;
417     }
419     // See if it is a keyword and whether a keyword must be fixed up.
420     if (
421       lazy.keywordEnabled &&
422       fixupFlags & FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP &&
423       !inputHadDuffProtocol &&
424       !checkSuffix(info).suffix &&
425       keywordURIFixup(uriString, info, isPrivateContext)
426     ) {
427       fixupConsecutiveDotsHost(info);
428       return info;
429     }
431     if (
432       info.fixedURI &&
433       (!info.fixupChangedProtocol || !checkSuffix(info).hasUnknownSuffix)
434     ) {
435       fixupConsecutiveDotsHost(info);
436       return info;
437     }
439     // If we still haven't been able to construct a valid URI, try to force a
440     // keyword match.
441     if (lazy.keywordEnabled && fixupFlags & FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP) {
442       tryKeywordFixupForURIInfo(info.originalInput, info, isPrivateContext);
443     }
445     if (!info.preferredURI) {
446       // We couldn't salvage anything.
447       throw new Components.Exception(
448         "Couldn't build a valid uri",
449         Cr.NS_ERROR_MALFORMED_URI
450       );
451     }
453     fixupConsecutiveDotsHost(info);
454     return info;
455   },
457   webNavigationFlagsToFixupFlags(href, navigationFlags) {
458     try {
459       Services.io.newURI(href);
460       // Remove LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP for valid uris.
461       navigationFlags &=
462         ~Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP;
463     } catch (ex) {}
465     let fixupFlags = FIXUP_FLAG_NONE;
466     if (
467       navigationFlags & Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP
468     ) {
469       fixupFlags |= FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
470     }
471     if (navigationFlags & Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS) {
472       fixupFlags |= FIXUP_FLAG_FIX_SCHEME_TYPOS;
473     }
474     return fixupFlags;
475   },
477   keywordToURI(keyword, isPrivateContext) {
478     if (Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT) {
479       // There's no search service in the content process, thus all the calls
480       // from it that care about keywords conversion should go through the
481       // parent process.
482       throw new Components.Exception(
483         "Can't invoke URIFixup in the content process",
484         Cr.NS_ERROR_NOT_AVAILABLE
485       );
486     }
487     let info = new URIFixupInfo(keyword);
489     // Strip leading "?" and leading/trailing spaces from aKeyword
490     if (keyword.startsWith("?")) {
491       keyword = keyword.substring(1);
492     }
493     keyword = keyword.trim();
495     if (!Services.search.hasSuccessfullyInitialized) {
496       return info;
497     }
499     // Try falling back to the search service's default search engine
500     // We must use an appropriate search engine depending on the private
501     // context.
502     let engine = isPrivateContext
503       ? Services.search.defaultPrivateEngine
504       : Services.search.defaultEngine;
506     // We allow default search plugins to specify alternate parameters that are
507     // specific to keyword searches.
508     let responseType = null;
509     if (engine.supportsResponseType("application/x-moz-keywordsearch")) {
510       responseType = "application/x-moz-keywordsearch";
511     }
512     let submission = engine.getSubmission(keyword, responseType, "keyword");
513     if (
514       !submission ||
515       // For security reasons (avoid redirecting to file, data, or other unsafe
516       // protocols) we only allow fixup to http/https search engines.
517       !submission.uri.scheme.startsWith("http")
518     ) {
519       throw new Components.Exception(
520         "Invalid search submission uri",
521         Cr.NS_ERROR_NOT_AVAILABLE
522       );
523     }
524     let submissionPostDataStream = submission.postData;
525     if (submissionPostDataStream) {
526       info.postData = submissionPostDataStream;
527     }
529     info.keywordProviderName = engine.name;
530     info.keywordAsSent = keyword;
531     info.preferredURI = submission.uri;
532     return info;
533   },
535   forceHttpFixup(uriString) {
536     if (!uriString) {
537       throw new Components.Exception(
538         "Should pass a non-null uri",
539         Cr.NS_ERROR_FAILURE
540       );
541     }
543     let info = new URIFixupInfo(uriString);
544     let { scheme, fixedSchemeUriString, fixupChangedProtocol } = extractScheme(
545       uriString,
546       FIXUP_FLAG_FIX_SCHEME_TYPOS
547     );
549     if (scheme != "http" && scheme != "https") {
550       throw new Components.Exception(
551         "Scheme should be either http or https",
552         Cr.NS_ERROR_FAILURE
553       );
554     }
556     info.fixupChangedProtocol = fixupChangedProtocol;
557     info.fixedURI = Services.io.newURI(fixedSchemeUriString);
559     let host = info.fixedURI.host;
560     if (host != "http" && host != "https" && host != "localhost") {
561       let modifiedHostname = maybeAddPrefixAndSuffix(host);
562       updateHostAndScheme(info, modifiedHostname);
563       info.preferredURI = info.fixedURI;
564     }
566     return info;
567   },
569   checkHost(uri, listener, originAttributes) {
570     let { displayHost, asciiHost } = uri;
571     if (!displayHost) {
572       throw new Components.Exception(
573         "URI must have displayHost",
574         Cr.NS_ERROR_FAILURE
575       );
576     }
577     if (!asciiHost) {
578       throw new Components.Exception(
579         "URI must have asciiHost",
580         Cr.NS_ERROR_FAILURE
581       );
582     }
584     let isIPv4Address = host => {
585       let parts = host.split(".");
586       if (parts.length != 4) {
587         return false;
588       }
589       return parts.every(part => {
590         let n = parseInt(part, 10);
591         return n >= 0 && n <= 255;
592       });
593     };
595     // Avoid showing fixup information if we're suggesting an IP. Note that
596     // decimal representations of IPs are normalized to a 'regular'
597     // dot-separated IP address by network code, but that only happens for
598     // numbers that don't overflow. Longer numbers do not get normalized,
599     // but still work to access IP addresses. So for instance,
600     // 1097347366913 (ff7f000001) gets resolved by using the final bytes,
601     // making it the same as 7f000001, which is 127.0.0.1 aka localhost.
602     // While 2130706433 would get normalized by network, 1097347366913
603     // does not, and we have to deal with both cases here:
604     if (isIPv4Address(asciiHost) || /^(?:\d+|0x[a-f0-9]+)$/i.test(asciiHost)) {
605       return;
606     }
608     // For dotless hostnames, we want to ensure this ends with a '.' but don't
609     // want the . showing up in the UI if we end up notifying the user, so we
610     // use a separate variable.
611     let lookupName = displayHost;
612     if (lazy.dnsResolveFullyQualifiedNames && !lookupName.includes(".")) {
613       lookupName += ".";
614     }
616     Services.obs.notifyObservers(null, "uri-fixup-check-dns");
617     Services.dns.asyncResolve(
618       lookupName,
619       Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
620       0,
621       null,
622       listener,
623       Services.tm.mainThread,
624       originAttributes
625     );
626   },
628   isDomainKnown,
630   _isKnownExternalProtocol(scheme) {
631     if (this._trustExternalProtocolService) {
632       return lazy.externalProtocolService.externalProtocolHandlerExists(scheme);
633     }
635     try {
636       // nsIExternalProtocolService.getProtocolHandlerInfo() on Android throws
637       // error due to not implemented.
638       return lazy.handlerService.exists(
639         lazy.externalProtocolService.getProtocolHandlerInfo(scheme)
640       );
641     } catch (e) {
642       return false;
643     }
644   },
646   classID: Components.ID("{c6cf88b7-452e-47eb-bdc9-86e3561648ef}"),
647   QueryInterface: ChromeUtils.generateQI(["nsIURIFixup"]),
650 export function URIFixupInfo(originalInput = "") {
651   this._originalInput = originalInput;
654 URIFixupInfo.prototype = {
655   set consumer(consumer) {
656     this._consumer = consumer || null;
657   },
658   get consumer() {
659     return this._consumer || null;
660   },
662   set preferredURI(uri) {
663     this._preferredURI = uri;
664   },
665   get preferredURI() {
666     return this._preferredURI || null;
667   },
669   set fixedURI(uri) {
670     this._fixedURI = uri;
671   },
672   get fixedURI() {
673     return this._fixedURI || null;
674   },
676   set keywordProviderName(name) {
677     this._keywordProviderName = name;
678   },
679   get keywordProviderName() {
680     return this._keywordProviderName || "";
681   },
683   set keywordAsSent(keyword) {
684     this._keywordAsSent = keyword;
685   },
686   get keywordAsSent() {
687     return this._keywordAsSent || "";
688   },
690   set wasSchemelessInput(changed) {
691     this._wasSchemelessInput = changed;
692   },
693   get wasSchemelessInput() {
694     return !!this._wasSchemelessInput;
695   },
697   set fixupChangedProtocol(changed) {
698     this._fixupChangedProtocol = changed;
699   },
700   get fixupChangedProtocol() {
701     return !!this._fixupChangedProtocol;
702   },
704   set fixupCreatedAlternateURI(changed) {
705     this._fixupCreatedAlternateURI = changed;
706   },
707   get fixupCreatedAlternateURI() {
708     return !!this._fixupCreatedAlternateURI;
709   },
711   set originalInput(input) {
712     this._originalInput = input;
713   },
714   get originalInput() {
715     return this._originalInput || "";
716   },
718   set postData(postData) {
719     this._postData = postData;
720   },
721   get postData() {
722     return this._postData || null;
723   },
725   classID: Components.ID("{33d75835-722f-42c0-89cc-44f328e56a86}"),
726   QueryInterface: ChromeUtils.generateQI(["nsIURIFixupInfo"]),
729 // Helpers
732  * Implementation of isDomainKnown, so we don't have to go through the
733  * service.
734  * @param {string} asciiHost
735  * @returns {boolean} whether the domain is known
736  */
737 function isDomainKnown(asciiHost) {
738   if (lazy.dnsFirstForSingleWords) {
739     return true;
740   }
741   // Check if this domain is known as an actual
742   // domain (which will prevent a keyword query)
743   // Note that any processing of the host here should stay in sync with
744   // code in the front-end(s) that set the pref.
745   let lastDotIndex = asciiHost.lastIndexOf(".");
746   if (lastDotIndex == asciiHost.length - 1) {
747     asciiHost = asciiHost.substring(0, asciiHost.length - 1);
748     lastDotIndex = asciiHost.lastIndexOf(".");
749   }
750   if (lazy.knownDomains.has(asciiHost.toLowerCase())) {
751     return true;
752   }
753   // If there's no dot or only a leading dot we are done, otherwise we'll check
754   // against the known suffixes.
755   if (lastDotIndex <= 0) {
756     return false;
757   }
758   // Don't use getPublicSuffix here, since the suffix is not in the PSL,
759   // thus it couldn't tell if the suffix is made up of one or multiple
760   // dot-separated parts.
761   let lastPart = asciiHost.substr(lastDotIndex + 1);
762   let suffixes = lazy.knownSuffixes.get(lastPart);
763   if (suffixes) {
764     return Array.from(suffixes).some(s => asciiHost.endsWith(s));
765   }
766   return false;
770  * Checks the suffix of info.fixedURI against the Public Suffix List.
771  * If the suffix is unknown due to a typo this will try to fix it up.
772  * @param {URIFixupInfo} info about the uri to check.
773  * @note this may modify the public suffix of info.fixedURI.
774  * @returns {object} result The lookup result.
775  * @returns {string} result.suffix The public suffix if one can be identified.
776  * @returns {boolean} result.hasUnknownSuffix True when the suffix is not in the
777  *     Public Suffix List and it's not in knownSuffixes. False in the other cases.
778  */
779 function checkAndFixPublicSuffix(info) {
780   let uri = info.fixedURI;
781   let asciiHost = uri?.asciiHost;
782   if (
783     !asciiHost ||
784     !asciiHost.includes(".") ||
785     asciiHost.endsWith(".") ||
786     isDomainKnown(asciiHost)
787   ) {
788     return { suffix: "", hasUnknownSuffix: false };
789   }
791   // Quick bailouts for most common cases, according to Alexa Top 1 million.
792   if (
793     /^\w/.test(asciiHost) &&
794     (asciiHost.endsWith(".com") ||
795       asciiHost.endsWith(".net") ||
796       asciiHost.endsWith(".org") ||
797       asciiHost.endsWith(".ru") ||
798       asciiHost.endsWith(".de"))
799   ) {
800     return {
801       suffix: asciiHost.substring(asciiHost.lastIndexOf(".") + 1),
802       hasUnknownSuffix: false,
803     };
804   }
805   try {
806     let suffix = Services.eTLD.getKnownPublicSuffix(uri);
807     if (suffix) {
808       return { suffix, hasUnknownSuffix: false };
809     }
810   } catch (ex) {
811     return { suffix: "", hasUnknownSuffix: false };
812   }
813   // Suffix is unknown, try to fix most common 3 chars TLDs typos.
814   // .com is the most commonly mistyped tld, so it has more cases.
815   let suffix = Services.eTLD.getPublicSuffix(uri);
816   if (!suffix || lazy.numberRegex.test(suffix)) {
817     return { suffix: "", hasUnknownSuffix: false };
818   }
819   for (let [typo, fixed] of [
820     ["ocm", "com"],
821     ["con", "com"],
822     ["cmo", "com"],
823     ["xom", "com"],
824     ["vom", "com"],
825     ["cpm", "com"],
826     ["com'", "com"],
827     ["ent", "net"],
828     ["ner", "net"],
829     ["nte", "net"],
830     ["met", "net"],
831     ["rog", "org"],
832     ["ogr", "org"],
833     ["prg", "org"],
834     ["orh", "org"],
835   ]) {
836     if (suffix == typo) {
837       let host = uri.host.substring(0, uri.host.length - typo.length) + fixed;
838       let updatePreferredURI = info.preferredURI == info.fixedURI;
839       info.fixedURI = uri.mutate().setHost(host).finalize();
840       if (updatePreferredURI) {
841         info.preferredURI = info.fixedURI;
842       }
843       return { suffix: fixed, hasUnknownSuffix: false };
844     }
845   }
846   return { suffix: "", hasUnknownSuffix: true };
849 function tryKeywordFixupForURIInfo(uriString, fixupInfo, isPrivateContext) {
850   try {
851     let keywordInfo = Services.uriFixup.keywordToURI(
852       uriString,
853       isPrivateContext
854     );
855     fixupInfo.keywordProviderName = keywordInfo.keywordProviderName;
856     fixupInfo.keywordAsSent = keywordInfo.keywordAsSent;
857     fixupInfo.preferredURI = keywordInfo.preferredURI;
858     return true;
859   } catch (ex) {}
860   return false;
864  * This generates an alternate fixedURI, by adding a prefix and a suffix to
865  * the fixedURI host, if and only if the protocol is http. It should _never_
866  * modify URIs with other protocols.
867  * @param {URIFixupInfo} info an URIInfo object
868  * @param {integer} fixupFlags the fixup flags
869  * @returns {boolean} Whether an alternate uri was generated
870  */
871 function maybeSetAlternateFixedURI(info, fixupFlags) {
872   let uri = info.fixedURI;
873   if (
874     !(fixupFlags & FIXUP_FLAGS_MAKE_ALTERNATE_URI) ||
875     // Code only works for http. Not for any other protocol including https!
876     !uri.schemeIs("http") ||
877     // Security - URLs with user / password info should NOT be fixed up
878     uri.userPass ||
879     // Don't fix up hosts with ports
880     uri.port != -1
881   ) {
882     return false;
883   }
885   let oldHost = uri.host;
886   // Don't create an alternate uri for localhost, because it would be confusing.
887   // Ditto for 'http' and 'https' as these are frequently the result of typos, e.g.
888   // 'https//foo' (note missing : ).
889   if (oldHost == "localhost" || oldHost == "http" || oldHost == "https") {
890     return false;
891   }
893   // Get the prefix and suffix to stick onto the new hostname. By default these
894   // are www. & .com but they could be any other value, e.g. www. & .org
895   let newHost = maybeAddPrefixAndSuffix(oldHost);
897   if (newHost == oldHost) {
898     return false;
899   }
901   return updateHostAndScheme(info, newHost);
905  * Try to fixup a file URI.
906  * @param {string} uriString The file URI to fix.
907  * @returns {nsIURI} a fixed uri or null.
908  * @note FileURIFixup only returns a URI if it has to add the file: protocol.
909  */
910 function fileURIFixup(uriString) {
911   let attemptFixup = false;
912   let path = uriString;
913   if (AppConstants.platform == "win") {
914     // Check for "\"" in the url-string, just a drive (e.g. C:),
915     // or 'A:/...' where the "protocol" is also a single letter.
916     attemptFixup =
917       uriString.includes("\\") ||
918       (uriString[1] == ":" && (uriString.length == 2 || uriString[2] == "/"));
919     if (uriString[1] == ":" && uriString[2] == "/") {
920       path = uriString.replace(/\//g, "\\");
921     }
922   } else {
923     // UNIX: Check if it starts with "/" or "~".
924     attemptFixup = /^[~/]/.test(uriString);
925   }
926   if (attemptFixup) {
927     try {
928       // Test if this is a valid path by trying to create a local file
929       // object. The URL of that is returned if successful.
930       let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
931       file.initWithPath(path);
932       return Services.io.newURI(
933         lazy.fileProtocolHandler.getURLSpecFromActualFile(file)
934       );
935     } catch (ex) {
936       // Not a file uri.
937     }
938   }
939   return null;
943  * Tries to fixup a string to an nsIURI by adding the default protocol.
945  * Should fix things like:
946  *    no-scheme.com
947  *    ftp.no-scheme.com
948  *    ftp4.no-scheme.com
949  *    no-scheme.com/query?foo=http://www.foo.com
950  *    user:pass@no-scheme.com
952  * @param {string} uriString The string to fixup.
953  * @returns {nsIURI} an nsIURI built adding the default protocol to the string,
954  *          or null if fixing was not possible.
955  */
956 function fixupURIProtocol(uriString) {
957   let schemePos = uriString.indexOf("://");
958   if (schemePos == -1 || schemePos > uriString.search(/[:\/]/)) {
959     uriString = "http://" + uriString;
960   }
961   try {
962     return Services.io.newURI(uriString);
963   } catch (ex) {
964     // We generated an invalid uri.
965   }
966   return null;
970  * Tries to fixup a string to a search url.
971  * @param {string} uriString the string to fixup.
972  * @param {URIFixupInfo} fixupInfo The fixup info object, modified in-place.
973  * @param {boolean} isPrivateContext Whether this happens in a private context.
974  * @param {nsIInputStream} postData optional POST data for the search
975  * @returns {boolean} Whether the keyword fixup was succesful.
976  */
977 function keywordURIFixup(uriString, fixupInfo, isPrivateContext) {
978   // Here is a few examples of strings that should be searched:
979   // "what is mozilla"
980   // "what is mozilla?"
981   // "docshell site:mozilla.org" - has a space in the origin part
982   // "?site:mozilla.org - anything that begins with a question mark
983   // "mozilla'.org" - Things that have a quote before the first dot/colon
984   // "mozilla/test" - unknown host
985   // ".mozilla", "mozilla." - starts or ends with a dot ()
986   // "user@nonQualifiedHost"
988   // These other strings should not be searched, because they could be URIs:
989   // "www.blah.com" - Domain with a standard or known suffix
990   // "knowndomain" - known domain
991   // "nonQualifiedHost:8888?something" - has a port
992   // "user:pass@nonQualifiedHost"
993   // "blah.com."
995   // We do keyword lookups if the input starts with a question mark.
996   if (uriString.startsWith("?")) {
997     return tryKeywordFixupForURIInfo(
998       fixupInfo.originalInput,
999       fixupInfo,
1000       isPrivateContext
1001     );
1002   }
1004   // Check for IPs.
1005   const userPassword = lazy.userPasswordRegex.exec(uriString);
1006   const ipString = userPassword
1007     ? uriString.replace(userPassword[2], "")
1008     : uriString;
1009   if (lazy.IPv4LikeRegex.test(ipString) || lazy.IPv6LikeRegex.test(ipString)) {
1010     return false;
1011   }
1013   // Avoid keyword lookup if we can identify a host and it's known, or ends
1014   // with a dot and has some path.
1015   // Note that if dnsFirstForSingleWords is true isDomainKnown will always
1016   // return true, so we can avoid checking dnsFirstForSingleWords after this.
1017   let asciiHost = fixupInfo.fixedURI?.asciiHost;
1018   if (
1019     asciiHost &&
1020     (isDomainKnown(asciiHost) ||
1021       (asciiHost.endsWith(".") &&
1022         asciiHost.indexOf(".") != asciiHost.length - 1))
1023   ) {
1024     return false;
1025   }
1027   // Avoid keyword lookup if the url seems to have password.
1028   if (fixupInfo.fixedURI?.password) {
1029     return false;
1030   }
1032   // Even if the host is unknown, avoid keyword lookup if the string has
1033   // uri-like characteristics, unless it looks like "user@unknownHost".
1034   // Note we already excluded passwords at this point.
1035   if (
1036     !isURILike(uriString, fixupInfo.fixedURI?.displayHost) ||
1037     (fixupInfo.fixedURI?.userPass && fixupInfo.fixedURI?.pathQueryRef === "/")
1038   ) {
1039     return tryKeywordFixupForURIInfo(
1040       fixupInfo.originalInput,
1041       fixupInfo,
1042       isPrivateContext
1043     );
1044   }
1046   return false;
1050  * Mimics the logic in Services.io.extractScheme, but avoids crossing XPConnect.
1051  * This also tries to fixup the scheme if it was clearly mistyped.
1052  * @param {string} uriString the string to examine
1053  * @param {integer} fixupFlags The original fixup flags
1054  * @returns {object}
1055  *          scheme: a typo fixed scheme or empty string if one could not be identified
1056  *          fixedSchemeUriString: uri string with a typo fixed scheme
1057  *          fixupChangedProtocol: true if the scheme is fixed up
1058  */
1059 function extractScheme(uriString, fixupFlags = FIXUP_FLAG_NONE) {
1060   const matches = uriString.match(lazy.possibleProtocolRegex);
1061   const hasColon = matches?.[2] === ":";
1062   const hasSlash2 = matches?.[3] === "//";
1064   const isFixupSchemeTypos =
1065     lazy.fixupSchemeTypos && fixupFlags & FIXUP_FLAG_FIX_SCHEME_TYPOS;
1067   if (
1068     !matches ||
1069     (!hasColon && !hasSlash2) ||
1070     (!hasColon && !isFixupSchemeTypos)
1071   ) {
1072     return {
1073       scheme: "",
1074       fixedSchemeUriString: uriString,
1075       fixupChangedProtocol: false,
1076     };
1077   }
1079   let scheme = matches[1].replace("\t", "").toLowerCase();
1080   let fixedSchemeUriString = uriString;
1082   if (isFixupSchemeTypos && hasSlash2) {
1083     // Fix up typos for string that user would have intented as protocol.
1084     const afterProtocol = uriString.substring(matches[0].length);
1085     fixedSchemeUriString = `${scheme}://${afterProtocol}`;
1086   }
1088   let fixupChangedProtocol = false;
1090   if (isFixupSchemeTypos) {
1091     // Fix up common scheme typos.
1092     // TODO: Use levenshtein distance here?
1093     fixupChangedProtocol = [
1094       ["ttp", "http"],
1095       ["htp", "http"],
1096       ["ttps", "https"],
1097       ["tps", "https"],
1098       ["ps", "https"],
1099       ["htps", "https"],
1100       ["ile", "file"],
1101       ["le", "file"],
1102     ].some(([typo, fixed]) => {
1103       if (scheme === typo) {
1104         scheme = fixed;
1105         fixedSchemeUriString =
1106           scheme + fixedSchemeUriString.substring(typo.length);
1107         return true;
1108       }
1109       return false;
1110     });
1111   }
1113   return {
1114     scheme,
1115     fixedSchemeUriString,
1116     fixupChangedProtocol,
1117   };
1121  * View-source is a pseudo scheme. We're interested in fixing up the stuff
1122  * after it. The easiest way to do that is to call this method again with
1123  * the "view-source:" lopped off and then prepend it again afterwards.
1124  * @param {string} uriString The original string to fixup
1125  * @param {integer} fixupFlags The original fixup flags
1126  * @param {nsIInputStream} postData Optional POST data for the search
1127  * @returns {object} {preferredURI, postData} The fixed URI and relative postData
1128  * @throws if it's not possible to fixup the url
1129  */
1130 function fixupViewSource(uriString, fixupFlags) {
1131   // We disable keyword lookup and alternate URIs so that small typos don't
1132   // cause us to look at very different domains.
1133   let newFixupFlags = fixupFlags & ~FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
1134   let innerURIString = uriString.substring(12).trim();
1136   // Prevent recursion.
1137   const { scheme: innerScheme } = extractScheme(innerURIString);
1138   if (innerScheme == "view-source") {
1139     throw new Components.Exception(
1140       "Prevent view-source recursion",
1141       Cr.NS_ERROR_FAILURE
1142     );
1143   }
1145   let info = Services.uriFixup.getFixupURIInfo(innerURIString, newFixupFlags);
1146   if (!info.preferredURI) {
1147     throw new Components.Exception(
1148       "Couldn't build a valid uri",
1149       Cr.NS_ERROR_MALFORMED_URI
1150     );
1151   }
1152   return {
1153     preferredURI: Services.io.newURI("view-source:" + info.preferredURI.spec),
1154     postData: info.postData,
1155   };
1159  * Fixup the host of fixedURI if it contains consecutive dots.
1160  * @param {URIFixupInfo} info an URIInfo object
1161  */
1162 function fixupConsecutiveDotsHost(fixupInfo) {
1163   const uri = fixupInfo.fixedURI;
1165   try {
1166     if (!uri?.host.includes("..")) {
1167       return;
1168     }
1169   } catch (e) {
1170     return;
1171   }
1173   try {
1174     const isPreferredEqualsToFixed = fixupInfo.preferredURI?.equals(uri);
1176     fixupInfo.fixedURI = uri
1177       .mutate()
1178       .setHost(uri.host.replace(/\.+/g, "."))
1179       .finalize();
1181     if (isPreferredEqualsToFixed) {
1182       fixupInfo.preferredURI = fixupInfo.fixedURI;
1183     }
1184   } catch (e) {
1185     if (e.result !== Cr.NS_ERROR_MALFORMED_URI) {
1186       throw e;
1187     }
1188   }
1192  * Return whether or not given string is uri like.
1193  * This function returns true like following strings.
1194  * - ":8080"
1195  * - "localhost:8080" (if given host is "localhost")
1196  * - "/foo?bar"
1197  * - "/foo#bar"
1198  * @param {string} uriString.
1199  * @param {string} host.
1200  * @param {boolean} true if uri like.
1201  */
1202 function isURILike(uriString, host) {
1203   const indexOfSlash = uriString.indexOf("/");
1204   if (
1205     indexOfSlash >= 0 &&
1206     (indexOfSlash < uriString.indexOf("?", indexOfSlash) ||
1207       indexOfSlash < uriString.indexOf("#", indexOfSlash))
1208   ) {
1209     return true;
1210   }
1212   if (uriString.startsWith(host)) {
1213     uriString = uriString.substring(host.length);
1214   }
1216   return lazy.portRegex.test(uriString);
1220  * Add prefix and suffix to a hostname if both are missing.
1222  * If the host does not start with the prefix, add the prefix to
1223  * the hostname.
1225  * By default the prefix and suffix are www. and .com but they could
1226  * be any value e.g. www. and .org as they use the preferences
1227  * "browser.fixup.alternate.prefix" and "browser.fixup.alternative.suffix"
1229  * If no changes were made, it returns an empty string.
1231  * @param {string} oldHost.
1232  * @return {String} Fixed up hostname or an empty string.
1233  */
1234 function maybeAddPrefixAndSuffix(oldHost) {
1235   let prefix = Services.prefs.getCharPref(
1236     "browser.fixup.alternate.prefix",
1237     "www."
1238   );
1239   let suffix = Services.prefs.getCharPref(
1240     "browser.fixup.alternate.suffix",
1241     ".com"
1242   );
1243   let newHost = "";
1244   let numDots = (oldHost.match(/\./g) || []).length;
1245   if (numDots == 0) {
1246     newHost = prefix + oldHost + suffix;
1247   } else if (numDots == 1) {
1248     if (prefix && oldHost == prefix) {
1249       newHost = oldHost + suffix;
1250     } else if (suffix && !oldHost.startsWith(prefix)) {
1251       newHost = prefix + oldHost;
1252     }
1253   }
1254   return newHost ? newHost : oldHost;
1258  * Given an instance of URIFixupInfo, update its fixedURI.
1260  * First, change the protocol to the one stored in
1261  * "browser.fixup.alternate.protocol".
1263  * Then, try to update fixedURI's host to newHost.
1265  * @param {URIFixupInfo} info.
1266  * @param {string} newHost.
1267  * @return {boolean}
1268  *          True, if info was updated without any errors.
1269  *          False, if NS_ERROR_MALFORMED_URI error.
1270  * @throws If a non-NS_ERROR_MALFORMED_URI error occurs.
1271  */
1272 function updateHostAndScheme(info, newHost) {
1273   let oldHost = info.fixedURI.host;
1274   let oldScheme = info.fixedURI.scheme;
1275   try {
1276     info.fixedURI = info.fixedURI
1277       .mutate()
1278       .setScheme(lazy.alternateProtocol)
1279       .setHost(newHost)
1280       .finalize();
1281   } catch (ex) {
1282     if (ex.result != Cr.NS_ERROR_MALFORMED_URI) {
1283       throw ex;
1284     }
1285     return false;
1286   }
1287   if (oldScheme != info.fixedURI.scheme) {
1288     info.fixupChangedProtocol = true;
1289   }
1290   if (oldHost != info.fixedURI.host) {
1291     info.fixupCreatedAlternateURI = true;
1292   }
1293   return true;