// Copyright 2014 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/supervised_user_navigation_observer.h"

#include <memory>
#include <string>
#include <utility>

#include "base/check.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/favicon/large_icon_service_factory.h"
#include "chrome/browser/global_features.h"
#include "chrome/browser/history/history_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_key.h"
#include "chrome/browser/supervised_user/classify_url_navigation_throttle.h"
#include "chrome/browser/supervised_user/family_link_settings_service_factory.h"
#include "chrome/browser/supervised_user/supervised_user_browser_utils.h"
#include "chrome/browser/supervised_user/supervised_user_service_factory.h"
#include "chrome/browser/supervised_user/supervised_user_url_filtering_service_factory.h"
#include "components/favicon/core/large_icon_service.h"
#include "components/google/core/common/google_util.h"
#include "components/history/content/browser/history_context_helper.h"
#include "components/history/core/browser/history_service.h"
#include "components/history/core/browser/history_types.h"
#include "components/policy/core/common/policy_pref_names.h"
#include "components/sessions/content/content_serialized_navigation_builder.h"
#include "components/supervised_user/core/browser/family_link_settings_service.h"
#include "components/supervised_user/core/browser/supervised_user_interstitial.h"
#include "components/supervised_user/core/browser/supervised_user_service.h"
#include "components/supervised_user/core/browser/supervised_user_url_filtering_service.h"
#include "components/supervised_user/core/browser/web_content_handler.h"
#include "components/supervised_user/core/common/features.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/reload_type.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/web_contents.h"
#include "services/metrics/public/cpp/metrics_utils.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/metrics/public/cpp/ukm_recorder.h"
#include "url/gurl.h"

#if BUILDFLAG(IS_ANDROID)
#include "chrome/browser/supervised_user/android/supervised_user_web_content_handler_impl.h"
#include "components/supervised_user/core/browser/android/android_parental_controls.h"
#elif BUILDFLAG(IS_CHROMEOS)
#include "chrome/browser/supervised_user/chromeos/supervised_user_web_content_handler_impl.h"
#elif BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_WIN)
#include "chrome/browser/supervised_user/linux_mac_windows/supervised_user_web_content_handler_impl.h"
#endif

namespace {

std::unique_ptr<supervised_user::WebContentHandler> CreateWebContentHandler(
    content::WebContents* web_contents,
    GURL url,
    Profile* profile,
    content::FrameTreeNodeId frame_id,
    int navigation_id) {
#if BUILDFLAG(IS_CHROMEOS)
  return std::make_unique<SupervisedUserWebContentHandlerImpl>(
      web_contents, url,
      *LargeIconServiceFactory::GetForBrowserContext(profile), frame_id,
      navigation_id);
#elif BUILDFLAG(IS_ANDROID)
  return std::make_unique<SupervisedUserWebContentHandlerImpl>(
      web_contents, frame_id, navigation_id);
#elif BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_WIN)
  return std::make_unique<SupervisedUserWebContentHandlerImpl>(
      web_contents, frame_id, navigation_id);
#endif
}

static constexpr supervised_user::WebFilterMetricsOptions
    kWebFilterMetricsOptionsForNavigationObserver = {
        .filtering_context =
            supervised_user::FilteringContext::kNavigationObserver};
static constexpr supervised_user::WebFilterMetricsOptions
    kWebFilterMetricsOptionsForFamilyLinkSettingsUpdated = {
        .filtering_context =
            supervised_user::FilteringContext::kFamilyLinkSettingsUpdated};

}  // namespace

using content::NavigationEntry;

SupervisedUserNavigationObserver::~SupervisedUserNavigationObserver() = default;

