| // Copyright 2014 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| |
| #include "components/omnibox/browser/autocomplete_input.h" |
| |
| #include <string_view> |
| #include <vector> |
| |
| #include "base/i18n/case_conversion.h" |
| #include "base/logging.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_split.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/trace_event/memory_usage_estimator.h" |
| #include "base/trace_event/typed_macros.h" |
| #include "build/build_config.h" |
| #include "components/omnibox/browser/autocomplete_scheme_classifier.h" |
| #include "components/search_engines/template_url.h" |
| #include "components/search_engines/template_url_service.h" |
| #include "components/search_engines/template_url_starter_pack_data.h" |
| #include "components/url_formatter/url_fixer.h" |
| #include "components/url_formatter/url_formatter.h" |
| #include "net/base/registry_controlled_domains/registry_controlled_domain.h" |
| #include "net/base/url_util.h" |
| #include "third_party/metrics_proto/omnibox_event.pb.h" |
| #include "third_party/re2/src/re2/re2.h" |
| #include "url/url_canon_ip.h" |
| #include "url/url_util.h" |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| #include "chromeos/constants/url_constants.h" // nogncheck |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| |
| namespace { |
| |
| // Hardcode constant to avoid any dependencies on content/. |
| const char kViewSourceScheme[] = "view-source"; |
| |
| void AdjustCursorPositionIfNecessary(size_t num_leading_chars_removed, |
| size_t* cursor_position) { |
| if (*cursor_position == std::u16string::npos) |
| return; |
| if (num_leading_chars_removed < *cursor_position) |
| *cursor_position -= num_leading_chars_removed; |
| else |
| *cursor_position = 0; |
| } |
| |
| // Finds all terms in |text| that start with http:// or https:// plus at least |
| // one more character and puts the text after the prefix in |
| // |terms_prefixed_by_http_or_https|. |
| void PopulateTermsPrefixedByHttpOrHttps( |
| const std::u16string& text, |
| std::vector<std::u16string>* terms_prefixed_by_http_or_https) { |
| // Split on whitespace rather than use ICU's word iterator because, for |
| // example, ICU's iterator may break on punctuation (such as ://) or decide |
| // to split a single term in a hostname (if it seems to think that the |
| // hostname is multiple words). Neither of these behaviors is desirable. |
| const std::string separator(url::kStandardSchemeSeparator); |
| for (const auto& term : base::SplitString(text, u" ", base::TRIM_WHITESPACE, |
| base::SPLIT_WANT_ALL)) { |
| const std::string term_utf8(base::UTF16ToUTF8(term)); |
| static const char* kSchemes[2] = {url::kHttpScheme, url::kHttpsScheme}; |
| for (const char* scheme : kSchemes) { |
| const std::string prefix(scheme + separator); |
| // Doing an ASCII comparison is okay because prefix is ASCII. |
| if (base::StartsWith(term_utf8, prefix, |
| base::CompareCase::INSENSITIVE_ASCII) && |
| (term_utf8.length() > prefix.length())) { |
| terms_prefixed_by_http_or_https->push_back( |
| term.substr(prefix.length())); |
| } |
| } |
| } |
| } |
| |
| // Offsets |parts| of a URL after the scheme by |offset| amount. |
| void OffsetComponentsExcludingScheme(url::Parsed* parts, int offset) { |
| url::Component* components[] = { |
| &parts->username, &parts->password, &parts->host, &parts->port, |
| &parts->path, &parts->query, &parts->ref, |
| }; |
| for (url::Component* component : components) { |
| url_formatter::OffsetComponent(offset, component); |
| } |
| } |
| |
| bool HasScheme(const std::u16string& input, const char* scheme) { |
| std::string utf8_input(base::UTF16ToUTF8(input)); |
| url::Component view_source_scheme; |
| if (url::FindAndCompareScheme(utf8_input, kViewSourceScheme, |
| &view_source_scheme)) { |
| utf8_input.erase(0, view_source_scheme.end() + 1); |
| } |
| return url::FindAndCompareScheme(utf8_input, scheme, nullptr); |
| } |
| |
| } // namespace |
| |
| AutocompleteInput::AutocompleteInput() |
| : cursor_position_(std::u16string::npos), |
| current_page_classification_(metrics::OmniboxEventProto::INVALID_SPEC), |
| type_(metrics::OmniboxInputType::EMPTY), |
| prevent_inline_autocomplete_(false), |
| prefer_keyword_(false), |
| allow_exact_keyword_match_(true), |
| keyword_mode_entry_method_(metrics::OmniboxEventProto::INVALID), |
| omit_asynchronous_matches_(false), |
| should_use_https_as_default_scheme_(false), |
| added_default_scheme_to_typed_url_(false), |
| https_port_for_testing_(0), |
| use_fake_https_for_https_upgrade_testing_(false) {} |
| |
| AutocompleteInput::AutocompleteInput( |
| const std::u16string& text, |
| metrics::OmniboxEventProto::PageClassification current_page_classification, |
| const AutocompleteSchemeClassifier& scheme_classifier, |
| bool should_use_https_as_default_scheme, |
| int https_port_for_testing, |
| bool use_fake_https_for_https_upgrade_testing) |
| : AutocompleteInput(text, |
| std::string::npos, |
| current_page_classification, |
| scheme_classifier, |
| should_use_https_as_default_scheme, |
| https_port_for_testing, |
| use_fake_https_for_https_upgrade_testing) {} |
| |
| AutocompleteInput::AutocompleteInput( |
| const std::u16string& text, |
| size_t cursor_position, |
| metrics::OmniboxEventProto::PageClassification current_page_classification, |
| const AutocompleteSchemeClassifier& scheme_classifier, |
| bool should_use_https_as_default_scheme, |
| int https_port_for_testing, |
| bool use_fake_https_for_https_upgrade_testing) |
| : AutocompleteInput(text, |
| cursor_position, |
| "", |
| current_page_classification, |
| scheme_classifier, |
| should_use_https_as_default_scheme, |
| https_port_for_testing, |
| use_fake_https_for_https_upgrade_testing) {} |
| |
| AutocompleteInput::AutocompleteInput( |
| const std::u16string& text, |
| size_t cursor_position, |
| const std::string& desired_tld, |
| metrics::OmniboxEventProto::PageClassification current_page_classification, |
| const AutocompleteSchemeClassifier& scheme_classifier, |
| bool should_use_https_as_default_scheme, |
| int https_port_for_testing, |
| bool use_fake_https_for_https_upgrade_testing) |
| : AutocompleteInput() { |
| cursor_position_ = cursor_position; |
| current_page_classification_ = current_page_classification; |
| desired_tld_ = desired_tld; |
| should_use_https_as_default_scheme_ = should_use_https_as_default_scheme; |
| https_port_for_testing_ = https_port_for_testing; |
| use_fake_https_for_https_upgrade_testing_ = |
| use_fake_https_for_https_upgrade_testing; |
| Init(text, scheme_classifier); |
| } |
| |
| void AutocompleteInput::Init( |
| const std::u16string& text, |
| const AutocompleteSchemeClassifier& scheme_classifier) { |
| DCHECK(cursor_position_ <= text.length() || |
| cursor_position_ == std::u16string::npos) |
| << "Text: '" << text << "', cp: " << cursor_position_; |
| // None of the providers care about leading white space so we always trim it. |
| // Providers that care about trailing white space handle trimming themselves. |
| if ((base::TrimWhitespace(text, base::TRIM_LEADING, &text_) & |
| base::TRIM_LEADING) != 0) |
| AdjustCursorPositionIfNecessary(text.length() - text_.length(), |
| &cursor_position_); |
| |
| GURL canonicalized_url; |
| type_ = Parse(text_, desired_tld_, scheme_classifier, &parts_, &scheme_, |
| &canonicalized_url); |
| PopulateTermsPrefixedByHttpOrHttps(text_, &terms_prefixed_by_http_or_https_); |
| |
| DCHECK(!added_default_scheme_to_typed_url_); |
| typed_url_had_http_scheme_ = |
| base::StartsWith(text, |
| base::ASCIIToUTF16(base::StrCat( |
| {url::kHttpScheme, url::kStandardSchemeSeparator})), |
| base::CompareCase::INSENSITIVE_ASCII) && |
| canonicalized_url.SchemeIs(url::kHttpScheme); |
| GURL upgraded_url; |
| if (should_use_https_as_default_scheme_ && |
| type_ == metrics::OmniboxInputType::URL && |
| ShouldUpgradeToHttps(text, canonicalized_url, https_port_for_testing_, |
| use_fake_https_for_https_upgrade_testing_, |
| &upgraded_url)) { |
| DCHECK(upgraded_url.is_valid()); |
| added_default_scheme_to_typed_url_ = true; |
| scheme_ = std::u16string(url::kHttpsScheme16); |
| canonicalized_url = upgraded_url; |
| // We changed the scheme from http to https. Offset remaining components |
| // by one. |
| OffsetComponentsExcludingScheme(&parts_, 1); |
| } |
| |
| if (((type_ == metrics::OmniboxInputType::UNKNOWN) || |
| (type_ == metrics::OmniboxInputType::URL)) && |
| canonicalized_url.is_valid() && |
| (!canonicalized_url.IsStandard() || canonicalized_url.SchemeIsFile() || |
| canonicalized_url.SchemeIsFileSystem() || |
| !canonicalized_url.host().empty())) |
| canonicalized_url_ = canonicalized_url; |
| } |
| |
| AutocompleteInput::AutocompleteInput(const AutocompleteInput& other) = default; |
| |
| AutocompleteInput::~AutocompleteInput() = default; |
| |
| // static |
| std::string AutocompleteInput::TypeToString(metrics::OmniboxInputType type) { |
| switch (type) { |
| case metrics::OmniboxInputType::EMPTY: |
| return "invalid"; |
| case metrics::OmniboxInputType::UNKNOWN: |
| return "unknown"; |
| case metrics::OmniboxInputType::DEPRECATED_REQUESTED_URL: |
| return "deprecated-requested-url"; |
| case metrics::OmniboxInputType::URL: |
| return "url"; |
| case metrics::OmniboxInputType::QUERY: |
| return "query"; |
| case metrics::OmniboxInputType::DEPRECATED_FORCED_QUERY: |
| return "deprecated-forced-query"; |
| } |
| return std::string(); |
| } |
| |
| // static |
| metrics::OmniboxInputType AutocompleteInput::Parse( |
| const std::u16string& text, |
| const std::string& desired_tld, |
| const AutocompleteSchemeClassifier& scheme_classifier, |
| url::Parsed* parts, |
| std::u16string* scheme, |
| GURL* canonicalized_url) { |
| size_t first_non_white = text.find_first_not_of(base::kWhitespaceUTF16, 0); |
| if (first_non_white == std::u16string::npos) |
| return metrics::OmniboxInputType::EMPTY; // All whitespace. |
| |
| // Ask our parsing back-end to help us understand what the user typed. We |
| // use the URLFixerUpper here because we want to be smart about what we |
| // consider a scheme. For example, we shouldn't consider www.google.com:80 |
| // to have a scheme. |
| url::Parsed local_parts; |
| if (!parts) |
| parts = &local_parts; |
| const std::u16string parsed_scheme(url_formatter::SegmentURL(text, parts)); |
| if (scheme) |
| *scheme = parsed_scheme; |
| const std::string parsed_scheme_utf8(base::UTF16ToUTF8(parsed_scheme)); |
| DCHECK(base::IsStringASCII(parsed_scheme_utf8)); |
| |
| // If we can't canonicalize the user's input, the rest of the autocomplete |
| // system isn't going to be able to produce a navigable URL match for it. |
| // So we just return QUERY immediately in these cases. |
| GURL placeholder_canonicalized_url; |
| if (!canonicalized_url) |
| canonicalized_url = &placeholder_canonicalized_url; |
| *canonicalized_url = |
| url_formatter::FixupURL(base::UTF16ToUTF8(text), desired_tld); |
| if (!canonicalized_url->is_valid()) |
| return metrics::OmniboxInputType::QUERY; |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| if (base::EqualsCaseInsensitiveASCII(parsed_scheme_utf8, |
| chromeos::kAppInstallUriScheme)) { |
| return metrics::OmniboxInputType::URL; |
| } |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| |
| if (base::EqualsCaseInsensitiveASCII(parsed_scheme_utf8, url::kFileScheme)) { |
| // A user might or might not type a scheme when entering a file URL. In |
| // either case, |parsed_scheme_utf8| will tell us that this is a file URL, |
| // but |parts->scheme| might be empty, e.g. if the user typed "C:\foo". |
| |
| #if (BUILDFLAG(IS_IOS) || BUILDFLAG(IS_ANDROID)) |
| // On iOS and Android, which cannot display file:/// URLs, treat this case |
| // like a query. |
| return metrics::OmniboxInputType::QUERY; |
| #else |
| return metrics::OmniboxInputType::URL; |
| #endif // BUILDFLAG(IS_IOS) |
| } |
| |
| // Treat javascript: scheme queries followed by things that are unlikely to |
| // be code as UNKNOWN, rather than script to execute (URL). |
| if (base::EqualsCaseInsensitiveASCII(parsed_scheme_utf8, |
| url::kJavaScriptScheme) && |
| RE2::FullMatch(base::UTF16ToUTF8(text), "(?i)javascript:([^;=().\"]*)")) { |
| return metrics::OmniboxInputType::UNKNOWN; |
| } |
| |
| // If the user typed a scheme, and it's HTTP or HTTPS, we know how to parse it |
| // well enough that we can fall through to the heuristics below. If it's |
| // something else, we can just determine our action based on what we do with |
| // any input of this scheme. In theory we could do better with some schemes |
| // (e.g. "ftp" or "view-source") but I'll wait to spend the effort on that |
| // until I run into some cases that really need it. |
| if (parts->scheme.is_nonempty() && |
| !base::EqualsCaseInsensitiveASCII(parsed_scheme_utf8, url::kHttpScheme) && |
| !base::EqualsCaseInsensitiveASCII(parsed_scheme_utf8, |
| url::kHttpsScheme)) { |
| metrics::OmniboxInputType type = |
| scheme_classifier.GetInputTypeForScheme(parsed_scheme_utf8); |
| if (type != metrics::OmniboxInputType::EMPTY) |
| return type; |
| |
| // We don't know about this scheme. It might be that the user typed a |
| // URL of the form "username:password@foo.com", or a custom query, such as |
| // "site:socialmedia.com @tagname". |
| const std::u16string http_scheme_prefix = base::ASCIIToUTF16( |
| std::string(url::kHttpScheme) + url::kStandardSchemeSeparator); |
| const std::u16string tentative_url_candidate = http_scheme_prefix + text; |
| url::Parsed http_parts; |
| std::u16string http_scheme; |
| GURL http_canonicalized_url; |
| metrics::OmniboxInputType http_type = |
| Parse(tentative_url_candidate, desired_tld, scheme_classifier, |
| &http_parts, &http_scheme, &http_canonicalized_url); |
| DCHECK_EQ(std::string(url::kHttpScheme), base::UTF16ToUTF8(http_scheme)); |
| |
| if ((http_type == metrics::OmniboxInputType::URL) && |
| http_parts.username.is_nonempty() && |
| http_parts.password.is_nonempty()) { |
| // Recognize and re-classify queries like: `site:web.com @query` |
| auto tentative_password_sv = http_parts.password.as_string_view_on( |
| tentative_url_candidate.c_str()); |
| if (tentative_password_sv.find(u' ') != tentative_password_sv.npos) { |
| *canonicalized_url = GURL::EmptyGURL(); |
| return metrics::OmniboxInputType::QUERY; |
| } |
| |
| // Manually re-jigger the parsed parts to match |text| (without the |
| // http scheme added). |
| http_parts.scheme.reset(); |
| OffsetComponentsExcludingScheme( |
| &http_parts, -static_cast<int>(http_scheme_prefix.length())); |
| |
| *parts = http_parts; |
| if (scheme) |
| scheme->clear(); |
| *canonicalized_url = http_canonicalized_url; |
| |
| return metrics::OmniboxInputType::URL; |
| } |
| |
| // We don't know about this scheme and it doesn't look like the user |
| // typed a username and password. It's likely to be a search operator |
| // like "site:" or "link:". We classify it as UNKNOWN so the user has |
| // the option of treating it as a URL if we're wrong. |
| // Note that SegmentURL() is smart so we aren't tricked by "c:\foo" or |
| // "www.example.com:81" in this case. |
| return metrics::OmniboxInputType::UNKNOWN; |
| } |
| |
| // Either the user didn't type a scheme, in which case we need to distinguish |
| // between an HTTP URL and a query, or the scheme is HTTP or HTTPS, in which |
| // case we should reject invalid formulations. |
| |
| // Determine the host family. We get this information by (re-)canonicalizing |
| // the already-canonicalized host rather than using the user's original input, |
| // in case fixup affected the result here (e.g. an input that looks like an |
| // IPv4 address but with a non-empty desired TLD would return IPV4 before |
| // fixup and NEUTRAL afterwards, and we want to treat it as NEUTRAL). |
| url::CanonHostInfo host_info; |
| net::CanonicalizeHost(canonicalized_url->host(), &host_info); |
| |
| // Check if the canonicalized host has a known TLD, which we'll want to know |
| // below. |
| const size_t registry_length = |
| net::registry_controlled_domains::GetCanonicalHostRegistryLength( |
| canonicalized_url->host(), |
| net::registry_controlled_domains::EXCLUDE_UNKNOWN_REGISTRIES, |
| net::registry_controlled_domains::EXCLUDE_PRIVATE_REGISTRIES); |
| DCHECK_NE(std::string::npos, registry_length); |
| const bool has_known_tld = registry_length != 0; |
| |
| // See if the hostname is valid. While IE and GURL allow hostnames to contain |
| // many other characters (perhaps for weird intranet machines), it's extremely |
| // unlikely that a user would be trying to type those in for anything other |
| // than a search query. |
| // |
| // Per https://tools.ietf.org/html/rfc6761, the .invalid TLD is considered |
| // non-navigable and thus is treated like a non-compliant hostname. (Though |
| // just the word "invalid" is not a hostname). |
| const std::u16string original_host( |
| text.substr(parts->host.begin, parts->host.len)); |
| if (text != u"invalid" && (host_info.family == url::CanonHostInfo::NEUTRAL) && |
| (!net::IsCanonicalizedHostCompliant(canonicalized_url->host()) || |
| canonicalized_url->DomainIs("invalid"))) { |
| // Invalid hostname. There are several possible cases: |
| // * The user is typing a multi-word query. If we see a space anywhere in |
| // the input host we assume this is a search and return QUERY. (We check |
| // the input string instead of canonicalized_url->host() in case fixup |
| // escaped the space.) |
| // * The user is typing some garbage string. Return QUERY. |
| // * Our checker is too strict and the user is typing a real-world URL |
| // that's "invalid" but resolves. To catch these, we return UNKNOWN when |
| // the user explicitly typed a scheme or when the hostname has a known |
| // TLD, so we'll still search by default but we'll show the accidental |
| // search infobar if necessary. |
| // |
| // This means we would block the following kinds of navigation attempts: |
| // * Navigations to a hostname with spaces |
| // * Navigations to a hostname with invalid characters and an unknown TLD |
| // These might be possible in intranets, but we're not going to support them |
| // without concrete evidence that doing so is necessary. |
| return (parts->scheme.is_nonempty() || |
| (has_known_tld && |
| (original_host.find(' ') == std::u16string::npos))) |
| ? metrics::OmniboxInputType::UNKNOWN |
| : metrics::OmniboxInputType::QUERY; |
| } |
| |
| // For hostnames that look like IP addresses, distinguish between IPv6 |
| // addresses, which are basically guaranteed to be navigations, and IPv4 |
| // addresses, which are much fuzzier. |
| if (host_info.family == url::CanonHostInfo::IPV6) |
| return metrics::OmniboxInputType::URL; |
| if (host_info.family == url::CanonHostInfo::IPV4) { |
| // The host may be a real IP address, or something that looks a bit like it |
| // (e.g. "1.2" or "3232235521"). We check whether it was convertible to an |
| // IP with a non-zero first octet; IPs with first octet zero are "source |
| // IPs" and are almost never navigable as destination addresses. |
| // |
| // The one exception to this is 0.0.0.0; on many systems, attempting to |
| // navigate to this IP actually navigates to localhost. To support this |
| // case, when the converted IP is 0.0.0.0, we go ahead and run the "did the |
| // user actually type four components" test in the conditional below, so |
| // that we'll allow explicit attempts to navigate to "0.0.0.0". If the |
| // input was anything else (e.g. "0"), we'll fall through to returning QUERY |
| // afterwards. |
| if ((host_info.address[0] != 0) || |
| ((host_info.address[1] == 0) && (host_info.address[2] == 0) && |
| (host_info.address[3] == 0))) { |
| // This is theoretically a navigable IP. We have four cases. The first |
| // three are: |
| // * If the user typed four distinct components, this is an IP for sure. |
| // * If the user typed two or three components, this is almost certainly a |
| // query, especially for two components (as in "13.5/7.25"), but we'll |
| // allow navigation for an explicit scheme or trailing slash below. |
| // * If the user typed one component, this is likely a query, but could be |
| // a non-dotted-quad version of an IP address. |
| // Unfortunately, since we called CanonicalizeHost() on the |
| // already-canonicalized host, all of these cases will have been changed |
| // to have four components (e.g. 13.2 -> 13.0.0.2), so we have to call |
| // CanonicalizeHost() again, this time on the original input, so that we |
| // can get the correct number of IP components. |
| // |
| // The fourth case is that the user typed something ambiguous like ".1.2" |
| // that fixup converted to an IP address ("1.0.0.2"). In this case the |
| // call to CanonicalizeHost() will return NEUTRAL here. Since it's not |
| // clear what the user intended, we fall back to our other heuristics. |
| net::CanonicalizeHost(base::UTF16ToUTF8(original_host), &host_info); |
| if ((host_info.family == url::CanonHostInfo::IPV4) && |
| (host_info.num_ipv4_components == 4)) |
| return metrics::OmniboxInputType::URL; |
| } |
| |
| // By this point, if we have an "IP" with first octet zero, we know it |
| // wasn't "0.0.0.0", so mark it as non-navigable. |
| if (host_info.address[0] == 0) |
| return metrics::OmniboxInputType::QUERY; |
| } |
| |
| // Now that we've ruled out all schemes other than http or https and done a |
| // little more sanity checking, the presence of a scheme means this is likely |
| // a URL. |
| if (parts->scheme.is_nonempty()) |
| return metrics::OmniboxInputType::URL; |
| |
| // Check to see if the username is set and, if so, whether it contains a |
| // space. Usernames usually do not contain a space. If a username contains |
| // a space, that's likely an indication of incorrectly parsing of the input. |
| const bool username_has_space = |
| parts->username.is_nonempty() && |
| (text.substr(parts->username.begin, parts->username.len) |
| .find_first_of(base::kWhitespaceUTF16) != std::u16string::npos); |
| |
| // Generally, trailing slashes force the input to be treated as a URL. |
| // However, if the username has a space, this may be input like |
| // "dep missing: @test/", which should not be parsed as a URL (with the |
| // username "dep missing: "). |
| if (parts->path.is_nonempty() && !username_has_space) { |
| char16_t c = text[parts->path.end() - 1]; |
| if ((c == '\\') || (c == '/')) |
| return metrics::OmniboxInputType::URL; |
| } |
| |
| // Handle the cases we detected in the IPv4 code above as "almost certainly a |
| // query" now that we know the user hasn't tried to force navigation via a |
| // scheme/trailing slash. |
| if ((host_info.family == url::CanonHostInfo::IPV4) && |
| (host_info.num_ipv4_components > 1)) |
| return metrics::OmniboxInputType::QUERY; |
| |
| // The URL did not have an explicit scheme and has an unusual-looking |
| // username (with a space). It's not likely to be a URL. |
| if (username_has_space) |
| return metrics::OmniboxInputType::UNKNOWN; |
| |
| // If there is more than one recognized non-host component, this is likely to |
| // be a URL, even if the TLD is unknown (in which case this is likely an |
| // intranet URL). |
| if (NumNonHostComponents(*parts) > 1) |
| return metrics::OmniboxInputType::URL; |
| |
| // If we reach here with a username, our input looks something like |
| // "user@host". Unless there is a desired TLD, we think this is more likely |
| // an email address than an HTTP auth attempt, so we search by default. (When |
| // there _is_ a desired TLD, the user hit ctrl-enter, and we assume that |
| // implies an attempted navigation.) |
| if (canonicalized_url->has_username() && desired_tld.empty()) |
| return metrics::OmniboxInputType::UNKNOWN; |
| |
| // If the host has a known TLD or a port, it's probably a URL. Just localhost |
| // is considered a valid host name due to https://tools.ietf.org/html/rfc6761. |
| if (has_known_tld || canonicalized_url->DomainIs("localhost") || |
| canonicalized_url->has_port()) |
| return metrics::OmniboxInputType::URL; |
| |
| // The .example and .test TLDs are special-cased as known TLDs due to |
| // https://tools.ietf.org/html/rfc6761. Unlike localhost, these are not valid |
| // host names, so they must have at least one subdomain to be a URL. |
| // .local is used for Multicast DNS in https://www.rfc-editor.org/rfc/rfc6762. |
| for (const std::string_view domain : {"example", "test", "local"}) { |
| // The +1 accounts for a possible trailing period. |
| if (canonicalized_url->DomainIs(domain) && |
| (canonicalized_url->host().length() > (domain.length() + 1))) |
| return metrics::OmniboxInputType::URL; |
| } |
| |
| // No scheme, username, port, and no known TLD on the host. |
| // This could be: |
| // * A single word "foo"; possibly an intranet site, but more likely a search. |
| // This is ideally an UNKNOWN, and we can let the Alternate Nav URL code |
| // catch our mistakes. |
| // * A URL with a valid TLD we don't know about yet. If e.g. a registrar adds |
| // "xxx" as a TLD, then until we add it to our data file, Chrome won't know |
| // "foo.xxx" is a real URL. So ideally this is a URL, but we can't really |
| // distinguish this case from: |
| // * A "URL-like" string that's not really a URL (like |
| // "browser.tabs.closeButtons" or "java.awt.event.*"). This is ideally a |
| // QUERY. Since this is indistinguishable from the case above, and this |
| // case is much more likely, claim these are UNKNOWN, which should default |
| // to the right thing and let users correct us on a case-by-case basis. |
| return metrics::OmniboxInputType::UNKNOWN; |
| } |
| |
| // static |
| void AutocompleteInput::ParseForEmphasizeComponents( |
| const std::u16string& text, |
| const AutocompleteSchemeClassifier& scheme_classifier, |
| url::Component* scheme, |
| url::Component* host) { |
| url::Parsed parts; |
| std::u16string scheme_str; |
| Parse(text, std::string(), scheme_classifier, &parts, &scheme_str, nullptr); |
| |
| *scheme = parts.scheme; |
| *host = parts.host; |
| |
| int after_scheme_and_colon = parts.scheme.end() + 1; |
| // For the view-source and blob schemes, we should emphasize the host of the |
| // URL qualified by the view-source or blob prefix. |
| if ((base::EqualsCaseInsensitiveASCII(scheme_str, kViewSourceScheme) || |
| base::EqualsCaseInsensitiveASCII(scheme_str, url::kBlobScheme)) && |
| (static_cast<int>(text.length()) > after_scheme_and_colon)) { |
| // Obtain the URL prefixed by view-source or blob and parse it. |
| std::u16string real_url(text.substr(after_scheme_and_colon)); |
| url::Parsed real_parts; |
| AutocompleteInput::Parse(real_url, std::string(), scheme_classifier, |
| &real_parts, nullptr, nullptr); |
| if (real_parts.scheme.is_nonempty() || real_parts.host.is_nonempty()) { |
| if (real_parts.scheme.is_nonempty()) { |
| *scheme = |
| url::Component(after_scheme_and_colon + real_parts.scheme.begin, |
| real_parts.scheme.len); |
| } else { |
| scheme->reset(); |
| } |
| if (real_parts.host.is_nonempty()) { |
| *host = url::Component(after_scheme_and_colon + real_parts.host.begin, |
| real_parts.host.len); |
| } else { |
| host->reset(); |
| } |
| } |
| } else if (base::EqualsCaseInsensitiveASCII(scheme_str, |
| url::kFileSystemScheme) && |
| parts.inner_parsed() && parts.inner_parsed()->scheme.is_valid()) { |
| *host = parts.inner_parsed()->host; |
| } |
| } |
| |
| // static |
| bool AutocompleteInput::ShouldUpgradeToHttps( |
| const std::u16string& text, |
| const GURL& url, |
| int https_port_for_testing, |
| bool use_fake_https_for_https_upgrade_testing, |
| GURL* upgraded_url) { |
| if (url::HostIsIPAddress(url.host()) || |
| net::IsHostnameNonUnique(url.host())) { |
| #if !BUILDFLAG(IS_IOS) |
| // Never upgrade IP addresses or non-unique hostnames on non-iOS builds. |
| return false; |
| #else |
| // On iOS, tests use a loopback IP address instead of hostnames due to |
| // platform limitations. Only allow them when running tests. |
| if (!https_port_for_testing || !url::HostIsIPAddress(url.host())) { |
| return false; |
| } |
| #endif |
| } |
| |
| if (url.scheme() == url::kHttpScheme && |
| !base::StartsWith(text, base::ASCIIToUTF16(url.scheme()), |
| base::CompareCase::INSENSITIVE_ASCII) && |
| (url.port().empty() || https_port_for_testing)) { |
| // Use HTTPS as the default scheme for URLs that are typed without a scheme. |
| // Inputs of type UNKNOWN can still be valid URLs, but these will be mainly |
| // intranet hosts which we don't to upgrade to HTTPS so we only check the |
| // URL type here. |
| // In particular, we don't want to upgrade these types of inputs: |
| // - Non-unique hostnames such as intranet hosts |
| // - Single word hostnames (these are most likely non-unique). |
| // - IP addresses |
| // - URLs with a specified port. If it's a non-standard HTTP port, we can't |
| // simply change the scheme to HTTPS and assume that these will load over |
| // HTTPS. URLs with HTTP port 80 get their port dropped so they will be |
| // upgraded (e.g. example.com:80 will load https://example.com). |
| DCHECK_EQ(url::kHttpScheme, url.scheme()); |
| GURL::Replacements replacements; |
| #if !BUILDFLAG(IS_IOS) |
| // We sometimes use a fake HTTPS server on iOS as we can't serve good HTTPS |
| // from a test server. On all other platforms, we never use fake HTTPS |
| // server. |
| DCHECK(!use_fake_https_for_https_upgrade_testing); |
| #else |
| // On iOS, use_fake_https_for_https_upgrade_testing should only be true if |
| // https_port_for_testing is also true. |
| DCHECK(!use_fake_https_for_https_upgrade_testing || https_port_for_testing); |
| #endif |
| |
| if (!use_fake_https_for_https_upgrade_testing) { |
| replacements.SetSchemeStr(url::kHttpsScheme); |
| } |
| // This needs to be in scope when ReplaceComponents() is called: |
| const std::string port_str = base::NumberToString(https_port_for_testing); |
| if (https_port_for_testing) { |
| // We'll only get here in tests. |
| #if BUILDFLAG(IS_IOS) |
| if (url.port().empty()) { |
| // On iOS, if the URL doesn't have a port, this is probably an |
| // incomplete URL that's still being typed. Ignore. |
| return false; |
| } |
| #else |
| // On other platforms, tests should always have a non-default port on the |
| // input text. |
| DCHECK(!url.port().empty()); |
| #endif |
| replacements.SetPortStr(port_str); |
| } |
| *upgraded_url = url.ReplaceComponents(replacements); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| // static |
| std::u16string AutocompleteInput::FormattedStringWithEquivalentMeaning( |
| const GURL& url, |
| const std::u16string& formatted_url, |
| const AutocompleteSchemeClassifier& scheme_classifier, |
| size_t* offset) { |
| if (!url_formatter::CanStripTrailingSlash(url)) |
| return formatted_url; |
| const std::u16string url_with_path(formatted_url + u'/'); |
| if (AutocompleteInput::Parse(formatted_url, std::string(), scheme_classifier, |
| nullptr, nullptr, nullptr) == |
| AutocompleteInput::Parse(url_with_path, std::string(), scheme_classifier, |
| nullptr, nullptr, nullptr)) { |
| return formatted_url; |
| } |
| // If offset is past the addition, shift it. |
| if (offset && *offset == formatted_url.size()) |
| ++(*offset); |
| return url_with_path; |
| } |
| |
| // static |
| int AutocompleteInput::NumNonHostComponents(const url::Parsed& parts) { |
| int num_nonhost_components = 0; |
| if (parts.scheme.is_nonempty()) |
| ++num_nonhost_components; |
| if (parts.username.is_nonempty()) |
| ++num_nonhost_components; |
| if (parts.password.is_nonempty()) |
| ++num_nonhost_components; |
| if (parts.port.is_nonempty()) |
| ++num_nonhost_components; |
| if (parts.path.is_nonempty()) |
| ++num_nonhost_components; |
| if (parts.query.is_nonempty()) |
| ++num_nonhost_components; |
| if (parts.ref.is_nonempty()) |
| ++num_nonhost_components; |
| return num_nonhost_components; |
| } |
| |
| // static |
| bool AutocompleteInput::HasHTTPScheme(const std::u16string& input) { |
| return HasScheme(input, url::kHttpScheme); |
| } |
| |
| // static |
| bool AutocompleteInput::HasHTTPSScheme(const std::u16string& input) { |
| return HasScheme(input, url::kHttpsScheme); |
| } |
| |
| // static |
| AutocompleteInput::FeaturedKeywordMode |
| AutocompleteInput::GetFeaturedKeywordMode(std::u16string_view text) { |
| if (text == u"@") |
| return FeaturedKeywordMode::kExact; |
| if (text.starts_with(u'@')) |
| return FeaturedKeywordMode::kPrefix; |
| return FeaturedKeywordMode::kFalse; |
| } |
| |
| // static |
| const TemplateURL* AutocompleteInput::AdjustInputForStarterPackEngines( |
| TemplateURLService* model, |
| AutocompleteInput* input) { |
| DCHECK(model); |
| |
| // If not in keyword mode, then `input` is definitely not in a starter pack |
| // scope, so early exit. |
| if (!input->prefer_keyword()) { |
| return nullptr; |
| } |
| |
| // If in a starter pack scope, should run the provider with only |
| // the user text AFTER the keyword. E.g. if the input is "@history text", |
| // set the autocomplete input to just "text". |
| const TemplateURL* template_url = |
| AutocompleteInput::GetSubstitutingTemplateURLForInput(model, input); |
| if (template_url && template_url->starter_pack_id() > 0) { |
| return template_url; |
| } |
| |
| return nullptr; |
| } |
| |
| // static |
| const TemplateURL* AutocompleteInput::GetSubstitutingTemplateURLForInput( |
| const TemplateURLService* model, |
| AutocompleteInput* input) { |
| if (!input->allow_exact_keyword_match()) { |
| return nullptr; |
| } |
| |
| DCHECK(model); |
| std::u16string keyword, remaining_input; |
| if (!ExtractKeywordFromInput(*input, model, &keyword, &remaining_input)) { |
| return nullptr; |
| } |
| |
| const TemplateURL* template_url = model->GetTemplateURLForKeyword(keyword); |
| if (template_url && |
| template_url->SupportsReplacement(model->search_terms_data())) { |
| // Adjust cursor position iff it was set before, otherwise leave it as is. |
| size_t cursor_position = std::u16string::npos; |
| // The adjustment assumes that the keyword was stripped from the beginning |
| // of the original input. |
| if (input->cursor_position() != std::u16string::npos && |
| !remaining_input.empty() && |
| base::EndsWith(input->text(), remaining_input, |
| base::CompareCase::SENSITIVE)) { |
| int offset = input->text().length() - input->cursor_position(); |
| // The cursor should never be past the last character or before the |
| // first character. |
| DCHECK_GE(offset, 0); |
| DCHECK_LE(offset, static_cast<int>(input->text().length())); |
| if (offset <= 0) { |
| // Normalize the cursor to be exactly after the last character. |
| cursor_position = remaining_input.length(); |
| } else { |
| // If somehow the cursor was before the remaining text, set it to 0, |
| // otherwise adjust it relative to the remaining text. |
| cursor_position = offset > static_cast<int>(remaining_input.length()) |
| ? 0u |
| : remaining_input.length() - offset; |
| } |
| } |
| input->UpdateText(remaining_input, cursor_position, input->parts()); |
| return template_url; |
| } |
| |
| return nullptr; |
| } |
| |
| // static |
| bool AutocompleteInput::ExtractKeywordFromInput( |
| const AutocompleteInput& input, |
| const TemplateURLService* template_url_service, |
| std::u16string* keyword, |
| std::u16string* remaining_input) { |
| if ((input.type() == metrics::OmniboxInputType::EMPTY)) { |
| return false; |
| } |
| |
| DCHECK(template_url_service); |
| *keyword = CleanUserInputKeyword( |
| template_url_service, |
| SplitKeywordFromInput(input.text(), true, remaining_input)); |
| return !keyword->empty(); |
| } |
| |
| // static |
| std::u16string AutocompleteInput::SplitReplacementStringFromInput( |
| const std::u16string& input, |
| bool trim_leading_whitespace) { |
| // The input may contain leading whitespace, strip it. |
| std::u16string trimmed_input; |
| base::TrimWhitespace(input, base::TRIM_LEADING, &trimmed_input); |
| |
| // And extract the replacement string. |
| std::u16string remaining_input; |
| SplitKeywordFromInput(trimmed_input, trim_leading_whitespace, |
| &remaining_input); |
| return remaining_input; |
| } |
| |
| // static |
| std::u16string AutocompleteInput::CleanUserInputKeyword( |
| const TemplateURLService* template_url_service, |
| const std::u16string& keyword) { |
| DCHECK(template_url_service); |
| std::u16string result(base::i18n::ToLower(keyword)); |
| base::TrimWhitespace(result, base::TRIM_ALL, &result); |
| // If this keyword is found with no additional cleaning of input, return it. |
| if (template_url_service->GetTemplateURLForKeyword(result) != nullptr) { |
| return result; |
| } |
| |
| // If keyword is not found, try removing a "http" or "https" scheme if any. |
| url::Component scheme_component; |
| if (url::ExtractScheme(result.c_str(), static_cast<int>(result.length()), |
| &scheme_component)) { |
| const std::u16string_view scheme = std::u16string_view(result).substr( |
| scheme_component.begin, scheme_component.len); |
| if (scheme == url::kHttpScheme16 || scheme == url::kHttpsScheme16) { |
| // Remove the scheme and the trailing ':'. |
| result.erase(0, scheme_component.end() + 1); |
| if (template_url_service->GetTemplateURLForKeyword(result) != nullptr) { |
| return result; |
| } |
| // Many schemes usually have "//" after them, so strip it too. |
| constexpr std::u16string_view kAfterScheme(u"//"); |
| if (base::StartsWith(result, kAfterScheme)) { |
| result.erase(0, kAfterScheme.length()); |
| } |
| if (template_url_service->GetTemplateURLForKeyword(result) != nullptr) { |
| return result; |
| } |
| } |
| } |
| |
| // Remove leading "www.", if any, and again try to find a matching keyword. |
| // The 'www.' stripping is done directly here instead of calling |
| // url_formatter::StripWWW because we're not assuming that the keyword is a |
| // hostname. |
| constexpr std::u16string_view kWww(u"www."); |
| result = base::StartsWith(result, kWww, base::CompareCase::SENSITIVE) |
| ? result.substr(kWww.length()) |
| : std::move(result); |
| if (template_url_service->GetTemplateURLForKeyword(result) != nullptr) { |
| return result; |
| } |
| |
| // Remove trailing "/", if any. |
| if (!result.empty() && result.back() == '/') { |
| result.pop_back(); |
| } |
| return result; |
| } |
| |
| // static |
| std::u16string AutocompleteInput::AutocompleteInput::SplitKeywordFromInput( |
| const std::u16string& input, |
| bool trim_leading_whitespace, |
| std::u16string* remaining_input) { |
| // Find end of first token. The AutocompleteController has trimmed leading |
| // whitespace, so we need not skip over that. |
| const size_t first_white(input.find_first_of(base::kWhitespaceUTF16)); |
| DCHECK_NE(0U, first_white); |
| if (first_white == std::u16string::npos) { |
| return input; // Only one token provided. |
| } |
| |
| // Set |remaining_input| to everything after the first token. |
| if (remaining_input != nullptr) { |
| const size_t remaining_start = |
| trim_leading_whitespace |
| ? input.find_first_not_of(base::kWhitespaceUTF16, first_white) |
| : first_white + 1; |
| |
| if (remaining_start < input.length()) { |
| remaining_input->assign(input.begin() + remaining_start, input.end()); |
| } |
| } |
| |
| // Return first token as keyword. |
| return input.substr(0, first_white); |
| } |
| |
| void AutocompleteInput::UpdateText(const std::u16string& text, |
| size_t cursor_position, |
| const url::Parsed& parts) { |
| DCHECK(cursor_position <= text.length() || |
| cursor_position == std::u16string::npos) |
| << "Text: '" << text << "', cp: " << cursor_position; |
| text_ = text; |
| cursor_position_ = cursor_position; |
| parts_ = parts; |
| } |
| |
| void AutocompleteInput::Clear() { |
| text_.clear(); |
| cursor_position_ = std::u16string::npos; |
| current_url_ = GURL(); |
| current_title_.clear(); |
| current_page_classification_ = metrics::OmniboxEventProto::INVALID_SPEC; |
| type_ = metrics::OmniboxInputType::EMPTY; |
| parts_ = url::Parsed(); |
| scheme_.clear(); |
| canonicalized_url_ = GURL(); |
| prevent_inline_autocomplete_ = false; |
| prefer_keyword_ = false; |
| allow_exact_keyword_match_ = false; |
| omit_asynchronous_matches_ = false; |
| focus_type_ = metrics::OmniboxFocusType::INTERACTION_DEFAULT; |
| terms_prefixed_by_http_or_https_.clear(); |
| lens_overlay_suggest_inputs_.reset(); |
| https_port_for_testing_ = 0; |
| use_fake_https_for_https_upgrade_testing_ = false; |
| } |
| |
| size_t AutocompleteInput::EstimateMemoryUsage() const { |
| size_t res = 0; |
| |
| res += base::trace_event::EstimateMemoryUsage(text_); |
| res += base::trace_event::EstimateMemoryUsage(current_url_); |
| res += base::trace_event::EstimateMemoryUsage(current_title_); |
| res += base::trace_event::EstimateMemoryUsage(scheme_); |
| res += base::trace_event::EstimateMemoryUsage(canonicalized_url_); |
| res += base::trace_event::EstimateMemoryUsage(desired_tld_); |
| res += |
| base::trace_event::EstimateMemoryUsage(terms_prefixed_by_http_or_https_); |
| |
| return res; |
| } |
| |
| void AutocompleteInput::WriteIntoTrace(perfetto::TracedValue context) const { |
| auto dict = std::move(context).WriteDictionary(); |
| dict.Add("text", text_); |
| } |
| |
| bool AutocompleteInput::IsZeroSuggest() const { |
| return focus_type_ != metrics::OmniboxFocusType::INTERACTION_DEFAULT; |
| } |
| |
| bool AutocompleteInput::InKeywordMode() const { |
| return keyword_mode_entry_method_ != metrics::OmniboxEventProto::INVALID; |
| } |
| |
| AutocompleteInput::FeaturedKeywordMode |
| AutocompleteInput::GetFeaturedKeywordMode() const { |
| return GetFeaturedKeywordMode(text_); |
| } |