diff --git a/src/Umbraco.Abstractions/Constants-Conventions.cs b/src/Umbraco.Abstractions/Constants-Conventions.cs index 6672f22239..0bfb890abd 100644 --- a/src/Umbraco.Abstractions/Constants-Conventions.cs +++ b/src/Umbraco.Abstractions/Constants-Conventions.cs @@ -208,6 +208,17 @@ namespace Umbraco.Core public const string AllMembersListId = "all-members"; } + /// + /// Constants for Umbraco URLs/Querystrings. + /// + public static class Url + { + /// + /// Querystring parameter name used for Umbraco's alternative template functionality. + /// + public const string AltTemplate = "altTemplate"; + } + /// /// Defines the alias identifiers for built-in Umbraco relation types. /// @@ -274,6 +285,7 @@ namespace Umbraco.Core //TODO: return a list of built in types so we can use that to prevent deletion in the uI } + } } } diff --git a/src/Umbraco.Web/Routing/PublishedRouter.cs b/src/Umbraco.Web/Routing/PublishedRouter.cs index 74fe9f93e6..9148ce2e31 100644 --- a/src/Umbraco.Web/Routing/PublishedRouter.cs +++ b/src/Umbraco.Web/Routing/PublishedRouter.cs @@ -20,6 +20,7 @@ namespace Umbraco.Web.Routing /// public class PublishedRouter : IPublishedRouter { + private readonly IWebRoutingSection _webRoutingSection; private readonly ContentFinderCollection _contentFinders; private readonly IContentLastChanceFinder _contentLastChanceFinder; private readonly ServiceContext _services; @@ -33,6 +34,7 @@ namespace Umbraco.Web.Routing /// Initializes a new instance of the class. /// public PublishedRouter( + IWebRoutingSection webRoutingSection, ContentFinderCollection contentFinders, IContentLastChanceFinder contentLastChanceFinder, IVariationContextAccessor variationContextAccessor, @@ -41,6 +43,7 @@ namespace Umbraco.Web.Routing IUmbracoSettingsSection umbracoSettingsSection, IUserService userService) { + _webRoutingSection = webRoutingSection ?? throw new ArgumentNullException(nameof(webRoutingSection)); _contentFinders = contentFinders ?? throw new ArgumentNullException(nameof(contentFinders)); _contentLastChanceFinder = contentLastChanceFinder ?? throw new ArgumentNullException(nameof(contentLastChanceFinder)); _services = services ?? throw new ArgumentNullException(nameof(services)); @@ -641,14 +644,74 @@ namespace Umbraco.Web.Routing return; } - if (request.HasTemplate) - { - _logger.Debug("FindTemplate: Has a template already, and no alternate template."); - return; - } + // 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 + // + optionally, apply the alternate template on internal redirects + var useAltTemplate = request.IsInitialPublishedContent + || (_webRoutingSection.InternalRedirectPreservesTemplate && request.IsInternalRedirectPublishedContent); + var altTemplate = useAltTemplate + ? request.UmbracoContext.HttpContext.Request[Constants.Conventions.Url.AltTemplate] + : null; - var templateId = request.PublishedContent.TemplateId; - request.TemplateModel = GetTemplateModel(templateId); + 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) + { + _logger.Debug("FindTemplate: Has a template already, and no alternate template."); + return; + } + + // TODO: When we remove the need for a database for templates, then this id should be irrelevant, + // not sure how were going to do this nicely. + + // TODO: We need to limit altTemplate to only allow templates that are assigned to the current document type! + // if the template isn't assigned to the document type we should log a warning and return 404 + + var templateId = request.PublishedContent.TemplateId; + request.TemplateModel = GetTemplateModel(templateId); + } + 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) + _logger.Debug("FindTemplate: Has a template already, but also an alternative template."); + _logger.Debug("FindTemplate: Look for alternative template alias={AltTemplate}", altTemplate); + + // IsAllowedTemplate deals both with DisableAlternativeTemplates and ValidateAlternativeTemplates settings + if (request.PublishedContent.IsAllowedTemplate(altTemplate)) + { + // allowed, use + var template = _services.FileService.GetTemplate(altTemplate); + + if (template != null) + { + request.TemplateModel = template; + _logger.Debug("FindTemplate: Got alternative template id={TemplateId} alias={TemplateAlias}", template.Id, template.Alias); + } + else + { + _logger.Debug("FindTemplate: The alternative template with alias={AltTemplate} does not exist, ignoring.", altTemplate); + } + } + else + { + _logger.Warn("FindTemplate: Alternative template {TemplateAlias} is not allowed on node {NodeId}, ignoring.", altTemplate, request.PublishedContent.Id); + + // no allowed, back to default + var templateId = request.PublishedContent.TemplateId; + request.TemplateModel = GetTemplateModel(templateId); + } + } if (request.HasTemplate == false) {