| // Copyright (c) 2013 The Chromium Authors. All rights reserved. |
| // 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 <string> |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/bind_helpers.h" |
| #include "base/files/file_util.h" |
| #include "base/files/scoped_temp_dir.h" |
| #include "base/macros.h" |
| #include "base/task_scheduler/post_task.h" |
| #include "base/threading/platform_thread.h" |
| #include "base/values.h" |
| #include "content/browser/indexed_db/indexed_db_context_impl.h" |
| #include "content/grit/content_resources.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_url_parameters.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 "net/traffic_annotation/network_traffic_annotation.h" |
| #include "storage/common/database/database_identifier.h" |
| #include "third_party/zlib/google/zip.h" |
| #include "ui/base/text/bytes_formatting.h" |
| #include "url/origin.h" |
| |
| using url::Origin; |
| |
| namespace content { |
| |
| namespace { |
| |
| bool AllowWhitelistedPaths(const std::vector<base::FilePath>& allowed_paths, |
| const base::FilePath& candidate_path) { |
| for (const base::FilePath& allowed_path : allowed_paths) { |
| if (allowed_path.IsParent(candidate_path)) |
| return true; |
| } |
| return false; |
| } |
| |
| } // namespace |
| |
| IndexedDBInternalsUI::IndexedDBInternalsUI(WebUI* web_ui) |
| : WebUIController(web_ui) { |
| web_ui->RegisterMessageCallback( |
| "getAllOrigins", |
| base::Bind(&IndexedDBInternalsUI::GetAllOrigins, base::Unretained(this))); |
| |
| web_ui->RegisterMessageCallback( |
| "downloadOriginData", |
| base::Bind(&IndexedDBInternalsUI::DownloadOriginData, |
| base::Unretained(this))); |
| web_ui->RegisterMessageCallback( |
| "forceClose", |
| base::Bind(&IndexedDBInternalsUI::ForceCloseOrigin, |
| base::Unretained(this))); |
| |
| WebUIDataSource* source = |
| WebUIDataSource::Create(kChromeUIIndexedDBInternalsHost); |
| source->SetJsonPath("strings.js"); |
| source->AddResourcePath("indexeddb_internals.js", |
| IDR_INDEXED_DB_INTERNALS_JS); |
| source->AddResourcePath("indexeddb_internals.css", |
| IDR_INDEXED_DB_INTERNALS_CSS); |
| source->SetDefaultResource(IDR_INDEXED_DB_INTERNALS_HTML); |
| source->UseGzip(); |
| |
| BrowserContext* browser_context = |
| web_ui->GetWebContents()->GetBrowserContext(); |
| WebUIDataSource::Add(browser_context, source); |
| } |
| |
| IndexedDBInternalsUI::~IndexedDBInternalsUI() {} |
| |
| void IndexedDBInternalsUI::AddContextFromStoragePartition( |
| StoragePartition* partition) { |
| scoped_refptr<IndexedDBContext> context = partition->GetIndexedDBContext(); |
| context->TaskRunner()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&IndexedDBInternalsUI::GetAllOriginsOnIndexedDBThread, |
| base::Unretained(this), context, partition->GetPath())); |
| } |
| |
| void IndexedDBInternalsUI::GetAllOrigins(const base::ListValue* args) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| BrowserContext* browser_context = |
| web_ui()->GetWebContents()->GetBrowserContext(); |
| |
| BrowserContext::StoragePartitionCallback cb = |
| base::Bind(&IndexedDBInternalsUI::AddContextFromStoragePartition, |
| base::Unretained(this)); |
| BrowserContext::ForEachStoragePartition(browser_context, cb); |
| } |
| |
| void IndexedDBInternalsUI::GetAllOriginsOnIndexedDBThread( |
| scoped_refptr<IndexedDBContext> context, |
| const base::FilePath& context_path) { |
| DCHECK(context->TaskRunner()->RunsTasksInCurrentSequence()); |
| |
| IndexedDBContextImpl* context_impl = |
| static_cast<IndexedDBContextImpl*>(context.get()); |
| |
| std::unique_ptr<base::ListValue> info_list( |
| context_impl->GetAllOriginsDetails()); |
| bool is_incognito = context_impl->is_incognito(); |
| |
| BrowserThread::PostTask( |
| BrowserThread::UI, FROM_HERE, |
| base::BindOnce(&IndexedDBInternalsUI::OnOriginsReady, |
| base::Unretained(this), base::Passed(&info_list), |
| is_incognito ? base::FilePath() : context_path)); |
| } |
| |
| void IndexedDBInternalsUI::OnOriginsReady( |
| std::unique_ptr<base::ListValue> origins, |
| const base::FilePath& path) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| web_ui()->CallJavascriptFunctionUnsafe("indexeddb.onOriginsReady", *origins, |
| base::Value(path.value())); |
| } |
| |
| static void FindContext(const base::FilePath& partition_path, |
| StoragePartition** result_partition, |
| scoped_refptr<IndexedDBContextImpl>* result_context, |
| StoragePartition* storage_partition) { |
| if (storage_partition->GetPath() == partition_path) { |
| *result_partition = storage_partition; |
| *result_context = static_cast<IndexedDBContextImpl*>( |
| storage_partition->GetIndexedDBContext()); |
| } |
| } |
| |
| bool IndexedDBInternalsUI::GetOriginData( |
| const base::ListValue* args, |
| base::FilePath* partition_path, |
| Origin* origin, |
| scoped_refptr<IndexedDBContextImpl>* context) { |
| base::FilePath::StringType path_string; |
| if (!args->GetString(0, &path_string)) |
| return false; |
| *partition_path = base::FilePath(path_string); |
| |
| std::string url_string; |
| if (!args->GetString(1, &url_string)) |
| return false; |
| |
| *origin = Origin::Create(GURL(url_string)); |
| |
| return GetOriginContext(*partition_path, *origin, context); |
| } |
| |
| bool IndexedDBInternalsUI::GetOriginContext( |
| const base::FilePath& path, |
| const Origin& origin, |
| scoped_refptr<IndexedDBContextImpl>* context) { |
| // search the origins to find the right context |
| BrowserContext* browser_context = |
| web_ui()->GetWebContents()->GetBrowserContext(); |
| |
| StoragePartition* result_partition; |
| BrowserContext::StoragePartitionCallback cb = |
| base::Bind(&FindContext, path, &result_partition, context); |
| BrowserContext::ForEachStoragePartition(browser_context, cb); |
| |
| if (!result_partition || !(context->get())) |
| return false; |
| |
| return true; |
| } |
| |
| void IndexedDBInternalsUI::DownloadOriginData(const base::ListValue* args) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| base::FilePath partition_path; |
| Origin origin; |
| scoped_refptr<IndexedDBContextImpl> context; |
| if (!GetOriginData(args, &partition_path, &origin, &context)) |
| return; |
| |
| DCHECK(context.get()); |
| context->TaskRunner()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&IndexedDBInternalsUI::DownloadOriginDataOnIndexedDBThread, |
| base::Unretained(this), partition_path, context, origin)); |
| } |
| |
| void IndexedDBInternalsUI::ForceCloseOrigin(const base::ListValue* args) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| base::FilePath partition_path; |
| Origin origin; |
| scoped_refptr<IndexedDBContextImpl> context; |
| if (!GetOriginData(args, &partition_path, &origin, &context)) |
| return; |
| |
| context->TaskRunner()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&IndexedDBInternalsUI::ForceCloseOriginOnIndexedDBThread, |
| base::Unretained(this), partition_path, context, origin)); |
| } |
| |
| void IndexedDBInternalsUI::DownloadOriginDataOnIndexedDBThread( |
| const base::FilePath& partition_path, |
| const scoped_refptr<IndexedDBContextImpl> context, |
| const Origin& origin) { |
| DCHECK(context->TaskRunner()->RunsTasksInCurrentSequence()); |
| // This runs on the IndexedDB task runner to prevent script from reopening |
| // the origin while we are zipping. |
| |
| // Make sure the database hasn't been deleted since the page was loaded. |
| if (!context->HasOrigin(origin)) |
| return; |
| |
| context->ForceClose(origin, IndexedDBContextImpl::FORCE_CLOSE_INTERNALS_PAGE); |
| size_t connection_count = context->GetConnectionCount(origin); |
| |
| base::ScopedTempDir temp_dir; |
| if (!temp_dir.CreateUniqueTempDir()) |
| return; |
| |
| // This will get cleaned up after the download has completed. |
| base::FilePath temp_path = temp_dir.Take(); |
| |
| std::string origin_id = storage::GetIdentifierFromOrigin(origin.GetURL()); |
| base::FilePath zip_path = |
| temp_path.AppendASCII(origin_id).AddExtension(FILE_PATH_LITERAL("zip")); |
| |
| std::vector<base::FilePath> paths = context->GetStoragePaths(origin); |
| zip::ZipWithFilterCallback(context->data_path(), zip_path, |
| base::Bind(AllowWhitelistedPaths, paths)); |
| |
| BrowserThread::PostTask( |
| BrowserThread::UI, FROM_HERE, |
| base::BindOnce(&IndexedDBInternalsUI::OnDownloadDataReady, |
| base::Unretained(this), partition_path, origin, temp_path, |
| zip_path, connection_count)); |
| } |
| |
| void IndexedDBInternalsUI::ForceCloseOriginOnIndexedDBThread( |
| const base::FilePath& partition_path, |
| const scoped_refptr<IndexedDBContextImpl> context, |
| const Origin& origin) { |
| DCHECK(context->TaskRunner()->RunsTasksInCurrentSequence()); |
| |
| // Make sure the database hasn't been deleted since the page was loaded. |
| if (!context->HasOrigin(origin)) |
| return; |
| |
| context->ForceClose(origin, IndexedDBContextImpl::FORCE_CLOSE_INTERNALS_PAGE); |
| size_t connection_count = context->GetConnectionCount(origin); |
| |
| BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, |
| base::BindOnce(&IndexedDBInternalsUI::OnForcedClose, |
| base::Unretained(this), partition_path, |
| origin, connection_count)); |
| } |
| |
| void IndexedDBInternalsUI::OnForcedClose(const base::FilePath& partition_path, |
| const Origin& origin, |
| size_t connection_count) { |
| web_ui()->CallJavascriptFunctionUnsafe( |
| "indexeddb.onForcedClose", base::Value(partition_path.value()), |
| base::Value(origin.Serialize()), |
| base::Value(static_cast<double>(connection_count))); |
| } |
| |
| void IndexedDBInternalsUI::OnDownloadDataReady( |
| const base::FilePath& partition_path, |
| const Origin& origin, |
| const base::FilePath temp_path, |
| const base::FilePath zip_path, |
| size_t connection_count) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| const GURL url = GURL(FILE_PATH_LITERAL("file://") + zip_path.value()); |
| 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<DownloadUrlParameters> dl_params( |
| DownloadUrlParameters::CreateForWebContentsMainFrame(web_contents, url, |
| traffic_annotation)); |
| const GURL referrer(web_contents->GetLastCommittedURL()); |
| dl_params->set_referrer(content::Referrer::SanitizeForRequest( |
| url, content::Referrer(referrer, blink::kWebReferrerPolicyDefault))); |
| |
| // This is how to watch for the download to finish: first wait for it |
| // to start, then attach a DownloadItem::Observer to observe the |
| // state change to the finished state. |
| dl_params->set_callback(base::Bind(&IndexedDBInternalsUI::OnDownloadStarted, |
| base::Unretained(this), partition_path, |
| origin, temp_path, connection_count)); |
| |
| BrowserContext* context = web_contents->GetBrowserContext(); |
| BrowserContext::GetDownloadManager(context)->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 DownloadItem::Observer { |
| public: |
| explicit FileDeleter(const base::FilePath& temp_dir) : temp_dir_(temp_dir) {} |
| ~FileDeleter() override; |
| |
| void OnDownloadUpdated(DownloadItem* download) override; |
| void OnDownloadOpened(DownloadItem* item) override {} |
| void OnDownloadRemoved(DownloadItem* item) override {} |
| void OnDownloadDestroyed(DownloadItem* item) override {} |
| |
| private: |
| const base::FilePath temp_dir_; |
| |
| DISALLOW_COPY_AND_ASSIGN(FileDeleter); |
| }; |
| |
| void FileDeleter::OnDownloadUpdated(DownloadItem* item) { |
| switch (item->GetState()) { |
| case DownloadItem::IN_PROGRESS: |
| break; |
| case DownloadItem::COMPLETE: |
| case DownloadItem::CANCELLED: |
| case DownloadItem::INTERRUPTED: { |
| item->RemoveObserver(this); |
| delete this; |
| break; |
| } |
| default: |
| NOTREACHED(); |
| } |
| } |
| |
| FileDeleter::~FileDeleter() { |
| base::PostTaskWithTraits(FROM_HERE, |
| {base::MayBlock(), base::TaskPriority::BACKGROUND, |
| base::TaskShutdownBehavior::BLOCK_SHUTDOWN}, |
| base::Bind(base::IgnoreResult(&base::DeleteFile), |
| std::move(temp_dir_), true)); |
| } |
| |
| void IndexedDBInternalsUI::OnDownloadStarted( |
| const base::FilePath& partition_path, |
| const Origin& origin, |
| const base::FilePath& temp_path, |
| size_t connection_count, |
| DownloadItem* item, |
| DownloadInterruptReason interrupt_reason) { |
| if (interrupt_reason != DOWNLOAD_INTERRUPT_REASON_NONE) { |
| LOG(ERROR) << "Error downloading database dump: " |
| << DownloadInterruptReasonToString(interrupt_reason); |
| return; |
| } |
| |
| item->AddObserver(new FileDeleter(temp_path)); |
| web_ui()->CallJavascriptFunctionUnsafe( |
| "indexeddb.onOriginDownloadReady", base::Value(partition_path.value()), |
| base::Value(origin.Serialize()), |
| base::Value(static_cast<double>(connection_count))); |
| } |
| |
| } // namespace content |