blob: 52d82134e15588c928788371632582488e66897c [file] [log] [blame]
// 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 <string>
#include "base/bind.h"
#include "base/callback.h"
#include "base/strings/stringprintf.h"
#include "base/test/metrics/histogram_tester.h"
#include "chrome/browser/metrics/subprocess_metrics_provider.h"
#include "chrome/browser/page_load_metrics/observers/ad_metrics/ads_page_load_metrics_observer.h"
#include "chrome/browser/page_load_metrics/observers/ad_metrics/frame_data.h"
#include "chrome/browser/page_load_metrics/page_load_metrics_test_waiter.h"
#include "chrome/browser/subresource_filter/subresource_filter_browser_test_harness.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/subresource_filter/content/browser/subresource_filter_observer_test_utils.h"
#include "components/subresource_filter/core/common/test_ruleset_utils.h"
#include "components/ukm/content/source_url_recorder.h"
#include "components/ukm/test_ukm_recorder.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/test_navigation_observer.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/frame/from_ad_state.h"
#include "url/gurl.h"
namespace subresource_filter {
namespace {
using content::RenderFrameHost;
using testing::CreateSuffixRule;
class AdTaggingBrowserTest : public SubresourceFilterBrowserTest {
public:
AdTaggingBrowserTest() : SubresourceFilterBrowserTest() {}
~AdTaggingBrowserTest() override {}
void SetUpOnMainThread() override {
SubresourceFilterBrowserTest::SetUpOnMainThread();
SetRulesetWithRules(
{CreateSuffixRule("ad_script.js"), CreateSuffixRule("ad=true")});
}
content::WebContents* GetWebContents() {
return browser()->tab_strip_model()->GetActiveWebContents();
}
// Used for giving identifiers to frames that can easily be searched for
// with content::FrameMatchingPredicate.
std::string GetUniqueFrameName() {
return base::StringPrintf("frame_%d", frame_count_++);
}
// Create a frame that navigates via the src attribute. It's created by ad
// script. Returns after navigation has completed.
content::RenderFrameHost* CreateSrcFrameFromAdScript(
const content::ToRenderFrameHost& adapter,
const GURL& url) {
return CreateFrameImpl(adapter, url, true /* ad_script */);
}
// Create a frame that navigates via the src attribute. Returns after
// navigation has completed.
content::RenderFrameHost* CreateSrcFrame(
const content::ToRenderFrameHost& adapter,
const GURL& url) {
return CreateFrameImpl(adapter, url, false /* ad_script */);
}
// Creates a frame and doc.writes the frame into it. Returns after
// navigation has completed.
content::RenderFrameHost* CreateDocWrittenFrame(
const content::ToRenderFrameHost& adapter) {
return CreateDocWrittenFrameImpl(adapter, false /* ad_script */);
}
// Creates a frame and doc.writes the frame into it. The script creating the
// frame is an ad script. Returns after navigation has completed.
content::RenderFrameHost* CreateDocWrittenFrameFromAdScript(
const content::ToRenderFrameHost& adapter) {
return CreateDocWrittenFrameImpl(adapter, true /* ad_script */);
}
// Given a RenderFrameHost, navigates the page to the given |url| and waits
// for the navigation to complete before returning.
void NavigateFrame(content::RenderFrameHost* render_frame_host,
const GURL& url);
GURL GetURL(const std::string& page) {
return embedded_test_server()->GetURL("/ad_tagging/" + page);
}
std::unique_ptr<page_load_metrics::PageLoadMetricsTestWaiter>
CreatePageLoadMetricsTestWaiter() {
return std::make_unique<page_load_metrics::PageLoadMetricsTestWaiter>(
GetWebContents());
}
private:
content::RenderFrameHost* CreateFrameImpl(
const content::ToRenderFrameHost& adapter,
const GURL& url,
bool ad_script);
content::RenderFrameHost* CreateDocWrittenFrameImpl(
const content::ToRenderFrameHost& adapter,
bool ad_script);
uint32_t frame_count_ = 0;
DISALLOW_COPY_AND_ASSIGN(AdTaggingBrowserTest);
};
content::RenderFrameHost* AdTaggingBrowserTest::CreateFrameImpl(
const content::ToRenderFrameHost& adapter,
const GURL& url,
bool ad_script) {
content::RenderFrameHost* rfh = adapter.render_frame_host();
std::string name = GetUniqueFrameName();
std::string script = base::StringPrintf(
"%s('%s','%s');", ad_script ? "createAdFrame" : "createFrame",
url.spec().c_str(), name.c_str());
content::WebContents* web_contents =
content::WebContents::FromRenderFrameHost(rfh);
content::TestNavigationObserver navigation_observer(web_contents, 1);
EXPECT_TRUE(content::ExecuteScript(rfh, script));
navigation_observer.Wait();
EXPECT_TRUE(navigation_observer.last_navigation_succeeded())
<< navigation_observer.last_net_error_code();
return content::FrameMatchingPredicate(
web_contents, base::BindRepeating(&content::FrameMatchesName, name));
}
content::RenderFrameHost* AdTaggingBrowserTest::CreateDocWrittenFrameImpl(
const content::ToRenderFrameHost& adapter,
bool ad_script) {
content::RenderFrameHost* rfh = adapter.render_frame_host();
std::string name = GetUniqueFrameName();
std::string script = base::StringPrintf(
"%s('%s', '%s');",
ad_script ? "createDocWrittenAdFrame" : "createDocWrittenFrame",
name.c_str(), GetURL("").spec().c_str());
content::WebContents* web_contents =
content::WebContents::FromRenderFrameHost(rfh);
content::TestNavigationObserver navigation_observer(web_contents, 1);
bool result = false;
EXPECT_TRUE(content::ExecuteScriptAndExtractBool(rfh, script, &result));
EXPECT_TRUE(result);
navigation_observer.Wait();
EXPECT_TRUE(navigation_observer.last_navigation_succeeded())
<< navigation_observer.last_net_error_code();
return content::FrameMatchingPredicate(
web_contents, base::BindRepeating(&content::FrameMatchesName, name));
}
// Given a RenderFrameHost, navigates the page to the given |url| and waits
// for the navigation to complete before returning.
void AdTaggingBrowserTest::NavigateFrame(
content::RenderFrameHost* render_frame_host,
const GURL& url) {
std::string script =
base::StringPrintf(R"(window.location='%s')", url.spec().c_str());
content::TestNavigationObserver navigation_observer(GetWebContents(), 1);
EXPECT_TRUE(content::ExecuteScript(render_frame_host, script));
navigation_observer.Wait();
EXPECT_TRUE(navigation_observer.last_navigation_succeeded())
<< navigation_observer.last_net_error_code();
}
IN_PROC_BROWSER_TEST_F(AdTaggingBrowserTest, FramesByURL) {
TestSubresourceFilterObserver observer(web_contents());
// Main frame.
ui_test_utils::NavigateToURL(browser(), GetURL("frame_factory.html"));
EXPECT_FALSE(observer.GetIsAdSubframe(
GetWebContents()->GetMainFrame()->GetFrameTreeNodeId()));
// (1) Vanilla child.
content::RenderFrameHost* vanilla_child =
CreateSrcFrame(GetWebContents(), GetURL("frame_factory.html?1"));
EXPECT_FALSE(*observer.GetIsAdSubframe(vanilla_child->GetFrameTreeNodeId()));
// (2) Ad child.
RenderFrameHost* ad_child =
CreateSrcFrame(GetWebContents(), GetURL("frame_factory.html?2&ad=true"));
EXPECT_TRUE(*observer.GetIsAdSubframe(ad_child->GetFrameTreeNodeId()));
// (3) Ad child of 2.
RenderFrameHost* ad_child_2 =
CreateSrcFrame(ad_child, GetURL("frame_factory.html?sub=1&3&ad=true"));
EXPECT_TRUE(*observer.GetIsAdSubframe(ad_child_2->GetFrameTreeNodeId()));
// (4) Vanilla child of 2.
RenderFrameHost* vanilla_child_2 =
CreateSrcFrame(ad_child, GetURL("frame_factory.html?4"));
EXPECT_TRUE(*observer.GetIsAdSubframe(vanilla_child_2->GetFrameTreeNodeId()));
// (5) Vanilla child of 1. This tests something subtle.
// frame_factory.html?ad=true loads the same script that frame_factory.html
// uses to load frames. This tests that even though the script is tagged as an
// ad in the ad iframe, it's not considered an ad in the main frame, hence
// it's able to create an iframe that's not labeled as an ad.
RenderFrameHost* vanilla_child_3 =
CreateSrcFrame(vanilla_child, GetURL("frame_factory.html?5"));
EXPECT_FALSE(
*observer.GetIsAdSubframe(vanilla_child_3->GetFrameTreeNodeId()));
}
const char kSubresourceFilterOriginStatusHistogram[] =
"PageLoad.Clients.Ads.FrameCounts.AdFrames.PerFrame."
"OriginStatus";
const char kWindowOpenFromAdStateHistogram[] = "Blink.WindowOpen.FromAdState";
IN_PROC_BROWSER_TEST_F(AdTaggingBrowserTest, VerifySameOriginWithoutNavigate) {
base::HistogramTester histogram_tester;
// Main frame.
ui_test_utils::NavigateToURL(browser(), GetURL("frame_factory.html"));
// Ad frame via doc write.
CreateDocWrittenFrameFromAdScript(GetWebContents());
// Navigate away and ensure we report same origin.
ui_test_utils::NavigateToURL(browser(), GetURL(url::kAboutBlankURL));
histogram_tester.ExpectUniqueSample(kSubresourceFilterOriginStatusHistogram,
FrameData::OriginStatus::kSame, 1);
}
IN_PROC_BROWSER_TEST_F(AdTaggingBrowserTest, VerifyCrossOriginWithoutNavigate) {
base::HistogramTester histogram_tester;
// Main frame.
ui_test_utils::NavigateToURL(browser(), GetURL("frame_factory.html"));
// Regular frame that's cross origin and has a doc write ad of its own.
RenderFrameHost* regular_child = CreateSrcFrame(
GetWebContents(), embedded_test_server()->GetURL(
"b.com", "/ad_tagging/frame_factory.html"));
CreateDocWrittenFrameFromAdScript(regular_child);
// Navigate away and ensure we report cross origin.
ui_test_utils::NavigateToURL(browser(), GetURL(url::kAboutBlankURL));
// TODO(johnidel): Check that frame was reported properly. See
// crbug.com/914893.
}
// Ad script creates a frame and navigates it cross origin.
IN_PROC_BROWSER_TEST_F(AdTaggingBrowserTest,
VerifyCrossOriginWithImmediateNavigate) {
base::HistogramTester histogram_tester;
auto waiter = CreatePageLoadMetricsTestWaiter();
// Create the main frame and cross origin subframe from an ad script.
ui_test_utils::NavigateToURL(browser(), GetURL("frame_factory.html"));
CreateSrcFrameFromAdScript(GetWebContents(),
embedded_test_server()->GetURL(
"b.com", "/ads_observer/same_origin_ad.html"));
// Wait for all of the subresources to finish loading (includes favicon).
// Waiting for the navigation to finish is not sufficient, as it blocks on the
// main resource load finishing, not the iframe resource. Page loads 4
// resources, a favicon, and 2 resources for the iframe.
waiter->AddMinimumCompleteResourcesExpectation(8);
waiter->Wait();
// Navigate away and ensure we report cross origin.
ui_test_utils::NavigateToURL(browser(), GetURL(url::kAboutBlankURL));
histogram_tester.ExpectUniqueSample(kSubresourceFilterOriginStatusHistogram,
FrameData::OriginStatus::kCross, 1);
}
// Ad script creates a frame and navigates it same origin.
// It is then renavigated cross origin.
IN_PROC_BROWSER_TEST_F(AdTaggingBrowserTest,
VerifySameOriginWithCrossOriginRenavigate) {
base::HistogramTester histogram_tester;
// Create the main frame and same origin subframe from an ad script.
// This triggers the subresource_filter ad detection.
ui_test_utils::NavigateToURL(browser(), GetURL("frame_factory.html"));
RenderFrameHost* ad_child = CreateSrcFrameFromAdScript(
GetWebContents(), GetURL("frame_factory.html"));
// Navigate the subframe to a cross origin site.
NavigateFrame(ad_child, embedded_test_server()->GetURL(
"b.com", "/ad_tagging/frame_factory.html"));
// Navigate away and ensure we report same origin.
ui_test_utils::NavigateToURL(browser(), GetURL(url::kAboutBlankURL));
histogram_tester.ExpectUniqueSample(kSubresourceFilterOriginStatusHistogram,
FrameData::OriginStatus::kSame, 1);
}
// Test that a subframe with a non-ad url but loaded by ad script is an ad.
IN_PROC_BROWSER_TEST_F(AdTaggingBrowserTest, FrameLoadedByAdScript) {
TestSubresourceFilterObserver observer(web_contents());
// Main frame.
ui_test_utils::NavigateToURL(browser(), GetURL("frame_factory.html"));
// Child frame created by ad script.
RenderFrameHost* ad_child = CreateSrcFrameFromAdScript(
GetWebContents(), GetURL("frame_factory.html?1"));
EXPECT_TRUE(*observer.GetIsAdSubframe(ad_child->GetFrameTreeNodeId()));
}
// Test that same-origin doc.write created iframes are tagged as ads.
IN_PROC_BROWSER_TEST_F(AdTaggingBrowserTest, SameOriginFrameTagging) {
TestSubresourceFilterObserver observer(web_contents());
// Main frame.
ui_test_utils::NavigateToURL(browser(), GetURL("frame_factory.html"));
// (1) Vanilla child.
content::RenderFrameHost* vanilla_frame =
CreateDocWrittenFrame(GetWebContents());
EXPECT_FALSE(observer.GetIsAdSubframe(vanilla_frame->GetFrameTreeNodeId()));
// (2) Ad child.
content::RenderFrameHost* ad_frame =
CreateDocWrittenFrameFromAdScript(GetWebContents());
EXPECT_TRUE(*observer.GetIsAdSubframe(ad_frame->GetFrameTreeNodeId()));
}
void ExpectWindowOpenUkmEntry(const ukm::TestUkmRecorder& ukm_recorder,
bool from_main_frame,
const GURL& main_frame_url,
bool from_ad_subframe,
bool from_ad_script) {
auto entries = ukm_recorder.GetEntriesByName(
ukm::builders::AbusiveExperienceHeuristic_WindowOpen::kEntryName);
EXPECT_EQ(1u, entries.size());
// Check that the event is keyed to |main_frame_url| only if it was from the
// top frame.
if (from_main_frame) {
ukm_recorder.ExpectEntrySourceHasUrl(entries.back(), main_frame_url);
} else {
EXPECT_FALSE(ukm_recorder.GetSourceForSourceId(entries.back()->source_id));
}
// Check that a DocumentCreated entry was created, and it's keyed to
// |main_frame_url| only if it was from the top frame. However, we can always
// use the navigation source ID to link this source to |main_frame_url|.
const ukm::mojom::UkmEntry* dc_entry =
ukm_recorder.GetDocumentCreatedEntryForSourceId(
entries.back()->source_id);
EXPECT_TRUE(dc_entry);
EXPECT_EQ(entries.back()->source_id, dc_entry->source_id);
if (from_main_frame) {
ukm_recorder.ExpectEntrySourceHasUrl(dc_entry, main_frame_url);
} else {
EXPECT_FALSE(ukm_recorder.GetSourceForSourceId(dc_entry->source_id));
}
const ukm::UkmSource* navigation_source =
ukm_recorder.GetSourceForSourceId(*ukm_recorder.GetEntryMetric(
dc_entry, ukm::builders::DocumentCreated::kNavigationSourceIdName));
EXPECT_EQ(main_frame_url, navigation_source->url());
EXPECT_EQ(from_main_frame,
*ukm_recorder.GetEntryMetric(
dc_entry, ukm::builders::DocumentCreated::kIsMainFrameName));
ukm_recorder.ExpectEntryMetric(
entries.back(),
ukm::builders::AbusiveExperienceHeuristic_WindowOpen::kFromAdSubframeName,
from_ad_subframe);
ukm_recorder.ExpectEntryMetric(
entries.back(),
ukm::builders::AbusiveExperienceHeuristic_WindowOpen::kFromAdScriptName,
from_ad_script);
}
void ExpectWindowOpenUmaEntry(const base::HistogramTester& histogram_tester,
bool from_ad_subframe,
bool from_ad_script) {
SubprocessMetricsProvider::MergeHistogramDeltasForTesting();
blink::FromAdState state =
blink::GetFromAdState(from_ad_subframe, from_ad_script);
histogram_tester.ExpectBucketCount(kWindowOpenFromAdStateHistogram, state,
1 /* expected_count */);
}
enum class NavigationInitiationType {
kWindowOpen,
kSetLocation,
kAnchorLinkActivate,
};
class AdClickNavigationBrowserTest
: public AdTaggingBrowserTest,
public ::testing::WithParamInterface<
std::tuple<NavigationInitiationType, bool /* gesture */>> {
void SetUpCommandLine(base::CommandLine* command_line) override {
// Popups without user gesture is blocked by default. Turn off the switch
// here to unblock that, so as to be able to test that the UseCounter not
// being recorded was due to the filtering in our recording function, rather
// than the default popup blocking.
command_line->AppendSwitch(::switches::kDisablePopupBlocking);
}
};
IN_PROC_BROWSER_TEST_P(AdClickNavigationBrowserTest, UseCounter) {
NavigationInitiationType type;
bool gesture;
std::tie(type, gesture) = GetParam();
auto web_feature_waiter =
std::make_unique<page_load_metrics::PageLoadMetricsTestWaiter>(
GetWebContents());
blink::mojom::WebFeature ad_click_navigation_feature =
blink::mojom::WebFeature::kAdClickNavigation;
web_feature_waiter->AddWebFeatureExpectation(ad_click_navigation_feature);
GURL url =
embedded_test_server()->GetURL("a.com", "/ad_tagging/frame_factory.html");
ui_test_utils::NavigateToURL(browser(), url);
content::WebContents* main_tab = GetWebContents();
RenderFrameHost* child = CreateSrcFrame(
main_tab, embedded_test_server()->GetURL(
"a.com", "/ad_tagging/frame_factory.html?1&ad=true"));
std::string script;
switch (type) {
case NavigationInitiationType::kWindowOpen:
script = "window.open('frame_factory.html')";
break;
case NavigationInitiationType::kSetLocation:
script = "location='frame_factory.html'";
break;
case NavigationInitiationType::kAnchorLinkActivate:
script =
"var a = document.createElement('a');"
"a.setAttribute('href', 'frame_factory.html');"
"a.click();";
break;
}
if (gesture) {
EXPECT_TRUE(ExecJs(child, script));
web_feature_waiter->Wait();
} else {
switch (type) {
case NavigationInitiationType::kSetLocation:
case NavigationInitiationType::kAnchorLinkActivate: {
content::TestNavigationObserver navigation_observer(web_contents());
EXPECT_TRUE(ExecuteScriptWithoutUserGesture(child, script));
// To report metrics.
navigation_observer.Wait();
break;
}
case NavigationInitiationType::kWindowOpen: {
EXPECT_TRUE(ExecuteScriptWithoutUserGesture(child, script));
// To report metrics.
ASSERT_EQ(2, browser()->tab_strip_model()->count());
browser()->tab_strip_model()->MoveSelectedTabsTo(0);
ui_test_utils::NavigateToURL(browser(), url);
break;
}
}
EXPECT_FALSE(
web_feature_waiter->DidObserveWebFeature(ad_click_navigation_feature));
}
}
INSTANTIATE_TEST_SUITE_P(
/* no prefix */,
AdClickNavigationBrowserTest,
::testing::Combine(
::testing::Values(NavigationInitiationType::kWindowOpen,
NavigationInitiationType::kSetLocation,
NavigationInitiationType::kAnchorLinkActivate),
::testing::Bool()));
class AdTaggingEventFromSubframeBrowserTest
: public AdTaggingBrowserTest,
public ::testing::WithParamInterface<
std::tuple<bool /* cross_origin */, bool /* from_ad_subframe */>> {};
IN_PROC_BROWSER_TEST_P(AdTaggingEventFromSubframeBrowserTest,
WindowOpenFromSubframe) {
bool cross_origin;
bool from_ad_subframe;
std::tie(cross_origin, from_ad_subframe) = GetParam();
SCOPED_TRACE(::testing::Message()
<< "cross_origin = " << cross_origin << ", "
<< "from_ad_subframe = " << from_ad_subframe);
ukm::TestAutoSetUkmRecorder ukm_recorder;
base::HistogramTester histogram_tester;
GURL main_frame_url =
embedded_test_server()->GetURL("a.com", "/ad_tagging/frame_factory.html");
ui_test_utils::NavigateToURL(browser(), main_frame_url);
content::WebContents* main_tab = GetWebContents();
std::string hostname = cross_origin ? "b.com" : "a.com";
std::string suffix = from_ad_subframe ? "&ad=true" : "";
RenderFrameHost* child = CreateSrcFrame(
main_tab, embedded_test_server()->GetURL(
hostname, "/ad_tagging/frame_factory.html?1" + suffix));
EXPECT_TRUE(content::ExecuteScript(child, "window.open();"));
bool from_ad_script = from_ad_subframe;
ExpectWindowOpenUkmEntry(ukm_recorder, false /* from_main_frame */,
main_frame_url, from_ad_subframe, from_ad_script);
ExpectWindowOpenUmaEntry(histogram_tester, from_ad_subframe, from_ad_script);
}
INSTANTIATE_TEST_SUITE_P(
/* no prefix */,
AdTaggingEventFromSubframeBrowserTest,
::testing::Combine(::testing::Bool(), ::testing::Bool()));
class AdTaggingEventWithScriptInStackBrowserTest
: public AdTaggingBrowserTest,
public ::testing::WithParamInterface<bool /* from_ad_script */> {};
IN_PROC_BROWSER_TEST_P(AdTaggingEventWithScriptInStackBrowserTest,
WindowOpenWithScriptInStack) {
bool from_ad_script = GetParam();
SCOPED_TRACE(::testing::Message() << "from_ad_script = " << from_ad_script);
ukm::TestAutoSetUkmRecorder ukm_recorder;
base::HistogramTester histogram_tester;
GURL main_frame_url = GetURL("frame_factory.html");
ui_test_utils::NavigateToURL(browser(), main_frame_url);
content::WebContents* main_tab = GetWebContents();
std::string script = from_ad_script ? "windowOpenFromAdScript();"
: "windowOpenFromNonAdScript();";
EXPECT_TRUE(content::ExecuteScript(main_tab, script));
bool from_ad_subframe = false;
ExpectWindowOpenUkmEntry(ukm_recorder, true /* from_main_frame */,
main_frame_url, from_ad_subframe, from_ad_script);
ExpectWindowOpenUmaEntry(histogram_tester, from_ad_subframe, from_ad_script);
}
INSTANTIATE_TEST_SUITE_P(
/* no prefix */,
AdTaggingEventWithScriptInStackBrowserTest,
::testing::Bool());
} // namespace
} // namespace subresource_filter