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

#include "extensions/browser/service_worker/service_worker_host.h"

#include <vector>

#include "base/containers/unique_ptr_adapters.h"
#include "base/trace_event/typed_macros.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/service_worker_context.h"
#include "content/public/browser/service_worker_external_request_result.h"
#include "content/public/browser/storage_partition.h"
#include "extensions/browser/bad_message.h"
#include "extensions/browser/event_router.h"
#include "extensions/browser/extension_function_dispatcher.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_util.h"
#include "extensions/browser/message_service_api.h"
#include "extensions/browser/permissions_manager.h"
#include "extensions/browser/process_map.h"
#include "extensions/browser/service_worker/service_worker_task_queue.h"
#include "extensions/common/api/messaging/port_context.h"
#include "extensions/common/constants.h"
#include "extensions/common/mojom/frame.mojom.h"
#include "extensions/common/permissions/permissions_data.h"
#include "extensions/common/trace_util.h"
#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h"
#include "third_party/blink/public/common/tokens/tokens.h"

namespace extensions {

using perfetto::protos::pbzero::ChromeTrackEvent;

namespace {
const void* const kUserDataKey = &kUserDataKey;

class ServiceWorkerHostList : public base::SupportsUserData::Data {
 public:
  std::vector<std::unique_ptr<ServiceWorkerHost>> list;

  static ServiceWorkerHostList* Get(
      content::RenderProcessHost* render_process_host,
      bool create_if_not_exists) {
    auto* service_worker_host_list = static_cast<ServiceWorkerHostList*>(
        render_process_host->GetUserData(kUserDataKey));
    if (!service_worker_host_list && !create_if_not_exists) {
      return nullptr;
    }
    if (!service_worker_host_list) {
      auto new_host_list = std::make_unique<ServiceWorkerHostList>();
      service_worker_host_list = new_host_list.get();
      render_process_host->SetUserData(kUserDataKey, std::move(new_host_list));
    }
    return service_worker_host_list;
  }
};

}  // namespace

ServiceWorkerHost::ServiceWorkerHost(
    content::RenderProcessHost* render_process_host,
    mojo::PendingAssociatedReceiver<mojom::ServiceWorkerHost> receiver)
    : render_process_host_(render_process_host) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  dispatcher_ =
      std::make_unique<ExtensionFunctionDispatcher>(GetBrowserContext());
  receiver_.Bind(std::move(receiver));
  receiver_.set_disconnect_handler(base::BindOnce(
      &ServiceWorkerHost::RemoteDisconnected, base::Unretained(this)));

  render_process_host_->AddObserver(this);
}

ServiceWorkerHost::~ServiceWorkerHost() {
  render_process_host_->RemoveObserver(this);
}

// static
void ServiceWorkerHost::BindReceiver(
    int render_process_id,
    mojo::PendingAssociatedReceiver<mojom::ServiceWorkerHost> receiver) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  auto* render_process_host =
      content::RenderProcessHost::FromID(render_process_id);
  if (!render_process_host) {
    return;
  }
  auto* service_worker_host_list = ServiceWorkerHostList::Get(
      render_process_host, /*create_if_not_exists=*/true);
  service_worker_host_list->list.push_back(std::make_unique<ServiceWorkerHost>(
      render_process_host, std::move(receiver)));
}

// static
std::vector<ServiceWorkerHost*> ServiceWorkerHost::GetServiceWorkerHostList(
    content::RenderProcessHost* rph) {
  ServiceWorkerHostList* host_list =
      ServiceWorkerHostList::Get(rph, /*create_if_not_exists=*/false);
  if (!host_list) {
    return {};
  }
  // Copy the vector of unique pointers to a vector of raw pointers so we're
  // not handing out our owned memory to the caller.
  std::vector<ServiceWorkerHost*> hosts;
  hosts.reserve(host_list->list.size());
  for (const std::unique_ptr<ServiceWorkerHost>& host : host_list->list) {
    hosts.push_back(host.get());
  }
  return hosts;
}

// static
ServiceWorkerHost* ServiceWorkerHost::GetWorkerFor(const WorkerId& worker_id) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  auto* render_process_host =
      content::RenderProcessHost::FromID(worker_id.render_process_id);
  if (!render_process_host) {
    return nullptr;
  }

  auto* service_worker_host_list = ServiceWorkerHostList::Get(
      render_process_host, /*create_if_not_exists=*/false);
  if (!service_worker_host_list) {
    return nullptr;
  }
  for (auto& worker : service_worker_host_list->list) {
    if (worker->worker_id_ == worker_id) {
      return worker.get();
    }
  }
  return nullptr;
}

void ServiceWorkerHost::RemoteDisconnected() {
  receiver_.reset();
  Destroy();
  // This instance has now been destroyed.
}

