blob: c430c6778a53fde10b697ec4cb29d4168e78f5ae [file] [log] [blame]
// 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 "content/browser/indexed_db/indexed_db_internals_ui.h"
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include "base/barrier_callback.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/task/thread_pool.h"
#include "components/services/storage/privileged/cpp/bucket_client_info.h"
#include "components/services/storage/privileged/mojom/indexed_db_internals_types.mojom-forward.h"
#include "content/browser/devtools/devtools_agent_host_impl.h"
#include "content/browser/devtools/render_frame_devtools_agent_host.h"
#include "content/browser/devtools/service_worker_devtools_agent_host.h"
#include "content/browser/devtools/service_worker_devtools_manager.h"
#include "content/browser/devtools/shared_worker_devtools_agent_host.h"
#include "content/browser/indexed_db/indexed_db_internals.mojom-forward.h"
#include "content/browser/indexed_db/indexed_db_internals.mojom.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/worker_host/shared_worker_service_impl.h"
#include "content/grit/indexed_db_resources.h"
#include "content/grit/indexed_db_resources_map.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/download_manager.h"
#include "content/public/browser/download_request_utils.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_ui.h"
#include "content/public/browser/web_ui_data_source.h"
#include "content/public/common/url_constants.h"
#include "mojo/public/cpp/bindings/struct_ptr.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
using storage::mojom::IdbPartitionMetadataPtr;
namespace content::indexed_db {
namespace {
scoped_refptr<DevToolsAgentHostImpl> GetDevToolsAgentHostForClient(
const storage::BucketClientInfo& client_info) {
int32_t process_id = client_info.process_id;
const blink::ExecutionContextToken& context_token = client_info.context_token;
if (client_info.document_token) {
auto* rfh = RenderFrameHostImpl::FromDocumentToken(
process_id, client_info.document_token.value());
return rfh ? RenderFrameDevToolsAgentHost::GetFor(rfh) : nullptr;
}
if (context_token.Is<blink::SharedWorkerToken>()) {
auto* rph = RenderProcessHost::FromID(process_id);
if (!rph || !rph->IsInitializedAndNotDead()) {
return nullptr;
}
auto* worker_service = static_cast<SharedWorkerServiceImpl*>(
static_cast<StoragePartitionImpl*>(rph->GetStoragePartition())
->GetSharedWorkerService());
SharedWorkerHost* shared_worker_host =
worker_service->GetSharedWorkerHostFromToken(
context_token.GetAs<blink::SharedWorkerToken>());
return shared_worker_host
? SharedWorkerDevToolsAgentHost::GetFor(shared_worker_host)
: nullptr;
}
if (context_token.Is<blink::ServiceWorkerToken>()) {
auto* rph = RenderProcessHost::FromID(process_id);
if (!rph || !rph->IsInitializedAndNotDead()) {
return nullptr;
}
ServiceWorkerContextWrapper* service_worker_context =
static_cast<StoragePartitionImpl*>(rph->GetStoragePartition())
->GetServiceWorkerContext();
for (const auto& [version_id, info] :
service_worker_context->GetRunningServiceWorkerInfos()) {
if (info.token != context_token.GetAs<blink::ServiceWorkerToken>()) {
continue;
}
ServiceWorkerVersion* version =
service_worker_context->GetLiveVersion(version_id);
return version ? ServiceWorkerDevToolsManager::GetInstance()
->GetDevToolsAgentHostForWorker(
version->GetInfo().process_id,
version->GetInfo().devtools_agent_route_id)
: nullptr;
}
return nullptr;
}
NOTREACHED();
}
} // namespace
IndexedDBInternalsUI::IndexedDBInternalsUI(WebUI* web_ui)
: WebUIController(web_ui) {
WebUIDataSource* source = WebUIDataSource::CreateAndAdd(
web_ui->GetWebContents()->GetBrowserContext(),
kChromeUIIndexedDBInternalsHost);
source->OverrideContentSecurityPolicy(
network::mojom::CSPDirectiveName::ScriptSrc,
"script-src chrome://resources 'self';");
source->OverrideContentSecurityPolicy(
network::mojom::CSPDirectiveName::TrustedTypes,
"trusted-types static-types lit-html-desktop;");
source->UseStringsJs();
source->AddResourcePaths(kIndexedDbResources);
source->AddResourcePath("", IDR_INDEXED_DB_INDEXEDDB_INTERNALS_HTML);
}
WEB_UI_CONTROLLER_TYPE_IMPL(IndexedDBInternalsUI)
IndexedDBInternalsUI::~IndexedDBInternalsUI() = default;
void IndexedDBInternalsUI::WebUIRenderFrameCreated(RenderFrameHost* rfh) {
// Enable the JavaScript Mojo bindings in the renderer process, so the JS
// code can call the Mojo APIs exposed by this WebUI.
rfh->EnableMojoJsBindings(nullptr);
}
void IndexedDBInternalsUI::BindInterface(
mojo::PendingReceiver<storage::mojom::IdbInternalsHandler> receiver) {
receiver_ =
std::make_unique<mojo::Receiver<storage::mojom::IdbInternalsHandler>>(
this, std::move(receiver));
}
void IndexedDBInternalsUI::GetAllBucketsAcrossAllStorageKeys(
GetAllBucketsAcrossAllStorageKeysCallback callback) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
BrowserContext* browser_context =
web_ui()->GetWebContents()->GetBrowserContext();
auto collect_partitions = base::BarrierCallback<IdbPartitionMetadataPtr>(
browser_context->GetLoadedStoragePartitionCount(),
base::BindOnce(
[](GetAllBucketsAcrossAllStorageKeysCallback callback,
std::vector<IdbPartitionMetadataPtr> partitions) {
std::move(callback).Run(std::nullopt, std::move(partitions));
},
std::move(callback)));
browser_context->ForEachLoadedStoragePartition(
[&](StoragePartition* partition) {
partition->GetIndexedDBControl().GetAllBucketsDetails(base::BindOnce(
[](base::WeakPtr<IndexedDBInternalsUI> handler,
base::RepeatingCallback<void(IdbPartitionMetadataPtr)>
collect_partitions,
base::FilePath partition_path, bool incognito,
std::vector<storage::mojom::IdbOriginMetadataPtr> origin_list) {
if (!handler) {
return;
}
for (const storage::mojom::IdbOriginMetadataPtr& origin :
origin_list) {
for (const storage::mojom::IdbStorageKeyMetadataPtr&
storage_key : origin->storage_keys) {
for (const storage::mojom::IdbBucketMetadataPtr& bucket :
storage_key->buckets) {
handler->bucket_to_partition_path_map_
[bucket->bucket_locator.id] = partition_path;
}
}
}
IdbPartitionMetadataPtr partition =
storage::mojom::IdbPartitionMetadata::New();
partition->partition_path =
incognito ? base::FilePath() : partition_path;
partition->origin_list = std::move(origin_list);
collect_partitions.Run(std::move(partition));
},
weak_factory_.GetWeakPtr(), collect_partitions,
partition->GetPath()));
});
}
storage::mojom::IndexedDBControl* IndexedDBInternalsUI::GetBucketControl(
storage::BucketId bucket_id) {
auto partition_path_iter = bucket_to_partition_path_map_.find(bucket_id);
if (partition_path_iter == bucket_to_partition_path_map_.end()) {
return nullptr;
}
const base::FilePath& partition_path = partition_path_iter->second;
// Search the storage partitions by path.
BrowserContext* browser_context =
web_ui()->GetWebContents()->GetBrowserContext();
storage::mojom::IndexedDBControl* control = nullptr;
browser_context->ForEachLoadedStoragePartition(
[&](StoragePartition* storage_partition) {
if (storage_partition->GetPath() == partition_path) {
DCHECK_EQ(control, nullptr);
control = &storage_partition->GetIndexedDBControl();
}
});
return control;
}
void IndexedDBInternalsUI::DownloadBucketData(
storage::BucketId bucket_id,
DownloadBucketDataCallback callback) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
storage::mojom::IndexedDBControl* control = GetBucketControl(bucket_id);
if (!control) {
std::move(callback).Run("IndexedDB control not found");
return;
}
control->ForceClose(
bucket_id, storage::mojom::ForceCloseReason::FORCE_CLOSE_INTERNALS_PAGE,
base::BindOnce(
[](base::WeakPtr<IndexedDBInternalsUI> handler,
storage::BucketId bucket_id,
storage::mojom::IndexedDBControl* control,
DownloadBucketDataCallback callback) {
if (!handler) {
return;
}
control->DownloadBucketData(
bucket_id,
base::BindOnce(&IndexedDBInternalsUI::OnDownloadDataReady,
handler, std::move(callback)));
},
weak_factory_.GetWeakPtr(), bucket_id, control, std::move(callback)));
}
void IndexedDBInternalsUI::ForceClose(storage::BucketId bucket_id,
ForceCloseCallback callback) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
storage::mojom::IndexedDBControl* control = GetBucketControl(bucket_id);
if (!control) {
std::move(callback).Run("IndexedDB control not found");
return;
}
control->ForceClose(
bucket_id, storage::mojom::ForceCloseReason::FORCE_CLOSE_INTERNALS_PAGE,
base::BindOnce(
[](ForceCloseCallback callback) {
std::move(callback).Run(std::nullopt);
},
std::move(callback)));
}
void IndexedDBInternalsUI::StartMetadataRecording(
storage::BucketId bucket_id,
StartMetadataRecordingCallback callback) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
storage::mojom::IndexedDBControl* control = GetBucketControl(bucket_id);
if (!control) {
std::move(callback).Run("IndexedDB control not found");
return;
}
control->StartMetadataRecording(
bucket_id, base::BindOnce(std::move(callback), std::nullopt));
}
void IndexedDBInternalsUI::StopMetadataRecording(
storage::BucketId bucket_id,
StopMetadataRecordingCallback callback) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
storage::mojom::IndexedDBControl* control = GetBucketControl(bucket_id);
if (!control) {
std::move(callback).Run("IndexedDB control not found", {});
return;
}
control->StopMetadataRecording(
bucket_id, base::BindOnce(std::move(callback), std::nullopt));
}
void IndexedDBInternalsUI::InspectClient(
const storage::BucketClientInfo& client_info,
InspectClientCallback callback) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (!devtools_agent_hosts_created_) {
// If a DevTools window has never been opened in this browser session,
// DevToolsAgentHosts will not have been created for RenderFrameHosts.
// Trigger their creation now so that the inspect call succeeds.
DevToolsAgentHostImpl::GetOrCreateAll();
devtools_agent_hosts_created_ = true;
}
scoped_refptr<DevToolsAgentHostImpl> dev_tools_agent =
GetDevToolsAgentHostForClient(client_info);
if (dev_tools_agent && dev_tools_agent->Inspect()) {
std::move(callback).Run(std::nullopt);
return;
}
std::move(callback).Run("Client not found");
}
void IndexedDBInternalsUI::OnDownloadDataReady(
DownloadBucketDataCallback callback,
bool success,
const base::FilePath& temp_path,
const base::FilePath& zip_path) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (!success) {
std::move(callback).Run("Error downloading database");
return;
}
const GURL url = GURL("file://" + zip_path.AsUTF8Unsafe());
WebContents* web_contents = web_ui()->GetWebContents();
net::NetworkTrafficAnnotationTag traffic_annotation =
net::DefineNetworkTrafficAnnotation("indexed_db_internals_handler", R"(
semantics {
sender: "Indexed DB Internals"
description:
"This is an internal Chrome webpage that displays debug "
"information about IndexedDB usage and data, used by developers."
trigger: "When a user navigates to chrome://indexeddb-internals/."
data: "None."
destination: LOCAL
}
policy {
cookies_allowed: NO
setting:
"This feature cannot be disabled by settings, but it's only "
"triggered by navigating to the specified URL."
policy_exception_justification:
"Not implemented. Indexed DB is Chrome's internal local data "
"storage."
})");
std::unique_ptr<download::DownloadUrlParameters> dl_params(
DownloadRequestUtils::CreateDownloadForWebContentsMainFrame(
web_contents, url, traffic_annotation));
content::Referrer referrer = content::Referrer::SanitizeForRequest(
url, content::Referrer(web_contents->GetLastCommittedURL(),
network::mojom::ReferrerPolicy::kDefault));
dl_params->set_referrer(referrer.url);
dl_params->set_referrer_policy(
Referrer::ReferrerPolicyForUrlRequest(referrer.policy));
// This is how to watch for the download to finish: first wait for it
// to start, then attach a download::DownloadItem::Observer to observe the
// state change to the finished state.
dl_params->set_callback(base::BindOnce(
&IndexedDBInternalsUI::OnDownloadStarted, weak_factory_.GetWeakPtr(),
temp_path, std::move(callback)));
BrowserContext* context = web_contents->GetBrowserContext();
context->GetDownloadManager()->DownloadUrl(std::move(dl_params));
}
// The entire purpose of this class is to delete the temp file after
// the download is complete.
class FileDeleter : public download::DownloadItem::Observer {
public:
explicit FileDeleter(const base::FilePath& temp_dir) : temp_dir_(temp_dir) {}
FileDeleter(const FileDeleter&) = delete;
FileDeleter& operator=(const FileDeleter&) = delete;
~FileDeleter() override;
void OnDownloadUpdated(download::DownloadItem* download) override;
void OnDownloadOpened(download::DownloadItem* item) override {}
void OnDownloadRemoved(download::DownloadItem* item) override {}
void OnDownloadDestroyed(download::DownloadItem* item) override {}
private:
const base::FilePath temp_dir_;
};
void FileDeleter::OnDownloadUpdated(download::DownloadItem* item) {
switch (item->GetState()) {
case download::DownloadItem::IN_PROGRESS:
break;
case download::DownloadItem::COMPLETE:
case download::DownloadItem::CANCELLED:
case download::DownloadItem::INTERRUPTED: {
item->RemoveObserver(this);
delete this;
break;
}
default:
NOTREACHED();
}
}
FileDeleter::~FileDeleter() {
base::ThreadPool::PostTask(
FROM_HERE,
{base::MayBlock(), base::TaskPriority::BEST_EFFORT,
base::TaskShutdownBehavior::BLOCK_SHUTDOWN},
base::GetDeletePathRecursivelyCallback(std::move(temp_dir_)));
}
void IndexedDBInternalsUI::OnDownloadStarted(
const base::FilePath& temp_path,
DownloadBucketDataCallback callback,
download::DownloadItem* item,
download::DownloadInterruptReason interrupt_reason) {
if (interrupt_reason != download::DOWNLOAD_INTERRUPT_REASON_NONE) {
LOG(ERROR) << "Error downloading database dump: "
<< DownloadInterruptReasonToString(interrupt_reason);
std::move(callback).Run("Error downloading database");
return;
}
item->AddObserver(new FileDeleter(temp_path));
std::move(callback).Run(std::nullopt);
}
} // namespace content::indexed_db