From b9fcda8f395745de037fb386eec47f7dbb13e14c Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Tue, 2 Aug 2016 17:01:32 +0200 Subject: [PATCH] U4-8361 301 Url Tracking Adds the ability to add certain headers to a PublishedContentRequest - these are internal for now until we're sure we want to expose them Adds response headers to tell browsers not to cache the 301 redirects so people can easily change their mind later --- .../Routing/ContentFinderByRedirectUrl.cs | 12 +- .../Routing/PublishedContentRequest.cs | 415 ++++++++++-------- src/Umbraco.Web/UmbracoModule.cs | 8 + 3 files changed, 244 insertions(+), 191 deletions(-) diff --git a/src/Umbraco.Web/Routing/ContentFinderByRedirectUrl.cs b/src/Umbraco.Web/Routing/ContentFinderByRedirectUrl.cs index aa68922f21..c6af13e874 100644 --- a/src/Umbraco.Web/Routing/ContentFinderByRedirectUrl.cs +++ b/src/Umbraco.Web/Routing/ContentFinderByRedirectUrl.cs @@ -1,4 +1,6 @@ -using Umbraco.Core; +using System.Collections.Generic; +using System.Web; +using Umbraco.Core; using Umbraco.Core.Logging; namespace Umbraco.Web.Routing @@ -27,6 +29,14 @@ namespace Umbraco.Web.Routing var service = contentRequest.RoutingContext.UmbracoContext.Application.Services.RedirectUrlService; var redirectUrl = service.GetMostRecentRedirectUrl(route); + // From: http://stackoverflow.com/a/22468386/5018 + // See http://issues.umbraco.org/issue/U4-8361#comment=67-30532 + // Setting automatic 301 redirects to not be cached because browsers cache these very aggressively which then leads + // to problems if you rename a page back to it's original name or create a new page with the original name + contentRequest.Cacheability = HttpCacheability.NoCache; + contentRequest.CacheExtensions = new List { "no-store, must-revalidate" }; + contentRequest.Headers = new Dictionary { { "Pragma", "no-cache" }, { "Expires", "0" } }; + if (redirectUrl == null) { LogHelper.Debug("No match for route: \"{0}\".", () => route); diff --git a/src/Umbraco.Web/Routing/PublishedContentRequest.cs b/src/Umbraco.Web/Routing/PublishedContentRequest.cs index 1ed0cf8150..2081fd69b5 100644 --- a/src/Umbraco.Web/Routing/PublishedContentRequest.cs +++ b/src/Umbraco.Web/Routing/PublishedContentRequest.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; +using System.Web; using System.Web.Security; using Umbraco.Core; using Umbraco.Core.Configuration; @@ -14,14 +15,14 @@ using RenderingEngine = Umbraco.Core.RenderingEngine; namespace Umbraco.Web.Routing { - /// - /// Represents a request for one specified Umbraco IPublishedContent to be rendered - /// by one specified template, using one specified Culture and RenderingEngine. - /// - public class PublishedContentRequest - { - private bool _readonly; - private bool _readonlyUri; + /// + /// Represents a request for one specified Umbraco IPublishedContent to be rendered + /// by one specified template, using one specified Culture and RenderingEngine. + /// + public class PublishedContentRequest + { + private bool _readonly; + private bool _readonlyUri; /// /// Triggers before the published content request is prepared. @@ -31,65 +32,65 @@ namespace Umbraco.Web.Routing /// that might have been modified by an in-between equipement such as a load-balancer. public static event EventHandler Preparing; - /// - /// Triggers once the published content request has been prepared, but before it is processed. - /// - /// When the event triggers, preparation is done ie domain, culture, document, template, - /// rendering engine, etc. have been setup. It is then possible to change anything, before - /// the request is actually processed and rendered by Umbraco. - public static event EventHandler Prepared; + /// + /// Triggers once the published content request has been prepared, but before it is processed. + /// + /// When the event triggers, preparation is done ie domain, culture, document, template, + /// rendering engine, etc. have been setup. It is then possible to change anything, before + /// the request is actually processed and rendered by Umbraco. + public static event EventHandler Prepared; - // the engine that does all the processing - // because in order to keep things clean and separated, - // the content request is just a data holder - private readonly PublishedContentRequestEngine _engine; + // the engine that does all the processing + // because in order to keep things clean and separated, + // the content request is just a data holder + private readonly PublishedContentRequestEngine _engine; // the cleaned up uri // the cleaned up Uri has no virtual directory, no trailing slash, no .aspx extension, etc. private Uri _uri; - /// - /// Initializes a new instance of the class with a specific Uri and routing context. - /// - /// The request Uri. - /// A routing context. + /// + /// Initializes a new instance of the class with a specific Uri and routing context. + /// + /// The request Uri. + /// A routing context. /// A callback method to return the roles for the provided login name when required /// - public PublishedContentRequest(Uri uri, RoutingContext routingContext, IWebRoutingSection routingConfig, Func> getRolesForLogin) - { + public PublishedContentRequest(Uri uri, RoutingContext routingContext, IWebRoutingSection routingConfig, Func> getRolesForLogin) + { if (uri == null) throw new ArgumentNullException("uri"); if (routingContext == null) throw new ArgumentNullException("routingContext"); Uri = uri; RoutingContext = routingContext; - GetRolesForLogin = getRolesForLogin; + GetRolesForLogin = getRolesForLogin; - _engine = new PublishedContentRequestEngine( + _engine = new PublishedContentRequestEngine( routingConfig, this); RenderingEngine = RenderingEngine.Unknown; - } + } [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("Use the constructor specifying all dependencies instead")] - public PublishedContentRequest(Uri uri, RoutingContext routingContext) + public PublishedContentRequest(Uri uri, RoutingContext routingContext) : this(uri, routingContext, UmbracoConfig.For.UmbracoSettings().WebRouting, s => Roles.Provider.GetRolesForUser(s)) - { - } + { + } - /// - /// Gets the engine associated to the request. - /// - internal PublishedContentRequestEngine Engine { get { return _engine; } } + /// + /// Gets the engine associated to the request. + /// + internal PublishedContentRequestEngine Engine { get { return _engine; } } - /// - /// Prepares the request. - /// + /// + /// Prepares the request. + /// public void Prepare() - { - _engine.PrepareRequest(); - } + { + _engine.PrepareRequest(); + } /// /// Called to configure the request @@ -104,56 +105,57 @@ namespace Umbraco.Web.Routing _engine.ConfigureRequest(); } - /// - /// Updates the request when there is no template to render the content. - /// - internal void UpdateOnMissingTemplate() - { + /// + /// Updates the request when there is no template to render the content. + /// + internal void UpdateOnMissingTemplate() + { var __readonly = _readonly; - _readonly = false; - _engine.UpdateRequestOnMissingTemplate(); - _readonly = __readonly; - } + _readonly = false; + _engine.UpdateRequestOnMissingTemplate(); + _readonly = __readonly; + } /// /// Triggers the Preparing event. /// internal void OnPreparing() - { - var handler = Preparing; + { + var handler = Preparing; if (handler != null) handler(this, EventArgs.Empty); - _readonlyUri = true; - } + _readonlyUri = true; + } - /// - /// Triggers the Prepared event. - /// - internal void OnPrepared() - { + /// + /// Triggers the Prepared event. + /// + internal void OnPrepared() + { var handler = Prepared; if (handler != null) handler(this, EventArgs.Empty); - if (HasPublishedContent == false) + if (HasPublishedContent == false) Is404 = true; // safety - _readonly = true; - } + _readonly = true; + } - /// - /// Gets or sets the cleaned up Uri used for routing. - /// - /// The cleaned up Uri has no virtual directory, no trailing slash, no .aspx extension, etc. - public Uri Uri { - get - { - return _uri; - } - set - { + /// + /// Gets or sets the cleaned up Uri used for routing. + /// + /// The cleaned up Uri has no virtual directory, no trailing slash, no .aspx extension, etc. + public Uri Uri + { + get + { + return _uri; + } + set + { if (_readonlyUri) throw new InvalidOperationException("Cannot modify Uri after Preparing has triggered."); - _uri = value; - } + _uri = value; + } } private void EnsureWriteable() @@ -162,37 +164,37 @@ namespace Umbraco.Web.Routing throw new InvalidOperationException("Cannot modify a PublishedContentRequest once it is read-only."); } - #region PublishedContent + #region PublishedContent - /// - /// The requested IPublishedContent, if any, else null. - /// - private IPublishedContent _publishedContent; + /// + /// The requested IPublishedContent, if any, else null. + /// + private IPublishedContent _publishedContent; - /// - /// The initial requested IPublishedContent, if any, else null. - /// - /// The initial requested content is the content that was found by the finders, - /// before anything such as 404, redirect... took place. - private IPublishedContent _initialPublishedContent; + /// + /// The initial requested IPublishedContent, if any, else null. + /// + /// The initial requested content is the content that was found by the finders, + /// before anything such as 404, redirect... took place. + private IPublishedContent _initialPublishedContent; - /// - /// Gets or sets the requested content. - /// - /// Setting the requested content clears Template. - public IPublishedContent PublishedContent - { - get { return _publishedContent; } - set - { + /// + /// Gets or sets the requested content. + /// + /// Setting the requested content clears Template. + public IPublishedContent PublishedContent + { + get { return _publishedContent; } + set + { EnsureWriteable(); - _publishedContent = value; + _publishedContent = value; IsInternalRedirectPublishedContent = false; - TemplateModel = null; - } - } + TemplateModel = null; + } + } - /// + /// /// Sets the requested content, following an internal redirect. /// /// The requested content. @@ -200,8 +202,8 @@ namespace Umbraco.Web.Routing /// preserve or reset the template, if any. public void SetInternalRedirectPublishedContent(IPublishedContent content) { - if (content == null) throw new ArgumentNullException("content"); - EnsureWriteable(); + if (content == null) throw new ArgumentNullException("content"); + EnsureWriteable(); // unless a template has been set already by the finder, // template should be null at that point. @@ -225,7 +227,7 @@ namespace Umbraco.Web.Routing // set published content - this resets the template, and sets IsInternalRedirect to false PublishedContent = content; - IsInternalRedirectPublishedContent = isInternalRedirect; + IsInternalRedirectPublishedContent = isInternalRedirect; // must restore the template if it's an internal redirect & the config option is set if (isInternalRedirect && UmbracoConfig.For.UmbracoSettings().WebRouting.InternalRedirectPreservesTemplate) @@ -243,16 +245,16 @@ namespace Umbraco.Web.Routing /// before anything such as 404, redirect... took place. public IPublishedContent InitialPublishedContent { get { return _initialPublishedContent; } } - /// - /// Gets value indicating whether the current published content is the initial one. - /// - public bool IsInitialPublishedContent - { - get - { - return _initialPublishedContent != null && _initialPublishedContent == _publishedContent; - } - } + /// + /// Gets value indicating whether the current published content is the initial one. + /// + public bool IsInitialPublishedContent + { + get + { + return _initialPublishedContent != null && _initialPublishedContent == _publishedContent; + } + } /// /// Indicates that the current PublishedContent is the initial one. @@ -282,19 +284,19 @@ namespace Umbraco.Web.Routing get { return PublishedContent != null; } } - #endregion + #endregion - #region Template + #region Template /// /// The template model, if any, else null. /// private ITemplate _template; - /// + /// /// Gets or sets the template model to use to display the requested content. /// - internal ITemplate TemplateModel + internal ITemplate TemplateModel { get { @@ -316,9 +318,9 @@ namespace Umbraco.Web.Routing /// public string TemplateAlias { - get - { - return _template == null ? null : _template.Alias; + get + { + return _template == null ? null : _template.Alias; } } @@ -348,7 +350,7 @@ namespace Umbraco.Web.Routing if (model == null) return false; - TemplateModel = model; + TemplateModel = model; return true; } @@ -368,10 +370,10 @@ namespace Umbraco.Web.Routing /// /// The RenderingEngine becomes unknown. public void ResetTemplate() - { - EnsureWriteable(); - TemplateModel = null; - } + { + EnsureWriteable(); + TemplateModel = null; + } /// /// Gets a value indicating whether the content request has a template. @@ -381,15 +383,15 @@ namespace Umbraco.Web.Routing get { return _template != null; } } - #endregion + #endregion - #region Domain and Culture + #region Domain and Culture - [Obsolete("Do not use this property, use the non-legacy UmbracoDomain property instead")] - public Domain Domain - { - get { return new Domain(UmbracoDomain); } - } + [Obsolete("Do not use this property, use the non-legacy UmbracoDomain property instead")] + public Domain Domain + { + get { return new Domain(UmbracoDomain); } + } //TODO: Should we publicize the setter now that we are using a non-legacy entity?? /// @@ -397,85 +399,85 @@ namespace Umbraco.Web.Routing /// public IDomain UmbracoDomain { get; internal set; } - /// - /// Gets or sets the content request's domain Uri. - /// - /// The Domain may contain "example.com" whereas the Uri will be fully qualified eg "http://example.com/". - public Uri DomainUri { get; internal set; } + /// + /// Gets or sets the content request's domain Uri. + /// + /// The Domain may contain "example.com" whereas the Uri will be fully qualified eg "http://example.com/". + public Uri DomainUri { get; internal set; } - /// - /// Gets a value indicating whether the content request has a domain. - /// - public bool HasDomain - { - get { return UmbracoDomain != null; } - } + /// + /// Gets a value indicating whether the content request has a domain. + /// + public bool HasDomain + { + get { return UmbracoDomain != null; } + } - private CultureInfo _culture; + private CultureInfo _culture; - /// - /// Gets or sets the content request's culture. - /// - public CultureInfo Culture - { + /// + /// Gets or sets the content request's culture. + /// + public CultureInfo Culture + { get { return _culture; } set { EnsureWriteable(); _culture = value; } - } + } - // note: do we want to have an ordered list of alternate cultures, + // note: do we want to have an ordered list of alternate cultures, // to allow for fallbacks when doing dictionnary lookup and such? - #endregion + #endregion - #region Rendering + #region Rendering - /// - /// Gets or sets whether the rendering engine is MVC or WebForms. - /// - public RenderingEngine RenderingEngine { get; internal set; } + /// + /// Gets or sets whether the rendering engine is MVC or WebForms. + /// + public RenderingEngine RenderingEngine { get; internal set; } - #endregion + #endregion - /// - /// Gets or sets the current RoutingContext. - /// - public RoutingContext RoutingContext { get; private set; } + /// + /// Gets or sets the current RoutingContext. + /// + public RoutingContext RoutingContext { get; private set; } - internal Func> GetRolesForLogin { get; private set; } + internal Func> GetRolesForLogin { get; private set; } - /// - /// The "umbraco page" object. - /// - private page _umbracoPage; + /// + /// The "umbraco page" object. + /// + private page _umbracoPage; - /// - /// Gets or sets the "umbraco page" object. - /// - /// - /// This value is only used for legacy/webforms code. - /// - internal page UmbracoPage - { - get - { - if (_umbracoPage == null) - throw new InvalidOperationException("The UmbracoPage object has not been initialized yet."); + /// + /// Gets or sets the "umbraco page" object. + /// + /// + /// This value is only used for legacy/webforms code. + /// + internal page UmbracoPage + { + get + { + if (_umbracoPage == null) + throw new InvalidOperationException("The UmbracoPage object has not been initialized yet."); - return _umbracoPage; - } - set { _umbracoPage = value; } - } + return _umbracoPage; + } + set { _umbracoPage = value; } + } - #region Status + #region Status - /// + /// /// Gets or sets a value indicating whether the requested content could not be found. /// - /// This is set in the PublishedContentRequestBuilder. + /// This is set in the PublishedContentRequestBuilder. public bool Is404 { get; internal set; } /// @@ -581,7 +583,40 @@ namespace Umbraco.Web.Routing ResponseStatusCode = code; ResponseStatusDescription = description; } - - #endregion + + #endregion + + /// + /// Gets or sets the System.Web.HttpCacheability + /// + /// Is set to System.Web.HttpCacheability.Private by default, which is the ASP.NET default. + private HttpCacheability _cacheability = HttpCacheability.Private; + internal HttpCacheability Cacheability + { + get { return _cacheability; } + set { _cacheability = value; } + } + + /// + /// Gets or sets a list of Extensions to append to the Response.Cache object + /// + private List _cacheExtensions = new List(); + internal List CacheExtensions + { + get { return _cacheExtensions; } + set { _cacheExtensions = value; } + } + + /// + /// Gets or sets a dictionary of Headers to append to the Response object + /// + private Dictionary _headers = new Dictionary(); + internal Dictionary Headers + { + get { return _headers; } + set { _headers = value; } + } + + } } \ No newline at end of file diff --git a/src/Umbraco.Web/UmbracoModule.cs b/src/Umbraco.Web/UmbracoModule.cs index 762a5f3e67..517fd112c9 100644 --- a/src/Umbraco.Web/UmbracoModule.cs +++ b/src/Umbraco.Web/UmbracoModule.cs @@ -323,6 +323,14 @@ namespace Umbraco.Web LogHelper.Debug("Response status: Redirect={0}, Is404={1}, StatusCode={2}", () => pcr.IsRedirect ? (pcr.IsRedirectPermanent ? "permanent" : "redirect") : "none", () => pcr.Is404 ? "true" : "false", () => pcr.ResponseStatusCode); + + response.Cache.SetCacheability(pcr.Cacheability); + + foreach (var cacheExtension in pcr.CacheExtensions) + response.Cache.AppendCacheExtension(cacheExtension); + + foreach (var header in pcr.Headers) + response.Cache.AppendCacheExtension(string.Format("{0}, {1}", header.Key, header.Value)); if (pcr.IsRedirect) {