blob: 07b778bddbdeb27f1910fb88431de63ce7d27dcc [file] [log] [blame]
/*
* Copyright 2014 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.
*/
/**
* @fileoverview Code for 'show caches' and 'purge cache' UI on caches page.
*
* @author xqyin@google.com (XiaoQian Yin)
*/
goog.provide('pagespeed.Caches');
goog.require('goog.events');
goog.require('goog.net.EventType');
goog.require('goog.net.XhrIo');
goog.require('goog.object');
goog.require('goog.string');
/**
* @constructor
* @param {goog.testing.net.XhrIo=} opt_xhr Optional mock XmlHttpRequests
* handler for testing.
*/
pagespeed.Caches = function(opt_xhr) {
/**
* The XmlHttpRequests handler used for cache purging and showing metadata
* cache entry. We pass a mock XhrIo when testing. Default is a normal XhrIo.
* @private {goog.net.XhrIo|goog.testing.net.XhrIo}
*/
this.xhr_ = opt_xhr || new goog.net.XhrIo();
/**
* A string to indicate whether the current input is from metadata tab or
* purge cache tab.
* @private {string}
*/
this.inputIsFrom_ = '';
/**
* The option of whether to sort the entries in Purge Set table in time
* descending order. The default purge set is in time ascending order.
* @private {boolean}
*/
this.sortPurgeSetDescendingTime_ = false;
// The navigation bar to switch among different display modes.
var modeElement = document.createElement('table');
modeElement.id = 'nav-bar';
modeElement.className = 'pagespeed-sub-tabs';
modeElement.innerHTML =
'<tr><td><a id="' +
pagespeed.Caches.DisplayMode.METADATA_CACHE +
'" href="javascript:void(0);">' +
'Show Metadata Cache</a> - </td>' +
'<td><a id="' +
pagespeed.Caches.DisplayMode.CACHE_STRUCTURE +
'" href="javascript:void(0);">' +
'Show Cache Structure</a> - </td>' +
'<td><a id="' +
pagespeed.Caches.DisplayMode.PHYSICAL_CACHE +
'" href="javascript:void(0);">' +
'Physical Caches</a> - </td>' +
'<td><a id="' +
pagespeed.Caches.DisplayMode.PURGE_CACHE +
'" href="javascript:void(0);">' +
'Purge Cache</a></td></tr>';
document.body.insertBefore(
modeElement,
document.getElementById(pagespeed.Caches.DisplayDiv.METADATA_CACHE));
// TODO(xqyin): Maybe use closure tab pane UI element instead.
// Create a div to display the result of showing metadata cache entry.
var metadataResult = document.createElement('pre');
metadataResult.id = pagespeed.Caches.ElementId.METADATA_RESULT;
metadataResult.className = 'pagespeed-caches-result';
var metadataElement = document.getElementById(
pagespeed.Caches.DisplayDiv.METADATA_CACHE);
metadataElement.appendChild(metadataResult);
// Create a div to display the result of cache purging.
var purgeResult = document.createElement('div');
purgeResult.id = pagespeed.Caches.ElementId.PURGE_RESULT;
purgeResult.className = 'pagespeed-caches-result';
var purgeElement = document.getElementById(
pagespeed.Caches.DisplayDiv.PURGE_CACHE);
purgeElement.insertBefore(purgeResult, purgeElement.firstChild);
};
/**
* Display the detailed structure of selected cache.
* @param {string} id The id of the div element to display.
* @export
*/
pagespeed.Caches.toggleDetail = function(id) {
var toggle_button = document.getElementById(id + '_toggle');
var summary_div = document.getElementById(id + '_summary');
var detail_div = document.getElementById(id + '_detail');
if (toggle_button.checked) {
summary_div.style.display = 'none';
detail_div.style.display = 'block';
} else {
summary_div.style.display = 'block';
detail_div.style.display = 'none';
}
};
/**
* Enum for display mode values. These are the ids of the links on the
* navigation bar. Users click the link and call show(div) method to display
* the corresponding div element.
* @enum {string}
*/
pagespeed.Caches.DisplayMode = {
METADATA_CACHE: 'show_metadata_mode',
CACHE_STRUCTURE: 'cache_struct_mode',
PHYSICAL_CACHE: 'physical_cache_mode',
PURGE_CACHE: 'purge_cache_mode'
};
/**
* Enum for display div values. These are the ids of the div elements that
* contain actual content. Only the chosen div element will be displayed on
* the page. Others will be hidden.
* @enum {string}
*/
pagespeed.Caches.DisplayDiv = {
METADATA_CACHE: 'show_metadata',
CACHE_STRUCTURE: 'cache_struct',
PHYSICAL_CACHE: 'physical_cache',
PURGE_CACHE: 'purge_cache'
};
/**
* Enum for ids of elements that will be used in cache purging and showing
* metadata cache.
*/
pagespeed.Caches.ElementId = {
METADATA_TEXT: 'metadata_text',
METADATA_SUBMIT: 'metadata_submit',
METADATA_RESULT: 'metadata_result',
METADATA_CLEAR: 'metadata_clear',
PURGE_TEXT: 'purge_text',
PURGE_SUBMIT: 'purge_submit',
PURGE_RESULT: 'purge_result',
PURGE_ALL: 'purge_all',
PURGE_GLOBAL: 'purge_global',
PURGE_TABLE: 'purge_table',
USER_AGENT: 'user_agent',
SORT: 'sort'
};
/**
* The info sent by server indicating the successful cache purging.
* @private {string}
* @const
*/
pagespeed.Caches.PURGE_SUCCESS_ = 'Purge successful';
/**
* The info sent by server when a purge failure is due to the option
* not being enabled in the config file.
* @private {string}
* @const
*/
pagespeed.Caches.PURGE_NOT_ENABLED_ = 'Purging not enabled';
/**
* Parse the location URL to get its anchor part and show the div element.
*/
pagespeed.Caches.prototype.parseLocation = function() {
var div = location.hash.substr(1);
if (div == '') {
this.show(pagespeed.Caches.DisplayDiv.METADATA_CACHE);
} else if (goog.object.contains(pagespeed.Caches.DisplayDiv, div)) {
this.show(div);
}
};
/**
* Show the corresponding div element of chosen display mode.
* @param {string} div The div element to display according to the current
* display mode.
*/
pagespeed.Caches.prototype.show = function(div) {
// Only shows the div chosen by users.
for (var i in pagespeed.Caches.DisplayDiv) {
var chartDiv = pagespeed.Caches.DisplayDiv[i];
if (chartDiv == div) {
document.getElementById(chartDiv).className = '';
} else {
document.getElementById(chartDiv).className =
'pagespeed-hidden-offscreen';
}
}
// Underline the current tab.
var currentTab = document.getElementById(div + '_mode');
for (var i in pagespeed.Caches.DisplayMode) {
var link = document.getElementById(pagespeed.Caches.DisplayMode[i]);
if (link == currentTab) {
link.className = 'pagespeed-underline-link';
} else {
link.className = '';
}
}
location.href = location.href.split('#')[0] + '#' + div;
};
/**
* Extracts text packaged as JSON (in a value: field) with a prepended
* XSSI guard.
* @param {string} responseText Raw response.
* @return {string} Decoded value.
* @private
*/
pagespeed.Caches.decodeFromJson_ = function(responseText) {
// 4 = length of the )]}\n XSSI guard.
return JSON.parse(responseText.substring(4)).value;
};
/**
* Encode special characters and remove whitespace from both sides of the url.
* @param {string} url The url from the input form.
* @return {string} The escaped url with whitespace removed.
*/
pagespeed.Caches.prototype.escapedUrl = function(url) {
return encodeURIComponent(url.trim());
};
/**
* Send the cache purging URL to the sever.
*/
pagespeed.Caches.prototype.sendPurgeRequest = function() {
if (!this.xhr_.isActive()) {
var urlId = pagespeed.Caches.ElementId.PURGE_TEXT;
var escapedText = this.escapedUrl(document.getElementById(urlId).value);
if (escapedText == '*') {
this.inputIsFrom_ = pagespeed.Caches.ElementId.PURGE_ALL;
} else {
this.inputIsFrom_ = pagespeed.Caches.ElementId.PURGE_TEXT;
}
var url = '?purge=' + escapedText;
this.xhr_.send(url);
}
};
/**
* Send '*' to server to purge the entire cache.
*/
pagespeed.Caches.prototype.sendPurgeAllRequest = function() {
if (!this.xhr_.isActive()) {
var url = '?purge=*';
this.inputIsFrom_ = pagespeed.Caches.ElementId.PURGE_ALL;
this.xhr_.send(url);
}
};
/**
* Send request to get the updated purge set.
*/
pagespeed.Caches.prototype.sendPurgeSetRequest = function() {
if (!this.xhr_.isActive()) {
var url = '?new_set=';
this.inputIsFrom_ = pagespeed.Caches.ElementId.PURGE_TABLE;
this.xhr_.send(url);
}
};
/**
* Send the cache purging URL to the sever.
* @param {Event} event The DOM event that activated this.
*/
pagespeed.Caches.prototype.sendMetadataRequest = function(event) {
if (!this.xhr_.isActive()) {
event.preventDefault();
var urlId = pagespeed.Caches.ElementId.METADATA_TEXT;
var userAgentId = pagespeed.Caches.ElementId.USER_AGENT;
var url =
'?url=' +
this.escapedUrl(document.getElementById(urlId).value) +
'&user_agent=' +
this.escapedUrl(document.getElementById(userAgentId).value) +
'&json=1';
this.inputIsFrom_ = pagespeed.Caches.ElementId.METADATA_RESULT;
this.xhr_.send(url);
}
};
/**
* Updates the option of sorting the purge set in time descending order.
*/
pagespeed.Caches.prototype.toggleSorting = function() {
this.sortPurgeSetDescendingTime_ = !this.sortPurgeSetDescendingTime_;
this.sendPurgeSetRequest();
};
/**
* Parse the string sent by server and update the Purge Set table.
* @param {string} text The updated Purge Set in string format sent by server.
*/
pagespeed.Caches.prototype.updatePurgeSet = function(text) {
var messages = text.split('\n');
var globalTimeVal = messages.shift();
var globalTime =
document.getElementById(pagespeed.Caches.ElementId.PURGE_GLOBAL);
globalTime.textContent = 'Everything before this time stamp is invalid: ' +
globalTimeVal.split('@')[1];
var tableDiv = document.getElementById(
pagespeed.Caches.ElementId.PURGE_TABLE);
tableDiv.innerHTML = ''; // Clears any old version of table.
if (messages.length > 0) {
tableDiv.appendChild(document.createElement('hr'));
var table = document.createElement('table');
if (this.sortPurgeSetDescendingTime_) {
messages.reverse();
}
for (var i = 0; i < messages.length; ++i) {
// Decode the results of PurgeSet::ToString in
// pagespeed/kernel/cache/purge_set.cc
var index = messages[i].lastIndexOf('@');
var url = messages[i].substring(0, index);
var time = messages[i].substring(index + 1);
var tableRow = table.insertRow(-1);
tableRow.insertCell(0).textContent = time;
var code = document.createElement('code');
code.className = 'pagespeed-caches-purge-url';
code.textContent = url;
var codeCell = tableRow.insertCell(1);
codeCell.appendChild(code);
}
var header = table.createTHead().insertRow(0);
var cell = header.insertCell(0);
cell.className = 'pagespeed-caches-date-column';
if (messages.length == 1) {
cell.textContent = 'Invalidation Time';
} else {
var checkBox = document.createElement('input');
checkBox.setAttribute('type', 'checkbox');
checkBox.id = pagespeed.Caches.ElementId.SORT;
checkBox.checked = this.sortPurgeSetDescendingTime_ ? true : false;
checkBox.title = 'Change sort order.';
if (this.sortPurgeSetDescendingTime_) {
cell.textContent = 'Invalidation Time (Descending)';
} else {
cell.textContent = 'Invalidation Time (Ascending)';
}
cell.appendChild(checkBox);
goog.events.listen(checkBox, 'change',
goog.bind(this.toggleSorting, this));
}
cell = header.insertCell(1);
cell.textContent = 'URL';
cell.className = 'pagespeed-stats-url-column';
tableDiv.appendChild(table);
}
};
/**
* Display the result of cache purging and showing metadata cache entry.
*/
pagespeed.Caches.prototype.showResult = function() {
if (this.xhr_.isSuccess()) {
var text = this.xhr_.getResponseText();
if (this.inputIsFrom_ == pagespeed.Caches.ElementId.METADATA_RESULT) {
text = pagespeed.Caches.decodeFromJson_(text);
document.getElementById(this.inputIsFrom_).textContent = text;
} else if (this.inputIsFrom_ == pagespeed.Caches.ElementId.PURGE_TABLE) {
this.updatePurgeSet(text);
} else {
// Add updating purge set to the queue if the current request is
// to purge cache.
window.setTimeout(goog.bind(this.sendPurgeSetRequest, this), 0);
var id = pagespeed.Caches.ElementId.PURGE_RESULT;
var purgeResult = document.getElementById(id);
// TODO(jmarantz): Split the 'purge entire cache' case off in C++ instead
// of doing the check here.
if (text == pagespeed.Caches.PURGE_SUCCESS_ &&
this.inputIsFrom_ == pagespeed.Caches.ElementId.PURGE_TEXT) {
purgeResult.textContent = 'Added to Purge Set';
} else if (text.indexOf(pagespeed.Caches.PURGE_NOT_ENABLED_) != -1) {
// The error response is rich HTML, which should be properly
// escaped, XSS free, and not affected by user input, per code in
// admin_site.cc, AdminSite::PrintCaches().
purgeResult.innerHTML = text;
} else {
purgeResult.textContent = text;
}
}
} else {
console.log(this.xhr_.getLastError());
}
};
/**
* Present a form to enter a URL for cache purging.
*/
pagespeed.Caches.prototype.purgeInit = function() {
// It's actually not a 'form' element because we add listeners to the
// buttons to submit query params. We don't want users to submit the
// form by pressing enter without clicking the buttons.
var purgeUI = document.createElement('table');
purgeUI.innerHTML =
'URL: <input id="' + pagespeed.Caches.ElementId.PURGE_TEXT +
'" type="text" name="purge" size="110"/><br>' +
'<input id="' + pagespeed.Caches.ElementId.PURGE_SUBMIT +
'" type="button" value="Purge Individual URL"/>' +
'<input id="' + pagespeed.Caches.ElementId.PURGE_ALL +
'" type="button" value="Purge Entire Cache"/>';
var purgeElement = document.getElementById(
pagespeed.Caches.DisplayDiv.PURGE_CACHE);
purgeElement.insertBefore(purgeUI, purgeElement.firstChild);
};
/**
* The Main entry to start processing.
* @export
*/
pagespeed.Caches.Start = function() {
var cachesOnload = function() {
var cachesObj = new pagespeed.Caches();
cachesObj.purgeInit();
cachesObj.parseLocation();
for (var i in pagespeed.Caches.DisplayDiv) {
goog.events.listen(
document.getElementById(pagespeed.Caches.DisplayMode[i]), 'click',
goog.bind(cachesObj.show, cachesObj, pagespeed.Caches.DisplayDiv[i]));
}
// IE6/7 don't support this event. Then the back button would not work
// because no requests captured.
goog.events.listen(window, 'hashchange',
goog.bind(cachesObj.parseLocation, cachesObj));
goog.events.listen(
document.getElementById(pagespeed.Caches.ElementId.PURGE_SUBMIT),
'click', goog.bind(cachesObj.sendPurgeRequest, cachesObj));
goog.events.listen(
document.getElementById(pagespeed.Caches.ElementId.PURGE_ALL),
'click', goog.bind(cachesObj.sendPurgeAllRequest, cachesObj));
goog.events.listen(
document.getElementById(pagespeed.Caches.ElementId.METADATA_SUBMIT),
'click', goog.bind(cachesObj.sendMetadataRequest, cachesObj));
goog.events.listen(
cachesObj.xhr_, goog.net.EventType.COMPLETE,
goog.bind(cachesObj.showResult, cachesObj));
goog.events.listen(
document.getElementById(pagespeed.Caches.ElementId.METADATA_CLEAR),
'click', goog.bind(location.reload, location));
cachesObj.sendPurgeSetRequest();
};
goog.events.listen(window, 'load', cachesOnload);
};