#20085: Fixing output cache control strategy

FileResult actions were incorrectly cached

Work Item: 20085
This commit is contained in:
Sebastien Ros
2013-10-08 11:47:09 -07:00
parent 094e6d370d
commit dd852c3317
4 changed files with 139 additions and 73 deletions

View File

@@ -34,7 +34,8 @@ namespace Orchard.OutputCache.Filters {
private readonly ICacheService _cacheService;
private readonly ISignals _signals;
private readonly ShellSettings _shellSettings;
private static readonly string[] AuthorizedContentTypes = new [] { "text/html", "text/xml", "text/json" };
private readonly ICacheControlStrategy _cacheControlStrategy;
private Stream _previousFilter;
private const string RefreshKey = "__r";
@@ -48,7 +49,8 @@ namespace Orchard.OutputCache.Filters {
IClock clock,
ICacheService cacheService,
ISignals signals,
ShellSettings shellSettings) {
ShellSettings shellSettings,
ICacheControlStrategy cacheControlStrategy) {
_cacheManager = cacheManager;
_cacheStorageProvider = cacheStorageProvider;
_tagCache = tagCache;
@@ -59,6 +61,7 @@ namespace Orchard.OutputCache.Filters {
_cacheService = cacheService;
_signals = signals;
_shellSettings = shellSettings;
_cacheControlStrategy = cacheControlStrategy;
Logger = NullLogger.Instance;
}
@@ -257,7 +260,6 @@ namespace Orchard.OutputCache.Filters {
// adds some caching information to the output if requested
if (_debugMode) {
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-Until", _cacheItem.ValidUntilUtc.ToString("r"));
}
@@ -281,18 +283,27 @@ namespace Orchard.OutputCache.Filters {
ApplyCacheControl(_cacheItem, response);
// no cache content available, intercept the execution results for caching
_previousFilter = response.Filter;
response.Filter = _filter = new CapturingResponseFilter(response.Filter);
}
public void OnActionExecuted(ActionExecutedContext filterContext) {
}
// only cache view results, but don't return already as we still need to process redirections
if (!(filterContext.Result is ViewResultBase) && !( AuthorizedContentTypes.Contains(filterContext.HttpContext.Response.ContentType))) {
public void OnResultExecuted(ResultExecutedContext filterContext) {
var response = filterContext.HttpContext.Response;
if (!_cacheControlStrategy.IsCacheable(filterContext.Result, response)) {
_filter = null;
if (_previousFilter != null) {
response.Filter = _previousFilter;
}
}
// ignore error results from cache
if (filterContext.HttpContext.Response.StatusCode != (int)HttpStatusCode.OK) {
if (response.StatusCode != (int)HttpStatusCode.OK) {
// Never cache non-200 responses.
filterContext.HttpContext.Response.Cache.SetCacheability(HttpCacheability.NoCache);
@@ -300,6 +311,9 @@ namespace Orchard.OutputCache.Filters {
filterContext.HttpContext.Response.Cache.SetMaxAge(new TimeSpan(0));
_filter = null;
if (_previousFilter != null) {
response.Filter = _previousFilter;
}
return;
}
@@ -310,6 +324,10 @@ namespace Orchard.OutputCache.Filters {
// ignore in admin
if (AdminFilter.IsApplied(new RequestContext(filterContext.HttpContext, new RouteData()))) {
_filter = null;
if (_previousFilter != null) {
response.Filter = _previousFilter;
}
return;
}
@@ -317,16 +335,85 @@ namespace Orchard.OutputCache.Filters {
// ignore authenticated requests
if (_workContext.CurrentUser != null) {
_filter = null;
if (_previousFilter != null) {
response.Filter = _previousFilter;
}
return;
}
// handle redirections
TransformRedirect(filterContext);
// save the result only if the content can be intercepted
if (_filter == null) return;
// flush here to force the Filter to get the rendered content
if (response.IsClientConnected)
response.Flush();
var output = _filter.GetContents(response.ContentEncoding);
if (String.IsNullOrWhiteSpace(output)) {
return;
}
response.Filter = null;
response.Write(output);
// check if there is a specific rule not to cache the whole route
var configurations = _cacheService.GetRouteConfigurations();
var route = filterContext.Controller.ControllerContext.RouteData.Route;
var key = _cacheService.GetRouteDescriptorKey(filterContext.HttpContext, route);
var configuration = configurations.FirstOrDefault(c => c.RouteKey == key);
// do not cache ?
if (configuration != null && configuration.Duration == 0) {
return;
}
// default duration of specific one ?
var cacheDuration = configuration != null && configuration.Duration.HasValue ? configuration.Duration.Value : _cacheDuration;
// include each of the content item ids as tags for the cache entry
var contentItemIds = _displayedContentItemHandler.GetDisplayed().Select(x => x.ToString(CultureInfo.InvariantCulture)).ToArray();
_cacheItem.ContentType = response.ContentType;
_cacheItem.StatusCode = response.StatusCode;
_cacheItem.CachedOnUtc = _now;
_cacheItem.ValidFor = cacheDuration;
_cacheItem.QueryString = filterContext.HttpContext.Request.Url.Query;
_cacheItem.Output = output;
_cacheItem.CacheKey = _cacheKey;
_cacheItem.InvariantCacheKey = _invariantCacheKey;
_cacheItem.Tenant = _shellSettings.Name;
_cacheItem.Url = filterContext.HttpContext.Request.Url.AbsolutePath;
_cacheItem.Tags = new[] { _invariantCacheKey }.Union(contentItemIds).ToArray();
Logger.Debug("Cache item added: " + _cacheItem.CacheKey);
// remove old cache data
_cacheService.RemoveByTag(_invariantCacheKey);
// add data to cache
_cacheStorageProvider.Set(_cacheKey, _cacheItem);
// add to the tags index
foreach (var tag in _cacheItem.Tags) {
_tagCache.Tag(tag, _cacheKey);
}
}
private void TransformRedirect(ResultExecutedContext filterContext) {
// todo: look for RedirectToRoute to, or intercept 302s
if (filterContext.HttpContext.Request.HttpMethod.Equals("POST", StringComparison.OrdinalIgnoreCase)
&& filterContext.Result is RedirectResult) {
Logger.Debug("Redirect on POST");
var redirectUrl = ((RedirectResult)filterContext.Result).Url;
var redirectUrl = ((RedirectResult) filterContext.Result).Url;
if (!VirtualPathUtility.IsAbsolute(redirectUrl)) {
var applicationRoot = filterContext.HttpContext.Request.ToRootUrlString();
@@ -366,74 +453,11 @@ namespace Orchard.OutputCache.Filters {
redirectUrl = redirectUrl + querystring;
}
filterContext.Result = new RedirectResult(redirectUrl, ((RedirectResult)filterContext.Result).Permanent);
filterContext.Result = new RedirectResult(redirectUrl, ((RedirectResult) filterContext.Result).Permanent);
filterContext.HttpContext.Response.Cache.SetCacheability(HttpCacheability.NoCache);
}
}
public void OnResultExecuted(ResultExecutedContext filterContext) {
var response = filterContext.HttpContext.Response;
// save the result only if the content can be intercepted
if (_filter == null) return;
// check if there is a specific rule not to cache the whole route
var configurations = _cacheService.GetRouteConfigurations();
var route = filterContext.Controller.ControllerContext.RouteData.Route;
var key = _cacheService.GetRouteDescriptorKey(filterContext.HttpContext, route);
var configuration = configurations.FirstOrDefault(c => c.RouteKey == key);
// flush here to force the Filter to get the rendered content
if (response.IsClientConnected)
response.Flush();
var output = _filter.GetContents(response.ContentEncoding);
if (String.IsNullOrWhiteSpace(output)) {
return;
}
response.Filter = null;
response.Write(output);
// do not cache ?
if (configuration != null && configuration.Duration == 0) {
return;
}
// default duration of specific one ?
var cacheDuration = configuration != null && configuration.Duration.HasValue ? configuration.Duration.Value : _cacheDuration;
// include each of the content item ids as tags for the cache entry
var contentItemIds = _displayedContentItemHandler.GetDisplayed().Select(x => x.ToString(CultureInfo.InvariantCulture)).ToArray();
_cacheItem.ContentType = response.ContentType;
_cacheItem.StatusCode = response.StatusCode;
_cacheItem.CachedOnUtc = _now;
_cacheItem.ValidFor = cacheDuration;
_cacheItem.QueryString = filterContext.HttpContext.Request.Url.Query;
_cacheItem.Output = output;
_cacheItem.CacheKey = _cacheKey;
_cacheItem.InvariantCacheKey = _invariantCacheKey;
_cacheItem.Tenant = _shellSettings.Name;
_cacheItem.Url = filterContext.HttpContext.Request.Url.AbsolutePath;
_cacheItem.Tags = new[] { _invariantCacheKey }.Union(contentItemIds).ToArray();
Logger.Debug("Cache item added: " + _cacheItem.CacheKey);
// remove old cache data
_cacheService.RemoveByTag(_invariantCacheKey);
// add data to cache
_cacheStorageProvider.Set(_cacheKey, _cacheItem);
// add to the tags index
foreach (var tag in _cacheItem.Tags) {
_tagCache.Tag(tag, _cacheKey);
}
}
public void OnResultExecuting(ResultExecutingContext filterContext) {
}

View File

@@ -99,7 +99,9 @@
<Compile Include="Models\CacheSettingsPartRecord.cs" />
<Compile Include="Models\CacheParameterRecord.cs" />
<Compile Include="Services\CacheService.cs" />
<Compile Include="Services\DefaultCacheControlStrategy.cs" />
<Compile Include="Services\DefaultTagCache.cs" />
<Compile Include="Services\ICacheControlStrategy.cs" />
<Compile Include="Services\IDisplayedContentItemHandler.cs" />
<Compile Include="Services\ITagCache.cs" />
<Compile Include="Services\DefaultCacheStorageProvider.cs" />

View File

@@ -0,0 +1,29 @@
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace Orchard.OutputCache.Services {
/// <summary>
/// Represent the logic deciding if the result of an action can be cached
/// </summary>
public class DefaultCacheControlStrategy : ICacheControlStrategy {
private static readonly string[] AuthorizedContentTypes = { "text/html", "text/xml", "text/json", "text/plain" };
public bool IsCacheable(ActionResult result, HttpResponseBase response) {
// only cache view results
if (result is ViewResultBase) {
return true;
}
if (result is FileResult) {
return false;
}
if (AuthorizedContentTypes.Contains(response.ContentType)) {
return true;
}
return false;
}
}
}

View File

@@ -0,0 +1,11 @@
using System.Web;
using System.Web.Mvc;
namespace Orchard.OutputCache.Services {
/// <summary>
/// Represents the logic deciding if the result of an action can be cached
/// </summary>
public interface ICacheControlStrategy : IDependency {
bool IsCacheable(ActionResult result, HttpResponseBase response);
}
}