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 // This file defines helper functions shared by the various implementations
6 // of OmniboxView.
7
8 #include "components/omnibox/browser/omnibox_view.h"
9
10 #include <algorithm>
11 #include <utility>
12
13 #include "base/strings/string16.h"
14 #include "base/strings/string_util.h"
15 #include "base/strings/utf_string_conversions.h"
16 #include "build/build_config.h"
17 #include "components/bookmarks/browser/bookmark_model.h"
18 #include "components/omnibox/browser/autocomplete_match.h"
19 #include "components/omnibox/browser/location_bar_model.h"
20 #include "components/omnibox/browser/omnibox_edit_controller.h"
21 #include "components/omnibox/browser/omnibox_edit_model.h"
22 #include "components/omnibox/browser/omnibox_field_trial.h"
23 #include "components/omnibox/common/omnibox_features.h"
24 #include "extensions/common/constants.h"
25 #include "ui/base/l10n/l10n_util.h"
26
27 #if !defined(OS_ANDROID) && !defined(OS_IOS)
28
29 #include "ui/gfx/paint_vector_icon.h"
30
31 #endif
32
33 namespace {
34
35 // Return true if either non prefix or split autocompletion is enabled.
RichAutocompletionEitherNonPrefixOrSplitEnabled()36 bool RichAutocompletionEitherNonPrefixOrSplitEnabled() {
37 return OmniboxFieldTrial::RichAutocompletionAutocompleteNonPrefixAll() ||
38 OmniboxFieldTrial::
39 RichAutocompletionAutocompleteNonPrefixShortcutProvider() ||
40 OmniboxFieldTrial::RichAutocompletionSplitTitleCompletion() ||
41 OmniboxFieldTrial::RichAutocompletionSplitUrlCompletion();
42 }
43
44 } // namespace
45
46 OmniboxView::State::State() = default;
47 OmniboxView::State::State(const State& state) = default;
48
49 // static
StripJavascriptSchemas(const base::string16 & text)50 base::string16 OmniboxView::StripJavascriptSchemas(const base::string16& text) {
51 const base::string16 kJsPrefix(
52 base::ASCIIToUTF16(url::kJavaScriptScheme) + base::ASCIIToUTF16(":"));
53
54 bool found_JavaScript = false;
55 size_t i = 0;
56 // Find the index of the first character that isn't whitespace, a control
57 // character, or a part of a JavaScript: scheme.
58 while (i < text.size()) {
59 if (base::IsUnicodeWhitespace(text[i]) || (text[i] < 0x20)) {
60 ++i;
61 } else {
62 if (!base::EqualsCaseInsensitiveASCII(text.substr(i, kJsPrefix.length()),
63 kJsPrefix))
64 break;
65
66 // We've found a JavaScript scheme. Continue searching to ensure that
67 // strings like "javascript:javascript:alert()" are fully stripped.
68 found_JavaScript = true;
69 i += kJsPrefix.length();
70 }
71 }
72
73 // If we found any "JavaScript:" schemes in the text, return the text starting
74 // at the first non-whitespace/control character after the last instance of
75 // the scheme.
76 if (found_JavaScript)
77 return text.substr(i);
78
79 return text;
80 }
81
82 // static
SanitizeTextForPaste(const base::string16 & text)83 base::string16 OmniboxView::SanitizeTextForPaste(const base::string16& text) {
84 if (text.empty())
85 return base::string16(); // Nothing to do.
86
87 size_t end = text.find_first_not_of(base::kWhitespaceUTF16);
88 if (end == base::string16::npos)
89 return base::ASCIIToUTF16(" "); // Convert all-whitespace to single space.
90 // Because |end| points at the first non-whitespace character, the loop
91 // below will skip leading whitespace.
92
93 // Reserve space for the sanitized output.
94 base::string16 output;
95 output.reserve(text.size()); // Guaranteed to be large enough.
96
97 // Copy all non-whitespace sequences.
98 // Do not copy trailing whitespace.
99 // Copy all other whitespace sequences that do not contain CR/LF.
100 // Convert all other whitespace sequences that do contain CR/LF to either ' '
101 // or nothing, depending on whether there are any other sequences that do not
102 // contain CR/LF.
103 bool output_needs_lf_conversion = false;
104 bool seen_non_lf_whitespace = false;
105 const auto copy_range = [&text, &output](size_t begin, size_t end) {
106 output +=
107 text.substr(begin, (end == base::string16::npos) ? end : (end - begin));
108 };
109 constexpr base::char16 kNewline[] = {'\n', 0};
110 constexpr base::char16 kSpace[] = {' ', 0};
111 while (true) {
112 // Copy this non-whitespace sequence.
113 size_t begin = end;
114 end = text.find_first_of(base::kWhitespaceUTF16, begin + 1);
115 copy_range(begin, end);
116
117 // Now there is either a whitespace sequence, or the end of the string.
118 if (end != base::string16::npos) {
119 // There is a whitespace sequence; see if it contains CR/LF.
120 begin = end;
121 end = text.find_first_not_of(base::kWhitespaceNoCrLfUTF16, begin);
122 if ((end != base::string16::npos) && (text[end] != '\n') &&
123 (text[end] != '\r')) {
124 // Found a non-trailing whitespace sequence without CR/LF. Copy it.
125 seen_non_lf_whitespace = true;
126 copy_range(begin, end);
127 continue;
128 }
129 }
130
131 // |end| either points at the end of the string or a CR/LF.
132 if (end != base::string16::npos)
133 end = text.find_first_not_of(base::kWhitespaceUTF16, end + 1);
134 if (end == base::string16::npos)
135 break; // Ignore any trailing whitespace.
136
137 // The preceding whitespace sequence contained CR/LF. Convert to a single
138 // LF that we'll fix up below the loop.
139 output_needs_lf_conversion = true;
140 output += '\n';
141 }
142
143 // Convert LFs to ' ' or '' depending on whether there were non-LF whitespace
144 // sequences.
145 if (output_needs_lf_conversion) {
146 base::ReplaceChars(output, kNewline,
147 seen_non_lf_whitespace ? kSpace : base::string16(),
148 &output);
149 }
150
151 return StripJavascriptSchemas(output);
152 }
153
154 OmniboxView::~OmniboxView() = default;
155
OpenMatch(const AutocompleteMatch & match,WindowOpenDisposition disposition,const GURL & alternate_nav_url,const base::string16 & pasted_text,size_t selected_line,base::TimeTicks match_selection_timestamp)156 void OmniboxView::OpenMatch(const AutocompleteMatch& match,
157 WindowOpenDisposition disposition,
158 const GURL& alternate_nav_url,
159 const base::string16& pasted_text,
160 size_t selected_line,
161 base::TimeTicks match_selection_timestamp) {
162 // Invalid URLs such as chrome://history can end up here.
163 if (!match.destination_url.is_valid() || !model_)
164 return;
165 model_->OpenMatch(match, disposition, alternate_nav_url, pasted_text,
166 selected_line, match_selection_timestamp);
167 }
168
IsEditingOrEmpty() const169 bool OmniboxView::IsEditingOrEmpty() const {
170 return (model_.get() && model_->user_input_in_progress()) ||
171 (GetOmniboxTextLength() == 0);
172 }
173
174 // TODO (manukh) OmniboxView::GetIcon is very similar to
175 // OmniboxPopupModel::GetMatchIcon. They contain certain inconsistencies
176 // concerning what flags are required to display url favicons and bookmark star
177 // icons. OmniboxPopupModel::GetMatchIcon also doesn't display default search
178 // provider icons. It's possible they have other inconsistencies as well. We may
179 // want to consider reusing the same code for both the popup and omnibox icons.
GetIcon(int dip_size,SkColor color,IconFetchedCallback on_icon_fetched) const180 ui::ImageModel OmniboxView::GetIcon(int dip_size,
181 SkColor color,
182 IconFetchedCallback on_icon_fetched) const {
183 #if defined(OS_ANDROID) || defined(OS_IOS)
184 // This is used on desktop only.
185 NOTREACHED();
186 return ui::ImageModel();
187 #else
188
189 // For tests, model_ will be null.
190 if (!model_) {
191 AutocompleteMatch fake_match;
192 fake_match.type = AutocompleteMatchType::URL_WHAT_YOU_TYPED;
193 const gfx::VectorIcon& vector_icon = fake_match.GetVectorIcon(false);
194 return ui::ImageModel::FromVectorIcon(vector_icon, color, dip_size);
195 }
196
197 if (model_->ShouldShowCurrentPageIcon()) {
198 LocationBarModel* location_bar_model = controller_->GetLocationBarModel();
199 return ui::ImageModel::FromVectorIcon(location_bar_model->GetVectorIcon(),
200 color, dip_size);
201 }
202
203 gfx::Image favicon;
204 AutocompleteMatch match = model_->CurrentMatch(nullptr);
205 if (AutocompleteMatch::IsSearchType(match.type)) {
206 // For search queries, display default search engine's favicon.
207 favicon = model_->client()->GetFaviconForDefaultSearchProvider(
208 std::move(on_icon_fetched));
209
210 } else {
211 // For site suggestions, display site's favicon.
212 favicon = model_->client()->GetFaviconForPageUrl(
213 match.destination_url, std::move(on_icon_fetched));
214 }
215
216 if (!favicon.IsEmpty())
217 return ui::ImageModel::FromImage(model_->client()->GetSizedIcon(favicon));
218 // If the client returns an empty favicon, fall through to provide the
219 // generic vector icon. |on_icon_fetched| may or may not be called later.
220 // If it's never called, the vector icon we provide below should remain.
221
222 // For bookmarked suggestions, display bookmark icon.
223 bookmarks::BookmarkModel* bookmark_model =
224 model_->client()->GetBookmarkModel();
225 const bool is_bookmarked =
226 bookmark_model && bookmark_model->IsBookmarked(match.destination_url);
227
228 const gfx::VectorIcon& vector_icon = match.GetVectorIcon(is_bookmarked);
229
230 return ui::ImageModel::FromVectorIcon(vector_icon, color, dip_size);
231 #endif // defined(OS_ANDROID) || defined(OS_IOS)
232 }
233
SetUserText(const base::string16 & text)234 void OmniboxView::SetUserText(const base::string16& text) {
235 SetUserText(text, true);
236 }
237
SetUserText(const base::string16 & text,bool update_popup)238 void OmniboxView::SetUserText(const base::string16& text, bool update_popup) {
239 if (model_)
240 model_->SetUserText(text);
241 SetWindowTextAndCaretPos(text, text.length(), update_popup, true);
242 }
243
RevertAll()244 void OmniboxView::RevertAll() {
245 CloseOmniboxPopup();
246 if (model_)
247 model_->Revert();
248 TextChanged();
249 }
250
CloseOmniboxPopup()251 void OmniboxView::CloseOmniboxPopup() {
252 if (model_)
253 model_->StopAutocomplete();
254 }
255
IsImeShowingPopup() const256 bool OmniboxView::IsImeShowingPopup() const {
257 // Default to claiming that the IME is not showing a popup, since hiding the
258 // omnibox dropdown is a bad user experience when we don't know for sure that
259 // we have to.
260 return false;
261 }
262
ShowVirtualKeyboardIfEnabled()263 void OmniboxView::ShowVirtualKeyboardIfEnabled() {}
264
HideImeIfNeeded()265 void OmniboxView::HideImeIfNeeded() {}
266
IsIndicatingQueryRefinement() const267 bool OmniboxView::IsIndicatingQueryRefinement() const {
268 // The default implementation always returns false. Mobile ports can override
269 // this method and implement as needed.
270 return false;
271 }
272
GetState(State * state)273 void OmniboxView::GetState(State* state) {
274 state->text = GetText();
275 state->keyword = model()->keyword();
276 state->is_keyword_selected = model()->is_keyword_selected();
277 GetSelectionBounds(&state->sel_start, &state->sel_end);
278 if (RichAutocompletionEitherNonPrefixOrSplitEnabled())
279 state->all_sel_length = GetAllSelectionsLength();
280 }
281
GetStateChanges(const State & before,const State & after)282 OmniboxView::StateChanges OmniboxView::GetStateChanges(const State& before,
283 const State& after) {
284 OmniboxView::StateChanges state_changes;
285 state_changes.old_text = &before.text;
286 state_changes.new_text = &after.text;
287 state_changes.new_sel_start = after.sel_start;
288 state_changes.new_sel_end = after.sel_end;
289 const bool old_sel_empty = before.sel_start == before.sel_end;
290 const bool new_sel_empty = after.sel_start == after.sel_end;
291 const bool sel_same_ignoring_direction =
292 std::min(before.sel_start, before.sel_end) ==
293 std::min(after.sel_start, after.sel_end) &&
294 std::max(before.sel_start, before.sel_end) ==
295 std::max(after.sel_start, after.sel_end);
296 state_changes.selection_differs =
297 (!old_sel_empty || !new_sel_empty) && !sel_same_ignoring_direction;
298 state_changes.text_differs = before.text != after.text;
299 state_changes.keyword_differs =
300 (after.is_keyword_selected != before.is_keyword_selected) ||
301 (after.is_keyword_selected && before.is_keyword_selected &&
302 after.keyword != before.keyword);
303
304 // When the user has deleted text, we don't allow inline autocomplete. Make
305 // sure to not flag cases like selecting part of the text and then pasting
306 // (or typing) the prefix of that selection. (We detect these by making
307 // sure the caret, which should be after any insertion, hasn't moved
308 // forward of the old selection start.)
309 state_changes.just_deleted_text =
310 before.text.length() > after.text.length() &&
311 after.sel_start <= std::min(before.sel_start, before.sel_end);
312 if (RichAutocompletionEitherNonPrefixOrSplitEnabled()) {
313 state_changes.just_deleted_text =
314 state_changes.just_deleted_text &&
315 after.sel_start <=
316 std::max(before.sel_start, before.sel_end) - before.all_sel_length;
317 }
318
319 return state_changes;
320 }
321
OmniboxView(OmniboxEditController * controller,std::unique_ptr<OmniboxClient> client)322 OmniboxView::OmniboxView(OmniboxEditController* controller,
323 std::unique_ptr<OmniboxClient> client)
324 : controller_(controller) {
325 // |client| can be null in tests.
326 if (client) {
327 model_.reset(new OmniboxEditModel(this, controller, std::move(client)));
328 }
329 }
330
TextChanged()331 void OmniboxView::TextChanged() {
332 EmphasizeURLComponents();
333 if (model_)
334 model_->OnChanged();
335 }
336
UpdateTextStyle(const base::string16 & display_text,const bool text_is_url,const AutocompleteSchemeClassifier & classifier)337 void OmniboxView::UpdateTextStyle(
338 const base::string16& display_text,
339 const bool text_is_url,
340 const AutocompleteSchemeClassifier& classifier) {
341 if (!text_is_url) {
342 SetEmphasis(true, gfx::Range::InvalidRange());
343 return;
344 }
345
346 enum DemphasizeComponents {
347 EVERYTHING,
348 ALL_BUT_SCHEME,
349 ALL_BUT_HOST,
350 NOTHING,
351 } deemphasize = NOTHING;
352
353 url::Component scheme, host;
354 AutocompleteInput::ParseForEmphasizeComponents(display_text, classifier,
355 &scheme, &host);
356
357 const base::string16 url_scheme =
358 display_text.substr(scheme.begin, scheme.len);
359 // Extension IDs are not human-readable, so deemphasize everything to draw
360 // attention to the human-readable name in the location icon text.
361 // Data URLs are rarely human-readable and can be used for spoofing, so draw
362 // attention to the scheme to emphasize "this is just a bunch of data".
363 // For normal URLs, the host is the best proxy for "identity".
364 if (url_scheme == base::UTF8ToUTF16(extensions::kExtensionScheme))
365 deemphasize = EVERYTHING;
366 else if (url_scheme == base::UTF8ToUTF16(url::kDataScheme))
367 deemphasize = ALL_BUT_SCHEME;
368 else if (host.is_nonempty())
369 deemphasize = ALL_BUT_HOST;
370
371 gfx::Range scheme_range = scheme.is_nonempty()
372 ? gfx::Range(scheme.begin, scheme.end())
373 : gfx::Range::InvalidRange();
374 switch (deemphasize) {
375 case EVERYTHING:
376 SetEmphasis(false, gfx::Range::InvalidRange());
377 break;
378 case NOTHING:
379 SetEmphasis(true, gfx::Range::InvalidRange());
380 break;
381 case ALL_BUT_SCHEME:
382 DCHECK(scheme_range.IsValid());
383 SetEmphasis(false, gfx::Range::InvalidRange());
384 SetEmphasis(true, scheme_range);
385 break;
386 case ALL_BUT_HOST:
387 SetEmphasis(false, gfx::Range::InvalidRange());
388 SetEmphasis(true, gfx::Range(host.begin, host.end()));
389 break;
390 }
391
392 // Emphasize the scheme for security UI display purposes (if necessary).
393 if (!model()->user_input_in_progress() && scheme_range.IsValid())
394 UpdateSchemeStyle(scheme_range);
395 }
396