1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "components/omnibox/autocomplete_input.h"
7 #include "base/strings/string_util.h"
8 #include "base/strings/utf_string_conversions.h"
9 #include "components/metrics/proto/omnibox_event.pb.h"
10 #include "components/omnibox/autocomplete_scheme_classifier.h"
11 #include "components/url_fixer/url_fixer.h"
12 #include "net/base/net_util.h"
13 #include "net/base/registry_controlled_domains/registry_controlled_domain.h"
14 #include "url/url_canon_ip.h"
15 #include "url/url_util.h"
19 // Hardcode constant to avoid any dependencies on content/.
20 const char kViewSourceScheme
[] = "view-source";
22 void AdjustCursorPositionIfNecessary(size_t num_leading_chars_removed
,
23 size_t* cursor_position
) {
24 if (*cursor_position
== base::string16::npos
)
26 if (num_leading_chars_removed
< *cursor_position
)
27 *cursor_position
-= num_leading_chars_removed
;
34 AutocompleteInput::AutocompleteInput()
35 : cursor_position_(base::string16::npos
),
36 current_page_classification_(metrics::OmniboxEventProto::INVALID_SPEC
),
37 type_(metrics::OmniboxInputType::INVALID
),
38 prevent_inline_autocomplete_(false),
39 prefer_keyword_(false),
40 allow_exact_keyword_match_(true),
41 want_asynchronous_matches_(true) {
44 AutocompleteInput::AutocompleteInput(
45 const base::string16
& text
,
46 size_t cursor_position
,
47 const base::string16
& desired_tld
,
48 const GURL
& current_url
,
49 metrics::OmniboxEventProto::PageClassification current_page_classification
,
50 bool prevent_inline_autocomplete
,
52 bool allow_exact_keyword_match
,
53 bool want_asynchronous_matches
,
54 const AutocompleteSchemeClassifier
& scheme_classifier
)
55 : cursor_position_(cursor_position
),
56 current_url_(current_url
),
57 current_page_classification_(current_page_classification
),
58 prevent_inline_autocomplete_(prevent_inline_autocomplete
),
59 prefer_keyword_(prefer_keyword
),
60 allow_exact_keyword_match_(allow_exact_keyword_match
),
61 want_asynchronous_matches_(want_asynchronous_matches
) {
62 DCHECK(cursor_position
<= text
.length() ||
63 cursor_position
== base::string16::npos
)
64 << "Text: '" << text
<< "', cp: " << cursor_position
;
65 // None of the providers care about leading white space so we always trim it.
66 // Providers that care about trailing white space handle trimming themselves.
67 if ((base::TrimWhitespace(text
, base::TRIM_LEADING
, &text_
) &
68 base::TRIM_LEADING
) != 0)
69 AdjustCursorPositionIfNecessary(text
.length() - text_
.length(),
72 GURL canonicalized_url
;
73 type_
= Parse(text_
, desired_tld
, scheme_classifier
, &parts_
, &scheme_
,
76 if (type_
== metrics::OmniboxInputType::INVALID
)
79 if (((type_
== metrics::OmniboxInputType::UNKNOWN
) ||
80 (type_
== metrics::OmniboxInputType::URL
)) &&
81 canonicalized_url
.is_valid() &&
82 (!canonicalized_url
.IsStandard() || canonicalized_url
.SchemeIsFile() ||
83 canonicalized_url
.SchemeIsFileSystem() ||
84 !canonicalized_url
.host().empty()))
85 canonicalized_url_
= canonicalized_url
;
87 size_t chars_removed
= RemoveForcedQueryStringIfNecessary(type_
, &text_
);
88 AdjustCursorPositionIfNecessary(chars_removed
, &cursor_position_
);
90 // Remove spaces between opening question mark and first actual character.
91 base::string16 trimmed_text
;
92 if ((base::TrimWhitespace(text_
, base::TRIM_LEADING
, &trimmed_text
) &
93 base::TRIM_LEADING
) != 0) {
94 AdjustCursorPositionIfNecessary(text_
.length() - trimmed_text
.length(),
101 AutocompleteInput::~AutocompleteInput() {
105 size_t AutocompleteInput::RemoveForcedQueryStringIfNecessary(
106 metrics::OmniboxInputType::Type type
,
107 base::string16
* text
) {
108 if ((type
!= metrics::OmniboxInputType::FORCED_QUERY
) || text
->empty() ||
111 // Drop the leading '?'.
117 std::string
AutocompleteInput::TypeToString(
118 metrics::OmniboxInputType::Type type
) {
120 case metrics::OmniboxInputType::INVALID
: return "invalid";
121 case metrics::OmniboxInputType::UNKNOWN
: return "unknown";
122 case metrics::OmniboxInputType::DEPRECATED_REQUESTED_URL
:
123 return "deprecated-requested-url";
124 case metrics::OmniboxInputType::URL
: return "url";
125 case metrics::OmniboxInputType::QUERY
: return "query";
126 case metrics::OmniboxInputType::FORCED_QUERY
: return "forced-query";
128 return std::string();
132 metrics::OmniboxInputType::Type
AutocompleteInput::Parse(
133 const base::string16
& text
,
134 const base::string16
& desired_tld
,
135 const AutocompleteSchemeClassifier
& scheme_classifier
,
137 base::string16
* scheme
,
138 GURL
* canonicalized_url
) {
139 size_t first_non_white
= text
.find_first_not_of(base::kWhitespaceUTF16
, 0);
140 if (first_non_white
== base::string16::npos
)
141 return metrics::OmniboxInputType::INVALID
; // All whitespace.
143 if (text
[first_non_white
] == L
'?') {
144 // If the first non-whitespace character is a '?', we magically treat this
146 return metrics::OmniboxInputType::FORCED_QUERY
;
149 // Ask our parsing back-end to help us understand what the user typed. We
150 // use the URLFixerUpper here because we want to be smart about what we
151 // consider a scheme. For example, we shouldn't consider www.google.com:80
153 url::Parsed local_parts
;
155 parts
= &local_parts
;
156 const base::string16
parsed_scheme(url_fixer::SegmentURL(text
, parts
));
158 *scheme
= parsed_scheme
;
159 const std::string
parsed_scheme_utf8(base::UTF16ToUTF8(parsed_scheme
));
161 // If we can't canonicalize the user's input, the rest of the autocomplete
162 // system isn't going to be able to produce a navigable URL match for it.
163 // So we just return QUERY immediately in these cases.
164 GURL placeholder_canonicalized_url
;
165 if (!canonicalized_url
)
166 canonicalized_url
= &placeholder_canonicalized_url
;
167 *canonicalized_url
= url_fixer::FixupURL(base::UTF16ToUTF8(text
),
168 base::UTF16ToUTF8(desired_tld
));
169 if (!canonicalized_url
->is_valid())
170 return metrics::OmniboxInputType::QUERY
;
172 if (LowerCaseEqualsASCII(parsed_scheme_utf8
, url::kFileScheme
)) {
173 // A user might or might not type a scheme when entering a file URL. In
174 // either case, |parsed_scheme_utf8| will tell us that this is a file URL,
175 // but |parts->scheme| might be empty, e.g. if the user typed "C:\foo".
176 return metrics::OmniboxInputType::URL
;
179 // If the user typed a scheme, and it's HTTP or HTTPS, we know how to parse it
180 // well enough that we can fall through to the heuristics below. If it's
181 // something else, we can just determine our action based on what we do with
182 // any input of this scheme. In theory we could do better with some schemes
183 // (e.g. "ftp" or "view-source") but I'll wait to spend the effort on that
184 // until I run into some cases that really need it.
185 if (parts
->scheme
.is_nonempty() &&
186 !LowerCaseEqualsASCII(parsed_scheme_utf8
, url::kHttpScheme
) &&
187 !LowerCaseEqualsASCII(parsed_scheme_utf8
, url::kHttpsScheme
)) {
188 metrics::OmniboxInputType::Type type
=
189 scheme_classifier
.GetInputTypeForScheme(parsed_scheme_utf8
);
190 if (type
!= metrics::OmniboxInputType::INVALID
)
193 // We don't know about this scheme. It might be that the user typed a
194 // URL of the form "username:password@foo.com".
195 const base::string16 http_scheme_prefix
=
196 base::ASCIIToUTF16(std::string(url::kHttpScheme
) +
197 url::kStandardSchemeSeparator
);
198 url::Parsed http_parts
;
199 base::string16 http_scheme
;
200 GURL http_canonicalized_url
;
201 metrics::OmniboxInputType::Type http_type
=
202 Parse(http_scheme_prefix
+ text
, desired_tld
, scheme_classifier
,
203 &http_parts
, &http_scheme
, &http_canonicalized_url
);
204 DCHECK_EQ(std::string(url::kHttpScheme
),
205 base::UTF16ToUTF8(http_scheme
));
207 if ((http_type
== metrics::OmniboxInputType::URL
) &&
208 http_parts
.username
.is_nonempty() &&
209 http_parts
.password
.is_nonempty()) {
210 // Manually re-jigger the parsed parts to match |text| (without the
211 // http scheme added).
212 http_parts
.scheme
.reset();
213 url::Component
* components
[] = {
214 &http_parts
.username
,
215 &http_parts
.password
,
222 for (size_t i
= 0; i
< arraysize(components
); ++i
) {
223 url_fixer::OffsetComponent(
224 -static_cast<int>(http_scheme_prefix
.length()), components
[i
]);
230 *canonicalized_url
= http_canonicalized_url
;
232 return metrics::OmniboxInputType::URL
;
235 // We don't know about this scheme and it doesn't look like the user
236 // typed a username and password. It's likely to be a search operator
237 // like "site:" or "link:". We classify it as UNKNOWN so the user has
238 // the option of treating it as a URL if we're wrong.
239 // Note that SegmentURL() is smart so we aren't tricked by "c:\foo" or
240 // "www.example.com:81" in this case.
241 return metrics::OmniboxInputType::UNKNOWN
;
244 // Either the user didn't type a scheme, in which case we need to distinguish
245 // between an HTTP URL and a query, or the scheme is HTTP or HTTPS, in which
246 // case we should reject invalid formulations.
248 // If we have an empty host it can't be a valid HTTP[S] URL. (This should
249 // only trigger for input that begins with a colon, which GURL will parse as a
250 // valid, non-standard URL; for standard URLs, an empty host would have
251 // resulted in an invalid |canonicalized_url| above.)
252 if (!parts
->host
.is_nonempty())
253 return metrics::OmniboxInputType::QUERY
;
255 // Sanity-check: GURL should have failed to canonicalize this URL if it had an
257 DCHECK_NE(url::PORT_INVALID
, url::ParsePort(text
.c_str(), parts
->port
));
259 // Likewise, the RCDS can reject certain obviously-invalid hosts. (We also
260 // use the registry length later below.)
261 const base::string16
host(text
.substr(parts
->host
.begin
, parts
->host
.len
));
262 const size_t registry_length
=
263 net::registry_controlled_domains::GetRegistryLength(
264 base::UTF16ToUTF8(host
),
265 net::registry_controlled_domains::EXCLUDE_UNKNOWN_REGISTRIES
,
266 net::registry_controlled_domains::EXCLUDE_PRIVATE_REGISTRIES
);
267 if (registry_length
== std::string::npos
) {
268 // Try to append the desired_tld.
269 if (!desired_tld
.empty()) {
270 base::string16
host_with_tld(host
);
271 if (host
[host
.length() - 1] != '.')
272 host_with_tld
+= '.';
273 host_with_tld
+= desired_tld
;
274 const size_t tld_length
=
275 net::registry_controlled_domains::GetRegistryLength(
276 base::UTF16ToUTF8(host_with_tld
),
277 net::registry_controlled_domains::EXCLUDE_UNKNOWN_REGISTRIES
,
278 net::registry_controlled_domains::EXCLUDE_PRIVATE_REGISTRIES
);
279 if (tld_length
!= std::string::npos
) {
280 // Something like "99999999999" that looks like a bad IP
281 // address, but becomes valid on attaching a TLD.
282 return metrics::OmniboxInputType::URL
;
285 // Could be a broken IP address, etc.
286 return metrics::OmniboxInputType::QUERY
;
290 // See if the hostname is valid. While IE and GURL allow hostnames to contain
291 // many other characters (perhaps for weird intranet machines), it's extremely
292 // unlikely that a user would be trying to type those in for anything other
293 // than a search query.
294 url::CanonHostInfo host_info
;
295 const std::string
canonicalized_host(net::CanonicalizeHost(
296 base::UTF16ToUTF8(host
), &host_info
));
297 if ((host_info
.family
== url::CanonHostInfo::NEUTRAL
) &&
298 !net::IsCanonicalizedHostCompliant(canonicalized_host
,
299 base::UTF16ToUTF8(desired_tld
))) {
300 // Invalid hostname. There are several possible cases:
301 // * Our checker is too strict and the user pasted in a real-world URL
302 // that's "invalid" but resolves. To catch these, we return UNKNOWN when
303 // the user explicitly typed a scheme, so we'll still search by default
304 // but we'll show the accidental search infobar if necessary.
305 // * The user is typing a multi-word query. If we see a space anywhere in
306 // the hostname we assume this is a search and return QUERY.
307 // * Our checker is too strict and the user is typing a real-world hostname
308 // that's "invalid" but resolves. We return UNKNOWN if the TLD is known.
309 // Note that we explicitly excluded hosts with spaces above so that
310 // "toys at amazon.com" will be treated as a search.
311 // * The user is typing some garbage string. Return QUERY.
313 // Thus we fall down in the following cases:
314 // * Trying to navigate to a hostname with spaces
315 // * Trying to navigate to a hostname with invalid characters and an unknown
317 // These are rare, though probably possible in intranets.
318 return (parts
->scheme
.is_nonempty() ||
319 ((registry_length
!= 0) &&
320 (host
.find(' ') == base::string16::npos
))) ?
321 metrics::OmniboxInputType::UNKNOWN
: metrics::OmniboxInputType::QUERY
;
324 // Now that we've ruled out all schemes other than http or https and done a
325 // little more sanity checking, the presence of a scheme means this is likely
327 if (parts
->scheme
.is_nonempty())
328 return metrics::OmniboxInputType::URL
;
330 // See if the host is an IP address.
331 if (host_info
.family
== url::CanonHostInfo::IPV6
)
332 return metrics::OmniboxInputType::URL
;
333 // If the user originally typed a host that looks like an IP address (a
334 // dotted quad), they probably want to open it. If the original input was
335 // something else (like a single number), they probably wanted to search for
336 // it, unless they explicitly typed a scheme. This is true even if the URL
337 // appears to have a path: "1.2/45" is more likely a search (for the answer
338 // to a math problem) than a URL. However, if there are more non-host
339 // components, then maybe this really was intended to be a navigation. For
340 // this reason we only check the dotted-quad case here, and save the "other
341 // IP addresses" case for after we check the number of non-host components
343 if ((host_info
.family
== url::CanonHostInfo::IPV4
) &&
344 (host_info
.num_ipv4_components
== 4))
345 return metrics::OmniboxInputType::URL
;
347 // Presence of a password means this is likely a URL. Note that unless the
348 // user has typed an explicit "http://" or similar, we'll probably think that
349 // the username is some unknown scheme, and bail out in the scheme-handling
351 if (parts
->password
.is_nonempty())
352 return metrics::OmniboxInputType::URL
;
354 // Trailing slashes force the input to be treated as a URL.
355 if (parts
->path
.is_nonempty()) {
356 char c
= text
[parts
->path
.end() - 1];
357 if ((c
== '\\') || (c
== '/'))
358 return metrics::OmniboxInputType::URL
;
361 // If there is more than one recognized non-host component, this is likely to
362 // be a URL, even if the TLD is unknown (in which case this is likely an
364 if (NumNonHostComponents(*parts
) > 1)
365 return metrics::OmniboxInputType::URL
;
367 // If the host has a known TLD or a port, it's probably a URL, with the
368 // following exceptions:
369 // * Any "IP addresses" that make it here are more likely searches
371 // * If we reach here with a username, our input looks like "user@host[.tld]".
372 // Because there is no scheme explicitly specified, we think this is more
373 // likely an email address than an HTTP auth attempt. Hence, we search by
374 // default and let users correct us on a case-by-case basis.
375 // Note that we special-case "localhost" as a known hostname.
376 if ((host_info
.family
!= url::CanonHostInfo::IPV4
) &&
377 ((registry_length
!= 0) || (host
== base::ASCIIToUTF16("localhost") ||
378 parts
->port
.is_nonempty()))) {
379 return parts
->username
.is_nonempty() ? metrics::OmniboxInputType::UNKNOWN
:
380 metrics::OmniboxInputType::URL
;
383 // If we reach this point, we know there's no known TLD on the input, so if
384 // the user wishes to add a desired_tld, the fixup code will oblige; thus this
386 if (!desired_tld
.empty())
387 return metrics::OmniboxInputType::URL
;
389 // No scheme, password, port, path, and no known TLD on the host.
391 // * An "incomplete IP address"; likely a search (see above).
392 // * An email-like input like "user@host", where "host" has no known TLD.
393 // It's not clear what the user means here and searching seems reasonable.
394 // * A single word "foo"; possibly an intranet site, but more likely a search.
395 // This is ideally an UNKNOWN, and we can let the Alternate Nav URL code
396 // catch our mistakes.
397 // * A URL with a valid TLD we don't know about yet. If e.g. a registrar adds
398 // "xxx" as a TLD, then until we add it to our data file, Chrome won't know
399 // "foo.xxx" is a real URL. So ideally this is a URL, but we can't really
400 // distinguish this case from:
401 // * A "URL-like" string that's not really a URL (like
402 // "browser.tabs.closeButtons" or "java.awt.event.*"). This is ideally a
403 // QUERY. Since this is indistinguishable from the case above, and this
404 // case is much more likely, claim these are UNKNOWN, which should default
405 // to the right thing and let users correct us on a case-by-case basis.
406 return metrics::OmniboxInputType::UNKNOWN
;
410 void AutocompleteInput::ParseForEmphasizeComponents(
411 const base::string16
& text
,
412 const AutocompleteSchemeClassifier
& scheme_classifier
,
413 url::Component
* scheme
,
414 url::Component
* host
) {
416 base::string16 scheme_str
;
417 Parse(text
, base::string16(), scheme_classifier
, &parts
, &scheme_str
, NULL
);
419 *scheme
= parts
.scheme
;
422 int after_scheme_and_colon
= parts
.scheme
.end() + 1;
423 // For the view-source scheme, we should emphasize the scheme and host of the
424 // URL qualified by the view-source prefix.
425 if (LowerCaseEqualsASCII(scheme_str
, kViewSourceScheme
) &&
426 (static_cast<int>(text
.length()) > after_scheme_and_colon
)) {
427 // Obtain the URL prefixed by view-source and parse it.
428 base::string16
real_url(text
.substr(after_scheme_and_colon
));
429 url::Parsed real_parts
;
430 AutocompleteInput::Parse(real_url
, base::string16(), scheme_classifier
,
431 &real_parts
, NULL
, NULL
);
432 if (real_parts
.scheme
.is_nonempty() || real_parts
.host
.is_nonempty()) {
433 if (real_parts
.scheme
.is_nonempty()) {
434 *scheme
= url::Component(
435 after_scheme_and_colon
+ real_parts
.scheme
.begin
,
436 real_parts
.scheme
.len
);
440 if (real_parts
.host
.is_nonempty()) {
441 *host
= url::Component(after_scheme_and_colon
+ real_parts
.host
.begin
,
442 real_parts
.host
.len
);
447 } else if (LowerCaseEqualsASCII(scheme_str
, url::kFileSystemScheme
) &&
448 parts
.inner_parsed() && parts
.inner_parsed()->scheme
.is_valid()) {
449 *host
= parts
.inner_parsed()->host
;
454 base::string16
AutocompleteInput::FormattedStringWithEquivalentMeaning(
456 const base::string16
& formatted_url
,
457 const AutocompleteSchemeClassifier
& scheme_classifier
) {
458 if (!net::CanStripTrailingSlash(url
))
459 return formatted_url
;
460 const base::string16
url_with_path(formatted_url
+ base::char16('/'));
461 return (AutocompleteInput::Parse(formatted_url
, base::string16(),
462 scheme_classifier
, NULL
, NULL
, NULL
) ==
463 AutocompleteInput::Parse(url_with_path
, base::string16(),
464 scheme_classifier
, NULL
, NULL
, NULL
)) ?
465 formatted_url
: url_with_path
;
469 int AutocompleteInput::NumNonHostComponents(const url::Parsed
& parts
) {
470 int num_nonhost_components
= 0;
471 if (parts
.scheme
.is_nonempty())
472 ++num_nonhost_components
;
473 if (parts
.username
.is_nonempty())
474 ++num_nonhost_components
;
475 if (parts
.password
.is_nonempty())
476 ++num_nonhost_components
;
477 if (parts
.port
.is_nonempty())
478 ++num_nonhost_components
;
479 if (parts
.path
.is_nonempty())
480 ++num_nonhost_components
;
481 if (parts
.query
.is_nonempty())
482 ++num_nonhost_components
;
483 if (parts
.ref
.is_nonempty())
484 ++num_nonhost_components
;
485 return num_nonhost_components
;
489 bool AutocompleteInput::HasHTTPScheme(const base::string16
& input
) {
490 std::string
utf8_input(base::UTF16ToUTF8(input
));
491 url::Component scheme
;
492 if (url::FindAndCompareScheme(utf8_input
, kViewSourceScheme
, &scheme
)) {
493 utf8_input
.erase(0, scheme
.end() + 1);
495 return url::FindAndCompareScheme(utf8_input
, url::kHttpScheme
, NULL
);
498 void AutocompleteInput::UpdateText(const base::string16
& text
,
499 size_t cursor_position
,
500 const url::Parsed
& parts
) {
501 DCHECK(cursor_position
<= text
.length() ||
502 cursor_position
== base::string16::npos
)
503 << "Text: '" << text
<< "', cp: " << cursor_position
;
505 cursor_position_
= cursor_position
;
509 void AutocompleteInput::Clear() {
511 cursor_position_
= base::string16::npos
;
512 current_url_
= GURL();
513 current_page_classification_
= metrics::OmniboxEventProto::INVALID_SPEC
;
514 type_
= metrics::OmniboxInputType::INVALID
;
515 parts_
= url::Parsed();
517 canonicalized_url_
= GURL();
518 prevent_inline_autocomplete_
= false;
519 prefer_keyword_
= false;
520 allow_exact_keyword_match_
= false;
521 want_asynchronous_matches_
= true;