// 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/webid/webid_utils.h"

#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/trace_event/trace_event.h"
#include "components/url_formatter/elide_url.h"
#include "components/url_formatter/url_formatter.h"
#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/browser/webid/flags.h"
#include "content/browser/webid/metrics.h"
#include "content/browser/webid/request_page_data.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/runtime_feature_state/runtime_feature_state_document_data.h"
#include "content/public/browser/webid/federated_embedder_login_request.h"
#include "content/public/browser/webid/federated_identity_api_permission_context_delegate.h"
#include "content/public/browser/webid/federated_identity_permission_context_delegate.h"
#include "content/public/common/web_identity.h"
#include "net/base/net_errors.h"
#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
#include "net/base/schemeful_site.h"
#include "net/base/url_util.h"
#include "third_party/blink/public/mojom/devtools/inspector_issue.mojom.h"
#include "third_party/blink/public/mojom/webid/federated_request.mojom.h"
#include "url/origin.h"

namespace content::webid {

using blink::mojom::FederatedRequestResult;

namespace {
constexpr net::registry_controlled_domains::PrivateRegistryFilter
    kDefaultPrivateRegistryFilter =
        net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES;
}  // namespace

bool IsSameSiteWithAncestors(const url::Origin& origin,
                             RenderFrameHost* render_frame_host) {
  while (render_frame_host) {
    if (!net::SchemefulSite::IsSameSite(
            origin, render_frame_host->GetLastCommittedOrigin())) {
      return false;
    }
    render_frame_host = render_frame_host->GetParent();
  }
  return true;
}

bool IsSameOriginWithAncestors(const url::Origin& origin,
                               RenderFrameHost* render_frame_host) {
  while (render_frame_host) {
    if (!origin.IsSameOriginWith(render_frame_host->GetLastCommittedOrigin())) {
      return false;
    }
    render_frame_host = render_frame_host->GetParentOrOuterDocument();
  }
  return true;
}

void SetIdpSigninStatus(base::WeakPtr<BrowserContext> context,
                        network::mojom::RequestDestination destination,
                        FrameTreeNodeId frame_tree_node_id,
                        const std::optional<url::Origin>& initiator,
                        const url::Origin& idp_origin,
                        blink::mojom::IdpSigninStatus status) {
  if (!context) {
    return;
  }
  FrameTreeNode* frame_tree_node = nullptr;
  // frame_tree_node_id may be invalid if we are loading the first frame
  // of the tab, but check the destination because we don't want to allow
  // Set-Login subresource headers if we don't have a frame to check.
  // This is because we want to ensure that Set-Login is only used by
  // the same-site origin or a top-level navigation.
  if (!frame_tree_node_id &&
      destination != network::mojom::RequestDestination::kDocument) {
    return;
  }
  if (frame_tree_node_id) {
    frame_tree_node = FrameTreeNode::GloballyFindByID(frame_tree_node_id);
    // If the id was valid, but the lookup failed, we ignore the load because we
    // cannot do same-site checks.
    if (!frame_tree_node) {
      return;
    }
  }

  if (destination != network::mojom::RequestDestination::kDocument) {
    if (!initiator || !net::SchemefulSite::IsSameSite(idp_origin, *initiator)) {
      return;
    }
  }

  // Make sure we're same-origin with our ancestors.
  if (frame_tree_node) {
    if (frame_tree_node->IsInFencedFrameTree()) {
      return;
    }

    if (!IsSameSiteWithAncestors(idp_origin, frame_tree_node->parent())) {
      return;
    }
  }

  auto* delegate = context->GetFederatedIdentityPermissionContext();
  if (!delegate) {
    // The embedder may not have a delegate (e.g. webview)
    return;
  }
  delegate->SetIdpSigninStatus(
      idp_origin, status == blink::mojom::IdpSigninStatus::kSignedIn,
      std::nullopt);
}

std::optional<std::string> ComputeConsoleMessageForHttpResponseCode(
    const char* endpoint_name,
    int http_response_code) {
  // Do not add error message for OK response status.
  if (http_response_code >= 200 && http_response_code <= 299)
    return std::nullopt;

  if (http_response_code < 0) {
    // In this case, the |response_code| represents a NET_ERROR, so we should
    // use a helper function to ensure we use a meaningful message.
    return base::StringPrintf(
        "The fetch of the %s resulted in a network error: %s", endpoint_name,
        net::ErrorToShortString(http_response_code).c_str());
  }
  // In this case, the |response_code| represents an HTTP error code, which is
  // standard and hence the number by itself should be understood.
  return base::StringPrintf(
      "When fetching the %s, a %d HTTP response code was received.",
      endpoint_name, http_response_code);
}

bool IsEndpointSameOrigin(const GURL& identity_provider_config_url,
                          const GURL& endpoint_url) {
  return url::Origin::Create(identity_provider_config_url)
      .IsSameOriginWith(endpoint_url);
}

bool ShouldFailAccountsEndpointRequestBecauseNotSignedInWithIdp(
    const GURL& identity_provider_config_url,
    FederatedIdentityPermissionContextDelegate* permission_delegate) {
  const url::Origin idp_origin =
      url::Origin::Create(identity_provider_config_url);
  const std::optional<bool> idp_signin_status =
      permission_delegate->GetIdpSigninStatus(idp_origin);
  return !idp_signin_status.value_or(true);
}

void UpdateIdpSigninStatusForAccountsEndpointResponse(
    const GURL& identity_provider_config_url,
    FetchStatus fetch_status,
    bool does_idp_have_failing_signin_status,
    FederatedIdentityPermissionContextDelegate* permission_delegate) {
  url::Origin idp_origin = url::Origin::Create(identity_provider_config_url);

  // Record metrics on effect of IDP sign-in status API.
  const std::optional<bool> idp_signin_status =
      permission_delegate->GetIdpSigninStatus(idp_origin);
  Metrics::RecordIdpSigninMatchStatus(idp_signin_status,
                                      fetch_status.parse_status);

  if (fetch_status.parse_status == ParseStatus::kSuccess) {
    // `does_idp_have_failing_signin_status` fails the request prior to fetching
    // the accounts endpoint for FedCmIdpSigninStatusMode::ENABLED mode but not
    // FedCmIdpSigninStatusMode::METRICS_ONLY mode. Do not set the IdP sign-in
    // status here if `does_idp_have_failing_signin_status` in
    // FedCmIdpSigninStatusMode::METRICS_ONLY mode in order to better emulate
    // FedCmIdpSigninStatusMode::ENABLED behavior.
    if (!does_idp_have_failing_signin_status) {
      permission_delegate->SetIdpSigninStatus(idp_origin, true, std::nullopt);
    }
  } else {
    RecordIdpSignOutNetError(fetch_status.response_code);
    // Ensures that we only fetch accounts unconditionally once.
    permission_delegate->SetIdpSigninStatus(idp_origin, false, std::nullopt);
  }
}

std::string GetConsoleErrorMessageFromResult(FederatedRequestResult status) {
  switch (status) {
    case FederatedRequestResult::kShouldEmbargo: {
      return "User declined or dismissed prompt. API exponential cool down "
             "triggered.";
    }
    case FederatedRequestResult::kIdpNotPotentiallyTrustworthy: {
      return "The IdP is not potentially trustworthy (are you using HTTP?)";
    }
    case FederatedRequestResult::kDisabledInSettings: {
      return "FedCM was disabled either temporarily based on previous user "
             "action or permanently via site settings. Try manage third-party "
             "sign-in via the icon to the left of the URL bar or via site "
             "settings.";
    }
    case FederatedRequestResult::kDisabledInFlags: {
      return "FedCM was disabled in flags.";
    }
    case FederatedRequestResult::kTooManyRequests: {
      return "Only one navigator.credentials.get request may be outstanding at "
             "one time.";
    }
    case FederatedRequestResult::kWellKnownHttpNotFound: {
      return "The provider's FedCM well-known file cannot be found.";
    }
    case FederatedRequestResult::kWellKnownNoResponse: {
      return "The provider's FedCM well-known file fetch resulted in an "
             "error response code.";
    }
    case FederatedRequestResult::kWellKnownBlockedByConnectionAllowlist: {
      return "The provider's FedCM well-known file fetch was blocked by "
             "connection allowlist.";
    }
    case FederatedRequestResult::kWellKnownInvalidResponse: {
      return "Provider's FedCM well-known file is invalid.";
    }
    case FederatedRequestResult::kWellKnownListEmpty: {
      return "Provider's FedCM well-known file has no config URLs.";
    }
    case FederatedRequestResult::kWellKnownInvalidContentType: {
      return "Provider's FedCM well-known content type must be a JSON content "
             "type.";
    }
    case FederatedRequestResult::kConfigNotInWellKnown: {
      return "Provider's FedCM config file not listed in its well-known file.";
    }
    case FederatedRequestResult::kWellKnownTooBig: {
      return "Provider's FedCM well-known file contains too many config URLs.";
    }
    case FederatedRequestResult::kConfigHttpNotFound: {
      return "The provider's FedCM config file cannot be found.";
    }
    case FederatedRequestResult::kConfigNoResponse: {
      return "The provider's FedCM config file fetch resulted in an "
             "error response code.";
    }
    case FederatedRequestResult::kConfigBlockedByConnectionAllowlist: {
      return "The provider's FedCM config file fetch was blocked by connection "
             "allowlist.";
    }
    case FederatedRequestResult::kConfigInvalidResponse: {
      return "Provider's FedCM config file is invalid.";
    }
    case FederatedRequestResult::kConfigInvalidContentType: {
      return "Provider's FedCM config file content type must be a JSON content "
             "type.";
    }
    case FederatedRequestResult::kAccountsHttpNotFound: {
      return "The provider's accounts list endpoint cannot be found.";
    }
    case FederatedRequestResult::kAccountsNoResponse: {
      return "The provider's accounts list fetch resulted in an error response "
             "code.";
    }
    case FederatedRequestResult::kAccountsBlockedByConnectionAllowlist: {
      return "The provider's accounts list fetch was blocked by connection "
             "allowlist.";
    }
    case FederatedRequestResult::kAccountsInvalidResponse: {
      return "Provider's accounts list is invalid. Should have received an "
             "\"accounts\" list, where each account must have at least \"id\", "
             "\"name\", and \"email\".";
    }
    case FederatedRequestResult::kAccountsListEmpty: {
      return "Provider's accounts list is empty.";
    }
    case FederatedRequestResult::kAccountsInvalidContentType: {
      return "Provider's accounts list endpoint content type must be a JSON "
             "content type.";
    }
    case FederatedRequestResult::kIdTokenHttpNotFound: {
      return "The provider's id token endpoint cannot be found.";
    }
    case FederatedRequestResult::kIdTokenNoResponse: {
      return "The provider's token fetch resulted in an error response "
             "code.";
    }
    case FederatedRequestResult::kIdTokenBlockedByConnectionAllowlist: {
      return "The provider's token fetch was blocked by connection allowlist.";
    }
    case FederatedRequestResult::kIdTokenInvalidResponse: {
      return "Provider's token is invalid.";
    }
    case FederatedRequestResult::kIdTokenIdpErrorResponse: {
      return "Provider is unable to issue a token, but provided details on the "
             "error that occurred.";
    }
    case FederatedRequestResult::kIdTokenCrossSiteIdpErrorResponse: {
      return "Provider is unable to issue a token, but provided details on the "
             "error that occurred. The error URL must be same-site with the "
             "config URL.";
    }
    case FederatedRequestResult::kIdTokenInvalidContentType: {
      return "Provider's token endpoint content type must be a JSON content "
             "type.";
    }
    case FederatedRequestResult::kCanceled: {
      return "The request has been aborted.";
    }
    case FederatedRequestResult::kRpPageNotVisible: {
      return "RP page is not visible.";
    }
    case FederatedRequestResult::kSilentMediationFailure: {
      return "Silent mediation was requested, but the conditions to achieve it "
             "were not met.";
    }
    case FederatedRequestResult::kMissingTransientUserActivation: {
      return "FedCM active mode requires transient user activation.";
    }
    case FederatedRequestResult::kReplacedByActiveMode: {
      return "The request is replaced by a new one with active mode.";
    }
    case FederatedRequestResult::kNotSignedInWithIdp: {
      return "Not signed in with the identity provider.";
    }
    case FederatedRequestResult::kRelyingPartyOriginIsOpaque: {
      return "FedCM is not supported on an opaque origin.";
    }
    case FederatedRequestResult::kTypeNotMatching: {
      return "The requested IdP type did not match the registered IdP.";
    }
    case FederatedRequestResult::kUiDismissedNoEmbargo: {
      return "Prompt dismissed. API exponential cool down not "
             "triggered.";
    }
    case FederatedRequestResult::kError: {
      return "Error retrieving a token.";
    }
    case FederatedRequestResult::kCorsError: {
      return "Server did not send the correct CORS headers.";
    }
    case FederatedRequestResult::kSuppressedBySegmentationPlatform: {
      return "UI is suppressed because historical data shows that the user "
             "is less likely to login via FedCM passive mode on this website. "
             "For testing purposes, disable the #fedcm-segmentation-platform "
             "flag.";
    }
    case FederatedRequestResult::kSuccess: {
      // Should not be called with success, as we should not add a console
      // message for success.
      NOTREACHED();
    }
  }
}

std::string GetDisconnectConsoleErrorMessage(
    DisconnectStatus disconnect_status_for_metrics) {
  switch (disconnect_status_for_metrics) {
    case DisconnectStatus::kSuccess: {
      NOTREACHED();
    }
    case DisconnectStatus::kTooManyRequests: {
      return "There is a pending disconnect() call.";
    }
    case DisconnectStatus::kUnhandledRequest: {
      return "The disconnect request did not finish by the time the page was "
             "closed.";
    }
    case DisconnectStatus::kNoAccountToDisconnect: {
      return "There is no account to disconnect.";
    }
    case DisconnectStatus::kDisconnectUrlIsCrossOrigin: {
      return "The disconnect URL is cross origin";
    }
    case DisconnectStatus::kDisconnectFailedOnServer: {
      return "The disconnect request failed on the server";
    }
    case DisconnectStatus::kConfigHttpNotFound: {
      return "The config file cannot be found.";
    }
    case DisconnectStatus::kConfigNoResponse: {
      return "The config file returned an error response code.";
    }
    case DisconnectStatus::kConfigInvalidResponse: {
      return "The config file returned some invalid response.";
    }
    case DisconnectStatus::kDisabledInSettings: {
      return "FedCM is disabled by user settings.";
    }
    case DisconnectStatus::kDisabledInFlags: {
      return "The disconnect API is disabled by a flag.";
    }
    case DisconnectStatus::kWellKnownHttpNotFound: {
      return "The well known file cannot be found.";
    }
    case DisconnectStatus::kWellKnownNoResponse: {
      return "The well-known file returned an error response code.";
    }
    case DisconnectStatus::kWellKnownBlockedByConnectionAllowlist: {
      return "The well-known file fetch was blocked by connection allowlist.";
    }
    case DisconnectStatus::kWellKnownInvalidResponse: {
      return "The well-known filed returned some invalid response.";
    }
    case DisconnectStatus::kWellKnownListEmpty: {
      return "The well-known file returned an empty list.";
    }
    case DisconnectStatus::kConfigNotInWellKnown: {
      return "The config file is not in the well-known file.";
    }
    case DisconnectStatus::kWellKnownTooBig: {
      return "Provider's FedCM well-known file contains too many config URLs.";
    }
    case DisconnectStatus::kWellKnownInvalidContentType: {
      return "Provider's well-known content type must be a JSON content type.";
    }
    case DisconnectStatus::kConfigInvalidContentType: {
      return "Provider's FedCM config file content type must be a JSON content "
             "type.";
    }
    case DisconnectStatus::kConfigBlockedByConnectionAllowlist: {
      return "Provider's FedCM config file fetch was blocked by connection "
             "allowlist.";
    }
    case DisconnectStatus::kDisconnectBlockedByConnectionAllowlist: {
      return "The disconnect request was blocked by connection allowlist.";
    }
    case DisconnectStatus::kIdpNotPotentiallyTrustworthy: {
      return "The provider's config file URL is not potentially trustworthy.";
    }
  }
}

std::string FormatUrlToSite(const GURL& url) {
  // We do not use url_formatter::FormatUrlForSecurityDisplay() directly because
  // our UI intentionally shows only the eTLD+1, as it makes for a shorter text
  // that is also clearer to users. The identity provider's well-known file is
  // in the root of the eTLD+1, and sign-in status within identity provider and
  // relying party can be domain-wide because it relies on cookies.
  std::string formatted_url_str =
      net::IsLocalhost(url)
          ? url.GetHost()
          : net::registry_controlled_domains::GetDomainAndRegistry(
                url, kDefaultPrivateRegistryFilter);
  return base::UTF16ToUTF8(url_formatter::FormatUrlForSecurityDisplay(
      GURL(url.GetScheme() + "://" + formatted_url_str),
      url_formatter::SchemeDisplay::OMIT_HTTP_AND_HTTPS));
}

bool HasSharingPermissionOrIdpHasThirdPartyCookiesAccess(
    RenderFrameHost& host,
    const GURL& provider_url,
    const url::Origin& embedder_origin,
    const url::Origin& requester_origin,
    const std::optional<std::string>& account_id,
    FederatedIdentityPermissionContextDelegate* sharing_permission_delegate,
    FederatedIdentityApiPermissionContextDelegate* api_permission_delegate) {
  if (api_permission_delegate->HasThirdPartyCookiesAccess(host, provider_url,
                                                          embedder_origin)) {
    return true;
  }
  if (account_id) {
    return sharing_permission_delegate
        ->GetLastUsedTimestamp(requester_origin, embedder_origin,
                               url::Origin::Create(provider_url), *account_id)
        .has_value();
  }
  return sharing_permission_delegate->HasSharingPermission(
      requester_origin, embedder_origin, url::Origin::Create(provider_url));
}

RequestPageData* GetPageData(Page& page) {
  return RequestPageData::GetOrCreateForPage(page);
}

RequesterFrameType ComputeRequesterFrameType(const RenderFrameHost& rfh,
                                             const url::Origin& requester,
                                             const url::Origin& embedder) {
  // Since FedCM methods are not supported in FencedFrames, we can know whether
  // this is a main frame by calling GetParent().
  if (!rfh.GetParent()) {
    return RequesterFrameType::kMainFrame;
  }
  return net::SchemefulSite::IsSameSite(requester, embedder)
             ? RequesterFrameType::kSameSiteIframe
             : RequesterFrameType::kCrossSiteIframe;
}

void MaybeAddResponseCodeToConsole(RenderFrameHost& render_frame_host,
                                   const char* fetch_description,
                                   int response_code) {
  std::optional<std::string> console_message =
      ComputeConsoleMessageForHttpResponseCode(fetch_description,
                                               response_code);
  if (console_message) {
    render_frame_host.AddMessageToConsole(
        blink::mojom::ConsoleMessageLevel::kError, *console_message);
  }
}

bool DidNavigationHandleHaveActivation(NavigationHandle* handle) {
  return handle != nullptr;
  // TODO(crbug.com/477971553): re-enable the waiving of the user activation
  // requirement outside of agentic mode. The following criteria [1] isn't
  // working as we expected, specifically when redirects are happening inside
  // of pop-up windows.
  // [1] handle->StartedWithTransientActivation()
}

perfetto::NamedTrack CreatePerfettoTrackForFedCM(void* class_pointer) {
  return perfetto::NamedTrack::ThreadScoped(
      "FedCM", reinterpret_cast<uintptr_t>(class_pointer));
}

bool HasEmbedderLoginRequest(RenderFrameHost* rfh) {
  if (!rfh) {
    return false;
  }
  return !!FederatedEmbedderLoginRequest::Get(
      WebContents::FromRenderFrameHost(rfh));
}

}  // namespace content::webid
