using System;
using System.Linq;
using System.Text;
using System.Xml;
using System.Globalization;
using System.Diagnostics;
// legacy
using Umbraco.Core;
using Umbraco.Core.Logging;
using umbraco.BusinessLogic;
using umbraco.cms.businesslogic.web;
using umbraco.cms.businesslogic.template;
using umbraco.cms.businesslogic.member;
using umbraco.cms.businesslogic.language;
namespace Umbraco.Web.Routing
{
///
/// represents a request for one specified Umbraco document to be rendered
/// by one specified template, using one particular culture.
///
internal class DocumentRequest
{
public DocumentRequest(Uri uri, RoutingContext routingContext)
{
this.Uri = uri;
RoutingContext = routingContext;
}
///
/// the id of the requested node, if any, else zero.
///
int _nodeId = 0;
///
/// the requested node, if any, else null.
///
XmlNode _node = null;
#region Properties
///
/// Returns the current RoutingContext
///
public RoutingContext RoutingContext { get; private set; }
public Uri Uri { get; private set; }
///
/// Gets or sets the document request's domain.
///
public Domain Domain { get; private set; }
public Uri DomainUri { get; private set; }
///
/// Gets a value indicating whether the document request has a domain.
///
public bool HasDomain
{
get { return this.Domain != null; }
}
///
/// Gets or sets the document request's culture
///
public CultureInfo Culture { get; private set; }
// fixme - do we want to have an ordered list of alternate cultures,
// to allow for fallbacks when doing dictionnary lookup and such?
///
/// Gets or sets the document request's document xml node.
///
public XmlNode Node
{
get
{
return _node;
}
set
{
_node = value;
this.Template = null;
if (_node != null)
_nodeId = int.Parse(RoutingContext.ContentStore.GetNodeProperty(_node, "@id"));
else
_nodeId = 0;
}
}
///
/// Gets or sets the document request's template.
///
public Template Template { get; set; }
///
/// Gets a value indicating whether the document request has a template.
///
public bool HasTemplate
{
get { return this.Template != null; }
}
///
/// Gets the id of the document.
///
/// Thrown when the document request has no document.
public int NodeId
{
get
{
if (this.Node == null)
throw new InvalidOperationException("DocumentRequest has no document.");
return _nodeId;
}
}
///
/// Gets a value indicating whether the document request has a document.
///
public bool HasNode
{
get { return this.Node != null; }
}
///
/// Gets or sets a value indicating whether the requested document could not be found.
///
public bool Is404 { get; private set; }
///
/// Gets a value indicating whether the document request triggers a redirect.
///
public bool IsRedirect { get { return !string.IsNullOrWhiteSpace(this.RedirectUrl); } }
///
/// Gets the url to redirect to, when the document request triggers a redirect.
///
public string RedirectUrl { get; set; }
#endregion
#region Lookup
///
/// Determines the site root (if any) matching the http request.
///
/// A value indicating whether a domain was found.
public bool LookupDomain()
{
const string tracePrefix = "LookupDomain: ";
// note - we are not handling schemes nor ports here.
LogHelper.Debug("{0}Uri=\"{1}\"", () => tracePrefix, () => this.Uri);
// try to find a domain matching the current request
var domainAndUri = DomainHelper.DomainMatch(Domain.GetDomains(), RoutingContext.UmbracoContext.UmbracoUrl, 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);
this.Domain = domainAndUri.Domain;
this.DomainUri = domainAndUri.Uri;
this.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();
this.Culture = defaultLanguage == null ? CultureInfo.CurrentUICulture : new CultureInfo(defaultLanguage.CultureAlias);
}
LogHelper.Debug("{0}Culture=\"{1}\"", () => tracePrefix, () => this.Culture.Name);
return this.Domain != null;
}
///
/// Determines the Umbraco document (if any) matching the http request.
///
/// A value indicating whether a document and template nave been found.
public bool LookupDocument()
{
const string tracePrefix = "LookupDocument: ";
LogHelper.Debug("{0}Path=\"{1}\"", () => tracePrefix, () => this.Uri.AbsolutePath);
// 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, (this.HasNode ? "a document was found" : "no document was found"))))
{
RoutingContext.DocumentLookups.Any(lookup => lookup.TrySetDocument(this));
}
// fixme - not handling umbracoRedirect
// should come after internal redirects
// so after ResolveDocument2() => docreq.IsRedirect => handled by the module!
// handle not-found, redirects, access, template
LookupDocument2();
// handle umbracoRedirect (moved from umbraco.page)
FollowRedirect();
bool resolved = this.HasNode && this.HasTemplate;
return resolved;
}
///
/// Performs the document resolution second pass.
///
/// The second pass consists in handling "not found", internal redirects, access validation, and template.
void LookupDocument2()
{
const string tracePrefix = "LookupDocument2: ";
// 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 (!this.HasNode)
{
this.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.DocumentLastChanceLookup;
if (lastChance == null || !lastChance.TrySetDocument(this))
{
LogHelper.Debug("{0}Failed to find a document, give up", () => tracePrefix);
break;
}
else
{
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 (this.HasNode)
EnsureNodeAccess();
// resolve template
if (this.HasNode)
LookupTemplate();
// 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 (!this.HasNode && i++ < maxLoop);
if (i == maxLoop || j == maxLoop)
{
LogHelper.Debug("{0}Looks like we're running into an infinite loop, abort", () => tracePrefix);
this.Node = 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.
bool FollowInternalRedirects()
{
const string tracePrefix = "FollowInternalRedirects: ";
if (this.Node == null)
throw new InvalidOperationException("There is no node.");
bool redirect = false;
string internalRedirect = RoutingContext.ContentStore.GetNodeProperty(this.Node, "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
this.Node = null;
LogHelper.Debug("{0}Failed to redirect to id={1}: invalid value", () => tracePrefix, () => internalRedirect);
}
else if (internalRedirectId == this.NodeId)
{
// redirect to self
LogHelper.Debug("{0}Redirecting to self, ignore", () => tracePrefix);
}
else
{
// redirect to another page
var node = RoutingContext.ContentStore.GetNodeById(internalRedirectId);
this.Node = 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.
void EnsureNodeAccess()
{
const string tracePrefix = "EnsurePageAccess: ";
if (this.Node == null)
throw new InvalidOperationException("There is no node.");
var path = RoutingContext.ContentStore.GetNodeProperty(this.Node, "@path");
if (Access.IsProtected(this.NodeId, path))
{
LogHelper.Debug("{0}Page is protected, check for access", () => tracePrefix);
var user = System.Web.Security.Membership.GetUser();
if (user == null || !Member.IsLoggedOn())
{
LogHelper.Debug("{0}Not logged in, redirect to login page", () => tracePrefix);
var loginPageId = Access.GetLoginPage(path);
if (loginPageId != this.NodeId)
this.Node = RoutingContext.ContentStore.GetNodeById(loginPageId);
}
else if (!Access.HasAccces(this.NodeId, user.ProviderUserKey))
{
LogHelper.Debug("{0}Current member has not access, redirect to error page", () => tracePrefix);
var errorPageId = Access.GetErrorPage(path);
if (errorPageId != this.NodeId)
this.Node = RoutingContext.ContentStore.GetNodeById(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.
///
void LookupTemplate()
{
const string tracePrefix = "LookupTemplate: ";
if (this.Node == null)
throw new InvalidOperationException("There is no node.");
var templateAlias = RoutingContext.UmbracoContext.HttpContext.Request.QueryString["altTemplate"];
if (string.IsNullOrWhiteSpace(templateAlias))
templateAlias = RoutingContext.UmbracoContext.HttpContext.Request.Form["altTemplate"];
// fixme - we might want to support cookies?!? NO but provide a hook to change the template
if (!this.HasTemplate || !string.IsNullOrWhiteSpace(templateAlias))
{
if (string.IsNullOrWhiteSpace(templateAlias))
{
templateAlias = RoutingContext.ContentStore.GetNodeProperty(this.Node, "@template");
LogHelper.Debug("{0}Look for template id={1}", () => tracePrefix, () => templateAlias);
int templateId;
if (!int.TryParse(templateAlias, out templateId))
templateId = 0;
this.Template = templateId > 0 ? new Template(templateId) : null;
}
else
{
LogHelper.Debug("{0}Look for template alias=\"{1}\" (altTemplate)", () => tracePrefix, () => templateAlias);
this.Template = Template.GetByAlias(templateAlias);
}
if (!this.HasTemplate)
{
LogHelper.Debug("{0}No template was found", () => tracePrefix);
//TODO: I like the idea of this new setting, but lets get this in to the core at a later time, for now lets just get the basics working.
//if (Settings.HandleMissingTemplateAs404)
//{
// this.Node = null;
// LogHelper.Debug("{0}Assume page not found (404)", tracePrefix);
//}
// else we have no template
// and there isn't much more we can do about it
}
else
{
LogHelper.Debug("{0}Found", () => tracePrefix);
}
}
}
///
/// Follows external redirection through umbracoRedirect document property.
///
void FollowRedirect()
{
if (this.HasNode)
{
int redirectId;
if (!int.TryParse(RoutingContext.ContentStore.GetNodeProperty(this.Node, "umbracoRedirect"), out redirectId))
redirectId = -1;
string redirectUrl = "#";
if (redirectId > 0)
redirectUrl = RoutingContext.NiceUrlProvider.GetNiceUrl(redirectId);
if (redirectUrl != "#")
this.RedirectUrl = redirectUrl;
}
}
#endregion
}
}