void ServiceWorkerHost::DidInitializeServiceWorkerContext(
    const ExtensionId& extension_id,
    int64_t service_worker_version_id,
    int worker_thread_id,
    const blink::ServiceWorkerToken& service_worker_token,
    mojo::PendingAssociatedRemote<mojom::EventDispatcher> event_dispatcher) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  content::BrowserContext* browser_context = GetBrowserContext();
  if (!browser_context) {
    return;
  }

  // Ensure the worker thread ID is not the main thread ID as that is
  // invalid input.
  if (worker_thread_id == kMainThreadId) {
    bad_message::ReceivedBadMessage(render_process_host_,
                                    bad_message::SWH_BAD_WORKER_THREAD_ID);
    return;
  }

  ExtensionRegistry* registry = ExtensionRegistry::Get(browser_context);
  DCHECK(registry);
  if (!registry->enabled_extensions().GetByID(extension_id)) {
    // This can happen if the extension is unloaded at this point. Just
    // checking the extension process (as below) is insufficient because
    // tearing down processes is async and happens after extension unload.
    return;
  }

  int render_process_id = render_process_host_->GetDeprecatedID();
  auto* process_map = ProcessMap::Get(browser_context);
  if (!process_map || !process_map->Contains(extension_id, render_process_id)) {
    // We check the process in addition to the registry to guard against
    // situations in which an extension may still be enabled, but no longer
    // running in a given process.
    return;
  }
  worker_id_.extension_id = extension_id;
  worker_id_.version_id = service_worker_version_id;
  worker_id_.render_process_id = render_process_id;
  worker_id_.thread_id = worker_thread_id;

  ServiceWorkerTaskQueue::Get(browser_context)
      ->RendererDidInitializeServiceWorkerContext(
          render_process_id, extension_id, service_worker_version_id,
          worker_thread_id, service_worker_token);
  EventRouter::Get(browser_context)
      ->BindServiceWorkerEventDispatcher(render_process_id, worker_thread_id,
                                         std::move(event_dispatcher));
}

void ServiceWorkerHost::DidStartServiceWorkerContext(
    const ExtensionId& extension_id,
    const base::UnguessableToken& activation_token,
    const GURL& service_worker_scope,
    int64_t service_worker_version_id,
    int worker_thread_id) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  content::BrowserContext* browser_context = GetBrowserContext();
  if (!browser_context) {
    return;
  }

  DCHECK_NE(kMainThreadId, worker_thread_id);
  int render_process_id = render_process_host_->GetDeprecatedID();
  auto* process_map = ProcessMap::Get(browser_context);
  if (!process_map || !process_map->Contains(extension_id, render_process_id)) {
    // We can legitimately get here if the extension was already unloaded.
    return;
  }
  CHECK((service_worker_scope.SchemeIs(kExtensionScheme)
#if BUILDFLAG(ARKWEB_ARKWEB_EXTENSIONS)
         || service_worker_scope.SchemeIs(kArkwebExtensionScheme)
#endif
             ) &&
        extension_id == service_worker_scope.host());

  ServiceWorkerTaskQueue::Get(browser_context)
      ->RendererDidStartServiceWorkerContext(
          render_process_id, extension_id, activation_token,
          service_worker_scope, service_worker_version_id, worker_thread_id);
}

void ServiceWorkerHost::DidStopServiceWorkerContext(
    const ExtensionId& extension_id,
    const base::UnguessableToken& activation_token,
    const GURL& service_worker_scope,
    int64_t service_worker_version_id,
    int worker_thread_id) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  content::BrowserContext* browser_context = GetBrowserContext();
  if (!browser_context) {
    return;
  }

  DCHECK_NE(kMainThreadId, worker_thread_id);
  int render_process_id = render_process_host_->GetDeprecatedID();
  auto* process_map = ProcessMap::Get(browser_context);
  if (!process_map || !process_map->Contains(extension_id, render_process_id)) {
    // We can legitimately get here if the extension was already unloaded.
    return;
  }
  CHECK((service_worker_scope.SchemeIs(kExtensionScheme)
#if BUILDFLAG(ARKWEB_ARKWEB_EXTENSIONS)
        || service_worker_scope.SchemeIs(kArkwebExtensionScheme)
#endif
  ) &&
        extension_id == service_worker_scope.host());
  CHECK_NE(blink::mojom::kInvalidServiceWorkerVersionId,
           service_worker_version_id);
  ServiceWorkerTaskQueue::Get(browser_context)
      ->RendererDidStopServiceWorkerContext(
          render_process_id, extension_id, activation_token,
          service_worker_scope, service_worker_version_id, worker_thread_id);
}

void ServiceWorkerHost::RequestWorker(mojom::RequestParamsPtr params,
                                      RequestWorkerCallback callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  if (!GetBrowserContext()) {
    std::move(callback).Run(/*kFailed=*/true, base::Value::List(),
                            "No browser context", nullptr);
    return;
  }

  dispatcher_->DispatchForServiceWorker(std::move(params),
                                        render_process_host_->GetDeprecatedID(),
                                        std::move(callback));
}

void ServiceWorkerHost::WorkerResponseAck(const base::Uuid& request_uuid) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  if (!GetBrowserContext()) {
    return;
  }

  dispatcher_->ProcessResponseAck(request_uuid);
}

