diff --git a/content/browser/interest_group/ad_auction_service_impl_unittest.cc b/content/browser/interest_group/ad_auction_service_impl_unittest.cc index e1fc8a4157b717..de26ce33478272 100644 --- a/content/browser/interest_group/ad_auction_service_impl_unittest.cc +++ b/content/browser/interest_group/ad_auction_service_impl_unittest.cc @@ -1133,9 +1133,11 @@ TEST_F(AdAuctionServiceImplTest, UpdateAllUpdatableFields) { "%s/interest_group/new_trusted_bidding_signals_url.json", "trustedBiddingSignalsKeys": ["new_key"], "ads": [{"renderUrl": "%s/new_ad_render_url", + "sizeGroup": "group_new", "metadata": {"new_a": "b"} }], "adComponents": [{"renderUrl": "https://example.com/component_url", + "sizeGroup": "group_new", "metadata": {"new_c": "d"} }], "adSizes": {"size_new": {"width": "300px", "height": "150px"}}, @@ -1162,8 +1164,15 @@ TEST_F(AdAuctionServiceImplTest, UpdateAllUpdatableFields) { interest_group.ads.emplace(); blink::InterestGroup::Ad ad( /*render_url=*/GURL("https://example.com/render"), + /*size_group=*/"group_old", /*metadata=*/"{\"ad\":\"metadata\",\"here\":[1,2,3]}"); interest_group.ads->emplace_back(std::move(ad)); + interest_group.ad_components.emplace(); + blink::InterestGroup::Ad ad_component( + /*render_url=*/GURL("https://example.com/render"), + /*size_group=*/"group_old", + /*metadata=*/"{\"ad\":\"metadata\",\"here\":[1,2,3]}"); + interest_group.ad_components->emplace_back(std::move(ad_component)); interest_group.ad_sizes.emplace(); interest_group.ad_sizes->emplace( "size_old", blink::AdSize(640, blink::AdSize::LengthUnit::kPixels, 480, @@ -1227,11 +1236,13 @@ TEST_F(AdAuctionServiceImplTest, UpdateAllUpdatableFields) { ASSERT_EQ(group.ads->size(), 1u); EXPECT_EQ(group.ads.value()[0].render_url.spec(), base::StringPrintf("%s/new_ad_render_url", kOriginStringA)); + EXPECT_EQ(group.ads.value()[0].size_group, "group_new"); EXPECT_EQ(group.ads.value()[0].metadata, "{\"new_a\":\"b\"}"); ASSERT_TRUE(group.ad_components.has_value()); ASSERT_EQ(group.ad_components->size(), 1u); EXPECT_EQ(group.ad_components.value()[0].render_url.spec(), "https://example.com/component_url"); + EXPECT_EQ(group.ad_components.value()[0].size_group, "group_new"); EXPECT_EQ(group.ad_components.value()[0].metadata, "{\"new_c\":\"d\"}"); ASSERT_TRUE(group.ad_sizes.has_value()); ASSERT_EQ(group.ad_sizes->size(), 1u); diff --git a/content/browser/interest_group/auction_runner_unittest.cc b/content/browser/interest_group/auction_runner_unittest.cc index f421d37aa718f9..0a92c106bf2967 100644 --- a/content/browser/interest_group/auction_runner_unittest.cc +++ b/content/browser/interest_group/auction_runner_unittest.cc @@ -7869,79 +7869,116 @@ TEST_F(AuctionRunnerTest, BadBid) { const struct TestCase { const char* expected_error_message; double bid; - GURL render_url; - absl::optional> ad_component_urls; + blink::AdDescriptor ad_descriptor; + absl::optional> ad_component_descriptors; base::TimeDelta duration; } kTestCases[] = { // Bids that aren't positive integers. { "Invalid bid value", -10, - GURL("https://ad1.com"), + blink::AdDescriptor(GURL("https://ad1.com")), absl::nullopt, base::TimeDelta(), }, { "Invalid bid value", 0, - GURL("https://ad1.com"), + blink::AdDescriptor(GURL("https://ad1.com")), absl::nullopt, base::TimeDelta(), }, { "Invalid bid value", std::numeric_limits::infinity(), - GURL("https://ad1.com"), + blink::AdDescriptor(GURL("https://ad1.com")), absl::nullopt, base::TimeDelta(), }, { "Invalid bid value", std::numeric_limits::quiet_NaN(), - GURL("https://ad1.com"), + blink::AdDescriptor(GURL("https://ad1.com")), absl::nullopt, base::TimeDelta(), }, // Invalid render URL. { - "Bid render URL must be a valid ad URL", + "Bid render ad must have a valid URL and size (if specified)", 1, - GURL(":"), + blink::AdDescriptor(GURL(":")), absl::nullopt, base::TimeDelta(), }, // Non-HTTPS render URLs. { - "Bid render URL must be a valid ad URL", + "Bid render ad must have a valid URL and size (if specified)", 1, - GURL("data:,foo"), + blink::AdDescriptor(GURL("data:,foo")), absl::nullopt, base::TimeDelta(), }, { - "Bid render URL must be a valid ad URL", + "Bid render ad must have a valid URL and size (if specified)", 1, - GURL("http://ad1.com"), + blink::AdDescriptor(GURL("http://ad1.com")), absl::nullopt, base::TimeDelta(), }, // HTTPS render URL that's not in the list of allowed renderUrls. { - "Bid render URL must be a valid ad URL", + "Bid render ad must have a valid URL and size (if specified)", 1, - GURL("https://ad2.com"), + blink::AdDescriptor(GURL("https://ad2.com")), + absl::nullopt, + base::TimeDelta(), + }, + + // HTTPS render URL with an invalid size value. + { + "Bid render ad must have a valid URL and size (if specified)", + 1, + blink::AdDescriptor( + GURL("https://ad1.com"), + blink::AdSize(0, blink::AdSize::LengthUnit::kPixels, 100, + blink::AdSize::LengthUnit::kPixels)), + absl::nullopt, + base::TimeDelta(), + }, + + // HTTPS render URL with an invalid size unit. + { + "Bid render ad must have a valid URL and size (if specified)", + 1, + blink::AdDescriptor( + GURL("https://ad1.com"), + blink::AdSize(100, blink::AdSize::LengthUnit::kInvalid, 100, + blink::AdSize::LengthUnit::kPixels)), + absl::nullopt, + base::TimeDelta(), + }, + + // HTTPS render URL with a size specification that does not match any + // allowed ad descriptors. + { + "Bid render ad must have a valid URL and size (if specified)", + 1, + blink::AdDescriptor( + GURL("https://ad1.com"), + blink::AdSize(100, blink::AdSize::LengthUnit::kPixels, 100, + blink::AdSize::LengthUnit::kPixels)), absl::nullopt, base::TimeDelta(), }, // Invalid component URL. { - "Bid ad components URL must match a valid ad component URL", + "Bid ad component must have a valid URL and size (if specified)", 1, - GURL("https://ad1.com"), + blink::AdDescriptor(GURL("https://ad1.com")), std::vector{blink::AdDescriptor(GURL(":"))}, base::TimeDelta(), }, @@ -7949,28 +7986,63 @@ TEST_F(AuctionRunnerTest, BadBid) { // HTTPS component URL that's not in the list of allowed ad component // URLs. { - "Bid ad components URL must match a valid ad component URL", + "Bid ad component must have a valid URL and size (if specified)", 1, - GURL("https://ad1.com"), + blink::AdDescriptor(GURL("https://ad1.com")), std::vector{ blink::AdDescriptor(GURL("https://ad2.com-component1.com"))}, base::TimeDelta(), }, { - "Bid ad components URL must match a valid ad component URL", + "Bid ad component must have a valid URL and size (if specified)", 1, - GURL("https://ad1.com"), + blink::AdDescriptor(GURL("https://ad1.com")), std::vector{ blink::AdDescriptor(GURL("https://ad1.com-component1.com")), blink::AdDescriptor(GURL("https://ad2.com-component1.com"))}, base::TimeDelta(), }, + // HTTPS component URL with an invalid size value. + { + "Bid ad component must have a valid URL and size (if specified)", + 1, + blink::AdDescriptor(GURL("https://ad1.com")), + std::vector{blink::AdDescriptor( + GURL("https://ad1.com-component1.com"), + blink::AdSize(0, blink::AdSize::LengthUnit::kPixels, 100, + blink::AdSize::LengthUnit::kPixels))}, + base::TimeDelta(), + }, + // HTTPS component URL with an invalid size unit. + { + "Bid ad component must have a valid URL and size (if specified)", + 1, + blink::AdDescriptor(GURL("https://ad1.com")), + std::vector{blink::AdDescriptor( + GURL("https://ad1.com-component1.com"), + blink::AdSize(100, blink::AdSize::LengthUnit::kInvalid, 100, + blink::AdSize::LengthUnit::kPixels))}, + base::TimeDelta(), + }, + // HTTPS component URL with a size specification that does not match any + // allowed ad descriptors. + { + "Bid ad component must have a valid URL and size (if specified)", + 1, + blink::AdDescriptor(GURL("https://ad1.com")), + std::vector{blink::AdDescriptor( + GURL("https://ad1.com-component1.com"), + blink::AdSize(100, blink::AdSize::LengthUnit::kPixels, 100, + blink::AdSize::LengthUnit::kPixels))}, + base::TimeDelta(), + }, + // Negative time. { "Invalid bid duration", 1, - GURL("https://ad2.com"), + blink::AdDescriptor(GURL("https://ad2.com")), absl::nullopt, base::Milliseconds(-1), }, @@ -7989,8 +8061,8 @@ TEST_F(AuctionRunnerTest, BadBid) { ASSERT_TRUE(bidder2_worklet); bidder1_worklet->InvokeGenerateBidCallback( - test_case.bid, blink::AdDescriptor(test_case.render_url), - /*mojo_kanon_bid=*/nullptr, test_case.ad_component_urls, + test_case.bid, test_case.ad_descriptor, + /*mojo_kanon_bid=*/nullptr, test_case.ad_component_descriptors, test_case.duration); // Bidder 2 doesn't bid. bidder2_worklet->InvokeGenerateBidCallback(/*bid=*/absl::nullopt); @@ -13820,7 +13892,7 @@ TEST_P(AuctionRunnerKAnonTest, MojoValidation) { const struct TestCase { std::set run_in_modes; const char* expected_error_message; - GURL render_url; + blink::AdDescriptor ad_descriptor; auction_worklet::mojom::BidderWorkletKAnonEnforcedBidPtr mojo_bid; bool expect_winner; } kTestCases[] = { @@ -13829,7 +13901,7 @@ TEST_P(AuctionRunnerKAnonTest, MojoValidation) { {{auction_worklet::mojom::KAnonymityBidMode::kEnforce, auction_worklet::mojom::KAnonymityBidMode::kSimulate}, "Received different k-anon bid when unenforced bid already k-anon", - GURL("https://ad1.com"), + blink::AdDescriptor(GURL("https://ad1.com")), auction_worklet::mojom::BidderWorkletKAnonEnforcedBid::NewBid( auction_worklet::mojom::BidderWorkletBid::New( "ad", 5.0, /*ad_cost=*/absl::nullopt, @@ -13839,8 +13911,8 @@ TEST_P(AuctionRunnerKAnonTest, MojoValidation) { // A non-k-anon bid as k-anon one. Enforced, so auction fails. { {auction_worklet::mojom::KAnonymityBidMode::kEnforce}, - "Bid render URL must be a valid ad URL", - GURL("https://ad2.com"), + "Bid render ad must have a valid URL and size (if specified)", + blink::AdDescriptor(GURL("https://ad2.com")), auction_worklet::mojom::BidderWorkletKAnonEnforcedBid::NewBid( auction_worklet::mojom::BidderWorkletBid::New( "ad", 5.0, /*ad_cost=*/absl::nullopt, @@ -13851,8 +13923,8 @@ TEST_P(AuctionRunnerKAnonTest, MojoValidation) { // A non-k-anon bid as k-anon one. Simulate, so auction succeeds. { {auction_worklet::mojom::KAnonymityBidMode::kSimulate}, - "Bid render URL must be a valid ad URL", - GURL("https://ad2.com"), + "Bid render ad must have a valid URL and size (if specified)", + blink::AdDescriptor(GURL("https://ad2.com")), auction_worklet::mojom::BidderWorkletKAnonEnforcedBid::NewBid( auction_worklet::mojom::BidderWorkletBid::New( "ad", 5.0, /*ad_cost=*/absl::nullopt, @@ -13864,7 +13936,7 @@ TEST_P(AuctionRunnerKAnonTest, MojoValidation) { { {auction_worklet::mojom::KAnonymityBidMode::kNone}, "Received k-anon bid data when not considering k-anon", - GURL("https://ad1.com"), + blink::AdDescriptor(GURL("https://ad1.com")), auction_worklet::mojom::BidderWorkletKAnonEnforcedBid:: NewSameAsNonEnforced(nullptr), /*expect_winner=*/true, @@ -13894,9 +13966,8 @@ TEST_P(AuctionRunnerKAnonTest, MojoValidation) { ASSERT_TRUE(seller_worklet); auto bidder1_worklet = mock_auction_process_manager_->TakeBidderWorklet(kBidder1Url); - bidder1_worklet->InvokeGenerateBidCallback( - 1.0, blink::AdDescriptor(test_case.render_url), - test_case.mojo_bid.Clone()); + bidder1_worklet->InvokeGenerateBidCallback(1.0, test_case.ad_descriptor, + test_case.mojo_bid.Clone()); // All of these tests only get one scoreAd, since k-anon bid is invalid. auto score_ad_params = seller_worklet->WaitForScoreAd(); diff --git a/content/browser/interest_group/interest_group_auction.cc b/content/browser/interest_group/interest_group_auction.cc index a0392a22ec59b8..5ef9e4e420beff 100644 --- a/content/browser/interest_group/interest_group_auction.cc +++ b/content/browser/interest_group/interest_group_auction.cc @@ -56,6 +56,7 @@ #include "services/network/public/mojom/url_loader_factory.mojom-forward.h" #include "third_party/abseil-cpp/absl/types/optional.h" #include "third_party/blink/public/common/interest_group/ad_auction_constants.h" +#include "third_party/blink/public/common/interest_group/ad_display_size_utils.h" #include "third_party/blink/public/common/interest_group/auction_config.h" #include "third_party/blink/public/common/interest_group/interest_group.h" #include "third_party/blink/public/mojom/interest_group/interest_group_types.mojom.h" @@ -113,32 +114,63 @@ base::flat_map KAnonKeysToMojom( return std::move(result); } -// Finds InterestGroup::Ad in `ads` that matches `render_url`, if any. Returns -// nullptr if `render_url` is invalid. +// Finds InterestGroup::Ad in `ads` that matches `ad_descriptor`, if any. +// Returns nullptr if `ad_descriptor` is invalid. const blink::InterestGroup::Ad* FindMatchingAd( const std::vector& ads, const base::flat_map& kanon_keys, const blink::InterestGroup& interest_group, InterestGroupAuction::Bid::BidRole bid_role, bool is_component_ad, - const GURL& render_url) { + const blink::AdDescriptor& ad_descriptor) { // TODO(mmenke): Validate render URLs on load and make this a DCHECK just // before the return instead, since then `ads` will necessarily only contain // valid URLs at that point. - if (!IsUrlValid(render_url)) + if (!IsUrlValid(ad_descriptor.url)) { return nullptr; + } + + if (ad_descriptor.size && !IsValidAdSize(ad_descriptor.size.value())) { + return nullptr; + } if (bid_role != InterestGroupAuction::Bid::BidRole::kUnenforcedKAnon) { const std::string kanon_key = - is_component_ad ? blink::KAnonKeyForAdComponentBid(render_url) - : blink::KAnonKeyForAdBid(interest_group, render_url); + is_component_ad + ? blink::KAnonKeyForAdComponentBid(ad_descriptor) + : blink::KAnonKeyForAdBid(interest_group, ad_descriptor); if (!IsKAnon(kanon_keys, kanon_key)) { return nullptr; } } for (const auto& ad : ads) { - if (ad.render_url == render_url) { + if (ad.render_url != ad_descriptor.url) { + continue; + } + if (!ad.size_group && !ad_descriptor.size) { + // Neither `blink::InterestGroup::Ad` nor the ad from the bid have any + // size specifications. They are considered as matching ad as long as + // they have the same url. + return &ad; + } + if (!ad.size_group || !ad_descriptor.size) { + // Since only one of the ads has a size specification, they are considered + // not matching. + continue; + } + // Both `blink::InterestGroup::Ad` and the ad from the bid have size + // specifications. They are considered as matching ad only if their + // size also matches. + auto has_matching_ad_size = [&interest_group, + &ad_descriptor](const std::string& ad_size) { + return interest_group.ad_sizes->at(ad_size) == *ad_descriptor.size; + }; + if (base::ranges::any_of(interest_group.size_groups->at(ad.size_group), + has_matching_ad_size)) { + // Each size group may also correspond to multiple ad sizes. If any of + // those ad sizes matches with the ad size from `ad_descriptor`, they are + // considered as matching ads. return &ad; } } @@ -1325,10 +1357,10 @@ class InterestGroupAuction::BuyerHelper bid_state.bidder->interest_group; const blink::InterestGroup::Ad* matching_ad = FindMatchingAd( *interest_group.ads, bid_state.kanon_keys, interest_group, bid_role, - /*is_component_ad=*/false, mojo_bid->ad_descriptor.url); + /*is_component_ad=*/false, mojo_bid->ad_descriptor); if (!matching_ad) { generate_bid_client_receiver_set_.ReportBadMessage( - "Bid render URL must be a valid ad URL"); + "Bid render ad must have a valid URL and size (if specified)"); return nullptr; } @@ -1356,9 +1388,9 @@ class InterestGroupAuction::BuyerHelper *mojo_bid->ad_component_descriptors) { if (!FindMatchingAd(*interest_group.ad_components, bid_state.kanon_keys, interest_group, bid_role, /*is_component_ad=*/true, - ad_component_descriptor.url)) { + ad_component_descriptor)) { generate_bid_client_receiver_set_.ReportBadMessage( - "Bid ad components URL must match a valid ad component URL"); + "Bid ad component must have a valid URL and size (if specified)"); return nullptr; } } diff --git a/content/browser/interest_group/interest_group_browsertest.cc b/content/browser/interest_group/interest_group_browsertest.cc index b0aa79cf45a817..f1cb910b4cb53a 100644 --- a/content/browser/interest_group/interest_group_browsertest.cc +++ b/content/browser/interest_group/interest_group_browsertest.cc @@ -128,6 +128,9 @@ base::Value::List MakeAdsValue( for (const auto& ad : ads) { base::Value::Dict entry; entry.Set("renderUrl", ad.render_url.spec()); + if (ad.size_group) { + entry.Set("sizeGroup", std::move(ad.size_group.value())); + } if (ad.metadata) entry.Set("metadata", JsonToValue(*ad.metadata)); list.Append(std::move(entry)); @@ -2766,35 +2769,42 @@ IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, EXPECT_EQ( kSuccess, - JoinInterestGroupAndVerify(blink::InterestGroup( - /*expiry=*/base::Time(), - /*owner=*/origin, - /*name=*/"cars", - /*priority=*/0.0, /*enable_bidding_signals_prioritization=*/false, - /*priority_vector=*/absl::nullopt, - /*priority_signals_overrides=*/absl::nullopt, - /*seller_capabilities=*/{}, - /*all_sellers_capabilities=*/ - blink::SellerCapabilities::kLatencyStats, /*execution_mode=*/ - blink::InterestGroup::ExecutionMode::kCompatibilityMode, - /*bidding_url=*/absl::nullopt, - /*bidding_wasm_helper_url=*/absl::nullopt, - /*update_url=*/absl::nullopt, - /*trusted_bidding_signals_url=*/absl::nullopt, - /*trusted_bidding_signals_keys=*/absl::nullopt, - /*user_bidding_signals=*/absl::nullopt, - /*ads=*/absl::nullopt, - /*ad_components=*/absl::nullopt, - /*ad_sizes=*/ - {{{"size_1", blink::AdSize(150, blink::AdSize::LengthUnit::kPixels, - 75, blink::AdSize::LengthUnit::kPixels)}}}, - /*size_groups=*/{{{"group_1", {"size_1"}}}}))); + JoinInterestGroupAndVerify( + blink::TestInterestGroupBuilder(origin, "cars") + .SetExpiry(base::Time()) + .SetAllSellerCapabilities( + blink::SellerCapabilities::kLatencyStats) + .SetExecutionMode( + blink::InterestGroup::ExecutionMode::kCompatibilityMode) + .SetAds({{{GURL("https://example.com/render"), + /*size_group=*/"group_1", + /*metadata=*/absl::nullopt}}}) + .SetAdComponents({{{GURL("https://example.com/component"), + /*size_group=*/"group_1", + /*metadata=*/absl::nullopt}}}) + .SetAdSizes( + {{{"size_1", + blink::AdSize(150, blink::AdSize::LengthUnit::kPixels, 75, + blink::AdSize::LengthUnit::kPixels)}}}) + .SetSizeGroups({{{"group_1", {"size_1"}}}}) + .Build())); WaitForAccessObserved({}); std::vector groups = GetInterestGroupsForOwner(origin); ASSERT_EQ(groups.size(), 1u); const blink::InterestGroup& group = groups[0].interest_group; + ASSERT_TRUE(group.ads.has_value()); + ASSERT_EQ(group.ads->size(), 1u); + EXPECT_EQ(group.ads.value()[0].render_url, + GURL("https://example.com/render")); + ASSERT_TRUE(group.ads.value()[0].size_group.has_value()); + EXPECT_EQ(group.ads.value()[0].size_group, "group_1"); + ASSERT_EQ(group.ad_components->size(), 1u); + EXPECT_EQ(group.ad_components.value()[0].render_url, + GURL("https://example.com/component")); + ASSERT_TRUE(group.ad_components.value()[0].size_group.has_value()); + EXPECT_EQ(group.ad_components.value()[0].size_group, "group_1"); EXPECT_EQ(group.ad_sizes->size(), 1u); ASSERT_EQ(group.ad_sizes->at("size_1"), blink::AdSize(150, blink::AdSize::LengthUnit::kPixels, 75, @@ -3117,6 +3127,267 @@ IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, WaitForAccessObserved({}); } +IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, + JoinInterestGroupInvalidAdSizeGroupEmptyName) { + GURL url = https_server_->GetURL("a.test", "/echo"); + std::string origin_string = url::Origin::Create(url).Serialize(); + ASSERT_TRUE(NavigateToURL(shell(), url)); + + EXPECT_EQ( + base::StringPrintf( + "TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': " + "ads[0].sizeGroup '' for AuctionAdInterestGroup with owner '%s' and " + "name 'cars' Size group name cannot be empty.", + origin_string.c_str()), + EvalJs(shell(), JsReplace(R"( +(async function() { + try { + await navigator.joinAdInterestGroup( + { + name: 'cars', + owner: $1, + ads: [{renderUrl: "https://test.com", sizeGroup: ""}], + }, + /*joinDurationSec=*/1); + } catch (e) { + return e.toString(); + } + return 'done'; +})())", + origin_string.c_str()))); + WaitForAccessObserved({}); +} + +IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, + JoinInterestGroupInvalidAdSizeGroupNoSizeGroups) { + GURL url = https_server_->GetURL("a.test", "/echo"); + std::string origin_string = url::Origin::Create(url).Serialize(); + ASSERT_TRUE(NavigateToURL(shell(), url)); + + EXPECT_EQ( + base::StringPrintf( + "TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': " + "ads[0].sizeGroup 'nonexistent' for AuctionAdInterestGroup with " + "owner '%s' and name 'cars' The assigned size group does not exist " + "in sizeGroups map.", + origin_string.c_str()), + EvalJs(shell(), JsReplace(R"( +(async function() { + try { + await navigator.joinAdInterestGroup( + { + name: 'cars', + owner: $1, + ads: [{renderUrl: "https://test.com", sizeGroup: "nonexistent"}], + }, + /*joinDurationSec=*/1); + } catch (e) { + return e.toString(); + } + return 'done'; +})())", + origin_string.c_str()))); + WaitForAccessObserved({}); +} + +IN_PROC_BROWSER_TEST_F( + InterestGroupBrowserTest, + JoinInterestGroupInvalidAdSizeGroupNotContainedInSizeGroups) { + GURL url = https_server_->GetURL("a.test", "/echo"); + std::string origin_string = url::Origin::Create(url).Serialize(); + ASSERT_TRUE(NavigateToURL(shell(), url)); + + EXPECT_EQ( + base::StringPrintf( + "TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': " + "ads[0].sizeGroup 'nonexistent' for AuctionAdInterestGroup with " + "owner '%s' and name 'cars' The assigned size group does not exist " + "in sizeGroups map.", + origin_string.c_str()), + EvalJs(shell(), JsReplace(R"( +(async function() { + try { + await navigator.joinAdInterestGroup( + { + name: 'cars', + owner: $1, + ads: [{renderUrl: "https://test.com", sizeGroup: "nonexistent"}], + adSizes: {"size_1": {"width": "50px", "height": "50px"}}, + sizeGroups: {"group_1": ["size_1"]}, + }, + /*joinDurationSec=*/1); + } catch (e) { + return e.toString(); + } + return 'done'; +})())", + origin_string.c_str()))); + WaitForAccessObserved({}); +} + +IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, + JoinInterestGroupInvalidAdSizeGroupNoAdSize) { + GURL url = https_server_->GetURL("a.test", "/echo"); + std::string origin_string = url::Origin::Create(url).Serialize(); + ASSERT_TRUE(NavigateToURL(shell(), url)); + + EXPECT_EQ( + base::StringPrintf( + "TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': " + "sizeGroups '' for AuctionAdInterestGroup with owner '%s' and name " + "'cars' An adSizes map must exist for sizeGroups to work.", + origin_string.c_str()), + EvalJs(shell(), JsReplace(R"( +(async function() { + try { + await navigator.joinAdInterestGroup( + { + name: 'cars', + owner: $1, + ads: [{renderUrl: "https://test.com", sizeGroup: "group_1"}], + sizeGroups: {"group_1": ["nonexistent"]}, + }, + /*joinDurationSec=*/1); + } catch (e) { + return e.toString(); + } + return 'done'; +})())", + origin_string.c_str()))); + WaitForAccessObserved({}); +} + +IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, + JoinInterestGroupInvalidAdComponentSizeGroupEmptyName) { + GURL url = https_server_->GetURL("a.test", "/echo"); + std::string origin_string = url::Origin::Create(url).Serialize(); + ASSERT_TRUE(NavigateToURL(shell(), url)); + + EXPECT_EQ( + base::StringPrintf( + "TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': " + "adComponents[0].sizeGroup '' for AuctionAdInterestGroup with owner " + "'%s' and name 'cars' Size group name cannot be empty.", + origin_string.c_str()), + EvalJs(shell(), JsReplace(R"( +(async function() { + try { + await navigator.joinAdInterestGroup( + { + name: 'cars', + owner: $1, + adComponents: [{renderUrl: "https://test.com", sizeGroup: ""}], + }, + /*joinDurationSec=*/1); + } catch (e) { + return e.toString(); + } + return 'done'; +})())", + origin_string.c_str()))); + WaitForAccessObserved({}); +} + +IN_PROC_BROWSER_TEST_F( + InterestGroupBrowserTest, + JoinInterestGroupInvalidAdComponentSizeGroupNoSizeGroups) { + GURL url = https_server_->GetURL("a.test", "/echo"); + std::string origin_string = url::Origin::Create(url).Serialize(); + ASSERT_TRUE(NavigateToURL(shell(), url)); + + EXPECT_EQ( + base::StringPrintf( + "TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': " + "adComponents[0].sizeGroup 'nonexistent' for AuctionAdInterestGroup " + "with owner '%s' and name 'cars' The assigned size group does not " + "exist in sizeGroups map.", + origin_string.c_str()), + EvalJs(shell(), JsReplace(R"( +(async function() { + try { + await navigator.joinAdInterestGroup( + { + name: 'cars', + owner: $1, + adComponents: [{renderUrl: "https://test.com", sizeGroup: "nonexistent"}], + }, + /*joinDurationSec=*/1); + } catch (e) { + return e.toString(); + } + return 'done'; +})())", + origin_string.c_str()))); + WaitForAccessObserved({}); +} + +IN_PROC_BROWSER_TEST_F( + InterestGroupBrowserTest, + JoinInterestGroupInvalidAdComponentSizeGroupNotContainedInSizeGroups) { + GURL url = https_server_->GetURL("a.test", "/echo"); + std::string origin_string = url::Origin::Create(url).Serialize(); + ASSERT_TRUE(NavigateToURL(shell(), url)); + + EXPECT_EQ( + base::StringPrintf( + "TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': " + "adComponents[0].sizeGroup 'nonexistent' for AuctionAdInterestGroup " + "with owner '%s' and name 'cars' The assigned size group does not " + "exist in sizeGroups map.", + origin_string.c_str()), + EvalJs(shell(), JsReplace(R"( +(async function() { + try { + await navigator.joinAdInterestGroup( + { + name: 'cars', + owner: $1, + adComponents: [{renderUrl: "https://test.com", sizeGroup: "nonexistent"}], + adSizes: {"size_1": {"width": "50px", "height": "50px"}}, + sizeGroups: {"group_1": ["size_1"]}, + }, + /*joinDurationSec=*/1); + } catch (e) { + return e.toString(); + } + return 'done'; +})())", + origin_string.c_str()))); + WaitForAccessObserved({}); +} + +IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, + JoinInterestGroupInvalidAdComponentSizeGroupNoAdSizes) { + GURL url = https_server_->GetURL("a.test", "/echo"); + std::string origin_string = url::Origin::Create(url).Serialize(); + ASSERT_TRUE(NavigateToURL(shell(), url)); + + EXPECT_EQ( + base::StringPrintf( + "TypeError: Failed to execute 'joinAdInterestGroup' on 'Navigator': " + "sizeGroups '' for AuctionAdInterestGroup with owner '%s' and name " + "'cars' An adSizes map must exist for sizeGroups to work.", + origin_string.c_str()), + EvalJs(shell(), JsReplace(R"( +(async function() { + try { + await navigator.joinAdInterestGroup( + { + name: 'cars', + owner: $1, + adComponents: [{renderUrl: "https://test.com", sizeGroup: "group_1"}], + sizeGroups: {"group_1": ["nonexistent"]}, + }, + /*joinDurationSec=*/1); + } catch (e) { + return e.toString(); + } + return 'done'; +})())", + origin_string.c_str()))); + WaitForAccessObserved({}); +} + IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, JoinInterestGroupInvalidAdSize) { GURL url = https_server_->GetURL("a.test", "/echo"); @@ -5438,6 +5709,51 @@ IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, RunAdAuctionWithWinner) { ->trusted_params->isolation_info.network_isolation_key()); } +// Runs auction just like test InterestGroupBrowserTest.RunAdAuctionWithWinner, +// but runs with the ads specified with sizes info. +IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, + RunAdAuctionWithSizeWithWinner) { + GURL test_url = https_server_->GetURL("a.test", "/page_with_iframe.html"); + ASSERT_TRUE(NavigateToURL(shell(), test_url)); + url::Origin test_origin = url::Origin::Create(test_url); + GURL ad_url = https_server_->GetURL("c.test", "/echo?render_cars"); + + EXPECT_EQ( + kSuccess, + JoinInterestGroupAndVerify( + blink::TestInterestGroupBuilder( + /*owner=*/test_origin, + /*name=*/"cars") + .SetBiddingUrl(https_server_->GetURL( + "a.test", "/interest_group/bidding_logic_with_size.js")) + .SetTrustedBiddingSignalsUrl(https_server_->GetURL( + "a.test", "/interest_group/trusted_bidding_signals.json")) + .SetTrustedBiddingSignalsKeys({{"key1"}}) + .SetAds(/*ads=*/{ + {{ad_url, "group_1", R"({"ad":"metadata","here":[1,2]})"}}}) + .SetAdSizes( + {{{"size_1", + blink::AdSize(100, blink::AdSize::LengthUnit::kScreenWidth, + 50, blink::AdSize::LengthUnit::kPixels)}}}) + .SetSizeGroups({{{"group_1", {"size_1"}}}}) + .Build())); + + std::string auction_config = JsReplace( + R"({ + seller: $1, + decisionLogicUrl: $2, + interestGroupBuyers: [$1], + auctionSignals: {x: 1}, + sellerSignals: {yet: 'more', info: 1}, + sellerTimeout: 200, + perBuyerSignals: {$1: {even: 'more', x: 4.5}}, + perBuyerTimeouts: {$1: 100, '*': 150} + })", + test_origin, + https_server_->GetURL("a.test", "/interest_group/decision_logic.js")); + RunAuctionAndWaitForURLAndNavigateIframe(auction_config, ad_url); +} + IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, RunAdAuctionWithWinnerReplacedURN) { GURL test_url = https_server_->GetURL("a.test", "/page_with_iframe.html"); diff --git a/content/browser/interest_group/interest_group_storage.cc b/content/browser/interest_group/interest_group_storage.cc index 862006b9afa5e7..5e9b17f1d8ff8b 100644 --- a/content/browser/interest_group/interest_group_storage.cc +++ b/content/browser/interest_group/interest_group_storage.cc @@ -131,6 +131,9 @@ base::Value ToValue(const blink::InterestGroup::Ad& ad) { base::Value value(base::Value::Type::DICT); base::Value::Dict& dict = value.GetDict(); dict.Set("url", ad.render_url.spec()); + if (ad.size_group) { + dict.Set("size_group", ad.size_group.value()); + } if (ad.metadata) dict.Set("metadata", ad.metadata.value()); return value; @@ -141,6 +144,10 @@ blink::InterestGroup::Ad FromInterestGroupAdValue( const std::string* maybe_url = dict.FindString("url"); if (maybe_url) result.render_url = GURL(*maybe_url); + const std::string* maybe_size_group = dict.FindString("size_group"); + if (maybe_size_group) { + result.size_group = *maybe_size_group; + } const std::string* maybe_metadata = dict.FindString("metadata"); if (maybe_metadata) result.metadata = *maybe_metadata; diff --git a/content/browser/interest_group/interest_group_storage_unittest.cc b/content/browser/interest_group/interest_group_storage_unittest.cc index d2515d46c53981..e16f32bbf0f790 100644 --- a/content/browser/interest_group/interest_group_storage_unittest.cc +++ b/content/browser/interest_group/interest_group_storage_unittest.cc @@ -115,23 +115,29 @@ class InterestGroupStorageTest : public testing::Test { /*ads=*/ std::vector{ blink::InterestGroup::Ad(GURL("https://full.example.com/ad1"), - "metadata1"), + "group_1", "metadata1"), blink::InterestGroup::Ad(GURL("https://full.example.com/ad2"), - "metadata2")}, + "group_2", "metadata2")}, /*ad_components=*/ std::vector{ blink::InterestGroup::Ad( - GURL("https://full.example.com/adcomponent1"), "metadata1c"), + GURL("https://full.example.com/adcomponent1"), "group_1", + "metadata1c"), blink::InterestGroup::Ad( - GURL("https://full.example.com/adcomponent2"), "metadata2c")}, + GURL("https://full.example.com/adcomponent2"), "group_2", + "metadata2c")}, /*ad_sizes=*/ {{{"size_1", blink::AdSize(300, blink::AdSize::LengthUnit::kPixels, 150, blink::AdSize::LengthUnit::kPixels)}, {"size_2", blink::AdSize(640, blink::AdSize::LengthUnit::kPixels, 480, - blink::AdSize::LengthUnit::kPixels)}}}, + blink::AdSize::LengthUnit::kPixels)}, + {"size_3", + blink::AdSize(100, blink::AdSize::LengthUnit::kScreenWidth, 100, + blink::AdSize::LengthUnit::kScreenWidth)}}}, /*size_groups=*/ {{{"group_1", std::vector{"size_1"}}, - {"group_2", std::vector{"size_1", "size_2"}}}}); + {"group_2", std::vector{"size_1", "size_2"}}, + {"group_3", std::vector{"size_3"}}}}); std::unique_ptr storage = CreateStorage(); storage->JoinInterestGroup(partial, partial_origin.GetURL()); @@ -164,10 +170,11 @@ class InterestGroupStorageTest : public testing::Test { update.trusted_bidding_signals_keys = std::vector{"a", "b2", "c", "d"}; update.ads = full.ads; - update.ads->emplace_back(GURL("https://full.example.com/ad3"), "metadata3"); + update.ads->emplace_back(GURL("https://full.example.com/ad3"), "group_3", + "metadata3"); update.ad_components = full.ad_components; update.ad_components->emplace_back( - GURL("https://full.example.com/adcomponent3"), "metadata3c"); + GURL("https://full.example.com/adcomponent3"), "group_3", "metadata3c"); storage->UpdateInterestGroup(blink::InterestGroupKey(full.owner, full.name), update); diff --git a/content/browser/interest_group/interest_group_update_manager.cc b/content/browser/interest_group/interest_group_update_manager.cc index 6f0ca5057e5c55..c60437e10a4fdb 100644 --- a/content/browser/interest_group/interest_group_update_manager.cc +++ b/content/browser/interest_group/interest_group_update_manager.cc @@ -270,6 +270,10 @@ constexpr net::NetworkTrafficAnnotationTag kTrafficAnnotation = return absl::nullopt; blink::InterestGroup::Ad ad; ad.render_url = GURL(*maybe_render_url); + const std::string* maybe_size_group = ads_dict->FindString("sizeGroup"); + if (maybe_size_group) { + ad.size_group = *maybe_size_group; + } const base::Value* maybe_metadata = ads_dict->Find("metadata"); if (maybe_metadata) { std::string metadata; diff --git a/content/test/content_unittests_bundle_data.filelist b/content/test/content_unittests_bundle_data.filelist index d6cbbf8344f2a0..2822cc1a67ca73 100644 --- a/content/test/content_unittests_bundle_data.filelist +++ b/content/test/content_unittests_bundle_data.filelist @@ -998,6 +998,8 @@ data/interest_group/bidding_logic_use_wasm.js data/interest_group/bidding_logic_use_wasm.js.mock-http-headers data/interest_group/bidding_logic_with_debugging_report.js data/interest_group/bidding_logic_with_debugging_report.js.mock-http-headers +data/interest_group/bidding_logic_with_size.js +data/interest_group/bidding_logic_with_size.js.mock-http-headers data/interest_group/bidding_no_direct_from_seller_signals_validator.js data/interest_group/bidding_no_direct_from_seller_signals_validator.js.mock-http-headers data/interest_group/component_auction_bidding_argument_validator.js diff --git a/content/test/data/interest_group/bidding_logic_with_size.js b/content/test/data/interest_group/bidding_logic_with_size.js new file mode 100644 index 00000000000000..98bb7b35845544 --- /dev/null +++ b/content/test/data/interest_group/bidding_logic_with_size.js @@ -0,0 +1,39 @@ +// Copyright 2023 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Basic generate bid script that offers a bid of 1 using the first ad's +// `renderUrl` and, if present, the first adComponent's `renderUrl`. +function generateBid(interestGroup, auctionSignals, perBuyerSignals, + trustedBiddingSignals, browserSignals) { + const ad = interestGroup.ads[0]; + + // `auctionSignals` controls whether or not component auctions are allowed. + let allowComponentAuction = + typeof auctionSignals === 'string' && + auctionSignals.includes('bidderAllowsComponentAuction'); + + let result = { + 'ad': ad, + 'bid': 1, + 'render': { url: ad.renderUrl, width: "100sw", height: "50px" }, + 'allowComponentAuction': allowComponentAuction + }; + if (interestGroup.adComponents && interestGroup.adComponents[0]) + result.adComponents = [interestGroup.adComponents[0].renderUrl]; + return result; +} + +function reportWin(auctionSignals, perBuyerSignals, sellerSignals, + browserSignals) { + sendReportTo(browserSignals.interestGroupOwner + '/echoall?report_bidder'); + registerAdBeacon({ + 'auctionwinner': + browserSignals.interestGroupOwner.replace('a.test', 'd.test') + + '/echoall?report_win_beacon' + }); + if (typeof privateAggregation !== 'undefined') { + privateAggregation.reportContributionForEvent( + 'auctionwinner', { bucket: 3n, value: 5 }); + } +} diff --git a/content/test/data/interest_group/bidding_logic_with_size.js.mock-http-headers b/content/test/data/interest_group/bidding_logic_with_size.js.mock-http-headers new file mode 100644 index 00000000000000..051fe4066b7f0a --- /dev/null +++ b/content/test/data/interest_group/bidding_logic_with_size.js.mock-http-headers @@ -0,0 +1,3 @@ +HTTP/1.1 200 OK +Content-Type: Application/Javascript +X-Allow-FLEDGE: true diff --git a/third_party/blink/common/interest_group/interest_group.cc b/third_party/blink/common/interest_group/interest_group.cc index 9efd7f063ba979..a45fae9f8e6e12 100644 --- a/third_party/blink/common/interest_group/interest_group.cc +++ b/third_party/blink/common/interest_group/interest_group.cc @@ -69,20 +69,31 @@ std::string ConvertAdSizeToString(const blink::AdSize& ad_size) { InterestGroup::Ad::Ad() = default; InterestGroup::Ad::Ad(GURL render_url, absl::optional metadata) - : render_url(std::move(render_url)), metadata(std::move(metadata)) {} + : Ad(std::move(render_url), absl::nullopt, std::move(metadata)) {} + +InterestGroup::Ad::Ad(GURL render_url, + absl::optional size_group, + absl::optional metadata) + : render_url(std::move(render_url)), + size_group(std::move(size_group)), + metadata(std::move(metadata)) {} InterestGroup::Ad::~Ad() = default; size_t InterestGroup::Ad::EstimateSize() const { size_t size = 0u; size += render_url.spec().length(); + if (size_group) { + size += size_group->size(); + } if (metadata) size += metadata->size(); return size; } bool InterestGroup::Ad::operator==(const Ad& other) const { - return render_url == other.render_url && metadata == other.metadata; + return std::tie(render_url, size_group, metadata) == + std::tie(other.render_url, other.size_group, other.metadata); } InterestGroup::InterestGroup() = default; @@ -183,15 +194,29 @@ bool InterestGroup::IsValid() const { if (ads) { for (const auto& ad : ads.value()) { - if (!IsUrlAllowedForRenderUrls(ad.render_url)) + if (!IsUrlAllowedForRenderUrls(ad.render_url)) { return false; + } + if (ad.size_group) { + if (ad.size_group->empty() || !size_groups || + !size_groups->contains(ad.size_group.value())) { + return false; + } + } } } if (ad_components) { for (const auto& ad : ad_components.value()) { - if (!IsUrlAllowedForRenderUrls(ad.render_url)) + if (!IsUrlAllowedForRenderUrls(ad.render_url)) { return false; + } + if (ad.size_group) { + if (ad.size_group->empty() || !size_groups || + !size_groups->contains(ad.size_group.value())) { + return false; + } + } } } diff --git a/third_party/blink/common/interest_group/interest_group_mojom_traits.cc b/third_party/blink/common/interest_group/interest_group_mojom_traits.cc index 2f9c222503b18b..b10379c7ffe19b 100644 --- a/third_party/blink/common/interest_group/interest_group_mojom_traits.cc +++ b/third_party/blink/common/interest_group/interest_group_mojom_traits.cc @@ -14,6 +14,7 @@ bool StructTraits< blink::InterestGroup::Ad>::Read(blink::mojom::InterestGroupAdDataView data, blink::InterestGroup::Ad* out) { if (!data.ReadRenderUrl(&out->render_url) || + !data.ReadSizeGroup(&out->size_group) || !data.ReadMetadata(&out->metadata)) { return false; } diff --git a/third_party/blink/common/interest_group/interest_group_mojom_traits_test.cc b/third_party/blink/common/interest_group/interest_group_mojom_traits_test.cc index 3acbee0ab7ebf2..e62b5313edf27f 100644 --- a/third_party/blink/common/interest_group/interest_group_mojom_traits_test.cc +++ b/third_party/blink/common/interest_group/interest_group_mojom_traits_test.cc @@ -190,8 +190,37 @@ TEST(InterestGroupMojomTraitsTest, SerializeAndDeserializeUserBiddingSignals) { TEST(InterestGroupMojomTraitsTest, SerializeAndDeserializeAds) { InterestGroup interest_group = CreateInterestGroup(); interest_group.ads.emplace(); - interest_group.ads->emplace_back(GURL(kUrl1), /*metadata=*/absl::nullopt); - interest_group.ads->emplace_back(GURL(kUrl2), /*metadata=*/"[]"); + interest_group.ads->emplace_back(GURL(kUrl1), /*size_group=*/absl::nullopt, + /*metadata=*/absl::nullopt); + interest_group.ads->emplace_back(GURL(kUrl2), /*size_group=*/absl::nullopt, + /*metadata=*/"[]"); + SerializeAndDeserializeAndCompare(interest_group); +} + +TEST(InterestGroupMojomTraitsTest, SerializeAndDeserializeAdsWithSizeGroups) { + InterestGroup interest_group = CreateInterestGroup(); + // All three of the following mappings must be valid in order for the + // serialization and deserialization to succeed, when there is an ad with a + // size group assigned. + // 1. Ad --> size group + // 2. Size groups --> sizes + // 3. Size --> blink::AdSize + interest_group.ads.emplace(); + interest_group.ads->emplace_back(GURL(kUrl1), /*size_group=*/"group_1", + /*metadata=*/absl::nullopt); + interest_group.ads->emplace_back(GURL(kUrl2), /*size_group=*/"group_2", + /*metadata=*/"[]"); + interest_group.ad_sizes.emplace(); + interest_group.ad_sizes->emplace( + "size_1", blink::AdSize(300, blink::AdSize::LengthUnit::kPixels, 150, + blink::AdSize::LengthUnit::kPixels)); + interest_group.ad_sizes->emplace( + "size_2", blink::AdSize(640, blink::AdSize::LengthUnit::kPixels, 480, + blink::AdSize::LengthUnit::kPixels)); + std::vector size_list = {"size_1", "size_2"}; + interest_group.size_groups.emplace(); + interest_group.size_groups->emplace("group_1", size_list); + interest_group.size_groups->emplace("group_2", size_list); SerializeAndDeserializeAndCompare(interest_group); } @@ -199,8 +228,40 @@ TEST(InterestGroupMojomTraitsTest, SerializeAndDeserializeAdComponents) { InterestGroup interest_group = CreateInterestGroup(); interest_group.ad_components.emplace(); interest_group.ad_components->emplace_back(GURL(kUrl1), + /*size_group=*/absl::nullopt, /*metadata=*/absl::nullopt); - interest_group.ad_components->emplace_back(GURL(kUrl2), /*metadata=*/"[]"); + interest_group.ad_components->emplace_back( + GURL(kUrl2), /*size_group=*/absl::nullopt, /*metadata=*/"[]"); + SerializeAndDeserializeAndCompare(interest_group); +} + +TEST(InterestGroupMojomTraitsTest, + SerializeAndDeserializeAdComponentsWithSize) { + InterestGroup interest_group = CreateInterestGroup(); + // All three of the following mappings must be valid in order for the + // serialization and deserialization to succeed, when there is an ad component + // with a size group assigned. + // 1. Ad component --> size group + // 2. Size groups --> sizes + // 3. Size --> blink::AdSize + interest_group.ad_components.emplace(); + interest_group.ad_components->emplace_back(GURL(kUrl1), + /*size_group=*/"group_1", + /*metadata=*/absl::nullopt); + interest_group.ad_components->emplace_back(GURL(kUrl2), + /*size_group=*/"group_2", + /*metadata=*/"[]"); + interest_group.ad_sizes.emplace(); + interest_group.ad_sizes->emplace( + "size_1", blink::AdSize(300, blink::AdSize::LengthUnit::kPixels, 150, + blink::AdSize::LengthUnit::kPixels)); + interest_group.ad_sizes->emplace( + "size_2", blink::AdSize(640, blink::AdSize::LengthUnit::kPixels, 480, + blink::AdSize::LengthUnit::kPixels)); + std::vector size_list = {"size_1", "size_2"}; + interest_group.size_groups.emplace(); + interest_group.size_groups->emplace("group_1", size_list); + interest_group.size_groups->emplace("group_2", size_list); SerializeAndDeserializeAndCompare(interest_group); } diff --git a/third_party/blink/common/interest_group/test_interest_group_builder.cc b/third_party/blink/common/interest_group/test_interest_group_builder.cc index ffdae89d76bc7d..c7aadd41530f37 100644 --- a/third_party/blink/common/interest_group/test_interest_group_builder.cc +++ b/third_party/blink/common/interest_group/test_interest_group_builder.cc @@ -156,4 +156,17 @@ TestInterestGroupBuilder& TestInterestGroupBuilder::SetAdComponents( return *this; } +TestInterestGroupBuilder& TestInterestGroupBuilder::SetAdSizes( + absl::optional> ad_sizes) { + interest_group_.ad_sizes = std::move(ad_sizes); + return *this; +} + +TestInterestGroupBuilder& TestInterestGroupBuilder::SetSizeGroups( + absl::optional>> + size_groups) { + interest_group_.size_groups = std::move(size_groups); + return *this; +} + } // namespace blink diff --git a/third_party/blink/public/common/interest_group/interest_group.h b/third_party/blink/public/common/interest_group/interest_group.h index 6e17f1d33511a8..245640b52d6d0d 100644 --- a/third_party/blink/public/common/interest_group/interest_group.h +++ b/third_party/blink/public/common/interest_group/interest_group.h @@ -38,6 +38,9 @@ struct BLINK_COMMON_EXPORT InterestGroup { struct BLINK_COMMON_EXPORT Ad { Ad(); Ad(GURL render_url, absl::optional metadata); + Ad(GURL render_url, + absl::optional size_group, + absl::optional metadata); ~Ad(); // Returns the approximate size of the contents of this InterestGroup::Ad, @@ -46,6 +49,8 @@ struct BLINK_COMMON_EXPORT InterestGroup { // Must use https. GURL render_url; + // Optional size group assigned to this Ad. + absl::optional size_group; // Opaque JSON data, passed as an object to auction worklet. absl::optional metadata; @@ -122,7 +127,7 @@ struct BLINK_COMMON_EXPORT InterestGroup { absl::optional>> size_groups; - static_assert(__LINE__ == 125, R"( + static_assert(__LINE__ == 130, R"( If modifying InterestGroup fields, make sure to also modify: * IsValid(), EstimateSize(), and IsEqualForTesting() in this class diff --git a/third_party/blink/public/common/interest_group/interest_group_mojom_traits.h b/third_party/blink/public/common/interest_group/interest_group_mojom_traits.h index 27cdafccee8826..da6e946859771e 100644 --- a/third_party/blink/public/common/interest_group/interest_group_mojom_traits.h +++ b/third_party/blink/public/common/interest_group/interest_group_mojom_traits.h @@ -28,6 +28,11 @@ struct BLINK_COMMON_EXPORT StructTraits& size_group( + const blink::InterestGroup::Ad& ad) { + return ad.size_group; + } + static const absl::optional& metadata( const blink::InterestGroup::Ad& ad) { return ad.metadata; diff --git a/third_party/blink/public/common/interest_group/test_interest_group_builder.h b/third_party/blink/public/common/interest_group/test_interest_group_builder.h index ff05fb34a962df..c35da097a73531 100644 --- a/third_party/blink/public/common/interest_group/test_interest_group_builder.h +++ b/third_party/blink/public/common/interest_group/test_interest_group_builder.h @@ -62,6 +62,11 @@ class TestInterestGroupBuilder { absl::optional> ads); TestInterestGroupBuilder& SetAdComponents( absl::optional> ad_components); + TestInterestGroupBuilder& SetAdSizes( + absl::optional> ad_sizes); + TestInterestGroupBuilder& SetSizeGroups( + absl::optional>> + size_groups); private: InterestGroup interest_group_; diff --git a/third_party/blink/public/mojom/interest_group/interest_group_types.mojom b/third_party/blink/public/mojom/interest_group/interest_group_types.mojom index 08b48ca378cd73..635e658cda6258 100644 --- a/third_party/blink/public/mojom/interest_group/interest_group_types.mojom +++ b/third_party/blink/public/mojom/interest_group/interest_group_types.mojom @@ -16,6 +16,8 @@ import "url/mojom/url.mojom"; struct InterestGroupAd { // Must use https. url.mojom.Url render_url; + // Optional size groups assigned to this Ad. + string? size_group; // Opaque JSON data, persisted, then passed as object to auction worklet. string? metadata; }; diff --git a/third_party/blink/renderer/modules/ad_auction/auction_ad.idl b/third_party/blink/renderer/modules/ad_auction/auction_ad.idl index 115cab4aceb611..fd3f9bf5721c76 100644 --- a/third_party/blink/renderer/modules/ad_auction/auction_ad.idl +++ b/third_party/blink/renderer/modules/ad_auction/auction_ad.idl @@ -7,5 +7,6 @@ dictionary AuctionAd { required USVString renderUrl; + USVString sizeGroup; any metadata; }; diff --git a/third_party/blink/renderer/modules/ad_auction/navigator_auction.cc b/third_party/blink/renderer/modules/ad_auction/navigator_auction.cc index 599aff8cf53c5d..63b1547f4c6b98 100644 --- a/third_party/blink/renderer/modules/ad_auction/navigator_auction.cc +++ b/third_party/blink/renderer/modules/ad_auction/navigator_auction.cc @@ -656,6 +656,9 @@ bool CopyAdsFromIdlToMojo(const ExecutionContext& context, return false; } mojo_ad->render_url = render_url; + if (ad->hasSizeGroup()) { + mojo_ad->size_group = ad->sizeGroup(); + } if (ad->hasMetadata()) { if (!Jsonify(script_state, ad->metadata().V8Value(), mojo_ad->metadata)) { exception_state.ThrowTypeError( @@ -686,6 +689,9 @@ bool CopyAdComponentsFromIdlToMojo(const ExecutionContext& context, return false; } mojo_ad->render_url = render_url; + if (ad->hasSizeGroup()) { + mojo_ad->size_group = ad->sizeGroup(); + } if (ad->hasMetadata()) { if (!Jsonify(script_state, ad->metadata().V8Value(), mojo_ad->metadata)) { exception_state.ThrowTypeError( diff --git a/third_party/blink/renderer/modules/ad_auction/validate_blink_interest_group.cc b/third_party/blink/renderer/modules/ad_auction/validate_blink_interest_group.cc index 9ed9ab5c9da623..8d75905d53b234 100644 --- a/third_party/blink/renderer/modules/ad_auction/validate_blink_interest_group.cc +++ b/third_party/blink/renderer/modules/ad_auction/validate_blink_interest_group.cc @@ -93,6 +93,7 @@ size_t EstimateBlinkInterestGroupSize( if (group.ads) { for (const auto& ad : group.ads.value()) { size += ad->render_url.GetString().length(); + size += ad->size_group.length(); size += ad->metadata.length(); } } @@ -100,6 +101,7 @@ size_t EstimateBlinkInterestGroupSize( if (group.ad_components) { for (const auto& ad : group.ad_components.value()) { size += ad->render_url.GetString().length(); + size += ad->size_group.length(); size += ad->metadata.length(); } } @@ -228,6 +230,21 @@ bool ValidateBlinkInterestGroup(const mojom::blink::InterestGroup& group, error = "renderUrls must be HTTPS and have no embedded credentials."; return false; } + const WTF::String& ad_size_group = group.ads.value()[i]->size_group; + if (!ad_size_group.IsNull()) { + if (ad_size_group.empty()) { + error_field_name = String::Format("ads[%u].sizeGroup", i); + error_field_value = ad_size_group; + error = "Size group name cannot be empty."; + return false; + } + if (!group.size_groups || !group.size_groups->Contains(ad_size_group)) { + error_field_name = String::Format("ads[%u].sizeGroup", i); + error_field_value = ad_size_group; + error = "The assigned size group does not exist in sizeGroups map."; + return false; + } + } } } @@ -240,8 +257,26 @@ bool ValidateBlinkInterestGroup(const mojom::blink::InterestGroup& group, error = "renderUrls must be HTTPS and have no embedded credentials."; return false; } + const WTF::String& ad_component_size_group = + group.ad_components.value()[i]->size_group; + if (!ad_component_size_group.IsNull()) { + if (ad_component_size_group.empty()) { + error_field_name = String::Format("adComponents[%u].sizeGroup", i); + error_field_value = ad_component_size_group; + error = "Size group name cannot be empty."; + return false; + } + if (!group.size_groups || + !group.size_groups->Contains(ad_component_size_group)) { + error_field_name = String::Format("adComponents[%u].sizeGroup", i); + error_field_value = ad_component_size_group; + error = "The assigned size group does not exist in sizeGroups map."; + return false; + } + } } } + if (group.ad_sizes) { for (auto const& it : group.ad_sizes.value()) { if (it.key == "") { diff --git a/third_party/blink/renderer/modules/ad_auction/validate_blink_interest_group_test.cc b/third_party/blink/renderer/modules/ad_auction/validate_blink_interest_group_test.cc index 8b637b196d2143..43f7d092efd13c 100644 --- a/third_party/blink/renderer/modules/ad_auction/validate_blink_interest_group_test.cc +++ b/third_party/blink/renderer/modules/ad_auction/validate_blink_interest_group_test.cc @@ -392,7 +392,7 @@ TEST_F(ValidateBlinkInterestGroupTest, AdRenderUrlValidation) { CreateMinimalInterestGroup(); blink_interest_group->ads.emplace(); blink_interest_group->ads->emplace_back(mojom::blink::InterestGroupAd::New( - test_case_url, /*metadata=*/String())); + test_case_url, /*size_group=*/String(), /*metadata=*/String())); if (test_case.expect_allowed) { ExpectInterestGroupIsValid(blink_interest_group); } else { @@ -408,9 +408,9 @@ TEST_F(ValidateBlinkInterestGroupTest, AdRenderUrlValidation) { blink_interest_group->ads.emplace(); blink_interest_group->ads->emplace_back(mojom::blink::InterestGroupAd::New( KURL(String::FromUTF8("https://origin.test/")), - /*metadata=*/String())); + /*size_group=*/String(), /*metadata=*/String())); blink_interest_group->ads->emplace_back(mojom::blink::InterestGroupAd::New( - test_case_url, /*metadata=*/String())); + test_case_url, /*size_group=*/String(), /*metadata=*/String())); if (test_case.expect_allowed) { ExpectInterestGroupIsValid(blink_interest_group); } else { @@ -469,6 +469,7 @@ TEST_F(ValidateBlinkInterestGroupTest, AdComponentRenderUrlValidation) { blink_interest_group->ad_components.emplace(); blink_interest_group->ad_components->emplace_back( mojom::blink::InterestGroupAd::New(test_case_url, + /*size_group=*/String(), /*metadata=*/String())); if (test_case.expect_allowed) { ExpectInterestGroupIsValid(blink_interest_group); @@ -487,9 +488,10 @@ TEST_F(ValidateBlinkInterestGroupTest, AdComponentRenderUrlValidation) { blink_interest_group->ad_components->emplace_back( mojom::blink::InterestGroupAd::New( KURL(String::FromUTF8("https://origin.test/")), - /*metadata=*/String())); + /*size_group=*/String(), /*metadata=*/String())); blink_interest_group->ad_components->emplace_back( mojom::blink::InterestGroupAd::New(test_case_url, + /*size_group=*/String(), /*metadata=*/String())); if (test_case.expect_allowed) { ExpectInterestGroupIsValid(blink_interest_group); @@ -522,7 +524,8 @@ TEST_F(ValidateBlinkInterestGroupTest, MalformedUrl) { blink_interest_group->name = kName; blink_interest_group->ads.emplace(); blink_interest_group->ads->emplace_back(mojom::blink::InterestGroupAd::New( - KURL(kMalformedUrl), /*metadata=*/String())); + KURL(kMalformedUrl), /*size_group=*/String(), + /*metadata=*/String())); String error_field_name; String error_field_value; String error; @@ -932,7 +935,82 @@ TEST_F(ValidateBlinkInterestGroupTest, InvalidSizeGroups) { blink_interest_group->size_groups->insert( test_case.size_group, WTF::Vector(1, test_case.size_name)); ExpectInterestGroupIsNotValid( - /*expected_error_field_name=*/blink_interest_group, "sizeGroups", + blink_interest_group, /*expected_error_field_name=*/"sizeGroups", + test_case.expected_error_field_value, test_case.expected_error); + } +} + +TEST_F(ValidateBlinkInterestGroupTest, AdSizeGroupEmptyNameOrNotInSizeGroups) { + constexpr char kSizeGroupError[] = + "The assigned size group does not exist in sizeGroups map."; + constexpr char kNameError[] = "Size group name cannot be empty."; + struct { + const char* ad_size_group; + const char* size_group; + const char* expected_error_field_value; + const char* expected_error; + } test_cases[] = { + {"", "group_name", "", kNameError}, + {"group_name", "different_group_name", "group_name", kSizeGroupError}, + {"group_name", "", "group_name", kSizeGroupError}, + }; + for (const auto& test_case : test_cases) { + mojom::blink::InterestGroupPtr blink_interest_group = + CreateMinimalInterestGroup(); + blink_interest_group->ads.emplace(); + blink_interest_group->ads->emplace_back(mojom::blink::InterestGroupAd::New( + KURL("https://origin.test/foo?bar"), + /*size_group=*/test_case.ad_size_group, + /*metadata=*/String())); + blink_interest_group->ad_sizes.emplace(); + blink_interest_group->ad_sizes->insert( + "size_name", blink::mojom::blink::AdSize::New( + 300, blink::AdSize::LengthUnit::kPixels, 150, + blink::AdSize::LengthUnit::kPixels)); + blink_interest_group->size_groups.emplace(); + blink_interest_group->size_groups->insert( + test_case.size_group, WTF::Vector(1, "size_name")); + ExpectInterestGroupIsNotValid( + blink_interest_group, /*expected_error_field_name=*/"ads[0].sizeGroup", + test_case.expected_error_field_value, test_case.expected_error); + } +} + +TEST_F(ValidateBlinkInterestGroupTest, + AdComponentSizeGroupEmptyNameOrNotInSizeGroups) { + constexpr char kSizeGroupError[] = + "The assigned size group does not exist in sizeGroups map."; + constexpr char kNameError[] = "Size group name cannot be empty."; + struct { + const char* ad_component_size_group; + const char* size_group; + const char* expected_error_field_value; + const char* expected_error; + } test_cases[] = { + {"", "group_name", "", kNameError}, + {"group_name", "different_group_name", "group_name", kSizeGroupError}, + {"group_name", "", "group_name", kSizeGroupError}, + }; + for (const auto& test_case : test_cases) { + mojom::blink::InterestGroupPtr blink_interest_group = + CreateMinimalInterestGroup(); + blink_interest_group->ad_components.emplace(); + blink_interest_group->ad_components->emplace_back( + mojom::blink::InterestGroupAd::New( + KURL("https://origin.test/foo?bar"), + /*size_group=*/test_case.ad_component_size_group, + /*metadata=*/String())); + blink_interest_group->ad_sizes.emplace(); + blink_interest_group->ad_sizes->insert( + "size_name", blink::mojom::blink::AdSize::New( + 300, blink::AdSize::LengthUnit::kPixels, 150, + blink::AdSize::LengthUnit::kPixels)); + blink_interest_group->size_groups.emplace(); + blink_interest_group->size_groups->insert( + test_case.size_group, WTF::Vector(1, "size_name")); + ExpectInterestGroupIsNotValid( + blink_interest_group, + /*expected_error_field_name=*/"adComponents[0].sizeGroup", test_case.expected_error_field_value, test_case.expected_error); } }