using System; using System.Globalization; using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Models; using Umbraco.Web.Configuration; 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; /// /// 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; /// /// Initializes a new instance of the class with a specific Uri and routing context. /// /// The request Uri. /// A routing context. internal PublishedContentRequest(Uri uri, RoutingContext routingContext) { if (uri == null) throw new ArgumentNullException("uri"); if (routingContext == null) throw new ArgumentNullException("routingContext"); Uri = uri; RoutingContext = routingContext; _engine = new PublishedContentRequestEngine(this); RenderingEngine = RenderingEngine.Unknown; } /// /// Gets the engine associated to the request. /// internal PublishedContentRequestEngine Engine { get { return _engine; } } /// /// Prepares the request. /// internal void Prepare() { _engine.PrepareRequest(); } /// /// 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 Prepared event. /// internal void OnPrepared() { if (Prepared != null) Prepared(this, EventArgs.Empty); if (!HasPublishedContent) 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; private set; } 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) { 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 && LegacyUmbracoSettings.For().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; } /// /// Gets a value indicating whether the content request has a template. /// public bool HasTemplate { get { return _template != null; } } #endregion #region Domain and Culture /// /// Gets or sets the content request's domain. /// public Domain Domain { 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 Domain != 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; } /// /// 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 umbraco page object is only available once Finalize()"); 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); } } /// /// 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 } }