SupervisedUserNavigationObserver::SupervisedUserNavigationObserver(
    content::WebContents* web_contents)
    : content::WebContentsUserData<SupervisedUserNavigationObserver>(
          *web_contents),
      content::WebContentsObserver(web_contents),
      receivers_(web_contents, this) {
  url_filtering_service_observation_.Observe(
      supervised_user_url_filtering_service());

#if BUILDFLAG(IS_ANDROID)
  // TODO(crbug.com/543033880): Extract this feature to a separate class.
  Profile* profile =
      Profile::FromBrowserContext(web_contents->GetBrowserContext());

  pref_change_registrar_.Init(profile->GetPrefs());
  pref_change_registrar_.Add(
      policy::policy_prefs::kForceGoogleSafeSearch,
      base::BindRepeating(
          &SupervisedUserNavigationObserver::OnForceGoogleSafeSearchChanged,
          base::Unretained(this)));
#endif
}

// static
void SupervisedUserNavigationObserver::BindSupervisedUserCommands(
    mojo::PendingAssociatedReceiver<
        supervised_user::mojom::SupervisedUserCommands> receiver,
    content::RenderFrameHost* rfh) {
  auto* web_contents = content::WebContents::FromRenderFrameHost(rfh);
  if (!web_contents) {
    return;
  }
  auto* navigation_observer =
      SupervisedUserNavigationObserver::FromWebContents(web_contents);
  if (!navigation_observer) {
    return;
  }
  navigation_observer->receivers_.Bind(rfh, std::move(receiver));
}

// static
void SupervisedUserNavigationObserver::OnRequestBlocked(
    content::WebContents* web_contents,
    supervised_user::WebFilteringResult filtering_result,
    int64_t navigation_id,
    content::FrameTreeNodeId frame_id,
    const OnInterstitialResultCallback& callback) {
  SupervisedUserNavigationObserver* navigation_observer =
      SupervisedUserNavigationObserver::FromWebContents(web_contents);

  // Cancel the navigation if there is no navigation observer.
  if (!navigation_observer) {
    callback.Run(
        supervised_user::InterstitialResultCallbackActions::kCancelNavigation,
        /*already_requested_permission=*/false, /*is_main_frame=*/false);
    return;
  }

  navigation_observer->OnRequestBlockedInternal(filtering_result, navigation_id,
                                                frame_id, callback);
}

void SupervisedUserNavigationObserver::DidFinishNavigation(
    content::NavigationHandle* navigation_handle) {
  if (!navigation_handle->HasCommitted()) {
    return;
  }

  content::FrameTreeNodeId frame_id = navigation_handle->GetFrameTreeNodeId();
  int64_t navigation_id = navigation_handle->GetNavigationId();

  // If this is a different navigation than the one that triggered the
  // interstitial in the frame, then interstitial is done.
  if (supervised_user_interstitials_.contains(frame_id) &&
      navigation_id != supervised_user_interstitials_[frame_id]
                           ->web_content_handler()
                           ->GetInterstitialNavigationId()) {
    OnInterstitialDone(frame_id);
  }

  // Only filter same page navigations (eg. pushState/popState); others will
  // have been filtered by the NavigationThrottle.
  if (navigation_handle->IsSameDocument() &&
      navigation_handle->IsInPrimaryMainFrame()) {
    auto* render_frame_host = web_contents()->GetPrimaryMainFrame();
    int process_id = render_frame_host->GetProcess()->GetDeprecatedID();
    int routing_id = render_frame_host->GetRoutingID();
    bool skip_manual_parent_filter =
        supervised_user::ShouldContentSkipParentAllowlistFiltering(
            web_contents());
    supervised_user_url_filtering_service()->GetFilteringBehavior(
        web_contents()->GetLastCommittedURL(), skip_manual_parent_filter,
        base::BindOnce(
            &SupervisedUserNavigationObserver::URLFilterCheckCallback,
            weak_ptr_factory_.GetWeakPtr(), process_id, routing_id),
        kWebFilterMetricsOptionsForNavigationObserver);
  }
}

void SupervisedUserNavigationObserver::FrameDeleted(
    content::FrameTreeNodeId frame_tree_node_id) {
  supervised_user_interstitials_.erase(frame_tree_node_id);
}

