| // Copyright 2016 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/subresource_filter/content/renderer/subresource_filter_agent.h" |
| |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/check.h" |
| #include "base/feature_list.h" |
| #include "base/memory/ref_counted.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/time/time.h" |
| #include "components/subresource_filter/content/common/subresource_filter_utils.h" |
| #include "components/subresource_filter/content/renderer/unverified_ruleset_dealer.h" |
| #include "components/subresource_filter/content/renderer/web_document_subresource_filter_impl.h" |
| #include "components/subresource_filter/core/common/document_subresource_filter.h" |
| #include "components/subresource_filter/core/common/memory_mapped_ruleset.h" |
| #include "components/subresource_filter/core/common/scoped_timers.h" |
| #include "components/subresource_filter/core/common/time_measurements.h" |
| #include "content/public/common/content_features.h" |
| #include "content/public/common/url_constants.h" |
| #include "content/public/renderer/render_frame.h" |
| #include "ipc/ipc_message.h" |
| #include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h" |
| #include "third_party/blink/public/common/associated_interfaces/associated_interface_registry.h" |
| #include "third_party/blink/public/common/frame/frame_ad_evidence.h" |
| #include "third_party/blink/public/platform/web_worker_fetch_context.h" |
| #include "third_party/blink/public/web/web_document.h" |
| #include "third_party/blink/public/web/web_document_loader.h" |
| #include "third_party/blink/public/web/web_local_frame.h" |
| #include "url/url_constants.h" |
| |
| namespace subresource_filter { |
| |
| SubresourceFilterAgent::SubresourceFilterAgent( |
| content::RenderFrame* render_frame, |
| UnverifiedRulesetDealer* ruleset_dealer, |
| std::unique_ptr<AdResourceTracker> ad_resource_tracker) |
| : content::RenderFrameObserver(render_frame), |
| content::RenderFrameObserverTracker<SubresourceFilterAgent>(render_frame), |
| ruleset_dealer_(ruleset_dealer), |
| ad_resource_tracker_(std::move(ad_resource_tracker)) { |
| DCHECK(ruleset_dealer); |
| } |
| |
| void SubresourceFilterAgent::Initialize() { |
| const GURL& url = GetDocumentURL(); |
| // The initial empty document will always inherit activation. |
| DCHECK(ShouldInheritActivation(url)); |
| |
| // We must check for provisional here because in that case 2 RenderFrames will |
| // be created for the same FrameTreeNode in the browser. The browser service |
| // only expects us to call SendSubframeWasCreatedByAdScript() and |
| // SendFrameIsAdSubframe() a single time each for a newly created RenderFrame, |
| // so we must choose one. A provisional frame is created when a navigation is |
| // performed cross-site and the navigation is done there to isolate it from |
| // the previous frame tree. We choose to send this message from the initial |
| // (non-provisional) "about:blank" frame that is created before the navigation |
| // to match previous behaviour, and because this frame will always exist. |
| // Whereas the provisional frame would only be created to perform the |
| // navigation conditionally, so we ignore sending the IPC there. |
| if (!IsMainFrame() && !IsProvisional()) { |
| if (IsSubframeCreatedByAdScript()) |
| SendSubframeWasCreatedByAdScript(); |
| |
| // As this is the initial empty document, we won't have received any message |
| // from the browser and so we must populate the ad evidence here. |
| SetAdEvidenceForInitialEmptySubframe(); |
| } |
| |
| // `render_frame()` can be null in unit tests. |
| if (render_frame()) { |
| render_frame()->GetAssociatedInterfaceRegistry()->AddInterface( |
| base::BindRepeating( |
| &SubresourceFilterAgent::OnSubresourceFilterAgentRequest, |
| base::Unretained(this))); |
| |
| if (IsMainFrame()) { |
| // If a main frame has an activated opener, we will activate the |
| // subresource filter for the initial empty document, which was created |
| // before the constructor for `this`. This ensures that a popup's final |
| // document is appropriately activated, even when the the initial |
| // navigation is aborted and there are no further documents created. |
| // TODO(dcheng): Navigation is an asynchronous operation, and the opener |
| // frame may have been destroyed between the time the window is opened |
| // and the RenderFrame in the window is constructed leading us to here. |
| // To avoid that race condition the activation state would need to be |
| // determined without the use of the opener frame. |
| if (GetInheritedActivationState(render_frame()).activation_level != |
| mojom::ActivationLevel::kDisabled) { |
| ConstructFilter(GetInheritedActivationStateForNewDocument(), url); |
| } |
| } else { |
| // Child frames always have a parent, so the empty initial document can |
| // always inherit activation. |
| ConstructFilter(GetInheritedActivationStateForNewDocument(), url); |
| } |
| } |
| } |
| |
| SubresourceFilterAgent::~SubresourceFilterAgent() { |
| // Filter may outlive us, so reset the ad tracker. |
| if (filter_for_last_created_document_) |
| filter_for_last_created_document_->set_ad_resource_tracker(nullptr); |
| } |
| |
| GURL SubresourceFilterAgent::GetDocumentURL() { |
| return render_frame()->GetWebFrame()->GetDocument().Url(); |
| } |
| |
| bool SubresourceFilterAgent::IsMainFrame() { |
| return render_frame()->IsMainFrame(); |
| } |
| |
| bool SubresourceFilterAgent::IsParentAdSubframe() { |
| return render_frame()->GetWebFrame()->Parent()->IsAdSubframe(); |
| } |
| |
| bool SubresourceFilterAgent::IsProvisional() { |
| return render_frame()->GetWebFrame()->IsProvisional(); |
| } |
| |
| bool SubresourceFilterAgent::IsSubframeCreatedByAdScript() { |
| return render_frame()->GetWebFrame()->IsSubframeCreatedByAdScript(); |
| } |
| |
| void SubresourceFilterAgent::SetSubresourceFilterForCurrentDocument( |
| std::unique_ptr<blink::WebDocumentSubresourceFilter> filter) { |
| blink::WebLocalFrame* web_frame = render_frame()->GetWebFrame(); |
| DCHECK(web_frame->GetDocumentLoader()); |
| web_frame->GetDocumentLoader()->SetSubresourceFilter(filter.release()); |
| } |
| |
| void SubresourceFilterAgent:: |
| SignalFirstSubresourceDisallowedForCurrentDocument() { |
| GetSubresourceFilterHost()->DidDisallowFirstSubresource(); |
| } |
| |
| void SubresourceFilterAgent::SendDocumentLoadStatistics( |
| const mojom::DocumentLoadStatistics& statistics) { |
| GetSubresourceFilterHost()->SetDocumentLoadStatistics(statistics.Clone()); |
| } |
| |
| void SubresourceFilterAgent::SendFrameIsAdSubframe() { |
| GetSubresourceFilterHost()->FrameIsAdSubframe(); |
| } |
| |
| void SubresourceFilterAgent::SendSubframeWasCreatedByAdScript() { |
| GetSubresourceFilterHost()->SubframeWasCreatedByAdScript(); |
| } |
| |
| bool SubresourceFilterAgent::IsAdSubframe() { |
| return render_frame()->GetWebFrame()->IsAdSubframe(); |
| } |
| |
| void SubresourceFilterAgent::SetAdEvidence( |
| const blink::FrameAdEvidence& ad_evidence) { |
| render_frame()->GetWebFrame()->SetAdEvidence(ad_evidence); |
| } |
| |
| const absl::optional<blink::FrameAdEvidence>& |
| SubresourceFilterAgent::AdEvidence() { |
| return render_frame()->GetWebFrame()->AdEvidence(); |
| } |
| |
| // static |
| mojom::ActivationState SubresourceFilterAgent::GetInheritedActivationState( |
| content::RenderFrame* render_frame) { |
| if (!render_frame) |
| return mojom::ActivationState(); |
| |
| blink::WebFrame* frame_to_inherit_from = |
| render_frame->IsMainFrame() ? render_frame->GetWebFrame()->Opener() |
| : render_frame->GetWebFrame()->Parent(); |
| |
| if (!frame_to_inherit_from || !frame_to_inherit_from->IsWebLocalFrame()) |
| return mojom::ActivationState(); |
| |
| blink::WebSecurityOrigin render_frame_origin = |
| render_frame->GetWebFrame()->GetSecurityOrigin(); |
| blink::WebSecurityOrigin inherited_origin = |
| frame_to_inherit_from->GetSecurityOrigin(); |
| |
| // Only inherit from same-origin frames. |
| if (render_frame_origin.IsSameOriginWith(inherited_origin)) { |
| auto* agent = |
| SubresourceFilterAgent::Get(content::RenderFrame::FromWebFrame( |
| frame_to_inherit_from->ToWebLocalFrame())); |
| if (agent && agent->filter_for_last_created_document_) |
| return agent->filter_for_last_created_document_->activation_state(); |
| } |
| |
| return mojom::ActivationState(); |
| } |
| |
| void SubresourceFilterAgent::RecordHistogramsOnFilterCreation( |
| const mojom::ActivationState& activation_state) { |
| // Note: mojom::ActivationLevel used to be called mojom::ActivationState, the |
| // legacy name is kept for the histogram. |
| mojom::ActivationLevel activation_level = activation_state.activation_level; |
| UMA_HISTOGRAM_ENUMERATION("SubresourceFilter.DocumentLoad.ActivationState", |
| activation_level); |
| |
| if (IsMainFrame()) { |
| UMA_HISTOGRAM_BOOLEAN( |
| "SubresourceFilter.MainFrameLoad.RulesetIsAvailableAnyActivationLevel", |
| ruleset_dealer_->IsRulesetFileAvailable()); |
| } |
| if (activation_level != mojom::ActivationLevel::kDisabled) { |
| UMA_HISTOGRAM_BOOLEAN("SubresourceFilter.DocumentLoad.RulesetIsAvailable", |
| ruleset_dealer_->IsRulesetFileAvailable()); |
| } |
| } |
| |
| void SubresourceFilterAgent::ResetInfoForNextDocument() { |
| activation_state_for_next_document_ = mojom::ActivationState(); |
| } |
| |
| mojom::SubresourceFilterHost* |
| SubresourceFilterAgent::GetSubresourceFilterHost() { |
| if (!subresource_filter_host_) { |
| render_frame()->GetRemoteAssociatedInterfaces()->GetInterface( |
| &subresource_filter_host_); |
| } |
| return subresource_filter_host_.get(); |
| } |
| |
| void SubresourceFilterAgent::OnSubresourceFilterAgentRequest( |
| mojo::PendingAssociatedReceiver<mojom::SubresourceFilterAgent> receiver) { |
| receiver_.reset(); |
| receiver_.Bind(std::move(receiver)); |
| } |
| |
| void SubresourceFilterAgent::ActivateForNextCommittedLoad( |
| mojom::ActivationStatePtr activation_state, |
| const absl::optional<blink::FrameAdEvidence>& ad_evidence) { |
| activation_state_for_next_document_ = *activation_state; |
| if (!IsMainFrame()) { |
| DCHECK(ad_evidence.has_value()); |
| SetAdEvidence(ad_evidence.value()); |
| } else { |
| DCHECK(!ad_evidence.has_value()); |
| } |
| } |
| |
| void SubresourceFilterAgent::OnDestruct() { |
| delete this; |
| } |
| |
| void SubresourceFilterAgent::SetAdEvidenceForInitialEmptySubframe() { |
| DCHECK(!IsAdSubframe()); |
| DCHECK(!AdEvidence().has_value()); |
| |
| blink::FrameAdEvidence ad_evidence(IsParentAdSubframe()); |
| ad_evidence.set_created_by_ad_script( |
| IsSubframeCreatedByAdScript() |
| ? blink::mojom::FrameCreationStackEvidence::kCreatedByAdScript |
| : blink::mojom::FrameCreationStackEvidence::kNotCreatedByAdScript); |
| ad_evidence.set_is_complete(); |
| SetAdEvidence(ad_evidence); |
| |
| if (ad_evidence.IndicatesAdSubframe()) { |
| SendFrameIsAdSubframe(); |
| } |
| } |
| |
| void SubresourceFilterAgent::DidCreateNewDocument() { |
| // TODO(csharrison): Use WebURL and WebSecurityOrigin for efficiency here, |
| // which requires changes to the unit tests. |
| const GURL& url = GetDocumentURL(); |
| |
| // A new browser-side host is created for each new page (i.e. new main frame |
| // document) so we have to reset the remote so we re-bind on the next |
| // message. |
| if (IsMainFrame()) |
| subresource_filter_host_.reset(); |
| |
| const mojom::ActivationState activation_state = |
| ShouldInheritActivation(url) ? GetInheritedActivationStateForNewDocument() |
| : activation_state_for_next_document_; |
| |
| ResetInfoForNextDocument(); |
| |
| // Do not pollute the histograms with uninteresting main frame documents. |
| const bool should_record_histograms = |
| !IsMainFrame() || url.SchemeIsHTTPOrHTTPS() || url.SchemeIsFile(); |
| if (should_record_histograms) { |
| RecordHistogramsOnFilterCreation(activation_state); |
| } |
| |
| ConstructFilter(activation_state, url); |
| } |
| |
| const mojom::ActivationState |
| SubresourceFilterAgent::GetInheritedActivationStateForNewDocument() { |
| DCHECK(ShouldInheritActivation(GetDocumentURL())); |
| return GetInheritedActivationState(render_frame()); |
| } |
| |
| void SubresourceFilterAgent::ConstructFilter( |
| const mojom::ActivationState activation_state, |
| const GURL& url) { |
| // Filter may outlive us, so reset the ad tracker. |
| if (filter_for_last_created_document_) |
| filter_for_last_created_document_->set_ad_resource_tracker(nullptr); |
| filter_for_last_created_document_.reset(); |
| |
| if (activation_state.activation_level == mojom::ActivationLevel::kDisabled || |
| !ruleset_dealer_->IsRulesetFileAvailable()) |
| return; |
| |
| scoped_refptr<const MemoryMappedRuleset> ruleset = |
| ruleset_dealer_->GetRuleset(); |
| if (!ruleset) |
| return; |
| |
| base::OnceClosure first_disallowed_load_callback( |
| base::BindOnce(&SubresourceFilterAgent:: |
| SignalFirstSubresourceDisallowedForCurrentDocument, |
| AsWeakPtr())); |
| auto filter = std::make_unique<WebDocumentSubresourceFilterImpl>( |
| url::Origin::Create(url), activation_state, std::move(ruleset), |
| std::move(first_disallowed_load_callback)); |
| filter->set_ad_resource_tracker(ad_resource_tracker_.get()); |
| filter_for_last_created_document_ = filter->AsWeakPtr(); |
| SetSubresourceFilterForCurrentDocument(std::move(filter)); |
| } |
| |
| void SubresourceFilterAgent::DidFailProvisionalLoad() { |
| // TODO(engedy): Add a test with `frame-ancestor` violation to exercise this. |
| ResetInfoForNextDocument(); |
| } |
| |
| void SubresourceFilterAgent::DidFinishLoad() { |
| if (!filter_for_last_created_document_) |
| return; |
| const auto& statistics = |
| filter_for_last_created_document_->filter().statistics(); |
| SendDocumentLoadStatistics(statistics); |
| } |
| |
| void SubresourceFilterAgent::WillCreateWorkerFetchContext( |
| blink::WebWorkerFetchContext* worker_fetch_context) { |
| if (!filter_for_last_created_document_) |
| return; |
| if (!ruleset_dealer_->IsRulesetFileAvailable()) |
| return; |
| base::File ruleset_file = ruleset_dealer_->DuplicateRulesetFile(); |
| if (!ruleset_file.IsValid()) |
| return; |
| |
| worker_fetch_context->SetSubresourceFilterBuilder( |
| std::make_unique<WebDocumentSubresourceFilterImpl::BuilderImpl>( |
| url::Origin::Create(GetDocumentURL()), |
| filter_for_last_created_document_->filter().activation_state(), |
| std::move(ruleset_file), |
| base::BindOnce(&SubresourceFilterAgent:: |
| SignalFirstSubresourceDisallowedForCurrentDocument, |
| AsWeakPtr()))); |
| } |
| |
| void SubresourceFilterAgent::OnOverlayPopupAdDetected() { |
| GetSubresourceFilterHost()->OnAdsViolationTriggered( |
| subresource_filter::mojom::AdsViolation::kOverlayPopupAd); |
| } |
| void SubresourceFilterAgent::OnLargeStickyAdDetected() { |
| GetSubresourceFilterHost()->OnAdsViolationTriggered( |
| subresource_filter::mojom::AdsViolation::kLargeStickyAd); |
| } |
| |
| } // namespace subresource_filter |