1// Copyright (c) 2012 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 "webkit/browser/appcache/view_appcache_internals_job.h"
6
7#include <algorithm>
8#include <string>
9
10#include "base/base64.h"
11#include "base/bind.h"
12#include "base/format_macros.h"
13#include "base/i18n/time_formatting.h"
14#include "base/logging.h"
15#include "base/memory/weak_ptr.h"
16#include "base/strings/string_number_conversions.h"
17#include "base/strings/string_util.h"
18#include "base/strings/stringprintf.h"
19#include "base/strings/utf_string_conversions.h"
20#include "net/base/escape.h"
21#include "net/base/io_buffer.h"
22#include "net/base/net_errors.h"
23#include "net/http/http_response_headers.h"
24#include "net/url_request/url_request.h"
25#include "net/url_request/url_request_simple_job.h"
26#include "net/url_request/view_cache_helper.h"
27#include "webkit/browser/appcache/appcache.h"
28#include "webkit/browser/appcache/appcache_group.h"
29#include "webkit/browser/appcache/appcache_policy.h"
30#include "webkit/browser/appcache/appcache_response.h"
31#include "webkit/browser/appcache/appcache_service.h"
32
33namespace appcache {
34namespace {
35
36const char kErrorMessage[] = "Error in retrieving Application Caches.";
37const char kEmptyAppCachesMessage[] = "No available Application Caches.";
38const char kManifestNotFoundMessage[] = "Manifest not found.";
39const char kManifest[] = "Manifest: ";
40const char kSize[] = "Size: ";
41const char kCreationTime[] = "Creation Time: ";
42const char kLastAccessTime[] = "Last Access Time: ";
43const char kLastUpdateTime[] = "Last Update Time: ";
44const char kFormattedDisabledAppCacheMsg[] =
45    "<b><i><font color=\"FF0000\">"
46    "This Application Cache is disabled by policy.</font></i></b><br/>";
47const char kRemoveCacheLabel[] = "Remove";
48const char kViewCacheLabel[] = "View Entries";
49const char kRemoveCacheCommand[] = "remove-cache";
50const char kViewCacheCommand[] = "view-cache";
51const char kViewEntryCommand[] = "view-entry";
52
53void EmitPageStart(std::string* out) {
54  out->append(
55      "<!DOCTYPE HTML>\n"
56      "<html><title>AppCache Internals</title>\n"
57      "<meta http-equiv=\"Content-Security-Policy\""
58      "  content=\"object-src 'none'; script-src 'none'\">\n"
59      "<style>\n"
60      "body { font-family: sans-serif; font-size: 0.8em; }\n"
61      "tt, code, pre { font-family: WebKitHack, monospace; }\n"
62      "form { display: inline; }\n"
63      ".subsection_body { margin: 10px 0 10px 2em; }\n"
64      ".subsection_title { font-weight: bold; }\n"
65      "</style>\n"
66      "</head><body>\n");
67}
68
69void EmitPageEnd(std::string* out) {
70  out->append("</body></html>\n");
71}
72
73void EmitListItem(const std::string& label,
74                  const std::string& data,
75                  std::string* out) {
76  out->append("<li>");
77  out->append(net::EscapeForHTML(label));
78  out->append(net::EscapeForHTML(data));
79  out->append("</li>\n");
80}
81
82void EmitAnchor(const std::string& url, const std::string& text,
83                std::string* out) {
84  out->append("<a href=\"");
85  out->append(net::EscapeForHTML(url));
86  out->append("\">");
87  out->append(net::EscapeForHTML(text));
88  out->append("</a>");
89}
90
91void EmitCommandAnchor(const char* label,
92                       const GURL& base_url,
93                       const char* command,
94                       const char* param,
95                       std::string* out) {
96  std::string query(command);
97  query.push_back('=');
98  query.append(param);
99  GURL::Replacements replacements;
100  replacements.SetQuery(query.data(),
101                        url_parse::Component(0, query.length()));
102  GURL command_url = base_url.ReplaceComponents(replacements);
103  EmitAnchor(command_url.spec(), label, out);
104}
105
106void EmitAppCacheInfo(const GURL& base_url,
107                      AppCacheService* service,
108                      const AppCacheInfo* info,
109                      std::string* out) {
110  std::string manifest_url_base64;
111  base::Base64Encode(info->manifest_url.spec(), &manifest_url_base64);
112
113  out->append("\n<p>");
114  out->append(kManifest);
115  EmitAnchor(info->manifest_url.spec(), info->manifest_url.spec(), out);
116  out->append("<br/>\n");
117  if (!service->appcache_policy()->CanLoadAppCache(
118          info->manifest_url, info->manifest_url)) {
119    out->append(kFormattedDisabledAppCacheMsg);
120  }
121  out->append("\n<br/>\n");
122  EmitCommandAnchor(kRemoveCacheLabel, base_url,
123                    kRemoveCacheCommand, manifest_url_base64.c_str(), out);
124  out->append("&nbsp;&nbsp;");
125  EmitCommandAnchor(kViewCacheLabel, base_url,
126                    kViewCacheCommand, manifest_url_base64.c_str(), out);
127  out->append("\n<br/>\n");
128  out->append("<ul>");
129  EmitListItem(
130      kSize,
131      UTF16ToUTF8(FormatBytesUnlocalized(info->size)),
132      out);
133  EmitListItem(
134      kCreationTime,
135      UTF16ToUTF8(TimeFormatFriendlyDateAndTime(info->creation_time)),
136      out);
137  EmitListItem(
138      kLastUpdateTime,
139      UTF16ToUTF8(TimeFormatFriendlyDateAndTime(info->last_update_time)),
140      out);
141  EmitListItem(
142      kLastAccessTime,
143      UTF16ToUTF8(TimeFormatFriendlyDateAndTime(info->last_access_time)),
144      out);
145  out->append("</ul></p></br>\n");
146}
147
148void EmitAppCacheInfoVector(
149    const GURL& base_url,
150    AppCacheService* service,
151    const AppCacheInfoVector& appcaches,
152    std::string* out) {
153  for (std::vector<AppCacheInfo>::const_iterator info =
154           appcaches.begin();
155       info != appcaches.end(); ++info) {
156    EmitAppCacheInfo(base_url, service, &(*info), out);
157  }
158}
159
160void EmitTableData(const std::string& data, bool align_right, bool bold,
161                   std::string* out) {
162  if (align_right)
163    out->append("<td align='right'>");
164  else
165    out->append("<td>");
166  if (bold)
167    out->append("<b>");
168  out->append(data);
169  if (bold)
170    out->append("</b>");
171  out->append("</td>");
172}
173
174std::string FormFlagsString(const AppCacheResourceInfo& info) {
175  std::string str;
176  if (info.is_manifest)
177    str.append("Manifest, ");
178  if (info.is_master)
179    str.append("Master, ");
180  if (info.is_intercept)
181    str.append("Intercept, ");
182  if (info.is_fallback)
183    str.append("Fallback, ");
184  if (info.is_explicit)
185    str.append("Explicit, ");
186  if (info.is_foreign)
187    str.append("Foreign, ");
188  return str;
189}
190
191std::string FormViewEntryAnchor(const GURL& base_url,
192                                const GURL& manifest_url, const GURL& entry_url,
193                                int64 response_id,
194                                int64 group_id) {
195  std::string manifest_url_base64;
196  std::string entry_url_base64;
197  std::string response_id_string;
198  std::string group_id_string;
199  base::Base64Encode(manifest_url.spec(), &manifest_url_base64);
200  base::Base64Encode(entry_url.spec(), &entry_url_base64);
201  response_id_string = base::Int64ToString(response_id);
202  group_id_string = base::Int64ToString(group_id);
203
204  std::string query(kViewEntryCommand);
205  query.push_back('=');
206  query.append(manifest_url_base64);
207  query.push_back('|');
208  query.append(entry_url_base64);
209  query.push_back('|');
210  query.append(response_id_string);
211  query.push_back('|');
212  query.append(group_id_string);
213
214  GURL::Replacements replacements;
215  replacements.SetQuery(query.data(),
216                        url_parse::Component(0, query.length()));
217  GURL view_entry_url = base_url.ReplaceComponents(replacements);
218
219  std::string anchor;
220  EmitAnchor(view_entry_url.spec(), entry_url.spec(), &anchor);
221  return anchor;
222}
223
224void EmitAppCacheResourceInfoVector(
225    const GURL& base_url,
226    const GURL& manifest_url,
227    const AppCacheResourceInfoVector& resource_infos,
228    int64 group_id,
229    std::string* out) {
230  out->append("<table border='0'>\n");
231  out->append("<tr>");
232  EmitTableData("Flags", false, true, out);
233  EmitTableData("URL", false, true, out);
234  EmitTableData("Size (headers and data)", true, true, out);
235  out->append("</tr>\n");
236  for (AppCacheResourceInfoVector::const_iterator
237          iter = resource_infos.begin();
238       iter != resource_infos.end(); ++iter) {
239    out->append("<tr>");
240    EmitTableData(FormFlagsString(*iter), false, false, out);
241    EmitTableData(FormViewEntryAnchor(base_url, manifest_url,
242                                      iter->url, iter->response_id,
243                                      group_id),
244                  false, false, out);
245    EmitTableData(UTF16ToUTF8(FormatBytesUnlocalized(iter->size)),
246                  true, false, out);
247    out->append("</tr>\n");
248  }
249  out->append("</table>\n");
250}
251
252void EmitResponseHeaders(net::HttpResponseHeaders* headers, std::string* out) {
253  out->append("<hr><pre>");
254  out->append(net::EscapeForHTML(headers->GetStatusLine()));
255  out->push_back('\n');
256
257  void* iter = NULL;
258  std::string name, value;
259  while (headers->EnumerateHeaderLines(&iter, &name, &value)) {
260    out->append(net::EscapeForHTML(name));
261    out->append(": ");
262    out->append(net::EscapeForHTML(value));
263    out->push_back('\n');
264  }
265  out->append("</pre>");
266}
267
268void EmitHexDump(const char *buf, size_t buf_len, size_t total_len,
269                 std::string* out) {
270  out->append("<hr><pre>");
271  base::StringAppendF(out, "Showing %d of %d bytes\n\n",
272                      static_cast<int>(buf_len), static_cast<int>(total_len));
273  net::ViewCacheHelper::HexDump(buf, buf_len, out);
274  if (buf_len < total_len)
275    out->append("\nNote: data is truncated...");
276  out->append("</pre>");
277}
278
279GURL DecodeBase64URL(const std::string& base64) {
280  std::string url;
281  base::Base64Decode(base64, &url);
282  return GURL(url);
283}
284
285bool ParseQuery(const std::string& query,
286                std::string* command, std::string* value) {
287  size_t position = query.find("=");
288  if (position == std::string::npos)
289    return false;
290  *command = query.substr(0, position);
291  *value = query.substr(position + 1);
292  return !command->empty() && !value->empty();
293}
294
295bool SortByManifestUrl(const AppCacheInfo& lhs,
296                       const AppCacheInfo& rhs) {
297  return lhs.manifest_url.spec() < rhs.manifest_url.spec();
298}
299
300bool SortByResourceUrl(const AppCacheResourceInfo& lhs,
301                       const AppCacheResourceInfo& rhs) {
302  return lhs.url.spec() < rhs.url.spec();
303}
304
305GURL ClearQuery(const GURL& url) {
306  GURL::Replacements replacements;
307  replacements.ClearQuery();
308  return url.ReplaceComponents(replacements);
309}
310
311// Simple base class for the job subclasses defined here.
312class BaseInternalsJob : public net::URLRequestSimpleJob {
313 protected:
314  BaseInternalsJob(net::URLRequest* request,
315                   net::NetworkDelegate* network_delegate,
316                   AppCacheService* service)
317      : URLRequestSimpleJob(request, network_delegate),
318        appcache_service_(service) {}
319  virtual ~BaseInternalsJob() {}
320
321  AppCacheService* appcache_service_;
322};
323
324// Job that lists all appcaches in the system.
325class MainPageJob : public BaseInternalsJob {
326 public:
327  MainPageJob(net::URLRequest* request,
328              net::NetworkDelegate* network_delegate,
329              AppCacheService* service)
330      : BaseInternalsJob(request, network_delegate, service),
331        weak_factory_(this) {
332  }
333
334  virtual void Start() OVERRIDE {
335    DCHECK(request_);
336    info_collection_ = new AppCacheInfoCollection;
337    appcache_service_->GetAllAppCacheInfo(
338        info_collection_.get(),
339        base::Bind(&MainPageJob::OnGotInfoComplete,
340                   weak_factory_.GetWeakPtr()));
341  }
342
343  // Produces a page containing the listing
344  virtual int GetData(std::string* mime_type,
345                      std::string* charset,
346                      std::string* out,
347                      const net::CompletionCallback& callback) const OVERRIDE {
348    mime_type->assign("text/html");
349    charset->assign("UTF-8");
350
351    out->clear();
352    EmitPageStart(out);
353    if (!info_collection_.get()) {
354      out->append(kErrorMessage);
355    } else if (info_collection_->infos_by_origin.empty()) {
356      out->append(kEmptyAppCachesMessage);
357    } else {
358      typedef std::map<GURL, AppCacheInfoVector> InfoByOrigin;
359      AppCacheInfoVector appcaches;
360      for (InfoByOrigin::const_iterator origin =
361               info_collection_->infos_by_origin.begin();
362           origin != info_collection_->infos_by_origin.end(); ++origin) {
363        appcaches.insert(appcaches.end(),
364                         origin->second.begin(), origin->second.end());
365      }
366      std::sort(appcaches.begin(), appcaches.end(), SortByManifestUrl);
367
368      GURL base_url = ClearQuery(request_->url());
369      EmitAppCacheInfoVector(base_url, appcache_service_, appcaches, out);
370    }
371    EmitPageEnd(out);
372    return net::OK;
373  }
374
375 private:
376  virtual ~MainPageJob() {}
377
378  void OnGotInfoComplete(int rv) {
379    if (rv != net::OK)
380      info_collection_ = NULL;
381    StartAsync();
382  }
383
384  base::WeakPtrFactory<MainPageJob> weak_factory_;
385  scoped_refptr<AppCacheInfoCollection> info_collection_;
386  DISALLOW_COPY_AND_ASSIGN(MainPageJob);
387};
388
389// Job that redirects back to the main appcache internals page.
390class RedirectToMainPageJob : public BaseInternalsJob {
391 public:
392  RedirectToMainPageJob(net::URLRequest* request,
393                        net::NetworkDelegate* network_delegate,
394                        AppCacheService* service)
395      : BaseInternalsJob(request, network_delegate, service) {}
396
397  virtual int GetData(std::string* mime_type,
398                      std::string* charset,
399                      std::string* data,
400                      const net::CompletionCallback& callback) const OVERRIDE {
401    return net::OK;  // IsRedirectResponse induces a redirect.
402  }
403
404  virtual bool IsRedirectResponse(GURL* location,
405                                  int* http_status_code) OVERRIDE {
406    *location = ClearQuery(request_->url());
407    *http_status_code = 307;
408    return true;
409  }
410
411 protected:
412  virtual ~RedirectToMainPageJob() {}
413};
414
415// Job that removes an appcache and then redirects back to the main page.
416class RemoveAppCacheJob : public RedirectToMainPageJob {
417 public:
418  RemoveAppCacheJob(
419      net::URLRequest* request,
420      net::NetworkDelegate* network_delegate,
421      AppCacheService* service,
422      const GURL& manifest_url)
423      : RedirectToMainPageJob(request, network_delegate, service),
424        manifest_url_(manifest_url),
425        weak_factory_(this) {
426  }
427
428  virtual void Start() OVERRIDE {
429    DCHECK(request_);
430
431    appcache_service_->DeleteAppCacheGroup(
432        manifest_url_,base::Bind(&RemoveAppCacheJob::OnDeleteAppCacheComplete,
433                                 weak_factory_.GetWeakPtr()));
434  }
435
436 private:
437  virtual ~RemoveAppCacheJob() {}
438
439  void OnDeleteAppCacheComplete(int rv) {
440    StartAsync();  // Causes the base class to redirect.
441  }
442
443  GURL manifest_url_;
444  base::WeakPtrFactory<RemoveAppCacheJob> weak_factory_;
445};
446
447
448// Job shows the details of a particular manifest url.
449class ViewAppCacheJob : public BaseInternalsJob,
450                        public AppCacheStorage::Delegate {
451 public:
452  ViewAppCacheJob(
453      net::URLRequest* request,
454      net::NetworkDelegate* network_delegate,
455      AppCacheService* service,
456      const GURL& manifest_url)
457      : BaseInternalsJob(request, network_delegate, service),
458        manifest_url_(manifest_url) {}
459
460  virtual void Start() OVERRIDE {
461    DCHECK(request_);
462    appcache_service_->storage()->LoadOrCreateGroup(manifest_url_, this);
463  }
464
465  // Produces a page containing the entries listing.
466  virtual int GetData(std::string* mime_type,
467                      std::string* charset,
468                      std::string* out,
469                      const net::CompletionCallback& callback) const OVERRIDE {
470    mime_type->assign("text/html");
471    charset->assign("UTF-8");
472    out->clear();
473    EmitPageStart(out);
474    if (appcache_info_.manifest_url.is_empty()) {
475      out->append(kManifestNotFoundMessage);
476    } else {
477      GURL base_url = ClearQuery(request_->url());
478      EmitAppCacheInfo(base_url, appcache_service_, &appcache_info_, out);
479      EmitAppCacheResourceInfoVector(base_url,
480                                     manifest_url_,
481                                     resource_infos_,
482                                     appcache_info_.group_id,
483                                     out);
484    }
485    EmitPageEnd(out);
486    return net::OK;
487  }
488
489 private:
490  virtual ~ViewAppCacheJob() {
491    appcache_service_->storage()->CancelDelegateCallbacks(this);
492  }
493
494  // AppCacheStorage::Delegate override
495  virtual void OnGroupLoaded(
496      AppCacheGroup* group, const GURL& manifest_url) OVERRIDE {
497    DCHECK_EQ(manifest_url_, manifest_url);
498    if (group && group->newest_complete_cache()) {
499      appcache_info_.manifest_url = manifest_url;
500      appcache_info_.group_id = group->group_id();
501      appcache_info_.size = group->newest_complete_cache()->cache_size();
502      appcache_info_.creation_time = group->creation_time();
503      appcache_info_.last_update_time =
504          group->newest_complete_cache()->update_time();
505      appcache_info_.last_access_time = base::Time::Now();
506      group->newest_complete_cache()->ToResourceInfoVector(&resource_infos_);
507      std::sort(resource_infos_.begin(), resource_infos_.end(),
508                SortByResourceUrl);
509    }
510    StartAsync();
511  }
512
513  GURL manifest_url_;
514  AppCacheInfo appcache_info_;
515  AppCacheResourceInfoVector resource_infos_;
516  DISALLOW_COPY_AND_ASSIGN(ViewAppCacheJob);
517};
518
519// Job that shows the details of a particular cached resource.
520class ViewEntryJob : public BaseInternalsJob,
521                     public AppCacheStorage::Delegate {
522 public:
523  ViewEntryJob(
524      net::URLRequest* request,
525      net::NetworkDelegate* network_delegate,
526      AppCacheService* service,
527      const GURL& manifest_url,
528      const GURL& entry_url,
529      int64 response_id, int64 group_id)
530      : BaseInternalsJob(request, network_delegate, service),
531        manifest_url_(manifest_url), entry_url_(entry_url),
532        response_id_(response_id), group_id_(group_id), amount_read_(0) {
533  }
534
535  virtual void Start() OVERRIDE {
536    DCHECK(request_);
537    appcache_service_->storage()->LoadResponseInfo(
538        manifest_url_, group_id_, response_id_, this);
539  }
540
541  // Produces a page containing the response headers and data.
542  virtual int GetData(std::string* mime_type,
543                      std::string* charset,
544                      std::string* out,
545                      const net::CompletionCallback& callback) const OVERRIDE {
546    mime_type->assign("text/html");
547    charset->assign("UTF-8");
548    out->clear();
549    EmitPageStart(out);
550    EmitAnchor(entry_url_.spec(), entry_url_.spec(), out);
551    out->append("<br/>\n");
552    if (response_info_.get()) {
553      if (response_info_->http_response_info())
554        EmitResponseHeaders(response_info_->http_response_info()->headers.get(),
555                            out);
556      else
557        out->append("Failed to read response headers.<br>");
558
559      if (response_data_.get()) {
560        EmitHexDump(response_data_->data(),
561                    amount_read_,
562                    response_info_->response_data_size(),
563                    out);
564      } else {
565        out->append("Failed to read response data.<br>");
566      }
567    } else {
568      out->append("Failed to read response headers and data.<br>");
569    }
570    EmitPageEnd(out);
571    return net::OK;
572  }
573
574 private:
575  virtual ~ViewEntryJob() {
576    appcache_service_->storage()->CancelDelegateCallbacks(this);
577  }
578
579  virtual void OnResponseInfoLoaded(
580      AppCacheResponseInfo* response_info, int64 response_id) OVERRIDE {
581    if (!response_info) {
582      StartAsync();
583      return;
584    }
585    response_info_ = response_info;
586
587    // Read the response data, truncating if its too large.
588    const int64 kLimit = 100 * 1000;
589    int64 amount_to_read =
590        std::min(kLimit, response_info->response_data_size());
591    response_data_ = new net::IOBuffer(amount_to_read);
592
593    reader_.reset(appcache_service_->storage()->CreateResponseReader(
594        manifest_url_, group_id_, response_id_));
595    reader_->ReadData(
596        response_data_.get(),
597        amount_to_read,
598        base::Bind(&ViewEntryJob::OnReadComplete, base::Unretained(this)));
599  }
600
601  void OnReadComplete(int result) {
602    reader_.reset();
603    amount_read_ = result;
604    if (result < 0)
605      response_data_ = NULL;
606    StartAsync();
607  }
608
609  GURL manifest_url_;
610  GURL entry_url_;
611  int64 response_id_;
612  int64 group_id_;
613  scoped_refptr<AppCacheResponseInfo> response_info_;
614  scoped_refptr<net::IOBuffer> response_data_;
615  int amount_read_;
616  scoped_ptr<AppCacheResponseReader> reader_;
617};
618
619}  // namespace
620
621net::URLRequestJob* ViewAppCacheInternalsJobFactory::CreateJobForRequest(
622    net::URLRequest* request,
623    net::NetworkDelegate* network_delegate,
624    AppCacheService* service) {
625  if (!request->url().has_query())
626    return new MainPageJob(request, network_delegate, service);
627
628  std::string command;
629  std::string param;
630  ParseQuery(request->url().query(), &command, &param);
631
632  if (command == kRemoveCacheCommand)
633    return new RemoveAppCacheJob(request, network_delegate, service,
634                                 DecodeBase64URL(param));
635
636  if (command == kViewCacheCommand)
637    return new ViewAppCacheJob(request, network_delegate, service,
638                               DecodeBase64URL(param));
639
640  std::vector<std::string> tokens;
641  int64 response_id;
642  int64 group_id;
643  if (command == kViewEntryCommand && Tokenize(param, "|", &tokens) == 4u &&
644      base::StringToInt64(tokens[2], &response_id) &&
645      base::StringToInt64(tokens[3], &group_id)) {
646    return new ViewEntryJob(request, network_delegate, service,
647                            DecodeBase64URL(tokens[0]),  // manifest url
648                            DecodeBase64URL(tokens[1]),  // entry url
649                            response_id, group_id);
650  }
651
652  return new RedirectToMainPageJob(request, network_delegate, service);
653}
654
655}  // namespace appcache
656