void SupervisedUserNavigationObserver::DidFinishLoad(
    content::RenderFrameHost* render_frame_host,
    const GURL& validated_url) {
  if (render_frame_host->IsInPrimaryMainFrame()) {
    bool main_frame_blocked = supervised_user_interstitials_.contains(
        render_frame_host->GetFrameTreeNodeId());
    int count = supervised_user_interstitials_.size();
    if (main_frame_blocked) {
      count = 0;
    }

    UMA_HISTOGRAM_COUNTS_1000("ManagedUsers.BlockedIframeCount", count);
    RecordPageLoadUKM(render_frame_host);
  }
}

void SupervisedUserNavigationObserver::RecordPageLoadUKM(
    content::RenderFrameHost* render_frame_host) {
  ukm::UkmRecorder* ukm_recorder = ukm::UkmRecorder::Get();
  ukm::SourceId source_id = render_frame_host->GetPageUkmSourceId();

  // To avoid the user potentially being identified based on parent-configured
  // allow/block lists, only output a UKM for page loads that were blocked or
  // partially blocked due to the async checks (but not due to allow/block list
  // configuration).
  const content::FrameTreeNodeId main_frame_id =
      render_frame_host->GetFrameTreeNodeId();
  if (supervised_user_interstitials_.contains(main_frame_id)) {
    // The main frame was blocked.
    if (supervised_user_interstitials_[main_frame_id]
            ->filtering_result()
            .reason ==
        supervised_user::FilteringBehaviorReason::ASYNC_CHECKER) {
      ukm::builders::FamilyLinkUser_BlockedContent(source_id)
          .SetMainFrameBlocked(true)
          .SetNumBlockedIframes(ukm::GetExponentialBucketMinForCounts1000(0))
          .Record(ukm_recorder);
    }
  } else {
    // The main frame was not blocked. Check for any blocked iframes.
    size_t blocked_frame_count = std::ranges::count_if(
        supervised_user_interstitials_, [](const auto& entry) {
          return entry.second->filtering_result().reason ==
                 supervised_user::FilteringBehaviorReason::ASYNC_CHECKER;
        });

    // If there were any blocked iframes, output a UKM.
    if (blocked_frame_count > 0) {
      ukm::builders::FamilyLinkUser_BlockedContent(source_id)
          .SetMainFrameBlocked(false)
          .SetNumBlockedIframes(
              ukm::GetExponentialBucketMinForCounts1000(blocked_frame_count))
          .Record(ukm_recorder);
    }
  }
}

void SupervisedUserNavigationObserver::OnUrlFilteringServiceChanged() {
  auto* main_frame = web_contents()->GetPrimaryMainFrame();
  int main_frame_process_id = main_frame->GetProcess()->GetDeprecatedID();
  int routing_id = main_frame->GetRoutingID();
  bool skip_manual_parent_filter =
      supervised_user::ShouldContentSkipParentAllowlistFiltering(
          web_contents());

  supervised_user_url_filtering_service()->GetFilteringBehavior(
      web_contents()->GetLastCommittedURL(), skip_manual_parent_filter,
      base::BindOnce(&SupervisedUserNavigationObserver::URLFilterCheckCallback,
                     weak_ptr_factory_.GetWeakPtr(), main_frame_process_id,
                     routing_id),
      kWebFilterMetricsOptionsForFamilyLinkSettingsUpdated);

  MaybeUpdateRequestedHosts();

  // Iframe filtering has been enabled.
  main_frame->ForEachRenderFrameHost(
      [this](content::RenderFrameHost* render_frame_host) {
        FilterRenderFrame(render_frame_host);
      });
}

