blob: baad777039ecf0d9aa173540945a8453ca75102e [file] [log] [blame]
// 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[] = " &nbsp;&nbsp; ";
// 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 &nbsp; 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 += "&nbsp; &nbsp;";
}
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