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; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Models; using umbraco; using umbraco.cms.businesslogic.web; 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; /// /// 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; // 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. /// A callback method to return the roles for the provided login name when required /// 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; _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) : 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; } } /// /// Prepares the request. /// public void Prepare() { _engine.PrepareRequest(); } /// /// Called to configure the request /// /// /// This public method is legacy, Prepare() has been made public now which should be used and ensures the domains are assigned and /// if a public content item is already assigned Prepare() now ensures that the finders are not executed. /// [Obsolete("Use Prepare() instead which configures the request and wires up everything correctly")] public void ConfigureRequest() { _engine.ConfigureRequest(); } /// /// Updates the request when there is no template to render the content. /// internal void UpdateOnMissingTemplate() { var __readonly = _readonly; _readonly = false; _engine.UpdateRequestOnMissingTemplate(); _readonly = __readonly; } /// /// Triggers the Preparing event. /// internal void OnPreparing() { var handler = Preparing; if (handler != null) handler(this, EventArgs.Empty); _readonlyUri = true; } /// /// Triggers the Prepared event. /// internal void OnPrepared() { var handler = Prepared; if (handler != null) handler(this, EventArgs.Empty); if (HasPublishedContent == false) Is404 = true; // safety _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 { if (_readonlyUri) throw new InvalidOperationException("Cannot modify Uri after Preparing has triggered."); _uri = value; } } private void EnsureWriteable() { if (_readonly) throw new InvalidOperationException("Cannot modify a PublishedContentRequest once it is read-only."); } #region 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; /// /// 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("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 { get { return _initialPublishedContent; } } /// /// 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. /// 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 { get { return PublishedContent != null; } } #endregion #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 { get { return _template; } set { _template = value; RenderingEngine = RenderingEngine.Unknown; // reset if (_template != null) RenderingEngine = _engine.FindTemplateRenderingEngine(_template.Alias); } } /// /// Gets the alias of the template to use to display the requested content. /// public string TemplateAlias { get { return _template == null ? null : _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 = ApplicationContext.Current.Services.FileService.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 { get { return _template != null; } } #endregion #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); } } //TODO: Should we publicize the setter now that we are using a non-legacy entity?? /// /// Gets or sets the content request's domain. /// 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 a value indicating whether the content request has a domain. /// public bool HasDomain { get { return UmbracoDomain != null; } } private 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, // 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 /// /// Gets or sets the current RoutingContext. /// public RoutingContext RoutingContext { get; private set; } internal Func> GetRolesForLogin { get; private set; } /// /// 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."); return _umbracoPage; } set { _umbracoPage = value; } } #region Status /// /// Gets or sets a value indicating whether the requested content could not be found. /// /// This is set in the PublishedContentRequestBuilder. public bool Is404 { get; internal set; } /// /// Indicates that the requested content could not be found. /// /// This is for public access, 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 void SetIs404() { EnsureWriteable(); Is404 = true; } /// /// Gets a value indicating whether the content request triggers a redirect (permanent or not). /// public bool IsRedirect { get { return 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("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 /// /// 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 /// 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; } } } }