| // Copyright (c) 2012 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 <algorithm> |
| #include <string> |
| |
| #include "webkit/appcache/view_appcache_internals_job.h" |
| |
| #include "base/base64.h" |
| #include "base/bind.h" |
| #include "base/format_macros.h" |
| #include "base/i18n/time_formatting.h" |
| #include "base/logging.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/string_number_conversions.h" |
| #include "base/string_util.h" |
| #include "base/stringprintf.h" |
| #include "base/utf_string_conversions.h" |
| #include "net/base/escape.h" |
| #include "net/base/io_buffer.h" |
| #include "net/base/net_errors.h" |
| #include "net/http/http_response_headers.h" |
| #include "net/url_request/url_request.h" |
| #include "net/url_request/url_request_simple_job.h" |
| #include "net/url_request/view_cache_helper.h" |
| #include "webkit/appcache/appcache.h" |
| #include "webkit/appcache/appcache_group.h" |
| #include "webkit/appcache/appcache_policy.h" |
| #include "webkit/appcache/appcache_response.h" |
| #include "webkit/appcache/appcache_service.h" |
| |
| namespace appcache { |
| namespace { |
| |
| const char kErrorMessage[] = "Error in retrieving Application Caches."; |
| const char kEmptyAppCachesMessage[] = "No available Application Caches."; |
| const char kManifestNotFoundMessage[] = "Manifest not found."; |
| const char kManifest[] = "Manifest: "; |
| const char kSize[] = "Size: "; |
| const char kCreationTime[] = "Creation Time: "; |
| const char kLastAccessTime[] = "Last Access Time: "; |
| const char kLastUpdateTime[] = "Last Update Time: "; |
| const char kFormattedDisabledAppCacheMsg[] = |
| "<b><i><font color=\"FF0000\">" |
| "This Application Cache is disabled by policy.</font></i></b><br/>"; |
| const char kRemoveCacheLabel[] = "Remove"; |
| const char kViewCacheLabel[] = "View Entries"; |
| const char kRemoveCacheCommand[] = "remove-cache"; |
| const char kViewCacheCommand[] = "view-cache"; |
| const char kViewEntryCommand[] = "view-entry"; |
| |
| void EmitPageStart(std::string* out) { |
| out->append( |
| "<!DOCTYPE HTML>\n" |
| "<html><title>AppCache Internals</title>\n" |
| "<meta http-equiv=\"X-WebKit-CSP\"" |
| " content=\"object-src 'none'; script-src 'none'\">\n" |
| "<style>\n" |
| "body { font-family: sans-serif; font-size: 0.8em; }\n" |
| "tt, code, pre { font-family: WebKitHack, monospace; }\n" |
| "form { display: inline; }\n" |
| ".subsection_body { margin: 10px 0 10px 2em; }\n" |
| ".subsection_title { font-weight: bold; }\n" |
| "</style>\n" |
| "</head><body>\n"); |
| } |
| |
| void EmitPageEnd(std::string* out) { |
| out->append("</body></html>\n"); |
| } |
| |
| void EmitListItem(const std::string& label, |
| const std::string& data, |
| std::string* out) { |
| out->append("<li>"); |
| out->append(net::EscapeForHTML(label)); |
| out->append(net::EscapeForHTML(data)); |
| out->append("</li>\n"); |
| } |
| |
| void EmitAnchor(const std::string& url, const std::string& text, |
| std::string* out) { |
| out->append("<a href=\""); |
| out->append(net::EscapeForHTML(url)); |
| out->append("\">"); |
| out->append(net::EscapeForHTML(text)); |
| out->append("</a>"); |
| } |
| |
| void EmitCommandAnchor(const char* label, |
| const GURL& base_url, |
| const char* command, |
| const char* param, |
| std::string* out) { |
| std::string query(command); |
| query.push_back('='); |
| query.append(param); |
| GURL::Replacements replacements; |
| replacements.SetQuery(query.data(), |
| url_parse::Component(0, query.length())); |
| GURL command_url = base_url.ReplaceComponents(replacements); |
| EmitAnchor(command_url.spec(), label, out); |
| } |
| |
| void EmitAppCacheInfo(const GURL& base_url, |
| AppCacheService* service, |
| const AppCacheInfo* info, |
| std::string* out) { |
| std::string manifest_url_base64; |
| base::Base64Encode(info->manifest_url.spec(), &manifest_url_base64); |
| |
| out->append("\n<p>"); |
| out->append(kManifest); |
| EmitAnchor(info->manifest_url.spec(), info->manifest_url.spec(), out); |
| out->append("<br/>\n"); |
| if (!service->appcache_policy()->CanLoadAppCache( |
| info->manifest_url, info->manifest_url)) { |
| out->append(kFormattedDisabledAppCacheMsg); |
| } |
| out->append("\n<br/>\n"); |
| EmitCommandAnchor(kRemoveCacheLabel, base_url, |
| kRemoveCacheCommand, manifest_url_base64.c_str(), out); |
| out->append(" "); |
| EmitCommandAnchor(kViewCacheLabel, base_url, |
| kViewCacheCommand, manifest_url_base64.c_str(), out); |
| out->append("\n<br/>\n"); |
| out->append("<ul>"); |
| EmitListItem( |
| kSize, |
| UTF16ToUTF8(FormatBytesUnlocalized(info->size)), |
| out); |
| EmitListItem( |
| kCreationTime, |
| UTF16ToUTF8(TimeFormatFriendlyDateAndTime(info->creation_time)), |
| out); |
| EmitListItem( |
| kLastUpdateTime, |
| UTF16ToUTF8(TimeFormatFriendlyDateAndTime(info->last_update_time)), |
| out); |
| EmitListItem( |
| kLastAccessTime, |
| UTF16ToUTF8(TimeFormatFriendlyDateAndTime(info->last_access_time)), |
| out); |
| out->append("</ul></p></br>\n"); |
| } |
| |
| void EmitAppCacheInfoVector( |
| const GURL& base_url, |
| AppCacheService* service, |
| const AppCacheInfoVector& appcaches, |
| std::string* out) { |
| for (std::vector<AppCacheInfo>::const_iterator info = |
| appcaches.begin(); |
| info != appcaches.end(); ++info) { |
| EmitAppCacheInfo(base_url, service, &(*info), out); |
| } |
| } |
| |
| void EmitTableData(const std::string& data, bool align_right, bool bold, |
| std::string* out) { |
| if (align_right) |
| out->append("<td align='right'>"); |
| else |
| out->append("<td>"); |
| if (bold) |
| out->append("<b>"); |
| out->append(data); |
| if (bold) |
| out->append("</b>"); |
| out->append("</td>"); |
| } |
| |
| std::string FormFlagsString(const AppCacheResourceInfo& info) { |
| std::string str; |
| if (info.is_manifest) |
| str.append("Manifest, "); |
| if (info.is_master) |
| str.append("Master, "); |
| if (info.is_intercept) |
| str.append("Intercept, "); |
| if (info.is_fallback) |
| str.append("Fallback, "); |
| if (info.is_explicit) |
| str.append("Explicit, "); |
| if (info.is_foreign) |
| str.append("Foreign, "); |
| return str; |
| } |
| |
| std::string FormViewEntryAnchor(const GURL& base_url, |
| const GURL& manifest_url, const GURL& entry_url, |
| int64 response_id, |
| int64 group_id) { |
| std::string manifest_url_base64; |
| std::string entry_url_base64; |
| std::string response_id_string; |
| std::string group_id_string; |
| base::Base64Encode(manifest_url.spec(), &manifest_url_base64); |
| base::Base64Encode(entry_url.spec(), &entry_url_base64); |
| response_id_string = base::Int64ToString(response_id); |
| group_id_string = base::Int64ToString(group_id); |
| |
| std::string query(kViewEntryCommand); |
| query.push_back('='); |
| query.append(manifest_url_base64); |
| query.push_back('|'); |
| query.append(entry_url_base64); |
| query.push_back('|'); |
| query.append(response_id_string); |
| query.push_back('|'); |
| query.append(group_id_string); |
| |
| GURL::Replacements replacements; |
| replacements.SetQuery(query.data(), |
| url_parse::Component(0, query.length())); |
| GURL view_entry_url = base_url.ReplaceComponents(replacements); |
| |
| std::string anchor; |
| EmitAnchor(view_entry_url.spec(), entry_url.spec(), &anchor); |
| return anchor; |
| } |
| |
| void EmitAppCacheResourceInfoVector( |
| const GURL& base_url, |
| const GURL& manifest_url, |
| const AppCacheResourceInfoVector& resource_infos, |
| int64 group_id, |
| std::string* out) { |
| out->append("<table border='0'>\n"); |
| out->append("<tr>"); |
| EmitTableData("Flags", false, true, out); |
| EmitTableData("URL", false, true, out); |
| EmitTableData("Size (headers and data)", true, true, out); |
| out->append("</tr>\n"); |
| for (AppCacheResourceInfoVector::const_iterator |
| iter = resource_infos.begin(); |
| iter != resource_infos.end(); ++iter) { |
| out->append("<tr>"); |
| EmitTableData(FormFlagsString(*iter), false, false, out); |
| EmitTableData(FormViewEntryAnchor(base_url, manifest_url, |
| iter->url, iter->response_id, |
| group_id), |
| false, false, out); |
| EmitTableData(UTF16ToUTF8(FormatBytesUnlocalized(iter->size)), |
| true, false, out); |
| out->append("</tr>\n"); |
| } |
| out->append("</table>\n"); |
| } |
| |
| void EmitResponseHeaders(net::HttpResponseHeaders* headers, std::string* out) { |
| out->append("<hr><pre>"); |
| out->append(net::EscapeForHTML(headers->GetStatusLine())); |
| out->push_back('\n'); |
| |
| void* iter = NULL; |
| std::string name, value; |
| while (headers->EnumerateHeaderLines(&iter, &name, &value)) { |
| out->append(net::EscapeForHTML(name)); |
| out->append(": "); |
| out->append(net::EscapeForHTML(value)); |
| out->push_back('\n'); |
| } |
| out->append("</pre>"); |
| } |
| |
| void EmitHexDump(const char *buf, size_t buf_len, size_t total_len, |
| std::string* out) { |
| out->append("<hr><pre>"); |
| base::StringAppendF(out, "Showing %d of %d bytes\n\n", |
| static_cast<int>(buf_len), static_cast<int>(total_len)); |
| net::ViewCacheHelper::HexDump(buf, buf_len, out); |
| if (buf_len < total_len) |
| out->append("\nNote: data is truncated..."); |
| out->append("</pre>"); |
| } |
| |
| GURL DecodeBase64URL(const std::string& base64) { |
| std::string url; |
| base::Base64Decode(base64, &url); |
| return GURL(url); |
| } |
| |
| bool ParseQuery(const std::string& query, |
| std::string* command, std::string* value) { |
| size_t position = query.find("="); |
| if (position == std::string::npos) |
| return false; |
| *command = query.substr(0, position); |
| *value = query.substr(position + 1); |
| return !command->empty() && !value->empty(); |
| } |
| |
| bool SortByManifestUrl(const AppCacheInfo& lhs, |
| const AppCacheInfo& rhs) { |
| return lhs.manifest_url.spec() < rhs.manifest_url.spec(); |
| } |
| |
| bool SortByResourceUrl(const AppCacheResourceInfo& lhs, |
| const AppCacheResourceInfo& rhs) { |
| return lhs.url.spec() < rhs.url.spec(); |
| } |
| |
| GURL ClearQuery(const GURL& url) { |
| GURL::Replacements replacements; |
| replacements.ClearQuery(); |
| return url.ReplaceComponents(replacements); |
| } |
| |
| // Simple base class for the job subclasses defined here. |
| class BaseInternalsJob : public net::URLRequestSimpleJob { |
| protected: |
| BaseInternalsJob(net::URLRequest* request, |
| net::NetworkDelegate* network_delegate, |
| AppCacheService* service) |
| : URLRequestSimpleJob(request, network_delegate), |
| appcache_service_(service) {} |
| virtual ~BaseInternalsJob() {} |
| |
| AppCacheService* appcache_service_; |
| }; |
| |
| // Job that lists all appcaches in the system. |
| class MainPageJob : public BaseInternalsJob { |
| public: |
| MainPageJob(net::URLRequest* request, |
| net::NetworkDelegate* network_delegate, |
| AppCacheService* service) |
| : BaseInternalsJob(request, network_delegate, service), |
| ALLOW_THIS_IN_INITIALIZER_LIST(weak_factory_(this)) { |
| } |
| |
| virtual void Start() { |
| DCHECK(request_); |
| info_collection_ = new AppCacheInfoCollection; |
| appcache_service_->GetAllAppCacheInfo( |
| info_collection_, base::Bind(&MainPageJob::OnGotInfoComplete, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| // Produces a page containing the listing |
| virtual int GetData(std::string* mime_type, |
| std::string* charset, |
| std::string* out, |
| const net::CompletionCallback& callback) const OVERRIDE { |
| mime_type->assign("text/html"); |
| charset->assign("UTF-8"); |
| |
| out->clear(); |
| EmitPageStart(out); |
| if (!info_collection_.get()) { |
| out->append(kErrorMessage); |
| } else if (info_collection_->infos_by_origin.empty()) { |
| out->append(kEmptyAppCachesMessage); |
| } else { |
| typedef std::map<GURL, AppCacheInfoVector> InfoByOrigin; |
| AppCacheInfoVector appcaches; |
| for (InfoByOrigin::const_iterator origin = |
| info_collection_->infos_by_origin.begin(); |
| origin != info_collection_->infos_by_origin.end(); ++origin) { |
| appcaches.insert(appcaches.end(), |
| origin->second.begin(), origin->second.end()); |
| } |
| std::sort(appcaches.begin(), appcaches.end(), SortByManifestUrl); |
| |
| GURL base_url = ClearQuery(request_->url()); |
| EmitAppCacheInfoVector(base_url, appcache_service_, appcaches, out); |
| } |
| EmitPageEnd(out); |
| return net::OK; |
| } |
| |
| private: |
| virtual ~MainPageJob() {} |
| |
| void OnGotInfoComplete(int rv) { |
| if (rv != net::OK) |
| info_collection_ = NULL; |
| StartAsync(); |
| } |
| |
| base::WeakPtrFactory<MainPageJob> weak_factory_; |
| scoped_refptr<AppCacheInfoCollection> info_collection_; |
| DISALLOW_COPY_AND_ASSIGN(MainPageJob); |
| }; |
| |
| // Job that redirects back to the main appcache internals page. |
| class RedirectToMainPageJob : public BaseInternalsJob { |
| public: |
| RedirectToMainPageJob(net::URLRequest* request, |
| net::NetworkDelegate* network_delegate, |
| AppCacheService* service) |
| : BaseInternalsJob(request, network_delegate, service) {} |
| |
| virtual int GetData(std::string* mime_type, |
| std::string* charset, |
| std::string* data, |
| const net::CompletionCallback& callback) const OVERRIDE { |
| return net::OK; // IsRedirectResponse induces a redirect. |
| } |
| |
| virtual bool IsRedirectResponse(GURL* location, int* http_status_code) { |
| *location = ClearQuery(request_->url()); |
| *http_status_code = 307; |
| return true; |
| } |
| |
| protected: |
| virtual ~RedirectToMainPageJob() {} |
| }; |
| |
| // Job that removes an appcache and then redirects back to the main page. |
| class RemoveAppCacheJob : public RedirectToMainPageJob { |
| public: |
| RemoveAppCacheJob( |
| net::URLRequest* request, |
| net::NetworkDelegate* network_delegate, |
| AppCacheService* service, |
| const GURL& manifest_url) |
| : RedirectToMainPageJob(request, network_delegate, service), |
| manifest_url_(manifest_url), |
| ALLOW_THIS_IN_INITIALIZER_LIST(weak_factory_(this)) { |
| } |
| |
| virtual void Start() { |
| DCHECK(request_); |
| |
| appcache_service_->DeleteAppCacheGroup( |
| manifest_url_,base::Bind(&RemoveAppCacheJob::OnDeleteAppCacheComplete, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| private: |
| virtual ~RemoveAppCacheJob() {} |
| |
| void OnDeleteAppCacheComplete(int rv) { |
| StartAsync(); // Causes the base class to redirect. |
| } |
| |
| GURL manifest_url_; |
| base::WeakPtrFactory<RemoveAppCacheJob> weak_factory_; |
| }; |
| |
| |
| // Job shows the details of a particular manifest url. |
| class ViewAppCacheJob : public BaseInternalsJob, |
| public AppCacheStorage::Delegate { |
| public: |
| ViewAppCacheJob( |
| net::URLRequest* request, |
| net::NetworkDelegate* network_delegate, |
| AppCacheService* service, |
| const GURL& manifest_url) |
| : BaseInternalsJob(request, network_delegate, service), |
| manifest_url_(manifest_url) {} |
| |
| virtual void Start() { |
| DCHECK(request_); |
| appcache_service_->storage()->LoadOrCreateGroup(manifest_url_, this); |
| } |
| |
| // Produces a page containing the entries listing. |
| virtual int GetData(std::string* mime_type, |
| std::string* charset, |
| std::string* out, |
| const net::CompletionCallback& callback) const OVERRIDE { |
| mime_type->assign("text/html"); |
| charset->assign("UTF-8"); |
| out->clear(); |
| EmitPageStart(out); |
| if (appcache_info_.manifest_url.is_empty()) { |
| out->append(kManifestNotFoundMessage); |
| } else { |
| GURL base_url = ClearQuery(request_->url()); |
| EmitAppCacheInfo(base_url, appcache_service_, &appcache_info_, out); |
| EmitAppCacheResourceInfoVector(base_url, |
| manifest_url_, |
| resource_infos_, |
| appcache_info_.group_id, |
| out); |
| } |
| EmitPageEnd(out); |
| return net::OK; |
| } |
| |
| private: |
| virtual ~ViewAppCacheJob() { |
| appcache_service_->storage()->CancelDelegateCallbacks(this); |
| } |
| |
| // AppCacheStorage::Delegate override |
| virtual void OnGroupLoaded( |
| AppCacheGroup* group, const GURL& manifest_url) OVERRIDE { |
| DCHECK_EQ(manifest_url_, manifest_url); |
| if (group && group->newest_complete_cache()) { |
| appcache_info_.manifest_url = manifest_url; |
| appcache_info_.group_id = group->group_id(); |
| appcache_info_.size = group->newest_complete_cache()->cache_size(); |
| appcache_info_.creation_time = group->creation_time(); |
| appcache_info_.last_update_time = |
| group->newest_complete_cache()->update_time(); |
| appcache_info_.last_access_time = base::Time::Now(); |
| group->newest_complete_cache()->ToResourceInfoVector(&resource_infos_); |
| std::sort(resource_infos_.begin(), resource_infos_.end(), |
| SortByResourceUrl); |
| } |
| StartAsync(); |
| } |
| |
| GURL manifest_url_; |
| AppCacheInfo appcache_info_; |
| AppCacheResourceInfoVector resource_infos_; |
| DISALLOW_COPY_AND_ASSIGN(ViewAppCacheJob); |
| }; |
| |
| // Job that shows the details of a particular cached resource. |
| class ViewEntryJob : public BaseInternalsJob, |
| public AppCacheStorage::Delegate { |
| public: |
| ViewEntryJob( |
| net::URLRequest* request, |
| net::NetworkDelegate* network_delegate, |
| AppCacheService* service, |
| const GURL& manifest_url, |
| const GURL& entry_url, |
| int64 response_id, int64 group_id) |
| : BaseInternalsJob(request, network_delegate, service), |
| manifest_url_(manifest_url), entry_url_(entry_url), |
| response_id_(response_id), group_id_(group_id), amount_read_(0) { |
| } |
| |
| virtual void Start() { |
| DCHECK(request_); |
| appcache_service_->storage()->LoadResponseInfo( |
| manifest_url_, group_id_, response_id_, this); |
| } |
| |
| // Produces a page containing the response headers and data. |
| virtual int GetData(std::string* mime_type, |
| std::string* charset, |
| std::string* out, |
| const net::CompletionCallback& callback) const OVERRIDE { |
| mime_type->assign("text/html"); |
| charset->assign("UTF-8"); |
| out->clear(); |
| EmitPageStart(out); |
| EmitAnchor(entry_url_.spec(), entry_url_.spec(), out); |
| out->append("<br/>\n"); |
| if (response_info_) { |
| if (response_info_->http_response_info()) |
| EmitResponseHeaders(response_info_->http_response_info()->headers, out); |
| else |
| out->append("Failed to read response headers.<br>"); |
| |
| if (response_data_) { |
| EmitHexDump(response_data_->data(), amount_read_, |
| response_info_->response_data_size(), out); |
| } else { |
| out->append("Failed to read response data.<br>"); |
| } |
| } else { |
| out->append("Failed to read response headers and data.<br>"); |
| } |
| EmitPageEnd(out); |
| return net::OK; |
| } |
| |
| private: |
| virtual ~ViewEntryJob() { |
| appcache_service_->storage()->CancelDelegateCallbacks(this); |
| } |
| |
| virtual void OnResponseInfoLoaded( |
| AppCacheResponseInfo* response_info, int64 response_id) OVERRIDE { |
| if (!response_info) { |
| StartAsync(); |
| return; |
| } |
| response_info_ = response_info; |
| |
| // Read the response data, truncating if its too large. |
| const int64 kLimit = 100 * 1000; |
| int64 amount_to_read = |
| std::min(kLimit, response_info->response_data_size()); |
| response_data_ = new net::IOBuffer(amount_to_read); |
| |
| reader_.reset(appcache_service_->storage()->CreateResponseReader( |
| manifest_url_, group_id_, response_id_)); |
| reader_->ReadData( |
| response_data_, amount_to_read, |
| base::Bind(&ViewEntryJob::OnReadComplete, base::Unretained(this))); |
| } |
| |
| void OnReadComplete(int result) { |
| reader_.reset(); |
| amount_read_ = result; |
| if (result < 0) |
| response_data_ = NULL; |
| StartAsync(); |
| } |
| |
| GURL manifest_url_; |
| GURL entry_url_; |
| int64 response_id_; |
| int64 group_id_; |
| scoped_refptr<AppCacheResponseInfo> response_info_; |
| scoped_refptr<net::IOBuffer> response_data_; |
| int amount_read_; |
| scoped_ptr<AppCacheResponseReader> reader_; |
| }; |
| |
| } // namespace |
| |
| net::URLRequestJob* ViewAppCacheInternalsJobFactory::CreateJobForRequest( |
| net::URLRequest* request, |
| net::NetworkDelegate* network_delegate, |
| AppCacheService* service) { |
| if (!request->url().has_query()) |
| return new MainPageJob(request, network_delegate, service); |
| |
| std::string command; |
| std::string param; |
| ParseQuery(request->url().query(), &command, ¶m); |
| |
| if (command == kRemoveCacheCommand) |
| return new RemoveAppCacheJob(request, network_delegate, service, |
| DecodeBase64URL(param)); |
| |
| if (command == kViewCacheCommand) |
| return new ViewAppCacheJob(request, network_delegate, service, |
| DecodeBase64URL(param)); |
| |
| std::vector<std::string> tokens; |
| int64 response_id; |
| int64 group_id; |
| if (command == kViewEntryCommand && Tokenize(param, "|", &tokens) == 4u && |
| base::StringToInt64(tokens[2], &response_id) && |
| base::StringToInt64(tokens[3], &group_id)) { |
| return new ViewEntryJob(request, network_delegate, service, |
| DecodeBase64URL(tokens[0]), // manifest url |
| DecodeBase64URL(tokens[1]), // entry url |
| response_id, group_id); |
| } |
| |
| return new RedirectToMainPageJob(request, network_delegate, service); |
| } |
| |
| } // namespace appcache |