#if BUILDFLAG(IS_ANDROID)
void SupervisedUserNavigationObserver::OnForceGoogleSafeSearchChanged(
    std::string_view safe_search_pref_name) {
  // Reloads the current page when all conditions hold:
  // 1. Last committed URL is a Google search URL.
  // 2. Safe search is forced.
  // 3. Android parental controls have search settings enabled.

  if (!google_util::IsGoogleSearchUrl(web_contents()->GetLastCommittedURL())) {
    // Uninteresting navigation (not a search page).
    return;
  }

  Profile* profile =
      Profile::FromBrowserContext(web_contents()->GetBrowserContext());
  if (!profile->GetPrefs()->GetBoolean(safe_search_pref_name)) {
    // Safe search is off. We can't undo safe search url params, because they
    // might've been added by the user as well.
    return;
  }

  if (!g_browser_process->device_parental_controls().IsSafeSearchForced()) {
    // Safe search is forced but for different reason than supervision - do not
    // step into other features shoes.
    return;
  }

  web_contents()->GetController().Reload(content::ReloadType::NORMAL,
                                         /*check_for_repost=*/false);
}
#endif  // BUILDFLAG(IS_ANDROID)

void SupervisedUserNavigationObserver::OnInterstitialDone(
    content::FrameTreeNodeId frame_id) {
  supervised_user_interstitials_.erase(frame_id);
}

void SupervisedUserNavigationObserver::OnRequestBlockedInternal(
    supervised_user::WebFilteringResult filtering_result,
    int64_t navigation_id,
    content::FrameTreeNodeId frame_id,
    const OnInterstitialResultCallback& callback) {
  // TODO(bauerb): Use SaneTime when available.
  base::Time timestamp = base::Time::Now();
  // Create a history entry for the attempt and mark it as such.  This history
  // entry should be marked as "not hidden" so the user can see attempted but
  // blocked navigations.  (This is in contrast to the normal behavior, wherein
  // Chrome marks navigations that result in an error as hidden.)  This is to
  // show the user the same thing that the custodian will see on the dashboard
  // (where it gets via a different mechanism unrelated to history).  Blocked
  // requests are also distinct from 404 navigations, and we report the attempt
  // accordingly.
  history::HistoryAddPageArgs add_page_args(
      filtering_result.url, timestamp,
      history::ContextIDForWebContents(web_contents()),
      /*nav_entry_id=*/0, /*local_navigation_id=*/std::nullopt,
      /*referrer=*/filtering_result.url, history::RedirectList(),
      ui::PAGE_TRANSITION_BLOCKED,
      /*hidden=*/false, history::SOURCE_BROWSED,
      history::VisitResponseCodeCategory::kNot404,
      /*did_replace_entry=*/false, /*consider_for_ntp_most_visited=*/true,
      history::VisitContextEphemerality::kNotEphemeral,
      /*title=*/std::nullopt,
      // TODO(crbug.com/40279734): Investigate whether we want to record blocked
      // navigations in the VisitedLinkDatabase, and if so, populate
      // top_level_url with a real value.
      /*top_level_url=*/std::nullopt);

  // Add the entry to the history database.
  Profile* profile =
      Profile::FromBrowserContext(web_contents()->GetBrowserContext());
  history::HistoryService* history_service =
      HistoryServiceFactory::GetForProfile(profile,
                                           ServiceAccessType::IMPLICIT_ACCESS);

  // |history_service| is null if saving history is disabled.
  if (history_service) {
    history_service->AddPage(add_page_args);
  }

  std::unique_ptr<NavigationEntry> entry = NavigationEntry::Create();
  entry->SetVirtualURL(filtering_result.url);
  entry->SetTimestamp(timestamp);
  auto serialized_entry = std::make_unique<sessions::SerializedNavigationEntry>(
      sessions::ContentSerializedNavigationBuilder::FromNavigationEntry(
          blocked_navigations_.size(), entry.get()));
  blocked_navigations_.push_back(std::move(serialized_entry));

  // Show the interstitial.
  const bool initial_page_load = true;
  MaybeShowInterstitial(filtering_result, initial_page_load, navigation_id,
                        frame_id, callback);
}

