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