blob: 0169e1f00bf9ea9e5ee62909b5583e6b59d82f49 [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/webui/sanitized_image_source.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/ref_counted_memory.h"
#include "base/strings/strcat.h"
#include "base/test/mock_callback.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/test/base/testing_profile.h"
#include "components/signin/public/identity_manager/identity_test_environment.h"
#include "content/public/test/browser_task_environment.h"
#include "net/http/http_status_code.h"
#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
#include "services/network/test/test_url_loader_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/image/image.h"
#include "url/url_util.h"
using data_decoder::mojom::AnimationFramePtr;
namespace {
AnimationFramePtr MakeImageFrame(SkColor color) {
auto frame = data_decoder::mojom::AnimationFrame::New();
SkBitmap bitmap;
bitmap.allocN32Pixels(5, 5);
bitmap.eraseColor(color);
frame->bitmap = bitmap;
frame->duration = base::TimeDelta();
return frame;
}
} // namespace
MATCHER_P(MemoryEq, other, "Eq matcher for base::RefCountedMemory contents") {
return arg->Equals(other);
}
class MockDataDecoderDelegate
: public SanitizedImageSource::DataDecoderDelegate {
public:
MOCK_METHOD(void,
DecodeImage,
(const std::string& data,
SanitizedImageSource::DecodeImageCallback callback));
MOCK_METHOD(void,
DecodeAnimation,
(const std::string& data,
SanitizedImageSource::DecodeAnimationCallback callback));
};
class SanitizedImageSourceTest : public testing::Test {
public:
void SetUp() override {
profile_ = std::make_unique<TestingProfile>();
auto data_decoder_delegate = std::make_unique<MockDataDecoderDelegate>();
mock_data_decoder_delegate_ = data_decoder_delegate.get();
sanitized_image_source_ = std::make_unique<SanitizedImageSource>(
profile_.get(),
base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
&test_url_loader_factory_),
std::move(data_decoder_delegate));
}
void TearDown() override {
mock_data_decoder_delegate_ = nullptr;
sanitized_image_source_.reset();
profile_.reset();
test_url_loader_factory_.ClearResponses();
}
protected:
content::BrowserTaskEnvironment task_environment_;
std::unique_ptr<TestingProfile> profile_;
network::TestURLLoaderFactory test_url_loader_factory_;
raw_ptr<MockDataDecoderDelegate> mock_data_decoder_delegate_ = nullptr;
std::unique_ptr<SanitizedImageSource> sanitized_image_source_;
};
// Verifies that the image source can handle multiple requests in parallel.
TEST_F(SanitizedImageSourceTest, MultiRequest) {
std::vector<std::tuple<SkColor, std::string, std::string>> data(
{{SK_ColorRED, "https://foo.com/img.png", "abc"},
{SK_ColorBLUE, "https://bar.com/img.png", "def"},
{SK_ColorGREEN, "https://baz.com/img.png", "ghi"}});
// Set up expectations and mock data.
base::MockCallback<content::URLDataSource::GotDataCallback> callback;
for (const auto& datum : data) {
SkColor color;
std::string url;
std::string body;
std::tie(color, url, body) = datum;
EXPECT_CALL(*mock_data_decoder_delegate_, DecodeAnimation(body, testing::_))
.Times(1)
.WillOnce(
[color](const std::string&,
SanitizedImageSource::DecodeAnimationCallback callback) {
std::vector<AnimationFramePtr> frames;
frames.push_back(MakeImageFrame(color));
std::move(callback).Run(std::move(frames));
});
auto image = gfx::Image::CreateFrom1xBitmap(MakeImageFrame(color)->bitmap);
EXPECT_CALL(callback, Run(MemoryEq(image.As1xPNGBytes()))).Times(1);
}
// Issue requests.
for (const auto& datum : data) {
std::string url;
std::tie(std::ignore, url, std::ignore) = datum;
sanitized_image_source_->StartDataRequest(
GURL(base::StrCat({chrome::kChromeUIImageURL, "?", url})),
content::WebContents::Getter(), callback.Get());
}
ASSERT_EQ(data.size(),
static_cast<unsigned long>(test_url_loader_factory_.NumPending()));
// Answer requests and check correctness.
for (size_t i = 0; i < data.size(); i++) {
auto [color, url, body] = data[i];
auto* request = test_url_loader_factory_.GetPendingRequest(i);
EXPECT_EQ(network::mojom::CredentialsMode::kOmit,
request->request.credentials_mode);
EXPECT_EQ(url, request->request.url);
test_url_loader_factory_.SimulateResponseWithoutRemovingFromPendingList(
request, body);
}
task_environment_.RunUntilIdle();
}
// Verifies that the image source sends back an empty image in case the external
// image download fails.
TEST_F(SanitizedImageSourceTest, FailedLoad) {
constexpr char kImageUrl[] = "https://foo.com/img.png";
// Set up expectations and mock data.
test_url_loader_factory_.AddResponse(kImageUrl, "", net::HTTP_NOT_FOUND);
EXPECT_CALL(*mock_data_decoder_delegate_,
DecodeAnimation(testing::_, testing::_))
.Times(0);
base::MockCallback<content::URLDataSource::GotDataCallback> callback;
EXPECT_CALL(callback, Run(testing::IsNull())).Times(1);
// Issue request.
sanitized_image_source_->StartDataRequest(
GURL(base::StrCat({chrome::kChromeUIImageURL, "?", kImageUrl})),
content::WebContents::Getter(), callback.Get());
task_environment_.RunUntilIdle();
}
// Verifies that the image source sends back an error in case the external
// image is served via an HTTP scheme.
TEST_F(SanitizedImageSourceTest, HttpScheme) {
constexpr char kImageUrl[] = "http://foo.com/img.png";
// Set up expectations and mock data.
test_url_loader_factory_.AddResponse(kImageUrl, "abcd");
EXPECT_CALL(*mock_data_decoder_delegate_,
DecodeAnimation(testing::_, testing::_))
.Times(0);
base::MockCallback<content::URLDataSource::GotDataCallback> callback;
EXPECT_CALL(callback, Run(testing::IsNull())).Times(1);
// Issue request.
sanitized_image_source_->StartDataRequest(
GURL(base::StrCat({chrome::kChromeUIImageURL, "?", kImageUrl})),
content::WebContents::Getter(), callback.Get());
task_environment_.RunUntilIdle();
}
// Verifies that the image source ignores requests with a wrong URL.
TEST_F(SanitizedImageSourceTest, WrongUrl) {
// Set up expectations and mock data.
EXPECT_CALL(*mock_data_decoder_delegate_,
DecodeAnimation(testing::_, testing::_))
.Times(0);
base::MockCallback<content::URLDataSource::GotDataCallback> callback;
EXPECT_CALL(callback, Run(testing::IsNull())).Times(2);
// Issue request.
sanitized_image_source_->StartDataRequest(
GURL("chrome://abc?https://foo.com/img.png"),
content::WebContents::Getter(), callback.Get());
sanitized_image_source_->StartDataRequest(
GURL(base::StrCat({chrome::kChromeUIImageURL, "?abc"})),
content::WebContents::Getter(), callback.Get());
task_environment_.RunUntilIdle();
EXPECT_EQ(0, test_url_loader_factory_.NumPending());
}
// Verifies that the image source sends a Google Photos auth token with its data
// request if and only if asked to by URL specification.
TEST_F(SanitizedImageSourceTest, GooglePhotosImage) {
constexpr char kImageUrl[] = "https://lh3.googleusercontent.com/img.png";
base::MockCallback<content::URLDataSource::GotDataCallback> callback;
signin::IdentityTestEnvironment identity_test_env;
identity_test_env.MakePrimaryAccountAvailable("test@gmail.com",
signin::ConsentLevel::kSync);
sanitized_image_source_->set_identity_manager_for_test(
identity_test_env.identity_manager());
// Verify that by default, requests are sent with no auth token.
sanitized_image_source_->StartDataRequest(
GURL(base::StrCat({chrome::kChromeUIImageURL, "?", kImageUrl})),
content::WebContents::Getter(), callback.Get());
EXPECT_FALSE(identity_test_env.IsAccessTokenRequestPending());
ASSERT_EQ(1, test_url_loader_factory_.NumPending());
EXPECT_FALSE(
test_url_loader_factory_.GetPendingRequest(0)->request.headers.HasHeader(
net::HttpRequestHeaders::kAuthorization));
// Encode a URL so that it can be used as a param value.
url::RawCanonOutputT<char> encoded_url;
url::EncodeURIComponent(kImageUrl, &encoded_url);
EXPECT_GT(encoded_url.length(), 0u);
// Verify that param-formatted requests can be sent with auth tokens.
sanitized_image_source_->StartDataRequest(
GURL(base::StrCat({chrome::kChromeUIImageURL, "?url=", encoded_url.view(),
"&isGooglePhotos=true"})),
content::WebContents::Getter(), callback.Get());
ASSERT_EQ(1, test_url_loader_factory_.NumPending());
EXPECT_TRUE(identity_test_env.IsAccessTokenRequestPending());
identity_test_env.WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
"token", base::Time::Max());
EXPECT_FALSE(identity_test_env.IsAccessTokenRequestPending());
ASSERT_EQ(2, test_url_loader_factory_.NumPending());
EXPECT_TRUE(
test_url_loader_factory_.GetPendingRequest(1)->request.headers.HasHeader(
net::HttpRequestHeaders::kAuthorization));
// Verify that param-formatted requests can be sent without auth tokens.
sanitized_image_source_->StartDataRequest(
GURL(base::StrCat({chrome::kChromeUIImageURL, "?url=", encoded_url.view(),
"&isGooglePhotos=false"})),
content::WebContents::Getter(), callback.Get());
EXPECT_FALSE(identity_test_env.IsAccessTokenRequestPending());
ASSERT_EQ(3, test_url_loader_factory_.NumPending());
EXPECT_FALSE(
test_url_loader_factory_.GetPendingRequest(2)->request.headers.HasHeader(
net::HttpRequestHeaders::kAuthorization));
sanitized_image_source_->StartDataRequest(
GURL(base::StrCat(
{chrome::kChromeUIImageURL, "?url=", encoded_url.view()})),
content::WebContents::Getter(), callback.Get());
EXPECT_FALSE(identity_test_env.IsAccessTokenRequestPending());
ASSERT_EQ(4, test_url_loader_factory_.NumPending());
EXPECT_FALSE(
test_url_loader_factory_.GetPendingRequest(3)->request.headers.HasHeader(
net::HttpRequestHeaders::kAuthorization));
// Verify that no download is attempted when authentication fails.
sanitized_image_source_->StartDataRequest(
GURL(base::StrCat({chrome::kChromeUIImageURL, "?url=", encoded_url.view(),
"&isGooglePhotos=true"})),
content::WebContents::Getter(), callback.Get());
ASSERT_EQ(4, test_url_loader_factory_.NumPending());
EXPECT_TRUE(identity_test_env.IsAccessTokenRequestPending());
identity_test_env.WaitForAccessTokenRequestIfNecessaryAndRespondWithError(
GoogleServiceAuthError(GoogleServiceAuthError::REQUEST_CANCELED));
EXPECT_FALSE(identity_test_env.IsAccessTokenRequestPending());
ASSERT_EQ(4, test_url_loader_factory_.NumPending());
// Verify that no auth token is sent for URLs not served by Google Photos.
constexpr char kBadImageUrl[] = "https://foo.com/img.png";
url::RawCanonOutputT<char> encoded_bad_url;
url::EncodeURIComponent(kBadImageUrl, &encoded_bad_url);
EXPECT_GT(encoded_bad_url.length(), 0u);
sanitized_image_source_->StartDataRequest(
GURL(base::StrCat({chrome::kChromeUIImageURL, "?url=",
encoded_bad_url.view(), "&isGooglePhotos=true"})),
content::WebContents::Getter(), callback.Get());
EXPECT_FALSE(identity_test_env.IsAccessTokenRequestPending());
ASSERT_EQ(5, test_url_loader_factory_.NumPending());
EXPECT_FALSE(
test_url_loader_factory_.GetPendingRequest(4)->request.headers.HasHeader(
net::HttpRequestHeaders::kAuthorization));
}
TEST_F(SanitizedImageSourceTest, StaticImage) {
const std::string test_body = "abc";
const std::string test_url = "https://foo.com/img.png";
// Set up expectations and mock data.
base::MockCallback<content::URLDataSource::GotDataCallback> callback;
EXPECT_CALL(*mock_data_decoder_delegate_,
DecodeAnimation(test_body, testing::_))
.Times(1)
.WillOnce([](const std::string&,
SanitizedImageSource::DecodeAnimationCallback callback) {
std::vector<AnimationFramePtr> frames;
frames.push_back(MakeImageFrame(SK_ColorRED));
std::move(callback).Run(std::move(frames));
});
auto image =
gfx::Image::CreateFrom1xBitmap(MakeImageFrame(SK_ColorRED)->bitmap);
EXPECT_CALL(callback, Run(MemoryEq(image.As1xPNGBytes()))).Times(1);
// Issue requests.
sanitized_image_source_->StartDataRequest(
GURL(base::StrCat({chrome::kChromeUIImageURL, "?", test_url})),
content::WebContents::Getter(), callback.Get());
// Answer requests and check correctness.
auto* request = test_url_loader_factory_.GetPendingRequest(0);
EXPECT_EQ(network::mojom::CredentialsMode::kOmit,
request->request.credentials_mode);
EXPECT_EQ(test_url, request->request.url);
test_url_loader_factory_.SimulateResponseWithoutRemovingFromPendingList(
request, test_body);
task_environment_.RunUntilIdle();
}
TEST_F(SanitizedImageSourceTest, StaticImageWithWebPEncode) {
const std::string test_body = "abc";
const std::string test_url = "https://foo.com/img.png";
// Set up expectations and mock data.
base::MockCallback<content::URLDataSource::GotDataCallback> callback;
EXPECT_CALL(*mock_data_decoder_delegate_,
DecodeAnimation(test_body, testing::_))
.Times(1)
.WillOnce([](const std::string&,
SanitizedImageSource::DecodeAnimationCallback callback) {
std::vector<AnimationFramePtr> frames;
frames.push_back(MakeImageFrame(SK_ColorRED));
std::move(callback).Run(std::move(frames));
});
auto image =
gfx::Image::CreateFrom1xBitmap(MakeImageFrame(SK_ColorRED)->bitmap);
EXPECT_CALL(callback, Run(testing::_))
.Times(1)
.WillOnce([](scoped_refptr<base::RefCountedMemory> bytes) {
std::string data_string(reinterpret_cast<char const*>(bytes->data()));
// Make sure the image is encoded into WebP format.
EXPECT_TRUE(base::StartsWith(data_string, "RIFF"));
});
// Issue requests.
sanitized_image_source_->StartDataRequest(
GURL(base::StrCat(
{chrome::kChromeUIImageURL, "?url=", test_url, "&encodeType=webp"})),
content::WebContents::Getter(), callback.Get());
// Answer requests and check correctness.
auto* request = test_url_loader_factory_.GetPendingRequest(0);
EXPECT_EQ(network::mojom::CredentialsMode::kOmit,
request->request.credentials_mode);
EXPECT_EQ(test_url, request->request.url);
test_url_loader_factory_.SimulateResponseWithoutRemovingFromPendingList(
request, test_body);
task_environment_.RunUntilIdle();
}
#if BUILDFLAG(IS_CHROMEOS)
TEST_F(SanitizedImageSourceTest, AnimatedImage) {
const std::string test_body = "abc";
const std::string test_url = "https://foo.com/img.png";
// Set up expectations and mock data.
base::MockCallback<content::URLDataSource::GotDataCallback> callback;
EXPECT_CALL(*mock_data_decoder_delegate_,
DecodeAnimation(test_body, testing::_))
.Times(1)
.WillOnce([](const std::string&,
SanitizedImageSource::DecodeAnimationCallback callback) {
std::vector<AnimationFramePtr> frames;
frames.push_back(MakeImageFrame(SK_ColorRED));
frames.push_back(MakeImageFrame(SK_ColorBLUE));
frames.push_back(MakeImageFrame(SK_ColorGREEN));
std::move(callback).Run(std::move(frames));
});
auto image =
gfx::Image::CreateFrom1xBitmap(MakeImageFrame(SK_ColorRED)->bitmap);
EXPECT_CALL(callback, Run(testing::_))
.Times(1)
.WillOnce([](scoped_refptr<base::RefCountedMemory> bytes) {
std::string data_string(reinterpret_cast<char const*>(bytes->data()));
// Make sure the image is encoded into WebP format.
EXPECT_TRUE(base::StartsWith(data_string, "RIFF"));
});
// Issue requests.
sanitized_image_source_->StartDataRequest(
GURL(base::StrCat({chrome::kChromeUIImageURL, "?", test_url})),
content::WebContents::Getter(), callback.Get());
// Answer requests and check correctness.
auto* request = test_url_loader_factory_.GetPendingRequest(0);
EXPECT_EQ(network::mojom::CredentialsMode::kOmit,
request->request.credentials_mode);
EXPECT_EQ(test_url, request->request.url);
test_url_loader_factory_.SimulateResponseWithoutRemovingFromPendingList(
request, test_body);
task_environment_.RunUntilIdle();
}
TEST_F(SanitizedImageSourceTest, AnimatedImageWithStaticEncode) {
const std::string test_body = "abc";
const std::string test_url = "https://foo.com/img.png";
// Set up expectations and mock data.
base::MockCallback<content::URLDataSource::GotDataCallback> callback;
EXPECT_CALL(*mock_data_decoder_delegate_, DecodeImage(test_body, testing::_))
.Times(1)
.WillOnce([](const std::string&,
SanitizedImageSource::DecodeImageCallback callback) {
std::move(callback).Run(std::move(MakeImageFrame(SK_ColorRED)->bitmap));
});
auto image =
gfx::Image::CreateFrom1xBitmap(MakeImageFrame(SK_ColorRED)->bitmap);
// Make sure the image is encoded into static PNG bytes.
EXPECT_CALL(callback, Run(MemoryEq(image.As1xPNGBytes()))).Times(1);
// Issue requests.
sanitized_image_source_->StartDataRequest(
GURL(base::StrCat({chrome::kChromeUIImageURL, "?url=", test_url,
"&staticEncode=true"})),
content::WebContents::Getter(), callback.Get());
// Answer requests and check correctness.
auto* request = test_url_loader_factory_.GetPendingRequest(0);
EXPECT_EQ(network::mojom::CredentialsMode::kOmit,
request->request.credentials_mode);
EXPECT_EQ(test_url, request->request.url);
test_url_loader_factory_.SimulateResponseWithoutRemovingFromPendingList(
request, test_body);
task_environment_.RunUntilIdle();
}
#endif // BUILDFLAG(IS_CHROMEOS)