blob: 15599df4e912e048022329481e999420b1b51714 [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/supervised_user/classify_url_navigation_throttle.h"
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include "base/feature_list.h"
#include "base/metrics/histogram_functions.h"
#include "base/task/single_thread_task_runner.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/signin/identity_manager_factory.h"
#include "chrome/browser/signin/signin_manager.h"
#include "chrome/browser/supervised_user/child_accounts/child_account_service_factory.h"
#include "chrome/browser/supervised_user/supervised_user_browser_utils.h"
#include "chrome/browser/supervised_user/supervised_user_navigation_observer.h"
#include "chrome/browser/supervised_user/supervised_user_service_factory.h"
#include "chrome/browser/supervised_user/supervised_user_verification_page.h"
#include "components/signin/public/identity_manager/tribool.h"
#include "components/supervised_user/core/browser/child_account_service.h"
#include "components/supervised_user/core/browser/family_link_user_capabilities.h"
#include "components/supervised_user/core/browser/supervised_user_interstitial.h"
#include "components/supervised_user/core/browser/supervised_user_preferences.h"
#include "components/supervised_user/core/browser/supervised_user_service.h"
#include "components/supervised_user/core/browser/supervised_user_url_filter.h"
#include "components/supervised_user/core/browser/supervised_user_utils.h"
#include "components/supervised_user/core/common/features.h"
#include "components/supervised_user/core/common/supervised_user_constants.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/navigation_throttle.h"
#include "content/public/browser/web_contents.h"
#include "url/gurl.h"
namespace supervised_user {
namespace {
#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN)
bool ShouldShowReAuthInterstitial(
content::NavigationHandle& navigation_handle) {
Profile* profile = Profile::FromBrowserContext(
navigation_handle.GetWebContents()->GetBrowserContext());
ChildAccountService* child_account_service =
ChildAccountServiceFactory::GetForProfile(profile);
return SupervisedUserVerificationPage::ShouldShowPage(*child_account_service);
}
#endif
} // namespace
ClassifyUrlNavigationThrottle::ThrottleCheckResult
ClassifyUrlNavigationThrottle::WillProcessRequest() {
// We do not yet support prerendering for supervised users.
if (navigation_handle()->IsInPrerenderedMainFrame()) {
return CANCEL;
}
CheckURL();
// It is possible that check was synchronous. If that's the case,
// short-circuit and show the interstitial immediately, also breaking the
// redirect chain.
if (auto result = list_.GetBlockingResult(); result.has_value()) {
// Defer navigation for the duration of interstitial.
return DeferAndScheduleInterstitial(*result);
}
return PROCEED;
}
ClassifyUrlNavigationThrottle::ThrottleCheckResult
ClassifyUrlNavigationThrottle::WillStartRequest() {
return WillProcessRequest();
}
ClassifyUrlNavigationThrottle::ThrottleCheckResult
ClassifyUrlNavigationThrottle::WillRedirectRequest() {
return WillProcessRequest();
}
ClassifyUrlNavigationThrottle::ThrottleCheckResult
ClassifyUrlNavigationThrottle::WillProcessResponse() {
list_.MarkNavigationRequestsCompleted();
if (!list_.IsDecided()) {
// Defer navigation until checks are conclusive
waiting_for_decision_.emplace();
deferred_ = true;
return DEFER;
}
if (auto result = list_.GetBlockingResult(); result.has_value()) {
// Defer navigation for the duration of interstitial.
return DeferAndScheduleInterstitial(*result);
}
// All checks decided that it's safe to proceed.
base::UmaHistogramTimes(kClassifiedEarlierThanContentResponseHistogramName,
list_.ElapsedSinceDecided());
VLOG(1) << "Decision was ready ahead of time:" << list_.ElapsedSinceDecided();
base::UmaHistogramEnumeration(kClassifyUrlThrottleFinalStatusHistogramName,
ClassifyUrlThrottleFinalStatus::kAllowed);
return PROCEED;
}
void ClassifyUrlNavigationThrottle::CheckURL() {
const GURL& url = currently_navigated_url();
ClassifyUrlCheckList::Key key = list_.NewCheck();
if (navigation_handle()->IsInPrimaryMainFrame()) {
url_filter()->GetFilteringBehaviorWithAsyncChecks(
url,
base::BindOnce(&ClassifyUrlNavigationThrottle::OnURLCheckDone,
weak_ptr_factory_.GetWeakPtr(), key),
ShouldContentSkipParentAllowlistFiltering(
navigation_handle()->GetWebContents()->GetOutermostWebContents()),
FilteringContext::kNavigationThrottle,
navigation_handle()->GetPageTransition());
} else {
url_filter()->GetFilteringBehaviorForSubFrameWithAsyncChecks(
url, navigation_handle()->GetWebContents()->GetVisibleURL(),
base::BindOnce(&ClassifyUrlNavigationThrottle::OnURLCheckDone,
weak_ptr_factory_.GetWeakPtr(), key),
FilteringContext::kNavigationThrottle,
navigation_handle()->GetPageTransition());
}
}
void ClassifyUrlNavigationThrottle::OnURLCheckDone(
ClassifyUrlCheckList::Key key,
SupervisedUserURLFilter::Result filtering_result) {
if (list_.IsDecided()) {
// If the verdict is already determined there's no point in processing the
// check. This will reduce noise in metrics, but side-effects might apply
// (eg. populating classification cache).
return;
}
// Updates the check results. This invalidates the InPending state.
list_.UpdateCheck(key, filtering_result);
if (!list_.IsDecided()) {
// Stop right here. More checks need to complete to know if navigation
// should be deferred or interstitial presented.
return;
}
// Checks are completed before needed
if (!deferred_) {
// If behavior == FilteringBehavior::kAllow then WillProcessResponse will
// eventually pick up. Otherwise, if the call is synchronous, the calling
// request or redirect event will test if the navigation should be blocked
// immediately.
return;
}
// Checks are completed after they were needed by WillProcessResponse.
if (auto blocking_result = list_.GetBlockingResult();
blocking_result.has_value()) {
ScheduleInterstitial(*blocking_result);
} else {
base::UmaHistogramTimes(kClassifiedLaterThanContentResponseHistogramName,
waiting_for_decision_->Elapsed());
VLOG(1) << "Had to delay decision:" << waiting_for_decision_->Elapsed();
base::UmaHistogramEnumeration(kClassifyUrlThrottleFinalStatusHistogramName,
ClassifyUrlThrottleFinalStatus::kAllowed);
Resume();
}
}
void ClassifyUrlNavigationThrottle::ScheduleInterstitial(
SupervisedUserURLFilter::Result result) {
// Don't show interstitial synchronously - it doesn't seem like a good idea to
// show an interstitial right in the middle of a call into a
// NavigationThrottle. This also lets OnInterstitialResult to be invoked
// synchronously, once a callback is passed into the
// SupervisedUserNavigationObserver.
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(&ClassifyUrlNavigationThrottle::ShowInterstitial,
weak_ptr_factory_.GetWeakPtr(), result));
}
void ClassifyUrlNavigationThrottle::ShowInterstitial(
SupervisedUserURLFilter::Result result) {
SupervisedUserNavigationObserver::OnRequestBlocked(
navigation_handle()->GetWebContents(), result.url, result.reason,
navigation_handle()->GetNavigationId(),
navigation_handle()->GetFrameTreeNodeId(),
base::BindRepeating(&ClassifyUrlNavigationThrottle::OnInterstitialResult,
weak_ptr_factory_.GetWeakPtr(), result));
}
void ClassifyUrlNavigationThrottle::OnInterstitialResult(
SupervisedUserURLFilter::Result result,
InterstitialResultCallbackActions action,
bool already_sent_request,
bool is_main_frame) {
switch (action) {
case InterstitialResultCallbackActions::kCancelNavigation: {
CancelDeferredNavigation(CANCEL);
break;
}
case InterstitialResultCallbackActions::kCancelWithInterstitial: {
CHECK(navigation_handle());
#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN)
if (ShouldShowReAuthInterstitial(*navigation_handle())) {
// Show the re-authentication interstitial if the user signed out of
// the content area, as parent's approval requires authentication.
// This interstitial is only available on Linux/Mac/Windows as
// ChromeOS and Android have different re-auth mechanisms.
CancelDeferredNavigation(
content::NavigationThrottle::ThrottleCheckResult(
CANCEL, net::ERR_BLOCKED_BY_CLIENT,
CreateReauthenticationInterstitialForBlockedSites(
*navigation_handle(), result.reason)));
return;
}
#endif
CancelDeferredNavigation(content::NavigationThrottle::ThrottleCheckResult(
CANCEL, net::ERR_BLOCKED_BY_CLIENT,
GetInterstitialHTML(result, already_sent_request, is_main_frame)));
break;
}
}
}
std::string ClassifyUrlNavigationThrottle::GetInterstitialHTML(
SupervisedUserURLFilter::Result result,
bool already_sent_request,
bool is_main_frame) const {
#if BUILDFLAG(IS_ANDROID)
if (supervised_user_service()->IsLocalBrowserFilteringEnabled() &&
UseInterstitialForLocalSupervision()) {
return SupervisedUserInterstitial::GetHTMLContentsWithoutApprovals(
result.url, g_browser_process->GetApplicationLocale());
}
#endif
Profile* profile = Profile::FromBrowserContext(
navigation_handle()->GetWebContents()->GetBrowserContext());
return SupervisedUserInterstitial::GetHTMLContentsWithApprovals(
supervised_user_service(), profile->GetPrefs(), result.reason,
already_sent_request, is_main_frame,
g_browser_process->GetApplicationLocale());
}
const GURL& ClassifyUrlNavigationThrottle::currently_navigated_url() const {
return navigation_handle()->GetURL();
}
SupervisedUserURLFilter* ClassifyUrlNavigationThrottle::url_filter() const {
return supervised_user_service()->GetURLFilter();
}
SupervisedUserService* ClassifyUrlNavigationThrottle::supervised_user_service()
const {
return SupervisedUserServiceFactory::GetForProfile(
Profile::FromBrowserContext(
navigation_handle()->GetWebContents()->GetBrowserContext()));
}
void ClassifyUrlNavigationThrottle::MaybeCreateAndAdd(
content::NavigationThrottleRegistry& registry) {
Profile* profile = Profile::FromBrowserContext(
registry.GetNavigationHandle().GetWebContents()->GetBrowserContext());
// Off the record profiles don't have the infrastructure to support the
// ClassifyUrlNavigationThrottle, so we should not add it.
if (profile->IsOffTheRecord()) {
return;
}
// This check is not making logical difference as the throttle would allow
// this navigation anyway, but in this case no metrics will be recorded.
if (SupervisedUserServiceFactory::GetInstance()
->GetForProfile(profile)
->GetURLFilter()
->GetWebFilterType() == WebFilterType::kDisabled) {
return;
}
registry.AddThrottle(
base::WrapUnique(new ClassifyUrlNavigationThrottle(registry)));
}
ClassifyUrlNavigationThrottle::ThrottleCheckResult
ClassifyUrlNavigationThrottle::DeferAndScheduleInterstitial(
SupervisedUserURLFilter::Result result) {
ScheduleInterstitial(result);
deferred_ = true;
return DEFER;
}
void ClassifyUrlNavigationThrottle::CancelDeferredNavigation(
ThrottleCheckResult result) {
base::UmaHistogramEnumeration(kClassifyUrlThrottleFinalStatusHistogramName,
ClassifyUrlThrottleFinalStatus::kBlocked);
content::NavigationThrottle::CancelDeferredNavigation(result);
}
const char* ClassifyUrlNavigationThrottle::GetNameForLogging() {
return "ClassifyUrlNavigationThrottle";
}
ClassifyUrlNavigationThrottle::ClassifyUrlNavigationThrottle(
content::NavigationThrottleRegistry& registry)
: content::NavigationThrottle(registry) {}
ClassifyUrlNavigationThrottle::~ClassifyUrlNavigationThrottle() = default;
ClassifyUrlNavigationThrottle::ClassifyUrlCheckList::ClassifyUrlCheckList() =
default;
ClassifyUrlNavigationThrottle::ClassifyUrlCheckList::~ClassifyUrlCheckList() =
default;
void ClassifyUrlNavigationThrottle::ClassifyUrlCheckList::
MarkNavigationRequestsCompleted() {
CHECK(!new_checks_disabled_);
new_checks_disabled_ = true;
}
ClassifyUrlNavigationThrottle::ClassifyUrlCheckList::Key
ClassifyUrlNavigationThrottle::ClassifyUrlCheckList::NewCheck() {
CHECK(!new_checks_disabled_) << "Can't add new checks after sealing";
results_.emplace_back();
return results_.size() - 1;
}
void ClassifyUrlNavigationThrottle::ClassifyUrlCheckList::UpdateCheck(
Key key,
SupervisedUserURLFilter::Result result) {
// Every time a check is completed update the timer, so that it only measures
// elapsed time from the last meaningful check to when the verdict was needed.
elapsed_.emplace();
results_[key] = result;
}
base::TimeDelta
ClassifyUrlNavigationThrottle::ClassifyUrlCheckList::ElapsedSinceDecided()
const {
return elapsed_->Elapsed();
}
std::optional<SupervisedUserURLFilter::Result>
ClassifyUrlNavigationThrottle::ClassifyUrlCheckList::GetBlockingResult() const {
for (const auto& result : results_) {
if (!result.has_value()) {
return std::nullopt;
}
if (result->IsBlocked()) {
return result;
}
}
return std::nullopt;
}
bool ClassifyUrlNavigationThrottle::ClassifyUrlCheckList::IsDecided() const {
// Overall verdict is pending when an individual check is pending,
// or all received checks are "allowed" but more checks can be scheduled.
for (const auto& result : results_) {
if (!result.has_value()) {
return false;
}
if (result->IsBlocked()) {
return true;
}
}
return new_checks_disabled_;
}
} // namespace supervised_user