blob: 5d908af3812cc514984d9a4f839a929d5676ba25 [file] [log] [blame]
// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/omnibox/browser/clipboard_provider.h"
#include <memory>
#include <string>
#include <utility>
#include "base/memory/ref_counted.h"
#include "base/run_loop.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "components/omnibox/browser/autocomplete_input.h"
#include "components/omnibox/browser/autocomplete_provider_listener.h"
#include "components/omnibox/browser/mock_autocomplete_provider_client.h"
#include "components/omnibox/browser/omnibox_field_trial.h"
#include "components/omnibox/browser/test_scheme_classifier.h"
#include "components/omnibox/common/omnibox_features.h"
#include "components/open_from_clipboard/fake_clipboard_recent_content.h"
#include "components/search_engines/search_engines_test_environment.h"
#include "components/search_engines/template_url_service_client.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/metrics_proto/omnibox_focus_type.pb.h"
#include "third_party/omnibox_proto/groups.pb.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_unittest_util.h"
#include "url/gurl.h"
#if !BUILDFLAG(IS_IOS)
#include "ui/base/clipboard/test/test_clipboard.h" // nogncheck
#endif
namespace {
const char kCurrentURL[] = "http://example.com/current";
const char kClipboardURL[] = "http://example.com/clipboard";
const char16_t kClipboardText[] = u"Search for me";
class CreateMatchWithContentCallbackWaiter {
public:
CreateMatchWithContentCallbackWaiter(
scoped_refptr<ClipboardProvider> provider,
AutocompleteMatch* match)
: received_(false) {
provider->UpdateClipboardMatchWithContent(
match, base::BindOnce(&CreateMatchWithContentCallbackWaiter::OnComplete,
weak_ptr_factory_.GetWeakPtr()));
}
void WaitForMatchUpdated() {
if (received_)
return;
base::RunLoop run_loop;
quit_closure_ = run_loop.QuitClosure();
run_loop.Run();
}
private:
void OnComplete() {
received_ = true;
if (quit_closure_)
std::move(quit_closure_).Run();
}
base::OnceClosure quit_closure_;
bool received_;
base::WeakPtrFactory<CreateMatchWithContentCallbackWaiter> weak_ptr_factory_{
this};
};
} // namespace
class ClipboardProviderTest : public testing::Test,
public AutocompleteProviderListener {
public:
ClipboardProviderTest()
: client_(new MockAutocompleteProviderClient()),
provider_(
new ClipboardProvider(client_.get(), this, &clipboard_content_)) {
SetClipboardUrl(GURL(kClipboardURL));
}
~ClipboardProviderTest() override = default;
void ClearClipboard() { clipboard_content_.SuppressClipboardContent(); }
void SetClipboardUrl(const GURL& url) {
clipboard_content_.SetClipboardURL(url, base::Minutes(9));
}
void SetClipboardText(const std::u16string& text) {
clipboard_content_.SetClipboardText(text, base::Minutes(9));
}
void SetClipboardImage(const gfx::Image& image) {
clipboard_content_.SetClipboardImage(image, base::Minutes(9));
}
bool IsClipboardEmpty() {
return clipboard_content_.GetRecentURLFromClipboard() == std::nullopt &&
clipboard_content_.GetRecentTextFromClipboard() == std::nullopt &&
!clipboard_content_.HasRecentImageFromClipboard();
}
AutocompleteInput CreateAutocompleteInput(
metrics::OmniboxFocusType focus_type) {
AutocompleteInput input(std::u16string(), metrics::OmniboxEventProto::OTHER,
classifier_);
input.set_current_url(GURL(kCurrentURL));
input.set_focus_type(focus_type);
return input;
}
void MatchesImageCallback(std::optional<AutocompleteMatch> match) {
matches_image_match_ = match;
}
protected:
// AutocompleteProviderListener:
void OnProviderUpdate(bool updated_matches,
const AutocompleteProvider* provider) override;
TestSchemeClassifier classifier_;
FakeClipboardRecentContent clipboard_content_;
search_engines::SearchEnginesTestEnvironment search_engines_test_environment_;
std::unique_ptr<MockAutocompleteProviderClient> client_;
scoped_refptr<ClipboardProvider> provider_;
std::optional<AutocompleteMatch> matches_image_match_;
base::test::TaskEnvironment task_environment_;
};
void ClipboardProviderTest::OnProviderUpdate(
bool updated_matches,
const AutocompleteProvider* provider) {
// No action required.
}
TEST_F(ClipboardProviderTest, NotFromOmniboxFocus) {
provider_->Start(
CreateAutocompleteInput(metrics::OmniboxFocusType::INTERACTION_DEFAULT),
false);
EXPECT_TRUE(provider_->matches().empty());
}
TEST_F(ClipboardProviderTest, EmptyClipboard) {
client_->set_template_url_service(
search_engines_test_environment_.template_url_service());
ClearClipboard();
provider_->Start(
CreateAutocompleteInput(metrics::OmniboxFocusType::INTERACTION_FOCUS),
false);
EXPECT_TRUE(provider_->matches().empty());
}
#if !BUILDFLAG(IS_ANDROID)
TEST_F(ClipboardProviderTest, MatchesImage) {
client_->set_template_url_service(
search_engines_test_environment_.template_url_service());
gfx::Image test_image = gfx::test::CreateImage(/*width=*/10, /*height=*/10);
scoped_refptr<base::RefCountedMemory> image_bytes =
provider_->EncodeClipboardImage(*test_image.ToImageSkia());
ASSERT_TRUE(image_bytes);
provider_->ConstructImageMatchCallback(
base::BindOnce(&ClipboardProviderTest::MatchesImageCallback,
base::Unretained(this)),
image_bytes);
ASSERT_TRUE(matches_image_match_);
EXPECT_EQ(AutocompleteMatchType::CLIPBOARD_IMAGE, matches_image_match_->type);
}
#endif // !BUILDFLAG(IS_ANDROID)
TEST_F(ClipboardProviderTest, DeleteMatch) {
client_->set_template_url_service(
search_engines_test_environment_.template_url_service());
SetClipboardText(kClipboardText);
provider_->Start(
CreateAutocompleteInput(metrics::OmniboxFocusType::INTERACTION_FOCUS),
false);
ASSERT_EQ(provider_->matches().size(), 1U);
provider_->DeleteMatch(provider_->matches().back());
ASSERT_EQ(provider_->matches().size(), 0U);
ASSERT_TRUE(IsClipboardEmpty());
}
TEST_F(ClipboardProviderTest, CreateBlankURLMatchOnStart) {
SetClipboardUrl(GURL(kClipboardURL));
client_->set_template_url_service(
search_engines_test_environment_.template_url_service());
provider_->Start(
CreateAutocompleteInput(metrics::OmniboxFocusType::INTERACTION_FOCUS),
false);
ASSERT_GE(provider_->matches().size(), 1U);
EXPECT_EQ(AutocompleteMatchType::CLIPBOARD_URL,
provider_->matches().back().type);
// Check the match is empty.
EXPECT_TRUE(provider_->matches().back().destination_url.is_empty());
}
TEST_F(ClipboardProviderTest, CreateBlankTextMatchOnStart) {
client_->set_template_url_service(
search_engines_test_environment_.template_url_service());
SetClipboardText(kClipboardText);
provider_->Start(
CreateAutocompleteInput(metrics::OmniboxFocusType::INTERACTION_FOCUS),
false);
ASSERT_GE(provider_->matches().size(), 1U);
EXPECT_EQ(AutocompleteMatchType::CLIPBOARD_TEXT,
provider_->matches().back().type);
// Check the match is empty.
EXPECT_TRUE(provider_->matches().back().contents.empty());
EXPECT_TRUE(provider_->matches().back().fill_into_edit.empty());
}
TEST_F(ClipboardProviderTest, CreateBlankImageMatchOnStart) {
client_->set_template_url_service(
search_engines_test_environment_.template_url_service());
gfx::Image test_image = gfx::test::CreateImage(/*width=*/10, /*height=*/10);
SetClipboardImage(test_image);
provider_->Start(
CreateAutocompleteInput(metrics::OmniboxFocusType::INTERACTION_FOCUS),
false);
ASSERT_GE(provider_->matches().size(), 1U);
EXPECT_EQ(AutocompleteMatchType::CLIPBOARD_IMAGE,
provider_->matches().back().type);
EXPECT_FALSE(provider_->matches().back().post_content.get());
}
TEST_F(ClipboardProviderTest, SkipImageMatchGivenWantAsynchronousMatchesFalse) {
client_->set_template_url_service(
search_engines_test_environment_.template_url_service());
gfx::Image test_image = gfx::test::CreateImage(/*width=*/10, /*height=*/10);
SetClipboardImage(test_image);
// When `input.omit_asynchronous_matches` is set to true, the clipboard
// provider should skip any asynchronous logic associated with creating an
// image match.
AutocompleteInput input =
CreateAutocompleteInput(metrics::OmniboxFocusType::INTERACTION_FOCUS);
input.set_omit_asynchronous_matches(true);
provider_->Start(input, false);
ASSERT_TRUE(provider_->done());
ASSERT_TRUE(provider_->matches().empty());
}
TEST_F(ClipboardProviderTest, CreateURLMatchWithContent) {
client_->set_template_url_service(
search_engines_test_environment_.template_url_service());
{
SCOPED_TRACE(kClipboardURL);
SetClipboardUrl(GURL(kClipboardURL));
EXPECT_CALL(*client_.get(), GetSchemeClassifier())
.WillOnce(testing::ReturnRef(classifier_));
AutocompleteMatch match = provider_->NewBlankURLMatch();
CreateMatchWithContentCallbackWaiter waiter(provider_, &match);
waiter.WaitForMatchUpdated();
EXPECT_EQ(GURL(kClipboardURL), match.destination_url);
EXPECT_EQ(AutocompleteMatchType::CLIPBOARD_URL, match.type);
}
{
SCOPED_TRACE("`javascript:` sanitization");
SetClipboardUrl(GURL("javascript:alert()"));
AutocompleteMatch match = provider_->NewBlankURLMatch();
CreateMatchWithContentCallbackWaiter waiter(provider_, &match);
waiter.WaitForMatchUpdated();
EXPECT_EQ(u"alert()", match.contents);
EXPECT_EQ(u"alert()", match.fill_into_edit);
EXPECT_EQ(AutocompleteMatchType::CLIPBOARD_TEXT, match.type);
}
{
SCOPED_TRACE("`JavaScript:` sanitization");
SetClipboardUrl(GURL("JavaScript:alert()"));
AutocompleteMatch match = provider_->NewBlankURLMatch();
CreateMatchWithContentCallbackWaiter waiter(provider_, &match);
waiter.WaitForMatchUpdated();
EXPECT_EQ(u"alert()", match.contents);
EXPECT_EQ(u"alert()", match.fill_into_edit);
EXPECT_EQ(AutocompleteMatchType::CLIPBOARD_TEXT, match.type);
}
}
TEST_F(ClipboardProviderTest, CreateTextMatchWithContent) {
client_->set_template_url_service(
search_engines_test_environment_.template_url_service());
{
SCOPED_TRACE(kClipboardText);
SetClipboardText(kClipboardText);
AutocompleteMatch match = provider_->NewBlankTextMatch();
CreateMatchWithContentCallbackWaiter waiter(provider_, &match);
waiter.WaitForMatchUpdated();
EXPECT_EQ(kClipboardText, match.contents);
EXPECT_EQ(kClipboardText, match.fill_into_edit);
EXPECT_EQ(AutocompleteMatchType::CLIPBOARD_TEXT, match.type);
}
{
SCOPED_TRACE("`javascript:` sanitization");
SetClipboardText(u"javascript:alert()");
AutocompleteMatch match = provider_->NewBlankTextMatch();
CreateMatchWithContentCallbackWaiter waiter(provider_, &match);
waiter.WaitForMatchUpdated();
EXPECT_EQ(u"alert()", match.contents);
EXPECT_EQ(u"alert()", match.fill_into_edit);
EXPECT_EQ(AutocompleteMatchType::CLIPBOARD_TEXT, match.type);
}
{
SCOPED_TRACE("`JavaScript:` sanitization");
SetClipboardText(u"JavaScript:alert()");
AutocompleteMatch match = provider_->NewBlankTextMatch();
CreateMatchWithContentCallbackWaiter waiter(provider_, &match);
waiter.WaitForMatchUpdated();
EXPECT_EQ(u"alert()", match.contents);
EXPECT_EQ(u"alert()", match.fill_into_edit);
EXPECT_EQ(AutocompleteMatchType::CLIPBOARD_TEXT, match.type);
}
{
SCOPED_TRACE("`javascript:javascript:` sanitization");
SetClipboardText(u"javascript:\n javascript:alert()");
AutocompleteMatch match = provider_->NewBlankTextMatch();
CreateMatchWithContentCallbackWaiter waiter(provider_, &match);
waiter.WaitForMatchUpdated();
EXPECT_EQ(u"alert()", match.contents);
EXPECT_EQ(u"alert()", match.fill_into_edit);
EXPECT_EQ(AutocompleteMatchType::CLIPBOARD_TEXT, match.type);
}
}
TEST_F(ClipboardProviderTest, CreateImageMatchWithContent) {
gfx::Image test_image = gfx::test::CreateImage(/*width=*/10, /*height=*/10);
SetClipboardImage(test_image);
client_->set_template_url_service(
search_engines_test_environment_.template_url_service());
AutocompleteMatch match = provider_->NewBlankImageMatch();
CreateMatchWithContentCallbackWaiter waiter(provider_, &match);
waiter.WaitForMatchUpdated();
EXPECT_FALSE(match.post_content->first.empty());
EXPECT_FALSE(match.post_content->second.empty());
EXPECT_EQ(AutocompleteMatchType::CLIPBOARD_IMAGE, match.type);
}
#if BUILDFLAG(IS_ANDROID)
TEST_F(ClipboardProviderTest, Android_MergedWithPZPSGroupOnNTP) {
SetClipboardText(kClipboardText);
client_->set_template_url_service(
search_engines_test_environment_.template_url_service());
AutocompleteInput input(std::u16string(), metrics::OmniboxEventProto::NTP,
classifier_);
input.set_focus_type(metrics::OmniboxFocusType::INTERACTION_FOCUS);
provider_->Start(input, false);
// Expect the clipboard entry, but not the content. Content is not directly
// available on mobile devices - the user needs to explicitly ask to reveal
// the content.
ASSERT_EQ(provider_->matches().size(), 1U);
const auto& match = provider_->matches().back();
EXPECT_EQ(AutocompleteMatchType::CLIPBOARD_TEXT, match.type);
EXPECT_EQ(omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST,
match.suggestion_group_id);
}
TEST_F(ClipboardProviderTest, Android_StandaloneSuggestionOnSearchActivity) {
SetClipboardText(kClipboardText);
client_->set_template_url_service(
search_engines_test_environment_.template_url_service());
AutocompleteInput input(std::u16string(),
metrics::OmniboxEventProto::ANDROID_SHORTCUTS_WIDGET,
classifier_);
input.set_focus_type(metrics::OmniboxFocusType::INTERACTION_FOCUS);
provider_->Start(input, false);
// Expect the clipboard entry, but not the content. Content is not directly
// available on mobile devices - the user needs to explicitly ask to reveal
// the content.
ASSERT_EQ(provider_->matches().size(), 1U);
const auto& match = provider_->matches().back();
EXPECT_EQ(AutocompleteMatchType::CLIPBOARD_TEXT, match.type);
EXPECT_EQ(omnibox::GROUP_MOBILE_CLIPBOARD, match.suggestion_group_id);
}
TEST_F(ClipboardProviderTest, Android_StandaloneSuggestionInNonNTPContext) {
SetClipboardText(kClipboardText);
client_->set_template_url_service(
search_engines_test_environment_.template_url_service());
AutocompleteInput input =
CreateAutocompleteInput(metrics::OmniboxFocusType::INTERACTION_FOCUS);
provider_->Start(input, false);
// Expect the clipboard entry, but not the content. Content is not directly
// available on mobile devices - the user needs to explicitly ask to reveal
// the content.
ASSERT_EQ(provider_->matches().size(), 1U);
const auto& match = provider_->matches().back();
EXPECT_EQ(AutocompleteMatchType::CLIPBOARD_TEXT, match.type);
EXPECT_EQ(omnibox::GROUP_MOBILE_CLIPBOARD, match.suggestion_group_id);
}
#endif