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

#include "android_webview/browser/network_service/aw_proxying_restricted_cookie_manager.h"

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

#include "android_webview/browser/aw_browser_context.h"
#include "android_webview/browser/aw_cookie_access_policy.h"
#include "android_webview/browser/cookie_manager.h"
#include "android_webview/common/aw_features.h"
#include "base/check.h"
#include "base/feature_list.h"
#include "base/memory/ptr_util.h"
#include "base/memory/read_only_shared_memory_region.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "mojo/public/cpp/base/shared_memory_version.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "net/cookies/parsed_cookie.h"
#include "net/storage_access_api/status.h"
#include "services/network/public/mojom/restricted_cookie_manager.mojom-shared.h"
#include "url/gurl.h"

namespace android_webview {

namespace {

using PrivacySetting = net::NetworkDelegate::PrivacySetting;

}  // namespace

class AwProxyingRestrictedCookieManagerListener
    : public network::mojom::CookieChangeListener {
 public:
  AwProxyingRestrictedCookieManagerListener(
      const GURL& url,
      const net::SiteForCookies& site_for_cookies,
      base::WeakPtr<AwProxyingRestrictedCookieManager>
          aw_restricted_cookie_manager,
      mojo::PendingRemote<network::mojom::CookieChangeListener> client_listener)
      : url_(url),
        site_for_cookies_(site_for_cookies),
        aw_restricted_cookie_manager_(aw_restricted_cookie_manager),
        client_listener_(std::move(client_listener)) {}

  void OnCookieChange(const net::CookieChangeInfo& change) override {
    if (aw_restricted_cookie_manager_) {
      PrivacySetting cookieState =
          aw_restricted_cookie_manager_->AllowCookies(url_);

      if (cookieState == PrivacySetting::kStateAllowed ||
          (cookieState == PrivacySetting::kPartitionedStateAllowedOnly &&
           change.cookie.IsPartitioned())) {
        client_listener_->OnCookieChange(change);
      }
    }
  }

 private:
  const GURL url_;
  const net::SiteForCookies site_for_cookies_;
  base::WeakPtr<AwProxyingRestrictedCookieManager>
      aw_restricted_cookie_manager_;
  mojo::Remote<network::mojom::CookieChangeListener> client_listener_;
};

// static
void AwProxyingRestrictedCookieManager::CreateAndBind(
    mojo::PendingRemote<network::mojom::RestrictedCookieManager> underlying_rcm,
    bool is_service_worker,
    int process_id,
    int frame_id,
    const net::SiteForCookies& site_for_cookies,
    mojo::PendingReceiver<network::mojom::RestrictedCookieManager> receiver,
    AwCookieAccessPolicy* aw_cookie_access_policy) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  std::optional<content::GlobalRenderFrameHostToken> frame_token;
  if (is_service_worker) {
    // A nullopt global_frame_token_ is used for service workers, so no value
    // for the per-WebView acceptThirdPartyCookies web setting can be obtained.
    // However, without third-party storage partitioning, WebView service
    // workers always have a first-party (same-site) site_for_cookies.
    CHECK(!site_for_cookies.IsNull(), base::NotFatalUntil::M159);
  } else {
    if (auto* rfh = content::RenderFrameHost::FromID(process_id, frame_id)) {
      frame_token = rfh->GetGlobalFrameToken();
    }
  }

  content::GetIOThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(
          &AwProxyingRestrictedCookieManager::CreateAndBindOnIoThread,
          std::move(underlying_rcm), frame_token, site_for_cookies,
          std::move(receiver), aw_cookie_access_policy));
}

AwProxyingRestrictedCookieManager::~AwProxyingRestrictedCookieManager() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
}

void AwProxyingRestrictedCookieManager::GetAllForUrl(
    const GURL& url,
    const net::SiteForCookies& /*site_for_cookies*/,
    const url::Origin& top_frame_origin,
    net::StorageAccessApiStatus /*storage_access_api_status*/,
    network::mojom::CookieManagerGetOptionsPtr options,
    bool is_ad_tagged,
    bool apply_devtools_overrides,
    bool force_disable_third_party_cookies,
    GetAllForUrlCallback callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::IO);

  PrivacySetting cookieState = AllowCookies(url);

  if (cookieState == PrivacySetting::kStateDisallowed) {
    std::move(callback).Run(std::vector<net::CookieWithAccessResult>());
    return;
  }

  bool disable_3pcs =
      force_disable_third_party_cookies ||
      cookieState == PrivacySetting::kPartitionedStateAllowedOnly;

  // WebView does not currently have a way to grant storage access requests with
  // user consent so we default this to be none.
  underlying_restricted_cookie_manager_->GetAllForUrl(
      url, site_for_cookies_, top_frame_origin,
      net::StorageAccessApiStatus::kNone, std::move(options), is_ad_tagged,
      apply_devtools_overrides, disable_3pcs, std::move(callback));
}

