1// Copyright (c) 2013 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "content/browser/indexed_db/indexed_db_internals_ui.h"
6
7#include <string>
8
9#include "base/bind.h"
10#include "base/files/scoped_temp_dir.h"
11#include "base/threading/platform_thread.h"
12#include "base/values.h"
13#include "content/browser/indexed_db/indexed_db_context_impl.h"
14#include "content/grit/content_resources.h"
15#include "content/public/browser/browser_context.h"
16#include "content/public/browser/browser_thread.h"
17#include "content/public/browser/download_manager.h"
18#include "content/public/browser/download_url_parameters.h"
19#include "content/public/browser/storage_partition.h"
20#include "content/public/browser/web_contents.h"
21#include "content/public/browser/web_ui.h"
22#include "content/public/browser/web_ui_data_source.h"
23#include "content/public/common/url_constants.h"
24#include "storage/common/database/database_identifier.h"
25#include "third_party/zlib/google/zip.h"
26#include "ui/base/text/bytes_formatting.h"
27
28namespace content {
29
30IndexedDBInternalsUI::IndexedDBInternalsUI(WebUI* web_ui)
31    : WebUIController(web_ui) {
32  web_ui->RegisterMessageCallback(
33      "getAllOrigins",
34      base::Bind(&IndexedDBInternalsUI::GetAllOrigins, base::Unretained(this)));
35
36  web_ui->RegisterMessageCallback(
37      "downloadOriginData",
38      base::Bind(&IndexedDBInternalsUI::DownloadOriginData,
39                 base::Unretained(this)));
40  web_ui->RegisterMessageCallback(
41      "forceClose",
42      base::Bind(&IndexedDBInternalsUI::ForceCloseOrigin,
43                 base::Unretained(this)));
44
45  WebUIDataSource* source =
46      WebUIDataSource::Create(kChromeUIIndexedDBInternalsHost);
47  source->SetUseJsonJSFormatV2();
48  source->SetJsonPath("strings.js");
49  source->AddResourcePath("indexeddb_internals.js",
50                          IDR_INDEXED_DB_INTERNALS_JS);
51  source->AddResourcePath("indexeddb_internals.css",
52                          IDR_INDEXED_DB_INTERNALS_CSS);
53  source->SetDefaultResource(IDR_INDEXED_DB_INTERNALS_HTML);
54
55  BrowserContext* browser_context =
56      web_ui->GetWebContents()->GetBrowserContext();
57  WebUIDataSource::Add(browser_context, source);
58}
59
60IndexedDBInternalsUI::~IndexedDBInternalsUI() {}
61
62void IndexedDBInternalsUI::AddContextFromStoragePartition(
63    StoragePartition* partition) {
64  scoped_refptr<IndexedDBContext> context = partition->GetIndexedDBContext();
65  context->TaskRunner()->PostTask(
66      FROM_HERE,
67      base::Bind(&IndexedDBInternalsUI::GetAllOriginsOnIndexedDBThread,
68                 base::Unretained(this),
69                 context,
70                 partition->GetPath()));
71}
72
73void IndexedDBInternalsUI::GetAllOrigins(const base::ListValue* args) {
74  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
75
76  BrowserContext* browser_context =
77      web_ui()->GetWebContents()->GetBrowserContext();
78
79  BrowserContext::StoragePartitionCallback cb =
80      base::Bind(&IndexedDBInternalsUI::AddContextFromStoragePartition,
81                 base::Unretained(this));
82  BrowserContext::ForEachStoragePartition(browser_context, cb);
83}
84
85void IndexedDBInternalsUI::GetAllOriginsOnIndexedDBThread(
86    scoped_refptr<IndexedDBContext> context,
87    const base::FilePath& context_path) {
88  DCHECK(context->TaskRunner()->RunsTasksOnCurrentThread());
89
90  IndexedDBContextImpl* context_impl =
91      static_cast<IndexedDBContextImpl*>(context.get());
92
93  scoped_ptr<base::ListValue> info_list(context_impl->GetAllOriginsDetails());
94  bool is_incognito = context_impl->is_incognito();
95
96  BrowserThread::PostTask(
97      BrowserThread::UI,
98      FROM_HERE,
99      base::Bind(&IndexedDBInternalsUI::OnOriginsReady,
100                 base::Unretained(this),
101                 base::Passed(&info_list),
102                 is_incognito ? base::FilePath() : context_path));
103}
104
105void IndexedDBInternalsUI::OnOriginsReady(scoped_ptr<base::ListValue> origins,
106                                          const base::FilePath& path) {
107  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
108  web_ui()->CallJavascriptFunction(
109      "indexeddb.onOriginsReady", *origins, base::StringValue(path.value()));
110}
111
112static void FindContext(const base::FilePath& partition_path,
113                        StoragePartition** result_partition,
114                        scoped_refptr<IndexedDBContextImpl>* result_context,
115                        StoragePartition* storage_partition) {
116  if (storage_partition->GetPath() == partition_path) {
117    *result_partition = storage_partition;
118    *result_context = static_cast<IndexedDBContextImpl*>(
119        storage_partition->GetIndexedDBContext());
120  }
121}
122
123bool IndexedDBInternalsUI::GetOriginData(
124    const base::ListValue* args,
125    base::FilePath* partition_path,
126    GURL* origin_url,
127    scoped_refptr<IndexedDBContextImpl>* context) {
128  base::FilePath::StringType path_string;
129  if (!args->GetString(0, &path_string))
130    return false;
131  *partition_path = base::FilePath(path_string);
132
133  std::string url_string;
134  if (!args->GetString(1, &url_string))
135    return false;
136
137  *origin_url = GURL(url_string);
138
139  return GetOriginContext(*partition_path, *origin_url, context);
140}
141
142bool IndexedDBInternalsUI::GetOriginContext(
143    const base::FilePath& path,
144    const GURL& origin_url,
145    scoped_refptr<IndexedDBContextImpl>* context) {
146  // search the origins to find the right context
147  BrowserContext* browser_context =
148      web_ui()->GetWebContents()->GetBrowserContext();
149
150  StoragePartition* result_partition;
151  BrowserContext::StoragePartitionCallback cb =
152      base::Bind(&FindContext, path, &result_partition, context);
153  BrowserContext::ForEachStoragePartition(browser_context, cb);
154
155  if (!result_partition || !(context->get()))
156    return false;
157
158  return true;
159}
160
161void IndexedDBInternalsUI::DownloadOriginData(const base::ListValue* args) {
162  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
163
164  base::FilePath partition_path;
165  GURL origin_url;
166  scoped_refptr<IndexedDBContextImpl> context;
167  if (!GetOriginData(args, &partition_path, &origin_url, &context))
168    return;
169
170  DCHECK(context.get());
171  context->TaskRunner()->PostTask(
172      FROM_HERE,
173      base::Bind(&IndexedDBInternalsUI::DownloadOriginDataOnIndexedDBThread,
174                 base::Unretained(this),
175                 partition_path,
176                 context,
177                 origin_url));
178}
179
180void IndexedDBInternalsUI::ForceCloseOrigin(const base::ListValue* args) {
181  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
182
183  base::FilePath partition_path;
184  GURL origin_url;
185  scoped_refptr<IndexedDBContextImpl> context;
186  if (!GetOriginData(args, &partition_path, &origin_url, &context))
187    return;
188
189  context->TaskRunner()->PostTask(
190      FROM_HERE,
191      base::Bind(&IndexedDBInternalsUI::ForceCloseOriginOnIndexedDBThread,
192                 base::Unretained(this),
193                 partition_path,
194                 context,
195                 origin_url));
196}
197
198void IndexedDBInternalsUI::DownloadOriginDataOnIndexedDBThread(
199    const base::FilePath& partition_path,
200    const scoped_refptr<IndexedDBContextImpl> context,
201    const GURL& origin_url) {
202  DCHECK(context->TaskRunner()->RunsTasksOnCurrentThread());
203
204  // Make sure the database hasn't been deleted since the page was loaded.
205  if (!context->IsInOriginSet(origin_url))
206    return;
207
208  context->ForceClose(origin_url,
209                      IndexedDBContextImpl::FORCE_CLOSE_INTERNALS_PAGE);
210  size_t connection_count = context->GetConnectionCount(origin_url);
211
212  base::ScopedTempDir temp_dir;
213  if (!temp_dir.CreateUniqueTempDir())
214    return;
215
216  // This will get cleaned up on the File thread after the download
217  // has completed.
218  base::FilePath temp_path = temp_dir.Take();
219
220  std::string origin_id = storage::GetIdentifierFromOrigin(origin_url);
221  base::FilePath zip_path =
222      temp_path.AppendASCII(origin_id).AddExtension(FILE_PATH_LITERAL("zip"));
223
224  // This happens on the "webkit" thread (which is really just the IndexedDB
225  // thread) as a simple way to avoid another script reopening the origin
226  // while we are zipping.
227  zip::Zip(context->GetFilePath(origin_url), zip_path, true);
228
229  BrowserThread::PostTask(BrowserThread::UI,
230                          FROM_HERE,
231                          base::Bind(&IndexedDBInternalsUI::OnDownloadDataReady,
232                                     base::Unretained(this),
233                                     partition_path,
234                                     origin_url,
235                                     temp_path,
236                                     zip_path,
237                                     connection_count));
238}
239
240void IndexedDBInternalsUI::ForceCloseOriginOnIndexedDBThread(
241    const base::FilePath& partition_path,
242    const scoped_refptr<IndexedDBContextImpl> context,
243    const GURL& origin_url) {
244  DCHECK(context->TaskRunner()->RunsTasksOnCurrentThread());
245
246  // Make sure the database hasn't been deleted since the page was loaded.
247  if (!context->IsInOriginSet(origin_url))
248    return;
249
250  context->ForceClose(origin_url,
251                      IndexedDBContextImpl::FORCE_CLOSE_INTERNALS_PAGE);
252  size_t connection_count = context->GetConnectionCount(origin_url);
253
254  BrowserThread::PostTask(BrowserThread::UI,
255                          FROM_HERE,
256                          base::Bind(&IndexedDBInternalsUI::OnForcedClose,
257                                     base::Unretained(this),
258                                     partition_path,
259                                     origin_url,
260                                     connection_count));
261}
262
263void IndexedDBInternalsUI::OnForcedClose(const base::FilePath& partition_path,
264                                         const GURL& origin_url,
265                                         size_t connection_count) {
266  web_ui()->CallJavascriptFunction(
267      "indexeddb.onForcedClose",
268      base::StringValue(partition_path.value()),
269      base::StringValue(origin_url.spec()),
270      base::FundamentalValue(static_cast<double>(connection_count)));
271}
272
273void IndexedDBInternalsUI::OnDownloadDataReady(
274    const base::FilePath& partition_path,
275    const GURL& origin_url,
276    const base::FilePath temp_path,
277    const base::FilePath zip_path,
278    size_t connection_count) {
279  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
280  const GURL url = GURL(FILE_PATH_LITERAL("file://") + zip_path.value());
281  BrowserContext* browser_context =
282      web_ui()->GetWebContents()->GetBrowserContext();
283  scoped_ptr<DownloadUrlParameters> dl_params(
284      DownloadUrlParameters::FromWebContents(web_ui()->GetWebContents(), url));
285  DownloadManager* dlm = BrowserContext::GetDownloadManager(browser_context);
286
287  const GURL referrer(web_ui()->GetWebContents()->GetLastCommittedURL());
288  dl_params->set_referrer(
289      content::Referrer(referrer, blink::WebReferrerPolicyDefault));
290
291  // This is how to watch for the download to finish: first wait for it
292  // to start, then attach a DownloadItem::Observer to observe the
293  // state change to the finished state.
294  dl_params->set_callback(base::Bind(&IndexedDBInternalsUI::OnDownloadStarted,
295                                     base::Unretained(this),
296                                     partition_path,
297                                     origin_url,
298                                     temp_path,
299                                     connection_count));
300  dlm->DownloadUrl(dl_params.Pass());
301}
302
303// The entire purpose of this class is to delete the temp file after
304// the download is complete.
305class FileDeleter : public DownloadItem::Observer {
306 public:
307  explicit FileDeleter(const base::FilePath& temp_dir) : temp_dir_(temp_dir) {}
308  virtual ~FileDeleter();
309
310  virtual void OnDownloadUpdated(DownloadItem* download) OVERRIDE;
311  virtual void OnDownloadOpened(DownloadItem* item) OVERRIDE {}
312  virtual void OnDownloadRemoved(DownloadItem* item) OVERRIDE {}
313  virtual void OnDownloadDestroyed(DownloadItem* item) OVERRIDE {}
314
315 private:
316  const base::FilePath temp_dir_;
317
318  DISALLOW_COPY_AND_ASSIGN(FileDeleter);
319};
320
321void FileDeleter::OnDownloadUpdated(DownloadItem* item) {
322  switch (item->GetState()) {
323    case DownloadItem::IN_PROGRESS:
324      break;
325    case DownloadItem::COMPLETE:
326    case DownloadItem::CANCELLED:
327    case DownloadItem::INTERRUPTED: {
328      item->RemoveObserver(this);
329      BrowserThread::DeleteOnFileThread::Destruct(this);
330      break;
331    }
332    default:
333      NOTREACHED();
334  }
335}
336
337FileDeleter::~FileDeleter() {
338  base::ScopedTempDir path;
339  bool will_delete ALLOW_UNUSED = path.Set(temp_dir_);
340  DCHECK(will_delete);
341}
342
343void IndexedDBInternalsUI::OnDownloadStarted(
344    const base::FilePath& partition_path,
345    const GURL& origin_url,
346    const base::FilePath& temp_path,
347    size_t connection_count,
348    DownloadItem* item,
349    DownloadInterruptReason interrupt_reason) {
350
351  if (interrupt_reason != DOWNLOAD_INTERRUPT_REASON_NONE) {
352    LOG(ERROR) << "Error downloading database dump: "
353               << DownloadInterruptReasonToString(interrupt_reason);
354    return;
355  }
356
357  item->AddObserver(new FileDeleter(temp_path));
358  web_ui()->CallJavascriptFunction(
359      "indexeddb.onOriginDownloadReady",
360      base::StringValue(partition_path.value()),
361      base::StringValue(origin_url.spec()),
362      base::FundamentalValue(static_cast<double>(connection_count)));
363}
364
365}  // namespace content
366