From 8f1a0d343a537df747e69265a150e95d089f8c38 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Tue, 9 Jul 2013 17:17:44 -0700 Subject: [PATCH] Fixing duplicated cached content when using HttpModule with specific envent handlers --HG-- branch : 1.x --- .../Filters/OutputCacheFilter.cs | 211 +++++++----------- src/Orchard.Web/Orchard.Web.csproj | 6 +- 2 files changed, 79 insertions(+), 138 deletions(-) diff --git a/src/Orchard.Web/Modules/Orchard.OutputCache/Filters/OutputCacheFilter.cs b/src/Orchard.Web/Modules/Orchard.OutputCache/Filters/OutputCacheFilter.cs index 75f32d9c8..4eaad5a48 100644 --- a/src/Orchard.Web/Modules/Orchard.OutputCache/Filters/OutputCacheFilter.cs +++ b/src/Orchard.Web/Modules/Orchard.OutputCache/Filters/OutputCacheFilter.cs @@ -11,7 +11,6 @@ using System.Web.Mvc; using System.Web.Routing; using Orchard.OutputCache.Models; using Orchard.OutputCache.Services; -using Orchard; using Orchard.Caching; using Orchard.ContentManagement; using Orchard.Environment.Configuration; @@ -22,10 +21,8 @@ using Orchard.Themes; using Orchard.UI.Admin; using Orchard.Utility.Extensions; -namespace Orchard.OutputCache.Filters -{ - public class OutputCacheFilter : FilterProvider, IActionFilter, IResultFilter - { +namespace Orchard.OutputCache.Filters { + public class OutputCacheFilter : FilterProvider, IActionFilter, IResultFilter { private readonly ICacheManager _cacheManager; private readonly IOutputCacheStorageProvider _cacheStorageProvider; @@ -38,8 +35,6 @@ namespace Orchard.OutputCache.Filters private readonly ISignals _signals; private readonly ShellSettings _shellSettings; - private const string AntiforgeryBeacon = ""; - private const string AntiforgeryTag = "().FirstOrDefault() ; + var outputCacheAttribute = filterContext.ActionDescriptor.GetCustomAttributes(typeof(OutputCacheAttribute), true).Cast().FirstOrDefault(); - if(outputCacheAttribute != null) { + if (outputCacheAttribute != null) { if (outputCacheAttribute.Duration <= 0 || outputCacheAttribute.NoStore) { Logger.Debug("Request ignored based on OutputCache attribute"); return; @@ -111,7 +104,7 @@ namespace Orchard.OutputCache.Filters Logger.Debug("Request on: " + filterContext.RequestContext.HttpContext.Request.RawUrl); // 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"); return; } @@ -123,7 +116,7 @@ namespace Orchard.OutputCache.Filters } // ignore child actions, e.g. HomeController is using RenderAction() - if (filterContext.IsChildAction){ + if (filterContext.IsChildAction) { Logger.Debug("Request ignored on Child actions"); return; } @@ -156,9 +149,9 @@ namespace Orchard.OutputCache.Filters context => { context.Monitor(_signals.When(CacheSettingsPart.CacheKey)); var varyQueryStringParameters = _workContext.CurrentSite.As().VaryQueryStringParameters; - - return string.IsNullOrWhiteSpace(varyQueryStringParameters) ? null - : varyQueryStringParameters.Split(new[]{","}, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToArray(); + + return string.IsNullOrWhiteSpace(varyQueryStringParameters) ? null + : 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 parameters = new Dictionary(filterContext.ActionParameters); - foreach(var key in queryString.AllKeys) { + foreach (var key in queryString.AllKeys) { if (key == null) continue; parameters[key] = queryString[key]; @@ -200,7 +193,7 @@ namespace Orchard.OutputCache.Filters // create a tag which doesn't care about querystring _invariantCacheKey = ComputeCacheKey(filterContext, null); - + // don't retrieve cache content if refused // in this case the result of the action will update the current cached version if (filterContext.RequestContext.HttpContext.Request.Headers["Cache-Control"] != "no-cache") { @@ -219,46 +212,20 @@ namespace Orchard.OutputCache.Filters var response = filterContext.HttpContext.Response; // render cached content - if (_cacheItem != null) - { + if (_cacheItem != null) { Logger.Debug("Cache item found, expires on " + _cacheItem.ValidUntilUtc); 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 - if (_debugMode) - { + if (_debugMode) { output += "\r\n"; response.AddHeader("X-Cached-On", _cacheItem.CachedOnUtc.ToString("r")); response.AddHeader("X-Cached-Until", _cacheItem.ValidUntilUtc.ToString("r")); } - filterContext.Result = new ContentResult - { + // shorcut action execution + filterContext.Result = new ContentResult { Content = output, ContentType = _cacheItem.ContentType }; @@ -288,7 +255,7 @@ namespace Orchard.OutputCache.Filters filterContext.HttpContext.Response.Cache.SetCacheability(HttpCacheability.NoCache); filterContext.HttpContext.Response.Cache.SetNoStore(); filterContext.HttpContext.Response.Cache.SetMaxAge(new TimeSpan(0)); - + _filter = null; return; } @@ -310,6 +277,8 @@ namespace Orchard.OutputCache.Filters return; } + // todo: look for RedirectToRoute to, or intercept 302s + if (filterContext.HttpContext.Request.HttpMethod.Equals("POST", StringComparison.OrdinalIgnoreCase) && 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; // 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); // do not cache ? - if (configuration != null && configuration.Duration == 0) - { + if (configuration != null && configuration.Duration == 0) { return; } // ignored url ? - if (IsIgnoredUrl(filterContext.RequestContext.HttpContext.Request.AppRelativeCurrentExecutionFilePath, _ignoredUrls)) - { + if (IsIgnoredUrl(filterContext.RequestContext.HttpContext.Request.AppRelativeCurrentExecutionFilePath, _ignoredUrls)) { return; } - if(response.IsClientConnected) + // 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)) - { + if (String.IsNullOrWhiteSpace(output)) { return; } - var tokenIndex = output.IndexOf(AntiforgeryTag, StringComparison.Ordinal); - - // 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(); - } + response.Filter = null; + response.Write(output); // default duration of specific one ? var cacheDuration = configuration != null && configuration.Duration.HasValue ? configuration.Duration.Value : _cacheDuration; @@ -442,13 +397,12 @@ namespace Orchard.OutputCache.Filters // add to the tags index foreach (var tag in _cacheItem.Tags) { - _tagCache.Tag(tag, _cacheKey); + _tagCache.Tag(tag, _cacheKey); } - + } - public void OnResultExecuting(ResultExecutingContext filterContext) - { + public void OnResultExecuting(ResultExecutingContext filterContext) { } /// @@ -468,7 +422,7 @@ namespace Orchard.OutputCache.Filters // 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 response.Cache.SetETag(cacheItem.GetHashCode().ToString(CultureInfo.InvariantCulture)); - + response.Cache.SetOmitVaryStar(true); 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) // 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 - response.Cache.SetVaryByCustom("browser"); + response.Cache.SetVaryByCustom("browser"); // enabling this would create an entry for each different browser sub-version // response.Cache.VaryByHeaders.UserAgent = true; - + } private string ComputeCacheKey(ControllerContext controllerContext, IEnumerable> parameters) { var url = controllerContext.HttpContext.Request.RawUrl; - if(!VirtualPathUtility.IsAbsolute(url)) { + if (!VirtualPathUtility.IsAbsolute(url)) { var applicationRoot = controllerContext.HttpContext.Request.ToRootUrlString(); - if(url.StartsWith(applicationRoot, StringComparison.OrdinalIgnoreCase)) { + if (url.StartsWith(applicationRoot, StringComparison.OrdinalIgnoreCase)) { 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 culture, string theme, IEnumerable> parameters){ + private string ComputeCacheKey(string tenant, string absoluteUrl, Func culture, string theme, IEnumerable> parameters) { var keyBuilder = new StringBuilder(); keyBuilder.Append("tenant=").Append(tenant).Append(";"); @@ -537,43 +491,36 @@ namespace Orchard.OutputCache.Filters /// /// Returns true if the given url should be ignored, as defined in the settings /// - private static bool IsIgnoredUrl(string url, string ignoredUrls) - { - if(String.IsNullOrEmpty(ignoredUrls)) - { + private static bool IsIgnoredUrl(string url, string ignoredUrls) { + if (String.IsNullOrEmpty(ignoredUrls)) { return false; } // remove ~ if present - if(url.StartsWith("~")) { + if (url.StartsWith("~")) { url = url.Substring(1); } - using (var urlReader = new StringReader(ignoredUrls)) - { + using (var urlReader = new StringReader(ignoredUrls)) { string relativePath; - while (null != (relativePath = urlReader.ReadLine())) - { + while (null != (relativePath = urlReader.ReadLine())) { // remove ~ if present if (relativePath.StartsWith("~")) { relativePath = relativePath.Substring(1); } - if (String.IsNullOrWhiteSpace(relativePath)) - { + if (String.IsNullOrWhiteSpace(relativePath)) { continue; } relativePath = relativePath.Trim(); // ignore comments - if(relativePath.StartsWith("#")) - { + if (relativePath.StartsWith("#")) { continue; } - if(String.Equals(relativePath, url, StringComparison.OrdinalIgnoreCase)) - { + if (String.Equals(relativePath, url, StringComparison.OrdinalIgnoreCase)) { return true; } } @@ -583,92 +530,86 @@ namespace Orchard.OutputCache.Filters } } - + /// /// Captures the response stream while writing to it /// - public class CapturingResponseFilter : Stream - { - private readonly Stream _sink; + public class CapturingResponseFilter : Stream { + // private readonly Stream _sink; private readonly MemoryStream _mem; - public CapturingResponseFilter(Stream sink) - { - _sink = sink; + public CapturingResponseFilter(Stream sink) { + // _sink = sink; _mem = new MemoryStream(); } // The following members of Stream must be overriden. - public override bool CanRead - { + public override bool CanRead { get { return true; } } - public override bool CanSeek - { + public override bool CanSeek { get { return false; } } - public override bool CanWrite - { + public override bool CanWrite { get { return false; } } - public override long Length - { + public override long Length { get { return 0; } } public override long Position { get; set; } - public override long Seek(long offset, SeekOrigin direction) - { + public override long Seek(long offset, SeekOrigin direction) { return 0; } - public override void SetLength(long length) - { - _sink.SetLength(length); + public override void SetLength(long length) { + // _sink.SetLength(length); } - public override void Close() - { - _sink.Close(); + public override void Close() { + // _sink.Close(); _mem.Close(); } - public override void Flush() - { - _sink.Flush(); + public override void Flush() { + // _sink.Flush(); } - public override int Read(byte[] buffer, int offset, int count) - { - return _sink.Read(buffer, offset, count); + public override int Read(byte[] buffer, int offset, int count) { + // return _sink.Read(buffer, offset, count); + return count; } // 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 - _sink.Write(buffer, offset, count); + // _sink.Write(buffer, offset, count); //Write out the response to the file. _mem.Write(buffer, 0, count); } - public string GetContents(Encoding enc) - { + public string GetContents(Encoding enc) { var buffer = new byte[_mem.Length]; _mem.Position = 0; _mem.Read(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; } } diff --git a/src/Orchard.Web/Orchard.Web.csproj b/src/Orchard.Web/Orchard.Web.csproj index 0adea40e2..21fc895ba 100644 --- a/src/Orchard.Web/Orchard.Web.csproj +++ b/src/Orchard.Web/Orchard.Web.csproj @@ -254,11 +254,11 @@ - False + True False - 30320 + 30321 /OrchardLocal - http://localhost:30320/OrchardLocal + http://localhost:30321/OrchardLocal False False