void AwProxyingRestrictedCookieManager::SetCanonicalCookie(
    network::mojom::RestrictedCanonicalCookieParamsPtr cookie_params,
    const GURL& url,
    const net::SiteForCookies& /*site_for_cookies*/,
    const url::Origin& top_frame_origin,
    net::StorageAccessApiStatus /*storage_access_api_status*/,
    bool is_ad_tagged,
    bool apply_devtools_overrides,
    SetCanonicalCookieCallback callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
  PrivacySetting cookieState = AllowCookies(url);

  if (cookieState == PrivacySetting::kStateDisallowed) {
    std::move(callback).Run(false);
    return;
  }

  if (cookie_params->partitioned ==
          network::mojom::RestrictedCookiePartition::PARTITIONED ||
      cookieState == PrivacySetting::kStateAllowed) {
    // WebView does not currently have a way to grant storage access requests
    // with user consent so we default this to be none.
    underlying_restricted_cookie_manager_->SetCanonicalCookie(
        std::move(cookie_params), url, site_for_cookies_, top_frame_origin,
        net::StorageAccessApiStatus::kNone, is_ad_tagged,
        apply_devtools_overrides, std::move(callback));
  } else {
    std::move(callback).Run(false);
  }
}

void AwProxyingRestrictedCookieManager::AddChangeListener(
    const GURL& url,
    const net::SiteForCookies& /*site_for_cookies*/,
    const url::Origin& top_frame_origin,
    net::StorageAccessApiStatus /*storage_access_api_status*/,
    mojo::PendingRemote<network::mojom::CookieChangeListener> listener,
    AddChangeListenerCallback callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::IO);

  mojo::PendingRemote<network::mojom::CookieChangeListener>
      proxy_listener_remote;
  auto proxy_listener =
      std::make_unique<AwProxyingRestrictedCookieManagerListener>(
          url, site_for_cookies_, weak_factory_.GetWeakPtr(),
          std::move(listener));

  mojo::MakeSelfOwnedReceiver(
      std::move(proxy_listener),
      proxy_listener_remote.InitWithNewPipeAndPassReceiver());

  // WebView does not currently have a way to grant storage access requests with
  // user consent so we default this to be none.
  underlying_restricted_cookie_manager_->AddChangeListener(
      url, site_for_cookies_, top_frame_origin,
      net::StorageAccessApiStatus::kNone, std::move(proxy_listener_remote),
      std::move(callback));
}

void AwProxyingRestrictedCookieManager::SetCookieFromString(
    const GURL& url,
    const net::SiteForCookies& /*site_for_cookies*/,
    const url::Origin& top_frame_origin,
    net::StorageAccessApiStatus /*storage_access_api_status*/,
    bool is_ad_tagged,
    bool apply_devtools_overrides,
    const std::string& cookie) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::IO);

  PrivacySetting cookieState = AllowCookies(url);

  if (cookieState == PrivacySetting::kStateDisallowed) {
    return;
  }

  // We have to do a quick parsing of the cookie string just to see if the
  // partitioned header is set before handing over the string to the restricted
  // cookie manager.
  net::ParsedCookie parsed_cookie(cookie);

  if (cookieState == PrivacySetting::kStateAllowed ||
      (parsed_cookie.IsValid() && parsed_cookie.IsPartitioned() &&
       parsed_cookie.IsSecure())) {
    // WebView does not currently have a way to grant storage access requests
    // with user consent so we default this to be none.
    underlying_restricted_cookie_manager_->SetCookieFromString(
        url, site_for_cookies_, top_frame_origin,
        net::StorageAccessApiStatus::kNone, is_ad_tagged,
        apply_devtools_overrides, cookie);
  }
}

