Fixing duplicated cached content when using HttpModule with specific envent

handlers

--HG--
branch : 1.x
This commit is contained in:
Sebastien Ros
2013-07-09 17:17:44 -07:00
parent ea5fc5f13e
commit 8f1a0d343a
2 changed files with 79 additions and 138 deletions

View File

@@ -11,7 +11,6 @@ using System.Web.Mvc;
using System.Web.Routing; using System.Web.Routing;
using Orchard.OutputCache.Models; using Orchard.OutputCache.Models;
using Orchard.OutputCache.Services; using Orchard.OutputCache.Services;
using Orchard;
using Orchard.Caching; using Orchard.Caching;
using Orchard.ContentManagement; using Orchard.ContentManagement;
using Orchard.Environment.Configuration; using Orchard.Environment.Configuration;
@@ -22,10 +21,8 @@ using Orchard.Themes;
using Orchard.UI.Admin; using Orchard.UI.Admin;
using Orchard.Utility.Extensions; using Orchard.Utility.Extensions;
namespace Orchard.OutputCache.Filters namespace Orchard.OutputCache.Filters {
{ public class OutputCacheFilter : FilterProvider, IActionFilter, IResultFilter {
public class OutputCacheFilter : FilterProvider, IActionFilter, IResultFilter
{
private readonly ICacheManager _cacheManager; private readonly ICacheManager _cacheManager;
private readonly IOutputCacheStorageProvider _cacheStorageProvider; private readonly IOutputCacheStorageProvider _cacheStorageProvider;
@@ -38,8 +35,6 @@ namespace Orchard.OutputCache.Filters
private readonly ISignals _signals; private readonly ISignals _signals;
private readonly ShellSettings _shellSettings; private readonly ShellSettings _shellSettings;
private const string AntiforgeryBeacon = "<!--OutputCacheFilterAntiForgeryToken-->";
private const string AntiforgeryTag = "<input name=\"__RequestVerificationToken\" type=\"hidden\" value=\"";
private const string RefreshKey = "__r"; private const string RefreshKey = "__r";
public OutputCacheFilter( public OutputCacheFilter(
@@ -52,8 +47,7 @@ namespace Orchard.OutputCache.Filters
IClock clock, IClock clock,
ICacheService cacheService, ICacheService cacheService,
ISignals signals, ISignals signals,
ShellSettings shellSettings) ShellSettings shellSettings) {
{
_cacheManager = cacheManager; _cacheManager = cacheManager;
_cacheStorageProvider = cacheStorageProvider; _cacheStorageProvider = cacheStorageProvider;
_tagCache = tagCache; _tagCache = tagCache;
@@ -87,15 +81,14 @@ namespace Orchard.OutputCache.Filters
public ILogger Logger { get; set; } public ILogger Logger { get; set; }
public void OnActionExecuting(ActionExecutingContext filterContext) public void OnActionExecuting(ActionExecutingContext filterContext) {
{
// use the action in the cacheKey so that the same route can't return cache for different actions // use the action in the cacheKey so that the same route can't return cache for different actions
_actionName = filterContext.ActionDescriptor.ActionName; _actionName = filterContext.ActionDescriptor.ActionName;
// apply OutputCacheAttribute logic if defined // apply OutputCacheAttribute logic if defined
var outputCacheAttribute = filterContext.ActionDescriptor.GetCustomAttributes(typeof (OutputCacheAttribute), true).Cast<OutputCacheAttribute>().FirstOrDefault() ; var outputCacheAttribute = filterContext.ActionDescriptor.GetCustomAttributes(typeof(OutputCacheAttribute), true).Cast<OutputCacheAttribute>().FirstOrDefault();
if(outputCacheAttribute != null) { if (outputCacheAttribute != null) {
if (outputCacheAttribute.Duration <= 0 || outputCacheAttribute.NoStore) { if (outputCacheAttribute.Duration <= 0 || outputCacheAttribute.NoStore) {
Logger.Debug("Request ignored based on OutputCache attribute"); Logger.Debug("Request ignored based on OutputCache attribute");
return; return;
@@ -111,7 +104,7 @@ namespace Orchard.OutputCache.Filters
Logger.Debug("Request on: " + filterContext.RequestContext.HttpContext.Request.RawUrl); Logger.Debug("Request on: " + filterContext.RequestContext.HttpContext.Request.RawUrl);
// don't cache POST requests // don't cache POST requests
if(filterContext.HttpContext.Request.HttpMethod.Equals("POST", StringComparison.OrdinalIgnoreCase) ) { if (filterContext.HttpContext.Request.HttpMethod.Equals("POST", StringComparison.OrdinalIgnoreCase)) {
Logger.Debug("Request ignored on POST"); Logger.Debug("Request ignored on POST");
return; return;
} }
@@ -123,7 +116,7 @@ namespace Orchard.OutputCache.Filters
} }
// ignore child actions, e.g. HomeController is using RenderAction() // ignore child actions, e.g. HomeController is using RenderAction()
if (filterContext.IsChildAction){ if (filterContext.IsChildAction) {
Logger.Debug("Request ignored on Child actions"); Logger.Debug("Request ignored on Child actions");
return; return;
} }
@@ -156,9 +149,9 @@ namespace Orchard.OutputCache.Filters
context => { context => {
context.Monitor(_signals.When(CacheSettingsPart.CacheKey)); context.Monitor(_signals.When(CacheSettingsPart.CacheKey));
var varyQueryStringParameters = _workContext.CurrentSite.As<CacheSettingsPart>().VaryQueryStringParameters; var varyQueryStringParameters = _workContext.CurrentSite.As<CacheSettingsPart>().VaryQueryStringParameters;
return string.IsNullOrWhiteSpace(varyQueryStringParameters) ? null return string.IsNullOrWhiteSpace(varyQueryStringParameters) ? null
: varyQueryStringParameters.Split(new[]{","}, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToArray(); : varyQueryStringParameters.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToArray();
} }
); );
@@ -189,7 +182,7 @@ namespace Orchard.OutputCache.Filters
var queryString = filterContext.RequestContext.HttpContext.Request.QueryString; var queryString = filterContext.RequestContext.HttpContext.Request.QueryString;
var parameters = new Dictionary<string, object>(filterContext.ActionParameters); var parameters = new Dictionary<string, object>(filterContext.ActionParameters);
foreach(var key in queryString.AllKeys) { foreach (var key in queryString.AllKeys) {
if (key == null) continue; if (key == null) continue;
parameters[key] = queryString[key]; parameters[key] = queryString[key];
@@ -200,7 +193,7 @@ namespace Orchard.OutputCache.Filters
// create a tag which doesn't care about querystring // create a tag which doesn't care about querystring
_invariantCacheKey = ComputeCacheKey(filterContext, null); _invariantCacheKey = ComputeCacheKey(filterContext, null);
// don't retrieve cache content if refused // don't retrieve cache content if refused
// in this case the result of the action will update the current cached version // in this case the result of the action will update the current cached version
if (filterContext.RequestContext.HttpContext.Request.Headers["Cache-Control"] != "no-cache") { if (filterContext.RequestContext.HttpContext.Request.Headers["Cache-Control"] != "no-cache") {
@@ -219,46 +212,20 @@ namespace Orchard.OutputCache.Filters
var response = filterContext.HttpContext.Response; var response = filterContext.HttpContext.Response;
// render cached content // render cached content
if (_cacheItem != null) if (_cacheItem != null) {
{
Logger.Debug("Cache item found, expires on " + _cacheItem.ValidUntilUtc); Logger.Debug("Cache item found, expires on " + _cacheItem.ValidUntilUtc);
var output = _cacheItem.Output; var output = _cacheItem.Output;
/*
*
* There is no need to replace the AntiForgeryToken as it is not used for unauthenticated requests
* and at this point, the request can't be authenticated
*
*
// replace any anti forgery token with a fresh value
if (output.Contains(AntiforgeryBeacon))
{
var viewContext = new ViewContext
{
HttpContext = filterContext.HttpContext,
Controller = filterContext.Controller
};
var htmlHelper = new HtmlHelper(viewContext, new ViewDataContainer());
var siteSalt = _workContext.CurrentSite.SiteSalt;
var token = htmlHelper.AntiForgeryToken(siteSalt);
output = output.Replace(AntiforgeryBeacon, token.ToString());
}
*/
// adds some caching information to the output if requested // adds some caching information to the output if requested
if (_debugMode) if (_debugMode) {
{
output += "\r\n<!-- Cached on " + _cacheItem.CachedOnUtc + " (UTC) until " + _cacheItem.ValidUntilUtc + " (UTC) -->"; output += "\r\n<!-- Cached on " + _cacheItem.CachedOnUtc + " (UTC) until " + _cacheItem.ValidUntilUtc + " (UTC) -->";
response.AddHeader("X-Cached-On", _cacheItem.CachedOnUtc.ToString("r")); response.AddHeader("X-Cached-On", _cacheItem.CachedOnUtc.ToString("r"));
response.AddHeader("X-Cached-Until", _cacheItem.ValidUntilUtc.ToString("r")); response.AddHeader("X-Cached-Until", _cacheItem.ValidUntilUtc.ToString("r"));
} }
filterContext.Result = new ContentResult // shorcut action execution
{ filterContext.Result = new ContentResult {
Content = output, Content = output,
ContentType = _cacheItem.ContentType ContentType = _cacheItem.ContentType
}; };
@@ -288,7 +255,7 @@ namespace Orchard.OutputCache.Filters
filterContext.HttpContext.Response.Cache.SetCacheability(HttpCacheability.NoCache); filterContext.HttpContext.Response.Cache.SetCacheability(HttpCacheability.NoCache);
filterContext.HttpContext.Response.Cache.SetNoStore(); filterContext.HttpContext.Response.Cache.SetNoStore();
filterContext.HttpContext.Response.Cache.SetMaxAge(new TimeSpan(0)); filterContext.HttpContext.Response.Cache.SetMaxAge(new TimeSpan(0));
_filter = null; _filter = null;
return; return;
} }
@@ -310,6 +277,8 @@ namespace Orchard.OutputCache.Filters
return; return;
} }
// todo: look for RedirectToRoute to, or intercept 302s
if (filterContext.HttpContext.Request.HttpMethod.Equals("POST", StringComparison.OrdinalIgnoreCase) if (filterContext.HttpContext.Request.HttpMethod.Equals("POST", StringComparison.OrdinalIgnoreCase)
&& filterContext.Result is RedirectResult) { && filterContext.Result is RedirectResult) {
@@ -359,8 +328,7 @@ namespace Orchard.OutputCache.Filters
} }
} }
public void OnResultExecuted(ResultExecutedContext filterContext) public void OnResultExecuted(ResultExecutedContext filterContext) {
{
var response = filterContext.HttpContext.Response; var response = filterContext.HttpContext.Response;
// save the result only if the content can be intercepted // save the result only if the content can be intercepted
@@ -379,40 +347,27 @@ namespace Orchard.OutputCache.Filters
var configuration = configurations.FirstOrDefault(c => c.RouteKey == key); var configuration = configurations.FirstOrDefault(c => c.RouteKey == key);
// do not cache ? // do not cache ?
if (configuration != null && configuration.Duration == 0) if (configuration != null && configuration.Duration == 0) {
{
return; return;
} }
// ignored url ? // ignored url ?
if (IsIgnoredUrl(filterContext.RequestContext.HttpContext.Request.AppRelativeCurrentExecutionFilePath, _ignoredUrls)) if (IsIgnoredUrl(filterContext.RequestContext.HttpContext.Request.AppRelativeCurrentExecutionFilePath, _ignoredUrls)) {
{
return; return;
} }
if(response.IsClientConnected) // flush here to force the Filter to get the rendered content
if (response.IsClientConnected)
response.Flush(); response.Flush();
var output = _filter.GetContents(response.ContentEncoding); var output = _filter.GetContents(response.ContentEncoding);
if (String.IsNullOrWhiteSpace(output)) if (String.IsNullOrWhiteSpace(output)) {
{
return; return;
} }
var tokenIndex = output.IndexOf(AntiforgeryTag, StringComparison.Ordinal); response.Filter = null;
response.Write(output);
// substitute antiforgery token by a beacon
if (tokenIndex != -1)
{
var tokenEnd = output.IndexOf(">", tokenIndex, StringComparison.Ordinal);
var sb = new StringBuilder();
sb.Append(output.Substring(0, tokenIndex));
sb.Append(AntiforgeryBeacon);
sb.Append(output.Substring(tokenEnd + 1));
output = sb.ToString();
}
// default duration of specific one ? // default duration of specific one ?
var cacheDuration = configuration != null && configuration.Duration.HasValue ? configuration.Duration.Value : _cacheDuration; var cacheDuration = configuration != null && configuration.Duration.HasValue ? configuration.Duration.Value : _cacheDuration;
@@ -442,13 +397,12 @@ namespace Orchard.OutputCache.Filters
// add to the tags index // add to the tags index
foreach (var tag in _cacheItem.Tags) { foreach (var tag in _cacheItem.Tags) {
_tagCache.Tag(tag, _cacheKey); _tagCache.Tag(tag, _cacheKey);
} }
} }
public void OnResultExecuting(ResultExecutingContext filterContext) public void OnResultExecuting(ResultExecutingContext filterContext) {
{
} }
/// <summary> /// <summary>
@@ -468,7 +422,7 @@ namespace Orchard.OutputCache.Filters
// an ETag is a string that uniquely identifies a specific version of a component. // an ETag is a string that uniquely identifies a specific version of a component.
// we use the cache item to detect if it's a new one // we use the cache item to detect if it's a new one
response.Cache.SetETag(cacheItem.GetHashCode().ToString(CultureInfo.InvariantCulture)); response.Cache.SetETag(cacheItem.GetHashCode().ToString(CultureInfo.InvariantCulture));
response.Cache.SetOmitVaryStar(true); response.Cache.SetOmitVaryStar(true);
if (_varyQueryStringParameters != null) { if (_varyQueryStringParameters != null) {
@@ -489,25 +443,25 @@ namespace Orchard.OutputCache.Filters
// create a unique cache per browser, in case a Theme is rendered differently (e.g., mobile) // create a unique cache per browser, in case a Theme is rendered differently (e.g., mobile)
// c.f. http://msdn.microsoft.com/en-us/library/aa478965.aspx // c.f. http://msdn.microsoft.com/en-us/library/aa478965.aspx
// c.f. http://stackoverflow.com/questions/6007287/outputcache-varybyheader-user-agent-or-varybycustom-browser // c.f. http://stackoverflow.com/questions/6007287/outputcache-varybyheader-user-agent-or-varybycustom-browser
response.Cache.SetVaryByCustom("browser"); response.Cache.SetVaryByCustom("browser");
// enabling this would create an entry for each different browser sub-version // enabling this would create an entry for each different browser sub-version
// response.Cache.VaryByHeaders.UserAgent = true; // response.Cache.VaryByHeaders.UserAgent = true;
} }
private string ComputeCacheKey(ControllerContext controllerContext, IEnumerable<KeyValuePair<string, object>> parameters) { private string ComputeCacheKey(ControllerContext controllerContext, IEnumerable<KeyValuePair<string, object>> parameters) {
var url = controllerContext.HttpContext.Request.RawUrl; var url = controllerContext.HttpContext.Request.RawUrl;
if(!VirtualPathUtility.IsAbsolute(url)) { if (!VirtualPathUtility.IsAbsolute(url)) {
var applicationRoot = controllerContext.HttpContext.Request.ToRootUrlString(); var applicationRoot = controllerContext.HttpContext.Request.ToRootUrlString();
if(url.StartsWith(applicationRoot, StringComparison.OrdinalIgnoreCase)) { if (url.StartsWith(applicationRoot, StringComparison.OrdinalIgnoreCase)) {
url = url.Substring(applicationRoot.Length); url = url.Substring(applicationRoot.Length);
} }
} }
return ComputeCacheKey(_shellSettings.Name, url, () => _workContext.CurrentCulture, _themeManager.GetRequestTheme(controllerContext.RequestContext).Id, parameters); return ComputeCacheKey(_shellSettings.Name, url, () => _workContext.CurrentCulture, _themeManager.GetRequestTheme(controllerContext.RequestContext).Id, parameters);
} }
private string ComputeCacheKey(string tenant, string absoluteUrl, Func<string> culture, string theme, IEnumerable<KeyValuePair<string, object>> parameters){ private string ComputeCacheKey(string tenant, string absoluteUrl, Func<string> culture, string theme, IEnumerable<KeyValuePair<string, object>> parameters) {
var keyBuilder = new StringBuilder(); var keyBuilder = new StringBuilder();
keyBuilder.Append("tenant=").Append(tenant).Append(";"); keyBuilder.Append("tenant=").Append(tenant).Append(";");
@@ -537,43 +491,36 @@ namespace Orchard.OutputCache.Filters
/// <summary> /// <summary>
/// Returns true if the given url should be ignored, as defined in the settings /// Returns true if the given url should be ignored, as defined in the settings
/// </summary> /// </summary>
private static bool IsIgnoredUrl(string url, string ignoredUrls) private static bool IsIgnoredUrl(string url, string ignoredUrls) {
{ if (String.IsNullOrEmpty(ignoredUrls)) {
if(String.IsNullOrEmpty(ignoredUrls))
{
return false; return false;
} }
// remove ~ if present // remove ~ if present
if(url.StartsWith("~")) { if (url.StartsWith("~")) {
url = url.Substring(1); url = url.Substring(1);
} }
using (var urlReader = new StringReader(ignoredUrls)) using (var urlReader = new StringReader(ignoredUrls)) {
{
string relativePath; string relativePath;
while (null != (relativePath = urlReader.ReadLine())) while (null != (relativePath = urlReader.ReadLine())) {
{
// remove ~ if present // remove ~ if present
if (relativePath.StartsWith("~")) { if (relativePath.StartsWith("~")) {
relativePath = relativePath.Substring(1); relativePath = relativePath.Substring(1);
} }
if (String.IsNullOrWhiteSpace(relativePath)) if (String.IsNullOrWhiteSpace(relativePath)) {
{
continue; continue;
} }
relativePath = relativePath.Trim(); relativePath = relativePath.Trim();
// ignore comments // ignore comments
if(relativePath.StartsWith("#")) if (relativePath.StartsWith("#")) {
{
continue; continue;
} }
if(String.Equals(relativePath, url, StringComparison.OrdinalIgnoreCase)) if (String.Equals(relativePath, url, StringComparison.OrdinalIgnoreCase)) {
{
return true; return true;
} }
} }
@@ -583,92 +530,86 @@ namespace Orchard.OutputCache.Filters
} }
} }
/// <summary> /// <summary>
/// Captures the response stream while writing to it /// Captures the response stream while writing to it
/// </summary> /// </summary>
public class CapturingResponseFilter : Stream public class CapturingResponseFilter : Stream {
{ // private readonly Stream _sink;
private readonly Stream _sink;
private readonly MemoryStream _mem; private readonly MemoryStream _mem;
public CapturingResponseFilter(Stream sink) public CapturingResponseFilter(Stream sink) {
{ // _sink = sink;
_sink = sink;
_mem = new MemoryStream(); _mem = new MemoryStream();
} }
// The following members of Stream must be overriden. // The following members of Stream must be overriden.
public override bool CanRead public override bool CanRead {
{
get { return true; } get { return true; }
} }
public override bool CanSeek public override bool CanSeek {
{
get { return false; } get { return false; }
} }
public override bool CanWrite public override bool CanWrite {
{
get { return false; } get { return false; }
} }
public override long Length public override long Length {
{
get { return 0; } get { return 0; }
} }
public override long Position { get; set; } public override long Position { get; set; }
public override long Seek(long offset, SeekOrigin direction) public override long Seek(long offset, SeekOrigin direction) {
{
return 0; return 0;
} }
public override void SetLength(long length) public override void SetLength(long length) {
{ // _sink.SetLength(length);
_sink.SetLength(length);
} }
public override void Close() public override void Close() {
{ // _sink.Close();
_sink.Close();
_mem.Close(); _mem.Close();
} }
public override void Flush() public override void Flush() {
{ // _sink.Flush();
_sink.Flush();
} }
public override int Read(byte[] buffer, int offset, int count) public override int Read(byte[] buffer, int offset, int count) {
{ // return _sink.Read(buffer, offset, count);
return _sink.Read(buffer, offset, count); return count;
} }
// Override the Write method to filter Response to a file. // Override the Write method to filter Response to a file.
public override void Write(byte[] buffer, int offset, int count) public override void Write(byte[] buffer, int offset, int count) {
{
//Here we will not write to the sink b/c we want to capture //Here we will not write to the sink b/c we want to capture
_sink.Write(buffer, offset, count); // _sink.Write(buffer, offset, count);
//Write out the response to the file. //Write out the response to the file.
_mem.Write(buffer, 0, count); _mem.Write(buffer, 0, count);
} }
public string GetContents(Encoding enc) public string GetContents(Encoding enc) {
{
var buffer = new byte[_mem.Length]; var buffer = new byte[_mem.Length];
_mem.Position = 0; _mem.Position = 0;
_mem.Read(buffer, 0, buffer.Length); _mem.Read(buffer, 0, buffer.Length);
return enc.GetString(buffer, 0, buffer.Length); return enc.GetString(buffer, 0, buffer.Length);
} }
public byte[] GetContents() {
return _mem.ToArray();
}
protected override void Dispose(bool disposing) {
_mem.Dispose();
}
} }
public class ViewDataContainer : IViewDataContainer public class ViewDataContainer : IViewDataContainer {
{
public ViewDataDictionary ViewData { get; set; } public ViewDataDictionary ViewData { get; set; }
} }

View File

@@ -254,11 +254,11 @@
<VisualStudio> <VisualStudio>
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}"> <FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}">
<WebProjectProperties> <WebProjectProperties>
<UseIIS>False</UseIIS> <UseIIS>True</UseIIS>
<AutoAssignPort>False</AutoAssignPort> <AutoAssignPort>False</AutoAssignPort>
<DevelopmentServerPort>30320</DevelopmentServerPort> <DevelopmentServerPort>30321</DevelopmentServerPort>
<DevelopmentServerVPath>/OrchardLocal</DevelopmentServerVPath> <DevelopmentServerVPath>/OrchardLocal</DevelopmentServerVPath>
<IISUrl>http://localhost:30320/OrchardLocal</IISUrl> <IISUrl>http://localhost:30321/OrchardLocal</IISUrl>
<NTLMAuthentication>False</NTLMAuthentication> <NTLMAuthentication>False</NTLMAuthentication>
<UseCustomServer>False</UseCustomServer> <UseCustomServer>False</UseCustomServer>
<CustomServerUrl> <CustomServerUrl>