// 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/ash/boca/on_task/on_task_locked_session_navigation_throttle.h"

#include <memory>

#include "ash/shell.h"
#include "ash/webui/boca_ui/url_constants.h"
#include "chrome/browser/ash/boca/on_task/on_task_locked_session_window_tracker.h"
#include "chrome/browser/ash/browser_delegate/browser_controller.h"
#include "chrome/browser/ash/browser_delegate/browser_delegate.h"
#include "chrome/browser/login_detection/login_detection_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chromeos/ash/components/boca/boca_role_util.h"
#include "chromeos/ash/components/boca/on_task/on_task_blocklist.h"
#include "chromeos/ash/components/browser_context_helper/browser_context_helper.h"
#include "chromeos/ash/components/browser_context_helper/browser_context_types.h"
#include "components/google/core/common/google_util.h"
#include "components/sessions/content/session_tab_helper.h"
#include "components/user_manager/user_manager.h"
#include "content/public/browser/global_routing_id.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/navigation_throttle.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/common/url_constants.h"
#include "extensions/common/extension_urls.h"
#include "net/base/url_util.h"
#include "net/http/http_request_headers.h"
#include "ui/base/page_transition_types.h"
#include "url/gurl.h"

namespace ash {  // namespace
namespace {

using ::boca::LockedNavigationOptions;

// Returns whether all the given query parameters are found in the URL.
bool DoAllQueryParamsExist(const std::set<std::string>& request_params,
                           const GURL& url) {
  if (!url.has_query()) {
    return false;
  }
  for (const auto& param : request_params) {
    std::string param_value;
    if (!net::GetValueForKeyInQuery(url, param, &param_value)) {
      return false;
    }
  }
  return true;
}

// Returns whether the url is the start of an Oauth login.
bool IsOauthLoginStart(const GURL& url) {
  return url.SchemeIsHTTPOrHTTPS() &&
         DoAllQueryParamsExist(login_detection::GetOAuthLoginStartQueryParams(),
                               url);
}

// Returns whether the url is the completion of an Oauth login.
bool IsOauthLoginComplete(const GURL& url) {
  return DoAllQueryParamsExist(
      login_detection::GetOAuthLoginCompleteQueryParams(), url);
}

bool IsChromeWebStoreURL(const GURL& url) {
  return (url.GetHost() == extension_urls::GetWebstoreLaunchURL().GetHost()) ||
         (url.GetHost() == extension_urls::GetNewWebstoreLaunchURL().GetHost());
}

content::WebContents* GetWebContentsForTabId(BrowserDelegate* browser,
                                             SessionID tab_id) {
  if (!browser || !tab_id.is_valid()) {
    return nullptr;
  }
  for (size_t i = 0; i < browser->GetWebContentsCount(); ++i) {
    content::WebContents* web_contents = browser->GetWebContentsAt(i);
    if (web_contents &&
        sessions::SessionTabHelper::IdForTab(web_contents) == tab_id) {
      return web_contents;
    }
  }
  return nullptr;
}

bool IsBocaAppHostURL(const GURL& url) {
  return (url.SchemeIs(content::kChromeUIUntrustedScheme) &&
          url.GetHost() == boca::kChromeBocaAppHost);
}

// Resolves and returns the parent tab for the navigating tab. Abstracts out the
// complex logic around identifying the parent tab for background navigations,
// especially those that spawn new tabs.
content::WebContents* GetParentTab(content::NavigationHandle* navigation_handle,
                                   OnTaskBlocklist* on_task_blocklist,
                                   LockedSessionWindowTracker* window_tracker) {
  content::WebContents* navigating_tab = navigation_handle->GetWebContents();
  content::WebContents* parent_tab =
      navigating_tab->GetFirstWebContentsInLiveOriginalOpenerChain();
  if (!parent_tab) {
    // Normally happens when the site uses noopener on links. We fall back to
    // the one tracked by the blocklist if there is one instead.
    const SessionID parent_tab_id =
        on_task_blocklist->GetParentTabId(navigating_tab);
    if (parent_tab_id.is_valid()) {
      BrowserDelegate* const tracked_browser =
          BrowserController::GetInstance()->GetDelegate(
              window_tracker->browser());
      parent_tab = GetWebContentsForTabId(tracked_browser, parent_tab_id);
    }

    // If there is none tracked by the blocklist (mostly for new uncommitted
    // background navigations that spawn new tabs), we try to derive this via
    // initiator routing ids and frame tokens.
    if (!parent_tab &&
        navigation_handle->GetInitiatorFrameToken().has_value()) {
      content::RenderFrameHost* const initiator_rfh =
          content::RenderFrameHost::FromFrameToken(
              content::GlobalRenderFrameHostToken(
                  navigation_handle->GetInitiatorProcessId(),
                  navigation_handle->GetInitiatorFrameToken().value()));
      if (initiator_rfh) {
        parent_tab = content::WebContents::FromRenderFrameHost(initiator_rfh);
        if (parent_tab == navigating_tab) {
          parent_tab = nullptr;
        }
      }
    }
  }
  return parent_tab;
}

}  // namespace

OnTaskLockedSessionNavigationThrottle::OnTaskLockedSessionNavigationThrottle(
    content::NavigationThrottleRegistry& registry)
    : content::NavigationThrottle(registry) {}

OnTaskLockedSessionNavigationThrottle::
    ~OnTaskLockedSessionNavigationThrottle() = default;

const char* OnTaskLockedSessionNavigationThrottle::GetNameForLogging() {
  return "OnTaskLockedSessionNavigationThrottle";
}

// static
void OnTaskLockedSessionNavigationThrottle::MaybeCreateAndAdd(
    content::NavigationThrottleRegistry& registry) {
  content::NavigationHandle& handle = registry.GetNavigationHandle();
  if (!ash::boca_util::IsEnabled(
          ash::BrowserContextHelper::Get()->GetUserByBrowserContext(
              handle.GetWebContents()->GetBrowserContext()))) {
    return;
  }

  LockedSessionWindowTracker* const window_tracker =
      LockedSessionWindowTrackerFactory::GetForBrowserContext(
          handle.GetWebContents()->GetBrowserContext());
  // We do not need to create the throttle when we are not currently observing a
  // window that needs to be in locked mode, or if the navigation throttle is
  // not ready to start (where we are adding new tabs), or if the navigation is
  // occurring outside the outermost main frame (such as subframes on the page
  // so resources can still load), or if it is a same document navigation (where
  // we are not navigating to a new page).
  if (!window_tracker || !window_tracker->browser() ||
      !window_tracker->can_start_navigation_throttle()) {
    return;
  }

  if (!handle.IsInOutermostMainFrame()) {
    return;
  }

  if (handle.IsSameDocument()) {
    return;
  }

  BrowserDelegate* const content_browser =
      BrowserController::GetInstance()->GetBrowserForTab(
          handle.GetWebContents());

  // Ensure we only apply the nav throttle on OnTask SWA navigations.
  if (content_browser &&
      (&content_browser->GetBrowser() != window_tracker->browser() &&
       content_browser->GetType() != BrowserType::kAppPopup)) {
    return;
  }
  window_tracker->ObserveWebContents(handle.GetWebContents());
  registry.AddThrottle(
      base::WrapUnique(new OnTaskLockedSessionNavigationThrottle(registry)));
}

void OnTaskLockedSessionNavigationThrottle::MaybeShowBlockedURLToast() {
  // Display the toast when the navigation is user-initiated. Note that
  // `HasUserGesture` does not capture browser-initiated navigations. The
  // negation of `IsRendererInitiated` tells us whether the navigation is
  // browser-generated.
  if (navigation_handle()->HasUserGesture() ||
      !navigation_handle()->IsRendererInitiated()) {
    LockedSessionWindowTracker* const window_tracker =
        LockedSessionWindowTrackerFactory::GetForBrowserContext(
            navigation_handle()->GetWebContents()->GetBrowserContext());

    // TODO: b/377767192 - Add tests to for scenarios regarding tab browser
    // instance changes
    if (window_tracker && !IsOutsideOnTaskAppNavigation()) {
      window_tracker->ShowURLBlockedToast();
    }
  }
}

bool OnTaskLockedSessionNavigationThrottle::MaybeProceedForOneLevelDeep(
    content::WebContents* tab,
    const GURL& url) {
  LockedSessionWindowTracker* const window_tracker =
      LockedSessionWindowTrackerFactory::GetForBrowserContext(
          navigation_handle()->GetWebContents()->GetBrowserContext());
  if (!window_tracker) {
    return false;
  }

  // Google search sometimes redirects to the captcha page. We let this
  // navigation proceed by default.
  if (google_util::IsGoogleDomainUrl(
          url, google_util::SubdomainPermission::DISALLOW_SUBDOMAIN,
          google_util::PortPermission::ALLOW_NON_STANDARD_PORTS) &&
      url.path().starts_with("/sorry/")) {
    return true;
  }

  OnTaskBlocklist* const on_task_blocklist =
      window_tracker->on_task_blocklist();
  if (!on_task_blocklist->CanPerformOneLevelNavigation(tab)) {
    return false;
  }
  on_task_blocklist->MaybeSetURLRestrictionLevel(
      navigation_handle()->GetWebContents(), url,
      LockedNavigationOptions::BLOCK_NAVIGATION);
  return true;
}

bool OnTaskLockedSessionNavigationThrottle::
    ShouldBlockSensitiveUrlNavigation() {
  // Block download urls, files, urls via post request (form submission being
  // an exception), blob urls, non-boca app chrome urls, and other local
  // schemes.
  const GURL& url = navigation_handle()->GetURL();
  return (navigation_handle()->IsDownload() ||
          (navigation_handle()->GetRequestMethod() !=
               net::HttpRequestHeaders::kGetMethod &&
           !navigation_handle()->IsFormSubmission()) ||
          (!url.SchemeIsHTTPOrHTTPS() && !IsBocaAppHostURL(url)) ||
          IsChromeWebStoreURL(url));
}

bool OnTaskLockedSessionNavigationThrottle::IsOutsideOnTaskAppNavigation() {
  // TODO(b/377347487): Add test for Navigations that happen outside the OnTask
  // SWA but attach the tab to the OnTask SWA subsequently.
  BrowserDelegate* const content_browser =
      BrowserController::GetInstance()->GetBrowserForTab(
          navigation_handle()->GetWebContents());
  LockedSessionWindowTracker* const window_tracker =
      LockedSessionWindowTrackerFactory::GetForBrowserContext(
          navigation_handle()->GetWebContents()->GetBrowserContext());
  // Handle the case where the creation of the tab is in the OnTask app
  // context, but is moved to a different browser right after (such as open link
  // in chrome window context menu).
  if (!content_browser ||
      (&content_browser->GetBrowser() != window_tracker->browser() &&
       content_browser->GetType() != BrowserType::kAppPopup)) {
    return true;
  }
  return false;
}

content::NavigationThrottle::ThrottleCheckResult
OnTaskLockedSessionNavigationThrottle::CheckRestrictions() {
  // If there is a client side redirect, let those through.
  if (navigation_handle()->GetNavigationEntry() &&
      (navigation_handle()->GetNavigationEntry()->GetTransitionType() &
       ui::PageTransition::PAGE_TRANSITION_CLIENT_REDIRECT)) {
    return PROCEED;
  }

  content::WebContents* const navigating_tab =
      navigation_handle()->GetWebContents();
  if (!navigating_tab) {
    return CANCEL;
  }

  LockedSessionWindowTracker* const window_tracker =
      LockedSessionWindowTrackerFactory::GetForBrowserContext(
          navigating_tab->GetBrowserContext());
  OnTaskBlocklist* const on_task_blocklist =
      window_tracker->on_task_blocklist();

  // Resolve and register parent child tab association early on to simplify
  // downstream blocklist processing.
  content::WebContents* const parent_tab =
      GetParentTab(navigation_handle(), on_task_blocklist, window_tracker);
  if (parent_tab) {
    on_task_blocklist->SetParentForTab(navigating_tab, parent_tab);
  }

  BrowserDelegate* const content_browser =
      BrowserController::GetInstance()->GetBrowserForTab(navigating_tab);

  if (IsOutsideOnTaskAppNavigation()) {
    return PROCEED;
  }

  if (ShouldBlockSensitiveUrlNavigation() &&
      !window_tracker->oauth_in_progress()) {
    MaybeShowBlockedURLToast();
    return CANCEL;
  }

  // Allow redirects triggered as separate navigation requests to go through so
  // they do not count towards the 1LD quota. We do not extend this to other
  // navigation restrictions to prevent users from circumventing said
  // restrictions.
  if (navigation_handle()->GetRedirectChain().size() > 1 &&
      on_task_blocklist->IsTabRestrictionOneLevelDeep(navigating_tab)) {
    return PROCEED;
  }
  const GURL& url = navigation_handle()->GetURL();

  // There is no nav restriction associated with the home tab so the blocklist
  // may enforce nav restrictions based on the previous active tab. We allow all
  // requests to the home URL to go through for now.
  // TODO(crbug.com/413468168) - Associate a nav restriction with the home tab.
  if (IsBocaAppHostURL(url)) {
    return PROCEED;
  }

  // Checks if the query is the end of an OAuth login. If so, then we want
  // to let these pass.
  if (IsOauthLoginComplete(url)) {
    should_redirects_pass_ = true;
    return PROCEED;
  }

  // Checks if the query is the start of an OAuth login. If so, then we want
  // to let these pass.
  if (IsOauthLoginStart(url)) {
    window_tracker->set_oauth_in_progress(true, content_browser);
    // Set `should_redirects_pass_` to true in case the Oauth login flow happens
    // in the main tab and not in a popup window. This ensures that we are still
    // letting Oauth login flows to proceed.
    should_redirects_pass_ = true;
    return PROCEED;
  }

  // If the navigation is taking place in a popup and isn't recognized as an
  // OAuth navigation, still give it a chance to finish. If by the end
  //  of the navigation we haven't determined that it is an OAuth login flow,
  //  the window_tracker will close the popup.
  if (content_browser && content_browser->GetType() == BrowserType::kAppPopup &&
      !window_tracker->CanOpenNewPopup()) {
    return PROCEED;
  }

  // This is a page reload, let the navigation pass since if we were able to get
  // to this page, then it was already filtered. This is so that one level deep
  // navigation can still reload the current page even though we have already
  // navigated one level deeper into the page.
  // Note: this throttle allows reloads that redirect to a different URL; if
  // that URL needs to be blocked by another blocklist, such as the one imposed
  // by the device admin panel, this would be enforced by a different
  // NavigationThrottle.
  if (on_task_blocklist->IsTabRestrictionOneLevelDeep(navigating_tab) &&
      navigation_handle()->GetReloadType() != content::ReloadType::NONE &&
      navigating_tab->GetLastCommittedURL().is_valid()) {
    should_redirects_pass_ = true;
    return PROCEED;
  }

  // Check for history navigations via the back and forward shortcuts or via
  // the context menu. Back needs to be explicitly allowed to go back in the
  // case this was a one level deep navigation and we do not want to block
  // the navigation from going back.
  if (on_task_blocklist->IsTabRestrictionOneLevelDeep(navigating_tab) &&
      navigation_handle()->GetNavigationEntry() &&
      navigation_handle()->GetNavigationEntry()->GetTransitionType() &
          ui::PageTransition::PAGE_TRANSITION_FORWARD_BACK) {
    content::NavigationController& controller = navigating_tab->GetController();
    int current_index = controller.GetLastCommittedEntryIndex();
    int pending_index = controller.GetPendingEntryIndex();
    if (pending_index < current_index) {
      should_redirects_pass_ = true;
      return PROCEED;
    }
  }

  policy::URLBlocklist::URLBlocklistState blocklist_state =
      on_task_blocklist->GetURLBlocklistState(url, navigating_tab);
  if (blocklist_state ==
      policy::URLBlocklist::URLBlocklistState::URL_IN_BLOCKLIST) {
    MaybeShowBlockedURLToast();
    return content::NavigationThrottle::CANCEL;
  }

  if (blocklist_state ==
      policy::URLBlocklist::URLBlocklistState::URL_IN_ALLOWLIST) {
    // If this navigation occurs on a tab restricted to one level deep
    // navigations, it will only be allowed if the tab hasn't performed a one
    // level deep navigation yet, which is true if the tab's last committed URL
    // hasn't changed from when the restrictions were enabled. Navigations in
    // newly opened tabs, such as when ctrl-clicking a link, also count as
    // navigating one level deep. For those cases, restrict the new tab to the
    // exact URL for subsequent navigations. The exact URL matching will occur
    // in `on_task_blocklist->CanPerformOneLevelNavigation()`. The budget needs
    // to be accounted for from the parent tab for such newly spawned child
    // tabs.
    content::WebContents* budget_tab = navigating_tab;
    const SessionID budget_tab_id =
        sessions::SessionTabHelper::IdForTab(budget_tab);
    if (!on_task_blocklist->IsParentTab(navigating_tab) &&
        !on_task_blocklist->child_tab_to_nav_filters().contains(
            budget_tab_id)) {
      // Newly spawned child tab.
      budget_tab = parent_tab;
    }

    LockedNavigationOptions::NavigationType tab_restriction =
        on_task_blocklist->GetRestrictionLevelForTab(navigating_tab);
    if (tab_restriction == LockedNavigationOptions::LIMITED_NAVIGATION) {
      if (!MaybeProceedForOneLevelDeep(budget_tab, url)) {
        MaybeShowBlockedURLToast();
        return content::NavigationThrottle::CANCEL;
      }
    } else if (tab_restriction ==
               LockedNavigationOptions::
                   SAME_DOMAIN_OPEN_OTHER_DOMAIN_LIMITED_NAVIGATION) {
      // We need to account for several scenarios here, because a navigation
      // needs to be allowed if it is within the same domain as the original URL
      // in the parent tab, but conditionally allowed to go one level deep (1LD)
      // if it has not already (in the same parent tab or a child tab). Domain
      // checks happen against the original URL of the parent tab, but 1LD
      // checks need to cover three possibilities:
      // 1. Navigation on the same parent tab.
      // 2. Navigation on a new child tab (spawned through ctrl+click for
      // instance).
      // 3. Navigation on a pre-existing child tab that may or may not have
      // already met the 1LD requirement.
      SessionID original_tab_id = SessionID::InvalidValue();
      if (budget_tab) {
        original_tab_id = sessions::SessionTabHelper::IdForTab(budget_tab);
      }

      const GURL source_url =
          on_task_blocklist->GetOneLevelDeepOriginalURL(original_tab_id);
      if (source_url.is_valid()) {
        if (OnTaskBlocklist::IsURLInDomain(url, source_url)) {
          // Same domain navigation.
          on_task_blocklist->MaybeSetURLRestrictionLevel(
              navigating_tab, url,
              LockedNavigationOptions::
                  SAME_DOMAIN_OPEN_OTHER_DOMAIN_LIMITED_NAVIGATION);
        } else if (on_task_blocklist->IsParentTab(navigating_tab) &&
                   !MaybeProceedForOneLevelDeep(navigating_tab, url)) {
          // Cannot go 1LD on the same parent tab.
          MaybeShowBlockedURLToast();
          return content::NavigationThrottle::CANCEL;
        } else if (const SessionID nav_tab_id =
                       sessions::SessionTabHelper::IdForTab(navigating_tab);
                   on_task_blocklist->child_tab_to_nav_filters().contains(
                       nav_tab_id) &&
                   on_task_blocklist->child_tab_to_nav_filters().at(
                       nav_tab_id) ==
                       LockedNavigationOptions::BLOCK_NAVIGATION) {
          // Cannot go 1LD on a pre-existing child tab.
          MaybeShowBlockedURLToast();
          return content::NavigationThrottle::CANCEL;
        } else if (!MaybeProceedForOneLevelDeep(budget_tab, url)) {
          // Disallowed 1LD navigation on a new child tab.
          MaybeShowBlockedURLToast();
          return content::NavigationThrottle::CANCEL;
        }
      }
    } else {
      // Set the restrictions for this new url if possible with the parent tab's
      // restrictions. This will be skipped if the tab which this
      // navigation is occurring in is already set.
      on_task_blocklist->MaybeSetURLRestrictionLevel(navigating_tab, url,
                                                     tab_restriction);
    }
    should_redirects_pass_ = true;
    return PROCEED;
  }
  MaybeShowBlockedURLToast();
  return content::NavigationThrottle::CANCEL;
}

content::NavigationThrottle::ThrottleCheckResult
OnTaskLockedSessionNavigationThrottle::WillStartRequest() {
  return CheckRestrictions();
}

content::NavigationThrottle::ThrottleCheckResult
OnTaskLockedSessionNavigationThrottle::WillProcessResponse() {
  LockedSessionWindowTracker* const window_tracker =
      LockedSessionWindowTrackerFactory::GetForBrowserContext(
          navigation_handle()->GetWebContents()->GetBrowserContext());

  // This check is needed other SWA launches during unlocked that needs to
  // process navigation responses.
  // TODO: b/377767192 - Add tests to for scenarios regarding tab browser
  // instance changes

  if (IsOutsideOnTaskAppNavigation() || should_redirects_pass_) {
    return PROCEED;
  }
  if (ShouldBlockSensitiveUrlNavigation() &&
      !window_tracker->oauth_in_progress()) {
    MaybeShowBlockedURLToast();
    return CANCEL;
  }
  return CheckRestrictions();
}

content::NavigationThrottle::ThrottleCheckResult
OnTaskLockedSessionNavigationThrottle::WillRedirectRequest() {
  BrowserDelegate* const content_browser =
      BrowserController::GetInstance()->GetBrowserForTab(
          navigation_handle()->GetWebContents());
  LockedSessionWindowTracker* const window_tracker =
      LockedSessionWindowTrackerFactory::GetForBrowserContext(
          navigation_handle()->GetWebContents()->GetBrowserContext());
  if (content_browser && content_browser->GetType() == BrowserType::kAppPopup) {
    // After the Oauth flow is completed, we let the `OnBrowserClosing`
    // observer from the `window_tracker` to set `oauth_in_progress` to be false
    // since a request may have returned with the auth code and marked as
    // complete, it may take some time for the redirect from oauth login flow to
    // the landing page to happen. If this is marked as false early, the window
    // may close before a redirect happens. We should let the `OnBrowserClosing`
    // call happen first. Similarly, since we aren't setting the bool for OnTask
    // for the popup browser, it can autoclose, so we can rely on that to reset
    // our `oauth_in_progress` flag.
    if (IsOauthLoginComplete(navigation_handle()->GetURL())) {
      return PROCEED;
    }

    // Checks to see if the popup window is an OAuth login. If the Oauth login
    // flow is just started, we tell the window tracker that it is in progress
    // to avoid closing the OAuth login window before it has been completed.
    if (IsOauthLoginStart(navigation_handle()->GetURL())) {
      window_tracker->set_oauth_in_progress(true, content_browser);
      return PROCEED;
    }
    if (window_tracker->oauth_in_progress()) {
      return content::NavigationThrottle::PROCEED;
    }
    MaybeShowBlockedURLToast();
    return content::NavigationThrottle::CANCEL;
  }

  if (should_redirects_pass_) {
    return PROCEED;
  }
  // This catch all case is to catch navigations where we identify a case where
  // we should not always pass all redirects (such as blob schemes or page
  // reload in case of server redirects).
  return CheckRestrictions();
}

}  // namespace ash
