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.
5 #include "components/omnibox/browser/history_quick_provider.h"
9 #include "base/basictypes.h"
10 #include "base/debug/crash_logging.h"
11 #include "base/i18n/break_iterator.h"
12 #include "base/logging.h"
13 #include "base/metrics/field_trial.h"
14 #include "base/prefs/pref_service.h"
15 #include "base/strings/string_number_conversions.h"
16 #include "base/strings/string_util.h"
17 #include "base/strings/utf_string_conversions.h"
18 #include "components/bookmarks/browser/bookmark_model.h"
19 #include "components/crash/core/common/crash_keys.h"
20 #include "components/history/core/browser/history_database.h"
21 #include "components/history/core/browser/history_service.h"
22 #include "components/metrics/proto/omnibox_input_type.pb.h"
23 #include "components/omnibox/browser/autocomplete_match_type.h"
24 #include "components/omnibox/browser/autocomplete_provider_client.h"
25 #include "components/omnibox/browser/autocomplete_result.h"
26 #include "components/omnibox/browser/history_url_provider.h"
27 #include "components/omnibox/browser/in_memory_url_index.h"
28 #include "components/omnibox/browser/in_memory_url_index_types.h"
29 #include "components/omnibox/browser/omnibox_field_trial.h"
30 #include "components/search_engines/template_url.h"
31 #include "components/search_engines/template_url_service.h"
32 #include "components/url_formatter/url_formatter.h"
33 #include "net/base/escape.h"
34 #include "net/base/registry_controlled_domains/registry_controlled_domain.h"
35 #include "url/third_party/mozilla/url_parse.h"
36 #include "url/url_util.h"
38 bool HistoryQuickProvider::disabled_
= false;
40 HistoryQuickProvider::HistoryQuickProvider(AutocompleteProviderClient
* client
)
41 : HistoryProvider(AutocompleteProvider::TYPE_HISTORY_QUICK
, client
),
42 languages_(client
->GetAcceptLanguages()),
43 in_memory_url_index_(client
->GetInMemoryURLIndex()) {
46 void HistoryQuickProvider::Start(const AutocompleteInput
& input
,
47 bool minimal_changes
) {
49 if (disabled_
|| input
.from_omnibox_focus())
52 // Don't bother with INVALID and FORCED_QUERY.
53 if ((input
.type() == metrics::OmniboxInputType::INVALID
) ||
54 (input
.type() == metrics::OmniboxInputType::FORCED_QUERY
))
57 autocomplete_input_
= input
;
59 // TODO(pkasting): We should just block here until this loads. Any time
60 // someone unloads the history backend, we'll get inconsistent inline
61 // autocomplete behavior here.
62 if (in_memory_url_index_
) {
67 HistoryQuickProvider::~HistoryQuickProvider() {
70 void HistoryQuickProvider::DoAutocomplete() {
71 // Get the matching URLs from the DB.
72 ScoredHistoryMatches matches
= in_memory_url_index_
->HistoryItemsForTerms(
73 autocomplete_input_
.text(), autocomplete_input_
.cursor_position(),
74 AutocompleteProvider::kMaxMatches
);
78 // Figure out if HistoryURL provider has a URL-what-you-typed match
79 // that ought to go first and what its score will be.
80 bool will_have_url_what_you_typed_match_first
= false;
81 int url_what_you_typed_match_score
= -1; // undefined
82 // These are necessary (but not sufficient) conditions for the omnibox
83 // input to be a URL-what-you-typed match. The username test checks that
84 // either the username does not exist (a regular URL such as http://site/)
85 // or, if the username exists (http://user@site/), there must be either
86 // a password or a port. Together these exclude pure username@site
87 // inputs because these are likely to be an e-mail address. HistoryURL
88 // provider won't promote the URL-what-you-typed match to first
90 const bool can_have_url_what_you_typed_match_first
=
91 (autocomplete_input_
.type() != metrics::OmniboxInputType::QUERY
) &&
92 (!autocomplete_input_
.parts().username
.is_nonempty() ||
93 autocomplete_input_
.parts().password
.is_nonempty() ||
94 autocomplete_input_
.parts().path
.is_nonempty());
95 if (can_have_url_what_you_typed_match_first
) {
96 history::HistoryService
* const history_service
=
97 client()->GetHistoryService();
98 // We expect HistoryService to be available. In case it's not,
99 // (e.g., due to Profile corruption) we let HistoryQuick provider
100 // completions (which may be available because it's a different
101 // data structure) compete with the URL-what-you-typed match as
103 if (history_service
) {
104 history::URLDatabase
* url_db
= history_service
->InMemoryDatabase();
105 // url_db can be NULL if it hasn't finished initializing (or
106 // failed to to initialize). In this case, we let HistoryQuick
107 // provider completions compete with the URL-what-you-typed
110 const std::string
host(base::UTF16ToUTF8(
111 autocomplete_input_
.text().substr(
112 autocomplete_input_
.parts().host
.begin
,
113 autocomplete_input_
.parts().host
.len
)));
114 // We want to put the URL-what-you-typed match first if either
115 // * the user visited the URL before (intranet or internet).
116 // * it's a URL on a host that user visited before and this
117 // is the root path of the host. (If the user types some
118 // of a path--more than a simple "/"--we let autocomplete compete
119 // normally with the URL-what-you-typed match.)
120 // TODO(mpearson): Remove this hacky code and simply score URL-what-
121 // you-typed in some sane way relative to possible completions:
122 // URL-what-you-typed should get some sort of a boost relative
123 // to completions, but completions should naturally win if
124 // they're a lot more popular. In this process, if the input
125 // is a bare intranet hostname that has been visited before, we
126 // may want to enforce that the only completions that can outscore
127 // the URL-what-you-typed match are on the same host (i.e., aren't
128 // from a longer internet hostname for which the omnibox input is
130 if (url_db
->GetRowForURL(
131 autocomplete_input_
.canonicalized_url(), NULL
) != 0) {
132 // We visited this URL before.
133 will_have_url_what_you_typed_match_first
= true;
134 // HistoryURLProvider gives visited what-you-typed URLs a high score.
135 url_what_you_typed_match_score
=
136 HistoryURLProvider::kScoreForBestInlineableResult
;
137 } else if (url_db
->IsTypedHost(host
) &&
138 (!autocomplete_input_
.parts().path
.is_nonempty() ||
139 ((autocomplete_input_
.parts().path
.len
== 1) &&
140 (autocomplete_input_
.text()[
141 autocomplete_input_
.parts().path
.begin
] == '/'))) &&
142 !autocomplete_input_
.parts().query
.is_nonempty() &&
143 !autocomplete_input_
.parts().ref
.is_nonempty()) {
144 // Not visited, but we've seen the host before.
145 will_have_url_what_you_typed_match_first
= true;
146 const size_t registry_length
=
147 net::registry_controlled_domains::GetRegistryLength(
149 net::registry_controlled_domains::EXCLUDE_UNKNOWN_REGISTRIES
,
150 net::registry_controlled_domains::EXCLUDE_PRIVATE_REGISTRIES
);
151 if (registry_length
== 0) {
152 // Known intranet hosts get one score.
153 url_what_you_typed_match_score
=
154 HistoryURLProvider::kScoreForUnvisitedIntranetResult
;
156 // Known internet hosts get another.
157 url_what_you_typed_match_score
=
158 HistoryURLProvider::kScoreForWhatYouTypedResult
;
165 // Loop over every result and add it to matches_. In the process,
166 // guarantee that scores are decreasing. |max_match_score| keeps
167 // track of the highest score we can assign to any later results we
168 // see. Also, reduce |max_match_score| if we think there will be
169 // a URL-what-you-typed match. (We want URL-what-you-typed matches for
170 // visited URLs to beat out any longer URLs, no matter how frequently
171 // they're visited.) The strength of this reduction depends on the
172 // likely score for the URL-what-you-typed result.
174 // |template_url_service| or |template_url| can be NULL in unit tests.
175 TemplateURLService
* template_url_service
= client()->GetTemplateURLService();
176 TemplateURL
* template_url
= template_url_service
?
177 template_url_service
->GetDefaultSearchProvider() : NULL
;
178 int max_match_score
= matches
.begin()->raw_score
;
179 if (will_have_url_what_you_typed_match_first
) {
180 max_match_score
= std::min(max_match_score
,
181 url_what_you_typed_match_score
- 1);
183 for (ScoredHistoryMatches::const_iterator match_iter
= matches
.begin();
184 match_iter
!= matches
.end(); ++match_iter
) {
185 const ScoredHistoryMatch
& history_match(*match_iter
);
186 // Culls results corresponding to queries from the default search engine.
187 // These are low-quality, difficult-to-understand matches for users, and the
188 // SearchProvider should surface past queries in a better way anyway.
190 !template_url
->IsSearchURL(history_match
.url_info
.url(),
191 template_url_service
->search_terms_data())) {
192 // Set max_match_score to the score we'll assign this result:
193 max_match_score
= std::min(max_match_score
, history_match
.raw_score
);
194 matches_
.push_back(QuickMatchToACMatch(history_match
, max_match_score
));
195 // Mark this max_match_score as being used:
201 AutocompleteMatch
HistoryQuickProvider::QuickMatchToACMatch(
202 const ScoredHistoryMatch
& history_match
,
204 const history::URLRow
& info
= history_match
.url_info
;
205 AutocompleteMatch
match(
206 this, score
, !!info
.visit_count(),
207 history_match
.url_matches
.empty() ?
208 AutocompleteMatchType::HISTORY_TITLE
:
209 AutocompleteMatchType::HISTORY_URL
);
210 match
.typed_count
= info
.typed_count();
211 match
.destination_url
= info
.url();
212 DCHECK(match
.destination_url
.is_valid());
214 // Format the URL autocomplete presentation.
215 const url_formatter::FormatUrlTypes format_types
=
216 url_formatter::kFormatUrlOmitAll
&
217 ~(!history_match
.match_in_scheme
? 0 : url_formatter::kFormatUrlOmitHTTP
);
218 match
.fill_into_edit
=
219 AutocompleteInput::FormattedStringWithEquivalentMeaning(
220 info
.url(), url_formatter::FormatUrl(
221 info
.url(), languages_
, format_types
,
222 net::UnescapeRule::SPACES
, nullptr, nullptr, nullptr),
223 client()->GetSchemeClassifier());
224 std::vector
<size_t> offsets
=
225 OffsetsFromTermMatches(history_match
.url_matches
);
226 base::OffsetAdjuster::Adjustments adjustments
;
227 match
.contents
= url_formatter::FormatUrlWithAdjustments(
228 info
.url(), languages_
, format_types
, net::UnescapeRule::SPACES
, nullptr,
229 nullptr, &adjustments
);
230 base::OffsetAdjuster::AdjustOffsets(adjustments
, &offsets
);
231 TermMatches new_matches
=
232 ReplaceOffsetsInTermMatches(history_match
.url_matches
, offsets
);
233 match
.contents_class
=
234 SpansFromTermMatch(new_matches
, match
.contents
.length(), true);
236 // Set |inline_autocompletion| and |allowed_to_be_default_match| if possible.
237 if (history_match
.can_inline
) {
238 // TODO(mpearson): remove this and all dependency of //components/omnibox
239 // on //components/crash/core/common once http://crbug.com/464926 is fixed
240 // (i.e. remove #include, exception in components/omnibox/DEPS, and deps
241 // in components/omnibox.gypi and components/omnibox/browser/BUILD.gn).
242 base::debug::ScopedCrashKey
crash_info(
243 crash_keys::kBug464926CrashKey
,
244 info
.url().spec().substr(0, 30) + " " +
245 base::UTF16ToUTF8(autocomplete_input_
.text()).substr(0, 20) + " " +
246 base::SizeTToString(history_match
.url_matches
.size()) + " " +
247 base::SizeTToString(offsets
.size()));
248 CHECK(!new_matches
.empty());
249 size_t inline_autocomplete_offset
= new_matches
[0].offset
+
250 new_matches
[0].length
;
251 // |inline_autocomplete_offset| may be beyond the end of the
252 // |fill_into_edit| if the user has typed an URL with a scheme and the
253 // last character typed is a slash. That slash is removed by the
254 // FormatURLWithOffsets call above.
255 if (inline_autocomplete_offset
< match
.fill_into_edit
.length()) {
256 match
.inline_autocompletion
=
257 match
.fill_into_edit
.substr(inline_autocomplete_offset
);
259 match
.allowed_to_be_default_match
= match
.inline_autocompletion
.empty() ||
260 !PreventInlineAutocomplete(autocomplete_input_
);
262 match
.EnsureUWYTIsAllowedToBeDefault(
264 client()->GetAcceptLanguages(),
265 client()->GetTemplateURLService());
267 // Format the description autocomplete presentation.
268 match
.description
= info
.title();
269 match
.description_class
= SpansFromTermMatch(
270 history_match
.title_matches
, match
.description
.length(), false);
272 match
.RecordAdditionalInfo("typed count", info
.typed_count());
273 match
.RecordAdditionalInfo("visit count", info
.visit_count());
274 match
.RecordAdditionalInfo("last visit", info
.last_visit());