1 // Copyright (c) 2012 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/browser/builtin_provider.h"
6
7 #include <stddef.h>
8
9 #include <algorithm>
10 #include <string>
11
12 #include "base/strings/string_util.h"
13 #include "base/strings/utf_string_conversions.h"
14 #include "components/omnibox/browser/autocomplete_input.h"
15 #include "components/omnibox/browser/autocomplete_match_classification.h"
16 #include "components/omnibox/browser/autocomplete_provider_client.h"
17 #include "components/omnibox/browser/history_provider.h"
18 #include "components/search_engines/omnibox_focus_type.h"
19 #include "components/url_formatter/url_fixer.h"
20 #include "third_party/metrics_proto/omnibox_input_type.pb.h"
21
22 const int BuiltinProvider::kRelevance = 860;
23
BuiltinProvider(AutocompleteProviderClient * client)24 BuiltinProvider::BuiltinProvider(AutocompleteProviderClient* client)
25 : AutocompleteProvider(AutocompleteProvider::TYPE_BUILTIN),
26 client_(client) {
27 builtins_ = client_->GetBuiltinURLs();
28 }
29
Start(const AutocompleteInput & input,bool minimal_changes)30 void BuiltinProvider::Start(const AutocompleteInput& input,
31 bool minimal_changes) {
32 matches_.clear();
33 if (input.focus_type() != OmniboxFocusType::DEFAULT ||
34 (input.type() == metrics::OmniboxInputType::EMPTY) ||
35 (input.type() == metrics::OmniboxInputType::QUERY))
36 return;
37
38 const size_t kAboutSchemeLength = strlen(url::kAboutScheme);
39 const base::string16 kAbout =
40 base::ASCIIToUTF16(url::kAboutScheme) +
41 base::ASCIIToUTF16(url::kStandardSchemeSeparator);
42 const base::string16 embedderAbout =
43 base::UTF8ToUTF16(client_->GetEmbedderRepresentationOfAboutScheme()) +
44 base::ASCIIToUTF16(url::kStandardSchemeSeparator);
45
46 const int kUrl = ACMatchClassification::URL;
47 const int kMatch = kUrl | ACMatchClassification::MATCH;
48
49 const base::string16 text = input.text();
50 bool starting_about = base::StartsWith(embedderAbout, text,
51 base::CompareCase::INSENSITIVE_ASCII);
52 if (starting_about ||
53 base::StartsWith(kAbout, text, base::CompareCase::INSENSITIVE_ASCII)) {
54 // Highlight the input portion matching |embedderAbout|; or if the user has
55 // input "about:" (with optional slashes), highlight the whole
56 // |embedderAbout|.
57 TermMatches style_matches;
58 if (starting_about)
59 style_matches.emplace_back(0, 0, text.length());
60 else if (text.length() > kAboutSchemeLength)
61 style_matches.emplace_back(0, 0, embedderAbout.length());
62 ACMatchClassifications styles =
63 ClassifyTermMatches(style_matches, std::string::npos, kMatch, kUrl);
64 // Include some common builtin URLs as the user types the scheme.
65 for (base::string16 url : client_->GetBuiltinsToProvideAsUserTypes())
66 AddMatch(url, base::string16(), styles);
67
68 } else {
69 // Match input about: or |embedderAbout| URL input against builtin URLs.
70 GURL url = url_formatter::FixupURL(base::UTF16ToUTF8(text), std::string());
71 const bool text_ends_with_slash =
72 base::EndsWith(text, base::ASCIIToUTF16("/"),
73 base::CompareCase::SENSITIVE);
74 // BuiltinProvider doesn't know how to suggest valid ?query or #fragment
75 // extensions to builtin URLs.
76 if (url.SchemeIs(client_->GetEmbedderRepresentationOfAboutScheme()) &&
77 url.has_host() && !url.has_query() && !url.has_ref()) {
78 // Suggest about:blank for substrings, taking URL fixup into account.
79 // Chrome does not support trailing slashes or paths for about:blank.
80 const base::string16 blank_host = base::ASCIIToUTF16("blank");
81 const base::string16 host = base::UTF8ToUTF16(url.host());
82 if (base::StartsWith(text, base::ASCIIToUTF16(url::kAboutScheme),
83 base::CompareCase::INSENSITIVE_ASCII) &&
84 base::StartsWith(blank_host, host,
85 base::CompareCase::INSENSITIVE_ASCII) &&
86 (url.path().length() <= 1) && !text_ends_with_slash) {
87 base::string16 match = base::ASCIIToUTF16(url::kAboutBlankURL);
88 const size_t corrected_length = kAboutSchemeLength + 1 + host.length();
89 TermMatches style_matches = {{0, 0, corrected_length}};
90 ACMatchClassifications styles =
91 ClassifyTermMatches(style_matches, match.length(), kMatch, kUrl);
92 AddMatch(match, match.substr(corrected_length), styles);
93 }
94
95 // Include the path for sub-pages (e.g. "chrome://settings/browser").
96 base::string16 host_and_path = base::UTF8ToUTF16(url.host() + url.path());
97 base::TrimString(host_and_path, base::ASCIIToUTF16("/"), &host_and_path);
98 size_t match_length = embedderAbout.length() + host_and_path.length();
99 for (Builtins::const_iterator i(builtins_.begin());
100 (i != builtins_.end()) && (matches_.size() < provider_max_matches_);
101 ++i) {
102 if (base::StartsWith(*i, host_and_path,
103 base::CompareCase::INSENSITIVE_ASCII)) {
104 base::string16 match_string = embedderAbout + *i;
105 TermMatches style_matches = {{0, 0, match_length}};
106 ACMatchClassifications styles = ClassifyTermMatches(
107 style_matches, match_string.length(), kMatch, kUrl);
108 // FixupURL() may have dropped a trailing slash on the user's input.
109 // Ensure that in that case, we don't inline autocomplete unless the
110 // autocompletion restores the slash. This prevents us from e.g.
111 // trying to add a 'y' to an input like "chrome://histor/".
112 base::string16 inline_autocompletion(
113 match_string.substr(match_length));
114 if (text_ends_with_slash && !base::StartsWith(
115 match_string.substr(match_length), base::ASCIIToUTF16("/"),
116 base::CompareCase::INSENSITIVE_ASCII))
117 inline_autocompletion = base::string16();
118 AddMatch(match_string, inline_autocompletion, styles);
119 }
120 }
121 }
122 }
123
124 // Provide a relevance score for each match.
125 for (size_t i = 0; i < matches_.size(); ++i)
126 matches_[i].relevance = kRelevance + matches_.size() - (i + 1);
127
128 // If allowing completions is okay and there's a match that's considered
129 // appropriate to be the default match, mark it as such and give it a high
130 // enough score to beat url-what-you-typed.
131 size_t default_match_index;
132
133 // None of the built in site URLs contain whitespaces so we can safely prevent
134 // autocompletion when the input has a trailing whitespace in order to avoid
135 // autocompleting e.g. 'chrome://s ettings' when the input is 'chrome://s '.
136 bool input_allowed_to_have_default_match =
137 !input.prevent_inline_autocomplete() &&
138 (input.text().empty() || !base::IsUnicodeWhitespace(input.text().back()));
139 if (input_allowed_to_have_default_match &&
140 HasMatchThatShouldBeDefault(&default_match_index)) {
141 matches_[default_match_index].relevance = 1250;
142 matches_[default_match_index].allowed_to_be_default_match = true;
143 }
144 }
145
~BuiltinProvider()146 BuiltinProvider::~BuiltinProvider() {}
147
AddMatch(const base::string16 & match_string,const base::string16 & inline_completion,const ACMatchClassifications & styles)148 void BuiltinProvider::AddMatch(const base::string16& match_string,
149 const base::string16& inline_completion,
150 const ACMatchClassifications& styles) {
151 AutocompleteMatch match(this, kRelevance, false,
152 AutocompleteMatchType::NAVSUGGEST);
153 match.fill_into_edit = match_string;
154 match.inline_autocompletion = inline_completion;
155 match.destination_url = GURL(match_string);
156 match.contents = match_string;
157 match.contents_class = styles;
158 matches_.push_back(match);
159 }
160
HasMatchThatShouldBeDefault(size_t * index) const161 bool BuiltinProvider::HasMatchThatShouldBeDefault(size_t* index) const {
162 if (matches_.size() == 0)
163 return false;
164
165 // If there's only one possible completion of the user's input and it's not
166 // empty, it should be allowed to be the default match.
167 if (matches_.size() == 1) {
168 *index = 0;
169 return !matches_[0].inline_autocompletion.empty();
170 }
171
172 // If there's a non-empty completion that is a prefix of all of the others,
173 // it should be allowed to be the default match.
174 size_t shortest = 0;
175 for (size_t i = 1; i < matches_.size(); ++i) {
176 if (matches_[i].contents.length() < matches_[shortest].contents.length())
177 shortest = i;
178 }
179 if (matches_[shortest].inline_autocompletion.empty())
180 return false;
181
182 for (size_t i = 0; i < matches_.size(); ++i) {
183 if (!base::StartsWith(matches_[i].contents, matches_[shortest].contents,
184 base::CompareCase::INSENSITIVE_ASCII)) {
185 return false;
186 }
187 }
188 *index = shortest;
189 return true;
190 }
191