void SupervisedUserNavigationObserver::URLFilterCheckCallback(
    int render_frame_process_id,
    int render_frame_routing_id,
    supervised_user::WebFilteringResult result) {
  auto* render_frame_host = content::RenderFrameHost::FromID(
      render_frame_process_id, render_frame_routing_id);

  // `render_frame_host` could be in an inactive state since this callback is
  // called asynchronously, and we should not reload an unrelated document.
  if (!render_frame_host || !render_frame_host->IsRenderFrameLive() ||
      !render_frame_host->IsActive()) {
    return;
  }

  content::FrameTreeNodeId frame_id = render_frame_host->GetFrameTreeNodeId();
  bool is_showing_interstitial =
      supervised_user_interstitials_.contains(frame_id);
  bool should_show_interstitial = result.IsBlocked();

  // If an interstitial is being shown where it shouldn't (for e.g. because a
  // parent just approved a request) reloading will clear it. On the other hand,
  // if an interstitial error page is not being shown but it should be shown,
  // then reloading will trigger the navigation throttle to show the error page.
  if (is_showing_interstitial != should_show_interstitial) {
    if (render_frame_host->IsInPrimaryMainFrame()) {
      web_contents()->GetController().Reload(content::ReloadType::NORMAL,
                                             /*=check_for_repost=*/false);
      return;
    }
    render_frame_host->Reload();
  }
}

void SupervisedUserNavigationObserver::MaybeShowInterstitial(
    supervised_user::WebFilteringResult filtering_result,
    bool initial_page_load,
    int64_t navigation_id,
    content::FrameTreeNodeId frame_id,
    const OnInterstitialResultCallback& callback) {
  Profile* profile =
      Profile::FromBrowserContext(web_contents()->GetBrowserContext());
  CHECK(profile);
  supervised_user::FamilyLinkSettingsService* family_link_settings_service =
      supervised_user::FamilyLinkSettingsServiceFactory::GetForKey(
          profile->GetProfileKey());
  CHECK(family_link_settings_service);
  auto web_content_handler = CreateWebContentHandler(
      web_contents(), filtering_result.url, profile, frame_id, navigation_id);
  CHECK(web_content_handler);
  std::unique_ptr<supervised_user::SupervisedUserInterstitial> interstitial =
      supervised_user::SupervisedUserInterstitial::Create(
          std::move(web_content_handler), *supervised_user_service(),
          *family_link_settings_service, filtering_result,
          base::UTF8ToUTF16(supervised_user::GetAccountGivenName(*profile)));
  supervised_user_interstitials_[frame_id] = std::move(interstitial);

  bool already_requested =
      requested_hosts_.contains(filtering_result.url.GetHost());
  bool is_main_frame =
      frame_id == web_contents()->GetPrimaryMainFrame()->GetFrameTreeNodeId();

  callback.Run(supervised_user::InterstitialResultCallbackActions::
                   kCancelWithInterstitial,
               already_requested, is_main_frame);
}

void SupervisedUserNavigationObserver::FilterRenderFrame(
    content::RenderFrameHost* render_frame_host) {
  // If the RenderFrameHost is not live return.
  // If the RenderFrameHost belongs to the main frame, return. This is because
  // the main frame is already filtered in
  // |SupervisedUserNavigationObserver::OnUrlFilteringServiceChanged|.
  if (!render_frame_host->IsRenderFrameLive() ||
      render_frame_host->IsInPrimaryMainFrame()) {
    return;
  }

  supervised_user_url_filtering_service()->GetFilteringBehaviorForSubFrame(
      /*url=*/render_frame_host->GetLastCommittedURL(),
      /*main_frame_url=*/web_contents()->GetLastCommittedURL(),
      base::BindOnce(&SupervisedUserNavigationObserver::URLFilterCheckCallback,
                     weak_ptr_factory_.GetWeakPtr(),
                     render_frame_host->GetProcess()->GetDeprecatedID(),
                     render_frame_host->GetRoutingID()),
      kWebFilterMetricsOptionsForNavigationObserver);
}

