// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/signin/core/browser/signin_header_helper.h"

#include <stddef.h>

#include <algorithm>
#include <string_view>

#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/strings/escape.h"
#include "base/strings/string_split.h"
#include "components/google/core/common/google_util.h"
#include "components/signin/core/browser/chrome_connected_header_helper.h"
#include "components/signin/core/browser/dice_response_params.h"
#include "google_apis/gaia/gaia_auth_util.h"
#include "net/http/http_request_headers.h"

#if BUILDFLAG(ENABLE_DICE_SUPPORT)
#include "components/signin/core/browser/dice_header_helper.h"
#endif

namespace signin {

const char kChromeConnectedHeader[] = "X-Chrome-Connected";
const char kChromeManageAccountsHeader[] = "X-Chrome-Manage-Accounts";
const char kAutoLoginHeader[] = "X-Auto-Login";
const char kDiceRequestHeader[] = "X-Chrome-ID-Consistency-Request";
const char kDiceResponseHeader[] = "X-Chrome-ID-Consistency-Response";
const char kDiceLinkedAccountsMetaHeader[] =
    "X-Chrome-ID-Consistency-Linked-Accounts-Meta";

ManageAccountsParams::ManageAccountsParams() = default;

ManageAccountsParams::ManageAccountsParams(const ManageAccountsParams&) =
    default;

ManageAccountsParams& ManageAccountsParams::operator=(
    const ManageAccountsParams&) = default;

RequestAdapter::RequestAdapter(const GURL& url,
                               const net::HttpRequestHeaders& original_headers,
                               net::HttpRequestHeaders* modified_headers,
                               std::vector<std::string>* headers_to_remove)
    : url_(url),
      original_headers_(original_headers),
      modified_headers_(modified_headers),
      headers_to_remove_(headers_to_remove) {
  DCHECK(modified_headers_);
  DCHECK(headers_to_remove_);
}

RequestAdapter::~RequestAdapter() = default;

const GURL& RequestAdapter::GetUrl() {
  return url_;
}

bool RequestAdapter::HasHeader(const std::string& name) {
  return (original_headers_->HasHeader(name) ||
          modified_headers_->HasHeader(name)) &&
         !std::ranges::contains(*headers_to_remove_, name);
}

void RequestAdapter::RemoveRequestHeaderByName(const std::string& name) {
  if (!std::ranges::contains(*headers_to_remove_, name)) {
    headers_to_remove_->push_back(name);
  }
}

void RequestAdapter::SetExtraHeaderByName(const std::string& name,
                                          const std::string& value) {
  modified_headers_->SetHeader(name, value);

  auto it = std::ranges::find(*headers_to_remove_, name);
  if (it != headers_to_remove_->end()) {
    headers_to_remove_->erase(it);
  }
}

std::string BuildMirrorRequestCookieIfPossible(
    const GURL& url,
    const GaiaId& gaia_id,
    AccountConsistencyMethod account_consistency,
    const content_settings::CookieSettings* cookie_settings,
    int profile_mode_mask) {
  return ChromeConnectedHeaderHelper::BuildRequestCookieIfPossible(
      url, gaia_id, account_consistency, cookie_settings, profile_mode_mask);
}

SigninHeaderHelper::SigninHeaderHelper() = default;
SigninHeaderHelper::~SigninHeaderHelper() = default;

bool SigninHeaderHelper::AppendOrRemoveRequestHeader(
    RequestAdapter* request,
    const GURL& redirect_url,
    const char* header_name,
    const std::string& header_value) {
  if (header_value.empty()) {
    // If the request is being redirected, and it has the account consistency
    // header, and current url is a Google URL, and the redirected one is not,
    // remove the header.
    if (!redirect_url.is_empty() && request->HasHeader(header_name) &&
        IsUrlEligibleForRequestHeader(request->GetUrl()) &&
        !IsUrlEligibleForRequestHeader(redirect_url)) {
      VLOG(1) << "Sign-in request header [" << header_name << "] removed.";
      request->RemoveRequestHeaderByName(header_name);
    }
    return false;
  }
  VLOG(1) << "Sign-in request header [" << header_name << "] added.";
  request->SetExtraHeaderByName(header_name, header_value);
  return true;
}

// static
SigninHeaderHelper::ResponseHeaderDictionary
SigninHeaderHelper::ParseAccountConsistencyResponseHeader(
    const std::string& header_value) {
  ResponseHeaderDictionary dictionary;
  for (std::string_view field :
       base::SplitStringPiece(header_value, ",", base::TRIM_WHITESPACE,
                              base::SPLIT_WANT_NONEMPTY)) {
    size_t delim = field.find_first_of('=');
    if (delim == std::string::npos) {
      DLOG(WARNING) << "Unexpected Gaia header field '" << field << "'.";
      continue;
    }
    dictionary.insert({std::string(field.substr(0, delim)),
                       base::UnescapeURLComponent(
                           field.substr(delim + 1),
                           base::UnescapeRule::PATH_SEPARATORS |
                               base::UnescapeRule::
                                   URL_SPECIAL_CHARS_EXCEPT_PATH_SEPARATORS)});
  }
  return dictionary;
}

bool IsUrlEligibleForMirrorCookie(const GURL& url) {
  ChromeConnectedHeaderHelper chrome_connected_helper(
      AccountConsistencyMethod::kMirror);
  return chrome_connected_helper.IsUrlEligibleForRequestHeader(url);
}

void AppendOrRemoveMirrorRequestHeader(
    RequestAdapter* request,
    const GURL& redirect_url,
    const GaiaId& gaia_id,
    Tribool is_child_account,
    AccountConsistencyMethod account_consistency,
    const content_settings::CookieSettings* cookie_settings,
    int profile_mode_mask,
    const std::string& source,
    bool force_account_consistency) {
  const GURL& url = redirect_url.is_empty() ? request->GetUrl() : redirect_url;
  ChromeConnectedHeaderHelper chrome_connected_helper(account_consistency);
  std::string chrome_connected_header_value;

#if BUILDFLAG(IS_CHROMEOS)
  bool is_secondary_account_addition_disallowed =
      (profile_mode_mask & PROFILE_MODE_ADD_ACCOUNT_DISABLED) != 0 &&
      account_consistency == AccountConsistencyMethod::kMirror;
#else
  bool is_secondary_account_addition_disallowed = false;
#endif

  // If Gaia cookies are not allowed, the signin will fail anyway. Do not attach
  // the header, so that the signin goes through the web and the page can show a
  // friendly error message.
  // If Gaia cookies are allowed but other google cookies are not (see
  // https://crbug.com/463848190), then the signin may succeed initially, but
  // the AccountReconcilor may not work properly. To avoid this, Mirror headers
  // are generally not attached, which causes the user to be signed in to the
  // web only and as a result Chrome is unaware of the account. However, when
  // secondary accounts are disallowed by policy, this web-only signin state is
  // not acceptable. In this case send the header anyway, and assume that the
  // Chrome UI will block the signin (if this assumption is not true, then the
  // user may experience inconsistencies due to AccountReconcilor malfunction).
  if (chrome_connected_helper.ShouldBuildRequestHeader(
          url, cookie_settings, /* check_only_gaia_url= */
          is_secondary_account_addition_disallowed)) {
    chrome_connected_header_value = chrome_connected_helper.BuildRequestHeader(
        true /* is_header_request */, url, gaia_id, is_child_account,
        profile_mode_mask, source, force_account_consistency);
  }
  chrome_connected_helper.AppendOrRemoveRequestHeader(
      request, redirect_url, kChromeConnectedHeader,
      chrome_connected_header_value);
}

ManageAccountsParams BuildManageAccountsParams(
    const std::string& header_value) {
  return ChromeConnectedHeaderHelper::BuildManageAccountsParams(header_value);
}

}  // namespace signin
