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. 4 5 #include "components/omnibox/autocomplete_input.h" 6 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" 16 17 namespace { 18 19 // Hardcode constant to avoid any dependencies on content/. 20 const char kViewSourceScheme[] = "view-source"; 21 22 void AdjustCursorPositionIfNecessary(size_t num_leading_chars_removed, 23 size_t* cursor_position) { 24 if (*cursor_position == base::string16::npos) 25 return; 26 if (num_leading_chars_removed < *cursor_position) 27 *cursor_position -= num_leading_chars_removed; 28 else 29 *cursor_position = 0; 30 } 31 32 } // namespace 33 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) { 42 } 43 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, 51 bool prefer_keyword, 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(), 70 &cursor_position_); 71 72 GURL canonicalized_url; 73 type_ = Parse(text_, desired_tld, scheme_classifier, &parts_, &scheme_, 74 &canonicalized_url); 75 76 if (type_ == metrics::OmniboxInputType::INVALID) 77 return; 78 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; 86 87 size_t chars_removed = RemoveForcedQueryStringIfNecessary(type_, &text_); 88 AdjustCursorPositionIfNecessary(chars_removed, &cursor_position_); 89 if (chars_removed) { 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(), 95 &cursor_position_); 96 text_ = trimmed_text; 97 } 98 } 99 } 100 101 AutocompleteInput::~AutocompleteInput() { 102 } 103 104 // static 105 size_t AutocompleteInput::RemoveForcedQueryStringIfNecessary( 106 metrics::OmniboxInputType::Type type, 107 base::string16* text) { 108 if ((type != metrics::OmniboxInputType::FORCED_QUERY) || text->empty() || 109 (*text)[0] != L'?') 110 return 0; 111 // Drop the leading '?'. 112 text->erase(0, 1); 113 return 1; 114 } 115 116 // static 117 std::string AutocompleteInput::TypeToString( 118 metrics::OmniboxInputType::Type type) { 119 switch (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"; 127 } 128 return std::string(); 129 } 130 131 // static 132 metrics::OmniboxInputType::Type AutocompleteInput::Parse( 133 const base::string16& text, 134 const base::string16& desired_tld, 135 const AutocompleteSchemeClassifier& scheme_classifier, 136 url::Parsed* parts, 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. 142 143 if (text[first_non_white] == L'?') { 144 // If the first non-whitespace character is a '?', we magically treat this 145 // as a query. 146 return metrics::OmniboxInputType::FORCED_QUERY; 147 } 148 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 152 // to have a scheme. 153 url::Parsed local_parts; 154 if (!parts) 155 parts = &local_parts; 156 const base::string16 parsed_scheme(url_fixer::SegmentURL(text, parts)); 157 if (scheme) 158 *scheme = parsed_scheme; 159 const std::string parsed_scheme_utf8(base::UTF16ToUTF8(parsed_scheme)); 160 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; 171 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; 177 } 178 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) 191 return type; 192 193 // We don't know about this scheme. It might be that the user typed a 194 // URL of the form "username:password (at) 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)); 206 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, 216 &http_parts.host, 217 &http_parts.port, 218 &http_parts.path, 219 &http_parts.query, 220 &http_parts.ref, 221 }; 222 for (size_t i = 0; i < arraysize(components); ++i) { 223 url_fixer::OffsetComponent( 224 -static_cast<int>(http_scheme_prefix.length()), components[i]); 225 } 226 227 *parts = http_parts; 228 if (scheme) 229 scheme->clear(); 230 *canonicalized_url = http_canonicalized_url; 231 232 return metrics::OmniboxInputType::URL; 233 } 234 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; 242 } 243 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. 247 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; 254 255 // Sanity-check: GURL should have failed to canonicalize this URL if it had an 256 // invalid port. 257 DCHECK_NE(url::PORT_INVALID, url::ParsePort(text.c_str(), parts->port)); 258 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; 283 } 284 } 285 // Could be a broken IP address, etc. 286 return metrics::OmniboxInputType::QUERY; 287 } 288 289 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. 312 // 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 316 // TLD 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; 322 } 323 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 326 // a URL. 327 if (parts->scheme.is_nonempty()) 328 return metrics::OmniboxInputType::URL; 329 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 342 // below. 343 if ((host_info.family == url::CanonHostInfo::IPV4) && 344 (host_info.num_ipv4_components == 4)) 345 return metrics::OmniboxInputType::URL; 346 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 350 // code above. 351 if (parts->password.is_nonempty()) 352 return metrics::OmniboxInputType::URL; 353 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; 359 } 360 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 363 // intranet URL). 364 if (NumNonHostComponents(*parts) > 1) 365 return metrics::OmniboxInputType::URL; 366 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 370 // (see above). 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; 381 } 382 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 385 // is a URL. 386 if (!desired_tld.empty()) 387 return metrics::OmniboxInputType::URL; 388 389 // No scheme, password, port, path, and no known TLD on the host. 390 // This could be: 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; 407 } 408 409 // static 410 void AutocompleteInput::ParseForEmphasizeComponents( 411 const base::string16& text, 412 const AutocompleteSchemeClassifier& scheme_classifier, 413 url::Component* scheme, 414 url::Component* host) { 415 url::Parsed parts; 416 base::string16 scheme_str; 417 Parse(text, base::string16(), scheme_classifier, &parts, &scheme_str, NULL); 418 419 *scheme = parts.scheme; 420 *host = parts.host; 421 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); 437 } else { 438 scheme->reset(); 439 } 440 if (real_parts.host.is_nonempty()) { 441 *host = url::Component(after_scheme_and_colon + real_parts.host.begin, 442 real_parts.host.len); 443 } else { 444 host->reset(); 445 } 446 } 447 } else if (LowerCaseEqualsASCII(scheme_str, url::kFileSystemScheme) && 448 parts.inner_parsed() && parts.inner_parsed()->scheme.is_valid()) { 449 *host = parts.inner_parsed()->host; 450 } 451 } 452 453 // static 454 base::string16 AutocompleteInput::FormattedStringWithEquivalentMeaning( 455 const GURL& url, 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; 466 } 467 468 // static 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; 486 } 487 488 // static 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); 494 } 495 return url::FindAndCompareScheme(utf8_input, url::kHttpScheme, NULL); 496 } 497 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; 504 text_ = text; 505 cursor_position_ = cursor_position; 506 parts_ = parts; 507 } 508 509 void AutocompleteInput::Clear() { 510 text_.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(); 516 scheme_.clear(); 517 canonicalized_url_ = GURL(); 518 prevent_inline_autocomplete_ = false; 519 prefer_keyword_ = false; 520 allow_exact_keyword_match_ = false; 521 want_asynchronous_matches_ = true; 522 } 523