blob: 3e4013579555cb970e91429b045dcf88f21bdd58 [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/devtools/protocol/extensions_handler.h"
#include <memory>
#include <string>
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/devtools/chrome_devtools_manager_delegate.h"
#include "chrome/browser/devtools/protocol/extensions.h"
#include "chrome/browser/devtools/protocol/protocol.h"
#include "chrome/browser/extensions/unpacked_installer.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "content/public/browser/devtools_agent_host.h"
#include "content/public/browser/service_worker_context.h"
#include "content/public/browser/web_contents.h"
#include "extensions/browser/api/storage/storage_area_namespace.h"
#include "extensions/browser/api/storage/storage_utils.h"
#include "extensions/browser/extension_registrar.h"
#include "extensions/browser/extension_util.h"
namespace {
// Gets an extension with ID `id`. If no extension is found, returns a
// std::nullopt.
std::optional<const extensions::Extension*> GetExtension(
const std::string& id,
scoped_refptr<content::DevToolsAgentHost> host) {
content::BrowserContext* context = host->GetBrowserContext();
if (!context) {
return std::nullopt;
}
extensions::ExtensionRegistry* registry =
extensions::ExtensionRegistry::Get(context);
const extensions::Extension* extension =
registry->enabled_extensions().GetByID(id);
return extension ? std::optional(extension) : std::nullopt;
}
// Returns true if `host` should be able to access data from `storage_area` for
// the given `extension`.
bool CanAccessStorage(scoped_refptr<content::DevToolsAgentHost> host,
const extensions::Extension& extension,
extensions::StorageAreaNamespace storage_area) {
content::BrowserContext* context = host->GetBrowserContext();
if (!context) {
return false;
}
// Allow a service worker to access extension storage if it corresponds to
// the extension whose storage is being accessed.
if (host->GetType() == content::DevToolsAgentHost::kTypeServiceWorker) {
if (!host->GetProcessHost()) {
return false;
}
return extensions::storage_utils::CanRendererAccessExtensionStorage(
*context, extension, /*storage_area=*/std::nullopt,
/*render_frame_host=*/nullptr, *host->GetProcessHost());
}
// Allow a page or frame target to access extension storage if it is
// associated with a renderer that hosts an extension origin or has an
// extension injected into it.
if (host->GetType() == ChromeDevToolsManagerDelegate::kTypeBackgroundPage ||
host->GetType() == content::DevToolsAgentHost::kTypePage ||
host->GetType() == content::DevToolsAgentHost::kTypeFrame) {
if (!host->GetWebContents() ||
!host->GetWebContents()->GetPrimaryMainFrame()) {
return false;
}
bool can_access_storage = false;
// The content/ layer doesn't expose a way for us to get the frame
// associated with DevToolsAgentHost. As a compromise, allow access if any
// frame associated with the WebContents has access. This is safe as there
// are no instances where a client can attach to one frame in a WebContents
// but not another.
host->GetWebContents()
->GetPrimaryMainFrame()
->ForEachRenderFrameHostWithAction(
[&extension,
&can_access_storage](content::RenderFrameHost* render_frame_host) {
if (extensions::storage_utils::CanRendererAccessExtensionStorage(
*render_frame_host->GetBrowserContext(), extension,
/*storage_area=*/std::nullopt, render_frame_host,
*render_frame_host->GetProcess())) {
can_access_storage = true;
return content::RenderFrameHost::FrameIterationAction::kStop;
}
return content::RenderFrameHost::FrameIterationAction::kContinue;
});
return can_access_storage;
}
return false;
}
struct GetExtensionAndStorageFrontendResult {
raw_ptr<const extensions::Extension> extension;
extensions::StorageAreaNamespace storage_namespace;
raw_ptr<extensions::StorageFrontend> frontend;
std::optional<std::string> error;
};
GetExtensionAndStorageFrontendResult GetExtensionAndStorageFrontend(
const std::string& target_id_,
const protocol::String& extension_id,
const protocol::String& storage_area) {
GetExtensionAndStorageFrontendResult result;
result.extension = nullptr;
result.frontend = nullptr;
auto host = content::DevToolsAgentHost::GetForId(target_id_);
CHECK(host);
content::BrowserContext* context = host->GetBrowserContext();
if (!context) {
result.error = "No associated browser context.";
return result;
}
const extensions::StorageAreaNamespace namespace_result =
extensions::StorageAreaFromString(storage_area);
if (namespace_result == extensions::StorageAreaNamespace::kInvalid) {
result.error = "Storage area is invalid.";
return result;
}
std::optional<const extensions::Extension*> optional_extension =
GetExtension(extension_id, host);
if (!optional_extension ||
!CanAccessStorage(host, **optional_extension, namespace_result)) {
result.error = "Extension not found.";
return result;
}
result.extension = *optional_extension;
result.storage_namespace = namespace_result;
result.frontend = extensions::StorageFrontend::Get(context);
return result;
}
} // namespace
ExtensionsHandler::ExtensionsHandler(protocol::UberDispatcher* dispatcher,
const std::string& target_id,
bool allow_loading_extensions)
: target_id_(target_id),
allow_loading_extensions_(allow_loading_extensions) {
protocol::Extensions::Dispatcher::wire(dispatcher, this);
}
ExtensionsHandler::~ExtensionsHandler() = default;
void ExtensionsHandler::LoadUnpacked(
const protocol::String& path,
std::unique_ptr<ExtensionsHandler::LoadUnpackedCallback> callback) {
if (!allow_loading_extensions_) {
std::move(callback)->sendFailure(
protocol::Response::ServerError("Method not available."));
return;
}
content::BrowserContext* context = ProfileManager::GetLastUsedProfile();
DCHECK(context);
scoped_refptr<extensions::UnpackedInstaller> installer(
extensions::UnpackedInstaller::Create(context));
installer->set_be_noisy_on_failure(false);
installer->set_completion_callback(
base::BindOnce(&ExtensionsHandler::OnLoaded, weak_factory_.GetWeakPtr(),
std::move(callback)));
installer->Load(base::FilePath(base::FilePath::FromUTF8Unsafe(path)));
}
void ExtensionsHandler::OnLoaded(std::unique_ptr<LoadUnpackedCallback> callback,
const extensions::Extension* extension,
const base::FilePath& path,
const std::string& err) {
if (err.empty()) {
std::move(callback)->sendSuccess(extension->id());
return;
}
std::move(callback)->sendFailure(protocol::Response::InvalidRequest(err));
}
void ExtensionsHandler::Uninstall(const protocol::String& id,
std::unique_ptr<UninstallCallback> callback) {
if (!allow_loading_extensions_) {
std::move(callback)->sendFailure(
protocol::Response::ServerError("Method not available."));
return;
}
content::BrowserContext* context = ProfileManager::GetLastUsedProfile();
DCHECK(context);
extensions::ExtensionRegistry* registry =
extensions::ExtensionRegistry::Get(context);
const extensions::Extension* extension = registry->GetInstalledExtension(id);
if (!extension) {
std::move(callback)->sendFailure(protocol::Response::ServerError(
"Uninstall failed. Reason: could not find extension."));
return;
}
if (extension->location() != extensions::mojom::ManifestLocation::kUnpacked) {
std::move(callback)->sendFailure(protocol::Response::ServerError(
"Uninstall failed. Reason: extension is not an unpacked extension."));
return;
}
std::u16string error;
bool initiated =
extensions::ExtensionRegistrar::Get(context)->UninstallExtension(
id, extensions::UNINSTALL_REASON_USER_INITIATED, &error,
base::BindOnce(&ExtensionsHandler::OnUninstalled,
weak_factory_.GetWeakPtr(), std::move(callback)));
if (!initiated) {
std::move(callback)->sendFailure(protocol::Response::ServerError(
"Uninstall failed. Reason: " + base::UTF16ToUTF8(error)));
}
}
void ExtensionsHandler::OnUninstalled(
std::unique_ptr<UninstallCallback> callback) {
std::move(callback)->sendSuccess();
}
void ExtensionsHandler::GetStorageItems(
const protocol::String& id,
const protocol::String& storage_area,
std::unique_ptr<protocol::Array<protocol::String>> keys,
std::unique_ptr<ExtensionsHandler::GetStorageItemsCallback> callback) {
GetExtensionAndStorageFrontendResult result =
GetExtensionAndStorageFrontend(target_id_, id, storage_area);
if (result.error) {
std::move(callback)->sendFailure(
protocol::Response::InvalidRequest(*result.error));
return;
}
result.frontend->GetValues(
result.extension.get(), result.storage_namespace,
keys ? std::optional(std::move(*keys)) : std::nullopt,
base::BindOnce(&ExtensionsHandler::OnGetStorageItemsFinished,
weak_factory_.GetWeakPtr(), std::move(callback)));
}
void ExtensionsHandler::OnGetStorageItemsFinished(
std::unique_ptr<ExtensionsHandler::GetStorageItemsCallback> callback,
extensions::StorageFrontend::GetResult result) {
if (!result.status.success) {
std::move(callback)->sendFailure(
protocol::Response::ServerError(*result.status.error));
return;
}
base::Value::Dict data = std::move(*result.data);
std::move(callback)->sendSuccess(
std::make_unique<base::Value::Dict>(std::move(data)));
}
void ExtensionsHandler::SetStorageItems(
const protocol::String& id,
const protocol::String& storage_area,
std::unique_ptr<protocol::DictionaryValue> values,
std::unique_ptr<SetStorageItemsCallback> callback) {
GetExtensionAndStorageFrontendResult result =
GetExtensionAndStorageFrontend(target_id_, id, storage_area);
if (result.error) {
std::move(callback)->sendFailure(
protocol::Response::InvalidRequest(*result.error));
return;
}
result.frontend->Set(
result.extension.get(), result.storage_namespace, values->Clone(),
base::BindOnce(&ExtensionsHandler::OnSetStorageItemsFinished,
weak_factory_.GetWeakPtr(), std::move(callback)));
}
void ExtensionsHandler::OnSetStorageItemsFinished(
std::unique_ptr<SetStorageItemsCallback> callback,
extensions::StorageFrontend::ResultStatus status) {
if (!status.success) {
std::move(callback)->sendFailure(
protocol::Response::ServerError(*status.error));
return;
}
std::move(callback)->sendSuccess();
}
void ExtensionsHandler::RemoveStorageItems(
const protocol::String& id,
const protocol::String& storage_area,
std::unique_ptr<protocol::Array<protocol::String>> keys,
std::unique_ptr<RemoveStorageItemsCallback> callback) {
GetExtensionAndStorageFrontendResult result =
GetExtensionAndStorageFrontend(target_id_, id, storage_area);
if (result.error) {
std::move(callback)->sendFailure(
protocol::Response::InvalidRequest(*result.error));
return;
}
result.frontend->Remove(
result.extension.get(), result.storage_namespace, *keys,
base::BindOnce(&ExtensionsHandler::OnRemoveStorageItemsFinished,
weak_factory_.GetWeakPtr(), std::move(callback)));
}
void ExtensionsHandler::OnRemoveStorageItemsFinished(
std::unique_ptr<RemoveStorageItemsCallback> callback,
extensions::StorageFrontend::ResultStatus status) {
if (!status.success) {
std::move(callback)->sendFailure(
protocol::Response::ServerError(*status.error));
return;
}
std::move(callback)->sendSuccess();
}
void ExtensionsHandler::ClearStorageItems(
const protocol::String& id,
const protocol::String& storage_area,
std::unique_ptr<ClearStorageItemsCallback> callback) {
GetExtensionAndStorageFrontendResult result =
GetExtensionAndStorageFrontend(target_id_, id, storage_area);
if (result.error) {
std::move(callback)->sendFailure(
protocol::Response::InvalidRequest(*result.error));
return;
}
result.frontend->Clear(
result.extension.get(), result.storage_namespace,
base::BindOnce(&ExtensionsHandler::OnClearStorageItemsFinished,
weak_factory_.GetWeakPtr(), std::move(callback)));
}
void ExtensionsHandler::OnClearStorageItemsFinished(
std::unique_ptr<ClearStorageItemsCallback> callback,
extensions::StorageFrontend::ResultStatus status) {
if (!status.success) {
std::move(callback)->sendFailure(
protocol::Response::ServerError(*status.error));
return;
}
std::move(callback)->sendSuccess();
}