using System; using System.Collections.Generic; using System.Globalization; using System.Web; using umbraco; using Umbraco.Core.Configuration; using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; 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 PublishedRequest { private readonly PublishedRouter _publishedRouter; private bool _readonly; // after prepared private bool _readonlyUri; // after preparing private Uri _uri; // clean uri, no virtual dir, no trailing slash nor .aspx, nothing private ITemplate _template; // template model if any else null private bool _is404; private DomainAndUri _domain; private CultureInfo _culture; private IPublishedContent _publishedContent; private IPublishedContent _initialPublishedContent; // found by finders before 404, redirects, etc private page _umbracoPage; // legacy /// /// Initializes a new instance of the class. /// /// The published router. /// The Umbraco context. /// The request Uri. internal PublishedRequest(PublishedRouter publishedRouter, UmbracoContext umbracoContext, Uri uri = null) { UmbracoContext = umbracoContext ?? throw new ArgumentNullException(nameof(umbracoContext)); _publishedRouter = publishedRouter ?? throw new ArgumentNullException(nameof(publishedRouter)); Uri = uri ?? umbracoContext.CleanedUmbracoUrl; RenderingEngine = RenderingEngine.Unknown; } /// /// Gets the UmbracoContext. /// public UmbracoContext UmbracoContext { get; } /// /// 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 => _uri; set { if (_readonlyUri) throw new InvalidOperationException("Cannot modify Uri after Preparing has triggered."); _uri = value; } } // utility for ensuring it is ok to set some properties private void EnsureWriteable() { if (_readonly) throw new InvalidOperationException("Cannot modify a PublishedContentRequest once it is read-only."); } /// /// Prepares the request. /// public void Prepare() { _publishedRouter.PrepareRequest(this); } #region Events /// /// Triggers before the published content request is prepared. /// /// When the event triggers, no preparation has been done. It is still possible to /// modify the request's Uri property, for example to restore its original, public-facing value /// 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 the Preparing event. /// internal void OnPreparing() { Preparing?.Invoke(this, EventArgs.Empty); _readonlyUri = true; } /// /// Triggers the Prepared event. /// internal void OnPrepared() { Prepared?.Invoke(this, EventArgs.Empty); if (HasPublishedContent == false) Is404 = true; // safety _readonly = true; } #endregion #region PublishedContent /// /// Gets or sets the requested content. /// /// Setting the requested content clears Template. public IPublishedContent PublishedContent { get { return _publishedContent; } set { EnsureWriteable(); _publishedContent = value; IsInternalRedirectPublishedContent = false; TemplateModel = null; } } /// /// Sets the requested content, following an internal redirect. /// /// The requested content. /// Depending on UmbracoSettings.InternalRedirectPreservesTemplate, will /// preserve or reset the template, if any. public void SetInternalRedirectPublishedContent(IPublishedContent content) { if (content == null) throw new ArgumentNullException(nameof(content)); EnsureWriteable(); // unless a template has been set already by the finder, // template should be null at that point. // IsInternalRedirect if IsInitial, or already IsInternalRedirect var isInternalRedirect = IsInitialPublishedContent || IsInternalRedirectPublishedContent; // redirecting to self if (content.Id == PublishedContent.Id) // neither can be null { // no need to set PublishedContent, we're done IsInternalRedirectPublishedContent = isInternalRedirect; return; } // else // save var template = _template; var renderingEngine = RenderingEngine; // set published content - this resets the template, and sets IsInternalRedirect to false PublishedContent = content; IsInternalRedirectPublishedContent = isInternalRedirect; // must restore the template if it's an internal redirect & the config option is set if (isInternalRedirect && UmbracoConfig.For.UmbracoSettings().WebRouting.InternalRedirectPreservesTemplate) { // restore _template = template; RenderingEngine = renderingEngine; } } /// /// Gets the initial requested content. /// /// The initial requested content is the content that was found by the finders, /// before anything such as 404, redirect... took place. public IPublishedContent InitialPublishedContent => _initialPublishedContent; /// /// Gets value indicating whether the current published content is the initial one. /// public bool IsInitialPublishedContent => _initialPublishedContent != null && _initialPublishedContent == _publishedContent; /// /// Indicates that the current PublishedContent is the initial one. /// public void SetIsInitialPublishedContent() { EnsureWriteable(); // note: it can very well be null if the initial content was not found _initialPublishedContent = _publishedContent; IsInternalRedirectPublishedContent = false; } /// /// Gets or sets a value indicating whether the current published content has been obtained /// from the initial published content following internal redirections exclusively. /// /// Used by PublishedContentRequestEngine.FindTemplate() to figure out whether to /// apply the internal redirect or not, when content is not the initial content. public bool IsInternalRedirectPublishedContent { get; private set; } /// /// Gets a value indicating whether the content request has a content. /// public bool HasPublishedContent => PublishedContent != null; #endregion #region Template /// /// Gets or sets the template model to use to display the requested content. /// internal ITemplate TemplateModel { get { return _template; } set { _template = value; RenderingEngine = RenderingEngine.Unknown; // reset if (_template != null) RenderingEngine = _publishedRouter.FindTemplateRenderingEngine(_template.Alias); } } /// /// Gets the alias of the template to use to display the requested content. /// public string TemplateAlias => _template?.Alias; /// /// Tries to set the template to use to display the requested content. /// /// The alias of the template. /// A value indicating whether a valid template with the specified alias was found. /// /// Successfully setting the template does refresh RenderingEngine. /// If setting the template fails, then the previous template (if any) remains in place. /// public bool TrySetTemplate(string alias) { EnsureWriteable(); if (string.IsNullOrWhiteSpace(alias)) { TemplateModel = null; return true; } // NOTE - can we stil get it with whitespaces in it due to old legacy bugs? alias = alias.Replace(" ", ""); var model = _publishedRouter.GetTemplate(alias); if (model == null) return false; TemplateModel = model; return true; } /// /// Sets the template to use to display the requested content. /// /// The template. /// Setting the template does refresh RenderingEngine. public void SetTemplate(ITemplate template) { EnsureWriteable(); TemplateModel = template; } /// /// Resets the template. /// /// The RenderingEngine becomes unknown. public void ResetTemplate() { EnsureWriteable(); TemplateModel = null; } /// /// Gets a value indicating whether the content request has a template. /// public bool HasTemplate => _template != null; internal void UpdateOnMissingTemplate() { var __readonly = _readonly; _readonly = false; _publishedRouter.UpdateRequestOnMissingTemplate(this); _readonly = __readonly; } #endregion #region Domain and Culture /// /// Gets or sets the content request's domain. /// /// Is a DomainAndUri object ie a standard Domain plus the fully qualified uri. For example, /// the Domain may contain "example.com" whereas the Uri will be fully qualified eg "http://example.com/". public DomainAndUri Domain { get { return _domain; } set { EnsureWriteable(); _domain = value; } } /// /// Gets a value indicating whether the content request has a domain. /// public bool HasDomain => Domain != null; /// /// 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, // to allow for fallbacks when doing dictionnary lookup and such? #endregion #region Rendering /// /// Gets or sets whether the rendering engine is MVC or WebForms. /// public RenderingEngine RenderingEngine { get; internal set; } #endregion #region Status /// /// Gets or sets a value indicating whether the requested content could not be found. /// /// This is set in the PublishedContentRequestBuilder and can also be used in /// custom content finders or Prepared event handlers, where we want to allow developers /// to indicate a request is 404 but not to cancel it. public bool Is404 { get { return _is404; } set { EnsureWriteable(); _is404 = value; } } /// /// Gets a value indicating whether the content request triggers a redirect (permanent or not). /// public bool IsRedirect => string.IsNullOrWhiteSpace(RedirectUrl) == false; /// /// Gets or sets a value indicating whether the redirect is permanent. /// public bool IsRedirectPermanent { get; private set; } /// /// Gets or sets the url to redirect to, when the content request triggers a redirect. /// public string RedirectUrl { get; private set; } /// /// Indicates that the content request should trigger a redirect (302). /// /// The url to redirect to. /// Does not actually perform a redirect, only registers that the response should /// redirect. Redirect will or will not take place in due time. public void SetRedirect(string url) { EnsureWriteable(); RedirectUrl = url; IsRedirectPermanent = false; } /// /// Indicates that the content request should trigger a permanent redirect (301). /// /// The url to redirect to. /// Does not actually perform a redirect, only registers that the response should /// redirect. Redirect will or will not take place in due time. public void SetRedirectPermanent(string url) { EnsureWriteable(); RedirectUrl = url; IsRedirectPermanent = true; } /// /// Indicates that the content requet should trigger a redirect, with a specified status code. /// /// The url to redirect to. /// The status code (300-308). /// Does not actually perform a redirect, only registers that the response should /// redirect. Redirect will or will not take place in due time. public void SetRedirect(string url, int status) { EnsureWriteable(); if (status < 300 || status > 308) throw new ArgumentOutOfRangeException(nameof(status), "Valid redirection status codes 300-308."); RedirectUrl = url; IsRedirectPermanent = (status == 301 || status == 308); if (status != 301 && status != 302) // default redirect statuses ResponseStatusCode = status; } /// /// Gets or sets the content request http response status code. /// /// Does not actually set the http response status code, only registers that the response /// should use the specified code. The code will or will not be used, in due time. public int ResponseStatusCode { get; private set; } /// /// Gets or sets the content request http response status description. /// /// Does not actually set the http response status description, only registers that the response /// should use the specified description. The description will or will not be used, in due time. public string ResponseStatusDescription { get; private set; } /// /// Sets the http response status code, along with an optional associated description. /// /// The http status code. /// The description. /// Does not actually set the http response status code and description, only registers that /// the response should use the specified code and description. The code and description will or will /// not be used, in due time. public void SetResponseStatus(int code, string description = null) { EnsureWriteable(); // .Status is deprecated // .SubStatusCode is IIS 7+ internal, ignore ResponseStatusCode = code; ResponseStatusDescription = description; } #endregion #region Response Cache /// /// Gets or sets the System.Web.HttpCacheability /// // Note: we used to set a default value here but that would then be the default // for ALL requests, we shouldn't overwrite it though if people are using [OutputCache] for example // see: https://our.umbraco.org/forum/using-umbraco-and-getting-started/79715-output-cache-in-umbraco-752 internal HttpCacheability Cacheability { get; set; } /// /// Gets or sets a list of Extensions to append to the Response.Cache object. /// internal List CacheExtensions { get; set; } = new List(); /// /// Gets or sets a dictionary of Headers to append to the Response object. /// internal Dictionary Headers { get; set; } = new Dictionary(); #endregion #region Legacy // for legacy/webforms code - todo - get rid of it eventually internal page UmbracoPage { get { if (_umbracoPage == null) throw new InvalidOperationException("The UmbracoPage object has not been initialized yet."); return _umbracoPage; } set { _umbracoPage = value; } } #endregion } }