| // Copyright 2013 Google Inc. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| // |
| // Author: jefftk@google.com (Jeff Kaufman) |
| // Author: xqyin@google.com (XiaoQian Yin) |
| |
| #include "pagespeed/system/admin_site.h" |
| |
| #include <cstddef> |
| #include <memory> |
| #include <set> |
| #include <vector> |
| |
| #include "net/instaweb/http/public/async_fetch.h" |
| #include "net/instaweb/http/public/http_cache.h" |
| #include "net/instaweb/rewriter/public/rewrite_options.h" |
| #include "net/instaweb/rewriter/public/rewrite_query.h" |
| #include "net/instaweb/rewriter/public/server_context.h" |
| #include "pagespeed/system/system_cache_path.h" |
| #include "pagespeed/system/system_caches.h" |
| #include "pagespeed/system/system_rewrite_options.h" |
| #include "net/instaweb/util/public/property_cache.h" |
| #include "net/instaweb/util/public/property_store.h" |
| #include "pagespeed/kernel/base/cache_interface.h" |
| #include "pagespeed/kernel/base/callback.h" |
| #include "pagespeed/kernel/base/message_handler.h" |
| #include "pagespeed/kernel/base/statistics.h" |
| #include "pagespeed/kernel/base/string.h" |
| #include "pagespeed/kernel/base/string_util.h" |
| #include "pagespeed/kernel/base/string_writer.h" |
| #include "pagespeed/kernel/base/timer.h" |
| #include "pagespeed/kernel/cache/purge_context.h" |
| #include "pagespeed/kernel/html/html_keywords.h" |
| #include "pagespeed/kernel/http/content_type.h" |
| #include "pagespeed/kernel/http/google_url.h" |
| #include "pagespeed/kernel/http/http_names.h" |
| #include "pagespeed/kernel/http/query_params.h" |
| #include "pagespeed/kernel/http/request_headers.h" |
| #include "pagespeed/kernel/http/response_headers.h" |
| #include "pagespeed/kernel/util/statistics_logger.h" |
| |
| namespace net_instaweb { |
| |
| // Generated from JS, CSS, and HTML source via net/instaweb/js/data_to_c.cc. |
| extern const char* CSS_admin_site_css; |
| extern const char* CSS_caches_css; |
| extern const char* CSS_console_css; |
| extern const char* CSS_graphs_css; |
| extern const char* CSS_statistics_css; |
| extern const char* JS_caches_js; |
| extern const char* JS_caches_js_opt; |
| extern const char* JS_console_js; |
| extern const char* JS_console_js_opt; |
| extern const char* JS_graphs_js; |
| extern const char* JS_graphs_js_opt; |
| extern const char* JS_messages_js; |
| extern const char* JS_messages_js_opt; |
| extern const char* JS_statistics_js; |
| extern const char* JS_statistics_js_opt; |
| |
| namespace { |
| |
| struct Tab { |
| const char* label; |
| const char* title; |
| const char* admin_link; // relative from /pagespeed_admin/ |
| const char* statistics_link; // relative from /mod_pagespeed_statistics |
| const char* space; // html for inter-link spacing. |
| }; |
| |
| const char kShortBreak[] = " "; |
| const char kLongBreak[] = " "; |
| |
| // TODO(jmarantz): disable or recolor links to pages that are not available |
| // based on the current config. |
| const Tab kTabs[] = { |
| {"Statistics", "Statistics", "statistics", "?", kLongBreak}, |
| {"Configuration", "Configuration", "config", "?config", kShortBreak}, |
| {"(SPDY)", "SPDY Configuration", "spdy_config", "?spdy_config", kLongBreak}, |
| {"Histograms", "Histograms", "histograms", "?histograms", kLongBreak}, |
| {"Caches", "Caches", "cache", "?cache", kLongBreak}, |
| {"Console", "Console", "console", NULL, kLongBreak}, |
| {"Message History", "Message History", "message_history", NULL, kLongBreak}, |
| {"Graphs", "Graphs", "graphs", NULL, kLongBreak}, |
| }; |
| |
| // Controls the generation of an HTML Admin page. Constructing it |
| // establishes the content-type as HTML and response code 200, and |
| // puts in a banner with links to all the admin pages, ready for |
| // appending more <body> elements. Destructing AdminHtml closes the |
| // body and completes the fetch. |
| class AdminHtml { |
| public: |
| AdminHtml(StringPiece current_link, StringPiece head_extra, |
| AdminSite::AdminSource source, Timer* timer, |
| AsyncFetch* fetch, MessageHandler* handler) |
| : fetch_(fetch), |
| handler_(handler) { |
| fetch->response_headers()->SetStatusAndReason(HttpStatus::kOK); |
| fetch->response_headers()->Add(HttpAttributes::kContentType, "text/html"); |
| |
| int64 now_ms = timer->NowMs(); |
| fetch->response_headers()->SetLastModified(now_ms); |
| |
| // Let PageSpeed dynamically minify the html, css, and javasript |
| // generated by the admin page, to the extent it's not done |
| // already by the tools. Note, this does mean that viewing the |
| // statistics and histograms pages will affect the statistics and |
| // histograms. If we decide this is too annoying, then we can |
| // instead procedurally minify the css/js and leave the html |
| // alone. |
| // |
| // Note that we at least turn off add_instrumenation here by explicitly |
| // giving a filter list without "+" or "-". |
| fetch->response_headers()->Add( |
| RewriteQuery::kPageSpeedFilters, |
| "rewrite_css,rewrite_javascript,collapse_whitespace"); |
| |
| // Generate some navigational links to help our users get to other |
| // admin pages. |
| fetch->Write("<!DOCTYPE html>\n<html><head>", handler_); |
| fetch->Write(StrCat("<style>", CSS_admin_site_css, "</style>"), handler_); |
| |
| GoogleString buf; |
| for (int i = 0, n = arraysize(kTabs); i < n; ++i) { |
| const Tab& tab = kTabs[i]; |
| const char* link = NULL; |
| switch (source) { |
| case AdminSite::kPageSpeedAdmin: |
| link = tab.admin_link; |
| break; |
| case AdminSite::kStatistics: |
| link = tab.statistics_link; |
| break; |
| case AdminSite::kOther: |
| link = NULL; |
| break; |
| } |
| if (link != NULL) { |
| StringPiece style; |
| if (tab.admin_link == current_link) { |
| style = " style='color:darkblue;text-decoration:underline;'"; |
| fetch->Write(StrCat("<title>PageSpeed ", tab.title, "</title>"), |
| handler_); |
| } |
| StrAppend(&buf, |
| "<a href='", link, "'", style, ">", tab.label, "</a>", |
| tab.space); |
| } |
| } |
| |
| fetch->Write(StrCat(head_extra, "</head>"), handler_); |
| fetch->Write( |
| StrCat("<body class='pagespeed-admin-body'>" |
| "<div class='pagespeed-admin-tabs'>\n" |
| "<b>Pagespeed Admin</b>", kLongBreak, "\n"), |
| handler_); |
| fetch->Write(buf, handler_); |
| fetch->Write("</div><hr/>\n", handler_); |
| fetch->Flush(handler_); |
| } |
| |
| ~AdminHtml() { |
| fetch_->Write("</body></html>", handler_); |
| fetch_->Done(true); |
| } |
| |
| private: |
| AsyncFetch* fetch_; |
| MessageHandler* handler_; |
| }; |
| |
| } // namespace |
| |
| AdminSite::AdminSite(StaticAssetManager* static_asset_manager, Timer* timer, |
| MessageHandler* message_handler) |
| : message_handler_(message_handler), |
| static_asset_manager_(static_asset_manager), |
| timer_(timer) { |
| } |
| |
| // Handler which serves PSOL console. |
| void AdminSite::ConsoleHandler(const SystemRewriteOptions& global_options, |
| const RewriteOptions& options, |
| AdminSource source, |
| const QueryParams& query_params, |
| AsyncFetch* fetch, Statistics* statistics) { |
| if (query_params.Has("json")) { |
| ConsoleJsonHandler(query_params, fetch, statistics); |
| return; |
| } |
| |
| MessageHandler* handler = message_handler_; |
| bool statistics_enabled = global_options.statistics_enabled(); |
| bool logging_enabled = global_options.statistics_logging_enabled(); |
| bool log_dir_set = !global_options.log_dir().empty(); |
| |
| // TODO(jmarantz): change StaticAssetManager to take options by const ref. |
| // TODO(sligocki): Move static content to a data2cc library. |
| StringPiece console_js = options.Enabled(RewriteOptions::kDebug) ? |
| JS_console_js : |
| JS_console_js_opt; |
| // TODO(sligocki): Do we want to have a minified version of console CSS? |
| GoogleString head_markup = StrCat( |
| "<style>", CSS_console_css, "</style>\n"); |
| AdminHtml admin_html("console", head_markup, source, timer_, fetch, |
| message_handler_); |
| if (statistics_enabled && logging_enabled && log_dir_set) { |
| fetch->Write("<div class='console_div' id='suggestions'>\n" |
| " <div class='console_div' id='pagespeed-graphs-container'>" |
| "</div>\n</div>\n" |
| "<script src='https://www.google.com/jsapi'></script>\n" |
| "<script>var pagespeedStatisticsUrl = '';</script>\n" |
| "<script>", handler); |
| // From the admin page, the console JSON is relative, so it can |
| // be set to ''. Formerly it was set to options.statistics_handler_path(), |
| // but there does not appear to be a disadvantage to always handling it |
| // from whatever URL served this console HTML. |
| // |
| // TODO(jmarantz): Change the JS to remove pagespeedStatisticsUrl. |
| fetch->Write(console_js, handler); |
| fetch->Write("google.setOnLoadCallback(pagespeed.startConsole);", handler); |
| fetch->Write("</script>\n", handler); |
| } else { |
| fetch->Write("<p>\n" |
| " Failed to load PageSpeed Console because:\n" |
| "</p>\n" |
| "<ul>\n", handler); |
| if (!statistics_enabled) { |
| fetch->Write(" <li>Statistics is not enabled.</li>\n", |
| handler); |
| } |
| if (!logging_enabled) { |
| fetch->Write(" <li>StatisticsLogging is not enabled." |
| "</li>\n", handler); |
| } |
| if (!log_dir_set) { |
| fetch->Write(" <li>LogDir is not set.</li>\n", handler); |
| } |
| fetch->Write("</ul>\n" |
| "<p>\n" |
| " In order to use the console you must configure these\n" |
| " options. See the <a href='https://developers.google.com/" |
| "speed/pagespeed/module/console'>console documentation</a>\n" |
| " for more details.\n" |
| "</p>\n", handler); |
| } |
| } |
| |
| void AdminSite::StatisticsJsonHandler(AsyncFetch* fetch, Statistics* stats) { |
| fetch->response_headers()->SetStatusAndReason(HttpStatus::kOK); |
| fetch->response_headers()->Add(HttpAttributes::kContentType, |
| kContentTypeJson.mime_type()); |
| stats->DumpJson(fetch, message_handler_); |
| fetch->Done(true); |
| } |
| |
| void AdminSite::StatisticsHandler(const RewriteOptions& options, |
| AdminSource source, AsyncFetch* fetch, |
| Statistics* stats) { |
| GoogleString head_markup = StrCat( |
| "<style>", CSS_statistics_css, "</style>\n"); |
| AdminHtml admin_html("statistics", head_markup, source, timer_, fetch, |
| message_handler_); |
| // We have to call Dump() here to write data to sources for |
| // system/system_test.sh: Line 79. We use JS to update the data in refreshes. |
| fetch->Write("<pre id='stat'>", message_handler_); |
| stats->Dump(fetch, message_handler_); |
| fetch->Write("</pre>\n", message_handler_); |
| StringPiece statistics_js = options.Enabled(RewriteOptions::kDebug) ? |
| JS_statistics_js : |
| JS_statistics_js_opt; |
| fetch->Write(StrCat("<script type='text/javascript'>", statistics_js, |
| "\npagespeed.Statistics.Start();</script>\n"), |
| message_handler_); |
| } |
| |
| void AdminSite::GraphsHandler(const RewriteOptions& options, |
| AdminSource source, |
| const QueryParams& query_params, |
| AsyncFetch* fetch, |
| Statistics* statistics) { |
| if (query_params.Has("json")) { |
| ConsoleJsonHandler(query_params, fetch, statistics); |
| return; |
| } |
| GoogleString head_markup = StrCat( |
| "<style>", CSS_graphs_css, "</style>\n"); |
| AdminHtml admin_html("graphs", head_markup, source, timer_, fetch, |
| message_handler_); |
| fetch->Write("<div id='cache_applied'>Loading Charts...</div>" |
| "<div id='cache_type'>Loading Charts...</div>" |
| "<div id='ipro'>Loading Charts...</div>" |
| "<div id='image_rewriting'>Loading Charts...</div>" |
| "<div id='realtime'>Loading Charts...</div>", |
| message_handler_); |
| fetch->Write("<script type='text/javascript' " |
| "src='https://www.google.com/jsapi'></script>", |
| message_handler_); |
| StringPiece graphs_js = options.Enabled(RewriteOptions::kDebug) ? |
| JS_graphs_js : |
| JS_graphs_js_opt; |
| fetch->Write(StrCat("<script type='text/javascript'>", graphs_js, |
| "\npagespeed.Graphs.Start();</script>\n"), |
| message_handler_); |
| } |
| |
| void AdminSite::ConsoleJsonHandler(const QueryParams& params, |
| AsyncFetch* fetch, Statistics* statistics) { |
| StatisticsLogger* console_logger = statistics->console_logger(); |
| if (console_logger == NULL) { |
| fetch->response_headers()->SetStatusAndReason(HttpStatus::kNotFound); |
| fetch->response_headers()->Add(HttpAttributes::kContentType, "text/plain"); |
| fetch->Write( |
| "console_logger must be enabled to use '?json' query parameter.", |
| message_handler_); |
| } else { |
| fetch->response_headers()->SetStatusAndReason(HttpStatus::kOK); |
| |
| fetch->response_headers()->Add(HttpAttributes::kContentType, |
| kContentTypeJson.mime_type()); |
| |
| std::set<GoogleString> var_titles; |
| // Default is to fetch data used on graphs page. |
| bool dump_for_graphs = true; |
| |
| // Default values for start_time, end_time, and granularity_ms in case the |
| // query does not include these parameters. |
| int64 start_time = 0; |
| int64 end_time = timer_->NowMs(); |
| // Granularity is the difference in ms between data points. If it is not |
| // specified by the query, the default value is 3000 ms, the same as the |
| // default logging granularity. |
| int64 granularity_ms = 3000; |
| for (int i = 0; i < params.size(); ++i) { |
| GoogleString value; |
| if (params.UnescapedValue(i, &value)) { |
| StringPiece name = params.name(i); |
| if (name =="start_time") { |
| StringToInt64(value, &start_time); |
| } else if (name == "end_time") { |
| StringToInt64(value, &end_time); |
| } else if (name == "var_titles") { |
| // Fetch specified data if users populate var_titles. |
| dump_for_graphs = false; |
| std::vector<StringPiece> variable_names; |
| SplitStringPieceToVector(value, ",", &variable_names, true); |
| for (size_t i = 0; i < variable_names.size(); ++i) { |
| var_titles.insert(variable_names[i].as_string()); |
| } |
| } else if (name == "granularity") { |
| StringToInt64(value, &granularity_ms); |
| } |
| } |
| } |
| console_logger->DumpJSON(dump_for_graphs, var_titles, start_time, end_time, |
| granularity_ms, fetch, message_handler_); |
| } |
| fetch->Done(true); |
| } |
| |
| void AdminSite::PrintHistograms(AdminSource source, AsyncFetch* fetch, |
| Statistics* stats) { |
| AdminHtml admin_html("histograms", "", source, timer_, fetch, |
| message_handler_); |
| stats->RenderHistograms(fetch, message_handler_); |
| } |
| |
| namespace { |
| |
| static const char kTableStart[] = |
| "<table class='pagespeed-caches-structure'>\n" |
| " <thead>\n" |
| " <tr>\n" |
| " <td>Cache</td><td>Detail</td><td>Structure</td>\n" |
| " </tr>\n" |
| " </thead>\n" |
| " <tbody>"; |
| |
| static const char kTableEnd[] = |
| " </tbody>\n" |
| "</table>"; |
| |
| // Takes a complicated descriptor like |
| // "HTTPCache(Fallback(small=Batcher(cache=Stats(parefix=memcached_async," |
| // "cache=Async(AprMemCache)),parallelism=1,max=1000),large=Stats(" |
| // "prefix=file_cache,cache=FileCache)))" |
| // and strips away the crap most users don't want to see, as they most |
| // likely did not configure it, and return |
| // "Async AprMemCache FileCache" |
| GoogleString HackCacheDescriptor(StringPiece name) { |
| GoogleString out; |
| // There's a lot of complicated syntax in the cache name giving the |
| // detailed hierarchical structure. This is really hard to read and |
| // overly cryptic; it's designed for unit tests. But let's extract |
| // a few keywords out of this to understand the main pointers. |
| static const char* kCacheKeywords[] = { |
| "Compressed", "Async", "SharedMemCache", "LRUCache", "AprMemCache", |
| "FileCache" |
| }; |
| const char* delim = ""; |
| for (int i = 0, n = arraysize(kCacheKeywords); i < n; ++i) { |
| if (name.find(kCacheKeywords[i]) != StringPiece::npos) { |
| StrAppend(&out, delim, kCacheKeywords[i]); |
| delim = " "; |
| } |
| } |
| if (out.empty()) { |
| name.CopyToString(&out); |
| } |
| return out; |
| } |
| |
| // Takes a complicated descriptor like |
| // "HTTPCache(Fallback(small=Batcher(cache=Stats(prefix=memcached_async," |
| // "cache=Async(AprMemCache)),parallelism=1,max=1000),large=Stats(" |
| // "prefix=file_cache,cache=FileCache)))" |
| // and injects HTML line-breaks and indentation based on the parent depth, |
| // yielding HTML that renders like this (with and <br/>) |
| // HTTPCache( |
| // Fallback( |
| // small=Batcher( |
| // cache=Stats( |
| // prefix=memcached_async, |
| // cache=Async( |
| // AprMemCache)), |
| // parallelism=1, |
| // max=1000), |
| // large=Stats( |
| // prefix=file_cache, |
| // cache=FileCache))) |
| GoogleString IndentCacheDescriptor(StringPiece name) { |
| GoogleString out, buf; |
| int depth = 0; |
| for (int i = 0, n = name.size(); i < n; ++i) { |
| StrAppend(&out, HtmlKeywords::Escape(name.substr(i, 1), &buf)); |
| switch (name[i]) { |
| case '(': |
| ++depth; |
| FALLTHROUGH_INTENDED; |
| case ',': |
| out += "<br/>"; |
| for (int j = 0; j < depth; ++j) { |
| out += " "; |
| } |
| break; |
| case ')': |
| --depth; |
| break; |
| } |
| } |
| return out; |
| } |
| |
| GoogleString CacheInfoHtmlSnippet(StringPiece label, StringPiece descriptor) { |
| GoogleString out, escaped; |
| StrAppend(&out, "<tr style='vertical-align:top;'><td>", label, |
| "</td><td><input id='", label); |
| StrAppend(&out, "_toggle' type='checkbox' ", |
| "onclick=\"pagespeed.Caches.toggleDetail('", label, |
| "')\"/></td><td><code id='", label, "_summary'>"); |
| StrAppend(&out, HtmlKeywords::Escape(HackCacheDescriptor(descriptor), |
| &escaped)); |
| StrAppend(&out, "</code><code id='", label, |
| "_detail' style='display:none;'>"); |
| StrAppend(&out, IndentCacheDescriptor(descriptor)); |
| StrAppend(&out, "</code></td></tr>\n"); |
| return out; |
| } |
| |
| } // namespace |
| |
| void AdminSite::PrintCaches(bool is_global, AdminSource source, |
| const GoogleUrl& stripped_gurl, |
| const QueryParams& query_params, |
| const RewriteOptions* options, |
| SystemCachePath* cache_path, |
| AsyncFetch* fetch, SystemCaches* system_caches, |
| CacheInterface* filesystem_metadata_cache, |
| HTTPCache* http_cache, |
| CacheInterface* metadata_cache, |
| PropertyCache* page_property_cache, |
| ServerContext* server_context) { |
| GoogleString url; |
| if ((source == kPageSpeedAdmin) && |
| query_params.Lookup1Unescaped("url", &url)) { |
| // Delegate to ShowCacheHandler to get the cached value for that |
| // URL, which it may do asynchronously, so we cannot use the |
| // AdminHtml abstraction which closes the connection in its |
| // destructor. |
| GoogleString json_dummy; |
| ServerContext::Format format = ServerContext::kFormatAsHtml; |
| if (query_params.Lookup1Unescaped("json", &json_dummy)) { |
| format = ServerContext::kFormatAsJson; |
| } |
| GoogleString ua; |
| query_params.Lookup1Unescaped("user_agent", &ua); |
| server_context->ShowCacheHandler( |
| format, url, ua, query_params.Has("Delete"), fetch, options->Clone()); |
| } else if ((source == kPageSpeedAdmin) && |
| query_params.Lookup1Unescaped("new_set", &url)) { |
| ResponseHeaders* response_headers = fetch->response_headers(); |
| response_headers->SetStatusAndReason(HttpStatus::kOK); |
| response_headers->Add(HttpAttributes::kContentType, "text/html"); |
| fetch->Write(options->PurgeSetString(), message_handler_); |
| fetch->Done(true); |
| } else if ((source == kPageSpeedAdmin) && |
| query_params.Lookup1Unescaped("purge", &url)) { |
| ResponseHeaders* response_headers = fetch->response_headers(); |
| if (!options->enable_cache_purge()) { |
| response_headers->SetStatusAndReason(HttpStatus::kOK); |
| response_headers->Add(HttpAttributes::kContentType, "text/html"); |
| // TODO(jmarantz): virtualize the formatting of this message so that |
| // it's correct in ngx_pagespeed and mod_pagespeed (and IISpeed etc). |
| fetch->Write("Purging not enabled: please add\n", message_handler_); |
| HtmlKeywords::WritePre( |
| StrCat(" ", server_context->FormatOption("EnableCachePurge", "on")), |
| "", fetch, message_handler_); |
| fetch->Write( |
| "\nto your configuration file. See the \n" |
| "<a href='https://developers.google.com/speed/pagespeed/module" |
| "/system#purge_cache'>documentation</a> " |
| "for more information about cache purging.", |
| message_handler_); |
| fetch->Done(true); |
| } else if (url == "*") { |
| PurgeHandler(url, cache_path, fetch); |
| } else if (url.empty()) { |
| response_headers->SetStatusAndReason(HttpStatus::kOK); |
| response_headers->Add(HttpAttributes::kContentType, "text/html"); |
| fetch->Write("Empty URL", message_handler_); |
| fetch->Done(true); |
| } else { |
| GoogleUrl origin(stripped_gurl.Origin()); |
| GoogleUrl resolved(origin, url); |
| if (!resolved.IsWebValid()) { |
| response_headers->SetStatusAndReason(HttpStatus::kOK); |
| response_headers->Add(HttpAttributes::kContentType, "text/html"); |
| GoogleString escaped_url; |
| HtmlKeywords::Escape(url, &escaped_url); |
| fetch->Write(StrCat("Invalid URL: ", escaped_url), message_handler_); |
| fetch->Done(true); |
| } else { |
| PurgeHandler(resolved.Spec(), cache_path, fetch); |
| } |
| } |
| } else { |
| GoogleString head_markup = StrCat( |
| "<style>", CSS_caches_css, "</style>\n"); |
| AdminHtml admin_html("cache", head_markup, source, timer_, fetch, |
| message_handler_); |
| |
| fetch->Write("<div id='show_metadata'>", message_handler_); |
| // Present a small form to enter a URL. |
| if (source == kPageSpeedAdmin) { |
| const char* user_agent = fetch->request_headers()->Lookup1( |
| HttpAttributes::kUserAgent); |
| fetch->Write(ServerContext::ShowCacheForm(user_agent), message_handler_); |
| } |
| fetch->Write("</div>\n", message_handler_); |
| // Display configured cache information. |
| if (system_caches != NULL) { |
| int flags = SystemCaches::kDefaultStatFlags; |
| if (is_global) { |
| flags |= SystemCaches::kGlobalView; |
| } |
| |
| // TODO(jmarantz): Consider whether it makes sense to disable |
| // either of these flags to limit the content when someone asks |
| // for info about the cache. |
| flags |= SystemCaches::kIncludeMemcached; |
| fetch->Write("<div id='cache_struct'>", |
| message_handler_); |
| fetch->Write(kTableStart, message_handler_); |
| CacheInterface* fsmdc = filesystem_metadata_cache; |
| fetch->Write(StrCat( |
| CacheInfoHtmlSnippet("HTTP Cache", http_cache->Name()), |
| CacheInfoHtmlSnippet("Metadata Cache", metadata_cache->Name()), |
| CacheInfoHtmlSnippet("Property Cache", |
| page_property_cache->property_store()->Name()), |
| CacheInfoHtmlSnippet("FileSystem Metadata Cache", |
| (fsmdc == NULL) ? "none" : fsmdc->Name())), |
| message_handler_); |
| fetch->Write(kTableEnd, message_handler_); |
| fetch->Write("</div>", message_handler_); |
| |
| fetch->Write("<div id='physical_cache'>", |
| message_handler_); |
| GoogleString backend_stats; |
| system_caches->PrintCacheStats( |
| static_cast<SystemCaches::StatFlags>(flags), &backend_stats); |
| if (!backend_stats.empty()) { |
| HtmlKeywords::WritePre(backend_stats, "", fetch, message_handler_); |
| } |
| fetch->Write("</div>", message_handler_); |
| |
| fetch->Write("<div id='purge_cache'>", |
| message_handler_); |
| // Filled in by JS in caches.js: updatePurgeSet(). |
| fetch->Write("<h3>Purge Set</h3>" |
| "<div id='purge_global'" |
| " class='pagespeed-caches-purge-global'></div>" |
| "<div id='purge_table'" |
| " class='pagespeed-caches-purge-table'></div>" |
| "</div>", // closes 'purge_cache'. |
| message_handler_); |
| } |
| StringPiece caches_js = options->Enabled(RewriteOptions::kDebug) ? |
| JS_caches_js : |
| JS_caches_js_opt; |
| // Practice what we preach: put the blocking JS in the tail. |
| // TODO(jmarantz): use static asset manager to compile & deliver JS |
| // externally. |
| fetch->Write(StrCat("<script type='text/javascript'>", caches_js, |
| "\npagespeed.Caches.Start();</script>\n"), |
| message_handler_); |
| } |
| } |
| |
| void AdminSite::PrintNormalConfig( |
| AdminSource source, AsyncFetch* fetch, |
| SystemRewriteOptions* global_system_rewrite_options) { |
| AdminHtml admin_html("config", "", source, timer_, fetch, message_handler_); |
| HtmlKeywords::WritePre( |
| global_system_rewrite_options->OptionsToString(), "", |
| fetch, message_handler_); |
| } |
| |
| void AdminSite::PrintSpdyConfig(AdminSource source, AsyncFetch* fetch, |
| const SystemRewriteOptions* spdy_config) { |
| AdminHtml admin_html("spdy_config", "", source, timer_, fetch, |
| message_handler_); |
| if (spdy_config == NULL) { |
| fetch->Write("SPDY-specific configuration missing.", message_handler_); |
| } else { |
| HtmlKeywords::WritePre(spdy_config->OptionsToString(), "", |
| fetch, message_handler_); |
| } |
| } |
| |
| void AdminSite::MessageHistoryHandler(const RewriteOptions& options, |
| AdminSource source, AsyncFetch* fetch) { |
| // Request for page /mod_pagespeed_message. |
| GoogleString log; |
| StringWriter log_writer(&log); |
| AdminHtml admin_html("message_history", "", source, timer_, fetch, |
| message_handler_); |
| if (message_handler_->Dump(&log_writer)) { |
| fetch->Write("<div id='log'>", message_handler_); |
| // Write pre-tag and color messages. |
| StringPieceVector messages; |
| message_handler_->ParseMessageDumpIntoMessages(log, &messages); |
| for (int i = 0, size = messages.size(); i < size; ++i) { |
| if (messages[i].length() > 0) { |
| switch (message_handler_->GetMessageType(messages[i])) { |
| case kError: { |
| HtmlKeywords::WritePre( |
| message_handler_->ReformatMessage(messages[i]), |
| "color:red; margin:0;", fetch, message_handler_); |
| break; |
| } |
| case kWarning: { |
| HtmlKeywords::WritePre( |
| message_handler_->ReformatMessage(messages[i]), |
| "color:brown; margin:0;", fetch, message_handler_); |
| break; |
| } |
| case kFatal: { |
| HtmlKeywords::WritePre( |
| message_handler_->ReformatMessage(messages[i]), |
| "color:orange; margin:0;", fetch, message_handler_); |
| break; |
| } |
| default: { |
| HtmlKeywords::WritePre( |
| message_handler_->ReformatMessage(messages[i]), |
| "margin:0;", fetch, message_handler_); |
| } |
| } |
| } |
| } |
| fetch->Write("</div>\n", message_handler_); |
| StringPiece messages_js = options.Enabled(RewriteOptions::kDebug) ? |
| JS_messages_js : |
| JS_messages_js_opt; |
| fetch->Write(StrCat("<script type='text/javascript'>", messages_js, |
| "\npagespeed.Messages.Start();</script>\n"), |
| message_handler_); |
| } else { |
| fetch->Write("<p>Writing to mod_pagespeed_message failed. \n" |
| "Verify that MessageBufferSize is not set to 0 " |
| "in pagespeed.conf.</p>\n", |
| message_handler_); |
| } |
| } |
| |
| void AdminSite::AdminPage( |
| bool is_global, const GoogleUrl& stripped_gurl, |
| const QueryParams& query_params, const RewriteOptions* options, |
| SystemCachePath* cache_path, AsyncFetch* fetch, SystemCaches* system_caches, |
| CacheInterface* filesystem_metadata_cache, HTTPCache* http_cache, |
| CacheInterface* metadata_cache, PropertyCache* page_property_cache, |
| ServerContext* server_context, Statistics* statistics, Statistics* stats, |
| SystemRewriteOptions* global_system_rewrite_options, |
| const SystemRewriteOptions* spdy_config) { |
| // The handler is "pagespeed_admin", so we must dispatch off of |
| // the remainder of the URL. For |
| // "http://example.com/pagespeed_admin/foo?a=b" we want to pull out |
| // "foo". |
| // |
| // Note that the comments here referring to "/pagespeed_admin" reflect |
| // only the default admin path in Apache for fresh installs. In fact |
| // we can put the handler on any path, and this code should still work; |
| // all the paths here are specified relative to the incoming URL. |
| StringPiece path = stripped_gurl.PathSansQuery(); // "/pagespeed_admin/foo" |
| path = path.substr(1); // "pagespeed_admin/foo" |
| |
| // If there are no slashes at all in the path, e.g. it's "pagespeed_admin", |
| // then the relative references to "config" etc will not work. We need |
| // to serve the admin pages on "/pagespeed_admin/". So if we got to this |
| // point and there are no slashes, then we can just redirect immediately |
| // by adding a slash. |
| // |
| // If the user has mapped the pagespeed_admin handler to a path with |
| // an embedded slash, say "pagespeed/myadmin", then it's hard to tell |
| // whether we should redirect, because we don't know what the the |
| // intended path is. In this case, we'll fall through to a leaf |
| // analysis on "myadmin", fail to find a match, and print a "Did You Mean" |
| // page. It's not as good as a redirect but since we can't tell an |
| // omitted slash from a typo it's the best we can do. |
| if (path.find('/') == StringPiece::npos) { |
| // If the URL is "/pagespeed_admin", then redirect to "/pagespeed_admin/" so |
| // that relative URL references will work. |
| ResponseHeaders* response_headers = fetch->response_headers(); |
| response_headers->SetStatusAndReason(HttpStatus::kMovedPermanently); |
| GoogleString admin_with_slash = StrCat(stripped_gurl.AllExceptQuery(), "/"); |
| response_headers->Add(HttpAttributes::kLocation, admin_with_slash); |
| response_headers->Add(HttpAttributes::kContentType, "text/html"); |
| GoogleString escaped_url; |
| HtmlKeywords::Escape(admin_with_slash, &escaped_url); |
| fetch->Write(StrCat("Redirecting to URL ", escaped_url), message_handler_); |
| fetch->Done(true); |
| } else { |
| StringPiece leaf = stripped_gurl.LeafSansQuery(); |
| if ((leaf == "statistics") || (leaf.empty())) { |
| StatisticsHandler(*options, kPageSpeedAdmin, fetch, stats); |
| } else if (leaf == "stats_json") { |
| StatisticsJsonHandler(fetch, stats); |
| } else if (leaf == "graphs") { |
| GraphsHandler(*options, kPageSpeedAdmin, query_params, fetch, statistics); |
| } else if (leaf == "config") { |
| PrintNormalConfig(kPageSpeedAdmin, fetch, global_system_rewrite_options); |
| } else if (leaf == "spdy_config") { |
| PrintSpdyConfig(kPageSpeedAdmin, fetch, spdy_config); |
| } else if (leaf == "console") { |
| // TODO(jmarantz): add vhost-local and aggregate message buffers. |
| ConsoleHandler(*global_system_rewrite_options, *options, kPageSpeedAdmin, |
| query_params, fetch, statistics); |
| } else if (leaf == "message_history") { |
| MessageHistoryHandler(*options, kPageSpeedAdmin, fetch); |
| } else if (leaf == "cache") { |
| PrintCaches(is_global, kPageSpeedAdmin, stripped_gurl, query_params, |
| options, cache_path, fetch, system_caches, |
| filesystem_metadata_cache, http_cache, metadata_cache, |
| page_property_cache, server_context); |
| } else if (leaf == "histograms") { |
| PrintHistograms(kPageSpeedAdmin, fetch, stats); |
| } else { |
| fetch->response_headers()->SetStatusAndReason(HttpStatus::kNotFound); |
| fetch->response_headers()->Add(HttpAttributes::kContentType, "text/html"); |
| fetch->Write("Unknown admin page: ", message_handler_); |
| HtmlKeywords::WritePre(leaf, "", fetch, message_handler_); |
| |
| // It's possible that the handler is installed on /a/b/c, and we |
| // are now reporting "unknown admin page: c". This is kind of a guess, |
| // but provide a nice link here to what might be the correct admin page. |
| // |
| // This is just a guess, so we don't want to redirect. |
| fetch->Write("<br/>Did you mean to visit: ", message_handler_); |
| GoogleString escaped_url; |
| HtmlKeywords::Escape(StrCat(stripped_gurl.AllExceptQuery(), "/"), |
| &escaped_url); |
| fetch->Write(StrCat("<a href='", escaped_url, "'>", escaped_url, |
| "</a>\n"), |
| message_handler_); |
| fetch->Done(true); |
| } |
| } |
| } |
| |
| void AdminSite::StatisticsPage( |
| bool is_global, const QueryParams& query_params, |
| const RewriteOptions* options, AsyncFetch* fetch, |
| SystemCaches* system_caches, CacheInterface* filesystem_metadata_cache, |
| HTTPCache* http_cache, CacheInterface* metadata_cache, |
| PropertyCache* page_property_cache, ServerContext* server_context, |
| Statistics* statistics, Statistics* stats, |
| SystemRewriteOptions* global_system_rewrite_options, |
| const SystemRewriteOptions* spdy_config) { |
| if (query_params.Has("json")) { |
| ConsoleJsonHandler(query_params, fetch, statistics); |
| } else if (query_params.Has("config")) { |
| PrintNormalConfig(kStatistics, fetch, global_system_rewrite_options); |
| } else if (query_params.Has("spdy_config")) { |
| PrintSpdyConfig(kStatistics, fetch, spdy_config); |
| } else if (query_params.Has("histograms")) { |
| PrintHistograms(kStatistics, fetch, stats); |
| } else if (query_params.Has("graphs")) { |
| GraphsHandler(*options, kStatistics, query_params, fetch, statistics); |
| } else if (query_params.Has("cache")) { |
| GoogleUrl empty_url; |
| PrintCaches(is_global, kStatistics, empty_url, query_params, |
| options, NULL, // cache_path is reference from statistics page. |
| fetch, system_caches, filesystem_metadata_cache, |
| http_cache, metadata_cache, page_property_cache, |
| server_context); |
| } else { |
| StatisticsHandler(*options, kStatistics, fetch, stats); |
| } |
| } |
| |
| namespace { |
| |
| // Provides a Done(bool, StringPiece) entry point for use as a Purge |
| // callback. Translates the success into an Http status code for the |
| // AsyncFetch, sending any failure reason in the response body. |
| class PurgeFetchCallbackGasket { |
| public: |
| PurgeFetchCallbackGasket(AsyncFetch* fetch, MessageHandler* handler) |
| : fetch_(fetch), |
| message_handler_(handler) { |
| } |
| void Done(bool success, StringPiece reason) { |
| ResponseHeaders* headers = fetch_->response_headers(); |
| headers->set_status_code(HttpStatus::kOK); |
| headers->Add(HttpAttributes::kContentType, "text/html"); |
| // TODO(xqyin): Currently we may still return 'purge successful' even if |
| // the URL does not exist in our cache. Figure out how to solve this case |
| // while we don't want to search the whole cache which could be very large. |
| if (success) { |
| fetch_->Write("Purge successful", message_handler_); |
| } else { |
| GoogleString buf; |
| fetch_->Write(HtmlKeywords::Escape(reason, &buf), message_handler_); |
| fetch_->Write("\n", message_handler_); |
| fetch_->Write(HtmlKeywords::Escape(error_, &buf), message_handler_); |
| } |
| fetch_->Done(true); |
| delete this; |
| } |
| |
| void set_error(StringPiece x) { x.CopyToString(&error_); } |
| |
| private: |
| AsyncFetch* fetch_; |
| MessageHandler* message_handler_; |
| GoogleString error_; |
| |
| DISALLOW_COPY_AND_ASSIGN(PurgeFetchCallbackGasket); |
| }; |
| |
| } // namespace |
| |
| void AdminSite::PurgeHandler(StringPiece url, SystemCachePath* cache_path, |
| AsyncFetch* fetch) { |
| PurgeContext* purge_context = cache_path->purge_context(); |
| int64 now_ms = timer_->NowMs(); |
| PurgeFetchCallbackGasket* gasket = |
| new PurgeFetchCallbackGasket(fetch, message_handler_); |
| PurgeContext::PurgeCallback* callback = NewCallback( |
| gasket, &PurgeFetchCallbackGasket::Done); |
| if (url.ends_with("*")) { |
| // If the url is "*" we'll just purge everything. Note that we will |
| // ignore any sub-paths in the expression. We can only purge the |
| // entire cache, or specific URLs, not general wildcards. |
| purge_context->SetCachePurgeGlobalTimestampMs(now_ms, callback); |
| } else { |
| purge_context->AddPurgeUrl(url, now_ms, callback); |
| } |
| } |
| |
| } // namespace net_instaweb |