Skip to content

Commit

Permalink
Add navigator.deprecatedReplaceInURN for FLEDGE
Browse files Browse the repository at this point in the history
This function is only available when FLEDGE ads are allowed in iframes.
It replaces sequences beginning/ending with "%%"/"%%" or "${"/"}" that
appear in the URL with the specified substitution.

Note that this replacement occurs after URL parsing, so
replacements that want to match an item need to match the parsed
representation according to the URL spec
(https://url.spec.whatwg.org/#url-parsing). In the case of the URL
path, the characters '{' and '}' will be percent-encoded during parsing
so the "${"/"}" replacement scheme cannot be used on a URL path, though
the "%%" replacement scheme still works. Both replacement types can be
used in the query parameters and fragment portions of the URL. The
URL created by the replacement will be parsed again by the browser.

Bug: 1322194
Change-Id: Ie0fd3478760e390fe774aee3078ba306a5fbd76c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3625390
Commit-Queue: Russ Hamilton <behamilton@google.com>
Reviewed-by: Matt Menke <mmenke@chromium.org>
Reviewed-by: Daniel Cheng <dcheng@chromium.org>
Reviewed-by: Dominic Farolino <dom@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1002835}
  • Loading branch information
brusshamilton authored and Chromium LUCI CQ committed May 12, 2022
1 parent 866df27 commit 74807ed
Show file tree
Hide file tree
Showing 12 changed files with 377 additions and 10 deletions.
58 changes: 58 additions & 0 deletions content/browser/fenced_frame/fenced_frame_url_mapping.cc
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

#include "base/check_op.h"
#include "base/guid.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "third_party/blink/public/common/fenced_frame/fenced_frame_utils.h"
Expand All @@ -28,6 +29,38 @@ GURL GenerateURN() {
base::GUID::GenerateRandomV4().AsLowercaseString());
}

// Returns a new string based on input where the matching substrings have been
// replaced with the corresponding substitutions. This function avoids repeated
// string operations by building the output based on all substitutions, one
// substitution at a time. This effectively performs all substitutions
// simultaneously, with the earliest match in the input taking precedence.
std::string SubstituteMappedStrings(
const std::string& input,
const std::vector<std::pair<std::string, std::string>>& substitutions) {
std::vector<std::string> output_vec;
size_t input_idx = 0;
while (input_idx < input.size()) {
size_t replace_idx = input.size();
size_t replace_end_idx = input.size();
std::pair<std::string, std::string> const* next_replacement = nullptr;
for (const auto& substitution : substitutions) {
size_t found_idx = input.find(substitution.first, input_idx);
if (found_idx < replace_idx) {
replace_idx = found_idx;
replace_end_idx = found_idx + substitution.first.size();
next_replacement = &substitution;
}
}
output_vec.push_back(input.substr(input_idx, replace_idx - input_idx));
if (replace_idx < input.size()) {
output_vec.push_back(next_replacement->second);
}
// move input index to after what we replaced (or end of string).
input_idx = replace_end_idx;
}
return base::StrCat(output_vec);
}

} // namespace

