blob: d06f21f0ef75539ccc011e73b17d80eb074cce81 [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// This file defines helper functions shared by the various implementations
// of OmniboxView.
#include "components/omnibox/browser/omnibox_view.h"
#include <algorithm>
#include <memory>
#include <string>
#include <utility>
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/omnibox/browser/autocomplete_controller.h"
#include "components/omnibox/browser/autocomplete_input.h"
#include "components/omnibox/browser/autocomplete_match.h"
#include "components/omnibox/browser/location_bar_model.h"
#include "components/omnibox/browser/omnibox_edit_controller.h"
#include "components/omnibox/browser/omnibox_edit_model.h"
#include "components/omnibox/browser/omnibox_field_trial.h"
#include "components/omnibox/common/omnibox_features.h"
#include "extensions/buildflags/buildflags.h"
#include "ui/base/l10n/l10n_util.h"
#include "url/url_constants.h"
#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
#include "ui/gfx/paint_vector_icon.h"
#endif
#if BUILDFLAG(ENABLE_EXTENSIONS)
// GN doesn't understand conditional includes, so we need nogncheck here.
#include "extensions/common/constants.h" // nogncheck
#endif
namespace {
// Return true if either non prefix or split autocompletion is enabled.
bool RichAutocompletionEitherNonPrefixOrSplitEnabled() {
return OmniboxFieldTrial::kRichAutocompletionAutocompleteNonPrefixAll.Get() ||
OmniboxFieldTrial::
kRichAutocompletionAutocompleteNonPrefixShortcutProvider.Get() ||
OmniboxFieldTrial::kRichAutocompletionSplitTitleCompletion.Get() ||
OmniboxFieldTrial::kRichAutocompletionSplitUrlCompletion.Get();
}
} // namespace
OmniboxView::State::State() = default;
OmniboxView::State::State(const State& state) = default;
// static
std::u16string OmniboxView::StripJavascriptSchemas(const std::u16string& text) {
const std::u16string kJsPrefix(
base::StrCat({url::kJavaScriptScheme16, u":"}));
bool found_JavaScript = false;
size_t i = 0;
// Find the index of the first character that isn't whitespace, a control
// character, or a part of a JavaScript: scheme.
while (i < text.size()) {
if (base::IsUnicodeWhitespace(text[i]) || (text[i] < 0x20)) {
++i;
} else {
if (!base::EqualsCaseInsensitiveASCII(text.substr(i, kJsPrefix.length()),
kJsPrefix))
break;
// We've found a JavaScript scheme. Continue searching to ensure that
// strings like "javascript:javascript:alert()" are fully stripped.
found_JavaScript = true;
i += kJsPrefix.length();
}
}
// If we found any "JavaScript:" schemes in the text, return the text starting
// at the first non-whitespace/control character after the last instance of
// the scheme.
if (found_JavaScript)
return text.substr(i);
return text;
}
// static
std::u16string OmniboxView::SanitizeTextForPaste(const std::u16string& text) {
if (text.empty())
return std::u16string(); // Nothing to do.
size_t end = text.find_first_not_of(base::kWhitespaceUTF16);
if (end == std::u16string::npos)
return u" "; // Convert all-whitespace to single space.
// Because |end| points at the first non-whitespace character, the loop
// below will skip leading whitespace.
// Reserve space for the sanitized output.
std::u16string output;
output.reserve(text.size()); // Guaranteed to be large enough.
// Copy all non-whitespace sequences.
// Do not copy trailing whitespace.
// Copy all other whitespace sequences that do not contain CR/LF.
// Convert all other whitespace sequences that do contain CR/LF to either ' '
// or nothing, depending on whether there are any other sequences that do not
// contain CR/LF.
bool output_needs_lf_conversion = false;
bool seen_non_lf_whitespace = false;
const auto copy_range = [&text, &output](size_t begin, size_t end) {
output +=
text.substr(begin, (end == std::u16string::npos) ? end : (end - begin));
};
constexpr char16_t kNewline[] = {'\n', 0};
constexpr char16_t kSpace[] = {' ', 0};
while (true) {
// Copy this non-whitespace sequence.
size_t begin = end;
end = text.find_first_of(base::kWhitespaceUTF16, begin + 1);
copy_range(begin, end);
// Now there is either a whitespace sequence, or the end of the string.
if (end != std::u16string::npos) {
// There is a whitespace sequence; see if it contains CR/LF.
begin = end;
end = text.find_first_not_of(base::kWhitespaceNoCrLfUTF16, begin);
if ((end != std::u16string::npos) && (text[end] != '\n') &&
(text[end] != '\r')) {
// Found a non-trailing whitespace sequence without CR/LF. Copy it.
seen_non_lf_whitespace = true;
copy_range(begin, end);
continue;
}
}
// |end| either points at the end of the string or a CR/LF.
if (end != std::u16string::npos)
end = text.find_first_not_of(base::kWhitespaceUTF16, end + 1);
if (end == std::u16string::npos)
break; // Ignore any trailing whitespace.
// The preceding whitespace sequence contained CR/LF. Convert to a single
// LF that we'll fix up below the loop.
output_needs_lf_conversion = true;
output += '\n';
}
// Convert LFs to ' ' or '' depending on whether there were non-LF whitespace
// sequences.
if (output_needs_lf_conversion) {
base::ReplaceChars(output, kNewline,
seen_non_lf_whitespace ? kSpace : std::u16string(),
&output);
}
return StripJavascriptSchemas(output);
}
OmniboxView::~OmniboxView() = default;
void OmniboxView::OpenMatch(const AutocompleteMatch& match,
WindowOpenDisposition disposition,
const GURL& alternate_nav_url,
const std::u16string& pasted_text,
size_t selected_line,
base::TimeTicks match_selection_timestamp) {
// Invalid URLs such as chrome://history can end up here.
if (!match.destination_url.is_valid() || !model_)
return;
model_->OpenMatch(match, disposition, alternate_nav_url, pasted_text,
selected_line, match_selection_timestamp);
}
bool OmniboxView::IsEditingOrEmpty() const {
return (model_.get() && model_->user_input_in_progress()) ||
(GetOmniboxTextLength() == 0);
}
// TODO (manukh) OmniboxView::GetIcon is very similar to
// OmniboxPopupModel::GetMatchIcon. They contain certain inconsistencies
// concerning what flags are required to display url favicons and bookmark star
// icons. OmniboxPopupModel::GetMatchIcon also doesn't display default search
// provider icons. It's possible they have other inconsistencies as well. We may
// want to consider reusing the same code for both the popup and omnibox icons.
ui::ImageModel OmniboxView::GetIcon(int dip_size,
SkColor color,
IconFetchedCallback on_icon_fetched) const {
#if BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_IOS)
// This is used on desktop only.
NOTREACHED();
return ui::ImageModel();
#else
// For tests, model_ will be null.
if (!model_) {
AutocompleteMatch fake_match;
fake_match.type = AutocompleteMatchType::URL_WHAT_YOU_TYPED;
const gfx::VectorIcon& vector_icon = fake_match.GetVectorIcon(false);
return ui::ImageModel::FromVectorIcon(vector_icon, color, dip_size);
}
if (model_->ShouldShowCurrentPageIcon()) {
LocationBarModel* location_bar_model = controller_->GetLocationBarModel();
return ui::ImageModel::FromVectorIcon(location_bar_model->GetVectorIcon(),
color, dip_size);
}
gfx::Image favicon;
AutocompleteMatch match = model_->CurrentMatch(nullptr);
if (AutocompleteMatch::IsSearchType(match.type)) {
// For search queries, display default search engine's favicon.
favicon = model_->client()->GetFaviconForDefaultSearchProvider(
std::move(on_icon_fetched));
} else {
// For site suggestions, display site's favicon.
favicon = model_->client()->GetFaviconForPageUrl(
match.destination_url, std::move(on_icon_fetched));
}
if (!favicon.IsEmpty())
return ui::ImageModel::FromImage(model_->client()->GetSizedIcon(favicon));
// If the client returns an empty favicon, fall through to provide the
// generic vector icon. |on_icon_fetched| may or may not be called later.
// If it's never called, the vector icon we provide below should remain.
// For bookmarked suggestions, display bookmark icon.
bookmarks::BookmarkModel* bookmark_model =
model_->client()->GetBookmarkModel();
const bool is_bookmarked =
bookmark_model && bookmark_model->IsBookmarked(match.destination_url);
const gfx::VectorIcon& vector_icon = match.GetVectorIcon(is_bookmarked);
return ui::ImageModel::FromVectorIcon(vector_icon, color, dip_size);
#endif // BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_IOS)
}
void OmniboxView::SetUserText(const std::u16string& text) {
SetUserText(text, true);
}
void OmniboxView::SetUserText(const std::u16string& text, bool update_popup) {
if (model_)
model_->SetUserText(text);
SetWindowTextAndCaretPos(text, text.length(), update_popup, true);
}
void OmniboxView::RevertAll() {
CloseOmniboxPopup();
if (model_)
model_->Revert();
TextChanged();
}
void OmniboxView::CloseOmniboxPopup() {
if (model_)
model_->StopAutocomplete();
}
void OmniboxView::StartPrefetch(const AutocompleteInput& input) {
if (model_)
model_->autocomplete_controller()->StartPrefetch(input);
}
bool OmniboxView::IsImeShowingPopup() const {
// Default to claiming that the IME is not showing a popup, since hiding the
// omnibox dropdown is a bad user experience when we don't know for sure that
// we have to.
return false;
}
void OmniboxView::ShowVirtualKeyboardIfEnabled() {}
void OmniboxView::HideImeIfNeeded() {}
bool OmniboxView::IsIndicatingQueryRefinement() const {
// The default implementation always returns false. Mobile ports can override
// this method and implement as needed.
return false;
}
void OmniboxView::GetState(State* state) {
state->text = GetText();
state->keyword = model()->keyword();
state->is_keyword_selected = model()->is_keyword_selected();
GetSelectionBounds(&state->sel_start, &state->sel_end);
if (RichAutocompletionEitherNonPrefixOrSplitEnabled())
state->all_sel_length = GetAllSelectionsLength();
}
OmniboxView::StateChanges OmniboxView::GetStateChanges(const State& before,
const State& after) {
OmniboxView::StateChanges state_changes;
state_changes.old_text = &before.text;
state_changes.new_text = &after.text;
state_changes.new_sel_start = after.sel_start;
state_changes.new_sel_end = after.sel_end;
const bool old_sel_empty = before.sel_start == before.sel_end;
const bool new_sel_empty = after.sel_start == after.sel_end;
const bool sel_same_ignoring_direction =
std::min(before.sel_start, before.sel_end) ==
std::min(after.sel_start, after.sel_end) &&
std::max(before.sel_start, before.sel_end) ==
std::max(after.sel_start, after.sel_end);
state_changes.selection_differs =
(!old_sel_empty || !new_sel_empty) && !sel_same_ignoring_direction;
state_changes.text_differs = before.text != after.text;
state_changes.keyword_differs =
(after.is_keyword_selected != before.is_keyword_selected) ||
(after.is_keyword_selected && before.is_keyword_selected &&
after.keyword != before.keyword);
// When the user has deleted text, we don't allow inline autocomplete. Make
// sure to not flag cases like selecting part of the text and then pasting
// (or typing) the prefix of that selection. (We detect these by making
// sure the caret, which should be after any insertion, hasn't moved
// forward of the old selection start.)
state_changes.just_deleted_text =
before.text.length() > after.text.length() &&
after.sel_start <= std::min(before.sel_start, before.sel_end);
if (RichAutocompletionEitherNonPrefixOrSplitEnabled()) {
state_changes.just_deleted_text =
state_changes.just_deleted_text &&
after.sel_start <=
std::max(before.sel_start, before.sel_end) - before.all_sel_length;
}
return state_changes;
}
OmniboxView::OmniboxView(OmniboxEditController* controller,
std::unique_ptr<OmniboxClient> client)
: controller_(controller) {
// |client| can be null in tests.
if (client) {
model_ =
std::make_unique<OmniboxEditModel>(this, controller, std::move(client));
}
}
void OmniboxView::TextChanged() {
EmphasizeURLComponents();
if (model_)
model_->OnChanged();
}
void OmniboxView::UpdateTextStyle(
const std::u16string& display_text,
const bool text_is_url,
const AutocompleteSchemeClassifier& classifier) {
if (!text_is_url) {
SetEmphasis(true, gfx::Range::InvalidRange());
return;
}
enum DemphasizeComponents {
EVERYTHING,
ALL_BUT_SCHEME,
ALL_BUT_HOST,
NOTHING,
} deemphasize = NOTHING;
url::Component scheme, host;
AutocompleteInput::ParseForEmphasizeComponents(display_text, classifier,
&scheme, &host);
const std::u16string url_scheme =
display_text.substr(scheme.begin, scheme.len);
const bool is_extension_url =
#if BUILDFLAG(ENABLE_EXTENSIONS)
url_scheme == base::UTF8ToUTF16(extensions::kExtensionScheme);
#else
false;
#endif
// Extension IDs are not human-readable, so deemphasize everything to draw
// attention to the human-readable name in the location icon text.
// Data URLs are rarely human-readable and can be used for spoofing, so draw
// attention to the scheme to emphasize "this is just a bunch of data".
// For normal URLs, the host is the best proxy for "identity".
if (is_extension_url)
deemphasize = EVERYTHING;
else if (url_scheme == url::kDataScheme16)
deemphasize = ALL_BUT_SCHEME;
else if (host.is_nonempty())
deemphasize = ALL_BUT_HOST;
gfx::Range scheme_range = scheme.is_nonempty()
? gfx::Range(scheme.begin, scheme.end())
: gfx::Range::InvalidRange();
switch (deemphasize) {
case EVERYTHING:
SetEmphasis(false, gfx::Range::InvalidRange());
break;
case NOTHING:
SetEmphasis(true, gfx::Range::InvalidRange());
break;
case ALL_BUT_SCHEME:
DCHECK(scheme_range.IsValid());
SetEmphasis(false, gfx::Range::InvalidRange());
SetEmphasis(true, scheme_range);
break;
case ALL_BUT_HOST:
SetEmphasis(false, gfx::Range::InvalidRange());
SetEmphasis(true, gfx::Range(host.begin, host.end()));
break;
}
// Emphasize the scheme for security UI display purposes (if necessary).
if (!model()->user_input_in_progress() && scheme_range.IsValid())
UpdateSchemeStyle(scheme_range);
}