// 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 "content/browser/permissions/permission_service_context.h"

#include <optional>
#include <utility>

#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "content/browser/permissions/permission_controller_impl.h"
#include "content/browser/permissions/permission_service_impl.h"
#include "content/browser/permissions/permission_util.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/navigation_handle.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 "mojo/public/cpp/bindings/remote.h"
#include "third_party/blink/public/common/permissions/permission_utils.h"
#include "third_party/blink/public/mojom/permissions/permission_status.mojom.h"
#include "url/origin.h"

namespace content {

// A holder owning document-associated PermissionServiceContext. The holder is
// used as PermissionServiceContext itself can't inherit from
// DocumentUserData, as PermissionServiceContext (unlike
// DocumentUserData) can be created when RenderFrameHost doesn't exist
// (e.g. for service workers).
struct PermissionServiceContext::DocumentPermissionServiceContextHolder
    : public DocumentUserData<DocumentPermissionServiceContextHolder> {
  explicit DocumentPermissionServiceContextHolder(RenderFrameHost* rfh)
      : DocumentUserData<DocumentPermissionServiceContextHolder>(rfh),
        permission_service_context(rfh) {}

  PermissionServiceContext permission_service_context;

  DOCUMENT_USER_DATA_KEY_DECL();
};

DOCUMENT_USER_DATA_KEY_IMPL(
    PermissionServiceContext::DocumentPermissionServiceContextHolder);

class PermissionServiceContext::PermissionSubscription {
 public:
  PermissionSubscription(
      blink::mojom::PermissionName permission_name,
      blink::mojom::PermissionStatusWithDetailsPtr last_known_status,
      PermissionServiceContext* context,
      mojo::PendingRemote<blink::mojom::PermissionObserver> observer)
      : permission_name_(permission_name),
        last_known_status_(std::move(last_known_status)),
        context_(context),
        observer_(std::move(observer)) {
    observer_.set_disconnect_handler(base::BindOnce(
        &PermissionSubscription::OnConnectionError, base::Unretained(this)));
  }
  PermissionSubscription(const PermissionSubscription&) = delete;
  PermissionSubscription& operator=(const PermissionSubscription&) = delete;

  ~PermissionSubscription() {
    DCHECK(id_);
    BrowserContext* browser_context = context_->GetBrowserContext();
    if (browser_context) {
      PermissionControllerImpl::FromBrowserContext(browser_context)
          ->UnsubscribeFromPermissionResultChange(id_);
    }
  }

  void OnConnectionError() {
    DCHECK(id_);
    context_->ObserverHadConnectionError(id_);
  }

  void StoreResultAtBFCacheEntry() {
    status_at_bf_cache_entry_ = last_known_status_.Clone();
  }

  void NotifyPermissionResultChangedIfNeeded() {
    DCHECK(status_at_bf_cache_entry_);
    if (!status_at_bf_cache_entry_->Equals(*last_known_status_)) {
      observer_->OnPermissionStatusChange(last_known_status_.Clone());
    }
    status_at_bf_cache_entry_.reset();
  }

  void OnPermissionStatusChanged(PermissionResult permission_result) {
    if (!observer_.is_connected()) {
      return;
    }

    blink::mojom::PermissionStatusWithDetailsPtr new_status =
        PermissionUtil::ToPermissionStatusWithDetails(permission_name_,
                                                      permission_result);
    if (new_status == last_known_status_) {
      return;
    }

    last_known_status_ = std::move(new_status);

    // Dispatching events while in BFCache is redundant. Permissions code in
    // renderer process would decide to drop the event by looking at document's
    // active status.
    if (!status_at_bf_cache_entry_) {
      observer_->OnPermissionStatusChange(last_known_status_.Clone());
    }
  }

  void set_id(PermissionController::SubscriptionId id) { id_ = id; }

  base::WeakPtr<PermissionSubscription> GetWeakPtr() {
    return weak_ptr_factory_.GetWeakPtr();
  }

 private:
  blink::mojom::PermissionName permission_name_;
  blink::mojom::PermissionStatusWithDetailsPtr last_known_status_;
  const raw_ptr<PermissionServiceContext> context_;
  mojo::Remote<blink::mojom::PermissionObserver> observer_;
  PermissionController::SubscriptionId id_;

