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