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 DocumentRequest object /// internal class DocumentRequestBuilder { private readonly DocumentRequest _documentRequest; private readonly UmbracoContext _umbracoContext; private readonly RoutingContext _routingContext; public DocumentRequestBuilder(DocumentRequest documentRequest) { _documentRequest = documentRequest; _umbracoContext = documentRequest.RoutingContext.UmbracoContext; _routingContext = documentRequest.RoutingContext; } /// /// Determines the rendering engine to use and sets the flag on the DocumentRequest /// 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 (!_documentRequest.HasTemplate) { _documentRequest.RenderingEngine = RenderingEngine.Mvc; return; } var templateAlias = _documentRequest.Template.Alias; 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 _documentRequest.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, () => _documentRequest.Uri); // try to find a domain matching the current request var domainAndUri = DomainHelper.DomainMatch(Domain.GetDomains(), _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); _documentRequest.Domain = domainAndUri.Domain; _documentRequest.DomainUri = domainAndUri.Uri; _documentRequest.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(); _documentRequest.Culture = defaultLanguage == null ? CultureInfo.CurrentUICulture : new CultureInfo(defaultLanguage.CultureAlias); } LogHelper.Debug("{0}Culture=\"{1}\"", () => tracePrefix, () => _documentRequest.Culture.Name); return _documentRequest.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, () => _documentRequest.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, (_documentRequest.HasNode ? "a document was found" : "no document was found")))) { _routingContext.DocumentLookups.Any(lookup => lookup.TrySetDocument(_documentRequest)); } // 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 = _documentRequest.HasNode && _documentRequest.HasTemplate; return resolved; } /// /// Performs the document resolution second pass. /// /// The second pass consists in handling "not found", internal redirects, access validation, and template. private 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 (!_documentRequest.HasNode) { _documentRequest.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(_documentRequest)) { 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 (_documentRequest.HasNode) EnsureNodeAccess(); // resolve template if (_documentRequest.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 (!_documentRequest.HasNode && i++ < maxLoop); if (i == maxLoop || j == maxLoop) { LogHelper.Debug("{0}Looks like we're running into an infinite loop, abort", () => tracePrefix); _documentRequest.Document = 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 (_documentRequest.Document == null) throw new InvalidOperationException("There is no node."); bool redirect = false; var internalRedirect = _documentRequest.Document.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) //_documentRequest.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 == _documentRequest.DocumentId) { // redirect to self LogHelper.Debug("{0}Redirecting to self, ignore", () => tracePrefix); } else { // redirect to another page var node = _routingContext.PublishedContentStore.GetDocumentById( _umbracoContext, internalRedirectId); _documentRequest.Document = 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 (_documentRequest.Document == null) throw new InvalidOperationException("There is no node."); var path = _documentRequest.Document.Path; if (Access.IsProtected(_documentRequest.DocumentId, 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 != _documentRequest.DocumentId) _documentRequest.Document = _routingContext.PublishedContentStore.GetDocumentById( _umbracoContext, loginPageId); } else if (!Access.HasAccces(_documentRequest.DocumentId, user.ProviderUserKey)) { LogHelper.Debug("{0}Current member has not access, redirect to error page", () => tracePrefix); var errorPageId = Access.GetErrorPage(path); if (errorPageId != _documentRequest.DocumentId) _documentRequest.Document = _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() { //return if the request already has a template assigned, this can be possible if an ILookup assigns one if (_documentRequest.HasTemplate) return; const string tracePrefix = "LookupTemplate: "; if (_documentRequest.Document == null) throw new InvalidOperationException("There is no node."); //gets item from query string, form, cookie or server vars var templateAlias = _umbracoContext.HttpContext.Request["altTemplate"]; if (templateAlias.IsNullOrWhiteSpace()) { //we don't have an alt template specified, so lookup the template id on the document and then lookup the template // associated with it. //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 = _documentRequest.Document.TemplateId; LogHelper.Debug("{0}Look for template id={1}", () => tracePrefix, () => templateId); if (templateId > 0) { //NOTE: This will throw an exception if the template id doesn't exist, but that is ok to inform the front end. var template = new Template(templateId); _documentRequest.Template = template; } } else { LogHelper.Debug("{0}Look for template alias=\"{1}\" (altTemplate)", () => tracePrefix, () => templateAlias); var template = Template.GetByAlias(templateAlias); _documentRequest.Template = template; } if (!_documentRequest.HasTemplate) { LogHelper.Debug("{0}No template was found."); // initial idea was: if we're not already 404 and UmbracoSettings.HandleMissingTemplateAs404 is true // then reset _documentRequest.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 _documentRequest.Document to null here } } /// /// Follows external redirection through umbracoRedirect document property. /// /// As per legacy, if the redirect does not work, we just ignore it. private void FollowRedirect() { if (_documentRequest.HasNode) { var redirectId = _documentRequest.Document.GetPropertyValue("umbracoRedirect", -1); string redirectUrl = "#"; if (redirectId > 0) redirectUrl = _routingContext.NiceUrlProvider.GetNiceUrl(redirectId); if (redirectUrl != "#") _documentRequest.RedirectUrl = redirectUrl; } } } }