Submit a sample of notification images to Safe Browsing
These will be scanned for social engineering behavior.
Only uploads if all of the following are true:
- User has opted in to SBER_LEVEL_SCOUT.
- Origin is not on CSD phishing whitelist.
- Device has sent < 5 reports in last 24 hours (see persistence TODO).
- Device is part of the enabled experiment group.
The notification image bitmap is downscaled to <= 512x512, encoded as a
PNG, then sent to the CSD server as a NotificationImageReportRequest
protobuf from chrome/common/safe_browsing/csd.proto.
BUG=678443
Review-Url: https://codereview.chromium.org/2637153002
Cr-Commit-Position: refs/heads/master@{#445365}
diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn
index 8991997..094508f 100644
--- a/chrome/browser/BUILD.gn
+++ b/chrome/browser/BUILD.gn
@@ -1993,6 +1993,8 @@
"safe_browsing/certificate_reporting_service.h",
"safe_browsing/certificate_reporting_service_factory.cc",
"safe_browsing/certificate_reporting_service_factory.h",
+ "safe_browsing/notification_image_reporter.cc",
+ "safe_browsing/notification_image_reporter.h",
"safe_browsing/permission_reporter.cc",
"safe_browsing/permission_reporter.h",
"safe_browsing/ping_manager.cc",
diff --git a/chrome/browser/notifications/platform_notification_service_impl.cc b/chrome/browser/notifications/platform_notification_service_impl.cc
index 25bcadd..a03b72c 100644
--- a/chrome/browser/notifications/platform_notification_service_impl.cc
+++ b/chrome/browser/notifications/platform_notification_service_impl.cc
@@ -21,6 +21,8 @@
#include "chrome/browser/profiles/profile_attributes_storage.h"
#include "chrome/browser/profiles/profile_io_data.h"
#include "chrome/browser/profiles/profile_manager.h"
+#include "chrome/browser/safe_browsing/ping_manager.h"
+#include "chrome/browser/safe_browsing/safe_browsing_service.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/common/pref_names.h"
#include "chrome/grit/generated_resources.h"
@@ -36,6 +38,7 @@
#include "content/public/common/notification_resources.h"
#include "content/public/common/platform_notification_data.h"
#include "extensions/features/features.h"
+#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/message_center/notification.h"
@@ -87,6 +90,18 @@
notification_id));
}
+void ReportNotificationImageOnIOThread(
+ scoped_refptr<safe_browsing::SafeBrowsingService> safe_browsing_service,
+ Profile* profile,
+ const GURL& origin,
+ const SkBitmap& image) {
+ DCHECK_CURRENTLY_ON(BrowserThread::IO);
+ if (!safe_browsing_service || !safe_browsing_service->enabled())
+ return;
+ safe_browsing_service->ping_manager()->ReportNotificationImage(
+ profile, safe_browsing_service->database_manager(), origin, image);
+}
+
} // namespace
// static
@@ -428,6 +443,13 @@
notification.set_type(message_center::NOTIFICATION_TYPE_IMAGE);
notification.set_image(
gfx::Image::CreateFrom1xBitmap(notification_resources.image));
+ // n.b. this should only be posted once per notification.
+ BrowserThread::PostTask(
+ BrowserThread::IO, FROM_HERE,
+ base::Bind(
+ &ReportNotificationImageOnIOThread,
+ make_scoped_refptr(g_browser_process->safe_browsing_service()),
+ profile, origin, notification_resources.image));
}
// Badges are only supported on Android, primarily because it's the only
diff --git a/chrome/browser/notifications/platform_notification_service_impl.h b/chrome/browser/notifications/platform_notification_service_impl.h
index feb64404..12c6e618 100644
--- a/chrome/browser/notifications/platform_notification_service_impl.h
+++ b/chrome/browser/notifications/platform_notification_service_impl.h
@@ -119,7 +119,8 @@
void OnCloseEventDispatchComplete(
content::PersistentNotificationStatus status);
- // Creates a new Web Notification-based Notification object.
+ // Creates a new Web Notification-based Notification object. Should only be
+ // called when the notification is first shown.
// TODO(peter): |delegate| can be a scoped_refptr, but properly passing this
// through requires changing a whole lot of Notification constructor calls.
Notification CreateNotificationFromData(
diff --git a/chrome/browser/safe_browsing/notification_image_reporter.cc b/chrome/browser/safe_browsing/notification_image_reporter.cc
new file mode 100644
index 0000000..984fc43
--- /dev/null
+++ b/chrome/browser/safe_browsing/notification_image_reporter.cc
@@ -0,0 +1,225 @@
+// Copyright 2017 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.
+
+#include "chrome/browser/safe_browsing/notification_image_reporter.h"
+
+#include <cmath>
+#include <vector>
+
+#include "base/bind.h"
+#include "base/feature_list.h"
+#include "base/logging.h"
+#include "base/memory/ptr_util.h"
+#include "base/memory/ref_counted_memory.h"
+#include "base/metrics/histogram_macros.h"
+#include "base/rand_util.h"
+#include "base/threading/sequenced_worker_pool.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/safe_browsing/safe_browsing_service.h"
+#include "chrome/common/safe_browsing/csd.pb.h"
+#include "components/safe_browsing_db/database_manager.h"
+#include "components/safe_browsing_db/safe_browsing_prefs.h"
+#include "components/variations/variations_associated_data.h"
+#include "content/public/browser/browser_thread.h"
+#include "net/base/net_errors.h"
+#include "net/url_request/report_sender.h"
+#include "skia/ext/image_operations.h"
+#include "third_party/skia/include/core/SkBitmap.h"
+#include "ui/gfx/codec/png_codec.h"
+#include "ui/gfx/geometry/size.h"
+#include "url/gurl.h"
+
+using content::BrowserThread;
+
+namespace safe_browsing {
+
+namespace {
+
+const size_t kMaxReportsPerDay = 5;
+const base::Feature kNotificationImageReporterFeature{
+ "NotificationImageReporterFeature", base::FEATURE_ENABLED_BY_DEFAULT};
+const char kReportChance[] = "ReportChance";
+const char kDefaultMimeType[] = "image/png";
+
+// Passed to ReportSender::Send as an ErrorCallback, so must take a GURL, but it
+// is unused.
+void LogReportResult(const GURL& url, int net_error) {
+ UMA_HISTOGRAM_SPARSE_SLOWLY("SafeBrowsing.NotificationImageReporter.NetError",
+ net_error);
+}
+
+} // namespace
+
+const char NotificationImageReporter::kReportingUploadUrl[] =
+ "https://safebrowsing.googleusercontent.com/safebrowsing/clientreport/"
+ "notification-image";
+
+NotificationImageReporter::NotificationImageReporter(
+ net::URLRequestContext* request_context)
+ : NotificationImageReporter(base::MakeUnique<net::ReportSender>(
+ request_context,
+ net::ReportSender::CookiesPreference::DO_NOT_SEND_COOKIES)) {}
+
+NotificationImageReporter::NotificationImageReporter(
+ std::unique_ptr<net::ReportSender> report_sender)
+ : report_sender_(std::move(report_sender)), weak_factory_on_io_(this) {
+ DCHECK_CURRENTLY_ON(BrowserThread::IO);
+}
+
+NotificationImageReporter::~NotificationImageReporter() {
+ DCHECK_CURRENTLY_ON(BrowserThread::IO);
+}
+
+void NotificationImageReporter::ReportNotificationImageOnIO(
+ Profile* profile,
+ const scoped_refptr<SafeBrowsingDatabaseManager>& database_manager,
+ const GURL& origin,
+ const SkBitmap& image) {
+ DCHECK_CURRENTLY_ON(BrowserThread::IO);
+ DCHECK(profile);
+ DCHECK_EQ(origin, origin.GetOrigin());
+ DCHECK(origin.is_valid());
+
+ // Skip whitelisted origins to cut down on report volume.
+ if (!database_manager || database_manager->MatchCsdWhitelistUrl(origin)) {
+ SkippedReporting();
+ return;
+ }
+
+ // Sample a Finch-controlled fraction only.
+ double report_chance = GetReportChance();
+ if (base::RandDouble() >= report_chance) {
+ SkippedReporting();
+ return;
+ }
+
+ // Avoid exceeding kMaxReportsPerDay.
+ base::Time a_day_ago = base::Time::Now() - base::TimeDelta::FromDays(1);
+ while (!report_times_.empty() &&
+ report_times_.front() < /* older than */ a_day_ago) {
+ report_times_.pop();
+ }
+ if (report_times_.size() >= kMaxReportsPerDay) {
+ SkippedReporting();
+ return;
+ }
+ // n.b. we write to report_times_ here even if we'll later end up skipping
+ // reporting because GetExtendedReportingLevel was not SBER_LEVEL_SCOUT. That
+ // saves us two thread hops, with the downside that we may underreport
+ // notifications on the first day that a user opts in to SBER_LEVEL_SCOUT.
+ report_times_.push(base::Time::Now());
+
+ BrowserThread::PostTask(
+ BrowserThread::UI, FROM_HERE,
+ base::Bind(&NotificationImageReporter::ReportNotificationImageOnUI,
+ weak_factory_on_io_.GetWeakPtr(), profile, origin, image));
+}
+
+double NotificationImageReporter::GetReportChance() const {
+ // Get the report_chance from the Finch experiment. If there is no active
+ // experiment, it will be set to the default of 0.
+ double report_chance = variations::GetVariationParamByFeatureAsDouble(
+ kNotificationImageReporterFeature, kReportChance, 0.0);
+
+ if (report_chance < 0.0 || report_chance > 1.0) {
+ DLOG(WARNING) << "Illegal value " << report_chance << " for the parameter "
+ << kReportChance << ". The value should be between 0 and 1.";
+ report_chance = 0.0;
+ }
+
+ return report_chance;
+}
+
+void NotificationImageReporter::SkippedReporting() {}
+
+// static
+void NotificationImageReporter::ReportNotificationImageOnUI(
+ const base::WeakPtr<NotificationImageReporter>& weak_this_on_io,
+ Profile* profile,
+ const GURL& origin,
+ const SkBitmap& image) {
+ DCHECK_CURRENTLY_ON(BrowserThread::UI);
+
+ // Skip reporting unless SBER2 Scout is enabled.
+ if (GetExtendedReportingLevel(*profile->GetPrefs()) != SBER_LEVEL_SCOUT) {
+ BrowserThread::PostTask(
+ BrowserThread::IO, FROM_HERE,
+ base::Bind(&NotificationImageReporter::SkippedReporting,
+ weak_this_on_io));
+ return;
+ }
+
+ BrowserThread::GetBlockingPool()->PostWorkerTask(
+ FROM_HERE,
+ base::Bind(
+ &NotificationImageReporter::DownscaleNotificationImageOnBlockingPool,
+ weak_this_on_io, origin, image));
+}
+
+// static
+void NotificationImageReporter::DownscaleNotificationImageOnBlockingPool(
+ const base::WeakPtr<NotificationImageReporter>& weak_this_on_io,
+ const GURL& origin,
+ const SkBitmap& image) {
+ DCHECK(BrowserThread::GetBlockingPool()->RunsTasksOnCurrentThread());
+
+ // Downscale to fit within 512x512. TODO(johnme): Get this from Finch.
+ const double MAX_SIZE = 512;
+ SkBitmap downscaled_image = image;
+ if ((image.width() > MAX_SIZE || image.height() > MAX_SIZE) &&
+ image.width() > 0 && image.height() > 0) {
+ double scale =
+ std::min(MAX_SIZE / image.width(), MAX_SIZE / image.height());
+ downscaled_image =
+ skia::ImageOperations::Resize(image, skia::ImageOperations::RESIZE_GOOD,
+ std::lround(scale * image.width()),
+ std::lround(scale * image.height()));
+ }
+
+ // Encode as PNG.
+ std::vector<unsigned char> png_bytes;
+ if (!gfx::PNGCodec::EncodeBGRASkBitmap(downscaled_image, false, &png_bytes)) {
+ NOTREACHED();
+ return;
+ }
+
+ BrowserThread::PostTask(
+ BrowserThread::IO, FROM_HERE,
+ base::Bind(&NotificationImageReporter::SendReportOnIO, weak_this_on_io,
+ origin, base::RefCountedBytes::TakeVector(&png_bytes),
+ gfx::Size(downscaled_image.width(), downscaled_image.height()),
+ gfx::Size(image.width(), image.height())));
+}
+
+void NotificationImageReporter::SendReportOnIO(
+ const GURL& origin,
+ scoped_refptr<base::RefCountedMemory> data,
+ const gfx::Size& dimensions,
+ const gfx::Size& original_dimensions) {
+ DCHECK_CURRENTLY_ON(BrowserThread::IO);
+
+ NotificationImageReportRequest report;
+ report.set_notification_origin(origin.spec());
+ report.mutable_image()->set_data(data->front(), data->size());
+ report.mutable_image()->set_mime_type(kDefaultMimeType);
+ report.mutable_image()->mutable_dimensions()->set_width(dimensions.width());
+ report.mutable_image()->mutable_dimensions()->set_height(dimensions.height());
+ if (dimensions != original_dimensions) {
+ report.mutable_image()->mutable_original_dimensions()->set_width(
+ original_dimensions.width());
+ report.mutable_image()->mutable_original_dimensions()->set_height(
+ original_dimensions.height());
+ }
+
+ std::string serialized_report;
+ report.SerializeToString(&serialized_report);
+ report_sender_->Send(
+ GURL(kReportingUploadUrl), "application/octet-stream", serialized_report,
+ base::Bind(&LogReportResult, GURL(kReportingUploadUrl), net::OK),
+ base::Bind(&LogReportResult));
+ // TODO(johnme): Consider logging bandwidth and/or duration to UMA.
+}
+
+} // namespace safe_browsing
diff --git a/chrome/browser/safe_browsing/notification_image_reporter.h b/chrome/browser/safe_browsing/notification_image_reporter.h
new file mode 100644
index 0000000..163b4b0
--- /dev/null
+++ b/chrome/browser/safe_browsing/notification_image_reporter.h
@@ -0,0 +1,109 @@
+// Copyright 2017 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.
+
+#ifndef CHROME_BROWSER_SAFE_BROWSING_NOTIFICATION_IMAGE_REPORTER_H_
+#define CHROME_BROWSER_SAFE_BROWSING_NOTIFICATION_IMAGE_REPORTER_H_
+
+#include <memory>
+#include <queue>
+
+#include "base/macros.h"
+#include "base/memory/ref_counted.h"
+#include "base/memory/weak_ptr.h"
+#include "base/time/time.h"
+
+class GURL;
+class Profile;
+class SkBitmap;
+
+namespace base {
+class RefCountedMemory;
+} // namespace base
+
+namespace gfx {
+class Size;
+} // namespace gfx
+
+namespace net {
+class ReportSender;
+class URLRequestContext;
+} // namespace net
+
+namespace safe_browsing {
+
+class SafeBrowsingDatabaseManager;
+
+// Provides functionality for building and sending reports about notification
+// content images to the Safe Browsing CSD server.
+class NotificationImageReporter {
+ public:
+ // CSD server URL to which notification image reports are sent.
+ static const char kReportingUploadUrl[];
+
+ explicit NotificationImageReporter(net::URLRequestContext* request_context);
+ virtual ~NotificationImageReporter();
+
+ // Report notification content image to SafeBrowsing CSD server if the user
+ // has opted in, the origin is not whitelisted, and it is randomly sampled.
+ // Can only be called on IO thread.
+ void ReportNotificationImageOnIO(
+ Profile* profile,
+ const scoped_refptr<SafeBrowsingDatabaseManager>& database_manager,
+ const GURL& origin,
+ const SkBitmap& image);
+
+ protected:
+ explicit NotificationImageReporter(
+ std::unique_ptr<net::ReportSender> report_sender);
+
+ // Get the percentage of images that should be reported from Finch.
+ virtual double GetReportChance() const;
+
+ // Tests may wish to override this to find out if reports are skipped. Called
+ // on the IO thread.
+ virtual void SkippedReporting();
+
+ private:
+ // Report notification content image to SafeBrowsing CSD server if necessary,
+ // by invoking DownscaleNotificationImageOnBlockingPool. This should be
+ // called on the IO thread and will invoke a member function on the IO thread
+ // using the IO-based WeapPtr.
+ static void ReportNotificationImageOnUI(
+ const base::WeakPtr<NotificationImageReporter>& weak_this_on_io,
+ Profile* profile,
+ const GURL& origin,
+ const SkBitmap& image);
+
+ // Downscales image to fit within 512x512 if necessary, and encodes as it PNG,
+ // then invokes SendReportOnIO. This should be called on a blocking pool
+ // thread and will invoke a member function on the IO thread using the
+ // IO-based WeakPtr.
+ static void DownscaleNotificationImageOnBlockingPool(
+ const base::WeakPtr<NotificationImageReporter>& weak_this_on_io,
+ const GURL& origin,
+ const SkBitmap& image);
+
+ // Serializes report using NotificationImageReportRequest protobuf defined in
+ // chrome/common/safe_browsing/csd.proto and sends it to CSD server.
+ void SendReportOnIO(const GURL& origin,
+ scoped_refptr<base::RefCountedMemory> data,
+ const gfx::Size& dimensions,
+ const gfx::Size& original_dimensions);
+
+ std::unique_ptr<net::ReportSender> report_sender_;
+
+ // Timestamps of when we sent notification images. Used to limit the number
+ // of requests that we send in a day. Only access on the IO thread.
+ // TODO(johnme): Serialize this so that it doesn't reset on browser restart.
+ std::queue<base::Time> report_times_;
+
+ // Keep this last. Only dereference these pointers on the IO thread.
+ base::WeakPtrFactory<NotificationImageReporter> weak_factory_on_io_;
+
+ DISALLOW_COPY_AND_ASSIGN(NotificationImageReporter);
+};
+
+} // namespace safe_browsing
+
+#endif // CHROME_BROWSER_SAFE_BROWSING_NOTIFICATION_IMAGE_REPORTER_H_
diff --git a/chrome/browser/safe_browsing/notification_image_reporter_unittest.cc b/chrome/browser/safe_browsing/notification_image_reporter_unittest.cc
new file mode 100644
index 0000000..a24fcea
--- /dev/null
+++ b/chrome/browser/safe_browsing/notification_image_reporter_unittest.cc
@@ -0,0 +1,285 @@
+// Copyright 2017 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.
+
+#include "chrome/browser/safe_browsing/notification_image_reporter.h"
+
+#include "base/callback.h"
+#include "base/memory/ptr_util.h"
+#include "base/run_loop.h"
+#include "base/test/scoped_feature_list.h"
+#include "chrome/browser/safe_browsing/mock_permission_report_sender.h"
+#include "chrome/browser/safe_browsing/ping_manager.h"
+#include "chrome/browser/safe_browsing/test_safe_browsing_service.h"
+#include "chrome/common/safe_browsing/csd.pb.h"
+#include "chrome/test/base/testing_browser_process.h"
+#include "chrome/test/base/testing_profile.h"
+#include "components/safe_browsing_db/safe_browsing_prefs.h"
+#include "components/safe_browsing_db/test_database_manager.h"
+#include "content/public/browser/browser_thread.h"
+#include "content/public/test/test_browser_thread_bundle.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/skia/include/core/SkBitmap.h"
+#include "third_party/skia/include/core/SkColor.h"
+#include "url/gurl.h"
+
+using content::BrowserThread;
+
+namespace safe_browsing {
+
+namespace {
+
+class TestingNotificationImageReporter : public NotificationImageReporter {
+ public:
+ explicit TestingNotificationImageReporter(
+ std::unique_ptr<net::ReportSender> report_sender)
+ : NotificationImageReporter(std::move(report_sender)) {}
+
+ void WaitForReportSkipped() {
+ base::RunLoop run_loop;
+ quit_closure_ = run_loop.QuitClosure();
+ run_loop.Run();
+ }
+
+ void SetReportingChance(bool reporting_chance) {
+ reporting_chance_ = reporting_chance;
+ }
+
+ protected:
+ double GetReportChance() const override { return reporting_chance_; }
+ void SkippedReporting() override {
+ DCHECK_CURRENTLY_ON(BrowserThread::IO);
+ BrowserThread::PostTask(
+ BrowserThread::UI, FROM_HERE,
+ base::Bind(&TestingNotificationImageReporter::SkippedReportingOnUI,
+ base::Unretained(this)));
+ }
+
+ private:
+ void SkippedReportingOnUI() {
+ if (quit_closure_) {
+ quit_closure_.Run();
+ quit_closure_.Reset();
+ }
+ }
+ base::Closure quit_closure_;
+ double reporting_chance_ = 1.0;
+};
+
+class FakeSafeBrowsingDatabaseManager : public TestSafeBrowsingDatabaseManager {
+ public:
+ bool MatchCsdWhitelistUrl(const GURL& url) override { return false; }
+
+ private:
+ ~FakeSafeBrowsingDatabaseManager() override {}
+};
+
+SkBitmap CreateBitmap(int width, int height) {
+ SkBitmap bitmap;
+ bitmap.allocN32Pixels(width, height);
+ bitmap.eraseColor(SK_ColorGREEN);
+ return bitmap;
+}
+
+} // namespace
+
+class NotificationImageReporterTest : public ::testing::Test {
+ public:
+ NotificationImageReporterTest();
+
+ void SetUp() override;
+ void TearDown() override;
+
+ private:
+ content::TestBrowserThreadBundle thread_bundle_; // Should be first member.
+
+ protected:
+ void SetExtendedReportingLevel(ExtendedReportingLevel level);
+ void ReportNotificationImage();
+
+ scoped_refptr<SafeBrowsingService> safe_browsing_service_;
+
+ std::unique_ptr<TestingProfile> profile_; // Written on UI, read on IO.
+
+ // Owned by |notification_image_reporter_|.
+ MockPermissionReportSender* mock_report_sender_;
+
+ TestingNotificationImageReporter* notification_image_reporter_;
+
+ std::unique_ptr<base::test::ScopedFeatureList> feature_list_;
+
+ GURL origin_; // Written on UI, read on IO.
+ SkBitmap image_; // Written on UI, read on IO.
+
+ private:
+ void SetUpOnIO();
+
+ void ReportNotificationImageOnIO();
+};
+
+NotificationImageReporterTest::NotificationImageReporterTest()
+ // Use REAL_IO_THREAD so DCHECK_CURRENTLY_ON distinguishes IO from UI.
+ : thread_bundle_(content::TestBrowserThreadBundle::REAL_IO_THREAD),
+ origin_("https://example.com") {
+ image_ = CreateBitmap(1 /* w */, 1 /* h */);
+}
+
+void NotificationImageReporterTest::SetUp() {
+ DCHECK_CURRENTLY_ON(BrowserThread::UI);
+
+ // Initialize SafeBrowsingService with FakeSafeBrowsingDatabaseManager.
+ TestSafeBrowsingServiceFactory sb_service_factory;
+ sb_service_factory.SetTestDatabaseManager(
+ new FakeSafeBrowsingDatabaseManager());
+ SafeBrowsingService::RegisterFactory(&sb_service_factory);
+ safe_browsing_service_ = sb_service_factory.CreateSafeBrowsingService();
+ SafeBrowsingService::RegisterFactory(nullptr);
+ TestingBrowserProcess::GetGlobal()->SetSafeBrowsingService(
+ safe_browsing_service_.get());
+ g_browser_process->safe_browsing_service()->Initialize();
+ base::RunLoop().RunUntilIdle(); // TODO(johnme): Might still be tasks on IO.
+
+ profile_ = base::MakeUnique<TestingProfile>();
+
+ base::RunLoop run_loop;
+ BrowserThread::PostTaskAndReply(
+ BrowserThread::IO, FROM_HERE,
+ base::Bind(&NotificationImageReporterTest::SetUpOnIO,
+ base::Unretained(this)),
+ run_loop.QuitClosure());
+ run_loop.Run();
+}
+
+void NotificationImageReporterTest::TearDown() {
+ TestingBrowserProcess::GetGlobal()->safe_browsing_service()->ShutDown();
+ base::RunLoop().RunUntilIdle(); // TODO(johnme): Might still be tasks on IO.
+ TestingBrowserProcess::GetGlobal()->SetSafeBrowsingService(nullptr);
+}
+
+void NotificationImageReporterTest::SetUpOnIO() {
+ DCHECK_CURRENTLY_ON(BrowserThread::IO);
+
+ mock_report_sender_ = new MockPermissionReportSender;
+ notification_image_reporter_ = new TestingNotificationImageReporter(
+ base::WrapUnique(mock_report_sender_));
+ safe_browsing_service_->ping_manager()->notification_image_reporter_ =
+ base::WrapUnique(notification_image_reporter_);
+}
+
+void NotificationImageReporterTest::SetExtendedReportingLevel(
+ ExtendedReportingLevel level) {
+ feature_list_ = base::MakeUnique<base::test::ScopedFeatureList>();
+ if (level == SBER_LEVEL_SCOUT)
+ feature_list_->InitWithFeatures({safe_browsing::kOnlyShowScoutOptIn}, {});
+
+ InitializeSafeBrowsingPrefs(profile_->GetPrefs());
+ SetExtendedReportingPref(profile_->GetPrefs(), level != SBER_LEVEL_OFF);
+}
+
+void NotificationImageReporterTest::ReportNotificationImage() {
+ DCHECK_CURRENTLY_ON(BrowserThread::UI);
+ BrowserThread::PostTask(
+ BrowserThread::IO, FROM_HERE,
+ base::Bind(&NotificationImageReporterTest::ReportNotificationImageOnIO,
+ base::Unretained(this)));
+}
+
+void NotificationImageReporterTest::ReportNotificationImageOnIO() {
+ DCHECK_CURRENTLY_ON(BrowserThread::IO);
+ if (!safe_browsing_service_->enabled())
+ return;
+ safe_browsing_service_->ping_manager()->ReportNotificationImage(
+ profile_.get(), safe_browsing_service_->database_manager(), origin_,
+ image_);
+}
+
+TEST_F(NotificationImageReporterTest, ReportSuccess) {
+ SetExtendedReportingLevel(SBER_LEVEL_SCOUT);
+
+ ReportNotificationImage();
+ mock_report_sender_->WaitForReportSent();
+
+ ASSERT_EQ(1, mock_report_sender_->GetAndResetNumberOfReportsSent());
+ EXPECT_EQ(GURL(NotificationImageReporter::kReportingUploadUrl),
+ mock_report_sender_->latest_report_uri());
+ EXPECT_EQ("application/octet-stream",
+ mock_report_sender_->latest_content_type());
+
+ NotificationImageReportRequest report;
+ ASSERT_TRUE(report.ParseFromString(mock_report_sender_->latest_report()));
+ EXPECT_EQ(origin_.spec(), report.notification_origin());
+ ASSERT_TRUE(report.has_image());
+ EXPECT_GT(report.image().data().size(), 0U);
+ ASSERT_TRUE(report.image().has_mime_type());
+ EXPECT_EQ(report.image().mime_type(), "image/png");
+ ASSERT_TRUE(report.image().has_dimensions());
+ EXPECT_EQ(1, report.image().dimensions().width());
+ EXPECT_EQ(1, report.image().dimensions().height());
+ EXPECT_FALSE(report.image().has_original_dimensions());
+}
+
+TEST_F(NotificationImageReporterTest, ImageDownscaling) {
+ SetExtendedReportingLevel(SBER_LEVEL_SCOUT);
+
+ image_ = CreateBitmap(640 /* w */, 360 /* h */);
+
+ ReportNotificationImage();
+ mock_report_sender_->WaitForReportSent();
+
+ NotificationImageReportRequest report;
+ ASSERT_TRUE(report.ParseFromString(mock_report_sender_->latest_report()));
+ ASSERT_TRUE(report.has_image());
+ EXPECT_GT(report.image().data().size(), 0U);
+ ASSERT_TRUE(report.image().has_dimensions());
+ EXPECT_EQ(512, report.image().dimensions().width());
+ EXPECT_EQ(288, report.image().dimensions().height());
+ ASSERT_TRUE(report.image().has_original_dimensions());
+ EXPECT_EQ(640, report.image().original_dimensions().width());
+ EXPECT_EQ(360, report.image().original_dimensions().height());
+}
+
+TEST_F(NotificationImageReporterTest, NoReportWithoutSBER) {
+ SetExtendedReportingLevel(SBER_LEVEL_OFF);
+
+ ReportNotificationImage();
+ notification_image_reporter_->WaitForReportSkipped();
+
+ EXPECT_EQ(0, mock_report_sender_->GetAndResetNumberOfReportsSent());
+}
+
+TEST_F(NotificationImageReporterTest, NoReportWithoutScout) {
+ SetExtendedReportingLevel(SBER_LEVEL_LEGACY);
+
+ ReportNotificationImage();
+ notification_image_reporter_->WaitForReportSkipped();
+
+ EXPECT_EQ(0, mock_report_sender_->GetAndResetNumberOfReportsSent());
+}
+
+TEST_F(NotificationImageReporterTest, NoReportWithoutReportingEnabled) {
+ SetExtendedReportingLevel(SBER_LEVEL_SCOUT);
+ notification_image_reporter_->SetReportingChance(0.0);
+
+ ReportNotificationImage();
+ notification_image_reporter_->WaitForReportSkipped();
+
+ EXPECT_EQ(0, mock_report_sender_->GetAndResetNumberOfReportsSent());
+}
+
+TEST_F(NotificationImageReporterTest, MaxReportsPerDay) {
+ SetExtendedReportingLevel(SBER_LEVEL_SCOUT);
+
+ const int kMaxReportsPerDay = 5;
+
+ for (int i = 0; i < kMaxReportsPerDay; i++) {
+ ReportNotificationImage();
+ mock_report_sender_->WaitForReportSent();
+ }
+ ReportNotificationImage();
+ notification_image_reporter_->WaitForReportSkipped();
+
+ EXPECT_EQ(kMaxReportsPerDay,
+ mock_report_sender_->GetAndResetNumberOfReportsSent());
+}
+
+} // namespace safe_browsing
diff --git a/chrome/browser/safe_browsing/ping_manager.cc b/chrome/browser/safe_browsing/ping_manager.cc
index 2a42e9b..b223a15f 100644
--- a/chrome/browser/safe_browsing/ping_manager.cc
+++ b/chrome/browser/safe_browsing/ping_manager.cc
@@ -12,6 +12,7 @@
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/values.h"
+#include "chrome/browser/safe_browsing/notification_image_reporter.h"
#include "chrome/browser/safe_browsing/permission_reporter.h"
#include "components/data_use_measurement/core/data_use_user_data.h"
#include "content/public/browser/browser_thread.h"
@@ -24,6 +25,7 @@
#include "net/url_request/url_request_context.h"
#include "net/url_request/url_request_context_getter.h"
#include "net/url_request/url_request_status.h"
+#include "third_party/skia/include/core/SkBitmap.h"
#include "url/gurl.h"
using content::BrowserThread;
@@ -82,8 +84,10 @@
DCHECK(!url_prefix_.empty());
if (request_context_getter) {
- permission_reporter_.reset(
- new PermissionReporter(request_context_getter->GetURLRequestContext()));
+ permission_reporter_ = base::MakeUnique<PermissionReporter>(
+ request_context_getter->GetURLRequestContext());
+ notification_image_reporter_ = base::MakeUnique<NotificationImageReporter>(
+ request_context_getter->GetURLRequestContext());
net_log_ = net::NetLogWithSource::Make(
request_context_getter->GetURLRequestContext()->net_log(),
@@ -170,6 +174,15 @@
permission_reporter_->SendReport(report_info);
}
+void SafeBrowsingPingManager::ReportNotificationImage(
+ Profile* profile,
+ const scoped_refptr<SafeBrowsingDatabaseManager>& database_manager,
+ const GURL& origin,
+ const SkBitmap& image) {
+ notification_image_reporter_->ReportNotificationImageOnIO(
+ profile, database_manager, origin, image);
+}
+
GURL SafeBrowsingPingManager::SafeBrowsingHitUrl(
const safe_browsing::HitReport& hit_report) const {
DCHECK(hit_report.threat_type == SB_THREAT_TYPE_URL_MALWARE ||
diff --git a/chrome/browser/safe_browsing/ping_manager.h b/chrome/browser/safe_browsing/ping_manager.h
index 9f0c1977..8f5e1dd 100644
--- a/chrome/browser/safe_browsing/ping_manager.h
+++ b/chrome/browser/safe_browsing/ping_manager.h
@@ -23,13 +23,18 @@
#include "net/url_request/url_fetcher_delegate.h"
#include "url/gurl.h"
+class Profile;
+class SkBitmap;
+
namespace net {
class URLRequestContextGetter;
} // namespace net
namespace safe_browsing {
+class NotificationImageReporter;
class PermissionReporter;
+class SafeBrowsingDatabaseManager;
class SafeBrowsingPingManager : public net::URLFetcherDelegate {
public:
@@ -55,7 +60,15 @@
// Report permission action to SafeBrowsing servers.
void ReportPermissionAction(const PermissionReportInfo& report_info);
+ // Report notification content image to SafeBrowsing CSD server if necessary.
+ void ReportNotificationImage(
+ Profile* profile,
+ const scoped_refptr<SafeBrowsingDatabaseManager>& database_manager,
+ const GURL& origin,
+ const SkBitmap& image);
+
private:
+ friend class NotificationImageReporterTest;
friend class PermissionReporterBrowserTest;
friend class SafeBrowsingPingManagerTest;
FRIEND_TEST_ALL_PREFIXES(SafeBrowsingPingManagerTest,
@@ -102,6 +115,9 @@
// Sends reports of permission actions.
std::unique_ptr<PermissionReporter> permission_reporter_;
+ // Sends reports of notification content images.
+ std::unique_ptr<NotificationImageReporter> notification_image_reporter_;
+
net::NetLogWithSource net_log_;
DISALLOW_COPY_AND_ASSIGN(SafeBrowsingPingManager);