FencedFrameURLMapping::PendingAdComponentsMap::PendingAdComponentsMap(
Expand Down Expand Up @@ -232,6 +265,31 @@ FencedFrameURLMapping::GetSharedStorageBudgetMetadata(const GURL& urn_uuid) {
return &it->second.shared_storage_budget_metadata.value();
}

void FencedFrameURLMapping::SubstituteMappedURL(
const GURL& urn_uuid,
const std::vector<std::pair<std::string, std::string>>& substitutions) {
auto it = urn_uuid_to_url_map_.find(urn_uuid);
if (it == urn_uuid_to_url_map_.end()) {
return;
}
MapInfo info = it->second;
info.mapped_url = GURL(
SubstituteMappedStrings(it->second.mapped_url.spec(), substitutions));
if (!info.mapped_url.is_valid()) {
return;
}
if (info.ad_component_urls) {
for (auto& ad_component_url : info.ad_component_urls.value()) {
ad_component_url =
GURL(SubstituteMappedStrings(ad_component_url.spec(), substitutions));
if (!ad_component_url.is_valid()) {
return;
}
}
}
it->second = std::move(info);
}

bool FencedFrameURLMapping::HasObserverForTesting(
const GURL& urn_uuid,
MappingResultObserver* observer) {
Expand Down
11 changes: 11 additions & 0 deletions content/browser/fenced_frame/fenced_frame_url_mapping.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <string>
#include <vector>

#include "base/containers/flat_map.h"
#include "base/memory/raw_ptr.h"
#include "content/common/content_export.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
Expand Down Expand Up @@ -201,6 +202,16 @@ class CONTENT_EXPORT FencedFrameURLMapping {
SharedStorageBudgetMetadata* GetSharedStorageBudgetMetadata(
const GURL& urn_uuid);

// Modifies the true URL from a URN by replacing substrings specified in the
// replacements map. The true URLs for any component ads associated with this
// URN will also have substrings substituted. This function will be removed
// once all FLEDGE auctions switch to using fenced frames.
// TODO(crbug.com/1253118): Remove this function when we remove support for
// showing FLEDGE ads in iframes.
void SubstituteMappedURL(
const GURL& urn_uuid,
const std::vector<std::pair<std::string, std::string>>& substitutions);

bool HasObserverForTesting(const GURL& urn_uuid,
MappingResultObserver* observer);

Expand Down
54 changes: 54 additions & 0 deletions content/browser/fenced_frame/fenced_frame_url_mapping_unittest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,60 @@ TEST(FencedFrameURLMappingTest,
/*expected_mapped_urls=*/ad_component_urls);
}

// Test the case `ad_component_urls` has a single URL.
TEST(FencedFrameURLMappingTest, SubstituteFencedFrameURLs) {
FencedFrameURLMapping fenced_frame_url_mapping;
GURL top_level_url(
"https://foo.test/page?%%TT%%${oo%%}p%%${p%%${%%l}%%%%%%%%evl%%");
url::Origin interest_group_owner = url::Origin::Create(top_level_url);
std::string interest_group_name = "bars";
std::vector<GURL> ad_component_urls{
GURL("https://bar.test/page?${REPLACED}")};

GURL urn_uuid =
fenced_frame_url_mapping.AddFencedFrameURLWithInterestGroupInfo(
top_level_url, {interest_group_owner, interest_group_name},
ad_component_urls);

fenced_frame_url_mapping.SubstituteMappedURL(
urn_uuid,
{{"%%notPresent%%",
"not inserted"}, // replacements not present not inserted
{"%%TT%%", "t"}, // %% replacement works
{"${oo%%}", "o"}, // mixture of sequences works
{"%%${p%%${%%l}%%%%%%", "_l"}, // mixture of sequences works
{"${%%l}", "Don't replace"}, // earlier replacements take precedence
{"%%evl%%",
"evel_%%still_got_it%%"}, // output can contain replacement sequences
{"%%still_got_it%%",
"not replaced"}, // output of replacement is not replaced
{"${REPLACED}", "component"}}); // replacements affect components

TestFencedFrameURLMappingResultObserver observer;
fenced_frame_url_mapping.ConvertFencedFrameURNToURL(urn_uuid, &observer);
EXPECT_TRUE(observer.mapping_complete_observed());
EXPECT_EQ(GURL("https://foo.test/page?top_level_%%still_got_it%%"),
observer.mapped_url());
EXPECT_EQ(interest_group_owner,
observer.ad_auction_data()->interest_group_owner);
EXPECT_EQ(interest_group_name,
observer.ad_auction_data()->interest_group_name);
EXPECT_TRUE(observer.pending_ad_components_map());

// Call with `add_to_new_map` set to false and true, to simulate ShadowDOM
// and MPArch behavior, respectively.
std::vector<GURL> expected_ad_component_urls{
GURL("https://bar.test/page?component")};
ValidatePendingAdComponentsMap(&fenced_frame_url_mapping,
/*add_to_new_map=*/true,
*observer.pending_ad_components_map(),
expected_ad_component_urls);
ValidatePendingAdComponentsMap(&fenced_frame_url_mapping,
/*add_to_new_map=*/false,
*observer.pending_ad_components_map(),
expected_ad_component_urls);
}

// Test the correctness of the URN format. The URN is expected to be in the
// format "urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" as per RFC-4122.
TEST(FencedFrameURLMappingTest, HasCorrectFormat) {
Expand Down
30 changes: 29 additions & 1 deletion content/browser/interest_group/ad_auction_service_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ void AdAuctionServiceImpl::DeprecatedGetURLFromURN(
const GURL& urn_url,
DeprecatedGetURLFromURNCallback callback) {
if (!blink::IsValidUrnUuidURL(urn_url)) {
std::move(callback).Run(absl::nullopt);
ReportBadMessageAndDeleteThis("Unexpected request: invalid URN");
return;
}
FencedFrameURLMappingObserver obs;
Expand All @@ -327,6 +327,34 @@ void AdAuctionServiceImpl::DeprecatedGetURLFromURN(
std::move(callback).Run(std::move(obs.mapped_url_));
}

void AdAuctionServiceImpl::DeprecatedReplaceInURN(
const GURL& urn_url,
std::vector<blink::mojom::ReplacementPtr> replacements,
DeprecatedReplaceInURNCallback callback) {
if (!blink::IsValidUrnUuidURL(urn_url)) {
ReportBadMessageAndDeleteThis("Unexpected request: invalid URN");
return;
}
std::vector<std::pair<std::string, std::string>> local_replacements;
for (const auto& replacement : replacements) {
if (!(base::StartsWith(replacement->match, "${") &&
base::EndsWith(replacement->match, "}")) &&
!(base::StartsWith(replacement->match, "%%") &&
base::EndsWith(replacement->match, "%%"))) {
ReportBadMessageAndDeleteThis("Unexpected request: bad replacement");
return;
}
local_replacements.emplace_back(std::move(replacement->match),
std::move(replacement->replacement));
}
content::FencedFrameURLMapping& mapping =
static_cast<RenderFrameHostImpl*>(render_frame_host())
->GetPage()
.fenced_frame_urls_map();
mapping.SubstituteMappedURL(urn_url, local_replacements);
std::move(callback).Run();
}

void AdAuctionServiceImpl::CreateAdRequest(
blink::mojom::AdRequestConfigPtr config,
CreateAdRequestCallback callback) {
Expand Down
4 changes: 4 additions & 0 deletions content/browser/interest_group/ad_auction_service_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ class CONTENT_EXPORT AdAuctionServiceImpl final
void DeprecatedGetURLFromURN(
const GURL& urn_url,
DeprecatedGetURLFromURNCallback callback) override;
void DeprecatedReplaceInURN(
const GURL& urn_url,
std::vector<blink::mojom::ReplacementPtr> replacements,
DeprecatedReplaceInURNCallback callback) override;
void CreateAdRequest(blink::mojom::AdRequestConfigPtr config,
CreateAdRequestCallback callback) override;
void FinalizeAd(const std::string& ads_guid,
Expand Down
130 changes: 130 additions & 0 deletions content/browser/interest_group/interest_group_browsertest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -896,6 +896,26 @@ class InterestGroupBrowserTest : public ContentBrowserTest {
return GURL(result.ExtractString());
}

bool ReplaceInURNInJS(
const GURL& urn_url,
const base::flat_map<std::string, std::string> replacements,
const absl::optional<ToRenderFrameHost> execution_target =
absl::nullopt) {
base::Value::Dict replacement_value;
for (const auto& replacement : replacements)
replacement_value.Set(replacement.first, replacement.second);
EvalJsResult result =
EvalJs(execution_target ? *execution_target : shell(),
JsReplace(R"(
(async function() {
await navigator.deprecatedReplaceInURN($1, $2);
return 'done';
})())",
urn_url, base::Value(std::move(replacement_value))));
EXPECT_EQ("", result.error);
return "done" == result;
}

void AttachInterestGroupObserver() {
manager_->AddInterestGroupObserver(observer_.get());
}
Expand Down Expand Up @@ -2995,6 +3015,58 @@ IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest, RunAdAuctionWithWinner) {
->trusted_params->isolation_info.network_isolation_key());
}

IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
RunAdAuctionWithWinnerReplacedURN) {
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 =
GURL(https_server_->GetURL("c.test", "/%%echo%%?${INTEREST_GROUP_NAME}")
.spec());
GURL expected_ad_url = https_server_->GetURL("c.test", "/echo?render_cars");

EXPECT_EQ(kSuccess, JoinInterestGroupAndVerify(
/*owner=*/test_origin,
/*name=*/"cars",
/*priority=*/0.0,
/*bidding_url=*/
https_server_->GetURL(
"a.test", "/interest_group/bidding_logic.js"),
/*ads=*/{{{ad_url, "{ad:'metadata', here:[1,2]}"}}}));

std::string auction_config = JsReplace(
R"({
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1],
})",
test_origin,
https_server_->GetURL("a.test", "/interest_group/decision_logic.js"));
auto result = RunAuctionAndWait(auction_config,
/*execution_target=*/absl::nullopt);
GURL urn_url = GURL(result.ExtractString());
EXPECT_TRUE(urn_url.is_valid());
EXPECT_EQ(url::kUrnScheme, urn_url.scheme_piece());

