2016-10-11 18:52:01 +02:00
using System ;
using System.Collections.Generic ;
using System.Linq ;
using System.Threading ;
using System.Globalization ;
using System.IO ;
2016-10-18 16:16:46 +02:00
using System.Web.Security ;
2016-10-11 18:52:01 +02:00
using umbraco ;
using Umbraco.Core ;
using Umbraco.Core.Configuration.UmbracoSettings ;
using Umbraco.Core.IO ;
using Umbraco.Core.Logging ;
using Umbraco.Core.Models ;
2017-05-30 10:50:09 +02:00
using Umbraco.Core.Models.PublishedContent ;
2016-10-11 18:52:01 +02:00
using Umbraco.Core.Services ;
using Umbraco.Web.Security ;
using RenderingEngine = Umbraco . Core . RenderingEngine ;
namespace Umbraco.Web.Routing
{
2017-10-31 12:48:24 +01:00
// fixme - make this public
// fixme - making sense to have an interface?
internal class PublishedRouter
2017-07-20 11:21:28 +02:00
{
private readonly IWebRoutingSection _webRoutingSection ;
2016-10-13 21:08:07 +02:00
private readonly ContentFinderCollection _contentFinders ;
2017-07-20 11:21:28 +02:00
private readonly IContentLastChanceFinder _contentLastChanceFinder ;
2016-10-11 18:52:01 +02:00
private readonly ServiceContext _services ;
private readonly ProfilingLogger _profilingLogger ;
2018-06-03 17:21:15 +02:00
private readonly IVariationContextAccessor _variationContextAccessor ;
2017-07-20 11:21:28 +02:00
private readonly ILogger _logger ;
2016-10-11 18:52:01 +02:00
/// <summary>
2017-10-31 12:48:24 +01:00
/// Initializes a new instance of the <see cref="PublishedRouter"/> class.
2016-10-11 18:52:01 +02:00
/// </summary>
2017-10-31 12:48:24 +01:00
public PublishedRouter (
2016-10-11 18:52:01 +02:00
IWebRoutingSection webRoutingSection ,
2016-10-13 21:08:07 +02:00
ContentFinderCollection contentFinders ,
2016-10-11 18:52:01 +02:00
IContentLastChanceFinder contentLastChanceFinder ,
2018-06-03 17:21:15 +02:00
IVariationContextAccessor variationContextAccessor ,
2016-10-11 18:52:01 +02:00
ServiceContext services ,
2016-10-18 16:16:46 +02:00
ProfilingLogger proflog ,
Func < string , IEnumerable < string > > getRolesForLogin = null )
2017-07-20 11:21:28 +02:00
{
2017-10-31 12:48:24 +01:00
_webRoutingSection = webRoutingSection ? ? throw new ArgumentNullException ( nameof ( webRoutingSection ) ) ; // fixme usage?
2017-05-12 14:49:44 +02:00
_contentFinders = contentFinders ? ? throw new ArgumentNullException ( nameof ( contentFinders ) ) ;
_contentLastChanceFinder = contentLastChanceFinder ? ? throw new ArgumentNullException ( nameof ( contentLastChanceFinder ) ) ;
_services = services ? ? throw new ArgumentNullException ( nameof ( services ) ) ;
_profilingLogger = proflog ? ? throw new ArgumentNullException ( nameof ( proflog ) ) ;
2018-06-03 17:21:15 +02:00
_variationContextAccessor = variationContextAccessor ? ? throw new ArgumentNullException ( nameof ( variationContextAccessor ) ) ;
2017-07-20 11:21:28 +02:00
_logger = proflog . Logger ;
2016-10-18 16:16:46 +02:00
2017-07-20 11:21:28 +02:00
GetRolesForLogin = getRolesForLogin ? ? ( s = > Roles . Provider . GetRolesForUser ( s ) ) ;
}
2016-10-11 18:52:01 +02:00
2017-09-19 15:51:47 +02:00
// fixme
// in 7.7 this is cached in the PublishedContentRequest, which ... makes little sense
// killing it entirely, if we need cache, just implement it properly !!
// this is all soooo weird
public Func < string , IEnumerable < string > > GetRolesForLogin { get ; }
2016-10-11 18:52:01 +02:00
2017-10-31 12:50:30 +01:00
public PublishedRequest CreateRequest ( UmbracoContext umbracoContext , Uri uri = null )
2017-07-20 11:21:28 +02:00
{
2017-10-31 12:50:30 +01:00
return new PublishedRequest ( this , umbracoContext , uri ? ? umbracoContext . CleanedUmbracoUrl ) ;
2017-07-20 11:21:28 +02:00
}
2016-10-11 18:52:01 +02:00
2017-05-12 14:49:44 +02:00
#region Request
2016-10-11 18:52:01 +02:00
2017-07-20 11:21:28 +02:00
/// <summary>
/// Tries to route the request.
/// </summary>
2017-10-31 12:50:30 +01:00
internal bool TryRouteRequest ( PublishedRequest request )
2017-07-20 11:21:28 +02:00
{
// disabled - is it going to change the routing?
//_pcr.OnPreparing();
2017-05-12 14:49:44 +02:00
2017-07-20 11:21:28 +02:00
FindDomain ( request ) ;
if ( request . IsRedirect ) return false ;
if ( request . HasPublishedContent ) return true ;
FindPublishedContent ( request ) ;
if ( request . IsRedirect ) return false ;
2017-05-12 14:49:44 +02:00
2017-07-20 11:21:28 +02:00
// don't handle anything - we just want to ensure that we find the content
//HandlePublishedContent();
//FindTemplate();
//FollowExternalRedirect();
//HandleWildcardDomains();
2017-05-12 14:49:44 +02:00
2017-07-20 11:21:28 +02:00
// disabled - we just want to ensure that we find the content
//_pcr.OnPrepared();
2017-05-12 14:49:44 +02:00
2017-07-20 11:21:28 +02:00
return request . HasPublishedContent ;
}
2017-05-30 10:50:09 +02:00
2018-06-03 17:21:15 +02:00
private void SetVariationContext ( string culture )
{
var variationContext = _variationContextAccessor . VariationContext ;
if ( variationContext ! = null & & variationContext . Culture = = culture ) return ;
_variationContextAccessor . VariationContext = new VariationContext ( culture ) ;
}
2017-05-12 14:49:44 +02:00
/// <summary>
/// Prepares the request.
/// </summary>
/// <returns>
/// Returns false if the request was not successfully prepared
/// </returns>
2017-10-31 12:50:30 +01:00
public bool PrepareRequest ( PublishedRequest request )
2017-07-20 11:21:28 +02:00
{
2016-10-11 18:52:01 +02:00
// 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.
// trigger the Preparing event - at that point anything can still be changed
// the idea is that it is possible to change the uri
//
request . OnPreparing ( ) ;
2017-07-20 11:21:28 +02:00
//find domain
FindDomain ( request ) ;
2016-10-11 18:52:01 +02:00
2017-07-20 11:21:28 +02:00
// if request has been flagged to redirect then return
// whoever called us is in charge of actually redirecting
if ( request . IsRedirect )
{
return false ;
}
2016-10-11 18:52:01 +02:00
2017-07-20 11:21:28 +02:00
// set the culture on the thread - once, so it's set when running document lookups
Thread . CurrentThread . CurrentUICulture = Thread . CurrentThread . CurrentCulture = request . Culture ;
2018-06-03 17:21:15 +02:00
SetVariationContext ( request . Culture . Name ) ;
2016-10-11 18:52:01 +02:00
//find the published content if it's not assigned. This could be manually assigned with a custom route handler, or
// with something like EnsurePublishedContentRequestAttribute or UmbracoVirtualNodeRouteHandler. Those in turn call this method
// to setup the rest of the pipeline but we don't want to run the finders since there's one assigned.
2017-07-20 11:21:28 +02:00
if ( request . PublishedContent = = null )
{
2016-10-11 18:52:01 +02:00
// find the document & template
FindPublishedContentAndTemplate ( request ) ;
2017-07-20 11:21:28 +02:00
}
2016-10-11 18:52:01 +02:00
// handle wildcard domains
HandleWildcardDomains ( request ) ;
// set the culture on the thread -- again, 'cos it might have changed due to a finder or wildcard domain
Thread . CurrentThread . CurrentUICulture = Thread . CurrentThread . CurrentCulture = request . Culture ;
2018-06-03 17:21:15 +02:00
SetVariationContext ( request . Culture . Name ) ;
2016-10-11 18:52:01 +02:00
// trigger the Prepared event - at that point it is still possible to change about anything
// even though the request might be flagged for redirection - we'll redirect _after_ the event
//
// also, OnPrepared() will make the PublishedContentRequest readonly, so nothing can change
//
request . OnPrepared ( ) ;
// we don't take care of anything so if the content has changed, it's up to the user
// to find out the appropriate template
//complete the PCR and assign the remaining values
2017-07-20 11:21:28 +02:00
return ConfigureRequest ( request ) ;
}
2016-10-11 18:52:01 +02:00
/// <summary>
/// Called by PrepareRequest once everything has been discovered, resolved and assigned to the PCR. This method
/// finalizes the PCR with the values assigned.
/// </summary>
/// <returns>
/// Returns false if the request was not successfully configured
/// </returns>
/// <remarks>
/// This method logic has been put into it's own method in case developers have created a custom PCR or are assigning their own values
/// but need to finalize it themselves.
/// </remarks>
2017-10-31 12:50:30 +01:00
public bool ConfigureRequest ( PublishedRequest frequest )
2016-10-11 18:52:01 +02:00
{
if ( frequest . HasPublishedContent = = false )
{
return false ;
}
// set the culture on the thread -- again, 'cos it might have changed in the event handler
Thread . CurrentThread . CurrentUICulture = Thread . CurrentThread . CurrentCulture = frequest . Culture ;
2018-06-03 17:21:15 +02:00
SetVariationContext ( frequest . Culture . Name ) ;
2016-10-11 18:52:01 +02:00
// if request has been flagged to redirect, or has no content to display,
// then return - whoever called us is in charge of actually redirecting
if ( frequest . IsRedirect | | frequest . HasPublishedContent = = false )
{
return false ;
}
// we may be 404 _and_ have a content
// 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
// note - the page() ctor below will cause the "page" to get the value of all its
// "elements" ie of all the IPublishedContent property. If we use the object value,
// that will trigger macro execution - which can't happen because macro execution
// requires that _pcr.UmbracoPage is already initialized = catch-22. The "legacy"
// pipeline did _not_ evaluate the macros, ie it is using the data value, and we
// have to keep doing it because of that catch-22.
// assign the legacy page back to the request
// handlers like default.aspx will want it and most macros currently need it
frequest . UmbracoPage = new page ( frequest ) ;
// used by many legacy objects
frequest . UmbracoContext . HttpContext . Items [ "pageID" ] = frequest . PublishedContent . Id ;
frequest . UmbracoContext . HttpContext . Items [ "pageElements" ] = frequest . UmbracoPage . Elements ;
return true ;
}
2017-07-20 11:21:28 +02:00
/// <summary>
/// Updates the request when there is no template to render the content.
/// </summary>
/// <remarks>This is called from Mvc when there's a document to render but no template.</remarks>
2017-10-31 12:50:30 +01:00
public void UpdateRequestOnMissingTemplate ( PublishedRequest request )
2017-07-20 11:21:28 +02:00
{
// clear content
var content = request . PublishedContent ;
2016-10-11 18:52:01 +02:00
request . PublishedContent = null ;
2017-07-20 11:21:28 +02:00
HandlePublishedContent ( request ) ; // will go 404
FindTemplate ( request ) ;
2016-10-11 18:52:01 +02:00
2017-07-20 11:21:28 +02:00
// if request has been flagged to redirect then return
// whoever called us is in charge of redirecting
if ( request . IsRedirect )
return ;
2016-10-11 18:52:01 +02:00
2017-07-20 11:21:28 +02:00
if ( request . HasPublishedContent = = false )
{
2016-10-11 18:52:01 +02:00
// means the engine could not find a proper document to handle 404
// restore the saved content so we know it exists
request . PublishedContent = content ;
2017-07-20 11:21:28 +02:00
return ;
}
2016-10-11 18:52:01 +02:00
2017-07-20 11:21:28 +02:00
if ( request . HasTemplate = = false )
{
// 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 ;
}
2016-10-11 18:52:01 +02:00
// see note in PrepareRequest()
2017-07-20 11:21:28 +02:00
// assign the legacy page back to the docrequest
// handlers like default.aspx will want it and most macros currently need it
request . UmbracoPage = new page ( request ) ;
2016-10-11 18:52:01 +02:00
// these two are used by many legacy objects
request . UmbracoContext . HttpContext . Items [ "pageID" ] = request . PublishedContent . Id ;
request . UmbracoContext . HttpContext . Items [ "pageElements" ] = request . UmbracoPage . Elements ;
2017-07-20 11:21:28 +02:00
}
2016-10-11 18:52:01 +02:00
2017-07-20 11:21:28 +02:00
#endregion
2016-10-11 18:52:01 +02:00
2017-07-20 11:21:28 +02:00
#region Domain
2016-10-11 18:52:01 +02:00
2017-07-20 11:21:28 +02:00
/// <summary>
/// Finds the site root (if any) matching the http request, and updates the PublishedContentRequest accordingly.
/// </summary>
/// <returns>A value indicating whether a domain was found.</returns>
2017-10-31 12:50:30 +01:00
internal bool FindDomain ( PublishedRequest request )
2017-07-20 11:21:28 +02:00
{
const string tracePrefix = "FindDomain: " ;
2016-10-11 18:52:01 +02:00
2017-07-20 11:21:28 +02:00
// note - we are not handling schemes nor ports here.
2016-10-11 18:52:01 +02:00
2018-04-03 16:15:59 +02:00
_logger . Debug < PublishedRouter > ( ( ) = > $"{tracePrefix}Uri=\" { request . Uri } \ "" ) ;
2016-10-11 18:52:01 +02:00
2018-04-26 16:03:08 +02:00
var domainsCache = request . UmbracoContext . PublishedSnapshot . Domains ;
2018-05-08 11:21:14 +10:00
var domains = domainsCache . GetAll ( includeWildcards : false ) . ToList ( ) ;
2018-05-08 11:06:07 +02:00
// determines whether a domain corresponds to a published document, since some
// domains may exist but on a document that has been unpublished - as a whole - or
// that is not published for the domain's culture - in which case the domain does
// not apply
bool IsPublishedContentDomain ( Domain domain )
2018-05-08 11:21:14 +10:00
{
2018-05-08 11:06:07 +02:00
// just get it from content cache - optimize there, not here
var domainDocument = request . UmbracoContext . PublishedSnapshot . Content . GetById ( domain . ContentId ) ;
// not published - at all
if ( domainDocument = = null )
2018-05-08 11:21:14 +10:00
return false ;
2018-05-08 11:06:07 +02:00
// invariant - always published
if ( ! domainDocument . ContentType . Variations . Has ( ContentVariation . CultureNeutral ) )
2018-05-08 11:21:14 +10:00
return true ;
2018-05-08 11:06:07 +02:00
// variant, ensure that the culture corresponding to the domain's language is published
return domainDocument . Cultures . ContainsKey ( domain . Culture . Name ) ;
}
domains = domains . Where ( IsPublishedContentDomain ) . ToList ( ) ;
2018-05-02 14:52:00 +10:00
2018-04-26 16:03:08 +02:00
var defaultCulture = domainsCache . DefaultCulture ;
2017-07-20 11:21:28 +02:00
// try to find a domain matching the current request
2018-04-26 16:03:08 +02:00
var domainAndUri = DomainHelper . SelectDomain ( domains , request . Uri , defaultCulture : defaultCulture ) ;
2016-10-11 18:52:01 +02:00
2017-07-20 11:21:28 +02:00
// handle domain - always has a contentId and a culture
if ( domainAndUri ! = null )
{
2016-10-11 18:52:01 +02:00
// matching an existing domain
2018-04-03 16:15:59 +02:00
_logger . Debug < PublishedRouter > ( ( ) = > $"{tracePrefix}Matches domain=\" { domainAndUri . Name } \ ", rootId={domainAndUri.ContentId}, culture=\"{domainAndUri.Culture}\"" ) ;
2016-10-11 18:52:01 +02:00
request . Domain = domainAndUri ;
request . Culture = domainAndUri . Culture ;
// canonical? not implemented at the moment
// if (...)
// {
// _pcr.RedirectUrl = "...";
// return true;
// }
}
else
2017-07-20 11:21:28 +02:00
{
// not matching any existing domain
2018-04-03 16:15:59 +02:00
_logger . Debug < PublishedRouter > ( ( ) = > $"{tracePrefix}Matches no domain" ) ;
2016-10-11 18:52:01 +02:00
2018-04-26 16:03:08 +02:00
request . Culture = defaultCulture = = null ? CultureInfo . CurrentUICulture : new CultureInfo ( defaultCulture ) ;
2017-07-20 11:21:28 +02:00
}
2016-10-11 18:52:01 +02:00
2018-04-03 16:15:59 +02:00
_logger . Debug < PublishedRouter > ( ( ) = > $"{tracePrefix}Culture=\" { request . Culture . Name } \ "" ) ;
2016-10-11 18:52:01 +02:00
2017-07-20 11:21:28 +02:00
return request . Domain ! = null ;
}
2016-10-11 18:52:01 +02:00
2017-07-20 11:21:28 +02:00
/// <summary>
/// Looks for wildcard domains in the path and updates <c>Culture</c> accordingly.
/// </summary>
2017-10-31 12:50:30 +01:00
internal void HandleWildcardDomains ( PublishedRequest request )
2017-07-20 11:21:28 +02:00
{
const string tracePrefix = "HandleWildcardDomains: " ;
2016-10-11 18:52:01 +02:00
2017-07-20 11:21:28 +02:00
if ( request . HasPublishedContent = = false )
return ;
2016-10-11 18:52:01 +02:00
2017-07-20 11:21:28 +02:00
var nodePath = request . PublishedContent . Path ;
2018-04-03 16:15:59 +02:00
_logger . Debug < PublishedRouter > ( ( ) = > $"{tracePrefix}Path=\" { nodePath } \ "" ) ;
2016-10-11 18:52:01 +02:00
var rootNodeId = request . HasDomain ? request . Domain . ContentId : ( int? ) null ;
2018-04-27 11:38:50 +10:00
var domain = DomainHelper . FindWildcardDomainInPath ( request . UmbracoContext . PublishedSnapshot . Domains . GetAll ( true ) , nodePath , rootNodeId ) ;
2016-10-11 18:52:01 +02:00
// always has a contentId and a culture
2017-07-20 11:21:28 +02:00
if ( domain ! = null )
{
2016-10-11 18:52:01 +02:00
request . Culture = domain . Culture ;
2018-04-03 16:15:59 +02:00
_logger . Debug < PublishedRouter > ( ( ) = > $"{tracePrefix}Got domain on node {domain.ContentId}, set culture to \" { request . Culture . Name } \ "." ) ;
2016-10-11 18:52:01 +02:00
}
2017-07-20 11:21:28 +02:00
else
{
2018-04-03 16:15:59 +02:00
_logger . Debug < PublishedRouter > ( ( ) = > $"{tracePrefix}No match." ) ;
2017-07-20 11:21:28 +02:00
}
}
2016-10-11 18:52:01 +02:00
2017-07-20 11:21:28 +02:00
#endregion
2016-10-11 18:52:01 +02:00
2017-07-20 11:21:28 +02:00
#region Rendering engine
2016-10-11 18:52:01 +02:00
/// <summary>
/// Finds the rendering engine to use to render a template specified by its alias.
/// </summary>
/// <param name="alias">The alias of the template.</param>
/// <returns>The rendering engine, or Unknown if the template was not found.</returns>
internal RenderingEngine FindTemplateRenderingEngine ( string alias )
{
if ( string . IsNullOrWhiteSpace ( alias ) )
return RenderingEngine . Unknown ;
alias = alias . Replace ( '\\' , '/' ) ; // forward slashes only
// NOTE: we could start with what's the current default?
// fixme - bad - we probably should be using the appropriate filesystems!
if ( FindTemplateRenderingEngineInDirectory ( new DirectoryInfo ( IOHelper . MapPath ( SystemDirectories . MvcViews ) ) ,
alias , new [ ] { ".cshtml" , ".vbhtml" } ) )
return RenderingEngine . Mvc ;
if ( FindTemplateRenderingEngineInDirectory ( new DirectoryInfo ( IOHelper . MapPath ( SystemDirectories . Masterpages ) ) ,
alias , new [ ] { ".master" } ) )
return RenderingEngine . WebForms ;
return RenderingEngine . Unknown ;
}
internal bool FindTemplateRenderingEngineInDirectory ( DirectoryInfo directory , string alias , string [ ] extensions )
{
if ( directory = = null | | directory . Exists = = false )
return false ;
var pos = alias . IndexOf ( '/' ) ;
if ( pos > 0 )
{
// recurse
var subdir = directory . GetDirectories ( alias . Substring ( 0 , pos ) ) . FirstOrDefault ( ) ;
alias = alias . Substring ( pos + 1 ) ;
return subdir ! = null & & FindTemplateRenderingEngineInDirectory ( subdir , alias , extensions ) ;
}
// look here
return directory . GetFiles ( ) . Any ( f = > extensions . Any ( e = > f . Name . InvariantEquals ( alias + e ) ) ) ;
}
2017-07-20 11:21:28 +02:00
#endregion
2016-10-11 18:52:01 +02:00
2017-07-20 11:21:28 +02:00
#region Document and template
2016-10-11 18:52:01 +02:00
/// <summary>
/// Gets a template.
/// </summary>
/// <param name="alias">The template alias</param>
/// <returns>The template.</returns>
2017-07-20 11:21:28 +02:00
public ITemplate GetTemplate ( string alias )
{
return _services . FileService . GetTemplate ( alias ) ;
}
/// <summary>
/// Finds the Umbraco document (if any) matching the request, and updates the PublishedContentRequest accordingly.
/// </summary>
/// <returns>A value indicating whether a document and template were found.</returns>
2017-10-31 12:50:30 +01:00
private void FindPublishedContentAndTemplate ( PublishedRequest request )
2017-07-20 11:21:28 +02:00
{
const string tracePrefix = "FindPublishedContentAndTemplate: " ;
2018-04-03 16:15:59 +02:00
_logger . Debug < PublishedRouter > ( ( ) = > $"{tracePrefix}Path=\" { request . Uri . AbsolutePath } \ "" ) ;
2017-07-20 11:21:28 +02:00
// run the document finders
FindPublishedContent ( request ) ;
2016-10-11 18:52:01 +02:00
// if request has been flagged to redirect then return
// whoever called us is in charge of actually redirecting
// -- do not process anything any further --
if ( request . IsRedirect )
2017-07-20 11:21:28 +02:00
return ;
2016-10-11 18:52:01 +02:00
2017-07-20 11:21:28 +02:00
// not handling umbracoRedirect here but after LookupDocument2
// so internal redirect, 404, etc has precedence over redirect
2016-10-11 18:52:01 +02:00
2017-07-20 11:21:28 +02:00
// handle not-found, redirects, access...
HandlePublishedContent ( request ) ;
2016-10-11 18:52:01 +02:00
2017-07-20 11:21:28 +02:00
// find a template
FindTemplate ( request ) ;
2016-10-11 18:52:01 +02:00
2017-07-20 11:21:28 +02:00
// handle umbracoRedirect
FollowExternalRedirect ( request ) ;
}
2016-10-11 18:52:01 +02:00
2017-07-20 11:21:28 +02:00
/// <summary>
/// Tries to find the document matching the request, by running the IPublishedContentFinder instances.
/// </summary>
2016-10-11 18:52:01 +02:00
/// <exception cref="InvalidOperationException">There is no finder collection.</exception>
2017-10-31 12:50:30 +01:00
internal void FindPublishedContent ( PublishedRequest request )
2017-07-20 11:21:28 +02:00
{
const string tracePrefix = "FindPublishedContent: " ;
2016-10-11 18:52:01 +02:00
2017-07-20 11:21:28 +02:00
// 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
2016-10-11 18:52:01 +02:00
2017-10-31 12:48:24 +01:00
using ( _profilingLogger . DebugDuration < PublishedRouter > (
2016-10-11 18:52:01 +02:00
$"{tracePrefix}Begin finders" ,
$"{tracePrefix}End finders, {(request.HasPublishedContent ? " a document was found " : " no document was found ")}" ) )
2017-07-20 11:21:28 +02:00
{
2016-10-11 18:52:01 +02:00
//iterate but return on first one that finds it
2017-07-20 11:21:28 +02:00
var found = _contentFinders . Any ( finder = >
{
2017-10-31 12:48:24 +01:00
_logger . Debug < PublishedRouter > ( "Finder " + finder . GetType ( ) . FullName ) ;
2017-07-20 11:21:28 +02:00
return finder . TryFindContent ( request ) ;
} ) ;
}
2016-10-11 18:52:01 +02:00
// 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.
request . SetIsInitialPublishedContent ( ) ;
2017-07-20 11:21:28 +02:00
}
/// <summary>
/// Handles the published content (if any).
/// </summary>
/// <remarks>
/// Handles "not found", internal redirects, access validation...
/// things that must be handled in one place because they can create loops
/// </remarks>
2017-10-31 12:50:30 +01:00
private void HandlePublishedContent ( PublishedRequest request )
2017-07-20 11:21:28 +02:00
{
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
{
2018-04-03 16:15:59 +02:00
_logger . Debug < PublishedRouter > ( ( ) = > $"{tracePrefix}{(i == 0 ? " Begin " : " Loop ")}" ) ;
2017-07-20 11:21:28 +02:00
// handle not found
if ( request . HasPublishedContent = = false )
{
2016-10-11 18:52:01 +02:00
request . Is404 = true ;
2018-04-03 16:15:59 +02:00
_logger . Debug < PublishedRouter > ( ( ) = > $"{tracePrefix}No document, try last chance lookup" ) ;
2017-07-20 11:21:28 +02:00
// if it fails then give up, there isn't much more that we can do
if ( _contentLastChanceFinder . TryFindContent ( request ) = = false )
{
2018-04-03 16:15:59 +02:00
_logger . Debug < PublishedRouter > ( ( ) = > $"{tracePrefix}Failed to find a document, give up" ) ;
2017-07-20 11:21:28 +02:00
break ;
}
2018-04-03 16:15:59 +02:00
_logger . Debug < PublishedRouter > ( ( ) = > $"{tracePrefix}Found a document" ) ;
2017-07-20 11:21:28 +02:00
}
// follow internal redirects as long as it's not running out of control ie infinite loop of some sort
j = 0 ;
while ( FollowInternalRedirects ( request ) & & j + + < maxLoop )
{ }
if ( j = = maxLoop ) // we're running out of control
break ;
// ensure access
if ( request . HasPublishedContent )
EnsurePublishedContentAccess ( request ) ;
// 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 ( request . HasPublishedContent = = false & & i + + < maxLoop ) ;
if ( i = = maxLoop | | j = = maxLoop )
{
2018-04-03 16:15:59 +02:00
_logger . Debug < PublishedRouter > ( ( ) = > $"{tracePrefix}Looks like we're running into an infinite loop, abort" ) ;
2016-10-11 18:52:01 +02:00
request . PublishedContent = null ;
2017-07-20 11:21:28 +02:00
}
2018-04-03 16:15:59 +02:00
_logger . Debug < PublishedRouter > ( ( ) = > $"{tracePrefix}End" ) ;
2017-07-20 11:21:28 +02:00
}
/// <summary>
/// Follows internal redirections through the <c>umbracoInternalRedirectId</c> document property.
/// </summary>
/// <returns>A value indicating whether redirection took place and led to a new published document.</returns>
/// <remarks>
/// <para>Redirecting to a different site root and/or culture will not pick the new site root nor the new culture.</para>
/// <para>As per legacy, if the redirect does not work, we just ignore it.</para>
/// </remarks>
2017-10-31 12:50:30 +01:00
private bool FollowInternalRedirects ( PublishedRequest request )
2017-07-20 11:21:28 +02:00
{
const string tracePrefix = "FollowInternalRedirects: " ;
if ( request . PublishedContent = = null )
throw new InvalidOperationException ( "There is no PublishedContent." ) ;
// don't try to find a redirect if the property doesn't exist
if ( request . PublishedContent . HasProperty ( Constants . Conventions . Content . InternalRedirectId ) = = false )
return false ;
2016-10-11 18:52:01 +02:00
2017-05-30 10:50:09 +02:00
var redirect = false ;
2017-07-20 11:21:28 +02:00
var valid = false ;
IPublishedContent internalRedirectNode = null ;
2018-04-18 19:46:47 +02:00
var internalRedirectId = request . PublishedContent . Value ( Constants . Conventions . Content . InternalRedirectId , defaultValue : - 1 ) ;
2016-10-11 18:52:01 +02:00
2017-07-20 11:21:28 +02:00
if ( internalRedirectId > 0 )
{
// try and get the redirect node from a legacy integer ID
valid = true ;
internalRedirectNode = request . UmbracoContext . ContentCache . GetById ( internalRedirectId ) ;
}
else
{
var udiInternalRedirectId = request . PublishedContent . Value < GuidUdi > ( Constants . Conventions . Content . InternalRedirectId ) ;
if ( udiInternalRedirectId ! = null )
{
// try and get the redirect node from a UDI Guid
valid = true ;
internalRedirectNode = request . UmbracoContext . ContentCache . GetById ( udiInternalRedirectId . Guid ) ;
}
}
if ( valid = = false )
{
// bad redirect - log and display the current page (legacy behavior)
2017-12-06 11:51:35 +01:00
_logger . Debug < PublishedRouter > ( $"{tracePrefix}Failed to redirect to id={request.PublishedContent.GetProperty(Constants.Conventions.Content.InternalRedirectId).GetSourceValue()}: value is not an int nor a GuidUdi." ) ;
2017-05-30 10:50:09 +02:00
}
if ( internalRedirectNode = = null )
{
2017-12-06 11:51:35 +01:00
_logger . Debug < PublishedRouter > ( $"{tracePrefix}Failed to redirect to id={request.PublishedContent.GetProperty(Constants.Conventions.Content.InternalRedirectId).GetSourceValue()}: no such published document." ) ;
2017-05-30 10:50:09 +02:00
}
2017-07-20 11:21:28 +02:00
else if ( internalRedirectId = = request . PublishedContent . Id )
{
// redirect to self
2017-10-31 12:48:24 +01:00
_logger . Debug < PublishedRouter > ( $"{tracePrefix}Redirecting to self, ignore" ) ;
2017-07-20 11:21:28 +02:00
}
else
{
2017-05-30 10:50:09 +02:00
request . SetInternalRedirectPublishedContent ( internalRedirectNode ) ; // don't use .PublishedContent here
2017-07-20 11:21:28 +02:00
redirect = true ;
2017-10-31 12:48:24 +01:00
_logger . Debug < PublishedRouter > ( $"{tracePrefix}Redirecting to id={internalRedirectId}" ) ;
2017-07-20 11:21:28 +02:00
}
2016-10-11 18:52:01 +02:00
2017-07-20 11:21:28 +02:00
return redirect ;
}
2016-10-11 18:52:01 +02:00
2017-07-20 11:21:28 +02:00
/// <summary>
/// Ensures that access to current node is permitted.
/// </summary>
/// <remarks>Redirecting to a different site root and/or culture will not pick the new site root nor the new culture.</remarks>
2017-10-31 12:50:30 +01:00
private void EnsurePublishedContentAccess ( PublishedRequest request )
2017-07-20 11:21:28 +02:00
{
const string tracePrefix = "EnsurePublishedContentAccess: " ;
2016-10-11 18:52:01 +02:00
2017-07-20 11:21:28 +02:00
if ( request . PublishedContent = = null )
throw new InvalidOperationException ( "There is no PublishedContent." ) ;
2016-10-11 18:52:01 +02:00
2017-07-20 11:21:28 +02:00
var path = request . PublishedContent . Path ;
2016-10-11 18:52:01 +02:00
2017-07-20 11:21:28 +02:00
var publicAccessAttempt = _services . PublicAccessService . IsProtected ( path ) ;
2016-10-11 18:52:01 +02:00
if ( publicAccessAttempt )
2017-07-20 11:21:28 +02:00
{
2018-04-03 16:15:59 +02:00
_logger . Debug < PublishedRouter > ( ( ) = > $"{tracePrefix}Page is protected, check for access" ) ;
2016-10-11 18:52:01 +02:00
2017-07-20 11:21:28 +02:00
var membershipHelper = new MembershipHelper ( request . UmbracoContext ) ;
2016-10-11 18:52:01 +02:00
2017-07-20 11:21:28 +02:00
if ( membershipHelper . IsLoggedIn ( ) = = false )
{
2018-04-03 16:15:59 +02:00
_logger . Debug < PublishedRouter > ( ( ) = > $"{tracePrefix}Not logged in, redirect to login page" ) ;
2016-10-11 18:52:01 +02:00
var loginPageId = publicAccessAttempt . Result . LoginNodeId ;
2017-07-20 11:21:28 +02:00
if ( loginPageId ! = request . PublishedContent . Id )
2018-04-27 11:38:50 +10:00
request . PublishedContent = request . UmbracoContext . PublishedSnapshot . Content . GetById ( loginPageId ) ;
2017-07-20 11:21:28 +02:00
}
2016-10-11 18:52:01 +02:00
else if ( _services . PublicAccessService . HasAccess ( request . PublishedContent . Id , _services . ContentService , GetRolesForLogin ( membershipHelper . CurrentUserName ) ) = = false )
2017-07-20 11:21:28 +02:00
{
2018-04-03 16:15:59 +02:00
_logger . Debug < PublishedRouter > ( ( ) = > $"{tracePrefix}Current member has not access, redirect to error page" ) ;
2017-07-20 11:21:28 +02:00
var errorPageId = publicAccessAttempt . Result . NoAccessNodeId ;
if ( errorPageId ! = request . PublishedContent . Id )
2018-04-27 11:38:50 +10:00
request . PublishedContent = request . UmbracoContext . PublishedSnapshot . Content . GetById ( errorPageId ) ;
2017-07-20 11:21:28 +02:00
}
else
{
2018-04-03 16:15:59 +02:00
_logger . Debug < PublishedRouter > ( ( ) = > $"{tracePrefix}Current member has access" ) ;
2017-07-20 11:21:28 +02:00
}
}
else
{
2018-04-03 16:15:59 +02:00
_logger . Debug < PublishedRouter > ( ( ) = > $"{tracePrefix}Page is not protected" ) ;
2017-07-20 11:21:28 +02:00
}
}
/// <summary>
/// Finds a template for the current node, if any.
/// </summary>
2017-10-31 12:50:30 +01:00
private void FindTemplate ( PublishedRequest request )
2017-07-20 11:21:28 +02:00
{
// 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: " ;
2016-10-11 18:52:01 +02:00
if ( request . PublishedContent = = null )
{
request . TemplateModel = null ;
return ;
}
2017-07-20 11:21:28 +02:00
// 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
2016-10-11 18:52:01 +02:00
// + optionnally, apply the alternate template on internal redirects
var useAltTemplate = _webRoutingSection . DisableAlternativeTemplates = = false
& & ( request . IsInitialPublishedContent
| | ( _webRoutingSection . InternalRedirectPreservesTemplate & & request . IsInternalRedirectPublishedContent ) ) ;
var altTemplate = useAltTemplate
? request . UmbracoContext . HttpContext . Request [ Constants . Conventions . Url . AltTemplate ]
2017-07-20 11:21:28 +02:00
: 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 ( request . HasTemplate )
{
2018-04-03 16:15:59 +02:00
_logger . Debug < PublishedRequest > ( "{0}Has a template already, and no alternate template." ) ;
2017-07-20 11:21:28 +02:00
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 = request . PublishedContent . TemplateId ;
if ( templateId > 0 )
{
2018-04-03 16:15:59 +02:00
_logger . Debug < PublishedRouter > ( ( ) = > $"{tracePrefix}Look for template id={templateId}" ) ;
2017-07-20 11:21:28 +02:00
var template = _services . FileService . GetTemplate ( templateId ) ;
if ( template = = null )
throw new InvalidOperationException ( "The template with Id " + templateId + " does not exist, the page cannot render" ) ;
2016-10-11 18:52:01 +02:00
request . TemplateModel = template ;
2018-04-03 16:15:59 +02:00
_logger . Debug < PublishedRouter > ( ( ) = > $"{tracePrefix}Got template id={template.Id} alias=\" { template . Alias } \ "" ) ;
2017-07-20 11:21:28 +02:00
}
else
{
2018-04-03 16:15:59 +02:00
_logger . Debug < PublishedRouter > ( ( ) = > $"{tracePrefix}No specified template." ) ;
2017-07-20 11:21:28 +02:00
}
}
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 ( request . HasTemplate )
2018-04-03 16:15:59 +02:00
_logger . Debug < PublishedRouter > ( ( ) = > $"{tracePrefix}Has a template already, but also an alternate template." ) ;
_logger . Debug < PublishedRouter > ( ( ) = > $"{tracePrefix}Look for alternate template alias=\" { altTemplate } \ "" ) ;
2017-07-20 11:21:28 +02:00
var template = _services . FileService . GetTemplate ( altTemplate ) ;
if ( template ! = null )
{
2016-10-11 18:52:01 +02:00
request . TemplateModel = template ;
2018-04-03 16:15:59 +02:00
_logger . Debug < PublishedRouter > ( ( ) = > $"{tracePrefix}Got template id={template.Id} alias=\" { template . Alias } \ "" ) ;
2017-07-20 11:21:28 +02:00
}
else
{
2018-04-03 16:15:59 +02:00
_logger . Debug < PublishedRouter > ( ( ) = > $"{tracePrefix}The template with alias=\" { altTemplate } \ " does not exist, ignoring." ) ;
2017-07-20 11:21:28 +02:00
}
}
if ( request . HasTemplate = = false )
{
2018-04-03 16:15:59 +02:00
_logger . Debug < PublishedRouter > ( ( ) = > $"{tracePrefix}No template was found." ) ;
2017-07-20 11:21:28 +02:00
// 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
{
2018-04-03 16:15:59 +02:00
_logger . Debug < PublishedRouter > ( ( ) = > $"{tracePrefix}Running with template id={request.TemplateModel.Id} alias=\" { request . TemplateModel . Alias } \ "" ) ;
2017-07-20 11:21:28 +02:00
}
}
/// <summary>
/// Follows external redirection through <c>umbracoRedirect</c> document property.
/// </summary>
/// <remarks>As per legacy, if the redirect does not work, we just ignore it.</remarks>
2017-10-31 12:50:30 +01:00
private void FollowExternalRedirect ( PublishedRequest request )
2017-07-20 11:21:28 +02:00
{
if ( request . HasPublishedContent = = false ) return ;
// don't try to find a redirect if the property doesn't exist
if ( request . PublishedContent . HasProperty ( Constants . Conventions . Content . Redirect ) = = false )
return ;
2017-05-30 10:50:09 +02:00
2018-04-18 19:46:47 +02:00
var redirectId = request . PublishedContent . Value ( Constants . Conventions . Content . Redirect , defaultValue : - 1 ) ;
2017-07-20 11:21:28 +02:00
var redirectUrl = "#" ;
if ( redirectId > 0 )
{
redirectUrl = request . UmbracoContext . UrlProvider . GetUrl ( redirectId ) ;
}
else
{
// might be a UDI instead of an int Id
var redirectUdi = request . PublishedContent . Value < GuidUdi > ( Constants . Conventions . Content . Redirect ) ;
if ( redirectUdi ! = null )
redirectUrl = request . UmbracoContext . UrlProvider . GetUrl ( redirectUdi . Guid ) ;
}
2017-05-30 10:50:09 +02:00
if ( redirectUrl ! = "#" )
2016-10-11 18:52:01 +02:00
request . SetRedirect ( redirectUrl ) ;
2017-07-20 11:21:28 +02:00
}
2016-10-11 18:52:01 +02:00
2017-07-20 11:21:28 +02:00
#endregion
}
2016-10-11 18:52:01 +02:00
}