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