diff options
Diffstat (limited to 'chromium/components/blocked_content')
32 files changed, 2723 insertions, 0 deletions
diff --git a/chromium/components/blocked_content/BUILD.gn b/chromium/components/blocked_content/BUILD.gn new file mode 100644 index 00000000000..4f86ea0ffcf --- /dev/null +++ b/chromium/components/blocked_content/BUILD.gn @@ -0,0 +1,95 @@ +# Copyright 2020 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. + +source_set("blocked_content") { + sources = [ + "list_item_position.cc", + "list_item_position.h", + "popup_blocker.cc", + "popup_blocker.h", + "popup_blocker_tab_helper.cc", + "popup_blocker_tab_helper.h", + "popup_navigation_delegate.h", + "popup_opener_tab_helper.cc", + "popup_opener_tab_helper.h", + "popup_tracker.cc", + "popup_tracker.h", + "pref_names.cc", + "pref_names.h", + "safe_browsing_triggered_popup_blocker.cc", + "safe_browsing_triggered_popup_blocker.h", + "url_list_manager.cc", + "url_list_manager.h", + ] + deps = [ + "//base", + "//components/content_settings/browser", + "//components/content_settings/core/browser", + "//components/embedder_support", + "//components/pref_registry", + "//components/prefs", + "//components/safe_browsing/content/triggers:ad_popup_trigger", + "//components/safe_browsing/core/db:util", + "//components/subresource_filter/content/browser", + "//components/ukm/content", + "//components/user_prefs", + "//content/public/browser", + "//services/metrics/public/cpp:ukm_builders", + "//third_party/blink/public/common", + ] + if (is_android) { + sources += [ + "android/popup_blocked_infobar_delegate.cc", + "android/popup_blocked_infobar_delegate.h", + ] + deps += [ + "//components/infobars/content", + "//components/infobars/core", + "//components/resources:android_resources", + "//components/strings:components_strings_grit", + ] + } +} + +source_set("test_support") { + testonly = true + sources = [ + "test/test_popup_navigation_delegate.cc", + "test/test_popup_navigation_delegate.h", + ] + deps = [ + ":blocked_content", + "//third_party/blink/public/common", + "//url", + ] +} + +source_set("unit_tests") { + testonly = true + sources = [ + "popup_blocker_tab_helper_unittest.cc", + "safe_browsing_triggered_popup_blocker_unittest.cc", + ] + deps = [ + ":blocked_content", + ":test_support", + "//base", + "//base/test:test_support", + "//components/content_settings/browser", + "//components/content_settings/browser:test_support", + "//components/content_settings/core/browser", + "//components/subresource_filter/content/browser", + "//components/subresource_filter/content/browser:test_support", + "//components/subresource_filter/core/browser", + "//components/sync_preferences:test_support", + "//components/user_prefs", + "//content/test:test_support", + "//net:test_support", + "//testing/gtest", + ] + if (is_android) { + sources += [ "android/popup_blocked_infobar_delegate_unittest.cc" ] + deps += [ "//components/infobars/content" ] + } +} diff --git a/chromium/components/blocked_content/DEPS b/chromium/components/blocked_content/DEPS new file mode 100644 index 00000000000..15f6f7aea29 --- /dev/null +++ b/chromium/components/blocked_content/DEPS @@ -0,0 +1,18 @@ +include_rules = [ + "+components/content_settings/browser", + "+components/content_settings/core", + "+components/embedder_support", + "+components/pref_registry", + "+components/prefs", + "+components/safe_browsing", + "+components/subresource_filter/content/browser", + "+components/subresource_filter/core/browser", + "+components/sync_preferences", + "+components/ukm", + "+components/user_prefs", + "+content/public/browser", + "+content/public/test", + "+services/metrics/public", + "+third_party/blink/public", + "+ui/base", +] diff --git a/chromium/components/blocked_content/OWNERS b/chromium/components/blocked_content/OWNERS new file mode 100644 index 00000000000..eff6602f728 --- /dev/null +++ b/chromium/components/blocked_content/OWNERS @@ -0,0 +1,5 @@ +avi@chromium.org +jochen@chromium.org +csharrison@chromium.org + +# COMPONENT: UI>Browser>PopupBlocker diff --git a/chromium/components/blocked_content/android/BUILD.gn b/chromium/components/blocked_content/android/BUILD.gn new file mode 100644 index 00000000000..1a42a65a8cf --- /dev/null +++ b/chromium/components/blocked_content/android/BUILD.gn @@ -0,0 +1,16 @@ +# Copyright 2020 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. + +import("//build/config/android/rules.gni") + +android_resources("java_resources") { + sources = [ + "res/drawable-hdpi/infobar_blocked_popups.png", + "res/drawable-mdpi/infobar_blocked_popups.png", + "res/drawable-xhdpi/infobar_blocked_popups.png", + "res/drawable-xxhdpi/infobar_blocked_popups.png", + "res/drawable-xxxhdpi/infobar_blocked_popups.png", + ] + custom_package = "org.chromium.components.blocked_content" +} diff --git a/chromium/components/blocked_content/android/DEPS b/chromium/components/blocked_content/android/DEPS new file mode 100644 index 00000000000..61e69ecd030 --- /dev/null +++ b/chromium/components/blocked_content/android/DEPS @@ -0,0 +1,6 @@ +include_rules = [ + "+components/infobars/content", + "+components/infobars/core", + "+components/resources/android", + "+components/strings", +] diff --git a/chromium/components/blocked_content/android/popup_blocked_infobar_delegate.cc b/chromium/components/blocked_content/android/popup_blocked_infobar_delegate.cc new file mode 100644 index 00000000000..0e4d3818aef --- /dev/null +++ b/chromium/components/blocked_content/android/popup_blocked_infobar_delegate.cc @@ -0,0 +1,141 @@ +// Copyright 2013 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 "components/blocked_content/android/popup_blocked_infobar_delegate.h" + +#include <stddef.h> +#include <utility> + +#include "components/blocked_content/popup_blocker_tab_helper.h" +#include "components/content_settings/core/browser/host_content_settings_map.h" +#include "components/content_settings/core/common/content_settings.h" +#include "components/content_settings/core/common/content_settings_types.h" +#include "components/infobars/content/content_infobar_manager.h" +#include "components/infobars/core/infobar.h" +#include "components/prefs/pref_service.h" +#include "components/resources/android/theme_resources.h" +#include "components/strings/grit/components_strings.h" +#include "ui/base/l10n/l10n_util.h" + +namespace blocked_content { + +// static +bool PopupBlockedInfoBarDelegate::Create( + infobars::ContentInfoBarManager* infobar_manager, + int num_popups, + HostContentSettingsMap* settings_map, + base::OnceClosure on_accept_callback) { + const GURL& url = infobar_manager->web_contents()->GetURL(); + std::unique_ptr<infobars::InfoBar> infobar( + infobar_manager->CreateConfirmInfoBar( + std::unique_ptr<ConfirmInfoBarDelegate>( + new PopupBlockedInfoBarDelegate(num_popups, url, settings_map, + std::move(on_accept_callback))))); + + // See if there is an existing popup infobar already. + // TODO(dfalcantara) When triggering more than one popup the infobar + // will be shown once, then hide then be shown again. + // This will be fixed once we have an in place replace infobar mechanism. + for (size_t i = 0; i < infobar_manager->infobar_count(); ++i) { + infobars::InfoBar* existing_infobar = infobar_manager->infobar_at(i); + if (existing_infobar->delegate()->AsPopupBlockedInfoBarDelegate()) { + infobar_manager->ReplaceInfoBar(existing_infobar, std::move(infobar)); + return false; + } + } + + infobar_manager->AddInfoBar(std::move(infobar)); + + return true; +} + +PopupBlockedInfoBarDelegate::~PopupBlockedInfoBarDelegate() = default; + +infobars::InfoBarDelegate::InfoBarIdentifier +PopupBlockedInfoBarDelegate::GetIdentifier() const { + return POPUP_BLOCKED_INFOBAR_DELEGATE_MOBILE; +} + +int PopupBlockedInfoBarDelegate::GetIconId() const { + return IDR_ANDROID_INFOBAR_BLOCKED_POPUPS; +} + +PopupBlockedInfoBarDelegate* +PopupBlockedInfoBarDelegate::AsPopupBlockedInfoBarDelegate() { + return this; +} + +PopupBlockedInfoBarDelegate::PopupBlockedInfoBarDelegate( + int num_popups, + const GURL& url, + HostContentSettingsMap* map, + base::OnceClosure on_accept_callback) + : ConfirmInfoBarDelegate(), + num_popups_(num_popups), + url_(url), + map_(map), + on_accept_callback_(std::move(on_accept_callback)) { + content_settings::SettingInfo setting_info; + std::unique_ptr<base::Value> setting = map->GetWebsiteSetting( + url, url, ContentSettingsType::POPUPS, std::string(), &setting_info); + can_show_popups_ = + setting_info.source != content_settings::SETTING_SOURCE_POLICY; +} + +base::string16 PopupBlockedInfoBarDelegate::GetMessageText() const { + return l10n_util::GetPluralStringFUTF16(IDS_POPUPS_BLOCKED_INFOBAR_TEXT, + num_popups_); +} + +int PopupBlockedInfoBarDelegate::GetButtons() const { + if (!can_show_popups_) + return 0; + + int buttons = BUTTON_OK; + + return buttons; +} + +base::string16 PopupBlockedInfoBarDelegate::GetButtonLabel( + InfoBarButton button) const { + switch (button) { + case BUTTON_OK: + return l10n_util::GetStringUTF16(IDS_POPUPS_BLOCKED_INFOBAR_BUTTON_SHOW); + case BUTTON_CANCEL: + return l10n_util::GetStringUTF16(IDS_PERMISSION_DENY); + default: + NOTREACHED(); + break; + } + return base::string16(); +} + +bool PopupBlockedInfoBarDelegate::Accept() { + DCHECK(can_show_popups_); + + // Create exceptions. + map_->SetNarrowestContentSetting(url_, url_, ContentSettingsType::POPUPS, + CONTENT_SETTING_ALLOW); + + // Launch popups. + content::WebContents* web_contents = + infobars::ContentInfoBarManager::WebContentsFromInfoBar(infobar()); + blocked_content::PopupBlockerTabHelper* popup_blocker_helper = + blocked_content::PopupBlockerTabHelper::FromWebContents(web_contents); + DCHECK(popup_blocker_helper); + blocked_content::PopupBlockerTabHelper::PopupIdMap blocked_popups = + popup_blocker_helper->GetBlockedPopupRequests(); + for (blocked_content::PopupBlockerTabHelper::PopupIdMap::iterator it = + blocked_popups.begin(); + it != blocked_popups.end(); ++it) { + popup_blocker_helper->ShowBlockedPopup(it->first, + WindowOpenDisposition::CURRENT_TAB); + } + + if (on_accept_callback_) + std::move(on_accept_callback_).Run(); + return true; +} + +} // namespace blocked_content diff --git a/chromium/components/blocked_content/android/popup_blocked_infobar_delegate.h b/chromium/components/blocked_content/android/popup_blocked_infobar_delegate.h new file mode 100644 index 00000000000..43e31266e73 --- /dev/null +++ b/chromium/components/blocked_content/android/popup_blocked_infobar_delegate.h @@ -0,0 +1,61 @@ +// Copyright 2013 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 COMPONENTS_BLOCKED_CONTENT_ANDROID_POPUP_BLOCKED_INFOBAR_DELEGATE_H_ +#define COMPONENTS_BLOCKED_CONTENT_ANDROID_POPUP_BLOCKED_INFOBAR_DELEGATE_H_ + +#include "base/callback.h" +#include "components/infobars/core/confirm_infobar_delegate.h" +#include "url/gurl.h" + +namespace infobars { +class ContentInfoBarManager; +} + +class HostContentSettingsMap; + +namespace blocked_content { + +class PopupBlockedInfoBarDelegate : public ConfirmInfoBarDelegate { + public: + // Creates a popup blocked infobar and delegate and adds the infobar to + // |infobar_manager|. Returns true if the infobar was created, and false if it + // replaced an existing popup infobar. |on_accept_callback| will be run if the + // accept button is pressed on the infobar. + static bool Create(infobars::ContentInfoBarManager* infobar_manager, + int num_popups, + HostContentSettingsMap* settings_map, + base::OnceClosure on_accept_callback); + + ~PopupBlockedInfoBarDelegate() override; + + PopupBlockedInfoBarDelegate(const PopupBlockedInfoBarDelegate&) = delete; + PopupBlockedInfoBarDelegate& operator=(const PopupBlockedInfoBarDelegate&) = + delete; + + private: + PopupBlockedInfoBarDelegate(int num_popups, + const GURL& url, + HostContentSettingsMap* map, + base::OnceClosure on_accept_callback); + + // ConfirmInfoBarDelegate: + infobars::InfoBarDelegate::InfoBarIdentifier GetIdentifier() const override; + int GetIconId() const override; + PopupBlockedInfoBarDelegate* AsPopupBlockedInfoBarDelegate() override; + base::string16 GetMessageText() const override; + int GetButtons() const override; + base::string16 GetButtonLabel(InfoBarButton button) const override; + bool Accept() override; + + const int num_popups_; + const GURL url_; + HostContentSettingsMap* map_; + bool can_show_popups_; + base::OnceClosure on_accept_callback_; +}; + +} // namespace blocked_content + +#endif // COMPONENTS_BLOCKED_CONTENT_ANDROID_POPUP_BLOCKED_INFOBAR_DELEGATE_H_ diff --git a/chromium/components/blocked_content/android/popup_blocked_infobar_delegate_unittest.cc b/chromium/components/blocked_content/android/popup_blocked_infobar_delegate_unittest.cc new file mode 100644 index 00000000000..e9d3bdf24e3 --- /dev/null +++ b/chromium/components/blocked_content/android/popup_blocked_infobar_delegate_unittest.cc @@ -0,0 +1,133 @@ +// Copyright 2020 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 "components/blocked_content/android/popup_blocked_infobar_delegate.h" + +#include "base/strings/utf_string_conversions.h" +#include "base/test/bind_test_util.h" +#include "base/test/scoped_feature_list.h" +#include "components/blocked_content/popup_blocker_tab_helper.h" +#include "components/blocked_content/safe_browsing_triggered_popup_blocker.h" +#include "components/blocked_content/test/test_popup_navigation_delegate.h" +#include "components/content_settings/browser/tab_specific_content_settings.h" +#include "components/content_settings/browser/test_tab_specific_content_settings_delegate.h" +#include "components/content_settings/core/browser/host_content_settings_map.h" +#include "components/infobars/content/content_infobar_manager.h" +#include "components/infobars/core/infobar.h" +#include "components/sync_preferences/testing_pref_service_syncable.h" +#include "content/public/test/test_renderer_host.h" +#include "testing/gmock/include/gmock/gmock.h" + +namespace blocked_content { +namespace { +constexpr char kPageUrl[] = "http://example_page.test"; +constexpr char kPopupUrl[] = "http://example_popup.test"; + +class TestInfoBarManager : public infobars::ContentInfoBarManager { + public: + explicit TestInfoBarManager(content::WebContents* web_contents) + : ContentInfoBarManager(web_contents) {} + + // infobars::InfoBarManager: + std::unique_ptr<infobars::InfoBar> CreateConfirmInfoBar( + std::unique_ptr<ConfirmInfoBarDelegate> delegate) override { + return std::make_unique<infobars::InfoBar>(std::move(delegate)); + } +}; + +} // namespace + +class PopupBlockedInfoBarDelegateTest + : public content::RenderViewHostTestHarness { + public: + ~PopupBlockedInfoBarDelegateTest() override { + settings_map_->ShutdownOnUIThread(); + } + + // content::RenderViewHostTestHarness: + void SetUp() override { + content::RenderViewHostTestHarness::SetUp(); + // Make sure the SafeBrowsingTriggeredPopupBlocker is not created. + feature_list_.InitAndDisableFeature(kAbusiveExperienceEnforce); + + HostContentSettingsMap::RegisterProfilePrefs(pref_service_.registry()); + settings_map_ = base::MakeRefCounted<HostContentSettingsMap>( + &pref_service_, false, false, false, false); + content_settings::TabSpecificContentSettings::CreateForWebContents( + web_contents(), + std::make_unique< + content_settings::TestTabSpecificContentSettingsDelegate>( + /*prefs=*/nullptr, settings_map_.get())); + + PopupBlockerTabHelper::CreateForWebContents(web_contents()); + helper_ = PopupBlockerTabHelper::FromWebContents(web_contents()); + infobar_manager_ = std::make_unique<TestInfoBarManager>(web_contents()); + + NavigateAndCommit(GURL(kPageUrl)); + } + + PopupBlockerTabHelper* helper() { return helper_; } + + TestInfoBarManager* infobar_manager() { return infobar_manager_.get(); } + + HostContentSettingsMap* settings_map() { return settings_map_.get(); } + + private: + base::test::ScopedFeatureList feature_list_; + PopupBlockerTabHelper* helper_ = nullptr; + sync_preferences::TestingPrefServiceSyncable pref_service_; + scoped_refptr<HostContentSettingsMap> settings_map_; + std::unique_ptr<TestInfoBarManager> infobar_manager_; +}; + +TEST_F(PopupBlockedInfoBarDelegateTest, ReplacesInfobarOnSecondPopup) { + EXPECT_TRUE(PopupBlockedInfoBarDelegate::Create( + infobar_manager(), 1, settings_map(), base::NullCallback())); + EXPECT_EQ(infobar_manager()->infobar_count(), 1u); + // First message should not contain "2"; + EXPECT_FALSE(base::Contains(infobar_manager() + ->infobar_at(0) + ->delegate() + ->AsConfirmInfoBarDelegate() + ->GetMessageText(), + base::ASCIIToUTF16("2"))); + + EXPECT_FALSE(PopupBlockedInfoBarDelegate::Create( + infobar_manager(), 2, settings_map(), base::NullCallback())); + EXPECT_EQ(infobar_manager()->infobar_count(), 1u); + // Second message blocks 2 popups, so should contain "2"; + EXPECT_TRUE(base::Contains(infobar_manager() + ->infobar_at(0) + ->delegate() + ->AsConfirmInfoBarDelegate() + ->GetMessageText(), + base::ASCIIToUTF16("2"))); +} + +TEST_F(PopupBlockedInfoBarDelegateTest, ShowsBlockedPopups) { + TestPopupNavigationDelegate::ResultHolder result; + helper()->AddBlockedPopup( + std::make_unique<TestPopupNavigationDelegate>(GURL(kPopupUrl), &result), + blink::mojom::WindowFeatures(), PopupBlockType::kNoGesture); + bool on_accept_called = false; + EXPECT_TRUE(PopupBlockedInfoBarDelegate::Create( + infobar_manager(), 1, settings_map(), + base::BindLambdaForTesting( + [&on_accept_called] { on_accept_called = true; }))); + EXPECT_FALSE(on_accept_called); + + EXPECT_TRUE(infobar_manager() + ->infobar_at(0) + ->delegate() + ->AsConfirmInfoBarDelegate() + ->Accept()); + EXPECT_TRUE(result.did_navigate); + EXPECT_TRUE(on_accept_called); + EXPECT_EQ(settings_map()->GetContentSetting(GURL(kPageUrl), GURL(kPageUrl), + ContentSettingsType::POPUPS, + std::string()), + CONTENT_SETTING_ALLOW); +} + +} // namespace blocked_content diff --git a/chromium/components/blocked_content/android/res/drawable-hdpi/infobar_blocked_popups.png b/chromium/components/blocked_content/android/res/drawable-hdpi/infobar_blocked_popups.png Binary files differnew file mode 100644 index 00000000000..c3ebd4ecb8a --- /dev/null +++ b/chromium/components/blocked_content/android/res/drawable-hdpi/infobar_blocked_popups.png diff --git a/chromium/components/blocked_content/android/res/drawable-mdpi/infobar_blocked_popups.png b/chromium/components/blocked_content/android/res/drawable-mdpi/infobar_blocked_popups.png Binary files differnew file mode 100644 index 00000000000..06bcee8d67b --- /dev/null +++ b/chromium/components/blocked_content/android/res/drawable-mdpi/infobar_blocked_popups.png diff --git a/chromium/components/blocked_content/android/res/drawable-xhdpi/infobar_blocked_popups.png b/chromium/components/blocked_content/android/res/drawable-xhdpi/infobar_blocked_popups.png Binary files differnew file mode 100644 index 00000000000..48e6a6a6ec2 --- /dev/null +++ b/chromium/components/blocked_content/android/res/drawable-xhdpi/infobar_blocked_popups.png diff --git a/chromium/components/blocked_content/android/res/drawable-xxhdpi/infobar_blocked_popups.png b/chromium/components/blocked_content/android/res/drawable-xxhdpi/infobar_blocked_popups.png Binary files differnew file mode 100644 index 00000000000..98379ed4c49 --- /dev/null +++ b/chromium/components/blocked_content/android/res/drawable-xxhdpi/infobar_blocked_popups.png diff --git a/chromium/components/blocked_content/android/res/drawable-xxxhdpi/infobar_blocked_popups.png b/chromium/components/blocked_content/android/res/drawable-xxxhdpi/infobar_blocked_popups.png Binary files differnew file mode 100644 index 00000000000..43e03065686 --- /dev/null +++ b/chromium/components/blocked_content/android/res/drawable-xxxhdpi/infobar_blocked_popups.png diff --git a/chromium/components/blocked_content/list_item_position.cc b/chromium/components/blocked_content/list_item_position.cc new file mode 100644 index 00000000000..c4715ace14f --- /dev/null +++ b/chromium/components/blocked_content/list_item_position.cc @@ -0,0 +1,27 @@ +// 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 "components/blocked_content/list_item_position.h" +#include "base/check_op.h" + +namespace blocked_content { + +ListItemPosition GetListItemPositionFromDistance(size_t distance, + size_t total_size) { + DCHECK(total_size); + if (total_size == 1u) { + DCHECK_EQ(0u, distance); + return ListItemPosition::kOnlyItem; + } + + if (distance == 0) + return ListItemPosition::kFirstItem; + + if (distance == total_size - 1) + return ListItemPosition::kLastItem; + + return ListItemPosition::kMiddleItem; +} + +} // namespace blocked_content diff --git a/chromium/components/blocked_content/list_item_position.h b/chromium/components/blocked_content/list_item_position.h new file mode 100644 index 00000000000..7d63fba1423 --- /dev/null +++ b/chromium/components/blocked_content/list_item_position.h @@ -0,0 +1,36 @@ +// 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 COMPONENTS_BLOCKED_CONTENT_LIST_ITEM_POSITION_H_ +#define COMPONENTS_BLOCKED_CONTENT_LIST_ITEM_POSITION_H_ + +#include <cstddef> + +namespace blocked_content { + +// This enum backs a histogram. Make sure you update enums.xml if you make +// any changes. +// +// Identifies an element's position in an ordered list. Used by both the +// framebust and popup UI on desktop platforms to indicate which element was +// clicked. +enum class ListItemPosition : int { + kOnlyItem = 0, + kFirstItem = 1, + kMiddleItem = 2, + kLastItem = 3, + + // Any new values should go before this one. + kMaxValue = kLastItem, +}; + +// Gets the list item position from the given distance/index and the total size +// of the collection. Distance is the measure from the beginning of the +// collection to the given element. +ListItemPosition GetListItemPositionFromDistance(size_t distance, + size_t total_size); + +} // namespace blocked_content + +#endif // COMPONENTS_BLOCKED_CONTENT_LIST_ITEM_POSITION_H_ diff --git a/chromium/components/blocked_content/popup_blocker.cc b/chromium/components/blocked_content/popup_blocker.cc new file mode 100644 index 00000000000..6b278ac2e49 --- /dev/null +++ b/chromium/components/blocked_content/popup_blocker.cc @@ -0,0 +1,137 @@ +// Copyright 2018 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 "components/blocked_content/popup_blocker.h" + +#include <string> + +#include "base/check.h" +#include "base/command_line.h" +#include "components/blocked_content/popup_blocker_tab_helper.h" +#include "components/blocked_content/popup_navigation_delegate.h" +#include "components/blocked_content/safe_browsing_triggered_popup_blocker.h" +#include "components/content_settings/core/browser/host_content_settings_map.h" +#include "components/content_settings/core/common/content_settings.h" +#include "components/embedder_support/switches.h" +#include "components/safe_browsing/content/triggers/ad_popup_trigger.h" +#include "content/public/browser/page_navigator.h" +#include "content/public/browser/render_frame_host.h" +#include "content/public/browser/web_contents.h" + +namespace blocked_content { +namespace { + +// If the popup should be blocked, returns the reason why it was blocked. +// Otherwise returns kNotBlocked. +PopupBlockType ShouldBlockPopup(content::WebContents* web_contents, + const GURL* opener_url, + bool user_gesture, + const content::OpenURLParams* open_url_params, + HostContentSettingsMap* settings_map) { + if (base::CommandLine::ForCurrentProcess()->HasSwitch( + embedder_support::kDisablePopupBlocking)) { + return PopupBlockType::kNotBlocked; + } + // If an explicit opener is not given, use the current committed load in this + // web contents. This is because A page can't spawn popups (or do anything + // else, either) until its load commits, so when we reach here, the popup was + // spawned by the NavigationController's last committed entry, not the active + // entry. For example, if a page opens a popup in an onunload() handler, then + // the active entry is the page to be loaded as we navigate away from the + // unloading page. + const GURL& url = + opener_url ? *opener_url : web_contents->GetLastCommittedURL(); + if (url.is_valid() && + settings_map->GetContentSetting(url, url, ContentSettingsType::POPUPS, + std::string()) == CONTENT_SETTING_ALLOW) { + return PopupBlockType::kNotBlocked; + } + + if (!user_gesture) + return PopupBlockType::kNoGesture; + + // This is trusted user action (e.g. shift-click), so make sure it is not + // blocked. + if (open_url_params && open_url_params->triggering_event_info != + blink::TriggeringEventInfo::kFromUntrustedEvent) { + return PopupBlockType::kNotBlocked; + } + + auto* safe_browsing_blocker = + SafeBrowsingTriggeredPopupBlocker::FromWebContents(web_contents); + if (safe_browsing_blocker && + safe_browsing_blocker->ShouldApplyAbusivePopupBlocker()) { + return PopupBlockType::kAbusive; + } + return PopupBlockType::kNotBlocked; +} + +// Tries to get the opener from either the |params| or |open_url_params|, +// otherwise uses the focused frame from |web_contents| as a proxy. +content::RenderFrameHost* GetSourceFrameForPopup( + PopupNavigationDelegate* params, + const content::OpenURLParams* open_url_params, + content::WebContents* web_contents) { + if (params->GetOpener()) + return params->GetOpener(); + // Make sure the source render frame host is alive before we attempt to + // retrieve it from |open_url_params|. + if (open_url_params) { + content::RenderFrameHost* source = content::RenderFrameHost::FromID( + open_url_params->source_render_frame_id, + open_url_params->source_render_process_id); + if (source) + return source; + } + // The focused frame is not always the frame initiating the popup navigation + // and is used as a fallback in case opener information is not available. + return web_contents->GetFocusedFrame(); +} + +} // namespace + +bool ConsiderForPopupBlocking(WindowOpenDisposition disposition) { + return disposition == WindowOpenDisposition::NEW_POPUP || + disposition == WindowOpenDisposition::NEW_FOREGROUND_TAB || + disposition == WindowOpenDisposition::NEW_BACKGROUND_TAB || + disposition == WindowOpenDisposition::NEW_WINDOW; +} + +std::unique_ptr<PopupNavigationDelegate> MaybeBlockPopup( + content::WebContents* web_contents, + const GURL* opener_url, + std::unique_ptr<PopupNavigationDelegate> delegate, + const content::OpenURLParams* open_url_params, + const blink::mojom::WindowFeatures& window_features, + HostContentSettingsMap* settings_map) { + DCHECK(web_contents); + DCHECK(!open_url_params || + open_url_params->user_gesture == delegate->GetOriginalUserGesture()); + PopupBlockerTabHelper::LogAction(PopupBlockerTabHelper::Action::kInitiated); + + // Check |popup_blocker| first since it is cheaper than ShouldBlockPopup(). + auto* popup_blocker = PopupBlockerTabHelper::FromWebContents(web_contents); + if (!popup_blocker) + return delegate; + + PopupBlockType block_type = ShouldBlockPopup( + web_contents, opener_url, delegate->GetOriginalUserGesture(), + open_url_params, settings_map); + if (block_type == PopupBlockType::kNotBlocked) + return delegate; + + // AddBlockedPopup() takes ownership of the delegate, so grab the source frame + // first. + content::RenderFrameHost* source_frame = + GetSourceFrameForPopup(delegate.get(), open_url_params, web_contents); + popup_blocker->AddBlockedPopup(std::move(delegate), window_features, + block_type); + auto* trigger = safe_browsing::AdPopupTrigger::FromWebContents(web_contents); + if (trigger) { + trigger->PopupWasBlocked(source_frame); + } + return nullptr; +} + +} // namespace blocked_content diff --git a/chromium/components/blocked_content/popup_blocker.h b/chromium/components/blocked_content/popup_blocker.h new file mode 100644 index 00000000000..be9cc439ac4 --- /dev/null +++ b/chromium/components/blocked_content/popup_blocker.h @@ -0,0 +1,58 @@ +// Copyright 2018 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 COMPONENTS_BLOCKED_CONTENT_POPUP_BLOCKER_H_ +#define COMPONENTS_BLOCKED_CONTENT_POPUP_BLOCKER_H_ + +#include "components/content_settings/core/browser/host_content_settings_map.h" +#include "third_party/blink/public/mojom/window_features/window_features.mojom-forward.h" +#include "ui/base/window_open_disposition.h" +#include "url/gurl.h" + +class HostContentSettingsMap; + +namespace content { +class WebContents; +struct OpenURLParams; +} // namespace content + +namespace blocked_content { +class PopupNavigationDelegate; + +// Classifies what caused a popup to be blocked. +enum class PopupBlockType { + kNotBlocked, + + // Popup blocked due to no user gesture. + kNoGesture, + // Popup blocked due to the abusive popup blocker. + kAbusive, +}; + +// Whether a new window opened with |disposition| would be considered for +// popup blocking. Note that this includes more dispositions than just +// NEW_POPUP since the popup blocker targets all new windows and tabs. +bool ConsiderForPopupBlocking(WindowOpenDisposition disposition); + +// Returns null if the popup request defined by |delegate| and the optional +// |open_url_params| should be blocked. In that case, it is also added to the +// |blocked_popups_| container. +// +// |opener_url| is an optional parameter used to compute how the popup +// permission will behave. If it is nullptr, the current committed URL will be +// used instead. +// +// If this function returns a non-null unique_ptr, the navigation was not +// blocked and should be continued. +std::unique_ptr<PopupNavigationDelegate> MaybeBlockPopup( + content::WebContents* web_contents, + const GURL* opener_url, + std::unique_ptr<PopupNavigationDelegate> delegate, + const content::OpenURLParams* open_url_params, + const blink::mojom::WindowFeatures& window_features, + HostContentSettingsMap* settings_map); + +} // namespace blocked_content + +#endif // COMPONENTS_BLOCKED_CONTENT_POPUP_BLOCKER_H_ diff --git a/chromium/components/blocked_content/popup_blocker_tab_helper.cc b/chromium/components/blocked_content/popup_blocker_tab_helper.cc new file mode 100644 index 00000000000..c3537d88c0e --- /dev/null +++ b/chromium/components/blocked_content/popup_blocker_tab_helper.cc @@ -0,0 +1,171 @@ +// Copyright 2013 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 "components/blocked_content/popup_blocker_tab_helper.h" + +#include <iterator> +#include <string> + +#include "base/metrics/histogram_macros.h" +#include "build/build_config.h" +#include "components/blocked_content/list_item_position.h" +#include "components/blocked_content/popup_navigation_delegate.h" +#include "components/blocked_content/popup_tracker.h" +#include "components/blocked_content/safe_browsing_triggered_popup_blocker.h" +#include "components/content_settings/browser/tab_specific_content_settings.h" +#include "content/public/browser/back_forward_cache.h" +#include "content/public/browser/navigation_controller.h" +#include "content/public/browser/navigation_handle.h" +#include "content/public/browser/page_navigator.h" +#include "content/public/browser/render_frame_host.h" +#include "content/public/browser/render_view_host.h" +#include "content/public/browser/web_contents.h" +#include "third_party/blink/public/mojom/window_features/window_features.mojom.h" + +namespace blocked_content { +const size_t kMaximumNumberOfPopups = 25; + +struct PopupBlockerTabHelper::BlockedRequest { + BlockedRequest(std::unique_ptr<PopupNavigationDelegate> delegate, + const blink::mojom::WindowFeatures& window_features, + PopupBlockType block_type) + : delegate(std::move(delegate)), + window_features(window_features), + block_type(block_type) {} + + std::unique_ptr<PopupNavigationDelegate> delegate; + blink::mojom::WindowFeatures window_features; + PopupBlockType block_type; +}; + +PopupBlockerTabHelper::PopupBlockerTabHelper(content::WebContents* web_contents) + : content::WebContentsObserver(web_contents) { + blocked_content::SafeBrowsingTriggeredPopupBlocker::MaybeCreate(web_contents); +} + +PopupBlockerTabHelper::~PopupBlockerTabHelper() = default; + +void PopupBlockerTabHelper::DidFinishNavigation( + content::NavigationHandle* navigation_handle) { + // Clear all page actions, blocked content notifications and browser actions + // for this tab, unless this is an same-document navigation. Also only + // consider main frame navigations that successfully committed. + if (!navigation_handle->IsInMainFrame() || + !navigation_handle->HasCommitted() || + navigation_handle->IsSameDocument()) { + return; + } + + // Close blocked popups. + if (!blocked_popups_.empty()) { + blocked_popups_.clear(); + HidePopupNotification(); + + // With back-forward cache we can restore the page, but |blocked_popups_| + // are lost here and can't be restored at the moment. + // Disable bfcache here to avoid potential loss of the page state. + web_contents() + ->GetController() + .GetBackForwardCache() + .DisableForRenderFrameHost( + navigation_handle->GetPreviousRenderFrameHostId(), + "PopupBlockerTabHelper"); + } +} + +void PopupBlockerTabHelper::HidePopupNotification() { + auto* tscs = content_settings::TabSpecificContentSettings::FromWebContents( + web_contents()); + if (tscs) + tscs->ClearPopupsBlocked(); +} + +void PopupBlockerTabHelper::AddBlockedPopup( + std::unique_ptr<PopupNavigationDelegate> delegate, + const blink::mojom::WindowFeatures& window_features, + PopupBlockType block_type) { + LogAction(Action::kBlocked); + if (blocked_popups_.size() >= kMaximumNumberOfPopups) + return; + + int id = next_id_; + next_id_++; + blocked_popups_[id] = std::make_unique<BlockedRequest>( + std::move(delegate), window_features, block_type); + content_settings::TabSpecificContentSettings::FromWebContents(web_contents()) + ->OnContentBlocked(ContentSettingsType::POPUPS); + auto* raw_delegate = blocked_popups_[id]->delegate.get(); + manager_.NotifyObservers(id, raw_delegate->GetURL()); + + raw_delegate->OnPopupBlocked(web_contents(), GetBlockedPopupsCount()); +} + +void PopupBlockerTabHelper::ShowBlockedPopup( + int32_t id, + WindowOpenDisposition disposition) { + auto it = blocked_popups_.find(id); + if (it == blocked_popups_.end()) + return; + + blocked_content::ListItemPosition position = + blocked_content::GetListItemPositionFromDistance( + std::distance(blocked_popups_.begin(), it), blocked_popups_.size()); + + UMA_HISTOGRAM_ENUMERATION("ContentSettings.Popups.ClickThroughPosition", + position); + + BlockedRequest* popup = it->second.get(); + + base::Optional<WindowOpenDisposition> updated_disposition; + if (disposition != WindowOpenDisposition::CURRENT_TAB) + updated_disposition = disposition; + + PopupNavigationDelegate::NavigateResult result = + popup->delegate->NavigateWithGesture(popup->window_features, + updated_disposition); + if (result.navigated_or_inserted_contents) { + auto* tracker = blocked_content::PopupTracker::CreateForWebContents( + result.navigated_or_inserted_contents, web_contents(), + result.disposition); + tracker->set_is_trusted(true); + } + + switch (popup->block_type) { + case PopupBlockType::kNotBlocked: + NOTREACHED(); + break; + case PopupBlockType::kNoGesture: + LogAction(Action::kClickedThroughNoGesture); + break; + case PopupBlockType::kAbusive: + LogAction(Action::kClickedThroughAbusive); + break; + } + + blocked_popups_.erase(id); + if (blocked_popups_.empty()) + HidePopupNotification(); +} + +size_t PopupBlockerTabHelper::GetBlockedPopupsCount() const { + return blocked_popups_.size(); +} + +PopupBlockerTabHelper::PopupIdMap +PopupBlockerTabHelper::GetBlockedPopupRequests() { + PopupIdMap result; + for (const auto& it : blocked_popups_) { + result[it.first] = it.second->delegate->GetURL(); + } + return result; +} + +// static +void PopupBlockerTabHelper::LogAction(Action action) { + UMA_HISTOGRAM_ENUMERATION("ContentSettings.Popups.BlockerActions", action); +} + +WEB_CONTENTS_USER_DATA_KEY_IMPL(PopupBlockerTabHelper) + +} // namespace blocked_content diff --git a/chromium/components/blocked_content/popup_blocker_tab_helper.h b/chromium/components/blocked_content/popup_blocker_tab_helper.h new file mode 100644 index 00000000000..8d1381f718e --- /dev/null +++ b/chromium/components/blocked_content/popup_blocker_tab_helper.h @@ -0,0 +1,105 @@ +// Copyright 2013 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 COMPONENTS_BLOCKED_CONTENT_POPUP_BLOCKER_TAB_HELPER_H_ +#define COMPONENTS_BLOCKED_CONTENT_POPUP_BLOCKER_TAB_HELPER_H_ + +#include <stddef.h> +#include <stdint.h> + +#include <map> +#include <memory> + +#include "base/macros.h" +#include "components/blocked_content/popup_blocker.h" +#include "components/blocked_content/url_list_manager.h" +#include "content/public/browser/web_contents_observer.h" +#include "content/public/browser/web_contents_user_data.h" +#include "ui/base/window_open_disposition.h" +#include "url/gurl.h" + +namespace blocked_content { +class PopupNavigationDelegate; + +// Per-tab class to manage blocked popups. +class PopupBlockerTabHelper + : public content::WebContentsObserver, + public content::WebContentsUserData<PopupBlockerTabHelper> { + public: + // Mapping from popup IDs to blocked popup requests. + typedef std::map<int32_t, GURL> PopupIdMap; + + // This enum is backed by a histogram. Make sure enums.xml is updated if this + // is updated. + enum class Action : int { + // A popup was initiated and was sent to the popup blocker for + // consideration. + kInitiated = 0, + + // A popup was blocked by the popup blocker. + kBlocked = 1, + + // A previously blocked popup was clicked through. For popups blocked + // without a user gesture. + kClickedThroughNoGesture = 2, + + // A previously blocked popup was clicked through. For popups blocked + // due to the abusive popup blocker. + kClickedThroughAbusive = 3, + + // Add new elements before this value. + kMaxValue = kClickedThroughAbusive + }; + + ~PopupBlockerTabHelper() override; + + // Returns the number of blocked popups. + size_t GetBlockedPopupsCount() const; + + PopupIdMap GetBlockedPopupRequests(); + + // Creates the blocked popup with |popup_id| in given |dispostion|. + // Note that if |disposition| is WindowOpenDisposition::CURRENT_TAB, + // blocked popup will be opened as it was specified by renderer. + void ShowBlockedPopup(int32_t popup_id, WindowOpenDisposition disposition); + + // Adds a new blocked popup to the UI. + void AddBlockedPopup(std::unique_ptr<PopupNavigationDelegate> delegate, + const blink::mojom::WindowFeatures& window_features, + PopupBlockType block_type); + + // content::WebContentsObserver overrides: + void DidFinishNavigation( + content::NavigationHandle* navigation_handle) override; + + // Logs a histogram measuring popup blocker actions. + static void LogAction(Action action); + + blocked_content::UrlListManager* manager() { return &manager_; } + + private: + struct BlockedRequest; + friend class content::WebContentsUserData<PopupBlockerTabHelper>; + + explicit PopupBlockerTabHelper(content::WebContents* web_contents); + + // Called when the blocked popup notification is hidden. + void HidePopupNotification(); + + blocked_content::UrlListManager manager_; + + // Note, this container should be sorted based on the position in the popup + // list, so it is keyed by an id which is continually increased. + std::map<int32_t, std::unique_ptr<BlockedRequest>> blocked_popups_; + + int32_t next_id_ = 0; + + WEB_CONTENTS_USER_DATA_KEY_DECL(); + + DISALLOW_COPY_AND_ASSIGN(PopupBlockerTabHelper); +}; + +} // namespace blocked_content + +#endif // COMPONENTS_BLOCKED_CONTENT_POPUP_BLOCKER_TAB_HELPER_H_ diff --git a/chromium/components/blocked_content/popup_blocker_tab_helper_unittest.cc b/chromium/components/blocked_content/popup_blocker_tab_helper_unittest.cc new file mode 100644 index 00000000000..1eedfe0421e --- /dev/null +++ b/chromium/components/blocked_content/popup_blocker_tab_helper_unittest.cc @@ -0,0 +1,189 @@ +// Copyright 2020 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 "components/blocked_content/popup_blocker_tab_helper.h" + +#include "base/test/scoped_feature_list.h" +#include "components/blocked_content/popup_navigation_delegate.h" +#include "components/blocked_content/safe_browsing_triggered_popup_blocker.h" +#include "components/blocked_content/test/test_popup_navigation_delegate.h" +#include "components/blocked_content/url_list_manager.h" +#include "components/content_settings/browser/tab_specific_content_settings.h" +#include "components/content_settings/browser/test_tab_specific_content_settings_delegate.h" +#include "components/content_settings/core/browser/host_content_settings_map.h" +#include "components/sync_preferences/testing_pref_service_syncable.h" +#include "content/public/test/test_renderer_host.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "third_party/blink/public/mojom/window_features/window_features.mojom.h" +#include "ui/base/window_open_disposition.h" + +namespace blocked_content { +namespace { +using testing::Pair; +using testing::UnorderedElementsAre; + +constexpr char kUrl1[] = "http://example1.test"; +constexpr char kUrl2[] = "http://example2.test"; + +// Observer which allows retrieving a map of all the blocked URLs. +class BlockedUrlListObserver : public UrlListManager::Observer { + public: + explicit BlockedUrlListObserver(PopupBlockerTabHelper* helper) { + observer_.Add(helper->manager()); + } + // UrlListManager::Observer: + void BlockedUrlAdded(int32_t id, const GURL& url) override { + blocked_urls_.insert({id, url}); + } + + const std::map<int32_t, GURL>& blocked_urls() const { return blocked_urls_; } + + private: + std::map<int32_t, GURL> blocked_urls_; + ScopedObserver<UrlListManager, UrlListManager::Observer> observer_{this}; +}; +} // namespace + +class PopupBlockerTabHelperTest : public content::RenderViewHostTestHarness { + public: + ~PopupBlockerTabHelperTest() override { settings_map_->ShutdownOnUIThread(); } + + // content::RenderViewHostTestHarness: + void SetUp() override { + content::RenderViewHostTestHarness::SetUp(); + // Make sure the SafeBrowsingTriggeredPopupBlocker is not created. + feature_list_.InitAndDisableFeature(kAbusiveExperienceEnforce); + + HostContentSettingsMap::RegisterProfilePrefs(pref_service_.registry()); + settings_map_ = base::MakeRefCounted<HostContentSettingsMap>( + &pref_service_, false, false, false, false); + content_settings::TabSpecificContentSettings::CreateForWebContents( + web_contents(), + std::make_unique< + content_settings::TestTabSpecificContentSettingsDelegate>( + /*prefs=*/nullptr, settings_map_.get())); + + PopupBlockerTabHelper::CreateForWebContents(web_contents()); + helper_ = PopupBlockerTabHelper::FromWebContents(web_contents()); + } + + PopupBlockerTabHelper* helper() { return helper_; } + + private: + base::test::ScopedFeatureList feature_list_; + PopupBlockerTabHelper* helper_ = nullptr; + sync_preferences::TestingPrefServiceSyncable pref_service_; + scoped_refptr<HostContentSettingsMap> settings_map_; +}; + +TEST_F(PopupBlockerTabHelperTest, BlocksAndShowsPopup) { + BlockedUrlListObserver observer(helper()); + TestPopupNavigationDelegate::ResultHolder result; + blink::mojom::WindowFeatures window_features; + window_features.has_x = true; + helper()->AddBlockedPopup( + std::make_unique<TestPopupNavigationDelegate>(GURL(kUrl1), &result), + window_features, PopupBlockType::kNoGesture); + EXPECT_EQ(result.total_popups_blocked_on_page, 1); + EXPECT_FALSE(result.did_navigate); + EXPECT_THAT(observer.blocked_urls(), + UnorderedElementsAre(Pair(0, GURL(kUrl1)))); + + helper()->ShowBlockedPopup(0, WindowOpenDisposition::NEW_FOREGROUND_TAB); + EXPECT_TRUE(result.did_navigate); + EXPECT_TRUE(result.navigation_window_features.has_x); + EXPECT_EQ(result.navigation_disposition, + WindowOpenDisposition::NEW_FOREGROUND_TAB); +} + +TEST_F(PopupBlockerTabHelperTest, MultiplePopups) { + BlockedUrlListObserver observer(helper()); + TestPopupNavigationDelegate::ResultHolder result1; + helper()->AddBlockedPopup( + std::make_unique<TestPopupNavigationDelegate>(GURL(kUrl1), &result1), + blink::mojom::WindowFeatures(), PopupBlockType::kNoGesture); + EXPECT_EQ(result1.total_popups_blocked_on_page, 1); + EXPECT_THAT(observer.blocked_urls(), + UnorderedElementsAre(Pair(0, GURL(kUrl1)))); + EXPECT_EQ(helper()->GetBlockedPopupsCount(), 1u); + + TestPopupNavigationDelegate::ResultHolder result2; + helper()->AddBlockedPopup( + std::make_unique<TestPopupNavigationDelegate>(GURL(kUrl2), &result2), + blink::mojom::WindowFeatures(), PopupBlockType::kNoGesture); + EXPECT_EQ(result2.total_popups_blocked_on_page, 2); + EXPECT_THAT(observer.blocked_urls(), + UnorderedElementsAre(Pair(0, GURL(kUrl1)), Pair(1, GURL(kUrl2)))); + EXPECT_EQ(helper()->GetBlockedPopupsCount(), 2u); + + helper()->ShowBlockedPopup(1, WindowOpenDisposition::NEW_FOREGROUND_TAB); + EXPECT_EQ(helper()->GetBlockedPopupsCount(), 1u); + EXPECT_TRUE(result2.did_navigate); + EXPECT_EQ(result2.navigation_disposition, + WindowOpenDisposition::NEW_FOREGROUND_TAB); + EXPECT_FALSE(result1.did_navigate); + + helper()->ShowBlockedPopup(0, WindowOpenDisposition::CURRENT_TAB); + EXPECT_EQ(helper()->GetBlockedPopupsCount(), 0u); + EXPECT_TRUE(result1.did_navigate); + EXPECT_FALSE(result1.navigation_disposition.has_value()); +} + +TEST_F(PopupBlockerTabHelperTest, DoesNotShowPopupWithInvalidID) { + TestPopupNavigationDelegate::ResultHolder result; + helper()->AddBlockedPopup( + std::make_unique<TestPopupNavigationDelegate>(GURL(kUrl1), &result), + blink::mojom::WindowFeatures(), PopupBlockType::kNoGesture); + EXPECT_EQ(helper()->GetBlockedPopupsCount(), 1u); + + // Invalid ID should not do anything. + helper()->ShowBlockedPopup(1, WindowOpenDisposition::NEW_FOREGROUND_TAB); + EXPECT_EQ(helper()->GetBlockedPopupsCount(), 1u); + EXPECT_FALSE(result.did_navigate); + + helper()->ShowBlockedPopup(0, WindowOpenDisposition::NEW_FOREGROUND_TAB); + EXPECT_EQ(helper()->GetBlockedPopupsCount(), 0u); + EXPECT_TRUE(result.did_navigate); +} + +TEST_F(PopupBlockerTabHelperTest, SetsContentSettingsPopupState) { + auto* content_settings = + content_settings::TabSpecificContentSettings::FromWebContents( + web_contents()); + EXPECT_FALSE(content_settings->IsContentBlocked(ContentSettingsType::POPUPS)); + + TestPopupNavigationDelegate::ResultHolder result; + helper()->AddBlockedPopup( + std::make_unique<TestPopupNavigationDelegate>(GURL(kUrl1), &result), + blink::mojom::WindowFeatures(), PopupBlockType::kNoGesture); + EXPECT_TRUE(content_settings->IsContentBlocked(ContentSettingsType::POPUPS)); + + helper()->AddBlockedPopup( + std::make_unique<TestPopupNavigationDelegate>(GURL(kUrl2), &result), + blink::mojom::WindowFeatures(), PopupBlockType::kNoGesture); + EXPECT_TRUE(content_settings->IsContentBlocked(ContentSettingsType::POPUPS)); + + helper()->ShowBlockedPopup(0, WindowOpenDisposition::NEW_FOREGROUND_TAB); + EXPECT_TRUE(content_settings->IsContentBlocked(ContentSettingsType::POPUPS)); + + helper()->ShowBlockedPopup(1, WindowOpenDisposition::NEW_FOREGROUND_TAB); + EXPECT_FALSE(content_settings->IsContentBlocked(ContentSettingsType::POPUPS)); +} + +TEST_F(PopupBlockerTabHelperTest, ClearsContentSettingsPopupStateOnNavigation) { + TestPopupNavigationDelegate::ResultHolder result; + helper()->AddBlockedPopup( + std::make_unique<TestPopupNavigationDelegate>(GURL(kUrl1), &result), + blink::mojom::WindowFeatures(), PopupBlockType::kNoGesture); + EXPECT_TRUE(content_settings::TabSpecificContentSettings::FromWebContents( + web_contents()) + ->IsContentBlocked(ContentSettingsType::POPUPS)); + + NavigateAndCommit(GURL(kUrl2)); + EXPECT_FALSE(content_settings::TabSpecificContentSettings::FromWebContents( + web_contents()) + ->IsContentBlocked(ContentSettingsType::POPUPS)); +} + +} // namespace blocked_content diff --git a/chromium/components/blocked_content/popup_navigation_delegate.h b/chromium/components/blocked_content/popup_navigation_delegate.h new file mode 100644 index 00000000000..8c7cb02e33b --- /dev/null +++ b/chromium/components/blocked_content/popup_navigation_delegate.h @@ -0,0 +1,55 @@ +// Copyright 2020 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 COMPONENTS_BLOCKED_CONTENT_POPUP_NAVIGATION_DELEGATE_H_ +#define COMPONENTS_BLOCKED_CONTENT_POPUP_NAVIGATION_DELEGATE_H_ + +#include <memory> + +#include "base/optional.h" +#include "third_party/blink/public/mojom/window_features/window_features.mojom-forward.h" +#include "ui/base/window_open_disposition.h" + +class GURL; + +namespace content { +class RenderFrameHost; +class WebContents; +} // namespace content + +namespace blocked_content { + +// A delegate interface to allow an embedder specific representation of a +// navigation. This is stored internally in the popup blocker to recover +// navigations when the user clicks through a previously blocked popup. +class PopupNavigationDelegate { + public: + virtual ~PopupNavigationDelegate() = default; + + // Gets the opener used if new WebContents are created for this navigation. + virtual content::RenderFrameHost* GetOpener() = 0; + + // Gets whether the blocked navigation was initiated by a user gesture. + virtual bool GetOriginalUserGesture() = 0; + + // Gets the URL to be loaded. + virtual const GURL& GetURL() = 0; + + // Performs the navigation. + struct NavigateResult { + content::WebContents* navigated_or_inserted_contents = nullptr; + WindowOpenDisposition disposition = WindowOpenDisposition::UNKNOWN; + }; + virtual NavigateResult NavigateWithGesture( + const blink::mojom::WindowFeatures& window_features, + base::Optional<WindowOpenDisposition> updated_disposition) = 0; + + // Called when the navigation represented by this class was blocked. + virtual void OnPopupBlocked(content::WebContents* web_contents, + int total_popups_blocked_on_page) = 0; +}; + +} // namespace blocked_content + +#endif // COMPONENTS_BLOCKED_CONTENT_POPUP_NAVIGATION_DELEGATE_H_ diff --git a/chromium/components/blocked_content/popup_opener_tab_helper.cc b/chromium/components/blocked_content/popup_opener_tab_helper.cc new file mode 100644 index 00000000000..b0707531d5b --- /dev/null +++ b/chromium/components/blocked_content/popup_opener_tab_helper.cc @@ -0,0 +1,129 @@ +// 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 "components/blocked_content/popup_opener_tab_helper.h" + +#include <utility> + +#include "base/check.h" +#include "base/memory/ptr_util.h" +#include "base/metrics/histogram_macros.h" +#include "base/time/tick_clock.h" +#include "components/blocked_content/popup_tracker.h" +#include "components/content_settings/core/browser/host_content_settings_map.h" +#include "components/content_settings/core/common/content_settings.h" +#include "components/ukm/content/source_url_recorder.h" +#include "content/public/browser/navigation_handle.h" +#include "content/public/browser/web_contents.h" +#include "services/metrics/public/cpp/metrics_utils.h" +#include "services/metrics/public/cpp/ukm_builders.h" +#include "services/metrics/public/cpp/ukm_recorder.h" +#include "ui/base/scoped_visibility_tracker.h" + +namespace blocked_content { + +// static +void PopupOpenerTabHelper::CreateForWebContents( + content::WebContents* contents, + const base::TickClock* tick_clock, + HostContentSettingsMap* settings_map) { + DCHECK(contents); + if (!FromWebContents(contents)) { + contents->SetUserData(UserDataKey(), + base::WrapUnique(new PopupOpenerTabHelper( + contents, tick_clock, settings_map))); + } +} + +PopupOpenerTabHelper::~PopupOpenerTabHelper() { + DCHECK(visibility_tracker_); + base::TimeDelta total_visible_time = + visibility_tracker_->GetForegroundDuration(); + if (did_tab_under()) { + UMA_HISTOGRAM_LONG_TIMES( + "Tab.TabUnder.VisibleTime", + total_visible_time - visible_time_before_tab_under_.value()); + UMA_HISTOGRAM_LONG_TIMES("Tab.TabUnder.VisibleTimeBefore", + visible_time_before_tab_under_.value()); + } + UMA_HISTOGRAM_LONG_TIMES("Tab.VisibleTime", total_visible_time); +} + +void PopupOpenerTabHelper::OnOpenedPopup(PopupTracker* popup_tracker) { + has_opened_popup_since_last_user_gesture_ = true; + MaybeLogPagePopupContentSettings(); + + last_popup_open_time_ = tick_clock_->NowTicks(); +} + +void PopupOpenerTabHelper::OnDidTabUnder() { + // The tab already did a tab-under. + if (did_tab_under()) + return; + + // Tab-under requires a popup, so this better not be null. + DCHECK(!last_popup_open_time_.is_null()); + UMA_HISTOGRAM_LONG_TIMES("Tab.TabUnder.PopupToTabUnderTime", + tick_clock_->NowTicks() - last_popup_open_time_); + + visible_time_before_tab_under_ = visibility_tracker_->GetForegroundDuration(); +} + +PopupOpenerTabHelper::PopupOpenerTabHelper(content::WebContents* web_contents, + const base::TickClock* tick_clock, + HostContentSettingsMap* settings_map) + : content::WebContentsObserver(web_contents), + tick_clock_(tick_clock), + settings_map_(settings_map) { + visibility_tracker_ = std::make_unique<ui::ScopedVisibilityTracker>( + tick_clock_, + web_contents->GetVisibility() != content::Visibility::HIDDEN); +} + +void PopupOpenerTabHelper::OnVisibilityChanged(content::Visibility visibility) { + // TODO(csharrison): Consider handling OCCLUDED tabs the same way as HIDDEN + // tabs. + if (visibility == content::Visibility::HIDDEN) + visibility_tracker_->OnHidden(); + else + visibility_tracker_->OnShown(); +} + +void PopupOpenerTabHelper::DidGetUserInteraction( + const blink::WebInputEvent::Type type) { + has_opened_popup_since_last_user_gesture_ = false; +} + +void PopupOpenerTabHelper::DidStartNavigation( + content::NavigationHandle* navigation_handle) { + // Treat browser-initiated navigations as user interactions. + if (!navigation_handle->IsRendererInitiated()) + has_opened_popup_since_last_user_gesture_ = false; +} + +void PopupOpenerTabHelper::MaybeLogPagePopupContentSettings() { + // If the user has opened a popup, record the page popup settings ukm. + const GURL& url = web_contents()->GetLastCommittedURL(); + if (!url.is_valid()) + return; + + const ukm::SourceId source_id = + ukm::GetSourceIdForWebContentsDocument(web_contents()); + + // Do not record duplicate Popup.Page events for popups opened in succession + // from the same opener. + if (source_id != last_opener_source_id_) { + bool user_allows_popups = settings_map_->GetContentSetting( + url, url, ContentSettingsType::POPUPS, + std::string()) == CONTENT_SETTING_ALLOW; + ukm::builders::Popup_Page(source_id) + .SetAllowed(user_allows_popups) + .Record(ukm::UkmRecorder::Get()); + last_opener_source_id_ = source_id; + } +} + +WEB_CONTENTS_USER_DATA_KEY_IMPL(PopupOpenerTabHelper) + +} // namespace blocked_content diff --git a/chromium/components/blocked_content/popup_opener_tab_helper.h b/chromium/components/blocked_content/popup_opener_tab_helper.h new file mode 100644 index 00000000000..363f0fea1e4 --- /dev/null +++ b/chromium/components/blocked_content/popup_opener_tab_helper.h @@ -0,0 +1,105 @@ +// 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 COMPONENTS_BLOCKED_CONTENT_POPUP_OPENER_TAB_HELPER_H_ +#define COMPONENTS_BLOCKED_CONTENT_POPUP_OPENER_TAB_HELPER_H_ + +#include <memory> + +#include "base/macros.h" +#include "base/optional.h" +#include "base/time/tick_clock.h" +#include "base/time/time.h" +#include "components/content_settings/core/browser/host_content_settings_map.h" +#include "content/public/browser/web_contents_observer.h" +#include "content/public/browser/web_contents_user_data.h" + +namespace base { +class TickClock; +} + +namespace content { +class WebContents; +} + +namespace ui { +class ScopedVisibilityTracker; +} + +namespace blocked_content { +class PopupTracker; + +// This class tracks WebContents for the purpose of logging metrics related to +// popup openers. +class PopupOpenerTabHelper + : public content::WebContentsObserver, + public content::WebContentsUserData<PopupOpenerTabHelper> { + public: + // |tick_clock| overrides the internal time for testing. This doesn't take + // ownership of |tick_clock| or |settings_map|, and they both must outlive the + // PopupOpenerTabHelper instance. + static void CreateForWebContents(content::WebContents* contents, + const base::TickClock* tick_clock, + HostContentSettingsMap* settings_map); + ~PopupOpenerTabHelper() override; + + void OnOpenedPopup(PopupTracker* popup_tracker); + void OnDidTabUnder(); + + bool has_opened_popup_since_last_user_gesture() const { + return has_opened_popup_since_last_user_gesture_; + } + + bool did_tab_under() const { + return visible_time_before_tab_under_.has_value(); + } + + private: + friend class content::WebContentsUserData<PopupOpenerTabHelper>; + + PopupOpenerTabHelper(content::WebContents* web_contents, + const base::TickClock* tick_clock, + HostContentSettingsMap* settings_map); + + // content::WebContentsObserver: + void OnVisibilityChanged(content::Visibility visibility) override; + void DidStartNavigation( + content::NavigationHandle* navigation_handle) override; + void DidGetUserInteraction(const blink::WebInputEvent::Type type) override; + + // Logs user popup content settings if the last committed URL is valid and + // we have not recorded the settings for the opener id of the helper's + // web contents at the time the function is called. + void MaybeLogPagePopupContentSettings(); + + // Visible time for this tab until a tab-under is detected. At which point it + // gets the visible time from the |visibility_tracker_|. Will be unset until a + // tab-under is detected. + base::Optional<base::TimeDelta> visible_time_before_tab_under_; + + // The clock which is used by the visibility trackers. + const base::TickClock* tick_clock_; + + // Keeps track of the total foreground time for this tab. + std::unique_ptr<ui::ScopedVisibilityTracker> visibility_tracker_; + + // Measures the time this WebContents opened a popup. + base::TimeTicks last_popup_open_time_; + + bool has_opened_popup_since_last_user_gesture_ = false; + + // The last source id used for logging Popup_Page. + ukm::SourceId last_opener_source_id_ = ukm::kInvalidSourceId; + + // The settings map for the web contents this object is associated with. + HostContentSettingsMap* settings_map_ = nullptr; + + WEB_CONTENTS_USER_DATA_KEY_DECL(); + + DISALLOW_COPY_AND_ASSIGN(PopupOpenerTabHelper); +}; + +} // namespace blocked_content + +#endif // COMPONENTS_BLOCKED_CONTENT_POPUP_OPENER_TAB_HELPER_H_ diff --git a/chromium/components/blocked_content/popup_tracker.cc b/chromium/components/blocked_content/popup_tracker.cc new file mode 100644 index 00000000000..2735f516abe --- /dev/null +++ b/chromium/components/blocked_content/popup_tracker.cc @@ -0,0 +1,174 @@ +// 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 "components/blocked_content/popup_tracker.h" + +#include <algorithm> + +#include "base/check.h" +#include "base/memory/ptr_util.h" +#include "base/metrics/histogram_macros.h" +#include "base/time/default_tick_clock.h" +#include "components/blocked_content/popup_opener_tab_helper.h" +#include "components/ukm/content/source_url_recorder.h" +#include "content/public/browser/navigation_handle.h" +#include "content/public/browser/web_contents.h" +#include "services/metrics/public/cpp/metrics_utils.h" +#include "services/metrics/public/cpp/ukm_builders.h" +#include "services/metrics/public/cpp/ukm_recorder.h" + +namespace blocked_content { +namespace { + +int CappedUserInteractions(int user_interactions, int max_interactions) { + return std::min(user_interactions, max_interactions); +} + +} // namespace + +PopupTracker* PopupTracker::CreateForWebContents( + content::WebContents* contents, + content::WebContents* opener, + WindowOpenDisposition disposition) { + DCHECK(contents); + DCHECK(opener); + auto* tracker = FromWebContents(contents); + if (!tracker) { + tracker = new PopupTracker(contents, opener, disposition); + contents->SetUserData(UserDataKey(), base::WrapUnique(tracker)); + } + return tracker; +} + +PopupTracker::~PopupTracker() = default; + +PopupTracker::PopupTracker(content::WebContents* contents, + content::WebContents* opener, + WindowOpenDisposition disposition) + : content::WebContentsObserver(contents), + scoped_observer_(this), + visibility_tracker_( + base::DefaultTickClock::GetInstance(), + contents->GetVisibility() != content::Visibility::HIDDEN), + opener_source_id_(ukm::GetSourceIdForWebContentsDocument(opener)), + window_open_disposition_(disposition) { + if (auto* popup_opener = PopupOpenerTabHelper::FromWebContents(opener)) + popup_opener->OnOpenedPopup(this); + + auto* observer_manager = + subresource_filter::SubresourceFilterObserverManager::FromWebContents( + contents); + if (observer_manager) { + scoped_observer_.Add(observer_manager); + } +} + +void PopupTracker::WebContentsDestroyed() { + base::TimeDelta total_foreground_duration = + visibility_tracker_.GetForegroundDuration(); + if (first_load_visible_time_start_) { + base::TimeDelta first_load_visible_time = + first_load_visible_time_ + ? *first_load_visible_time_ + : total_foreground_duration - *first_load_visible_time_start_; + UMA_HISTOGRAM_LONG_TIMES( + "ContentSettings.Popups.FirstDocumentEngagementTime2", + first_load_visible_time); + } + UMA_HISTOGRAM_CUSTOM_TIMES( + "ContentSettings.Popups.EngagementTime", total_foreground_duration, + base::TimeDelta::FromMilliseconds(1), base::TimeDelta::FromHours(6), 50); + if (web_contents()->GetClosedByUserGesture()) { + UMA_HISTOGRAM_CUSTOM_TIMES( + "ContentSettings.Popups.EngagementTime.GestureClose", + total_foreground_duration, base::TimeDelta::FromMilliseconds(1), + base::TimeDelta::FromHours(6), 50); + } + + if (opener_source_id_ != ukm::kInvalidSourceId) { + const int kMaxInteractions = 100; + const int kMaxSubcatagoryInteractions = 50; + ukm::builders::Popup_Closed(opener_source_id_) + .SetEngagementTime(ukm::GetExponentialBucketMinForUserTiming( + total_foreground_duration.InMilliseconds())) + .SetUserInitiatedClose(web_contents()->GetClosedByUserGesture()) + .SetTrusted(is_trusted_) + .SetSafeBrowsingStatus(static_cast<int>(safe_browsing_status_)) + .SetWindowOpenDisposition(static_cast<int>(window_open_disposition_)) + .SetNumInteractions( + CappedUserInteractions(num_interactions_, kMaxInteractions)) + .SetNumActivationInteractions(CappedUserInteractions( + num_activation_events_, kMaxSubcatagoryInteractions)) + .SetNumGestureScrollBeginInteractions(CappedUserInteractions( + num_gesture_scroll_begin_events_, kMaxSubcatagoryInteractions)) + .Record(ukm::UkmRecorder::Get()); + } +} + +void PopupTracker::DidFinishNavigation( + content::NavigationHandle* navigation_handle) { + if (!navigation_handle->HasCommitted() || + navigation_handle->IsSameDocument()) { + return; + } + + if (!first_load_visible_time_start_) { + first_load_visible_time_start_ = + visibility_tracker_.GetForegroundDuration(); + } else if (!first_load_visible_time_) { + first_load_visible_time_ = visibility_tracker_.GetForegroundDuration() - + *first_load_visible_time_start_; + } +} + +void PopupTracker::OnVisibilityChanged(content::Visibility visibility) { + // TODO(csharrison): Consider handling OCCLUDED tabs the same way as HIDDEN + // tabs. + if (visibility == content::Visibility::HIDDEN) + visibility_tracker_.OnHidden(); + else + visibility_tracker_.OnShown(); +} + +void PopupTracker::DidGetUserInteraction( + const blink::WebInputEvent::Type type) { + // TODO(csharrison): It would be nice if ctrl-W could be filtered out here, + // but the initial ctrl key press is registered as a kRawKeyDown. + num_interactions_++; + + if (type == blink::WebInputEvent::Type::kGestureScrollBegin) { + num_gesture_scroll_begin_events_++; + } else { + num_activation_events_++; + } +} + +// This method will always be called before the DidFinishNavigation associated +// with this handle. +// The exception is a navigation restoring a page from back-forward cache -- +// in that case don't issue any requests, therefore we don't get any +// safe browsing callbacks. See the comment above for the mitigation. +void PopupTracker::OnSafeBrowsingChecksComplete( + content::NavigationHandle* navigation_handle, + const subresource_filter::SubresourceFilterSafeBrowsingClient::CheckResult& + result) { + DCHECK(navigation_handle->IsInMainFrame()); + safe_browsing_status_ = PopupSafeBrowsingStatus::kSafe; + if (result.threat_type == + safe_browsing::SBThreatType::SB_THREAT_TYPE_URL_PHISHING || + result.threat_type == safe_browsing::SBThreatType:: + SB_THREAT_TYPE_URL_CLIENT_SIDE_PHISHING || + result.threat_type == + safe_browsing::SBThreatType::SB_THREAT_TYPE_SUBRESOURCE_FILTER) { + safe_browsing_status_ = PopupSafeBrowsingStatus::kUnsafe; + } +} + +void PopupTracker::OnSubresourceFilterGoingAway() { + scoped_observer_.RemoveAll(); +} + +WEB_CONTENTS_USER_DATA_KEY_IMPL(PopupTracker) + +} // namespace blocked_content diff --git a/chromium/components/blocked_content/popup_tracker.h b/chromium/components/blocked_content/popup_tracker.h new file mode 100644 index 00000000000..8366782de61 --- /dev/null +++ b/chromium/components/blocked_content/popup_tracker.h @@ -0,0 +1,113 @@ +// 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 COMPONENTS_BLOCKED_CONTENT_POPUP_TRACKER_H_ +#define COMPONENTS_BLOCKED_CONTENT_POPUP_TRACKER_H_ + +#include "base/macros.h" +#include "base/optional.h" +#include "base/scoped_observer.h" +#include "base/time/time.h" +#include "components/subresource_filter/content/browser/subresource_filter_observer.h" +#include "components/subresource_filter/content/browser/subresource_filter_observer_manager.h" +#include "content/public/browser/web_contents_observer.h" +#include "content/public/browser/web_contents_user_data.h" +#include "services/metrics/public/cpp/ukm_source_id.h" +#include "ui/base/scoped_visibility_tracker.h" +#include "ui/base/window_open_disposition.h" + +namespace content { +class WebContents; +} + +namespace blocked_content { + +// This class tracks new popups, and is used to log metrics on the visibility +// time of the first document in the popup. +// TODO(csharrison): Consider adding more metrics like total visibility for the +// lifetime of the WebContents. +class PopupTracker : public content::WebContentsObserver, + public content::WebContentsUserData<PopupTracker>, + public subresource_filter::SubresourceFilterObserver { + public: + // These values are persisted to logs. Entries should not be renumbered and + // numeric values should never be reused. + enum class PopupSafeBrowsingStatus { + kNoValue = 0, + kSafe = 1, + kUnsafe = 2, + kMaxValue = kUnsafe, + }; + + static PopupTracker* CreateForWebContents(content::WebContents* contents, + content::WebContents* opener, + WindowOpenDisposition disposition); + ~PopupTracker() override; + + void set_is_trusted(bool is_trusted) { is_trusted_ = is_trusted; } + + private: + friend class content::WebContentsUserData<PopupTracker>; + + PopupTracker(content::WebContents* contents, + content::WebContents* opener, + WindowOpenDisposition disposition); + + // content::WebContentsObserver: + void WebContentsDestroyed() override; + void DidFinishNavigation( + content::NavigationHandle* navigation_handle) override; + void OnVisibilityChanged(content::Visibility visibility) override; + void DidGetUserInteraction(const blink::WebInputEvent::Type type) override; + + // subresource_filter::SubresourceFilterObserver: + void OnSafeBrowsingChecksComplete( + content::NavigationHandle* navigation_handle, + const subresource_filter::SubresourceFilterSafeBrowsingClient:: + CheckResult& result) override; + void OnSubresourceFilterGoingAway() override; + + ScopedObserver<subresource_filter::SubresourceFilterObserverManager, + subresource_filter::SubresourceFilterObserver> + scoped_observer_; + + // Will be unset until the first navigation commits. Will be set to the total + // time the contents was visible at commit time. + base::Optional<base::TimeDelta> first_load_visible_time_start_; + // Will be unset until the second navigation commits. Is the total time the + // contents is visible while the first document is loading (after commit). + base::Optional<base::TimeDelta> first_load_visible_time_; + + ui::ScopedVisibilityTracker visibility_tracker_; + + // The number of user interactions occurring in this popup tab. + int num_interactions_ = 0; + // The number of user interacitons in a popup tab broken down into + // user activation and gesture scroll begin events. + int num_activation_events_ = 0; + int num_gesture_scroll_begin_events_ = 0; + + // The id of the web contents that created the popup at the time of creation. + // SourceIds are permanent so it's okay to use at any point so long as it's + // not invalid. + const ukm::SourceId opener_source_id_; + + bool is_trusted_ = false; + + // Whether the pop-up navigated to a site on the safe browsing list. Set when + // the safe browsing checks complete. + PopupSafeBrowsingStatus safe_browsing_status_ = + PopupSafeBrowsingStatus::kNoValue; + + // The window open disposition used when creating the popup. + const WindowOpenDisposition window_open_disposition_; + + WEB_CONTENTS_USER_DATA_KEY_DECL(); + + DISALLOW_COPY_AND_ASSIGN(PopupTracker); +}; + +} // namespace blocked_content + +#endif // COMPONENTS_BLOCKED_CONTENT_POPUP_TRACKER_H_ diff --git a/chromium/components/blocked_content/pref_names.cc b/chromium/components/blocked_content/pref_names.cc new file mode 100644 index 00000000000..a0d41b5f52e --- /dev/null +++ b/chromium/components/blocked_content/pref_names.cc @@ -0,0 +1,16 @@ +// Copyright 2020 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 "components/blocked_content/pref_names.h" + +namespace blocked_content { +namespace prefs { + +// A bool pref that indicates whether interventions for abusive experiences +// should be enforced. +const char kAbusiveExperienceInterventionEnforce[] = + "abusive_experience_intervention_enforce"; + +} // namespace prefs +} // namespace blocked_content diff --git a/chromium/components/blocked_content/pref_names.h b/chromium/components/blocked_content/pref_names.h new file mode 100644 index 00000000000..9d6050c92f6 --- /dev/null +++ b/chromium/components/blocked_content/pref_names.h @@ -0,0 +1,16 @@ +// Copyright 2020 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 COMPONENTS_BLOCKED_CONTENT_PREF_NAMES_H_ +#define COMPONENTS_BLOCKED_CONTENT_PREF_NAMES_H_ + +namespace blocked_content { +namespace prefs { + +extern const char kAbusiveExperienceInterventionEnforce[]; + +} // namespace prefs +} // namespace blocked_content + +#endif // COMPONENTS_BLOCKED_CONTENT_PREF_NAMES_H diff --git a/chromium/components/blocked_content/safe_browsing_triggered_popup_blocker.cc b/chromium/components/blocked_content/safe_browsing_triggered_popup_blocker.cc new file mode 100644 index 00000000000..792295121cf --- /dev/null +++ b/chromium/components/blocked_content/safe_browsing_triggered_popup_blocker.cc @@ -0,0 +1,194 @@ +// 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 "components/blocked_content/safe_browsing_triggered_popup_blocker.h" + +#include <utility> + +#include "base/memory/ptr_util.h" +#include "base/metrics/histogram_macros.h" +#include "components/blocked_content/pref_names.h" +#include "components/pref_registry/pref_registry_syncable.h" +#include "components/prefs/pref_service.h" +#include "components/safe_browsing/core/db/util.h" +#include "components/safe_browsing/core/db/v4_protocol_manager_util.h" +#include "components/subresource_filter/content/browser/subresource_filter_safe_browsing_activation_throttle.h" +#include "components/user_prefs/user_prefs.h" +#include "content/public/browser/back_forward_cache.h" +#include "content/public/browser/browser_context.h" +#include "content/public/browser/navigation_handle.h" +#include "content/public/browser/render_frame_host.h" +#include "content/public/browser/web_contents.h" +#include "third_party/blink/public/common/navigation/triggering_event_info.h" +#include "third_party/blink/public/mojom/devtools/console_message.mojom.h" + +namespace blocked_content { +namespace { + +void LogAction(SafeBrowsingTriggeredPopupBlocker::Action action) { + UMA_HISTOGRAM_ENUMERATION("ContentSettings.Popups.StrongBlockerActions", + action, + SafeBrowsingTriggeredPopupBlocker::Action::kCount); +} + +} // namespace + +using safe_browsing::SubresourceFilterLevel; + +const base::Feature kAbusiveExperienceEnforce{"AbusiveExperienceEnforce", + base::FEATURE_ENABLED_BY_DEFAULT}; + +SafeBrowsingTriggeredPopupBlocker::PageData::PageData() = default; + +SafeBrowsingTriggeredPopupBlocker::PageData::~PageData() { + if (is_triggered_) { + UMA_HISTOGRAM_COUNTS_100("ContentSettings.Popups.StrongBlocker.NumBlocked", + num_popups_blocked_); + } +} + +// static +void SafeBrowsingTriggeredPopupBlocker::RegisterProfilePrefs( + user_prefs::PrefRegistrySyncable* registry) { + registry->RegisterBooleanPref(prefs::kAbusiveExperienceInterventionEnforce, + true /* default_value */); +} + +// static +void SafeBrowsingTriggeredPopupBlocker::MaybeCreate( + content::WebContents* web_contents) { + if (!IsEnabled(web_contents)) + return; + + auto* observer_manager = + subresource_filter::SubresourceFilterObserverManager::FromWebContents( + web_contents); + if (!observer_manager) + return; + + if (FromWebContents(web_contents)) + return; + + web_contents->SetUserData( + UserDataKey(), base::WrapUnique(new SafeBrowsingTriggeredPopupBlocker( + web_contents, observer_manager))); +} + +SafeBrowsingTriggeredPopupBlocker::~SafeBrowsingTriggeredPopupBlocker() = + default; + +bool SafeBrowsingTriggeredPopupBlocker::ShouldApplyAbusivePopupBlocker() { + LogAction(Action::kConsidered); + if (!current_page_data_->is_triggered()) + return false; + + if (!IsEnabled(web_contents())) + return false; + + LogAction(Action::kBlocked); + current_page_data_->inc_num_popups_blocked(); + web_contents()->GetMainFrame()->AddMessageToConsole( + blink::mojom::ConsoleMessageLevel::kError, kAbusiveEnforceMessage); + return true; +} + +SafeBrowsingTriggeredPopupBlocker::SafeBrowsingTriggeredPopupBlocker( + content::WebContents* web_contents, + subresource_filter::SubresourceFilterObserverManager* observer_manager) + : content::WebContentsObserver(web_contents), + scoped_observer_(this), + current_page_data_(std::make_unique<PageData>()) { + DCHECK(observer_manager); + scoped_observer_.Add(observer_manager); +} + +void SafeBrowsingTriggeredPopupBlocker::DidFinishNavigation( + content::NavigationHandle* navigation_handle) { + if (!navigation_handle->IsInMainFrame()) + return; + + base::Optional<SubresourceFilterLevel> level; + level_for_next_committed_navigation_.swap(level); + + // Only care about main frame navigations that commit. + if (!navigation_handle->HasCommitted() || + navigation_handle->IsSameDocument()) { + return; + } + + DCHECK(current_page_data_); + current_page_data_ = std::make_unique<PageData>(); + if (navigation_handle->IsErrorPage()) + return; + + // Log a warning only if we've matched a warn-only safe browsing list. + if (level == SubresourceFilterLevel::ENFORCE) { + current_page_data_->set_is_triggered(true); + LogAction(Action::kEnforcedSite); + // When a page is restored from back-forward cache, we don't get + // OnSafeBrowsingChecksComplete callback, so |level| will always + // be empty. + // To work around this, we disable back-forward cache if the original + // page load had abusive enforcement - this means that not doing checks on + // back-forward navigation is fine as it's guaranteed that + // the original page load didn't have enforcement. + // Note that it's possible for the safe browsing list to update while + // the page is in the cache, the risk of this is mininal due to + // having a time limit for how long pages are allowed to be in the + // cache. + content::BackForwardCache::DisableForRenderFrameHost( + navigation_handle->GetRenderFrameHost(), + "SafeBrowsingTriggeredPopupBlocker"); + } else if (level == SubresourceFilterLevel::WARN) { + web_contents()->GetMainFrame()->AddMessageToConsole( + blink::mojom::ConsoleMessageLevel::kWarning, kAbusiveWarnMessage); + LogAction(Action::kWarningSite); + } + LogAction(Action::kNavigation); +} + +// This method will always be called before the DidFinishNavigation associated +// with this handle. +// The exception is a navigation restoring a page from back-forward cache -- +// in that case don't issue any requests, therefore we don't get any +// safe browsing callbacks. See the comment above for the mitigation. +void SafeBrowsingTriggeredPopupBlocker::OnSafeBrowsingChecksComplete( + content::NavigationHandle* navigation_handle, + const subresource_filter::SubresourceFilterSafeBrowsingClient::CheckResult& + result) { + DCHECK(navigation_handle->IsInMainFrame()); + base::Optional<safe_browsing::SubresourceFilterLevel> match_level; + if (result.threat_type == + safe_browsing::SBThreatType::SB_THREAT_TYPE_SUBRESOURCE_FILTER) { + auto abusive = result.threat_metadata.subresource_filter_match.find( + safe_browsing::SubresourceFilterType::ABUSIVE); + if (abusive != result.threat_metadata.subresource_filter_match.end()) + match_level = abusive->second; + } + + if (match_level.has_value()) { + level_for_next_committed_navigation_ = match_level; + } +} + +void SafeBrowsingTriggeredPopupBlocker::OnSubresourceFilterGoingAway() { + scoped_observer_.RemoveAll(); +} + +bool SafeBrowsingTriggeredPopupBlocker::IsEnabled( + content::WebContents* web_contents) { + // If feature is disabled, return false. This is done so that if the feature + // is broken it can be disabled irrespective of the policy. + if (!base::FeatureList::IsEnabled(kAbusiveExperienceEnforce)) + return false; + + // If enterprise policy is not set, this will return true which is the default + // preference value. + return user_prefs::UserPrefs::Get(web_contents->GetBrowserContext()) + ->GetBoolean(prefs::kAbusiveExperienceInterventionEnforce); +} + +WEB_CONTENTS_USER_DATA_KEY_IMPL(SafeBrowsingTriggeredPopupBlocker) + +} // namespace blocked_content diff --git a/chromium/components/blocked_content/safe_browsing_triggered_popup_blocker.h b/chromium/components/blocked_content/safe_browsing_triggered_popup_blocker.h new file mode 100644 index 00000000000..4daa6c70756 --- /dev/null +++ b/chromium/components/blocked_content/safe_browsing_triggered_popup_blocker.h @@ -0,0 +1,148 @@ +// 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 COMPONENTS_BLOCKED_CONTENT_SAFE_BROWSING_TRIGGERED_POPUP_BLOCKER_H_ +#define COMPONENTS_BLOCKED_CONTENT_SAFE_BROWSING_TRIGGERED_POPUP_BLOCKER_H_ + +#include <memory> + +#include "base/feature_list.h" +#include "base/macros.h" +#include "base/optional.h" +#include "base/scoped_observer.h" +#include "components/safe_browsing/core/db/util.h" +#include "components/subresource_filter/content/browser/subresource_filter_observer.h" +#include "components/subresource_filter/content/browser/subresource_filter_observer_manager.h" +#include "content/public/browser/web_contents_observer.h" + +namespace content { +class WebContents; +} // namespace content + +namespace user_prefs { +class PrefRegistrySyncable; +} + +namespace blocked_content { +extern const base::Feature kAbusiveExperienceEnforce; + +constexpr char kAbusiveEnforceMessage[] = + "Chrome prevented this site from opening a new tab or window. Learn more " + "at https://www.chromestatus.com/feature/5243055179300864"; +constexpr char kAbusiveWarnMessage[] = + "Chrome might start preventing this site from opening new tabs or " + "windows in the future. Learn more at " + "https://www.chromestatus.com/feature/5243055179300864"; + +// This class observes main frame navigation checks incoming from safe browsing +// (currently implemented by the subresource_filter component). For navigations +// which match the ABUSIVE safe browsing list, this class will help the popup +// tab helper in applying a stronger policy for blocked popups. +class SafeBrowsingTriggeredPopupBlocker + : public content::WebContentsObserver, + public content::WebContentsUserData<SafeBrowsingTriggeredPopupBlocker>, + public subresource_filter::SubresourceFilterObserver { + public: + // This enum backs a histogram. Please append new entries to the end, and + // update enums.xml when making changes. + enum class Action : int { + // User committed a navigation to a non-error page. + kNavigation, + + // Safe Browsing considered this page abusive and the page should be warned. + // Logged at navigation commit. + kWarningSite, + + // Safe Browsing considered this page abusive and the page should be be + // blocked against. Logged at navigation commit. + kEnforcedSite, + + // The popup blocker called into this object to ask if the strong blocking + // should be applied. + kConsidered, + + // This object responded to the popup blocker in the affirmative, and the + // popup was blocked. + kBlocked, + + // Add new entries before this one + kCount + }; + + static void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry); + + // Creates a SafeBrowsingTriggeredPopupBlocker and attaches it (via UserData) + // to |web_contents|. + static void MaybeCreate(content::WebContents* web_contents); + ~SafeBrowsingTriggeredPopupBlocker() override; + + bool ShouldApplyAbusivePopupBlocker(); + + private: + friend class content::WebContentsUserData<SafeBrowsingTriggeredPopupBlocker>; + // The |web_contents| and |observer_manager| are expected to be + // non-nullptr. + SafeBrowsingTriggeredPopupBlocker( + content::WebContents* web_contents, + subresource_filter::SubresourceFilterObserverManager* observer_manager); + + // content::WebContentsObserver: + void DidFinishNavigation( + content::NavigationHandle* navigation_handle) override; + + // subresource_filter::SubresourceFilterObserver: + void OnSafeBrowsingChecksComplete( + content::NavigationHandle* navigation_handle, + const subresource_filter::SubresourceFilterSafeBrowsingClient:: + CheckResult& result) override; + void OnSubresourceFilterGoingAway() override; + + // Enabled state is governed by both a feature flag and a pref (which can be + // controlled by enterprise policy). + static bool IsEnabled(content::WebContents* web_contents); + + // Data scoped to a single page. Will be reset at navigation commit. + class PageData { + public: + PageData(); + + // Logs UMA in the destructor based on the number of popups blocked. + ~PageData(); + + void inc_num_popups_blocked() { ++num_popups_blocked_; } + + void set_is_triggered(bool is_triggered) { is_triggered_ = is_triggered; } + bool is_triggered() const { return is_triggered_; } + + private: + // How many popups are blocked in this page. + int num_popups_blocked_ = 0; + + // Whether the current committed page load should trigger the stronger popup + // blocker. + bool is_triggered_ = false; + + DISALLOW_COPY_AND_ASSIGN(PageData); + }; + + ScopedObserver<subresource_filter::SubresourceFilterObserverManager, + subresource_filter::SubresourceFilterObserver> + scoped_observer_; + + // Whether the next main frame navigation that commits should trigger the + // stronger popup blocker in enforce or warn mode. + base::Optional<safe_browsing::SubresourceFilterLevel> + level_for_next_committed_navigation_; + + // Should never be nullptr. + std::unique_ptr<PageData> current_page_data_; + + WEB_CONTENTS_USER_DATA_KEY_DECL(); + + DISALLOW_COPY_AND_ASSIGN(SafeBrowsingTriggeredPopupBlocker); +}; + +} // namespace blocked_content + +#endif // COMPONENTS_BLOCKED_CONTENT_SAFE_BROWSING_TRIGGERED_POPUP_BLOCKER_H_ diff --git a/chromium/components/blocked_content/safe_browsing_triggered_popup_blocker_unittest.cc b/chromium/components/blocked_content/safe_browsing_triggered_popup_blocker_unittest.cc new file mode 100644 index 00000000000..827799894c9 --- /dev/null +++ b/chromium/components/blocked_content/safe_browsing_triggered_popup_blocker_unittest.cc @@ -0,0 +1,501 @@ +// 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 "components/blocked_content/safe_browsing_triggered_popup_blocker.h" + +#include <memory> +#include <string> +#include <utility> +#include <vector> + +#include "base/macros.h" +#include "base/run_loop.h" +#include "base/task/post_task.h" +#include "base/test/metrics/histogram_tester.h" +#include "base/test/scoped_feature_list.h" +#include "components/blocked_content/popup_blocker.h" +#include "components/blocked_content/popup_blocker_tab_helper.h" +#include "components/blocked_content/popup_navigation_delegate.h" +#include "components/blocked_content/test/test_popup_navigation_delegate.h" +#include "components/content_settings/browser/tab_specific_content_settings.h" +#include "components/content_settings/browser/test_tab_specific_content_settings_delegate.h" +#include "components/content_settings/core/browser/host_content_settings_map.h" +#include "components/subresource_filter/content/browser/fake_safe_browsing_database_manager.h" +#include "components/subresource_filter/content/browser/subresource_filter_client.h" +#include "components/subresource_filter/content/browser/subresource_filter_observer_manager.h" +#include "components/subresource_filter/content/browser/subresource_filter_safe_browsing_activation_throttle.h" +#include "components/subresource_filter/content/browser/subresource_filter_safe_browsing_client.h" +#include "components/subresource_filter/core/browser/subresource_filter_constants.h" +#include "components/sync_preferences/testing_pref_service_syncable.h" +#include "components/user_prefs/user_prefs.h" +#include "content/public/browser/browser_context.h" +#include "content/public/browser/browser_task_traits.h" +#include "content/public/browser/navigation_handle.h" +#include "content/public/browser/navigation_throttle.h" +#include "content/public/test/navigation_simulator.h" +#include "content/public/test/test_navigation_throttle_inserter.h" +#include "content/public/test/test_renderer_host.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/blink/public/common/navigation/triggering_event_info.h" +#include "third_party/blink/public/mojom/window_features/window_features.mojom.h" +#include "ui/base/page_transition_types.h" +#include "ui/base/window_open_disposition.h" +#include "url/gurl.h" + +namespace blocked_content { +const char kNumBlockedHistogram[] = + "ContentSettings.Popups.StrongBlocker.NumBlocked"; + +class SafeBrowsingTriggeredPopupBlockerTest + : public content::RenderViewHostTestHarness, + public subresource_filter::SubresourceFilterClient { + public: + SafeBrowsingTriggeredPopupBlockerTest() = default; + ~SafeBrowsingTriggeredPopupBlockerTest() override { + settings_map_->ShutdownOnUIThread(); + } + + // subresource_filter::SubresourceFilterClient: + void ShowNotification() override {} + subresource_filter::mojom::ActivationLevel OnPageActivationComputed( + content::NavigationHandle* navigation_handle, + subresource_filter::mojom::ActivationLevel initial_activation_level, + subresource_filter::ActivationDecision* decision) override { + return initial_activation_level; + } + + // content::RenderViewHostTestHarness: + void SetUp() override { + content::RenderViewHostTestHarness::SetUp(); + + fake_safe_browsing_database_ = + base::MakeRefCounted<FakeSafeBrowsingDatabaseManager>(); + + user_prefs::UserPrefs::Set(browser_context(), &pref_service_); + SafeBrowsingTriggeredPopupBlocker::RegisterProfilePrefs( + pref_service_.registry()); + HostContentSettingsMap::RegisterProfilePrefs(pref_service_.registry()); + settings_map_ = base::MakeRefCounted<HostContentSettingsMap>( + &pref_service_, false, false, false, false); + + scoped_feature_list_ = DefaultFeatureList(); + subresource_filter::SubresourceFilterObserverManager::CreateForWebContents( + web_contents()); + PopupBlockerTabHelper::CreateForWebContents(web_contents()); + content_settings::TabSpecificContentSettings::CreateForWebContents( + web_contents(), + std::make_unique< + content_settings::TestTabSpecificContentSettingsDelegate>( + /*prefs=*/nullptr, settings_map_.get())); + popup_blocker_ = + SafeBrowsingTriggeredPopupBlocker::FromWebContents(web_contents()); + + throttle_inserter_ = + std::make_unique<content::TestNavigationThrottleInserter>( + web_contents(), + base::BindRepeating( + &SafeBrowsingTriggeredPopupBlockerTest::CreateThrottle, + base::Unretained(this))); + } + + virtual std::unique_ptr<base::test::ScopedFeatureList> DefaultFeatureList() { + auto feature_list = std::make_unique<base::test::ScopedFeatureList>(); + feature_list->InitAndEnableFeature(kAbusiveExperienceEnforce); + return feature_list; + } + + FakeSafeBrowsingDatabaseManager* fake_safe_browsing_database() { + return fake_safe_browsing_database_.get(); + } + + base::test::ScopedFeatureList* ResetFeatureAndGet() { + scoped_feature_list_ = std::make_unique<base::test::ScopedFeatureList>(); + return scoped_feature_list_.get(); + } + + SafeBrowsingTriggeredPopupBlocker* popup_blocker() { return popup_blocker_; } + + void SimulateDeleteContents() { + DeleteContents(); + popup_blocker_ = nullptr; + } + + void MarkUrlAsAbusiveWithLevel(const GURL& url, + safe_browsing::SubresourceFilterLevel level) { + safe_browsing::ThreatMetadata metadata; + metadata.subresource_filter_match + [safe_browsing::SubresourceFilterType::ABUSIVE] = level; + fake_safe_browsing_database()->AddBlocklistedUrl( + url, safe_browsing::SB_THREAT_TYPE_SUBRESOURCE_FILTER, metadata); + } + + void MarkUrlAsAbusiveEnforce(const GURL& url) { + MarkUrlAsAbusiveWithLevel(url, + safe_browsing::SubresourceFilterLevel::ENFORCE); + } + + void MarkUrlAsAbusiveWarning(const GURL& url) { + MarkUrlAsAbusiveWithLevel(url, safe_browsing::SubresourceFilterLevel::WARN); + } + + const std::vector<std::string>& GetMainFrameConsoleMessages() { + content::RenderFrameHostTester* rfh_tester = + content::RenderFrameHostTester::For(main_rfh()); + return rfh_tester->GetConsoleMessages(); + } + + HostContentSettingsMap* settings_map() { return settings_map_.get(); } + + private: + std::unique_ptr<content::NavigationThrottle> CreateThrottle( + content::NavigationHandle* handle) { + return std::make_unique< + subresource_filter::SubresourceFilterSafeBrowsingActivationThrottle>( + handle, this, content::GetIOThreadTaskRunner({}), + fake_safe_browsing_database_); + } + + std::unique_ptr<base::test::ScopedFeatureList> scoped_feature_list_; + scoped_refptr<FakeSafeBrowsingDatabaseManager> fake_safe_browsing_database_; + SafeBrowsingTriggeredPopupBlocker* popup_blocker_ = nullptr; + std::unique_ptr<content::TestNavigationThrottleInserter> throttle_inserter_; + sync_preferences::TestingPrefServiceSyncable pref_service_; + scoped_refptr<HostContentSettingsMap> settings_map_; + + DISALLOW_COPY_AND_ASSIGN(SafeBrowsingTriggeredPopupBlockerTest); +}; + +struct RedirectSamplesAndResults { + GURL initial_url; + GURL redirect_url; + bool expect_strong_blocker; +}; + +// We always make our decision to trigger on the last entry in the chain. +TEST_F(SafeBrowsingTriggeredPopupBlockerTest, + MatchOnSafeBrowsingWithRedirectChain) { + GURL enforce_url("https://example.enforce"); + GURL warning_url("https://example.warning"); + GURL regular_url("https://example.regular"); + MarkUrlAsAbusiveEnforce(enforce_url); + MarkUrlAsAbusiveWarning(warning_url); + + const RedirectSamplesAndResults kTestCases[] = { + {enforce_url, regular_url, false}, + {regular_url, enforce_url, true}, + {warning_url, enforce_url, true}, + {enforce_url, warning_url, false}}; + + for (const auto& test_case : kTestCases) { + std::unique_ptr<content::NavigationSimulator> simulator = + content::NavigationSimulator::CreateRendererInitiated( + test_case.initial_url, web_contents()->GetMainFrame()); + simulator->Start(); + simulator->Redirect(test_case.redirect_url); + simulator->Commit(); + EXPECT_EQ(test_case.expect_strong_blocker, + popup_blocker()->ShouldApplyAbusivePopupBlocker()); + } +} + +TEST_F(SafeBrowsingTriggeredPopupBlockerTest, MatchingURL_BlocksPopupAndLogs) { + const GURL url("https://example.test/"); + MarkUrlAsAbusiveEnforce(url); + NavigateAndCommit(url); + EXPECT_TRUE(GetMainFrameConsoleMessages().empty()); + + EXPECT_TRUE(popup_blocker()->ShouldApplyAbusivePopupBlocker()); + EXPECT_EQ(1u, GetMainFrameConsoleMessages().size()); + EXPECT_EQ(GetMainFrameConsoleMessages().front(), kAbusiveEnforceMessage); +} + +TEST_F(SafeBrowsingTriggeredPopupBlockerTest, + MatchingURL_BlocksPopupFromOpenURL) { + const GURL url("https://example.test/"); + MarkUrlAsAbusiveEnforce(url); + NavigateAndCommit(url); + + // If the popup is coming from OpenURL params, the strong popup blocker is + // only going to look at the triggering event info. It will only block the + // popup if we know the triggering event is untrusted. + GURL popup_url("https://example.popup/"); + content::OpenURLParams params( + popup_url, content::Referrer(), WindowOpenDisposition::NEW_FOREGROUND_TAB, + ui::PAGE_TRANSITION_LINK, true /* is_renderer_initiated */); + params.user_gesture = true; + params.triggering_event_info = + blink::TriggeringEventInfo::kFromUntrustedEvent; + + MaybeBlockPopup(web_contents(), nullptr, + std::make_unique<TestPopupNavigationDelegate>( + popup_url, nullptr /* result_holder */), + ¶ms, blink::mojom::WindowFeatures(), settings_map()); + + EXPECT_EQ(1u, PopupBlockerTabHelper::FromWebContents(web_contents()) + ->GetBlockedPopupsCount()); +} + +TEST_F(SafeBrowsingTriggeredPopupBlockerTest, + MatchingURLTrusted_DoesNotBlockPopup) { + const GURL url("https://example.test/"); + MarkUrlAsAbusiveEnforce(url); + NavigateAndCommit(url); + + // If the popup is coming from OpenURL params, the strong popup blocker is + // only going to look at the triggering event info. It will only block the + // popup if we know the triggering event is untrusted. + GURL popup_url("https://example.popup/"); + content::OpenURLParams params( + popup_url, content::Referrer(), WindowOpenDisposition::NEW_FOREGROUND_TAB, + ui::PAGE_TRANSITION_LINK, true /* is_renderer_initiated */); + params.user_gesture = true; + params.triggering_event_info = blink::TriggeringEventInfo::kFromTrustedEvent; + + MaybeBlockPopup(web_contents(), nullptr, + std::make_unique<TestPopupNavigationDelegate>( + popup_url, nullptr /* result_holder */), + ¶ms, blink::mojom::WindowFeatures(), settings_map()); + + EXPECT_EQ(0u, PopupBlockerTabHelper::FromWebContents(web_contents()) + ->GetBlockedPopupsCount()); +} + +TEST_F(SafeBrowsingTriggeredPopupBlockerTest, NoMatch_NoBlocking) { + const GURL url("https://example.test/"); + NavigateAndCommit(url); + EXPECT_FALSE(popup_blocker()->ShouldApplyAbusivePopupBlocker()); + EXPECT_TRUE(GetMainFrameConsoleMessages().empty()); +} + +TEST_F(SafeBrowsingTriggeredPopupBlockerTest, FeatureEnabledByDefault) { + ResetFeatureAndGet(); + SafeBrowsingTriggeredPopupBlocker::MaybeCreate(web_contents()); + EXPECT_NE(nullptr, + SafeBrowsingTriggeredPopupBlocker::FromWebContents(web_contents())); +} + +TEST_F(SafeBrowsingTriggeredPopupBlockerTest, OnlyBlockOnMatchingUrls) { + const GURL url1("https://example.first/"); + const GURL url2("https://example.second/"); + const GURL url3("https://example.third/"); + // Only mark url2 as abusive. + MarkUrlAsAbusiveEnforce(url2); + + NavigateAndCommit(url1); + EXPECT_FALSE(popup_blocker()->ShouldApplyAbusivePopupBlocker()); + + NavigateAndCommit(url2); + EXPECT_TRUE(popup_blocker()->ShouldApplyAbusivePopupBlocker()); + + NavigateAndCommit(url3); + EXPECT_FALSE(popup_blocker()->ShouldApplyAbusivePopupBlocker()); + + NavigateAndCommit(url1); + EXPECT_FALSE(popup_blocker()->ShouldApplyAbusivePopupBlocker()); +} + +TEST_F(SafeBrowsingTriggeredPopupBlockerTest, + SameDocumentNavigation_MaintainsBlocking) { + const GURL url("https://example.first/"); + const GURL hash_url("https://example.first/#hash"); + + MarkUrlAsAbusiveEnforce(url); + NavigateAndCommit(url); + EXPECT_TRUE(popup_blocker()->ShouldApplyAbusivePopupBlocker()); + + // This is merely a same document navigation, keep the popup blocker. + NavigateAndCommit(hash_url); + EXPECT_TRUE(popup_blocker()->ShouldApplyAbusivePopupBlocker()); +} + +TEST_F(SafeBrowsingTriggeredPopupBlockerTest, + FailNavigation_MaintainsBlocking) { + const GURL url("https://example.first/"); + const GURL fail_url("https://example.fail/"); + + MarkUrlAsAbusiveEnforce(url); + NavigateAndCommit(url); + EXPECT_TRUE(popup_blocker()->ShouldApplyAbusivePopupBlocker()); + + // Abort the navigation before it commits. + content::NavigationSimulator::NavigateAndFailFromDocument( + fail_url, net::ERR_ABORTED, main_rfh()); + EXPECT_TRUE(popup_blocker()->ShouldApplyAbusivePopupBlocker()); + + // Committing an error page should probably reset the blocker though, despite + // the fact that it is probably a bug for an error page to spawn popups. + content::NavigationSimulator::NavigateAndFailFromDocument( + fail_url, net::ERR_CONNECTION_RESET, main_rfh()); + EXPECT_FALSE(popup_blocker()->ShouldApplyAbusivePopupBlocker()); +} + +TEST_F(SafeBrowsingTriggeredPopupBlockerTest, LogActions) { + base::HistogramTester histogram_tester; + const char kActionHistogram[] = "ContentSettings.Popups.StrongBlockerActions"; + int total_count = 0; + // Call this when a new histogram entry is logged. Call it multiple times if + // multiple entries are logged. + auto check_histogram = [&](SafeBrowsingTriggeredPopupBlocker::Action action, + int expected_count) { + histogram_tester.ExpectBucketCount( + kActionHistogram, static_cast<int>(action), expected_count); + total_count++; + }; + + const GURL url_enforce("https://example.enforce/"); + const GURL url_warn("https://example.warn/"); + const GURL url_nothing("https://example.nothing/"); + MarkUrlAsAbusiveEnforce(url_enforce); + MarkUrlAsAbusiveWarning(url_warn); + + // Navigate to an enforce site. + NavigateAndCommit(url_enforce); + check_histogram(SafeBrowsingTriggeredPopupBlocker::Action::kNavigation, 1); + check_histogram(SafeBrowsingTriggeredPopupBlocker::Action::kEnforcedSite, 1); + histogram_tester.ExpectTotalCount(kActionHistogram, total_count); + + // Block two popups. + EXPECT_TRUE(popup_blocker()->ShouldApplyAbusivePopupBlocker()); + check_histogram(SafeBrowsingTriggeredPopupBlocker::Action::kConsidered, 1); + check_histogram(SafeBrowsingTriggeredPopupBlocker::Action::kBlocked, 1); + histogram_tester.ExpectTotalCount(kActionHistogram, total_count); + + EXPECT_TRUE(popup_blocker()->ShouldApplyAbusivePopupBlocker()); + check_histogram(SafeBrowsingTriggeredPopupBlocker::Action::kConsidered, 2); + check_histogram(SafeBrowsingTriggeredPopupBlocker::Action::kBlocked, 2); + histogram_tester.ExpectTotalCount(kActionHistogram, total_count); + + // Only log the num blocked histogram after navigation. + histogram_tester.ExpectTotalCount(kNumBlockedHistogram, 0); + + // Navigate to a warn site. + NavigateAndCommit(url_warn); + histogram_tester.ExpectBucketCount(kNumBlockedHistogram, 2, 1); + + check_histogram(SafeBrowsingTriggeredPopupBlocker::Action::kNavigation, 2); + check_histogram(SafeBrowsingTriggeredPopupBlocker::Action::kWarningSite, 1); + histogram_tester.ExpectTotalCount(kActionHistogram, total_count); + + // Let one popup through. + EXPECT_FALSE(popup_blocker()->ShouldApplyAbusivePopupBlocker()); + check_histogram(SafeBrowsingTriggeredPopupBlocker::Action::kConsidered, 3); + histogram_tester.ExpectTotalCount(kActionHistogram, total_count); + + // Navigate to a site not matched in Safe Browsing. + NavigateAndCommit(url_nothing); + check_histogram(SafeBrowsingTriggeredPopupBlocker::Action::kNavigation, 3); + histogram_tester.ExpectTotalCount(kActionHistogram, total_count); + + // Let one popup through. + EXPECT_FALSE(popup_blocker()->ShouldApplyAbusivePopupBlocker()); + check_histogram(SafeBrowsingTriggeredPopupBlocker::Action::kConsidered, 4); + histogram_tester.ExpectTotalCount(kActionHistogram, total_count); + + histogram_tester.ExpectTotalCount(kNumBlockedHistogram, 1); +} + +TEST_F(SafeBrowsingTriggeredPopupBlockerTest, LogBlockMetricsOnClose) { + base::HistogramTester histogram_tester; + const GURL url_enforce("https://example.enforce/"); + MarkUrlAsAbusiveEnforce(url_enforce); + + NavigateAndCommit(url_enforce); + EXPECT_TRUE(popup_blocker()->ShouldApplyAbusivePopupBlocker()); + + histogram_tester.ExpectTotalCount(kNumBlockedHistogram, 0); + // Simulate deleting the web contents. + SimulateDeleteContents(); + histogram_tester.ExpectUniqueSample(kNumBlockedHistogram, 1, 1); +} + +TEST_F(SafeBrowsingTriggeredPopupBlockerTest, + WarningMatchWithoutAdBlockOnAbusiveSites_OnlyLogs) { + base::test::ScopedFeatureList scoped_feature_list; + scoped_feature_list.InitAndDisableFeature( + subresource_filter::kFilterAdsOnAbusiveSites); + + const GURL url("https://example.test/"); + MarkUrlAsAbusiveWarning(url); + NavigateAndCommit(url); + + // Warning should come at navigation commit time, not at popup time. + EXPECT_EQ(1u, GetMainFrameConsoleMessages().size()); + EXPECT_EQ(GetMainFrameConsoleMessages().front(), kAbusiveWarnMessage); + + EXPECT_FALSE(popup_blocker()->ShouldApplyAbusivePopupBlocker()); +} + +TEST_F(SafeBrowsingTriggeredPopupBlockerTest, + WarningMatchWithAdBlockOnAbusiveSites_OnlyLogs) { + base::test::ScopedFeatureList scoped_feature_list; + scoped_feature_list.InitAndEnableFeature( + subresource_filter::kFilterAdsOnAbusiveSites); + + const GURL url("https://example.test/"); + MarkUrlAsAbusiveWarning(url); + NavigateAndCommit(url); + + // Warning should come at navigation commit time, not at popup time. + EXPECT_EQ(2u, GetMainFrameConsoleMessages().size()); + EXPECT_EQ(GetMainFrameConsoleMessages().front(), kAbusiveWarnMessage); + EXPECT_EQ(GetMainFrameConsoleMessages().back(), + subresource_filter::kActivationWarningConsoleMessage); + + EXPECT_FALSE(popup_blocker()->ShouldApplyAbusivePopupBlocker()); +} + +TEST_F(SafeBrowsingTriggeredPopupBlockerTest, EnforcementRedirectPosition) { + // Turn on the feature to perform safebrowsing on redirects. + base::test::ScopedFeatureList scoped_feature_list; + + const GURL enforce_url("https://enforce.test/"); + const GURL warn_url("https://warn.test/"); + MarkUrlAsAbusiveEnforce(enforce_url); + MarkUrlAsAbusiveWarning(warn_url); + + using subresource_filter::RedirectPosition; + struct { + std::vector<const char*> urls; + base::Optional<RedirectPosition> last_enforcement_position; + } kTestCases[] = { + {{"https://normal.test/"}, base::nullopt}, + {{"https://enforce.test/"}, RedirectPosition::kOnly}, + {{"https://warn.test/"}, base::nullopt}, + + {{"https://normal.test/", "https://warn.test/"}, base::nullopt}, + {{"https://normal.test/", "https://normal.test/", + "https://enforce.test/"}, + RedirectPosition::kLast}, + + {{"https://enforce.test", "https://normal.test/", "https://warn.test/"}, + RedirectPosition::kFirst}, + {{"https://warn.test/", "https://normal.test/"}, base::nullopt}, + + {{"https://normal.test/", "https://enforce.test/", + "https://normal.test/"}, + RedirectPosition::kMiddle}, + }; + + for (const auto& test_case : kTestCases) { + base::HistogramTester histograms; + const GURL& first_url = GURL(test_case.urls.front()); + auto navigation_simulator = + content::NavigationSimulator::CreateRendererInitiated(first_url, + main_rfh()); + for (size_t i = 1; i < test_case.urls.size(); ++i) { + navigation_simulator->Redirect(GURL(test_case.urls[i])); + } + navigation_simulator->Commit(); + + histograms.ExpectTotalCount( + "SubresourceFilter.PageLoad.Activation.RedirectPosition2.Enforcement", + test_case.last_enforcement_position.has_value() ? 1 : 0); + if (test_case.last_enforcement_position.has_value()) { + histograms.ExpectUniqueSample( + "SubresourceFilter.PageLoad.Activation.RedirectPosition2.Enforcement", + static_cast<int>(test_case.last_enforcement_position.value()), 1); + } + } +} +} // namespace blocked_content diff --git a/chromium/components/blocked_content/url_list_manager.cc b/chromium/components/blocked_content/url_list_manager.cc new file mode 100644 index 00000000000..745946bd402 --- /dev/null +++ b/chromium/components/blocked_content/url_list_manager.cc @@ -0,0 +1,27 @@ +// Copyright 2018 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 "components/blocked_content/url_list_manager.h" + +namespace blocked_content { + +UrlListManager::UrlListManager() = default; + +UrlListManager::~UrlListManager() = default; + +void UrlListManager::AddObserver(Observer* observer) { + observers_.AddObserver(observer); +} + +void UrlListManager::RemoveObserver(Observer* observer) { + observers_.RemoveObserver(observer); +} + +void UrlListManager::NotifyObservers(int32_t id, const GURL& url) { + for (auto& observer : observers_) { + observer.BlockedUrlAdded(id, url); + } +} + +} // namespace blocked_content diff --git a/chromium/components/blocked_content/url_list_manager.h b/chromium/components/blocked_content/url_list_manager.h new file mode 100644 index 00000000000..0f26cd6a963 --- /dev/null +++ b/chromium/components/blocked_content/url_list_manager.h @@ -0,0 +1,47 @@ +// Copyright 2018 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 COMPONENTS_BLOCKED_CONTENT_URL_LIST_MANAGER_H_ +#define COMPONENTS_BLOCKED_CONTENT_URL_LIST_MANAGER_H_ + +#include <stdint.h> + +#include "base/macros.h" +#include "base/observer_list.h" + +class GURL; + +namespace blocked_content { + +// This class manages lists of blocked URLs in order to drive UI surfaces. +// Currently it is used by the redirect / popup blocked UIs. +// +// TODO(csharrison): Currently this object is composed within the framebust / +// popup tab helpers. Eventually those objects could be replaced almost entirely +// by shared logic here. +class UrlListManager { + public: + class Observer { + public: + virtual ~Observer() {} + virtual void BlockedUrlAdded(int32_t id, const GURL& url) = 0; + }; + + UrlListManager(); + ~UrlListManager(); + + void AddObserver(Observer* observer); + void RemoveObserver(Observer* observer); + + void NotifyObservers(int32_t id, const GURL& url); + + private: + base::ObserverList<Observer>::Unchecked observers_; + + DISALLOW_COPY_AND_ASSIGN(UrlListManager); +}; + +} // namespace blocked_content + +#endif // COMPONENTS_BLOCKED_CONTENT_URL_LIST_MANAGER_H_ |