| // Copyright 2021 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "content/browser/fenced_frame/fenced_frame_url_mapping.h" |
| |
| #include <cstring> |
| #include <map> |
| #include <optional> |
| #include <string> |
| |
| #include "base/containers/contains.h" |
| #include "base/feature_list.h" |
| #include "base/functional/callback.h" |
| #include "base/memory/ref_counted.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/not_fatal_until.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "content/browser/fenced_frame/fenced_frame_reporter.h" |
| #include "third_party/blink/public/common/features.h" |
| #include "third_party/blink/public/common/fenced_frame/fenced_frame_utils.h" |
| #include "third_party/blink/public/common/frame/fenced_frame_permissions_policies.h" |
| #include "third_party/blink/public/common/interest_group/ad_display_size.h" |
| #include "third_party/blink/public/common/interest_group/ad_display_size_utils.h" |
| #include "ui/display/screen.h" |
| #include "url/gurl.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| int AdSizeToPixels(double size, blink::AdSize::LengthUnit unit) { |
| switch (unit) { |
| case blink::AdSize::LengthUnit::kPixels: |
| return static_cast<int>(size); |
| case blink::AdSize::LengthUnit::kScreenWidth: { |
| double screen_width = display::Screen::GetScreen() |
| ->GetPrimaryDisplay() |
| .GetSizeInPixel() |
| .width(); |
| return static_cast<int>(size / 100.0 * screen_width); |
| } |
| case blink::AdSize::LengthUnit::kScreenHeight: { |
| double screen_height = display::Screen::GetScreen() |
| ->GetPrimaryDisplay() |
| .GetSizeInPixel() |
| .height(); |
| return static_cast<int>(size / 100.0 * screen_height); |
| } |
| case blink::AdSize::LengthUnit::kInvalid: |
| NOTREACHED_NORETURN(); |
| } |
| } |
| |
| gfx::Size AdSizeToGfxSize(const blink::AdSize& ad_size) { |
| int width_in_pixels = AdSizeToPixels(ad_size.width, ad_size.width_units); |
| int height_in_pixels = AdSizeToPixels(ad_size.height, ad_size.height_units); |
| |
| return gfx::Size(width_in_pixels, height_in_pixels); |
| } |
| |
| // TODO(crbug.com/40258855): Once the representation of size in fenced frame |
| // config is finalized, change the type of substituted width and height to the |
| // same. |
| // Substitute the size macros in ad url with the size from the winning bid. |
| GURL SubstituteSizeIntoURL(const blink::AdDescriptor& ad_descriptor) { |
| if (!ad_descriptor.size) { |
| return ad_descriptor.url; |
| } |
| |
| // Convert dimensions to pixels. |
| gfx::Size size = AdSizeToGfxSize(ad_descriptor.size.value()); |
| |
| std::string width = base::NumberToString(size.width()); |
| std::string height = base::NumberToString(size.height()); |
| std::vector<std::pair<std::string, std::string>> substitutions; |
| |
| // Set up the width and height macros, in two formats. |
| substitutions.emplace_back("{%AD_WIDTH%}", width); |
| substitutions.emplace_back("{%AD_HEIGHT%}", height); |
| if (base::FeatureList::IsEnabled( |
| blink::features::kFencedFramesM120FeaturesPart1)) { |
| substitutions.emplace_back("${AD_WIDTH}", width); |
| substitutions.emplace_back("${AD_HEIGHT}", height); |
| } |
| |
| return GURL(SubstituteMappedStrings(ad_descriptor.url.spec(), substitutions)); |
| } |
| |
| } // namespace |
| |
| FencedFrameURLMapping::FencedFrameURLMapping() = default; |
| |
| FencedFrameURLMapping::~FencedFrameURLMapping() = default; |
| |
| FencedFrameURLMapping::SharedStorageURNMappingResult:: |
| SharedStorageURNMappingResult() = default; |
| |
| FencedFrameURLMapping::SharedStorageURNMappingResult:: |
| SharedStorageURNMappingResult( |
| GURL mapped_url, |
| SharedStorageBudgetMetadata budget_metadata, |
| scoped_refptr<FencedFrameReporter> fenced_frame_reporter) |
| : mapped_url(std::move(mapped_url)), |
| budget_metadata(std::move(budget_metadata)), |
| fenced_frame_reporter(std::move(fenced_frame_reporter)) {} |
| |
| FencedFrameURLMapping::SharedStorageURNMappingResult:: |
| ~SharedStorageURNMappingResult() = default; |
| |
| void FencedFrameURLMapping::ImportPendingAdComponents( |
| const std::vector<std::pair<GURL, FencedFrameConfig>>& components) { |
| for (const auto& component_ad : components) { |
| // If this is called redundantly, do nothing. |
| // This happens in urn iframes, because the FencedFrameURLMapping is |
| // attached to the Page. In fenced frames, the Page is rooted at the fenced |
| // frame root, so a new FencedFrameURLMapping is created when the root is |
| // navigated. In urn iframes, the Page is rooted at the top-level frame, so |
| // the same FencedFrameURLMapping exists after "urn iframe root" |
| // navigations. |
| // TODO(crbug.com/40256574): Change this to a CHECK when we remove urn |
| // iframes. |
| if (IsMapped(component_ad.first)) { |
| return; |
| } |
| |
| UrnUuidToUrlMap::iterator it = |
| urn_uuid_to_url_map_.emplace(component_ad.first, component_ad.second) |
| .first; |
| it->second.nested_configs_.emplace(std::vector<FencedFrameConfig>(), |
| VisibilityToEmbedder::kTransparent, |
| VisibilityToContent::kTransparent); |
| } |
| } |
| |
| std::optional<GURL> FencedFrameURLMapping::AddFencedFrameURLForTesting( |
| const GURL& url, |
| scoped_refptr<FencedFrameReporter> fenced_frame_reporter) { |
| DCHECK(url.is_valid()); |
| CHECK(blink::IsValidFencedFrameURL(url)); |
| |
| auto it = AddMappingForUrl(url); |
| |
| if (!it.has_value()) { |
| // Insertion fails, the number of urn mappings has reached limit. |
| return std::nullopt; |
| } |
| |
| auto& [urn, config] = *it.value(); |
| |
| config.fenced_frame_reporter_ = std::move(fenced_frame_reporter); |
| config.mode_ = blink::FencedFrame::DeprecatedFencedFrameMode::kOpaqueAds; |
| // Give this frame the more restrictive option. |
| config.allows_information_inflow_ = false; |
| config.deprecated_should_freeze_initial_size_.emplace( |
| true, VisibilityToEmbedder::kTransparent, VisibilityToContent::kOpaque); |
| // We don't know at this point if the test being run needs the FLEDGE or |
| // Shared Storage permissions set. To be safe, we set both here. |
| config.effective_enabled_permissions_.insert( |
| config.effective_enabled_permissions_.end(), |
| std::begin(blink::kFencedFrameFledgeDefaultRequiredFeatures), |
| std::end(blink::kFencedFrameFledgeDefaultRequiredFeatures)); |
| config.effective_enabled_permissions_.insert( |
| config.effective_enabled_permissions_.end(), |
| std::begin(blink::kFencedFrameSharedStorageDefaultRequiredFeatures), |
| std::end(blink::kFencedFrameSharedStorageDefaultRequiredFeatures)); |
| return urn; |
| } |
| |
| void FencedFrameURLMapping::ClearMapForTesting() { |
| urn_uuid_to_url_map_.clear(); |
| pending_urn_uuid_to_url_map_.clear(); |
| } |
| |
| std::optional<FencedFrameURLMapping::UrnUuidToUrlMap::iterator> |
| FencedFrameURLMapping::AddMappingForUrl(const GURL& url) { |
| if (IsFull()) { |
| // Number of urn mappings has reached limit, url will not be inserted. |
| return std::nullopt; |
| } |
| |
| // Create a urn::uuid. |
| GURL urn_uuid = GenerateUrnUuid(); |
| DCHECK(!IsMapped(urn_uuid)); |
| |
| return urn_uuid_to_url_map_ |
| .emplace(urn_uuid, FencedFrameConfig(urn_uuid, url)) |
| .first; |
| } |
| |
| blink::FencedFrame::RedactedFencedFrameConfig |
| FencedFrameURLMapping::AssignFencedFrameURLAndInterestGroupInfo( |
| const GURL& urn_uuid, |
| std::optional<blink::AdSize> container_size, |
| const blink::AdDescriptor& ad_descriptor, |
| AdAuctionData ad_auction_data, |
| base::RepeatingClosure on_navigate_callback, |
| std::vector<blink::AdDescriptor> ad_component_descriptors, |
| scoped_refptr<FencedFrameReporter> fenced_frame_reporter) { |
| // Move pending mapped urn::uuid to `urn_uuid_to_url_map_`. |
| // TODO(crbug.com/40896818): Remove the check for whether `urn_uuid` has been |
| // mapped already once the crash is resolved. |
| CHECK(!IsMapped(urn_uuid)); |
| auto pending_it = pending_urn_uuid_to_url_map_.find(urn_uuid); |
| CHECK(pending_it != pending_urn_uuid_to_url_map_.end()); |
| pending_urn_uuid_to_url_map_.erase(pending_it); |
| |
| bool emplaced = false; |
| std::tie(std::ignore, emplaced) = |
| urn_uuid_to_url_map_.emplace(urn_uuid, FencedFrameConfig()); |
| DCHECK(emplaced); |
| auto& config = urn_uuid_to_url_map_[urn_uuid]; |
| |
| // Assign mapped URL and interest group info. |
| // TODO(crbug.com/40258855): Once the representation of size in fenced frame |
| // config is finalized, pass the ad size from the winning bid to its fenced |
| // frame config. |
| config.urn_uuid_.emplace(urn_uuid); |
| config.mapped_url_.emplace(SubstituteSizeIntoURL(ad_descriptor), |
| VisibilityToEmbedder::kOpaque, |
| VisibilityToContent::kTransparent); |
| if (container_size.has_value() && |
| blink::IsValidAdSize(container_size.value())) { |
| gfx::Size container_gfx_size = AdSizeToGfxSize(container_size.value()); |
| config.container_size_.emplace(container_gfx_size, |
| VisibilityToEmbedder::kTransparent, |
| VisibilityToContent::kOpaque); |
| } |
| if (ad_descriptor.size) { |
| gfx::Size content_size = AdSizeToGfxSize(ad_descriptor.size.value()); |
| config.content_size_.emplace(content_size, |
| VisibilityToEmbedder::kTransparent, |
| VisibilityToContent::kTransparent); |
| } |
| config.deprecated_should_freeze_initial_size_.emplace( |
| !ad_descriptor.size.has_value(), VisibilityToEmbedder::kTransparent, |
| VisibilityToContent::kOpaque); |
| config.ad_auction_data_.emplace( |
| (base::FeatureList::IsEnabled( |
| blink::features::kFencedFramesM120FeaturesPart2)) |
| ? ad_auction_data |
| : std::move(ad_auction_data), |
| VisibilityToEmbedder::kOpaque, VisibilityToContent::kOpaque); |
| config.on_navigate_callback_ = std::move(on_navigate_callback); |
| |
| config.effective_enabled_permissions_ = |
| std::vector<blink::mojom::PermissionsPolicyFeature>( |
| std::begin(blink::kFencedFrameFledgeDefaultRequiredFeatures), |
| std::end(blink::kFencedFrameFledgeDefaultRequiredFeatures)); |
| |
| std::vector<FencedFrameConfig> nested_configs; |
| nested_configs.reserve(ad_component_descriptors.size()); |
| for (const auto& ad_component_descriptor : ad_component_descriptors) { |
| // This config has no urn:uuid. It will later be set when being read into |
| // `nested_urn_config_pairs` in `GenerateURNConfigVectorForConfigs()`. |
| // For an ad component, the `fenced_frame_reporter` from its parent fenced |
| // frame is reused. The pointer to its parent's fenced frame reporter is |
| // copied to each ad component. This has the advantage that we do not need |
| // to traverse to its parent every time we need its parent's reporter. |
| // TODO(crbug.com/40258855): Once the representation of size in fenced frame |
| // config is finalized, pass the ad component size from the winning bid to |
| // its fenced frame config. |
| if (ad_component_descriptor.size) { |
| gfx::Size component_content_size = |
| AdSizeToGfxSize(ad_component_descriptor.size.value()); |
| nested_configs.emplace_back( |
| /*mapped_url=*/SubstituteSizeIntoURL(ad_component_descriptor), |
| /*content_size=*/component_content_size, |
| /*fenced_frame_reporter=*/fenced_frame_reporter, |
| /*is_ad_component=*/true); |
| } else { |
| nested_configs.emplace_back( |
| /*mapped_url=*/SubstituteSizeIntoURL(ad_component_descriptor), |
| /*fenced_frame_reporter=*/fenced_frame_reporter, |
| /*is_ad_component=*/true); |
| } |
| if (base::FeatureList::IsEnabled( |
| blink::features::kFencedFramesM120FeaturesPart2)) { |
| // M120 and afterwards: The ad auction data is added to the nested configs |
| // in order to enable leaveAdInterestGroup() for ad components. |
| nested_configs.back().ad_auction_data_.emplace( |
| ad_auction_data, VisibilityToEmbedder::kOpaque, |
| VisibilityToContent::kOpaque); |
| } |
| } |
| config.nested_configs_.emplace(std::move(nested_configs), |
| VisibilityToEmbedder::kOpaque, |
| VisibilityToContent::kTransparent); |
| |
| config.fenced_frame_reporter_ = std::move(fenced_frame_reporter); |
| config.mode_ = blink::FencedFrame::DeprecatedFencedFrameMode::kOpaqueAds; |
| config.allows_information_inflow_ = false; |
| |
| return config.RedactFor(FencedFrameEntity::kEmbedder); |
| } |
| |
| std::optional<GURL> FencedFrameURLMapping::GeneratePendingMappedURN() { |
| if (IsFull()) { |
| return std::nullopt; |
| } |
| |
| GURL urn_uuid = GenerateUrnUuid(); |
| CHECK(!IsMapped(urn_uuid)); |
| CHECK(!IsPendingMapped(urn_uuid)); |
| |
| pending_urn_uuid_to_url_map_.emplace( |
| urn_uuid, std::set<raw_ptr<MappingResultObserver>>()); |
| return urn_uuid; |
| } |
| |
| void FencedFrameURLMapping::ConvertFencedFrameURNToURL( |
| const GURL& urn_uuid, |
| MappingResultObserver* observer) { |
| DCHECK(blink::IsValidUrnUuidURL(urn_uuid)); |
| |
| if (IsPendingMapped(urn_uuid)) { |
| DCHECK(!pending_urn_uuid_to_url_map_.at(urn_uuid).count(observer)); |
| pending_urn_uuid_to_url_map_.at(urn_uuid).emplace(observer); |
| return; |
| } |
| |
| std::optional<FencedFrameProperties> properties; |
| |
| auto it = urn_uuid_to_url_map_.find(urn_uuid); |
| if (it != urn_uuid_to_url_map_.end()) { |
| properties = FencedFrameProperties(it->second); |
| } |
| |
| if (properties.has_value() && properties->ad_auction_data().has_value()) { |
| base::UmaHistogramBoolean("Ads.InterestGroup.Auction.AdNavigationStarted", |
| true); |
| } |
| |
| observer->OnFencedFrameURLMappingComplete(properties); |
| } |
| |
| void FencedFrameURLMapping::RemoveObserverForURN( |
| const GURL& urn_uuid, |
| MappingResultObserver* observer) { |
| auto it = pending_urn_uuid_to_url_map_.find(urn_uuid); |
| if (it == pending_urn_uuid_to_url_map_.end()) { |
| // A harmless race condition may occur that the pending urn to url map has |
| // changed out from under the place that is calling this function (so the |
| // destructors were already called), so it's empty. |
| return; |
| } |
| |
| auto observer_it = it->second.find(observer); |
| if (observer_it == it->second.end()) { |
| // Similarly, the observer may not be associated with the urn. |
| return; |
| } |
| |
| it->second.erase(observer_it); |
| } |
| |
| std::optional<FencedFrameConfig> |
| FencedFrameURLMapping::OnSharedStorageURNMappingResultDetermined( |
| const GURL& urn_uuid, |
| const SharedStorageURNMappingResult& mapping_result) { |
| auto pending_it = pending_urn_uuid_to_url_map_.find(urn_uuid); |
| CHECK(pending_it != pending_urn_uuid_to_url_map_.end(), |
| base::NotFatalUntil::M130); |
| |
| DCHECK(!IsMapped(urn_uuid)); |
| |
| std::optional<FencedFrameConfig> config = std::nullopt; |
| |
| // Only if the resolved URL is fenced-frame-compatible do we: |
| // 1.) Add it to `urn_uuid_to_url_map_` |
| // 2.) Report it back to any already-queued observers |
| // TODO(crbug.com/40223071): Simplify this by making Shared Storage only |
| // capable of producing URLs that fenced frames can navigate to. |
| if (blink::IsValidFencedFrameURL(mapping_result.mapped_url)) { |
| config = FencedFrameConfig(urn_uuid, mapping_result.mapped_url, |
| mapping_result.budget_metadata, |
| std::move(mapping_result.fenced_frame_reporter)); |
| config->mode_ = blink::FencedFrame::DeprecatedFencedFrameMode::kOpaqueAds; |
| config->effective_enabled_permissions_ = { |
| std::begin(blink::kFencedFrameSharedStorageDefaultRequiredFeatures), |
| std::end(blink::kFencedFrameSharedStorageDefaultRequiredFeatures)}; |
| config->allows_information_inflow_ = true; |
| |
| urn_uuid_to_url_map_.emplace(urn_uuid, *config); |
| } |
| |
| std::set<raw_ptr<MappingResultObserver>>& observers = pending_it->second; |
| |
| std::optional<FencedFrameProperties> properties = std::nullopt; |
| auto final_it = urn_uuid_to_url_map_.find(urn_uuid); |
| if (final_it != urn_uuid_to_url_map_.end()) { |
| properties = FencedFrameProperties(final_it->second); |
| } |
| |
| for (MappingResultObserver* observer : observers) { |
| observer->OnFencedFrameURLMappingComplete(properties); |
| } |
| |
| pending_urn_uuid_to_url_map_.erase(pending_it); |
| |
| return config; |
| } |
| |
| SharedStorageBudgetMetadata* |
| FencedFrameURLMapping::GetSharedStorageBudgetMetadataForTesting( |
| const GURL& urn_uuid) { |
| auto it = urn_uuid_to_url_map_.find(urn_uuid); |
| CHECK(it != urn_uuid_to_url_map_.end(), base::NotFatalUntil::M130); |
| |
| if (!it->second.shared_storage_budget_metadata_) |
| return nullptr; |
| |
| 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; |
| } |
| FencedFrameConfig info = it->second; |
| if (info.mapped_url_.has_value()) { |
| GURL substituted_url = GURL(SubstituteMappedStrings( |
| it->second.mapped_url_->GetValueIgnoringVisibility().spec(), |
| substitutions)); |
| if (!substituted_url.is_valid()) { |
| return; |
| } |
| info.mapped_url_->value_ = substituted_url; |
| } |
| if (info.nested_configs_.has_value()) { |
| for (auto& nested_config : info.nested_configs_->value_) { |
| GURL substituted_url = GURL(SubstituteMappedStrings( |
| nested_config.mapped_url_->GetValueIgnoringVisibility().spec(), |
| substitutions)); |
| if (!substituted_url.is_valid()) { |
| return; |
| } |
| nested_config.mapped_url_->value_ = substituted_url; |
| } |
| } |
| it->second = std::move(info); |
| } |
| |
| bool FencedFrameURLMapping::IsMapped(const GURL& urn_uuid) const { |
| return base::Contains(urn_uuid_to_url_map_, urn_uuid); |
| } |
| |
| bool FencedFrameURLMapping::IsPendingMapped(const GURL& urn_uuid) const { |
| return base::Contains(pending_urn_uuid_to_url_map_, urn_uuid); |
| } |
| |
| bool FencedFrameURLMapping::IsFull() const { |
| return urn_uuid_to_url_map_.size() + pending_urn_uuid_to_url_map_.size() >= |
| kMaxUrnMappingSize; |
| } |
| |
| } // namespace content |