void AwProxyingRestrictedCookieManager::GetCookiesString(
    const GURL& url,
    const net::SiteForCookies& /*site_for_cookies*/,
    const url::Origin& top_frame_origin,
    net::StorageAccessApiStatus /*storage_access_api_status*/,
    bool get_version_shared_memory,
    bool is_ad_tagged,
    bool apply_devtools_overrides,
    bool force_disable_third_party_cookies,
    GetCookiesStringCallback callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::IO);

  PrivacySetting cookieState = AllowCookies(url);

  if (cookieState == PrivacySetting::kStateDisallowed) {
    std::move(callback).Run(network::mojom::kInvalidCookieVersion,
                            base::ReadOnlySharedMemoryRegion(), "");
    return;
  }

  bool disable_3pcs =
      force_disable_third_party_cookies ||
      cookieState == PrivacySetting::kPartitionedStateAllowedOnly;

  // When using latched cookie policy, enable shared memory versioning since
  // the policy won't change during this RCM's lifetime. When the feature is
  // disabled, never request shared memory so that a full IPC is issued every
  // time, preventing clients from retaining access past the moment where access
  // was denied. See crbug.com/1393050 for original issue.
  const bool use_shared_memory =
      base::FeatureList::IsEnabled(features::kWebViewLatchedCookiePolicy) &&
      get_version_shared_memory;

  // WebView does not currently have a way to grant storage access requests with
  // user consent so we default this to be none.
  underlying_restricted_cookie_manager_->GetCookiesString(
      url, site_for_cookies_, top_frame_origin,
      net::StorageAccessApiStatus::kNone, use_shared_memory, is_ad_tagged,
      apply_devtools_overrides, disable_3pcs, std::move(callback));
}

void AwProxyingRestrictedCookieManager::CookiesEnabledFor(
    const GURL& url,
    const net::SiteForCookies& /*site_for_cookies*/,
    const url::Origin& top_frame_origin,
    net::StorageAccessApiStatus /*storage_access_api_status*/,
    bool apply_devtools_overrides,
    CookiesEnabledForCallback callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
  std::move(callback).Run(AllowCookies(url) == PrivacySetting::kStateAllowed);
}

AwProxyingRestrictedCookieManager::AwProxyingRestrictedCookieManager(
    mojo::PendingRemote<network::mojom::RestrictedCookieManager>
        underlying_restricted_cookie_manager,
    const std::optional<const content::GlobalRenderFrameHostToken>&
        global_frame_token,
    const net::SiteForCookies& site_for_cookies,
    AwCookieAccessPolicy* cookie_access_policy)
    : underlying_restricted_cookie_manager_(
          std::move(underlying_restricted_cookie_manager)),
      global_frame_token_(global_frame_token),
      cookie_access_policy_(*cookie_access_policy),
      site_for_cookies_(site_for_cookies) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::IO);

  // Latch cookie policy settings when feature is enabled. This allows shared
  // memory cookie versioning to work since the policy won't change during this
  // RCM's lifetime.
  if (base::FeatureList::IsEnabled(features::kWebViewLatchedCookiePolicy)) {
    latched_accept_cookies_ = cookie_access_policy_->GetShouldAcceptCookies();
    latched_accept_third_party_ =
        cookie_access_policy_->GetShouldAcceptThirdPartyCookies(
            global_frame_token_);
  }
}

// static
void AwProxyingRestrictedCookieManager::CreateAndBindOnIoThread(
    mojo::PendingRemote<network::mojom::RestrictedCookieManager> underlying_rcm,
    const std::optional<const content::GlobalRenderFrameHostToken>&
        global_frame_token,
    const net::SiteForCookies& site_for_cookies,
    mojo::PendingReceiver<network::mojom::RestrictedCookieManager> receiver,
    AwCookieAccessPolicy* cookie_access_policy) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
  auto wrapper = base::WrapUnique(new AwProxyingRestrictedCookieManager(
      std::move(underlying_rcm), global_frame_token, site_for_cookies,
      cookie_access_policy));
  mojo::MakeSelfOwnedReceiver(std::move(wrapper), std::move(receiver));
}

PrivacySetting AwProxyingRestrictedCookieManager::AllowCookies(
    const GURL& url) const {
  // When feature is enabled, use latched cookie policy state captured at
  // construction time. This enables shared memory cookie versioning.
  if (base::FeatureList::IsEnabled(features::kWebViewLatchedCookiePolicy)) {
    // Note: For service workers, cookie policy updates may be slightly
    // delayed. Service workers are only killed after some seconds of
    // inactivity (no controlled fetch events). This means a cookie policy
    // change may apply to a new page, but a reused service worker will still
    // have the old cookie policy until it is terminated and recreated. We
    // accept this as a limited edge case unlikely to cause issues in
    // practice.
    return AwCookieAccessPolicy::CanAccessCookies(url, site_for_cookies_,
                                                  latched_accept_cookies_,
                                                  latched_accept_third_party_);
  }

  // Original dynamic behavior.
  return cookie_access_policy_->AllowCookies(url, site_for_cookies_,
                                             global_frame_token_);
}

}  // namespace android_webview
