"Fossies" - the Fresh Open Source Software Archive

Member "incubator-pagespeed-mod-1.14.36.1/pagespeed/system/admin_site.cc" (28 Feb 2020, 37430 Bytes) of package /linux/www/apache_httpd_modules/incubator-pagespeed-mod-1.14.36.1.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) C and C++ source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. For more information about "admin_site.cc" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 1.13.35.2_vs_1.14.36.1.

    1 /*
    2  * Licensed to the Apache Software Foundation (ASF) under one
    3  * or more contributor license agreements.  See the NOTICE file
    4  * distributed with this work for additional information
    5  * regarding copyright ownership.  The ASF licenses this file
    6  * to you under the Apache License, Version 2.0 (the
    7  * "License"); you may not use this file except in compliance
    8  * with the License.  You may obtain a copy of the License at
    9  * 
   10  *   http://www.apache.org/licenses/LICENSE-2.0
   11  * 
   12  * Unless required by applicable law or agreed to in writing,
   13  * software distributed under the License is distributed on an
   14  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
   15  * KIND, either express or implied.  See the License for the
   16  * specific language governing permissions and limitations
   17  * under the License.
   18  */
   19 
   20 
   21 #include "pagespeed/system/admin_site.h"
   22 
   23 #include <cstddef>
   24 #include <memory>
   25 #include <set>
   26 #include <vector>
   27 
   28 #include "net/instaweb/http/public/async_fetch.h"
   29 #include "net/instaweb/http/public/http_cache.h"
   30 #include "net/instaweb/rewriter/public/rewrite_options.h"
   31 #include "net/instaweb/rewriter/public/rewrite_query.h"
   32 #include "net/instaweb/rewriter/public/server_context.h"
   33 #include "net/instaweb/util/public/property_cache.h"
   34 #include "net/instaweb/util/public/property_store.h"
   35 #include "strings/stringpiece_utils.h"
   36 #include "pagespeed/kernel/base/cache_interface.h"
   37 #include "pagespeed/kernel/base/callback.h"
   38 #include "pagespeed/kernel/base/message_handler.h"
   39 #include "pagespeed/kernel/base/statistics.h"
   40 #include "pagespeed/kernel/base/string.h"
   41 #include "pagespeed/kernel/base/string_util.h"
   42 #include "pagespeed/kernel/base/string_writer.h"
   43 #include "pagespeed/kernel/base/timer.h"
   44 #include "pagespeed/kernel/cache/purge_context.h"
   45 #include "pagespeed/kernel/html/html_keywords.h"
   46 #include "pagespeed/kernel/http/content_type.h"
   47 #include "pagespeed/kernel/http/google_url.h"
   48 #include "pagespeed/kernel/http/http_names.h"
   49 #include "pagespeed/kernel/http/query_params.h"
   50 #include "pagespeed/kernel/http/request_headers.h"
   51 #include "pagespeed/kernel/http/response_headers.h"
   52 #include "pagespeed/kernel/util/statistics_logger.h"
   53 #include "pagespeed/system/system_cache_path.h"
   54 #include "pagespeed/system/system_caches.h"
   55 #include "pagespeed/system/system_rewrite_options.h"
   56 
   57 namespace net_instaweb {
   58 
   59 // Generated from JS, CSS, and HTML source via net/instaweb/js/data_to_c.cc.
   60 extern const char* CSS_admin_site_css;
   61 extern const char* CSS_caches_css;
   62 extern const char* CSS_console_css;
   63 extern const char* CSS_graphs_css;
   64 extern const char* CSS_statistics_css;
   65 extern const char* JS_caches_js;
   66 extern const char* JS_caches_js_opt;
   67 extern const char* JS_console_js;
   68 extern const char* JS_console_js_opt;
   69 extern const char* JS_graphs_js;
   70 extern const char* JS_graphs_js_opt;
   71 extern const char* JS_messages_js;
   72 extern const char* JS_messages_js_opt;
   73 extern const char* JS_statistics_js;
   74 extern const char* JS_statistics_js_opt;
   75 
   76 namespace {
   77 
   78 struct Tab {
   79   const char* label;
   80   const char* title;
   81   const char* admin_link;       // relative from /pagespeed_admin/
   82   const char* statistics_link;  // relative from /mod_pagespeed_statistics
   83   const char* space;            // html for inter-link spacing.
   84 };
   85 
   86 const char kShortBreak[] = " ";
   87 const char kLongBreak[] = " &nbsp;&nbsp; ";
   88 
   89 // TODO(jmarantz): disable or recolor links to pages that are not available
   90 // based on the current config.
   91 const Tab kTabs[] = {
   92   {"Statistics", "Statistics", "statistics", "?", kLongBreak},
   93   {"Configuration", "Configuration", "config", "?config", kShortBreak},
   94   {"Histograms", "Histograms", "histograms", "?histograms", kLongBreak},
   95   {"Caches", "Caches", "cache", "?cache", kLongBreak},
   96   {"Console", "Console", "console", NULL, kLongBreak},
   97   {"Message History", "Message History", "message_history", NULL, kLongBreak},
   98   {"Graphs", "Graphs", "graphs", NULL, kLongBreak},
   99 };
  100 
  101 // Controls the generation of an HTML Admin page.  Constructing it
  102 // establishes the content-type as HTML and response code 200, and
  103 // puts in a banner with links to all the admin pages, ready for
  104 // appending more <body> elements.  Destructing AdminHtml closes the
  105 // body and completes the fetch.
  106 class AdminHtml {
  107  public:
  108   AdminHtml(StringPiece current_link, StringPiece head_extra,
  109             AdminSite::AdminSource source, Timer* timer,
  110             AsyncFetch* fetch, MessageHandler* handler)
  111       : fetch_(fetch),
  112         handler_(handler) {
  113     fetch->response_headers()->SetStatusAndReason(HttpStatus::kOK);
  114     fetch->response_headers()->Add(HttpAttributes::kContentType, "text/html");
  115 
  116     int64 now_ms = timer->NowMs();
  117     fetch->response_headers()->SetLastModified(now_ms);
  118 
  119     // Let PageSpeed dynamically minify the html, css, and javasript
  120     // generated by the admin page, to the extent it's not done
  121     // already by the tools.  Note, this does mean that viewing the
  122     // statistics and histograms pages will affect the statistics and
  123     // histograms.  If we decide this is too annoying, then we can
  124     // instead procedurally minify the css/js and leave the html
  125     // alone.
  126     //
  127     // Note that we at least turn off add_instrumenation here by explicitly
  128     // giving a filter list without "+" or "-".
  129     fetch->response_headers()->Add(
  130         RewriteQuery::kPageSpeedFilters,
  131         "rewrite_css,rewrite_javascript,collapse_whitespace");
  132 
  133     // Generate some navigational links to help our users get to other
  134     // admin pages.
  135     fetch->Write("<!DOCTYPE html>\n<html><head>", handler_);
  136     fetch->Write(StrCat("<style>", CSS_admin_site_css, "</style>"), handler_);
  137 
  138     GoogleString buf;
  139     for (int i = 0, n = arraysize(kTabs); i < n; ++i) {
  140       const Tab& tab = kTabs[i];
  141       const char* link = NULL;
  142       switch (source) {
  143         case AdminSite::kPageSpeedAdmin:
  144           link = tab.admin_link;
  145           break;
  146         case AdminSite::kStatistics:
  147           link = tab.statistics_link;
  148           break;
  149         case AdminSite::kOther:
  150           link = NULL;
  151           break;
  152       }
  153       if (link != NULL) {
  154         StringPiece style;
  155         if (tab.admin_link == current_link) {
  156           style = " style='color:darkblue;text-decoration:underline;'";
  157           fetch->Write(StrCat("<title>PageSpeed ", tab.title, "</title>"),
  158                        handler_);
  159         }
  160         StrAppend(&buf,
  161                   "<a href='", link, "'", style, ">", tab.label, "</a>",
  162                   tab.space);
  163       }
  164     }
  165 
  166     fetch->Write(StrCat(head_extra, "</head>"), handler_);
  167     fetch->Write(
  168         StrCat("<body class='pagespeed-admin-body'>"
  169                "<div class='pagespeed-admin-tabs'>\n"
  170                "<b>Pagespeed Admin</b>", kLongBreak, "\n"),
  171         handler_);
  172     fetch->Write(buf, handler_);
  173     fetch->Write("</div><hr/>\n", handler_);
  174     fetch->Flush(handler_);
  175   }
  176 
  177   ~AdminHtml() {
  178     fetch_->Write("</body></html>", handler_);
  179     fetch_->Done(true);
  180   }
  181 
  182  private:
  183   AsyncFetch* fetch_;
  184   MessageHandler* handler_;
  185 };
  186 
  187 }  // namespace
  188 
  189 AdminSite::AdminSite(StaticAssetManager* static_asset_manager, Timer* timer,
  190                      MessageHandler* message_handler)
  191     : message_handler_(message_handler),
  192       static_asset_manager_(static_asset_manager),
  193       timer_(timer) {
  194 }
  195 
  196 // Handler which serves PSOL console.
  197 void AdminSite::ConsoleHandler(const SystemRewriteOptions& global_options,
  198                                const RewriteOptions& options,
  199                                AdminSource source,
  200                                const QueryParams& query_params,
  201                                AsyncFetch* fetch, Statistics* statistics) {
  202   if (query_params.Has("json")) {
  203     ConsoleJsonHandler(query_params, fetch, statistics);
  204     return;
  205   }
  206 
  207   MessageHandler* handler = message_handler_;
  208   bool statistics_enabled = global_options.statistics_enabled();
  209   bool logging_enabled = global_options.statistics_logging_enabled();
  210   bool log_dir_set = !global_options.log_dir().empty();
  211 
  212   // TODO(jmarantz): change StaticAssetManager to take options by const ref.
  213   // TODO(sligocki): Move static content to a data2cc library.
  214   StringPiece console_js = options.Enabled(RewriteOptions::kDebug) ?
  215       JS_console_js :
  216       JS_console_js_opt;
  217   // TODO(sligocki): Do we want to have a minified version of console CSS?
  218   GoogleString head_markup = StrCat(
  219       "<style>", CSS_console_css, "</style>\n");
  220   AdminHtml admin_html("console", head_markup, source, timer_, fetch,
  221                        message_handler_);
  222   if (statistics_enabled && logging_enabled && log_dir_set) {
  223     fetch->Write("<div class='console_div' id='suggestions'>\n"
  224                  "  <div class='console_div' id='pagespeed-graphs-container'>"
  225                  "</div>\n</div>\n"
  226                  "<script src='https://www.google.com/jsapi'></script>\n"
  227                  "<script>var pagespeedStatisticsUrl = '';</script>\n"
  228                  "<script>", handler);
  229     // From the admin page, the console JSON is relative, so it can
  230     // be set to ''.  Formerly it was set to options.statistics_handler_path(),
  231     // but there does not appear to be a disadvantage to always handling it
  232     // from whatever URL served this console HTML.
  233     //
  234     // TODO(jmarantz): Change the JS to remove pagespeedStatisticsUrl.
  235     fetch->Write(console_js, handler);
  236     fetch->Write("google.setOnLoadCallback(pagespeed.startConsole);", handler);
  237     fetch->Write("</script>\n", handler);
  238   } else {
  239     fetch->Write("<p>\n"
  240                  "  Failed to load PageSpeed Console because:\n"
  241                  "</p>\n"
  242                  "<ul>\n", handler);
  243     if (!statistics_enabled) {
  244       fetch->Write("  <li>Statistics is not enabled.</li>\n",
  245                     handler);
  246     }
  247     if (!logging_enabled) {
  248       fetch->Write("  <li>StatisticsLogging is not enabled."
  249                     "</li>\n", handler);
  250     }
  251     if (!log_dir_set) {
  252       fetch->Write("  <li>LogDir is not set.</li>\n", handler);
  253     }
  254     fetch->Write("</ul>\n"
  255                   "<p>\n"
  256                   "  In order to use the console you must configure these\n"
  257                   "  options. See the <a href='https://developers.google.com/"
  258                   "speed/pagespeed/module/console'>console documentation</a>\n"
  259                   "  for more details.\n"
  260                   "</p>\n", handler);
  261   }
  262 }
  263 
  264 void AdminSite::StatisticsJsonHandler(AsyncFetch* fetch, Statistics* stats) {
  265   fetch->response_headers()->SetStatusAndReason(HttpStatus::kOK);
  266   fetch->response_headers()->Add(HttpAttributes::kContentType,
  267                                  kContentTypeJson.mime_type());
  268   stats->DumpJson(fetch, message_handler_);
  269   fetch->Done(true);
  270 }
  271 
  272 void AdminSite::StatisticsHandler(const RewriteOptions& options,
  273                                   AdminSource source, AsyncFetch* fetch,
  274                                   Statistics* stats) {
  275   GoogleString head_markup = StrCat(
  276       "<style>", CSS_statistics_css, "</style>\n");
  277   AdminHtml admin_html("statistics", head_markup, source, timer_, fetch,
  278                        message_handler_);
  279   // We have to call Dump() here to write data to sources for
  280   // system/system_test.sh: Line 79. We use JS to update the data in refreshes.
  281   fetch->Write("<pre id='stat'>", message_handler_);
  282   stats->Dump(fetch, message_handler_);
  283   fetch->Write("</pre>\n", message_handler_);
  284   StringPiece statistics_js = options.Enabled(RewriteOptions::kDebug) ?
  285         JS_statistics_js :
  286         JS_statistics_js_opt;
  287   fetch->Write(StrCat("<script type='text/javascript'>", statistics_js,
  288                       "\npagespeed.Statistics.Start();</script>\n"),
  289                message_handler_);
  290 }
  291 
  292 void AdminSite::GraphsHandler(const RewriteOptions& options,
  293                               AdminSource source,
  294                               const QueryParams& query_params,
  295                               AsyncFetch* fetch,
  296                               Statistics* statistics) {
  297   if (query_params.Has("json")) {
  298     ConsoleJsonHandler(query_params, fetch, statistics);
  299     return;
  300   }
  301   GoogleString head_markup = StrCat(
  302       "<style>", CSS_graphs_css, "</style>\n");
  303   AdminHtml admin_html("graphs", head_markup, source, timer_, fetch,
  304                        message_handler_);
  305   fetch->Write("<div id='cache_applied'>Loading Charts...</div>"
  306                "<div id='cache_type'>Loading Charts...</div>"
  307                "<div id='ipro'>Loading Charts...</div>"
  308                "<div id='image_rewriting'>Loading Charts...</div>"
  309                "<div id='realtime'>Loading Charts...</div>",
  310                message_handler_);
  311   fetch->Write("<script type='text/javascript' "
  312                "src='https://www.google.com/jsapi'></script>",
  313                message_handler_);
  314   StringPiece graphs_js = options.Enabled(RewriteOptions::kDebug) ?
  315         JS_graphs_js :
  316         JS_graphs_js_opt;
  317   fetch->Write(StrCat("<script type='text/javascript'>", graphs_js,
  318                       "\npagespeed.Graphs.Start();</script>\n"),
  319                message_handler_);
  320 }
  321 
  322 void AdminSite::ConsoleJsonHandler(const QueryParams& params,
  323                                    AsyncFetch* fetch, Statistics* statistics) {
  324   StatisticsLogger* console_logger = statistics->console_logger();
  325   if (console_logger == NULL) {
  326     fetch->response_headers()->SetStatusAndReason(HttpStatus::kNotFound);
  327     fetch->response_headers()->Add(HttpAttributes::kContentType, "text/plain");
  328     fetch->Write(
  329         "console_logger must be enabled to use '?json' query parameter.",
  330         message_handler_);
  331   } else {
  332     fetch->response_headers()->SetStatusAndReason(HttpStatus::kOK);
  333 
  334     // TODO(morlovich): It would be more secure to do:
  335     //    Content-Type: application/javascript; charset=utf-8
  336     // instead of using a JSON one.  There are probably a few other
  337     // anti-sniffing headers we could add as well.
  338     //
  339     // Also, it would be good to start what we serve with )]}' and a newline,
  340     // and updating the client js, to make completely sure the browser can't be
  341     // tricked into running it.
  342     fetch->response_headers()->Add(HttpAttributes::kContentType,
  343                                    kContentTypeJson.mime_type());
  344 
  345     std::set<GoogleString> var_titles;
  346     // Default is to fetch data used on graphs page.
  347     bool dump_for_graphs = true;
  348 
  349     // Default values for start_time, end_time, and granularity_ms in case the
  350     // query does not include these parameters.
  351     int64 start_time = 0;
  352     int64 end_time = timer_->NowMs();
  353     // Granularity is the difference in ms between data points. If it is not
  354     // specified by the query, the default value is 3000 ms, the same as the
  355     // default logging granularity.
  356     int64 granularity_ms = 3000;
  357     for (int i = 0; i < params.size(); ++i) {
  358       GoogleString value;
  359       if (params.UnescapedValue(i, &value)) {
  360         StringPiece name = params.name(i);
  361         if (name =="start_time") {
  362           StringToInt64(value, &start_time);
  363         } else if (name == "end_time") {
  364           StringToInt64(value, &end_time);
  365         } else if (name == "var_titles") {
  366           // Fetch specified data if users populate var_titles.
  367           dump_for_graphs = false;
  368           std::vector<StringPiece> variable_names;
  369           SplitStringPieceToVector(value, ",", &variable_names, true);
  370           for (size_t i = 0; i < variable_names.size(); ++i) {
  371             var_titles.insert(variable_names[i].as_string());
  372           }
  373         } else if (name == "granularity") {
  374           StringToInt64(value, &granularity_ms);
  375         }
  376       }
  377     }
  378     console_logger->DumpJSON(dump_for_graphs, var_titles, start_time, end_time,
  379                              granularity_ms, fetch, message_handler_);
  380   }
  381   fetch->Done(true);
  382 }
  383 
  384 void AdminSite::PrintHistograms(AdminSource source, AsyncFetch* fetch,
  385                                 Statistics* stats) {
  386   AdminHtml admin_html("histograms", "", source, timer_, fetch,
  387                        message_handler_);
  388   stats->RenderHistograms(fetch, message_handler_);
  389 }
  390 
  391 namespace {
  392 
  393 static const char kTableStart[] =
  394     "<table class='pagespeed-caches-structure'>\n"
  395     "  <thead>\n"
  396     "    <tr>\n"
  397     "      <td>Cache</td><td>Detail</td><td>Structure</td>\n"
  398     "    </tr>\n"
  399     "  </thead>\n"
  400     "  <tbody>";
  401 
  402 static const char kTableEnd[] =
  403     "  </tbody>\n"
  404     "</table>";
  405 
  406 // Takes a complicated descriptor like
  407 //    "HTTPCache(Fallback(small=Batcher(cache=Stats(parefix=memcached_async,"
  408 //    "cache=Async(AprMemCache)),parallelism=1,max=1000),large=Stats("
  409 //    "prefix=file_cache,cache=FileCache)))"
  410 // and strips away the crap most users don't want to see, as they most
  411 // likely did not configure it, and return
  412 //    "Async AprMemCache FileCache"
  413 GoogleString HackCacheDescriptor(StringPiece name) {
  414   GoogleString out;
  415   // There's a lot of complicated syntax in the cache name giving the
  416   // detailed hierarchical structure.  This is really hard to read and
  417   // overly cryptic; it's designed for unit tests.  But let's extract
  418   // a few keywords out of this to understand the main pointers.
  419   static const char* kCacheKeywords[] = {
  420     "Compressed", "Async", "SharedMemCache", "LRUCache", "AprMemCache",
  421     "FileCache", "RedisCache"
  422   };
  423   const char* delim = "";
  424   for (int i = 0, n = arraysize(kCacheKeywords); i < n; ++i) {
  425     if (name.find(kCacheKeywords[i]) != StringPiece::npos) {
  426       StrAppend(&out, delim, kCacheKeywords[i]);
  427       delim = " ";
  428     }
  429   }
  430   if (out.empty()) {
  431     name.CopyToString(&out);
  432   }
  433   return out;
  434 }
  435 
  436 // Takes a complicated descriptor like
  437 //    "HTTPCache(Fallback(small=Batcher(cache=Stats(prefix=memcached_async,"
  438 //    "cache=Async(AprMemCache)),parallelism=1,max=1000),large=Stats("
  439 //    "prefix=file_cache,cache=FileCache)))"
  440 // and injects HTML line-breaks and indentation based on the parent depth,
  441 // yielding HTML that renders like this (with &nbsp; and <br/>)
  442 //    HTTPCache(
  443 //       Fallback(
  444 //          small=Batcher(
  445 //             cache=Stats(
  446 //                prefix=memcached_async,
  447 //                cache=Async(
  448 //                   AprMemCache)),
  449 //             parallelism=1,
  450 //             max=1000),
  451 //          large=Stats(
  452 //             prefix=file_cache,
  453 //             cache=FileCache)))
  454 GoogleString IndentCacheDescriptor(StringPiece name) {
  455   GoogleString out, buf;
  456   int depth = 0;
  457   for (int i = 0, n = name.size(); i < n; ++i) {
  458     StrAppend(&out, HtmlKeywords::Escape(name.substr(i, 1), &buf));
  459     switch (name[i]) {
  460       case '(':
  461         ++depth;
  462         FALLTHROUGH_INTENDED;
  463       case ',':
  464         out += "<br/>";
  465         for (int j = 0; j < depth; ++j) {
  466           out += "&nbsp; &nbsp;";
  467         }
  468         break;
  469       case ')':
  470         --depth;
  471         break;
  472     }
  473   }
  474   return out;
  475 }
  476 
  477 GoogleString CacheInfoHtmlSnippet(StringPiece label, StringPiece descriptor) {
  478   GoogleString out, escaped;
  479   StrAppend(&out, "<tr style='vertical-align:top;'><td>", label,
  480             "</td><td><input id='", label);
  481   StrAppend(&out, "_toggle' type='checkbox' ",
  482             "onclick=\"pagespeed.Caches.toggleDetail('", label,
  483             "')\"/></td><td><code id='", label, "_summary'>");
  484   StrAppend(&out, HtmlKeywords::Escape(HackCacheDescriptor(descriptor),
  485                                        &escaped));
  486   StrAppend(&out, "</code><code id='", label,
  487             "_detail' style='display:none;'>");
  488   StrAppend(&out, IndentCacheDescriptor(descriptor));
  489   StrAppend(&out, "</code></td></tr>\n");
  490   return out;
  491 }
  492 
  493 }  // namespace
  494 
  495 void AdminSite::PrintCaches(bool is_global, AdminSource source,
  496                             const GoogleUrl& stripped_gurl,
  497                             const QueryParams& query_params,
  498                             const RewriteOptions* options,
  499                             SystemCachePath* cache_path,
  500                             AsyncFetch* fetch, SystemCaches* system_caches,
  501                             CacheInterface* filesystem_metadata_cache,
  502                             HTTPCache* http_cache,
  503                             CacheInterface* metadata_cache,
  504                             PropertyCache* page_property_cache,
  505                             ServerContext* server_context) {
  506   GoogleString url;
  507   if ((source == kPageSpeedAdmin) &&
  508       query_params.Lookup1Unescaped("url", &url)) {
  509     // Delegate to ShowCacheHandler to get the cached value for that
  510     // URL, which it may do asynchronously, so we cannot use the
  511     // AdminHtml abstraction which closes the connection in its
  512     // destructor.
  513     GoogleString json_dummy;
  514     ServerContext::Format format = ServerContext::kFormatAsHtml;
  515     if (query_params.Lookup1Unescaped("json", &json_dummy)) {
  516       format = ServerContext::kFormatAsJson;
  517     }
  518     GoogleString ua;
  519     query_params.Lookup1Unescaped("user_agent", &ua);
  520     server_context->ShowCacheHandler(
  521         format, url, ua,  query_params.Has("Delete"), fetch, options->Clone());
  522   } else if ((source == kPageSpeedAdmin) &&
  523              query_params.Lookup1Unescaped("new_set", &url)) {
  524     ResponseHeaders* response_headers = fetch->response_headers();
  525     response_headers->SetStatusAndReason(HttpStatus::kOK);
  526     response_headers->Add(HttpAttributes::kContentType, "text/html");
  527     fetch->Write(options->PurgeSetString(), message_handler_);
  528     fetch->Done(true);
  529   } else if ((source == kPageSpeedAdmin) &&
  530              query_params.Lookup1Unescaped("purge", &url)) {
  531     ResponseHeaders* response_headers = fetch->response_headers();
  532     if (!options->enable_cache_purge()) {
  533       response_headers->SetStatusAndReason(HttpStatus::kOK);
  534       response_headers->Add(HttpAttributes::kContentType, "text/html");
  535       // TODO(jmarantz): virtualize the formatting of this message so that
  536       // it's correct in ngx_pagespeed and mod_pagespeed (and IISpeed etc).
  537       fetch->Write("Purging not enabled: please add\n", message_handler_);
  538       HtmlKeywords::WritePre(
  539           StrCat("  ", server_context->FormatOption("EnableCachePurge", "on")),
  540           "", fetch, message_handler_);
  541       fetch->Write(
  542           "\nto your configuration file. See the \n"
  543           "<a href='https://developers.google.com/speed/pagespeed/module"
  544           "/system#purge_cache'>documentation</a> "
  545           "for more information about cache purging.",
  546           message_handler_);
  547       fetch->Done(true);
  548     } else if (url == "*") {
  549       PurgeHandler(url, cache_path, fetch);
  550     } else if (url.empty()) {
  551       response_headers->SetStatusAndReason(HttpStatus::kOK);
  552       response_headers->Add(HttpAttributes::kContentType, "text/html");
  553       fetch->Write("Empty URL", message_handler_);
  554       fetch->Done(true);
  555     } else {
  556       GoogleUrl origin(stripped_gurl.Origin());
  557       GoogleUrl resolved(origin, url);
  558       if (!resolved.IsWebValid()) {
  559         response_headers->SetStatusAndReason(HttpStatus::kOK);
  560         response_headers->Add(HttpAttributes::kContentType, "text/html");
  561         GoogleString escaped_url;
  562         HtmlKeywords::Escape(url, &escaped_url);
  563         fetch->Write(StrCat("Invalid URL: ", escaped_url), message_handler_);
  564         fetch->Done(true);
  565       } else {
  566         PurgeHandler(resolved.Spec(), cache_path, fetch);
  567       }
  568     }
  569   } else {
  570     GoogleString head_markup = StrCat(
  571         "<style>", CSS_caches_css, "</style>\n");
  572     AdminHtml admin_html("cache", head_markup, source, timer_, fetch,
  573                          message_handler_);
  574 
  575     fetch->Write("<div id='show_metadata'>", message_handler_);
  576     // Present a small form to enter a URL.
  577     if (source == kPageSpeedAdmin) {
  578       const char* user_agent = fetch->request_headers()->Lookup1(
  579           HttpAttributes::kUserAgent);
  580       fetch->Write(ServerContext::ShowCacheForm(user_agent), message_handler_);
  581     }
  582     fetch->Write("</div>\n", message_handler_);
  583     // Display configured cache information.
  584     if (system_caches != NULL) {
  585       int flags = SystemCaches::kDefaultStatFlags;
  586       if (is_global) {
  587         flags |= SystemCaches::kGlobalView;
  588       }
  589 
  590       // TODO(jmarantz): Consider whether it makes sense to disable
  591       // either of these flags to limit the content when someone asks
  592       // for info about the cache.
  593       flags |= SystemCaches::kIncludeMemcached;
  594       flags |= SystemCaches::kIncludeRedis;
  595       fetch->Write("<div id='cache_struct'>",
  596                    message_handler_);
  597       fetch->Write(kTableStart, message_handler_);
  598       CacheInterface* fsmdc = filesystem_metadata_cache;
  599       fetch->Write(StrCat(
  600           CacheInfoHtmlSnippet("HTTP Cache", http_cache->Name()),
  601           CacheInfoHtmlSnippet("Metadata Cache", metadata_cache->Name()),
  602           CacheInfoHtmlSnippet("Property Cache",
  603                                page_property_cache->property_store()->Name()),
  604           CacheInfoHtmlSnippet("FileSystem Metadata Cache",
  605                                (fsmdc == NULL) ? "none" : fsmdc->Name())),
  606                    message_handler_);
  607       fetch->Write(kTableEnd, message_handler_);
  608       fetch->Write("</div>", message_handler_);
  609 
  610       fetch->Write("<div id='physical_cache'>",
  611                    message_handler_);
  612       GoogleString backend_stats;
  613       system_caches->PrintCacheStats(
  614           static_cast<SystemCaches::StatFlags>(flags), &backend_stats);
  615       if (!backend_stats.empty()) {
  616         HtmlKeywords::WritePre(backend_stats, "", fetch, message_handler_);
  617       }
  618       fetch->Write("</div>", message_handler_);
  619 
  620       fetch->Write("<div id='purge_cache'>",
  621                    message_handler_);
  622       // Filled in by JS in caches.js: updatePurgeSet().
  623       fetch->Write("<h3>Purge Set</h3>"
  624                    "<div id='purge_global'"
  625                    " class='pagespeed-caches-purge-global'></div>"
  626                    "<div id='purge_table'"
  627                    " class='pagespeed-caches-purge-table'></div>"
  628                    "</div>",  // closes 'purge_cache'.
  629                    message_handler_);
  630     }
  631     StringPiece caches_js = options->Enabled(RewriteOptions::kDebug) ?
  632         JS_caches_js :
  633         JS_caches_js_opt;
  634     // Practice what we preach: put the blocking JS in the tail.
  635     // TODO(jmarantz): use static asset manager to compile & deliver JS
  636     // externally.
  637     fetch->Write(StrCat("<script type='text/javascript'>", caches_js,
  638                         "\npagespeed.Caches.Start();</script>\n"),
  639                  message_handler_);
  640   }
  641 }
  642 
  643 void AdminSite::PrintConfig(
  644     AdminSource source, AsyncFetch* fetch,
  645     SystemRewriteOptions* global_system_rewrite_options) {
  646   AdminHtml admin_html("config", "", source, timer_, fetch, message_handler_);
  647   HtmlKeywords::WritePre(
  648       global_system_rewrite_options->OptionsToString(), "",
  649       fetch, message_handler_);
  650 }
  651 
  652 void AdminSite::MessageHistoryHandler(const RewriteOptions& options,
  653                                       AdminSource source, AsyncFetch* fetch) {
  654   // Request for page /mod_pagespeed_message.
  655   GoogleString log;
  656   StringWriter log_writer(&log);
  657   AdminHtml admin_html("message_history", "", source, timer_, fetch,
  658                        message_handler_);
  659   if (message_handler_->Dump(&log_writer)) {
  660     fetch->Write("<div id='log'>", message_handler_);
  661     // Write pre-tag and color messages.
  662     StringPieceVector messages;
  663     message_handler_->ParseMessageDumpIntoMessages(log, &messages);
  664     for (int i = 0, size = messages.size(); i < size; ++i) {
  665       if (messages[i].length() > 0) {
  666         switch (message_handler_->GetMessageType(messages[i])) {
  667           case kError: {
  668             HtmlKeywords::WritePre(
  669                 message_handler_->ReformatMessage(messages[i]),
  670                 "color:red; margin:0;", fetch, message_handler_);
  671             break;
  672           }
  673           case kWarning: {
  674             HtmlKeywords::WritePre(
  675                 message_handler_->ReformatMessage(messages[i]),
  676                 "color:brown; margin:0;", fetch, message_handler_);
  677             break;
  678           }
  679           case kFatal: {
  680             HtmlKeywords::WritePre(
  681                 message_handler_->ReformatMessage(messages[i]),
  682                 "color:orange; margin:0;", fetch, message_handler_);
  683             break;
  684           }
  685           default: {
  686             HtmlKeywords::WritePre(
  687                 message_handler_->ReformatMessage(messages[i]),
  688                 "margin:0;", fetch, message_handler_);
  689           }
  690         }
  691       }
  692     }
  693     fetch->Write("</div>\n", message_handler_);
  694     StringPiece messages_js = options.Enabled(RewriteOptions::kDebug) ?
  695         JS_messages_js :
  696         JS_messages_js_opt;
  697     fetch->Write(StrCat("<script type='text/javascript'>", messages_js,
  698                         "\npagespeed.Messages.Start();</script>\n"),
  699                  message_handler_);
  700   } else {
  701     fetch->Write("<p>Writing to mod_pagespeed_message failed. \n"
  702                  "Verify that MessageBufferSize is not set to 0 "
  703                  "in pagespeed.conf.</p>\n",
  704                  message_handler_);
  705   }
  706 }
  707 
  708 void AdminSite::AdminPage(
  709     bool is_global, const GoogleUrl& stripped_gurl,
  710     const QueryParams& query_params, const RewriteOptions* options,
  711     SystemCachePath* cache_path, AsyncFetch* fetch, SystemCaches* system_caches,
  712     CacheInterface* filesystem_metadata_cache, HTTPCache* http_cache,
  713     CacheInterface* metadata_cache, PropertyCache* page_property_cache,
  714     ServerContext* server_context, Statistics* statistics, Statistics* stats,
  715     SystemRewriteOptions* global_system_rewrite_options) {
  716   // The handler is "pagespeed_admin", so we must dispatch off of
  717   // the remainder of the URL.  For
  718   // "http://example.com/pagespeed_admin/foo?a=b" we want to pull out
  719   // "foo".
  720   //
  721   // Note that the comments here referring to "/pagespeed_admin" reflect
  722   // only the default admin path in Apache for fresh installs.  In fact
  723   // we can put the handler on any path, and this code should still work;
  724   // all the paths here are specified relative to the incoming URL.
  725   StringPiece path = stripped_gurl.PathSansQuery();   // "/pagespeed_admin/foo"
  726   path = path.substr(1);                              // "pagespeed_admin/foo"
  727 
  728   // If there are no slashes at all in the path, e.g. it's "pagespeed_admin",
  729   // then the relative references to "config" etc will not work.  We need
  730   // to serve the admin pages on "/pagespeed_admin/".  So if we got to this
  731   // point and there are no slashes, then we can just redirect immediately
  732   // by adding a slash.
  733   //
  734   // If the user has mapped the pagespeed_admin handler to a path with
  735   // an embedded slash, say "pagespeed/myadmin", then it's hard to tell
  736   // whether we should redirect, because we don't know what the the
  737   // intended path is.  In this case, we'll fall through to a leaf
  738   // analysis on "myadmin", fail to find a match, and print a "Did You Mean"
  739   // page.  It's not as good as a redirect but since we can't tell an
  740   // omitted slash from a typo it's the best we can do.
  741   if (path.find('/') == StringPiece::npos) {
  742     // If the URL is "/pagespeed_admin", then redirect to "/pagespeed_admin/" so
  743     // that relative URL references will work.
  744     ResponseHeaders* response_headers = fetch->response_headers();
  745     response_headers->SetStatusAndReason(HttpStatus::kMovedPermanently);
  746     GoogleString admin_with_slash = StrCat(stripped_gurl.AllExceptQuery(), "/");
  747     response_headers->Add(HttpAttributes::kLocation, admin_with_slash);
  748     response_headers->Add(HttpAttributes::kContentType, "text/html");
  749     GoogleString escaped_url;
  750     HtmlKeywords::Escape(admin_with_slash, &escaped_url);
  751     fetch->Write(StrCat("Redirecting to URL ", escaped_url), message_handler_);
  752     fetch->Done(true);
  753   } else {
  754     StringPiece leaf = stripped_gurl.LeafSansQuery();
  755     if ((leaf == "statistics") || (leaf.empty())) {
  756       StatisticsHandler(*options, kPageSpeedAdmin, fetch, stats);
  757     } else if (leaf == "stats_json") {
  758       StatisticsJsonHandler(fetch, stats);
  759     } else if (leaf == "graphs") {
  760       GraphsHandler(*options, kPageSpeedAdmin, query_params, fetch, statistics);
  761     } else if (leaf == "config") {
  762       PrintConfig(kPageSpeedAdmin, fetch, global_system_rewrite_options);
  763     } else if (leaf == "console") {
  764       // TODO(jmarantz): add vhost-local and aggregate message buffers.
  765       ConsoleHandler(*global_system_rewrite_options, *options, kPageSpeedAdmin,
  766                      query_params, fetch, statistics);
  767     } else if (leaf == "message_history") {
  768       MessageHistoryHandler(*options, kPageSpeedAdmin, fetch);
  769     } else if (leaf == "cache") {
  770       PrintCaches(is_global, kPageSpeedAdmin, stripped_gurl, query_params,
  771                   options, cache_path, fetch, system_caches,
  772                   filesystem_metadata_cache, http_cache, metadata_cache,
  773                   page_property_cache, server_context);
  774     } else if (leaf == "histograms") {
  775       PrintHistograms(kPageSpeedAdmin, fetch, stats);
  776     } else {
  777       fetch->response_headers()->SetStatusAndReason(HttpStatus::kNotFound);
  778       fetch->response_headers()->Add(HttpAttributes::kContentType, "text/html");
  779       fetch->Write("Unknown admin page: ", message_handler_);
  780       HtmlKeywords::WritePre(leaf, "", fetch, message_handler_);
  781 
  782       // It's possible that the handler is installed on /a/b/c, and we
  783       // are now reporting "unknown admin page: c".  This is kind of a guess,
  784       // but provide a nice link here to what might be the correct admin page.
  785       //
  786       // This is just a guess, so we don't want to redirect.
  787       fetch->Write("<br/>Did you mean to visit: ", message_handler_);
  788       GoogleString escaped_url;
  789       HtmlKeywords::Escape(StrCat(stripped_gurl.AllExceptQuery(), "/"),
  790                            &escaped_url);
  791       fetch->Write(StrCat("<a href='", escaped_url, "'>", escaped_url,
  792                           "</a>\n"),
  793                    message_handler_);
  794       fetch->Done(true);
  795     }
  796   }
  797 }
  798 
  799 void AdminSite::StatisticsPage(
  800     bool is_global, const QueryParams& query_params,
  801     const RewriteOptions* options, AsyncFetch* fetch,
  802     SystemCaches* system_caches, CacheInterface* filesystem_metadata_cache,
  803     HTTPCache* http_cache, CacheInterface* metadata_cache,
  804     PropertyCache* page_property_cache, ServerContext* server_context,
  805     Statistics* statistics, Statistics* stats,
  806     SystemRewriteOptions* global_system_rewrite_options) {
  807   if (query_params.Has("json")) {
  808     ConsoleJsonHandler(query_params, fetch, statistics);
  809   } else if (query_params.Has("config")) {
  810     PrintConfig(kStatistics, fetch, global_system_rewrite_options);
  811   } else if (query_params.Has("histograms")) {
  812     PrintHistograms(kStatistics, fetch, stats);
  813   } else if (query_params.Has("graphs")) {
  814     GraphsHandler(*options, kStatistics, query_params, fetch, statistics);
  815   } else if (query_params.Has("cache")) {
  816     GoogleUrl empty_url;
  817     PrintCaches(is_global, kStatistics, empty_url, query_params,
  818                 options, NULL,  // cache_path is reference from statistics page.
  819                 fetch, system_caches, filesystem_metadata_cache,
  820                 http_cache, metadata_cache, page_property_cache,
  821                 server_context);
  822   } else {
  823     StatisticsHandler(*options, kStatistics, fetch, stats);
  824   }
  825 }
  826 
  827 namespace {
  828 
  829 // Provides a Done(bool, StringPiece) entry point for use as a Purge
  830 // callback. Translates the success into an Http status code for the
  831 // AsyncFetch, sending any failure reason in the response body.
  832 class PurgeFetchCallbackGasket {
  833  public:
  834   PurgeFetchCallbackGasket(AsyncFetch* fetch, MessageHandler* handler)
  835       : fetch_(fetch),
  836         message_handler_(handler) {
  837   }
  838   void Done(bool success, StringPiece reason) {
  839     ResponseHeaders* headers = fetch_->response_headers();
  840     headers->set_status_code(HttpStatus::kOK);
  841     headers->Add(HttpAttributes::kContentType, "text/html");
  842     // TODO(xqyin): Currently we may still return 'purge successful' even if
  843     // the URL does not exist in our cache. Figure out how to solve this case
  844     // while we don't want to search the whole cache which could be very large.
  845     if (success) {
  846       fetch_->Write("Purge successful", message_handler_);
  847     } else {
  848       GoogleString buf;
  849       fetch_->Write(HtmlKeywords::Escape(reason, &buf), message_handler_);
  850       fetch_->Write("\n", message_handler_);
  851       fetch_->Write(HtmlKeywords::Escape(error_, &buf), message_handler_);
  852     }
  853     fetch_->Done(true);
  854     delete this;
  855   }
  856 
  857   void set_error(StringPiece x) { x.CopyToString(&error_); }
  858 
  859  private:
  860   AsyncFetch* fetch_;
  861   MessageHandler* message_handler_;
  862   GoogleString error_;
  863 
  864   DISALLOW_COPY_AND_ASSIGN(PurgeFetchCallbackGasket);
  865 };
  866 
  867 }  // namespace
  868 
  869 void AdminSite::PurgeHandler(StringPiece url, SystemCachePath* cache_path,
  870                              AsyncFetch* fetch) {
  871   PurgeContext* purge_context = cache_path->purge_context();
  872   int64 now_ms = timer_->NowMs();
  873   PurgeFetchCallbackGasket* gasket =
  874       new PurgeFetchCallbackGasket(fetch, message_handler_);
  875   PurgeContext::PurgeCallback* callback = NewCallback(
  876       gasket, &PurgeFetchCallbackGasket::Done);
  877   if (strings::EndsWith(url, "*")) {
  878     // If the url is "*" we'll just purge everything.  Note that we will
  879     // ignore any sub-paths in the expression.  We can only purge the
  880     // entire cache, or specific URLs, not general wildcards.
  881     purge_context->SetCachePurgeGlobalTimestampMs(now_ms, callback);
  882   } else {
  883     purge_context->AddPurgeUrl(url, now_ms, callback);
  884   }
  885 }
  886 
  887 }  // namespace net_instaweb