{
TestFencedFrameURLMappingResultObserver observer;
ConvertFencedFrameURNToURL(urn_url, &observer);
EXPECT_TRUE(observer.mapped_url()) << urn_url;
EXPECT_EQ(ad_url, observer.mapped_url());
}

EXPECT_TRUE(ReplaceInURNInJS(
urn_url,
{{"${INTEREST_GROUP_NAME}", "render_cars"}, {"%%echo%%", "echo"}}));

{
TestFencedFrameURLMappingResultObserver observer;
ConvertFencedFrameURNToURL(urn_url, &observer);
EXPECT_EQ(expected_ad_url, observer.mapped_url());
}
NavigateIframeAndCheckURL(web_contents(), urn_url, expected_ad_url);
}

IN_PROC_BROWSER_TEST_F(
InterestGroupBrowserTest,
RunAdAuctionPerBuyerSignalsAndPerBuyerTimeoutsOriginNotInBuyers) {
Expand Down Expand Up @@ -3439,6 +3511,64 @@ perBuyerSignals: {$1: {even: 'more', x: 4.5}}
->trusted_params->isolation_info.network_isolation_key());
}

IN_PROC_BROWSER_TEST_P(InterestGroupFencedFrameBrowserTest,
RunAdAuctionWithWinnerReplacedURN) {
URLLoaderMonitor url_loader_monitor;

GURL test_url =
https_server_->GetURL("a.test", "/fenced_frames/opaque_ads.html");
ASSERT_TRUE(NavigateToURL(shell(), test_url));
url::Origin test_origin = url::Origin::Create(test_url);

GURL ad_url = https_server_->GetURL(
"c.test", "/set-header?Supports-Loading-Mode: %%LOADING_MODE%%");
GURL expected_ad_url = https_server_->GetURL(
"c.test", "/set-header?Supports-Loading-Mode: fenced-frame");
EXPECT_EQ(
kSuccess,
JoinInterestGroupAndVerify(blink::InterestGroup(
/*expiry=*/base::Time(),
/*owner=*/test_origin,
/*name=*/"cars",
/*priority=*/0.0,
/*bidding_url=*/
https_server_->GetURL("a.test", "/interest_group/bidding_logic.js"),
/*bidding_wasm_helper_url=*/absl::nullopt,
/*daily_update_url=*/absl::nullopt,
/*trusted_bidding_signals_url=*/
https_server_->GetURL("a.test",
"/interest_group/trusted_bidding_signals.json"),
/*trusted_bidding_signals_keys=*/{{"key1"}},
/*user_bidding_signals=*/"{some: 'json', data: {here: [1, 2]}}",
{{{ad_url, "{ad:'metadata', here:[1,2]}"}}},
/*ad_components=*/absl::nullopt)));

content::EvalJsResult urn_url_string = RunAuctionAndWait(
JsReplace(
R"({
seller: $1,
decisionLogicUrl: $2,
interestGroupBuyers: [$1],
auctionSignals: {x: 1},
sellerSignals: {yet: 'more', info: 1},
perBuyerSignals: {$1: {even: 'more', x: 4.5}}
})",
test_origin,
https_server_->GetURL("a.test", "/interest_group/decision_logic.js")),
shell());
ASSERT_TRUE(urn_url_string.value.is_string())
<< "Expected string, but got " << urn_url_string.value;

GURL urn_url(urn_url_string.ExtractString());
ASSERT_TRUE(urn_url.is_valid())
<< "URL is not valid: " << urn_url_string.ExtractString();
EXPECT_EQ(url::kUrnScheme, urn_url.scheme_piece());

ReplaceInURNInJS(urn_url, {{"%%LOADING_MODE%%", "fenced-frame"}});

NavigateFencedFrameAndWait(urn_url, expected_ad_url, shell());
}

// Runs two ad auctions with fenced frames enabled. Both auctions should
// succeed and are then loaded in separate fenced frames. Both auctions try to
// leave the interest group, but only the one whose ad matches the joining
Expand Down

0 comments on commit 74807ed

Please sign in to comment.