supervised_user::SupervisedUserInterstitial*
SupervisedUserNavigationObserver::GetInterstitialForFrame() {
  content::RenderFrameHost& target_frame = receivers_.CurrentTargetFrame();
  content::FrameTreeNodeId frame_id = target_frame.GetFrameTreeNodeId();

  if (auto it = supervised_user_interstitials_.find(frame_id);
      it != supervised_user_interstitials_.end()) {
    return it->second.get();
  }
  return nullptr;
}

void SupervisedUserNavigationObserver::GoBack() {
  // Do not allow back navigation for subframes.
  if (!receivers_.CurrentTargetFrame().IsInPrimaryMainFrame()) {
    return;
  }

  supervised_user::SupervisedUserInterstitial* interstitial =
      GetInterstitialForFrame();
  if (!interstitial) {
    return;
  }
  interstitial->GoBack();
}

void SupervisedUserNavigationObserver::RequestUrlAccessRemote(
    RequestUrlAccessRemoteCallback callback) {
  supervised_user::SupervisedUserInterstitial* interstitial =
      GetInterstitialForFrame();
  if (!interstitial) {
    std::move(callback).Run(/*request_issued=*/false);
    return;
  }
  interstitial->RequestUrlAccessRemote(
      base::BindOnce(&SupervisedUserNavigationObserver::RequestCreated,
                     weak_ptr_factory_.GetWeakPtr(), std::move(callback),
                     interstitial->filtering_result().url.GetHost()));
}

void SupervisedUserNavigationObserver::RequestUrlAccessLocal(
    RequestUrlAccessLocalCallback callback) {
  supervised_user::SupervisedUserInterstitial* interstitial =
      GetInterstitialForFrame();
  if (!interstitial) {
    std::move(callback).Run(/*request_issued=*/false);
    return;
  }
  interstitial->RequestUrlAccessLocal(std::move(callback));
}

#if BUILDFLAG(IS_ANDROID)
void SupervisedUserNavigationObserver::LearnMore(LearnMoreCallback callback) {
  supervised_user::SupervisedUserInterstitial* interstitial =
      GetInterstitialForFrame();
  if (!interstitial) {
    return;
  }
  interstitial->LearnMore(std::move(callback));
}
#endif

void SupervisedUserNavigationObserver::RequestCreated(
    RequestUrlAccessRemoteCallback callback,
    const std::string& host,
    bool successfully_created_request) {
  if (successfully_created_request) {
    requested_hosts_.insert(host);
  }
  std::move(callback).Run(successfully_created_request);
}

void SupervisedUserNavigationObserver::MaybeUpdateRequestedHosts() {
  for (auto iter = requested_hosts_.begin(); iter != requested_hosts_.end();) {
    supervised_user::WebFilteringResult result =
        supervised_user_url_filtering_service()->GetFilteringBehavior(
            GURL(*iter));

    if (result.IsFromManualList() && result.IsAllowed()) {
      iter = requested_hosts_.erase(iter);
    } else {
      iter++;
    }
  }
}

supervised_user::SupervisedUserService*
SupervisedUserNavigationObserver::supervised_user_service() const {
  return supervised_user::SupervisedUserServiceFactory::GetForProfile(
      Profile::FromBrowserContext(web_contents()->GetBrowserContext()));
}

supervised_user::SupervisedUserUrlFilteringService*
SupervisedUserNavigationObserver::supervised_user_url_filtering_service()
    const {
  return supervised_user::SupervisedUserUrlFilteringServiceFactory::
      GetForProfile(
          Profile::FromBrowserContext(web_contents()->GetBrowserContext()));
}

WEB_CONTENTS_USER_DATA_KEY_IMPL(SupervisedUserNavigationObserver);