  // Optional variable to store the last status before the corresponding
  // RenderFrameHost enters BFCache, and will be cleared when the
  // RenderFrameHost is restored from BFCache. Non-empty value indicates that
  // the RenderFrameHost is in BFCache.
  blink::mojom::PermissionStatusWithDetailsPtr status_at_bf_cache_entry_;
  base::WeakPtrFactory<PermissionSubscription> weak_ptr_factory_{this};
};

// static
PermissionServiceContext*
PermissionServiceContext::GetOrCreateForCurrentDocument(
    RenderFrameHost* render_frame_host) {
  return &DocumentPermissionServiceContextHolder::GetOrCreateForCurrentDocument(
              render_frame_host)
              ->permission_service_context;
}

// static
PermissionServiceContext* PermissionServiceContext::GetForCurrentDocument(
    RenderFrameHost* render_frame_host) {
  auto* holder = DocumentPermissionServiceContextHolder::GetForCurrentDocument(
      render_frame_host);
  return holder ? &holder->permission_service_context : nullptr;
}

PermissionServiceContext::PermissionServiceContext(
    RenderFrameHost* render_frame_host)
    : render_frame_host_(render_frame_host), render_process_host_(nullptr) {
  render_frame_host->GetProcess()->AddObserver(this);
}

PermissionServiceContext::PermissionServiceContext(
    RenderProcessHost* render_process_host)
    : render_frame_host_(nullptr), render_process_host_(render_process_host) {}

PermissionServiceContext::~PermissionServiceContext() {
  if (render_frame_host_) {
    render_frame_host_->GetProcess()->RemoveObserver(this);
  }
}

void PermissionServiceContext::CreateService(
    mojo::PendingReceiver<blink::mojom::PermissionService> receiver) {
  DCHECK(render_frame_host_);
  services_.Add(
      std::make_unique<PermissionServiceImpl>(
          this, url::Origin::Create(PermissionUtil::GetLastCommittedOriginAsURL(
                    render_frame_host_))),
      std::move(receiver));
}

void PermissionServiceContext::CreateServiceForWorker(
    const url::Origin& origin,
    mojo::PendingReceiver<blink::mojom::PermissionService> receiver) {
  services_.Add(std::make_unique<PermissionServiceImpl>(this, origin),
                std::move(receiver));
}

void PermissionServiceContext::CreateSubscription(
    const blink::mojom::PermissionDescriptorPtr& permission,
    const url::Origin& origin,
    PermissionResult current_result,
    blink::mojom::PermissionStatusWithDetailsPtr last_known_status,
    bool should_include_device_status,
    mojo::PendingRemote<blink::mojom::PermissionObserver> observer) {
  BrowserContext* browser_context = GetBrowserContext();
  if (!browser_context) {
    return;
  }

  auto subscription = std::make_unique<PermissionSubscription>(
      permission->name, last_known_status.Clone(), this, std::move(observer));

  subscription->OnPermissionStatusChanged(current_result);

  if (render_frame_host_ &&
      render_frame_host_->IsInLifecycleState(
          content::RenderFrameHost::LifecycleState::kInBackForwardCache)) {
    subscription->StoreResultAtBFCacheEntry();
  }

  GURL requesting_origin(origin.Serialize());
  auto subscription_id =
      PermissionControllerImpl::FromBrowserContext(browser_context)
          ->SubscribeToPermissionResultChange(
              permission->Clone(), render_process_host_, render_frame_host_,
              requesting_origin, should_include_device_status,
              base::BindRepeating(
                  &PermissionSubscription::OnPermissionStatusChanged,
                  subscription->GetWeakPtr()));
  subscription->set_id(subscription_id);
  subscriptions_[subscription_id] = std::move(subscription);
}

void PermissionServiceContext::ObserverHadConnectionError(
    PermissionController::SubscriptionId subscription_id) {
  size_t erased = subscriptions_.erase(subscription_id);
  DCHECK_EQ(1u, erased);
}

BrowserContext* PermissionServiceContext::GetBrowserContext() const {
  if (render_frame_host_)
    return render_frame_host_->GetBrowserContext();

  if (render_process_host_)
    return render_process_host_->GetBrowserContext();

  return nullptr;
}

std::optional<GURL> PermissionServiceContext::GetEmbeddingOrigin() const {
  if (render_frame_host_) {
    GURL origin_as_url(PermissionUtil::GetLastCommittedOriginAsURL(
        render_frame_host_->GetMainFrame()));
    if (!origin_as_url.is_empty()) {
      return origin_as_url;
    }
  }
  return std::nullopt;
}

void PermissionServiceContext::RenderProcessHostDestroyed(
    RenderProcessHost* host) {
  DCHECK(host == render_frame_host_->GetProcess());
  subscriptions_.clear();
  // RenderProcessHostImpl will always outlive 'this', but it gets cleaned up
  // earlier so we need to listen to this event so we can do our clean up as
  // well.
  host->RemoveObserver(this);
}

void PermissionServiceContext::StoreResultAtBFCacheEntry() {
  for (auto& iter : subscriptions_) {
    iter.second->StoreResultAtBFCacheEntry();
  }
}

void PermissionServiceContext::NotifyPermissionResultChangedIfNeeded() {
  for (auto& iter : subscriptions_) {
    iter.second->NotifyPermissionResultChangedIfNeeded();
  }
}

}  // namespace content
