diff --git a/src/Umbraco.Tests/Routing/FinderByNiceUrlWithDomainsTests.cs b/src/Umbraco.Tests/Routing/FinderByNiceUrlWithDomainsTests.cs index ea20439455..6ea32d75f1 100644 --- a/src/Umbraco.Tests/Routing/FinderByNiceUrlWithDomainsTests.cs +++ b/src/Umbraco.Tests/Routing/FinderByNiceUrlWithDomainsTests.cs @@ -158,16 +158,15 @@ namespace Umbraco.Tests.Routing var routingContext = GetRoutingContext(url); var uri = routingContext.UmbracoContext.CleanedUmbracoUrl; //very important to use the cleaned up umbraco url - var docreq = new PublishedContentRequest(uri, routingContext); + var pcr = new PublishedContentRequest(uri, routingContext); // must lookup domain else lookup by url fails - var builder = new PublishedContentRequestBuilder(docreq); - builder.LookupDomain(); + pcr.Engine.FindDomain(); var lookup = new FinderByNiceUrl(); - var result = lookup.TryFindDocument(docreq); + var result = lookup.TryFindDocument(pcr); Assert.IsTrue(result); - Assert.AreEqual(expectedId, docreq.PublishedContentId); + Assert.AreEqual(expectedId, pcr.PublishedContentId); } [TestCase("http://domain1.com/", 1001, "en-US")] @@ -197,17 +196,16 @@ namespace Umbraco.Tests.Routing var routingContext = GetRoutingContext(url); var uri = routingContext.UmbracoContext.CleanedUmbracoUrl; //very important to use the cleaned up umbraco url - var docreq = new PublishedContentRequest(uri, routingContext); + var pcr = new PublishedContentRequest(uri, routingContext); // must lookup domain else lookup by url fails - var builder = new PublishedContentRequestBuilder(docreq); - builder.LookupDomain(); - Assert.AreEqual(expectedCulture, docreq.Culture.Name); + pcr.Engine.FindDomain(); + Assert.AreEqual(expectedCulture, pcr.Culture.Name); var lookup = new FinderByNiceUrl(); - var result = lookup.TryFindDocument(docreq); + var result = lookup.TryFindDocument(pcr); Assert.IsTrue(result); - Assert.AreEqual(expectedId, docreq.PublishedContentId); + Assert.AreEqual(expectedId, pcr.PublishedContentId); } } } diff --git a/src/Umbraco.Tests/Routing/UrlsWithNestedDomains.cs b/src/Umbraco.Tests/Routing/UrlsWithNestedDomains.cs index f8131ec656..f6414ee62b 100644 --- a/src/Umbraco.Tests/Routing/UrlsWithNestedDomains.cs +++ b/src/Umbraco.Tests/Routing/UrlsWithNestedDomains.cs @@ -43,16 +43,15 @@ namespace Umbraco.Tests.Routing // route a rogue url url = "http://domain1.com/1001-1/1001-1-1"; var uri = routingContext.UmbracoContext.CleanedUmbracoUrl; //very important to use the cleaned up umbraco url - var docreq = new PublishedContentRequest(uri, routingContext); - var builder = new PublishedContentRequestBuilder(docreq); - builder.LookupDomain(); - Assert.IsTrue(docreq.HasDomain); + var pcr = new PublishedContentRequest(uri, routingContext); + pcr.Engine.FindDomain(); + Assert.IsTrue(pcr.HasDomain); // check that it's been routed var lookup = new FinderByNiceUrl(); - var result = lookup.TryFindDocument(docreq); + var result = lookup.TryFindDocument(pcr); Assert.IsTrue(result); - Assert.AreEqual(100111, docreq.PublishedContentId); + Assert.AreEqual(100111, pcr.PublishedContentId); // has the cache been polluted? cachedRoutes = ((DefaultRoutesCache)routingContext.UmbracoContext.RoutesCache).GetCachedRoutes(); diff --git a/src/Umbraco.Web/Mvc/RenderRouteHandler.cs b/src/Umbraco.Web/Mvc/RenderRouteHandler.cs index fab342df7b..f586477154 100644 --- a/src/Umbraco.Web/Mvc/RenderRouteHandler.cs +++ b/src/Umbraco.Web/Mvc/RenderRouteHandler.cs @@ -283,6 +283,25 @@ namespace Umbraco.Web.Mvc return def; } + internal IHttpHandler GetHandlerOnMissingTemplate(PublishedContentRequest pcr) + { + if (!pcr.HasPublishedContent) + // means the builder could not find a proper document to handle 404 + return new PublishedContentNotFoundHandler(); + + if (!pcr.HasTemplate) + // means the engine could find a proper document, but the document has no template + // at that point there isn't much we can do and there is no point returning + // to Mvc since Mvc can't do much + return new PublishedContentNotFoundHandler("In addition, no template exists to render the custom 404."); + + if (pcr.RenderingEngine != RenderingEngine.Mvc) + // back to webforms + return (global::umbraco.UmbracoDefault)System.Web.Compilation.BuildManager.CreateInstanceFromVirtualPath("~/default.aspx", typeof(global::umbraco.UmbracoDefault)); + + return null; + } + /// /// this will determine the controller and set the values in the route data /// @@ -303,13 +322,15 @@ namespace Umbraco.Web.Mvc //we want to return a blank page, but we'll leave that up to the NoTemplateHandler. if (!publishedContentRequest.HasTemplate && !routeDef.HasHijackedRoute) { - var handler = publishedContentRequest.ProcessNoTemplateInMvc(requestContext.HttpContext); - //though this code should never execute if the ProcessNoTemplateInMvc method redirects, it seems that we should check it - //and return null, this could be required for unit testing as well + publishedContentRequest.UpdateOnMissingTemplate(); if (publishedContentRequest.IsRedirect) { + requestContext.HttpContext.Response.Redirect(publishedContentRequest.RedirectUrl, true); return null; } + if (publishedContentRequest.Is404) + requestContext.HttpContext.Response.StatusCode = 404; + var handler = GetHandlerOnMissingTemplate(publishedContentRequest); // if it's not null it can be either the PublishedContentNotFoundHandler (no document was // found to handle 404, or document with no template was found) or the WebForms handler diff --git a/src/Umbraco.Web/Routing/PublishedContentRequest.cs b/src/Umbraco.Web/Routing/PublishedContentRequest.cs index 8f6795a8d5..560753648a 100644 --- a/src/Umbraco.Web/Routing/PublishedContentRequest.cs +++ b/src/Umbraco.Web/Routing/PublishedContentRequest.cs @@ -29,153 +29,17 @@ namespace Umbraco.Web.Routing internal class PublishedContentRequest { /// - /// Assigns the request to the http context and proceeds to process the request. If everything is successful, invoke the callback. + /// Triggers once the published content request has been prepared, but before it is processed. /// - /// - /// - /// - internal void ProcessRequest(HttpContextBase httpContext, UmbracoContext umbracoContext, Action onSuccess) - { - if (umbracoContext == null) - throw new NullReferenceException("The UmbracoContext.Current is null, ProcessRequest cannot proceed unless there is a current UmbracoContext"); - if (umbracoContext.RoutingContext == null) - throw new NullReferenceException("The UmbracoContext.RoutingContext has not been assigned, ProcessRequest cannot proceed unless there is a RoutingContext assigned to the UmbracoContext"); + /// 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; - //assign back since this is a front-end request - umbracoContext.PublishedContentRequest = this; - - // note - at that point the original legacy module did something do handle IIS custom 404 errors - // ie pages looking like /anything.aspx?404;/path/to/document - I guess the reason was to support - // "directory urls" without having to do wildcard mapping to ASP.NET on old IIS. This is a pain - // to maintain and probably not used anymore - removed as of 06/2012. @zpqrtbnk. - // - // to trigger Umbraco's not-found, one should configure IIS and/or ASP.NET custom 404 errors - // so that they point to a non-existing page eg /redirect-404.aspx - // TODO: SD: We need more information on this for when we release 4.10.0 as I'm not sure what this means. - - //find domain - _builder.LookupDomain(); - // redirect if it has been flagged - if (this.IsRedirect) - httpContext.Response.Redirect(this.RedirectUrl, true); - //set the culture on the thread - once, so it's set when running document lookups - Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture = this.Culture; - //find the document, found will be true if the doc request has found BOTH a node and a template - // though currently we don't use this value. - var found = _builder.LookupDocument(); - //set the culture on the thread -- again, 'cos it might have changed due to a wildcard domain - Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture = this.Culture; - //this could be called in the LookupDocument method, but I've just put it here for clarity. - _builder.DetermineRenderingEngine(); - - //TODO: here we should launch an event so that people can modify the doc request to do whatever they want. - - // redirect if it has been flagged - if (this.IsRedirect) - httpContext.Response.Redirect(this.RedirectUrl, true); - - // handle 404 - if (this.Is404) - { - httpContext.Response.StatusCode = 404; - - if (!this.HasPublishedContent) - { - httpContext.RemapHandler(new PublishedContentNotFoundHandler()); - return; - } - - // else we have a document to render - // not having a template is ok here, MVC will take care of it - } - - // just be safe - should never ever happen - if (!this.HasPublishedContent) - throw new Exception("No document to render."); - - // trigger PublishedContentRequest.Rendering event? - // with complete access to the content request? - - // render even though we might have no template - // to give MVC a chance to hijack routes - // pass off to our handlers (mvc or webforms) - - // assign the legacy page back to the docrequest - // handlers like default.aspx will want it and most macros currently need it - this.UmbracoPage = new page(this); - - // these two are used by many legacy objects - httpContext.Items["pageID"] = this.PublishedContentId; - httpContext.Items["pageElements"] = this.UmbracoPage.Elements; - - if (onSuccess != null) - onSuccess(this); - } - - /// - /// After execution is handed off to MVC, we can finally check if the request has: No Template assigned and also the - /// route is not hijacked. When this occurs, we need to send the routing back through the builder to check for - /// not found handlers. - /// - /// - /// - internal IHttpHandler ProcessNoTemplateInMvc(HttpContextBase httpContext) - { - var content = this.PublishedContent; - this.PublishedContent = null; - - _builder.LookupDocument2(); - _builder.DetermineRenderingEngine(); - - // redirect if it has been flagged - if (this.IsRedirect) - { - httpContext.Response.Redirect(this.RedirectUrl, true); - } - - - // here .Is404 _has_ to be true - httpContext.Response.StatusCode = 404; - - if (!this.HasPublishedContent) - { - // means the builder could not find a proper document to handle 404 - // restore the saved content so we know it exists - this.PublishedContent = content; - return new PublishedContentNotFoundHandler(); - } - - if (!this.HasTemplate) - { - // means the builder could find a proper document, but the document has no template - // at that point there isn't much we can do and there is no point returning - // to Mvc since Mvc can't do much - return new PublishedContentNotFoundHandler("In addition, no template exists to render the custom 404."); - } - - // render even though we might have no template - // to give MVC a chance to hijack routes - // pass off to our handlers (mvc or webforms) - - // assign the legacy page back to the docrequest - // handlers like default.aspx will want it and most macros currently need it - this.UmbracoPage = new page(this); - - // these two are used by many legacy objects - httpContext.Items["pageID"] = this.PublishedContentId; - httpContext.Items["pageElements"] = this.UmbracoPage.Elements; - - switch (this.RenderingEngine) - { - case Core.RenderingEngine.Mvc: - return null; - case Core.RenderingEngine.WebForms: - default: - return (global::umbraco.UmbracoDefault)System.Web.Compilation.BuildManager.CreateInstanceFromVirtualPath("~/default.aspx", typeof(global::umbraco.UmbracoDefault)); - } - } - - private PublishedContentRequestBuilder _builder; + // 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 PublishedContentRequestEngine _engine; /// /// Initializes a new instance of the class with a specific Uri and routing context. @@ -183,18 +47,47 @@ namespace Umbraco.Web.Routing /// The request Uri. /// A routing context. public PublishedContentRequest(Uri uri, RoutingContext routingContext) - { + { if (uri == null) throw new ArgumentNullException("uri"); if (routingContext == null) throw new ArgumentNullException("routingContext"); this.Uri = uri; this.RoutingContext = routingContext; - _builder = new PublishedContentRequestBuilder(this); - - // default is Mvc - this.RenderingEngine = RenderingEngine.Mvc; - } + _engine = new PublishedContentRequestEngine(this); + + this.RenderingEngine = RenderingEngine.Mvc; // default + } + + /// + /// 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() + { + _engine.UpdateRequestOnMissingTemplate(); + } + + /// + /// Triggers the Prepared event. + /// + internal void OnPrepared() + { + if (Prepared != null) + Prepared(this, EventArgs.Empty); + } /// /// Gets or sets the cleaned up Uri used for routing. @@ -214,6 +107,13 @@ namespace Umbraco.Web.Routing /// private IPublishedContent _publishedContent = null; + /// + /// 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 = null; + /// /// Gets or sets the requested content. /// @@ -225,11 +125,26 @@ namespace Umbraco.Web.Routing { _publishedContent = value; this.Template = null; - this.AlternateTemplateAlias = null; _publishedContentId = _publishedContent != null ? _publishedContent.Id : 0; } } + /// + /// 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 or sets a value indicating whether the current published content is the initial one. + /// + public bool IsInitialPublishedContent + { + get { return _initialPublishedContent != null && _initialPublishedContent == _publishedContent; } + set { _initialPublishedContent = _publishedContent; } + } + /// /// Gets the identifier of the requested content. /// @@ -269,16 +184,6 @@ namespace Umbraco.Web.Routing get { return this.Template != null ; } } - /// - /// Gets or sets the alternate template alias. - /// - /// - /// When null or empty, use the default template. - /// Alternate template works only when displaying the intended document and should be set - /// after PublishedContent since setting PublishedContent clears the alternate template. - /// - public string AlternateTemplateAlias { get; set; } - #endregion #region Domain and Culture diff --git a/src/Umbraco.Web/Routing/PublishedContentRequestBuilder.cs b/src/Umbraco.Web/Routing/PublishedContentRequestBuilder.cs deleted file mode 100644 index 63490a11ee..0000000000 --- a/src/Umbraco.Web/Routing/PublishedContentRequestBuilder.cs +++ /dev/null @@ -1,513 +0,0 @@ -using System; -using System.Globalization; -using System.IO; -using System.Linq; -using Umbraco.Core; -using Umbraco.Core.IO; -using Umbraco.Core.Logging; -using umbraco.cms.businesslogic.language; -using umbraco.cms.businesslogic.member; -using umbraco.cms.businesslogic.template; -using umbraco.cms.businesslogic.web; - -namespace Umbraco.Web.Routing -{ - /// - /// Looks up the document using ILookup's and sets any additional properties required on the PublishedContentRequest object - /// - internal class PublishedContentRequestBuilder - { - private readonly PublishedContentRequest _publishedContentRequest; - private readonly UmbracoContext _umbracoContext; - private readonly RoutingContext _routingContext; - - public PublishedContentRequestBuilder(PublishedContentRequest publishedContentRequest) - { - if (publishedContentRequest == null) throw new ArgumentNullException("publishedContentRequest"); - _publishedContentRequest = publishedContentRequest; - _umbracoContext = publishedContentRequest.RoutingContext.UmbracoContext; - _routingContext = publishedContentRequest.RoutingContext; - } - - /// - /// Determines the rendering engine to use and sets the flag on the PublishedContentRequest - /// - internal void DetermineRenderingEngine() - { - //First, if there is no template, we will default to use MVC because MVC supports Hijacking routes which - //sometimes don't require a template since the developer may want full control over the rendering. - //Webforms doesn't support this so MVC it is. MVC will also handle what to do if no template or hijacked route - //is there (i.e. blank page) - if (!_publishedContentRequest.HasTemplate) - { - _publishedContentRequest.RenderingEngine = RenderingEngine.Mvc; - return; - } - - //NOTE: Not sure how the alias is actually saved with a space as this shouldn't ever be the case? - // but apparently this happens. I think what should actually be done always is the template alias - // should be saved using the ToUmbracoAlias method and then we can use this here too, that way it - // it 100% consistent. I'll leave this here for now until further invenstigation. - var templateAlias = _publishedContentRequest.Template.Alias.Replace(" ", string.Empty); - //var templateAlias = _publishedContentRequest.Template.Alias.ToUmbracoAlias(StringAliasCaseType.PascalCase); - - Func determineEngine = - (directory, alias, extensions, renderingEngine) => - { - //so we have a template, now we need to figure out where the template is, this is done just by the Alias field - //ensure it exists - if (!directory.Exists) Directory.CreateDirectory(directory.FullName); - var file = directory.GetFiles() - .FirstOrDefault(x => extensions.Any(e => x.Name.InvariantEquals(alias + e))); - - if (file != null) - { - //it is mvc since we have a template there that exists with this alias - _publishedContentRequest.RenderingEngine = renderingEngine; - return true; - } - return false; - }; - - //first determine if it is MVC, we will favor mvc if there is a template with the same name in both - // folders, if it is then MVC will be selected - if (!determineEngine( - new DirectoryInfo(IOHelper.MapPath(SystemDirectories.MvcViews)), - templateAlias, - new[]{".cshtml", ".vbhtml"}, - RenderingEngine.Mvc)) - { - //if not, then determine if it is webforms (this should def match if a template is assigned and its not in the MVC folder) - // if it doesn't match, then MVC will be used by default anyways. - determineEngine( - new DirectoryInfo(IOHelper.MapPath(SystemDirectories.Masterpages)), - templateAlias, - new[] {".master"}, - RenderingEngine.WebForms); - } - - } - - /// - /// Determines the site root (if any) matching the http request. - /// - /// A value indicating whether a domain was found. - internal bool LookupDomain() - { - const string tracePrefix = "LookupDomain: "; - - // note - we are not handling schemes nor ports here. - - LogHelper.Debug("{0}Uri=\"{1}\"", () => tracePrefix, () => _publishedContentRequest.Uri); - - // try to find a domain matching the current request - var domainAndUri = DomainHelper.DomainMatch(Domain.GetDomains(), _umbracoContext.CleanedUmbracoUrl, false); - - // handle domain - if (domainAndUri != null) - { - // matching an existing domain - LogHelper.Debug("{0}Matches domain=\"{1}\", rootId={2}, culture=\"{3}\"", - () => tracePrefix, - () => domainAndUri.Domain.Name, - () => domainAndUri.Domain.RootNodeId, - () => domainAndUri.Domain.Language.CultureAlias); - - _publishedContentRequest.Domain = domainAndUri.Domain; - _publishedContentRequest.DomainUri = domainAndUri.Uri; - _publishedContentRequest.Culture = new CultureInfo(domainAndUri.Domain.Language.CultureAlias); - - // canonical? not implemented at the moment - // if (...) - // { - // this.RedirectUrl = "..."; - // return true; - // } - } - else - { - // not matching any existing domain - LogHelper.Debug("{0}Matches no domain", () => tracePrefix); - - var defaultLanguage = Language.GetAllAsList().FirstOrDefault(); - _publishedContentRequest.Culture = defaultLanguage == null ? CultureInfo.CurrentUICulture : new CultureInfo(defaultLanguage.CultureAlias); - } - - LogHelper.Debug("{0}Culture=\"{1}\"", () => tracePrefix, () => _publishedContentRequest.Culture.Name); - - return _publishedContentRequest.Domain != null; - } - - /// - /// Determines the Umbraco document (if any) matching the http request. - /// - /// A value indicating whether a document and template nave been found. - internal bool LookupDocument() - { - const string tracePrefix = "LookupDocument: "; - LogHelper.Debug("{0}Path=\"{1}\"", () => tracePrefix, () => _publishedContentRequest.Uri.AbsolutePath); - - // run the document lookups - LookupDocument1(); - - // not handling umbracoRedirect here but after LookupDocument2 - // so internal redirect, 404, etc has precedence over redirect - - // handle not-found, redirects, access, template - LookupDocument2(); - - // handle umbracoRedirect (moved from umbraco.page) - FollowRedirect(); - - // handle wildcard domains - HandleWildcardDomains(); - - bool resolved = _publishedContentRequest.HasPublishedContent && _publishedContentRequest.HasTemplate; - return resolved; - } - - /// - /// Performs the document resolution first pass. - /// - /// The first past consists in running the document lookups. - internal void LookupDocument1() - { - const string tracePrefix = "LookupDocument: "; - - // look for the document - // the first successful resolver, if any, will set this.Node, and may also set this.Template - // some lookups may implement caching - - using (DisposableTimer.DebugDuration( - () => string.Format("{0}Begin resolvers", tracePrefix), - () => string.Format("{0}End resolvers, {1}", tracePrefix, (_publishedContentRequest.HasPublishedContent ? "a document was found" : "no document was found")))) - { - _routingContext.PublishedContentFinders.Any(lookup => lookup.TryFindDocument(_publishedContentRequest)); - } - } - - /// - /// Performs the document resolution second pass. - /// - /// - /// The second pass consists in handling "not found", internal redirects, access validation, and template. - /// TODO: Rename this method accordingly .... but to what? - /// - internal void LookupDocument2() - { - const string tracePrefix = "LookupDocument2: "; - - // at that point if we have a .PublishedContent then it is the "expected" document - // time to read the alternate template alias, from querystring, form, cookie or server vars. - // it will be cleared as soon as .PublishedContent change, because then it's not the - // expected content anymore and the alternate template does not apply. - _publishedContentRequest.AlternateTemplateAlias = _umbracoContext.HttpContext.Request["altTemplate"]; - - _umbracoContext.HttpContext.Trace.Write(string.Format("test {0}", _publishedContentRequest.AlternateTemplateAlias)); - - // handle "not found", follow internal redirects, validate access, template - // because these might loop, we have to have some sort of infinite loop detection - int i = 0, j = 0; - const int maxLoop = 12; - do - { - LogHelper.Debug("{0}{1}", () => tracePrefix, () => (i == 0 ? "Begin" : "Loop")); - - // handle not found - if (!_publishedContentRequest.HasPublishedContent) - { - _publishedContentRequest.Is404 = true; - LogHelper.Debug("{0}No document, try last chance lookup", () => tracePrefix); - - // if it fails then give up, there isn't much more that we can do - var lastChance = _routingContext.PublishedContentLastChanceFinder; - if (lastChance == null || !lastChance.TryFindDocument(_publishedContentRequest)) - { - LogHelper.Debug("{0}Failed to find a document, give up", () => tracePrefix); - break; - } - - LogHelper.Debug("{0}Found a document", () => tracePrefix); - } - - // follow internal redirects as long as it's not running out of control ie infinite loop of some sort - j = 0; - while (FollowInternalRedirects() && j++ < maxLoop) ; - if (j == maxLoop) // we're running out of control - break; - - // ensure access - if (_publishedContentRequest.HasPublishedContent) - EnsureNodeAccess(); - - // loop while we don't have page, ie the redirect or access - // got us to nowhere and now we need to run the notFoundLookup again - // as long as it's not running out of control ie infinite loop of some sort - - } while (!_publishedContentRequest.HasPublishedContent && i++ < maxLoop); - - if (i == maxLoop || j == maxLoop) - { - LogHelper.Debug("{0}Looks like we're running into an infinite loop, abort", () => tracePrefix); - _publishedContentRequest.PublishedContent = null; - } - - // resolve template - will do nothing if a template is already set - // moved out of the loop because LookupTemplate does set .PublishedContent to null anymore - // (see node in LookupTemplate) - if (_publishedContentRequest.HasPublishedContent) - LookupTemplate(); - - LogHelper.Debug("{0}End", () => tracePrefix); - } - - /// - /// Follows internal redirections through the umbracoInternalRedirectId document property. - /// - /// A value indicating whether redirection took place and led to a new published document. - /// - /// Redirecting to a different site root and/or culture will not pick the new site root nor the new culture. - /// As per legacy, if the redirect does not work, we just ignore it. - /// - private bool FollowInternalRedirects() - { - const string tracePrefix = "FollowInternalRedirects: "; - - if (_publishedContentRequest.PublishedContent == null) - throw new InvalidOperationException("There is no node."); - - bool redirect = false; - var internalRedirect = _publishedContentRequest.PublishedContent.GetPropertyValue("umbracoInternalRedirectId"); - - if (!string.IsNullOrWhiteSpace(internalRedirect)) - { - LogHelper.Debug("{0}Found umbracoInternalRedirectId={1}", () => tracePrefix, () => internalRedirect); - - int internalRedirectId; - if (!int.TryParse(internalRedirect, out internalRedirectId)) - internalRedirectId = -1; - - if (internalRedirectId <= 0) - { - // bad redirect - log and display the current page (legacy behavior) - //_publishedContentRequest.Document = null; // no! that would be to force a 404 - LogHelper.Debug("{0}Failed to redirect to id={1}: invalid value", () => tracePrefix, () => internalRedirect); - } - else if (internalRedirectId == _publishedContentRequest.PublishedContentId) - { - // redirect to self - LogHelper.Debug("{0}Redirecting to self, ignore", () => tracePrefix); - } - else - { - // redirect to another page - var node = _routingContext.PublishedContentStore.GetDocumentById( - _umbracoContext, - internalRedirectId); - - _publishedContentRequest.PublishedContent = node; - if (node != null) - { - redirect = true; - LogHelper.Debug("{0}Redirecting to id={1}", () => tracePrefix, () => internalRedirectId); - } - else - { - LogHelper.Debug("{0}Failed to redirect to id={1}: no such published document", () => tracePrefix, () => internalRedirectId); - } - } - } - - return redirect; - } - - /// - /// Ensures that access to current node is permitted. - /// - /// Redirecting to a different site root and/or culture will not pick the new site root nor the new culture. - private void EnsureNodeAccess() - { - const string tracePrefix = "EnsurePageAccess: "; - - if (_publishedContentRequest.PublishedContent == null) - throw new InvalidOperationException("There is no node."); - - var path = _publishedContentRequest.PublishedContent.Path; - - if (Access.IsProtected(_publishedContentRequest.PublishedContentId, path)) - { - LogHelper.Debug("{0}Page is protected, check for access", () => tracePrefix); - - System.Web.Security.MembershipUser user = null; - try - { - user = System.Web.Security.Membership.GetUser(); - } - catch (ArgumentException) - { - LogHelper.Debug("{0}Membership.GetUser returned ArgumentException", () => tracePrefix); - } - - if (user == null || !Member.IsLoggedOn()) - { - LogHelper.Debug("{0}Not logged in, redirect to login page", () => tracePrefix); - var loginPageId = Access.GetLoginPage(path); - if (loginPageId != _publishedContentRequest.PublishedContentId) - _publishedContentRequest.PublishedContent = _routingContext.PublishedContentStore.GetDocumentById( - _umbracoContext, - loginPageId); - } - else if (!Access.HasAccces(_publishedContentRequest.PublishedContentId, user.ProviderUserKey)) - { - LogHelper.Debug("{0}Current member has not access, redirect to error page", () => tracePrefix); - var errorPageId = Access.GetErrorPage(path); - if (errorPageId != _publishedContentRequest.PublishedContentId) - _publishedContentRequest.PublishedContent = _routingContext.PublishedContentStore.GetDocumentById( - _umbracoContext, - errorPageId); - } - else - { - LogHelper.Debug("{0}Current member has access", () => tracePrefix); - } - } - else - { - LogHelper.Debug("{0}Page is not protected", () => tracePrefix); - } - } - - /// - /// Resolves a template for the current node. - /// - private void LookupTemplate() - { - // HERE we should let people register their own way of finding a template, same as with documents!!!! - // do we? - - const string tracePrefix = "LookupTemplate: "; - - if (_publishedContentRequest.PublishedContent == null) - throw new InvalidOperationException("There is no node."); - - if (_publishedContentRequest.AlternateTemplateAlias.IsNullOrWhiteSpace()) - { - // we don't have an alternate template specified. use the current one if there's one already, - // which can happen if a content lookup also set the template (LookupByNiceUrlAndTemplate...), - // else lookup the template id on the document then lookup the template with that id. - - if (_publishedContentRequest.HasTemplate) - { - LogHelper.Debug("{0}Has a template already, and no alternate template.", () => tracePrefix); - return; - } - - // TODO: When we remove the need for a database for templates, then this id should be irrelavent, - // not sure how were going to do this nicely. - - var templateId = _publishedContentRequest.PublishedContent.TemplateId; - - if (templateId > 0) - { - LogHelper.Debug("{0}Look for template id={1}", () => tracePrefix, () => templateId); - // don't use the Template ctor as the result is not cached... instead use this static method - var template = Template.GetTemplate(templateId); - if (template == null) - throw new InvalidOperationException("The template with Id " + templateId + " does not exist, the page cannot render"); - _publishedContentRequest.Template = template; - LogHelper.Debug("{0}Got template id={1} alias=\"{2}\"", () => tracePrefix, () => template.Id, () => template.Alias); - } - else - { - LogHelper.Debug("{0}No specified template.", () => tracePrefix); - } - } - else - { - // we have an alternate template specified. lookup the template with that alias - // this means the we override any template that a content lookup might have set - // so /path/to/page/template1?altTemplate=template2 will use template2 - - // ignore if the alias does not match - just trace - - if (_publishedContentRequest.HasTemplate) - LogHelper.Debug("{0}Has a template already, but also an alternate template.", () => tracePrefix); - LogHelper.Debug("{0}Look for alternate template alias=\"{1}\"", () => tracePrefix, () => _publishedContentRequest.AlternateTemplateAlias); - - var template = Template.GetByAlias(_publishedContentRequest.AlternateTemplateAlias, true); - if (template != null) - { - _publishedContentRequest.Template = template; - LogHelper.Debug("{0}Got template id={1} alias=\"{2}\"", () => tracePrefix, () => template.Id, () => template.Alias); - } - else - { - LogHelper.Debug("{0}The template with alias=\"{1}\" does not exist, ignoring.", () => tracePrefix, () => _publishedContentRequest.AlternateTemplateAlias); - } - } - - if (!_publishedContentRequest.HasTemplate) - { - LogHelper.Debug("{0}No template was found.", () => tracePrefix); - - // initial idea was: if we're not already 404 and UmbracoSettings.HandleMissingTemplateAs404 is true - // then reset _publishedContentRequest.Document to null to force a 404. - // - // but: because we want to let MVC hijack routes even though no template is defined, we decide that - // a missing template is OK but the request will then be forwarded to MVC, which will need to take - // care of everything. - // - // so, don't set _publishedContentRequest.Document to null here - } - else - { - LogHelper.Debug("{0}Running with template id={1} alias=\"{2}\"", () => tracePrefix, () => _publishedContentRequest.Template.Id, () => _publishedContentRequest.Template.Alias); - } - } - - /// - /// Follows external redirection through umbracoRedirect document property. - /// - /// As per legacy, if the redirect does not work, we just ignore it. - private void FollowRedirect() - { - if (_publishedContentRequest.HasPublishedContent) - { - var redirectId = _publishedContentRequest.PublishedContent.GetPropertyValue("umbracoRedirect", -1); - - string redirectUrl = "#"; - if (redirectId > 0) - redirectUrl = _routingContext.NiceUrlProvider.GetNiceUrl(redirectId); - if (redirectUrl != "#") - _publishedContentRequest.RedirectUrl = redirectUrl; - } - } - - /// - /// Looks for wildcard domains in the path and updates Culture accordingly. - /// - private void HandleWildcardDomains() - { - const string tracePrefix = "HandleWildcardDomains: "; - - if (!_publishedContentRequest.HasPublishedContent) - return; - - var nodePath = _publishedContentRequest.PublishedContent.Path; - LogHelper.Debug("{0}Path=\"{1}\"", () => tracePrefix, () => nodePath); - var rootNodeId = _publishedContentRequest.HasDomain ? _publishedContentRequest.Domain.RootNodeId : (int?)null; - var domain = DomainHelper.LookForWildcardDomain(Domain.GetDomains(), nodePath, rootNodeId); - - if (domain != null) - { - _publishedContentRequest.Culture = new CultureInfo(domain.Language.CultureAlias); - LogHelper.Debug("{0}Got domain on node {1}, set culture to \"{2}\".", () => tracePrefix, - () => domain.RootNodeId, () => _publishedContentRequest.Culture.Name); - } - else - { - LogHelper.Debug("{0}No match.", () => tracePrefix); - } - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Web/Routing/PublishedContentRequestEngine.cs b/src/Umbraco.Web/Routing/PublishedContentRequestEngine.cs new file mode 100644 index 0000000000..86ed01278d --- /dev/null +++ b/src/Umbraco.Web/Routing/PublishedContentRequestEngine.cs @@ -0,0 +1,642 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Globalization; +using System.IO; + +using Umbraco.Core; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; + +using umbraco; +using umbraco.cms.businesslogic.web; +using umbraco.cms.businesslogic.language; +using umbraco.cms.businesslogic.template; +using umbraco.cms.businesslogic.member; + +namespace Umbraco.Web.Routing +{ + internal class PublishedContentRequestEngine + { + private PublishedContentRequest _pcr; + private RoutingContext _routingContext; + + /// + /// Initializes a new instance of the class with a content request. + /// + /// The content request. + public PublishedContentRequestEngine(PublishedContentRequest pcr) + { + _pcr = pcr; + _routingContext = pcr.RoutingContext; + + var umbracoContext = _routingContext.UmbracoContext; + if (_routingContext == null) throw new ArgumentException("pcr.RoutingContext is null."); + if (umbracoContext == null) throw new ArgumentException("pcr.RoutingContext.UmbracoContext is null."); + if (umbracoContext.RoutingContext != _routingContext) throw new ArgumentException("RoutingContext confusion."); + // no! not set yet. + //if (umbracoContext.PublishedContentRequest != _pcr) throw new ArgumentException("PublishedContentRequest confusion."); + } + + #region Public + + /// + /// Prepares the request. + /// + public void PrepareRequest() + { + // note - at that point the original legacy module did something do handle IIS custom 404 errors + // ie pages looking like /anything.aspx?404;/path/to/document - I guess the reason was to support + // "directory urls" without having to do wildcard mapping to ASP.NET on old IIS. This is a pain + // to maintain and probably not used anymore - removed as of 06/2012. @zpqrtbnk. + // + // to trigger Umbraco's not-found, one should configure IIS and/or ASP.NET custom 404 errors + // so that they point to a non-existing page eg /redirect-404.aspx + // TODO: SD: We need more information on this for when we release 4.10.0 as I'm not sure what this means. + + //find domain + FindDomain(); + + // if request has been flagged to redirect then return + // whoever called us is in charge of actually redirecting + if (_pcr.IsRedirect) + return; + + // set the culture on the thread - once, so it's set when running document lookups + Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture = _pcr.Culture; + + // find the document & template + FindPublishedContentAndTemplate(); + + // set the culture on the thread -- again, 'cos it might have changed due to a wildcard domain + Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture = _pcr.Culture; + + // find the rendering engine + FindRenderingEngine(); + + // trigger the Prepared event - at that point it is still possible to change about anything + _pcr.OnPrepared(); + + // set the culture on the thread -- again, 'cos it might have changed in the event handler + Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture = _pcr.Culture; + + // if request has been flagged to redirect then return + // whoever called us is in charge of actually redirecting + if (_pcr.IsRedirect) + return; + + // safety + if (!_pcr.HasPublishedContent) + _pcr.Is404 = true; + + // handle 404 : return + // whoever called us is in charge of doing what's appropriate + if (_pcr.Is404) + return; + + // can't go beyond that point without a PublishedContent to render + // it's ok not to have a template, in order to give MVC a chance to hijack routes + + // assign the legacy page back to the docrequest + // handlers like default.aspx will want it and most macros currently need it + _pcr.UmbracoPage = new page(_pcr); + + // these two are used by many legacy objects + _routingContext.UmbracoContext.HttpContext.Items["pageID"] = _pcr.PublishedContentId; + _routingContext.UmbracoContext.HttpContext.Items["pageElements"] = _pcr.UmbracoPage.Elements; + } + + /// + /// Updates the request when there is no template to render the content. + /// + /// This is called from Mvc when there's a document to render but no template. + public void UpdateRequestOnMissingTemplate() + { + // clear content + var content = _pcr.PublishedContent; + _pcr.PublishedContent = null; + + HandlePublishedContent(); // will go 404 + FindTemplate(); + FindRenderingEngine(); + + // if request has been flagged to redirect then return + // whoever called us is in charge of redirecting + if (_pcr.IsRedirect) + return; + + if (!_pcr.HasPublishedContent) + { + // means the engine could not find a proper document to handle 404 + // restore the saved content so we know it exists + _pcr.PublishedContent = content; + return; + } + + if (!_pcr.HasTemplate) + { + // means we may have a document, but we have no template + // at that point there isn't much we can do and there is no point returning + // to Mvc since Mvc can't do much either + return; + } + + // assign the legacy page back to the docrequest + // handlers like default.aspx will want it and most macros currently need it + _pcr.UmbracoPage = new page(_pcr); + + // these two are used by many legacy objects + _routingContext.UmbracoContext.HttpContext.Items["pageID"] = _pcr.PublishedContentId; + _routingContext.UmbracoContext.HttpContext.Items["pageElements"] = _pcr.UmbracoPage.Elements; + } + + #endregion + + #region Domain + + /// + /// Finds the site root (if any) matching the http request, and updates the PublishedContentRequest accordingly. + /// + /// A value indicating whether a domain was found. + internal bool FindDomain() + { + const string tracePrefix = "FindDomain: "; + + // note - we are not handling schemes nor ports here. + + LogHelper.Debug("{0}Uri=\"{1}\"", () => tracePrefix, () => _pcr.Uri); + + // try to find a domain matching the current request + var domainAndUri = DomainHelper.DomainMatch(Domain.GetDomains(), _pcr.Uri, false); + + // handle domain + if (domainAndUri != null) + { + // matching an existing domain + LogHelper.Debug("{0}Matches domain=\"{1}\", rootId={2}, culture=\"{3}\"", + () => tracePrefix, + () => domainAndUri.Domain.Name, + () => domainAndUri.Domain.RootNodeId, + () => domainAndUri.Domain.Language.CultureAlias); + + _pcr.Domain = domainAndUri.Domain; + _pcr.DomainUri = domainAndUri.Uri; + _pcr.Culture = new CultureInfo(domainAndUri.Domain.Language.CultureAlias); + + // canonical? not implemented at the moment + // if (...) + // { + // _pcr.RedirectUrl = "..."; + // return true; + // } + } + else + { + // not matching any existing domain + LogHelper.Debug("{0}Matches no domain", () => tracePrefix); + + var defaultLanguage = Language.GetAllAsList().FirstOrDefault(); + _pcr.Culture = defaultLanguage == null ? CultureInfo.CurrentUICulture : new CultureInfo(defaultLanguage.CultureAlias); + } + + LogHelper.Debug("{0}Culture=\"{1}\"", () => tracePrefix, () => _pcr.Culture.Name); + + return _pcr.Domain != null; + } + + /// + /// Looks for wildcard domains in the path and updates Culture accordingly. + /// + private void HandleWildcardDomains() + { + const string tracePrefix = "HandleWildcardDomains: "; + + if (!_pcr.HasPublishedContent) + return; + + var nodePath = _pcr.PublishedContent.Path; + LogHelper.Debug("{0}Path=\"{1}\"", () => tracePrefix, () => nodePath); + var rootNodeId = _pcr.HasDomain ? _pcr.Domain.RootNodeId : (int?)null; + var domain = DomainHelper.LookForWildcardDomain(Domain.GetDomains(), nodePath, rootNodeId); + + if (domain != null) + { + _pcr.Culture = new CultureInfo(domain.Language.CultureAlias); + LogHelper.Debug("{0}Got domain on node {1}, set culture to \"{2}\".", () => tracePrefix, + () => domain.RootNodeId, () => _pcr.Culture.Name); + } + else + { + LogHelper.Debug("{0}No match.", () => tracePrefix); + } + } + + #endregion + + #region Rendering engine + + /// + /// Finds the rendering engine to use, and updates the PublishedContentRequest accordingly. + /// + internal void FindRenderingEngine() + { + //First, if there is no template, we will default to use MVC because MVC supports Hijacking routes which + //sometimes don't require a template since the developer may want full control over the rendering. + //Webforms doesn't support this so MVC it is. MVC will also handle what to do if no template or hijacked route + //is there (i.e. blank page) + if (!_pcr.HasTemplate) + { + _pcr.RenderingEngine = RenderingEngine.Mvc; + return; + } + + //NOTE: Not sure how the alias is actually saved with a space as this shouldn't ever be the case? + // but apparently this happens. I think what should actually be done always is the template alias + // should be saved using the ToUmbracoAlias method and then we can use this here too, that way it + // it 100% consistent. I'll leave this here for now until further invenstigation. + var templateAlias = _pcr.Template.Alias.Replace(" ", string.Empty); + //var templateAlias = _pcr.Template.Alias.ToUmbracoAlias(StringAliasCaseType.PascalCase); + + Func determineEngine = + (directory, alias, extensions, renderingEngine) => + { + //so we have a template, now we need to figure out where the template is, this is done just by the Alias field + //ensure it exists + if (!directory.Exists) Directory.CreateDirectory(directory.FullName); + var file = directory.GetFiles() + .FirstOrDefault(x => extensions.Any(e => x.Name.InvariantEquals(alias + e))); + + if (file != null) + { + //it is mvc since we have a template there that exists with this alias + _pcr.RenderingEngine = renderingEngine; + return true; + } + return false; + }; + + //first determine if it is MVC, we will favor mvc if there is a template with the same name in both + // folders, if it is then MVC will be selected + if (!determineEngine( + new DirectoryInfo(IOHelper.MapPath(SystemDirectories.MvcViews)), + templateAlias, + new[] { ".cshtml", ".vbhtml" }, + RenderingEngine.Mvc)) + { + //if not, then determine if it is webforms (this should def match if a template is assigned and its not in the MVC folder) + // if it doesn't match, then MVC will be used by default anyways. + determineEngine( + new DirectoryInfo(IOHelper.MapPath(SystemDirectories.Masterpages)), + templateAlias, + new[] { ".master" }, + RenderingEngine.WebForms); + } + + } + + #endregion + + #region Document and template + + /// + /// Finds the Umbraco document (if any) matching the request, and updates the PublishedContentRequest accordingly. + /// + /// A value indicating whether a document and template were found. + private bool FindPublishedContentAndTemplate() + { + const string tracePrefix = "FindPublishedContentAndTemplate: "; + LogHelper.Debug("{0}Path=\"{1}\"", () => tracePrefix, () => _pcr.Uri.AbsolutePath); + + // run the document finders + FindPublishedContent(); + + // not handling umbracoRedirect here but after LookupDocument2 + // so internal redirect, 404, etc has precedence over redirect + + // handle not-found, redirects, access... + HandlePublishedContent(); + + // find a template + FindTemplate(); + + // handle umbracoRedirect + FollowExternalRedirect(); + + // handle wildcard domains + HandleWildcardDomains(); + + return _pcr.HasPublishedContent && _pcr.HasTemplate; + } + + /// + /// Tries to find the document matching the request, by running the IPublishedContentFinder instances. + /// + internal void FindPublishedContent() + { + const string tracePrefix = "FindPublishedContent: "; + + // look for the document + // the first successful finder, if any, will set this.PublishedContent, and may also set this.Template + // some finders may implement caching + + using (DisposableTimer.DebugDuration( + () => string.Format("{0}Begin finders", tracePrefix), + () => string.Format("{0}End finders, {1}", tracePrefix, (_pcr.HasPublishedContent ? "a document was found" : "no document was found")))) + { + _routingContext.PublishedContentFinders.Any(lookup => lookup.TryFindDocument(_pcr)); + } + + // indicate that the published content (if any) we have at the moment is the + // one that was found by the standard finders before anything else took place. + _pcr.IsInitialPublishedContent = true; + } + + /// + /// Handles the published content (if any). + /// + /// + /// Handles "not found", internal redirects, access validation... + /// things that must be handled in one place because they can create loops + /// + private void HandlePublishedContent() + { + const string tracePrefix = "HandlePublishedContent: "; + + // because these might loop, we have to have some sort of infinite loop detection + int i = 0, j = 0; + const int maxLoop = 8; + do + { + LogHelper.Debug("{0}{1}", () => tracePrefix, () => (i == 0 ? "Begin" : "Loop")); + + // handle not found + if (!_pcr.HasPublishedContent) + { + _pcr.Is404 = true; + LogHelper.Debug("{0}No document, try last chance lookup", () => tracePrefix); + + // if it fails then give up, there isn't much more that we can do + var lastChance = _routingContext.PublishedContentLastChanceFinder; + if (lastChance == null || !lastChance.TryFindDocument(_pcr)) + { + LogHelper.Debug("{0}Failed to find a document, give up", () => tracePrefix); + break; + } + + LogHelper.Debug("{0}Found a document", () => tracePrefix); + } + + // follow internal redirects as long as it's not running out of control ie infinite loop of some sort + j = 0; + while (FollowInternalRedirects() && j++ < maxLoop) ; + if (j == maxLoop) // we're running out of control + break; + + // ensure access + if (_pcr.HasPublishedContent) + EnsurePublishedContentAccess(); + + // loop while we don't have page, ie the redirect or access + // got us to nowhere and now we need to run the notFoundLookup again + // as long as it's not running out of control ie infinite loop of some sort + + } while (!_pcr.HasPublishedContent && i++ < maxLoop); + + if (i == maxLoop || j == maxLoop) + { + LogHelper.Debug("{0}Looks like we're running into an infinite loop, abort", () => tracePrefix); + _pcr.PublishedContent = null; + } + + LogHelper.Debug("{0}End", () => tracePrefix); + } + + /// + /// Follows internal redirections through the umbracoInternalRedirectId document property. + /// + /// A value indicating whether redirection took place and led to a new published document. + /// + /// Redirecting to a different site root and/or culture will not pick the new site root nor the new culture. + /// As per legacy, if the redirect does not work, we just ignore it. + /// + private bool FollowInternalRedirects() + { + const string tracePrefix = "FollowInternalRedirects: "; + + if (_pcr.PublishedContent == null) + throw new InvalidOperationException("There is no PublishedContent."); + + bool redirect = false; + var internalRedirect = _pcr.PublishedContent.GetPropertyValue("umbracoInternalRedirectId"); + + if (!string.IsNullOrWhiteSpace(internalRedirect)) + { + LogHelper.Debug("{0}Found umbracoInternalRedirectId={1}", () => tracePrefix, () => internalRedirect); + + int internalRedirectId; + if (!int.TryParse(internalRedirect, out internalRedirectId)) + internalRedirectId = -1; + + if (internalRedirectId <= 0) + { + // bad redirect - log and display the current page (legacy behavior) + //_pcr.Document = null; // no! that would be to force a 404 + LogHelper.Debug("{0}Failed to redirect to id={1}: invalid value", () => tracePrefix, () => internalRedirect); + } + else if (internalRedirectId == _pcr.PublishedContentId) + { + // redirect to self + LogHelper.Debug("{0}Redirecting to self, ignore", () => tracePrefix); + } + else + { + // redirect to another page + var node = _routingContext.PublishedContentStore.GetDocumentById(_routingContext.UmbracoContext, internalRedirectId); + + _pcr.PublishedContent = node; + if (node != null) + { + redirect = true; + LogHelper.Debug("{0}Redirecting to id={1}", () => tracePrefix, () => internalRedirectId); + } + else + { + LogHelper.Debug("{0}Failed to redirect to id={1}: no such published document", () => tracePrefix, () => internalRedirectId); + } + } + } + + return redirect; + } + + /// + /// Ensures that access to current node is permitted. + /// + /// Redirecting to a different site root and/or culture will not pick the new site root nor the new culture. + private void EnsurePublishedContentAccess() + { + const string tracePrefix = "EnsurePublishedContentAccess: "; + + if (_pcr.PublishedContent == null) + throw new InvalidOperationException("There is no PublishedContent."); + + var path = _pcr.PublishedContent.Path; + + if (Access.IsProtected(_pcr.PublishedContentId, path)) + { + LogHelper.Debug("{0}Page is protected, check for access", () => tracePrefix); + + System.Web.Security.MembershipUser user = null; + try + { + user = System.Web.Security.Membership.GetUser(); + } + catch (ArgumentException) + { + LogHelper.Debug("{0}Membership.GetUser returned ArgumentException", () => tracePrefix); + } + + if (user == null || !Member.IsLoggedOn()) + { + LogHelper.Debug("{0}Not logged in, redirect to login page", () => tracePrefix); + var loginPageId = Access.GetLoginPage(path); + if (loginPageId != _pcr.PublishedContentId) + _pcr.PublishedContent = _routingContext.PublishedContentStore.GetDocumentById(_routingContext.UmbracoContext, loginPageId); + } + else if (!Access.HasAccces(_pcr.PublishedContentId, user.ProviderUserKey)) + { + LogHelper.Debug("{0}Current member has not access, redirect to error page", () => tracePrefix); + var errorPageId = Access.GetErrorPage(path); + if (errorPageId != _pcr.PublishedContentId) + _pcr.PublishedContent = _routingContext.PublishedContentStore.GetDocumentById(_routingContext.UmbracoContext, errorPageId); + } + else + { + LogHelper.Debug("{0}Current member has access", () => tracePrefix); + } + } + else + { + LogHelper.Debug("{0}Page is not protected", () => tracePrefix); + } + } + + /// + /// Finds a template for the current node. + /// + private void FindTemplate() + { + // NOTE: at the moment there is only 1 way to find a template, and then ppl must + // use the Prepared event to change the template if they wish. Should we also + // implement an ITemplateFinder logic? + + const string tracePrefix = "FindTemplate: "; + + if (_pcr.PublishedContent == null) + throw new InvalidOperationException("There is no PublishedContent."); + + // read the alternate template alias, from querystring, form, cookie or server vars, + // only if the published content is the initial once, else the alternate template + // does not apply + string altTemplate = _pcr.IsInitialPublishedContent + ? _routingContext.UmbracoContext.HttpContext.Request["altTemplate"] + : null; + + if (string.IsNullOrWhiteSpace(altTemplate)) + { + // we don't have an alternate template specified. use the current one if there's one already, + // which can happen if a content lookup also set the template (LookupByNiceUrlAndTemplate...), + // else lookup the template id on the document then lookup the template with that id. + + if (_pcr.HasTemplate) + { + LogHelper.Debug("{0}Has a template already, and no alternate template.", () => tracePrefix); + return; + } + + // TODO: When we remove the need for a database for templates, then this id should be irrelavent, + // not sure how were going to do this nicely. + + var templateId = _pcr.PublishedContent.TemplateId; + + if (templateId > 0) + { + LogHelper.Debug("{0}Look for template id={1}", () => tracePrefix, () => templateId); + // don't use the Template ctor as the result is not cached... instead use this static method + var template = Template.GetTemplate(templateId); + if (template == null) + throw new InvalidOperationException("The template with Id " + templateId + " does not exist, the page cannot render"); + _pcr.Template = template; + LogHelper.Debug("{0}Got template id={1} alias=\"{2}\"", () => tracePrefix, () => template.Id, () => template.Alias); + } + else + { + LogHelper.Debug("{0}No specified template.", () => tracePrefix); + } + } + else + { + // we have an alternate template specified. lookup the template with that alias + // this means the we override any template that a content lookup might have set + // so /path/to/page/template1?altTemplate=template2 will use template2 + + // ignore if the alias does not match - just trace + + if (_pcr.HasTemplate) + LogHelper.Debug("{0}Has a template already, but also an alternate template.", () => tracePrefix); + LogHelper.Debug("{0}Look for alternate template alias=\"{1}\"", () => tracePrefix, () => altTemplate); + + var template = Template.GetByAlias(altTemplate, true); + if (template != null) + { + _pcr.Template = template; + LogHelper.Debug("{0}Got template id={1} alias=\"{2}\"", () => tracePrefix, () => template.Id, () => template.Alias); + } + else + { + LogHelper.Debug("{0}The template with alias=\"{1}\" does not exist, ignoring.", () => tracePrefix, () => altTemplate); + } + } + + if (!_pcr.HasTemplate) + { + LogHelper.Debug("{0}No template was found.", () => tracePrefix); + + // initial idea was: if we're not already 404 and UmbracoSettings.HandleMissingTemplateAs404 is true + // then reset _pcr.Document to null to force a 404. + // + // but: because we want to let MVC hijack routes even though no template is defined, we decide that + // a missing template is OK but the request will then be forwarded to MVC, which will need to take + // care of everything. + // + // so, don't set _pcr.Document to null here + } + else + { + LogHelper.Debug("{0}Running with template id={1} alias=\"{2}\"", () => tracePrefix, () => _pcr.Template.Id, () => _pcr.Template.Alias); + } + } + + /// + /// Follows external redirection through umbracoRedirect document property. + /// + /// As per legacy, if the redirect does not work, we just ignore it. + private void FollowExternalRedirect() + { + if (_pcr.HasPublishedContent) + { + var redirectId = _pcr.PublishedContent.GetPropertyValue("umbracoRedirect", -1); + + string redirectUrl = "#"; + if (redirectId > 0) + redirectUrl = _routingContext.NiceUrlProvider.GetNiceUrl(redirectId); + if (redirectUrl != "#") + _pcr.RedirectUrl = redirectUrl; + } + } + + #endregion + } +} diff --git a/src/Umbraco.Web/Templates/TemplateRenderer.cs b/src/Umbraco.Web/Templates/TemplateRenderer.cs index 4f8180fbc7..b12d33c890 100644 --- a/src/Umbraco.Web/Templates/TemplateRenderer.cs +++ b/src/Umbraco.Web/Templates/TemplateRenderer.cs @@ -101,9 +101,8 @@ namespace Umbraco.Web.Templates } //ok, we have a document and a template assigned, now to do some rendering. - var builder = new PublishedContentRequestBuilder(contentRequest); //determine the rendering engine - builder.DetermineRenderingEngine(); + contentRequest.Engine.FindRenderingEngine(); //First, save all of the items locally that we know are used in the chain of execution, we'll need to restore these //after this page has rendered. diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 1b3ab1d1af..4e3f0162d5 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -327,6 +327,7 @@ + @@ -416,7 +417,6 @@ - diff --git a/src/Umbraco.Web/UmbracoModule.cs b/src/Umbraco.Web/UmbracoModule.cs index 378463c1d4..2201ab176b 100644 --- a/src/Umbraco.Web/UmbracoModule.cs +++ b/src/Umbraco.Web/UmbracoModule.cs @@ -80,9 +80,9 @@ namespace Umbraco.Web return; if (UmbracoContext.Current == null) - throw new NullReferenceException("The UmbracoContext.Current is null, ProcessRequest cannot proceed unless there is a current UmbracoContext"); + throw new InvalidOperationException("The UmbracoContext.Current is null, ProcessRequest cannot proceed unless there is a current UmbracoContext"); if (UmbracoContext.Current.RoutingContext == null) - throw new NullReferenceException("The UmbracoContext.RoutingContext has not been assigned, ProcessRequest cannot proceed unless there is a RoutingContext assigned to the UmbracoContext"); + throw new InvalidOperationException("The UmbracoContext.RoutingContext has not been assigned, ProcessRequest cannot proceed unless there is a RoutingContext assigned to the UmbracoContext"); var umbracoContext = UmbracoContext.Current; @@ -101,16 +101,32 @@ namespace Umbraco.Web // ok, process - var uri = umbracoContext.OriginalRequestUrl; - - // legacy - no idea what this is but does something with the query strings + // legacy - if query contains umbPage=something then url is rewritten to /something + // because with Umbraco 3.x on .NET 2.x the form auth url would be something like + // login.aspx?ReturnUrl=default.aspx&umbPage=solutions.aspx + // and then the next request would be default.aspx?umbPage=solutions.aspx + // instead of being solutions.aspx -- are we still impacted by that one now that + // we rewrite the urls properly internally? I guess not? + // there's a 2008 discussion here http://forum.umbraco.org/yaf_postst5377_FormsAuthenticationRedirectToLoginPage-problem.aspx + var uri = umbracoContext.CleanedUmbracoUrl; LegacyCleanUmbPageFromQueryString(ref uri); - // instanciate a request a process + // instanciate, prepare and process the published content request // important to use CleanedUmbracoUrl - lowercase path-only version of the current url - var docreq = new PublishedContentRequest(umbracoContext.CleanedUmbracoUrl, umbracoContext.RoutingContext); - docreq.ProcessRequest(httpContext, umbracoContext, - docreq2 => RewriteToUmbracoHandler(HttpContext.Current, uri.Query, docreq2.RenderingEngine)); + var pcr = new PublishedContentRequest(uri /*umbracoContext.CleanedUmbracoUrl*/, umbracoContext.RoutingContext); + umbracoContext.PublishedContentRequest = pcr; + pcr.Prepare(); + if (pcr.IsRedirect) + { + httpContext.Response.Redirect(pcr.RedirectUrl, true); + return; + } + if (pcr.Is404) + httpContext.Response.StatusCode = 404; + if (!pcr.HasPublishedContent) + httpContext.RemapHandler(new PublishedContentNotFoundHandler()); + else + RewriteToUmbracoHandler(httpContext, pcr); } /// @@ -307,22 +323,23 @@ namespace Umbraco.Web /// /// /// - private void RewriteToUmbracoHandler(HttpContext context, string currentQuery, RenderingEngine engine) + private void RewriteToUmbracoHandler(HttpContextBase context, PublishedContentRequest pcr) { + // NOTE: we do not want to use TransferRequest even though many docs say it is better with IIS7, turns out this is + // not what we need. The purpose of TransferRequest is to ensure that .net processes all of the rules for the newly + // rewritten url, but this is not what we want! + // read: http://forums.iis.net/t/1146511.aspx - //NOTE: We do not want to use TransferRequest even though many docs say it is better with IIS7, turns out this is - //not what we need. The purpose of TransferRequest is to ensure that .net processes all of the rules for the newly - //rewritten url, but this is not what we want! - // http://forums.iis.net/t/1146511.aspx + string query = pcr.Uri.Query.TrimStart(new[] { '?' }); string rewritePath; - switch (engine) + switch (pcr.RenderingEngine) { case RenderingEngine.Mvc: // GlobalSettings.Path has already been through IOHelper.ResolveUrl() so it begins with / and vdir (if any) rewritePath = GlobalSettings.Path.TrimEnd(new[] { '/' }) + "/RenderMvc"; - // we rewrite the path to the path of the handler (i.e. default.aspx or /umbraco/RenderMvc ) - context.RewritePath(rewritePath, "", currentQuery.TrimStart(new[] { '?' }), false); + // rewrite the path to the path of the handler (i.e. /umbraco/RenderMvc) + context.RewritePath(rewritePath, "", query, false); //if it is MVC we need to do something special, we are not using TransferRequest as this will //require us to rewrite the path with query strings and then reparse the query strings, this would @@ -333,17 +350,15 @@ namespace Umbraco.Web //we also cannot re-create this functionality because the setter for the HttpContext.Request.RequestContext is internal //so really, this is pretty much the only way without using Server.TransferRequest and if we did that, we'd have to rethink //a bunch of things! - var urlRouting = new UrlRoutingModule(); - urlRouting.PostResolveRequestCache(new HttpContextWrapper(context)); - + urlRouting.PostResolveRequestCache(context); break; + case RenderingEngine.WebForms: default: rewritePath = "~/default.aspx"; - // rewrite the path to the path of the handler (i.e. default.aspx or /umbraco/RenderMvc ) - context.RewritePath(rewritePath, "", currentQuery.TrimStart(new[] { '?' }), false); - + // rewrite the path to the path of the handler (i.e. default.aspx) + context.RewritePath(rewritePath, "", query, false); break; } diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/uQuery/uQuery-Nodes.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/uQuery/uQuery-Nodes.cs index 6139abefd3..be350419de 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/uQuery/uQuery-Nodes.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/uQuery/uQuery-Nodes.cs @@ -209,11 +209,11 @@ namespace umbraco uri = uri.MakeAbsolute(Umbraco.Web.UmbracoContext.Current.CleanedUmbracoUrl); uri = Umbraco.Web.UriUtility.UriToUmbraco(uri); - var docreq = new Umbraco.Web.Routing.PublishedContentRequest(uri, Umbraco.Web.UmbracoContext.Current.RoutingContext); - var builder = new Umbraco.Web.Routing.PublishedContentRequestBuilder(docreq); - builder.LookupDomain(); - builder.LookupDocument1(); // will _not_ follow redirects, handle 404, nothing - just run lookups - return docreq.HasPublishedContent ? docreq.PublishedContentId : uQuery.RootNodeId; + var pcr = new Umbraco.Web.Routing.PublishedContentRequest(uri, Umbraco.Web.UmbracoContext.Current.RoutingContext); + // partially prepare the request: do _not_ follow redirects, handle 404, nothing - just find a content + pcr.Engine.FindDomain(); + pcr.Engine.FindPublishedContent(); + return pcr.HasPublishedContent ? pcr.PublishedContentId : uQuery.RootNodeId; } ///