content::BrowserContext* ServiceWorkerHost::GetBrowserContext() {
  return render_process_host_->GetBrowserContext();
}

mojom::ServiceWorker* ServiceWorkerHost::GetServiceWorker() {
  if (!remote_.is_bound()) {
    content::ServiceWorkerContext* context =
        util::GetServiceWorkerContextForExtensionId(worker_id_.extension_id,
                                                    GetBrowserContext());
    CHECK(context);
    if (!context->IsLiveRunningServiceWorker(worker_id_.version_id)) {
      return nullptr;
    }

    context->GetRemoteAssociatedInterfaces(worker_id_.version_id)
        .GetInterface(&remote_);
  }
  return remote_.get();
}

void ServiceWorkerHost::UpdateExtensionPermissions(
    const Extension& extension,
    const PermissionSet& permissions) {
  if (extension.id() != worker_id_.extension_id) {
    return;
  }
  content::BrowserContext* browser_context = GetBrowserContext();
  if (!browser_context) {
    return;
  }
  content::ServiceWorkerContext* context =
      util::GetServiceWorkerContextForExtensionId(worker_id_.extension_id,
                                                  browser_context);
  CHECK(context);
  auto* service_worker_remote = GetServiceWorker();
  if (!service_worker_remote) {
    return;
  }

  const PermissionsData* permissions_data = extension.permissions_data();
  service_worker_remote->UpdatePermissions(
      std::move(*permissions_data->active_permissions().Clone()),
      std::move(*permissions_data->withheld_permissions().Clone()));
}

void ServiceWorkerHost::OpenChannelToExtension(
    extensions::mojom::ExternalConnectionInfoPtr info,
    extensions::mojom::ChannelType channel_type,
    const std::string& channel_name,
    const PortId& port_id,
    mojo::PendingAssociatedRemote<extensions::mojom::MessagePort> port,
    mojo::PendingAssociatedReceiver<extensions::mojom::MessagePortHost>
        port_host) {
  TRACE_EVENT("extensions", "ServiceWorkerHost::OpenChannelToExtension",
              ChromeTrackEvent::kRenderProcessHost, *render_process_host_);
  content::BrowserContext* browser_context = GetBrowserContext();
  if (!browser_context) {
    return;
  }

  MessageServiceApi::GetMessageService()->OpenChannelToExtension(
      browser_context, worker_id_, port_id, *info, channel_type, channel_name,
      std::move(port), std::move(port_host));
}

void ServiceWorkerHost::OpenChannelToNativeApp(
    const std::string& native_app_name,
    const PortId& port_id,
    mojo::PendingAssociatedRemote<extensions::mojom::MessagePort> port,
    mojo::PendingAssociatedReceiver<extensions::mojom::MessagePortHost>
        port_host) {
  TRACE_EVENT("extensions", "ServiceWorkerHost::OnOpenChannelToNativeApp",
              ChromeTrackEvent::kRenderProcessHost, *render_process_host_);
  content::BrowserContext* browser_context = GetBrowserContext();
  if (!browser_context) {
    return;
  }

  MessageServiceApi::GetMessageService()->OpenChannelToNativeApp(
      browser_context, worker_id_, port_id, native_app_name, std::move(port),
      std::move(port_host));
}

void ServiceWorkerHost::OpenChannelToTab(
    int32_t tab_id,
    int32_t frame_id,
    const std::optional<std::string>& document_id,
    extensions::mojom::ChannelType channel_type,
    const std::string& channel_name,
    const PortId& port_id,
    mojo::PendingAssociatedRemote<extensions::mojom::MessagePort> port,
    mojo::PendingAssociatedReceiver<extensions::mojom::MessagePortHost>
        port_host) {
  TRACE_EVENT("extensions", "ServiceWorkerHost::OpenChannelToTab",
              ChromeTrackEvent::kRenderProcessHost, *render_process_host_);
  content::BrowserContext* browser_context = GetBrowserContext();
  if (!browser_context) {
    return;
  }

  MessageServiceApi::GetMessageService()->OpenChannelToTab(
      browser_context, worker_id_, port_id, tab_id, frame_id,
      document_id ? *document_id : std::string(), channel_type, channel_name,
      std::move(port), std::move(port_host));
}

void ServiceWorkerHost::Destroy() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  auto* service_worker_host_list = ServiceWorkerHostList::Get(
      render_process_host_, /*create_if_not_exists=*/false);
  CHECK(service_worker_host_list);
  // std::erase_if will lead to a call to the destructor for this object.
  std::erase_if(service_worker_host_list->list, base::MatchesUniquePtr(this));
}

void ServiceWorkerHost::RenderProcessExited(
    content::RenderProcessHost* host,
    const content::ChildProcessTerminationInfo& info) {
  CHECK_EQ(host, render_process_host_);
  // TODO(crbug.com/40062641): Investigate clearing the user data from
  // RenderProcessHostImpl::Cleanup.
  Destroy();
  // This instance has now been deleted.
}

}  // namespace extensions