diff --git a/src/Umbraco.Web/IUmbracoComponentRenderer.cs b/src/Umbraco.Core/Net/IUmbracoComponentRenderer.cs similarity index 77% rename from src/Umbraco.Web/IUmbracoComponentRenderer.cs rename to src/Umbraco.Core/Net/IUmbracoComponentRenderer.cs index 4dc9036e6b..0391a01183 100644 --- a/src/Umbraco.Web/IUmbracoComponentRenderer.cs +++ b/src/Umbraco.Core/Net/IUmbracoComponentRenderer.cs @@ -1,8 +1,7 @@ using System.Collections.Generic; -using System.Web; -using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Strings; -namespace Umbraco.Web +namespace Umbraco.Core.Net { /// /// Methods used to render umbraco components as HTML in templates @@ -15,7 +14,7 @@ namespace Umbraco.Web /// /// If not specified, will use the template assigned to the node /// - IHtmlString RenderTemplate(int contentId, int? altTemplateId = null); + IHtmlEncodedString RenderTemplate(int contentId, int? altTemplateId = null); /// /// Renders the macro with the specified alias. @@ -23,7 +22,7 @@ namespace Umbraco.Web /// /// The alias. /// - IHtmlString RenderMacro(int contentId, string alias); + IHtmlEncodedString RenderMacro(int contentId, string alias); /// /// Renders the macro with the specified alias, passing in the specified parameters. @@ -32,7 +31,7 @@ namespace Umbraco.Web /// The alias. /// The parameters. /// - IHtmlString RenderMacro(int contentId, string alias, object parameters); + IHtmlEncodedString RenderMacro(int contentId, string alias, object parameters); /// /// Renders the macro with the specified alias, passing in the specified parameters. @@ -41,6 +40,6 @@ namespace Umbraco.Web /// The alias. /// The parameters. /// - IHtmlString RenderMacro(int contentId, string alias, IDictionary parameters); + IHtmlEncodedString RenderMacro(int contentId, string alias, IDictionary parameters); } } diff --git a/src/Umbraco.Web/UmbracoComponentRenderer.cs b/src/Umbraco.Core/Net/UmbracoComponentRenderer.cs similarity index 81% rename from src/Umbraco.Web/UmbracoComponentRenderer.cs rename to src/Umbraco.Core/Net/UmbracoComponentRenderer.cs index 7a338cca59..22703a111e 100644 --- a/src/Umbraco.Web/UmbracoComponentRenderer.cs +++ b/src/Umbraco.Core/Net/UmbracoComponentRenderer.cs @@ -1,19 +1,16 @@ using System; using System.Linq; -using System.Collections; using System.IO; using System.Web; -using System.Web.UI; using Umbraco.Core; using Umbraco.Web.Templates; using System.Collections.Generic; -using Umbraco.Core.Logging; using Umbraco.Core.Models.PublishedContent; -using Umbraco.Core.Services; -using Umbraco.Web.Composing; +using Umbraco.Core.Strings; +using Umbraco.Web; using Umbraco.Web.Macros; -namespace Umbraco.Web +namespace Umbraco.Core.Net { /// @@ -22,19 +19,18 @@ namespace Umbraco.Web /// /// Used by UmbracoHelper /// - internal class UmbracoComponentRenderer : IUmbracoComponentRenderer + // Migrated to .NET Core + public class UmbracoComponentRenderer : IUmbracoComponentRenderer { private readonly IUmbracoContextAccessor _umbracoContextAccessor; private readonly IMacroRenderer _macroRenderer; private readonly ITemplateRenderer _templateRenderer; - private readonly HtmlLocalLinkParser _linkParser; - public UmbracoComponentRenderer(IUmbracoContextAccessor umbracoContextAccessor, IMacroRenderer macroRenderer, ITemplateRenderer templateRenderer, HtmlLocalLinkParser linkParser) + public UmbracoComponentRenderer(IUmbracoContextAccessor umbracoContextAccessor, IMacroRenderer macroRenderer, ITemplateRenderer templateRenderer) { _umbracoContextAccessor = umbracoContextAccessor; _macroRenderer = macroRenderer; _templateRenderer = templateRenderer ?? throw new ArgumentNullException(nameof(templateRenderer)); - _linkParser = linkParser; } /// @@ -43,7 +39,7 @@ namespace Umbraco.Web /// /// If not specified, will use the template assigned to the node /// - public IHtmlString RenderTemplate(int contentId, int? altTemplateId = null) + public IHtmlEncodedString RenderTemplate(int contentId, int? altTemplateId = null) { using (var sw = new StringWriter()) { @@ -55,7 +51,7 @@ namespace Umbraco.Web { sw.Write("", contentId, ex); } - return new HtmlString(sw.ToString()); + return new HtmlEncodedString(sw.ToString()); } } @@ -65,7 +61,7 @@ namespace Umbraco.Web /// /// The alias. /// - public IHtmlString RenderMacro(int contentId, string alias) + public IHtmlEncodedString RenderMacro(int contentId, string alias) { return RenderMacro(contentId, alias, new { }); } @@ -77,7 +73,7 @@ namespace Umbraco.Web /// The alias. /// The parameters. /// - public IHtmlString RenderMacro(int contentId, string alias, object parameters) + public IHtmlEncodedString RenderMacro(int contentId, string alias, object parameters) { return RenderMacro(contentId, alias, parameters?.ToDictionary()); } @@ -89,7 +85,7 @@ namespace Umbraco.Web /// The alias. /// The parameters. /// - public IHtmlString RenderMacro(int contentId, string alias, IDictionary parameters) + public IHtmlEncodedString RenderMacro(int contentId, string alias, IDictionary parameters) { if (contentId == default) throw new ArgumentException("Invalid content id " + contentId); @@ -109,7 +105,7 @@ namespace Umbraco.Web /// The parameters. /// The content used for macro rendering /// - private IHtmlString RenderMacro(IPublishedContent content, string alias, IDictionary parameters) + private IHtmlEncodedString RenderMacro(IPublishedContent content, string alias, IDictionary parameters) { if (content == null) throw new ArgumentNullException(nameof(content)); @@ -121,7 +117,7 @@ namespace Umbraco.Web var html = _macroRenderer.Render(alias, content, macroProps).Text; - return new HtmlString(html); + return new HtmlEncodedString(html); } } } diff --git a/src/Umbraco.Core/Routing/DomainUtilities.cs b/src/Umbraco.Core/Routing/DomainUtilities.cs index c459ae4d14..e6566cddf1 100644 --- a/src/Umbraco.Core/Routing/DomainUtilities.cs +++ b/src/Umbraco.Core/Routing/DomainUtilities.cs @@ -27,7 +27,7 @@ namespace Umbraco.Web.Routing /// one document per culture), and domains, withing the context of a current Uri, assign /// a culture to that document. /// - internal static string GetCultureFromDomains(int contentId, string contentPath, Uri current, IUmbracoContext umbracoContext, ISiteDomainHelper siteDomainHelper) + public static string GetCultureFromDomains(int contentId, string contentPath, Uri current, IUmbracoContext umbracoContext, ISiteDomainHelper siteDomainHelper) { if (umbracoContext == null) throw new InvalidOperationException("A current UmbracoContext is required."); @@ -146,7 +146,7 @@ namespace Umbraco.Web.Routing /// the right one, unless it is null, in which case the method returns null. /// The filter, if any, will be called only with a non-empty argument, and _must_ return something. /// - internal static DomainAndUri SelectDomain(IEnumerable domains, Uri uri, string culture = null, string defaultCulture = null, Func, Uri, string, string, DomainAndUri> filter = null) + public static DomainAndUri SelectDomain(IEnumerable domains, Uri uri, string culture = null, string defaultCulture = null, Func, Uri, string, string, DomainAndUri> filter = null) { // sanitize the list to have proper uris for comparison (scheme, path end with /) // we need to end with / because example.com/foo cannot match example.com/foobar diff --git a/src/Umbraco.Core/Routing/PublishedRequest.cs b/src/Umbraco.Core/Routing/PublishedRequest.cs index 6e4d0008a9..a46eb26e7e 100644 --- a/src/Umbraco.Core/Routing/PublishedRequest.cs +++ b/src/Umbraco.Core/Routing/PublishedRequest.cs @@ -33,7 +33,7 @@ namespace Umbraco.Web.Routing /// The published router. /// The Umbraco context. /// The request Uri. - internal PublishedRequest(IPublishedRouter publishedRouter, IUmbracoContext umbracoContext, IWebRoutingSettings webRoutingSettings, Uri uri = null) + public PublishedRequest(IPublishedRouter publishedRouter, IUmbracoContext umbracoContext, IWebRoutingSettings webRoutingSettings, Uri uri = null) { UmbracoContext = umbracoContext ?? throw new ArgumentNullException(nameof(umbracoContext)); _publishedRouter = publishedRouter ?? throw new ArgumentNullException(nameof(publishedRouter)); diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index dc76ed39e7..ec8560dd1f 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -28,4 +28,8 @@ <_Parameter1>Umbraco.Tests.Benchmarks + + + + diff --git a/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs b/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs index 332813faf8..1c37968a6a 100644 --- a/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs +++ b/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs @@ -18,6 +18,7 @@ using Umbraco.Core.Migrations; using Umbraco.Core.Migrations.Install; using Umbraco.Core.Migrations.PostMigrations; using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Net; using Umbraco.Core.Persistence; using Umbraco.Core.PropertyEditors; using Umbraco.Core.PropertyEditors.Validators; @@ -30,7 +31,6 @@ using Umbraco.Core.Strings; using Umbraco.Core.Sync; using Umbraco.Examine; using Umbraco.Infrastructure.Media; -using Umbraco.Net; using Umbraco.Web; using Umbraco.Web.Actions; using Umbraco.Web.Cache; @@ -357,6 +357,8 @@ namespace Umbraco.Core.Runtime composition.Register(Lifetime.Singleton); + composition.RegisterUnique(); + } } diff --git a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj index 1af8ce9119..3310fef929 100644 --- a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj +++ b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj @@ -51,6 +51,7 @@ True Resources.resx + diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj b/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj index 3176f3d36f..5db99db044 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj @@ -20,4 +20,12 @@ + + + + + + + + diff --git a/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs b/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs index 72186dd081..3f848325cc 100644 --- a/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs +++ b/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs @@ -14,6 +14,7 @@ using Umbraco.Core.Logging; using Umbraco.Core.Mapping; using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Net; using Umbraco.Core.Persistence; using Umbraco.Core.Services; using Umbraco.Tests.Common; diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs index 0f4c1a1040..9f7134dd81 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs @@ -200,10 +200,10 @@ namespace Umbraco.Web.BackOffice.Controllers // "mediaTypeApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl( // controller => controller.GetAllowedChildren(0)) // }, - // { - // "macroRenderingApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl( - // controller => controller.GetMacroParameters(0)) - // }, + { + "macroRenderingApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl( + controller => controller.GetMacroParameters(0)) + }, { "macroApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl( controller => controller.Create(null)) diff --git a/src/Umbraco.Web/Editors/MacroRenderingController.cs b/src/Umbraco.Web.BackOffice/Controllers/MacroRenderingController.cs similarity index 73% rename from src/Umbraco.Web/Editors/MacroRenderingController.cs rename to src/Umbraco.Web.BackOffice/Controllers/MacroRenderingController.cs index 43b544cb1f..58c30aa0ee 100644 --- a/src/Umbraco.Web/Editors/MacroRenderingController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MacroRenderingController.cs @@ -6,23 +6,20 @@ using System.Net; using System.Net.Http; using System.Text; using System.Threading; -using System.Web.Http; -using System.Web.SessionState; +using Microsoft.AspNetCore.Mvc; using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Mvc; using Umbraco.Core; -using Umbraco.Core.Cache; -using Umbraco.Core.Configuration; -using Umbraco.Core.Logging; using Umbraco.Core.Mapping; using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; -using Umbraco.Core.Persistence; +using Umbraco.Core.Net; using Umbraco.Core.Services; using Umbraco.Core.Strings; +using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.Exceptions; using Umbraco.Web.Routing; -namespace Umbraco.Web.Editors +namespace Umbraco.Web.BackOffice.Controllers { /// /// API controller to deal with Macro data @@ -33,42 +30,34 @@ namespace Umbraco.Web.Editors /// Session, we don't want it to throw null reference exceptions. /// [PluginController("UmbracoApi")] - public class MacroRenderingController : UmbracoAuthorizedJsonController, IRequiresSessionState + public class MacroRenderingController : UmbracoAuthorizedJsonController { private readonly IMacroService _macroService; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly IShortStringHelper _shortStringHelper; + private readonly ISiteDomainHelper _siteDomainHelper; + private readonly UmbracoMapper _umbracoMapper; private readonly IUmbracoComponentRenderer _componentRenderer; private readonly IVariationContextAccessor _variationContextAccessor; public MacroRenderingController( - IGlobalSettings globalSettings, - IUmbracoContextAccessor umbracoContextAccessor, - ISqlContext sqlContext, - ServiceContext services, - AppCaches appCaches, - IProfilingLogger logger, - IRuntimeState runtimeState, - IShortStringHelper shortStringHelper, UmbracoMapper umbracoMapper, IUmbracoComponentRenderer componentRenderer, IVariationContextAccessor variationContextAccessor, IMacroService macroService, - IPublishedUrlProvider publishedUrlProvider) - : base( - globalSettings, - umbracoContextAccessor, - sqlContext, - services, - appCaches, - logger, - runtimeState, - shortStringHelper, - umbracoMapper, - publishedUrlProvider) + IUmbracoContextAccessor umbracoContextAccessor, + IShortStringHelper shortStringHelper, + ISiteDomainHelper siteDomainHelper) + { - _componentRenderer = componentRenderer; - _variationContextAccessor = variationContextAccessor; - _macroService = macroService; + _umbracoMapper = umbracoMapper ?? throw new ArgumentNullException(nameof(umbracoMapper)); + _componentRenderer = componentRenderer ?? throw new ArgumentNullException(nameof(componentRenderer)); + _variationContextAccessor = variationContextAccessor ?? throw new ArgumentNullException(nameof(variationContextAccessor)); + _macroService = macroService ?? throw new ArgumentNullException(nameof(macroService)); + _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); + _shortStringHelper = shortStringHelper ?? throw new ArgumentNullException(nameof(shortStringHelper)); + _siteDomainHelper = siteDomainHelper ?? throw new ArgumentNullException(nameof(siteDomainHelper)); } /// @@ -87,7 +76,7 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(HttpStatusCode.NotFound); } - return Mapper.Map>(macro).OrderBy(x => x.SortOrder); + return _umbracoMapper.Map>(macro).OrderBy(x => x.SortOrder); } /// @@ -103,7 +92,7 @@ namespace Umbraco.Web.Editors /// /// [HttpGet] - public HttpResponseMessage GetMacroResultAsHtmlForEditor(string macroAlias, int pageId, [FromUri] IDictionary macroParams) + public IActionResult GetMacroResultAsHtmlForEditor(string macroAlias, int pageId, [FromQuery] IDictionary macroParams) { return GetMacroResultAsHtml(macroAlias, pageId, macroParams); } @@ -116,7 +105,7 @@ namespace Umbraco.Web.Editors /// /// [HttpPost] - public HttpResponseMessage GetMacroResultAsHtmlForEditor(MacroParameterModel model) + public IActionResult GetMacroResultAsHtmlForEditor(MacroParameterModel model) { return GetMacroResultAsHtml(model.MacroAlias, model.PageId, model.MacroParams); } @@ -128,24 +117,22 @@ namespace Umbraco.Web.Editors public IDictionary MacroParams { get; set; } } - private HttpResponseMessage GetMacroResultAsHtml(string macroAlias, int pageId, IDictionary macroParams) + private IActionResult GetMacroResultAsHtml(string macroAlias, int pageId, IDictionary macroParams) { var m = _macroService.GetByAlias(macroAlias); if (m == null) throw new HttpResponseException(HttpStatusCode.NotFound); - var publishedContent = UmbracoContext.Content.GetById(true, pageId); + var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + var publishedContent = umbracoContext.Content.GetById(true, pageId); //if it isn't supposed to be rendered in the editor then return an empty string //currently we cannot render a macro if the page doesn't yet exist if (pageId == -1 || publishedContent == null || m.DontRender) { - var response = Request.CreateResponse(); //need to create a specific content result formatted as HTML since this controller has been configured //with only json formatters. - response.Content = new StringContent(string.Empty, Encoding.UTF8, "text/html"); - - return response; + return Content(string.Empty, "text/html", Encoding.UTF8); } @@ -157,7 +144,7 @@ namespace Umbraco.Web.Editors // in a 1:1 situation we do not handle the language being edited // so the macro renders in the wrong language - var culture = publishedContent.GetCultureFromDomains(); + var culture = DomainUtilities.GetCultureFromDomains(publishedContent.Id, publishedContent.Path, null, umbracoContext, _siteDomainHelper); if (culture != null) Thread.CurrentThread.CurrentCulture = Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(culture); @@ -165,18 +152,12 @@ namespace Umbraco.Web.Editors // must have an active variation context! _variationContextAccessor.VariationContext = new VariationContext(culture); - using (UmbracoContext.ForcedPreview(true)) + using (umbracoContext.ForcedPreview(true)) { - - var result = Request.CreateResponse(); //need to create a specific content result formatted as HTML since this controller has been configured //with only json formatters. - result.Content = new StringContent( - _componentRenderer.RenderMacro(pageId, m.Alias, macroParams).ToString(), - Encoding.UTF8, - "text/html"); - - return result; + return Content(_componentRenderer.RenderMacro(pageId, m.Alias, macroParams).ToString(), "text/html", + Encoding.UTF8); } } @@ -189,9 +170,9 @@ namespace Umbraco.Web.Editors var macroName = model.Filename.TrimEnd(".cshtml"); - var macro = new Macro(ShortStringHelper) + var macro = new Macro(_shortStringHelper) { - Alias = macroName.ToSafeAlias(ShortStringHelper), + Alias = macroName.ToSafeAlias(_shortStringHelper), Name = macroName, MacroSource = model.VirtualPath.EnsureStartsWith("~") }; diff --git a/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj b/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj index 3f6dd6969c..f2b4e79ba0 100644 --- a/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj +++ b/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj @@ -27,4 +27,12 @@ + + + + + + + + diff --git a/src/Umbraco.Web.Common/AspNetCore/UmbracoViewPage.cs b/src/Umbraco.Web.Common/AspNetCore/UmbracoViewPage.cs index 720024ead1..fe122af7b8 100644 --- a/src/Umbraco.Web.Common/AspNetCore/UmbracoViewPage.cs +++ b/src/Umbraco.Web.Common/AspNetCore/UmbracoViewPage.cs @@ -21,21 +21,14 @@ namespace Umbraco.Web.Common.AspNetCore public abstract class UmbracoViewPage : RazorPage { + private IUmbracoContext _umbracoContext; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private readonly IGlobalSettings _globalSettings; - private readonly IContentSettings _contentSettings; - private readonly IProfilerHtml _profilerHtml; + private IUmbracoContextAccessor UmbracoContextAccessor => Context.RequestServices.GetRequiredService(); + private IGlobalSettings GlobalSettings => Context.RequestServices.GetRequiredService(); + private IContentSettings ContentSettings => Context.RequestServices.GetRequiredService(); + private IProfilerHtml ProfilerHtml => Context.RequestServices.GetRequiredService(); - protected IUmbracoContext UmbracoContext => _umbracoContext ??= _umbracoContextAccessor.UmbracoContext; - - protected UmbracoViewPage() - { - _umbracoContextAccessor = Context.RequestServices.GetRequiredService(); - _globalSettings = Context.RequestServices.GetRequiredService(); - _contentSettings = Context.RequestServices.GetRequiredService(); - _profilerHtml = Context.RequestServices.GetRequiredService(); - } + protected IUmbracoContext UmbracoContext => _umbracoContext ??= UmbracoContextAccessor.UmbracoContext; public override void Write(object value) @@ -68,15 +61,15 @@ namespace Umbraco.Web.Common.AspNetCore { // creating previewBadge markup markupToInject = - string.Format(_contentSettings.PreviewBadge, - Current.IOHelper.ResolveUrl(_globalSettings.UmbracoPath), + string.Format(ContentSettings.PreviewBadge, + Current.IOHelper.ResolveUrl(GlobalSettings.UmbracoPath), Context.Request.GetEncodedUrl(), UmbracoContext.PublishedRequest.PublishedContent.Id); } else { // creating mini-profiler markup - markupToInject = _profilerHtml.Render(); + markupToInject = ProfilerHtml.Render(); } var sb = new StringBuilder(text); diff --git a/src/Umbraco.Web.Common/Macros/PartialViewMacroEngine.cs b/src/Umbraco.Web.Common/Macros/PartialViewMacroEngine.cs index ad1e1a06b0..3566ee48ee 100644 --- a/src/Umbraco.Web.Common/Macros/PartialViewMacroEngine.cs +++ b/src/Umbraco.Web.Common/Macros/PartialViewMacroEngine.cs @@ -2,13 +2,17 @@ using System.Collections.Generic; using System.IO; using System.Text.Encodings.Web; +using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewComponents; +using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Core.IO; using Umbraco.Core.Models.PublishedContent; using Umbraco.Extensions; @@ -25,8 +29,10 @@ namespace Umbraco.Web.Common.Macros private readonly IIOHelper _ioHelper; private readonly Func _getUmbracoContext; - public PartialViewMacroEngine(IUmbracoContextAccessor umbracoContextAccessor, - IHttpContextAccessor httpContextAccessor, IIOHelper ioHelper) + public PartialViewMacroEngine( + IUmbracoContextAccessor umbracoContextAccessor, + IHttpContextAccessor httpContextAccessor, + IIOHelper ioHelper) { _httpContextAccessor = httpContextAccessor; _ioHelper = ioHelper; @@ -72,8 +78,18 @@ namespace Umbraco.Web.Common.Macros routeVals.Values.Add("action", "Index"); routeVals.DataTokens.Add(Core.Constants.Web.UmbracoContextDataToken, umbCtx); //required for UmbracoViewPage - //lets render this controller as a child action - var viewContext = new ViewContext(); + var modelMetadataProvider = httpContext.RequestServices.GetRequiredService(); + var tempDataProvider = httpContext.RequestServices.GetRequiredService(); + + var viewContext = new ViewContext( + new ActionContext(httpContext, httpContext.GetRouteData(), new ControllerActionDescriptor()), + new FakeView(), + new ViewDataDictionary(modelMetadataProvider, new ModelStateDictionary()), + new TempDataDictionary(httpContext, tempDataProvider), + TextWriter.Null, + new HtmlHelperOptions() + ); + routeVals.DataTokens.Add("ParentActionViewContext", viewContext); @@ -93,7 +109,17 @@ namespace Umbraco.Web.Common.Macros return new MacroContent { Text = output }; } + private class FakeView : IView + { + /// + public Task RenderAsync(ViewContext context) + { + return Task.CompletedTask; + } + /// + public string Path { get; } = "View"; + } private string GetVirtualPathFromPhysicalPath(string physicalPath) { var rootpath = _ioHelper.MapPath("~/"); diff --git a/src/Umbraco.Web.Common/Macros/PartialViewMacroViewComponent.cs b/src/Umbraco.Web.Common/Macros/PartialViewMacroViewComponent.cs index 47568d5a62..9de523fd1b 100644 --- a/src/Umbraco.Web.Common/Macros/PartialViewMacroViewComponent.cs +++ b/src/Umbraco.Web.Common/Macros/PartialViewMacroViewComponent.cs @@ -1,10 +1,12 @@ -using Umbraco.Web.Models; +using System.Collections.Generic; +using Umbraco.Web.Models; using System.Linq; using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ViewEngines; using Umbraco.Core.Composing; using Umbraco.Core.Models.PublishedContent; -using Umbraco.Web.Mvc; namespace Umbraco.Web.Macros { @@ -18,7 +20,9 @@ namespace Umbraco.Web.Macros private readonly MacroModel _macro; private readonly IPublishedContent _content; - public PartialViewMacroViewComponent(MacroModel macro, IPublishedContent content) + public PartialViewMacroViewComponent( + MacroModel macro, + IPublishedContent content) { _macro = macro; _content = content; @@ -32,7 +36,37 @@ namespace Umbraco.Web.Macros _macro.Alias, _macro.Name, _macro.Properties.ToDictionary(x => x.Key, x => (object)x.Value)); - return View(_macro.MacroSource, model); + var result = View(_macro.MacroSource, model); + + return result; + } + + private class MacroViewEngine : ICompositeViewEngine + { + private readonly ICompositeViewEngine _compositeViewEngine; + private readonly IWebHostEnvironment _webHostEnvironment; + + public MacroViewEngine(ICompositeViewEngine compositeViewEngine, IWebHostEnvironment webHostEnvironment) + { + _compositeViewEngine = compositeViewEngine; + _webHostEnvironment = webHostEnvironment; + } + + public ViewEngineResult FindView(ActionContext context, string viewName, bool isMainPage) + { + var result = _compositeViewEngine.FindView(context, viewName, isMainPage); + return result; + } + + public ViewEngineResult GetView(string executingFilePath, string viewPath, bool isMainPage) + { + var result = _compositeViewEngine.GetView(executingFilePath, viewPath, isMainPage); + return result; + } + + public IReadOnlyList ViewEngines => _compositeViewEngine.ViewEngines; } } + + } diff --git a/src/Umbraco.Web.Common/Macros/UmbracoMacroResult.cs b/src/Umbraco.Web.Common/Macros/UmbracoMacroResult.cs new file mode 100644 index 0000000000..65405a24ca --- /dev/null +++ b/src/Umbraco.Web.Common/Macros/UmbracoMacroResult.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.Mvc.ViewComponents; + +namespace Umbraco.Web.Macros +{ + internal class UmbracoMacroResult : ViewViewComponentResult + { + + } +} diff --git a/src/Umbraco.Web.Common/Routing/PublicAccessChecker.cs b/src/Umbraco.Web.Common/Routing/PublicAccessChecker.cs new file mode 100644 index 0000000000..0d7b787721 --- /dev/null +++ b/src/Umbraco.Web.Common/Routing/PublicAccessChecker.cs @@ -0,0 +1,10 @@ +using Umbraco.Web.Security; + +namespace Umbraco.Web.Common.Routing +{ + public class PublicAccessChecker : IPublicAccessChecker + { + //TODO implement + public PublicAccessStatus HasMemberAccessToContent(int publishedContentId) => PublicAccessStatus.AccessAccepted; + } +} diff --git a/src/Umbraco.Web.Common/Routing/PublishedRouter.cs b/src/Umbraco.Web.Common/Routing/PublishedRouter.cs new file mode 100644 index 0000000000..6618d6edf5 --- /dev/null +++ b/src/Umbraco.Web.Common/Routing/PublishedRouter.cs @@ -0,0 +1,766 @@ +using System; +using System.Linq; +using System.Threading; +using System.Globalization; +using System.IO; +using Umbraco.Core; +using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Services; +using Umbraco.Web.Routing; +using Umbraco.Web.Security; +namespace Umbraco.Web.Common.Routing +{ + /// + /// Provides the default implementation. + /// + public class PublishedRouter : IPublishedRouter + { + private readonly IWebRoutingSettings _webRoutingSettings; + private readonly ContentFinderCollection _contentFinders; + private readonly IContentLastChanceFinder _contentLastChanceFinder; + private readonly IProfilingLogger _profilingLogger; + private readonly IVariationContextAccessor _variationContextAccessor; + private readonly ILogger _logger; + private readonly IPublishedUrlProvider _publishedUrlProvider; + private readonly IRequestAccessor _requestAccessor; + private readonly IPublishedValueFallback _publishedValueFallback; + private readonly IPublicAccessChecker _publicAccessChecker; + private readonly IFileService _fileService; + private readonly IContentTypeService _contentTypeService; + private readonly IPublicAccessService _publicAccessService; + + /// + /// Initializes a new instance of the class. + /// + public PublishedRouter( + IWebRoutingSettings webRoutingSettings, + ContentFinderCollection contentFinders, + IContentLastChanceFinder contentLastChanceFinder, + IVariationContextAccessor variationContextAccessor, + IProfilingLogger proflog, + IPublishedUrlProvider publishedUrlProvider, + IRequestAccessor requestAccessor, + IPublishedValueFallback publishedValueFallback, + IPublicAccessChecker publicAccessChecker, + IFileService fileService, + IContentTypeService contentTypeService, + IPublicAccessService publicAccessService) + { + _webRoutingSettings = webRoutingSettings ?? throw new ArgumentNullException(nameof(webRoutingSettings)); + _contentFinders = contentFinders ?? throw new ArgumentNullException(nameof(contentFinders)); + _contentLastChanceFinder = contentLastChanceFinder ?? throw new ArgumentNullException(nameof(contentLastChanceFinder)); + _profilingLogger = proflog ?? throw new ArgumentNullException(nameof(proflog)); + _variationContextAccessor = variationContextAccessor ?? throw new ArgumentNullException(nameof(variationContextAccessor)); + _logger = proflog; + _publishedUrlProvider = publishedUrlProvider; + _requestAccessor = requestAccessor; + _publishedValueFallback = publishedValueFallback; + _publicAccessChecker = publicAccessChecker; + _fileService = fileService; + _contentTypeService = contentTypeService; + _publicAccessService = publicAccessService; + } + + /// + public IPublishedRequest CreateRequest(IUmbracoContext umbracoContext, Uri uri = null) + { + return new PublishedRequest(this, umbracoContext, _webRoutingSettings, uri ?? umbracoContext.CleanedUmbracoUrl); + } + + #region Request + + /// + public bool TryRouteRequest(IPublishedRequest request) + { + // disabled - is it going to change the routing? + //_pcr.OnPreparing(); + + FindDomain(request); + if (request.IsRedirect) return false; + if (request.HasPublishedContent) return true; + FindPublishedContent(request); + if (request.IsRedirect) return false; + + // don't handle anything - we just want to ensure that we find the content + //HandlePublishedContent(); + //FindTemplate(); + //FollowExternalRedirect(); + //HandleWildcardDomains(); + + // disabled - we just want to ensure that we find the content + //_pcr.OnPrepared(); + + return request.HasPublishedContent; + } + + private void SetVariationContext(string culture) + { + var variationContext = _variationContextAccessor.VariationContext; + if (variationContext != null && variationContext.Culture == culture) return; + _variationContextAccessor.VariationContext = new VariationContext(culture); + } + + /// + public bool PrepareRequest(IPublishedRequest request) + { + // 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(); + + //find domain + FindDomain(request); + + // if request has been flagged to redirect then return + // whoever called us is in charge of actually redirecting + if (request.IsRedirect) + { + return false; + } + + // set the culture on the thread - once, so it's set when running document lookups + Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture = request.Culture; + SetVariationContext(request.Culture.Name); + + //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. + if (request.PublishedContent == null) + { + // find the document & template + FindPublishedContentAndTemplate(request); + } + + // 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; + SetVariationContext(request.Culture.Name); + + // 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 PublishedRequest 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 + return ConfigureRequest(request); + } + + /// + /// Called by PrepareRequest once everything has been discovered, resolved and assigned to the PCR. This method + /// finalizes the PCR with the values assigned. + /// + /// + /// Returns false if the request was not successfully configured + /// + /// + /// 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. + /// + public bool ConfigureRequest(IPublishedRequest frequest) + { + 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; + SetVariationContext(frequest.Culture.Name); + + // 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 + + return true; + } + + /// + public void UpdateRequestToNotFound(IPublishedRequest request) + { + // clear content + var content = request.PublishedContent; + request.PublishedContent = null; + + HandlePublishedContent(request); // will go 404 + FindTemplate(request); + + // if request has been flagged to redirect then return + // whoever called us is in charge of redirecting + if (request.IsRedirect) + return; + + if (request.HasPublishedContent == false) + { + // means the engine could not find a proper document to handle 404 + // restore the saved content so we know it exists + request.PublishedContent = content; + return; + } + + 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; + } + } + + #endregion + + #region Domain + + /// + /// Finds the site root (if any) matching the http request, and updates the PublishedRequest accordingly. + /// + /// A value indicating whether a domain was found. + internal bool FindDomain(IPublishedRequest request) + { + const string tracePrefix = "FindDomain: "; + + // note - we are not handling schemes nor ports here. + + _logger.Debug("{TracePrefix}Uri={RequestUri}", tracePrefix, request.Uri); + + var domainsCache = request.UmbracoContext.PublishedSnapshot.Domains; + var domains = domainsCache.GetAll(includeWildcards: false).ToList(); + + // 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) + { + // 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) + return false; + + // invariant - always published + if (!domainDocument.ContentType.VariesByCulture()) + return true; + + // 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(); + + var defaultCulture = domainsCache.DefaultCulture; + + // try to find a domain matching the current request + var domainAndUri = DomainUtilities.SelectDomain(domains, request.Uri, defaultCulture: defaultCulture); + + // handle domain - always has a contentId and a culture + if (domainAndUri != null) + { + // matching an existing domain + _logger.Debug("{TracePrefix}Matches domain={Domain}, rootId={RootContentId}, culture={Culture}", tracePrefix, domainAndUri.Name, domainAndUri.ContentId, domainAndUri.Culture); + + request.Domain = domainAndUri; + request.Culture = domainAndUri.Culture; + + // canonical? not implemented at the moment + // if (...) + // { + // _pcr.RedirectUrl = "..."; + // return true; + // } + } + else + { + // not matching any existing domain + _logger.Debug("{TracePrefix}Matches no domain", tracePrefix); + + request.Culture = defaultCulture == null ? CultureInfo.CurrentUICulture : new CultureInfo(defaultCulture); + } + + _logger.Debug("{TracePrefix}Culture={CultureName}", tracePrefix, request.Culture.Name); + + return request.Domain != null; + } + + /// + /// Looks for wildcard domains in the path and updates Culture accordingly. + /// + internal void HandleWildcardDomains(IPublishedRequest request) + { + const string tracePrefix = "HandleWildcardDomains: "; + + if (request.HasPublishedContent == false) + return; + + var nodePath = request.PublishedContent.Path; + _logger.Debug("{TracePrefix}Path={NodePath}", tracePrefix, nodePath); + var rootNodeId = request.HasDomain ? request.Domain.ContentId : (int?)null; + var domain = DomainUtilities.FindWildcardDomainInPath(request.UmbracoContext.PublishedSnapshot.Domains.GetAll(true), nodePath, rootNodeId); + + // always has a contentId and a culture + if (domain != null) + { + request.Culture = domain.Culture; + _logger.Debug("{TracePrefix}Got domain on node {DomainContentId}, set culture to {CultureName}", tracePrefix, domain.ContentId, request.Culture.Name); + } + else + { + _logger.Debug("{TracePrefix}No match.", tracePrefix); + } + } + + #endregion + + #region Rendering engine + + 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))); + } + + #endregion + + #region Document and template + + /// + public ITemplate GetTemplate(string alias) + { + return _fileService.GetTemplate(alias); + } + + /// + /// Finds the Umbraco document (if any) matching the request, and updates the PublishedRequest accordingly. + /// + /// A value indicating whether a document and template were found. + private void FindPublishedContentAndTemplate(IPublishedRequest request) + { + _logger.Debug("FindPublishedContentAndTemplate: Path={UriAbsolutePath}", request.Uri.AbsolutePath); + + // run the document finders + FindPublishedContent(request); + + // 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) + return; + + // not handling umbracoRedirect here but after LookupDocument2 + // so internal redirect, 404, etc has precedence over redirect + + // handle not-found, redirects, access... + HandlePublishedContent(request); + + // find a template + FindTemplate(request); + + // handle umbracoRedirect + FollowExternalRedirect(request); + } + + /// + /// Tries to find the document matching the request, by running the IPublishedContentFinder instances. + /// + /// There is no finder collection. + internal void FindPublishedContent(IPublishedRequest request) + { + 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 (_profilingLogger.DebugDuration( + $"{tracePrefix}Begin finders", + $"{tracePrefix}End finders, {(request.HasPublishedContent ? "a document was found" : "no document was found")}")) + { + //iterate but return on first one that finds it + var found = _contentFinders.Any(finder => + { + _logger.Debug("Finder {ContentFinderType}", finder.GetType().FullName); + return finder.TryFindContent(request); + }); + } + + // 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(); + } + + /// + /// 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(IPublishedRequest request) + { + // because these might loop, we have to have some sort of infinite loop detection + int i = 0, j = 0; + const int maxLoop = 8; + do + { + _logger.Debug("HandlePublishedContent: Loop {LoopCounter}", i); + + // handle not found + if (request.HasPublishedContent == false) + { + request.Is404 = true; + _logger.Debug("HandlePublishedContent: No document, try last chance lookup"); + + // if it fails then give up, there isn't much more that we can do + if (_contentLastChanceFinder.TryFindContent(request) == false) + { + _logger.Debug("HandlePublishedContent: Failed to find a document, give up"); + break; + } + + _logger.Debug("HandlePublishedContent: Found a document"); + } + + // 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) + { + _logger.Debug("HandlePublishedContent: Looks like we are running into an infinite loop, abort"); + request.PublishedContent = null; + } + + _logger.Debug("HandlePublishedContent: End"); + } + + /// + /// 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(IPublishedRequest request) + { + 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(Core.Constants.Conventions.Content.InternalRedirectId) == false) + return false; + + var redirect = false; + var valid = false; + IPublishedContent internalRedirectNode = null; + var internalRedirectId = request.PublishedContent.Value(_publishedValueFallback, Core.Constants.Conventions.Content.InternalRedirectId, defaultValue: -1); + + if (internalRedirectId > 0) + { + // try and get the redirect node from a legacy integer ID + valid = true; + internalRedirectNode = request.UmbracoContext.Content.GetById(internalRedirectId); + } + else + { + var udiInternalRedirectId = request.PublishedContent.Value(_publishedValueFallback, Core.Constants.Conventions.Content.InternalRedirectId); + if (udiInternalRedirectId != null) + { + // try and get the redirect node from a UDI Guid + valid = true; + internalRedirectNode = request.UmbracoContext.Content.GetById(udiInternalRedirectId.Guid); + } + } + + if (valid == false) + { + // bad redirect - log and display the current page (legacy behavior) + _logger.Debug("FollowInternalRedirects: Failed to redirect to id={InternalRedirectId}: value is not an int nor a GuidUdi.", + request.PublishedContent.GetProperty(Core.Constants.Conventions.Content.InternalRedirectId).GetSourceValue()); + } + + if (internalRedirectNode == null) + { + _logger.Debug("FollowInternalRedirects: Failed to redirect to id={InternalRedirectId}: no such published document.", + request.PublishedContent.GetProperty(Core.Constants.Conventions.Content.InternalRedirectId).GetSourceValue()); + } + else if (internalRedirectId == request.PublishedContent.Id) + { + // redirect to self + _logger.Debug("FollowInternalRedirects: Redirecting to self, ignore"); + } + else + { + request.SetInternalRedirectPublishedContent(internalRedirectNode); // don't use .PublishedContent here + redirect = true; + _logger.Debug("FollowInternalRedirects: Redirecting to id={InternalRedirectId}", 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(IPublishedRequest request) + { + if (request.PublishedContent == null) + throw new InvalidOperationException("There is no PublishedContent."); + + var path = request.PublishedContent.Path; + + var publicAccessAttempt = _publicAccessService.IsProtected(path); + + if (publicAccessAttempt) + { + _logger.Debug("EnsurePublishedContentAccess: Page is protected, check for access"); + + var status = _publicAccessChecker.HasMemberAccessToContent(request.PublishedContent.Id); + switch (status) + { + case PublicAccessStatus.NotLoggedIn: + _logger.Debug("EnsurePublishedContentAccess: Not logged in, redirect to login page"); + SetPublishedContentAsOtherPage(request, publicAccessAttempt.Result.LoginNodeId); + break; + case PublicAccessStatus.AccessDenied: + _logger.Debug("EnsurePublishedContentAccess: Current member has not access, redirect to error page"); + SetPublishedContentAsOtherPage(request, publicAccessAttempt.Result.NoAccessNodeId); + break; + case PublicAccessStatus.LockedOut: + _logger.Debug("Current member is locked out, redirect to error page"); + SetPublishedContentAsOtherPage(request, publicAccessAttempt.Result.NoAccessNodeId); + break; + case PublicAccessStatus.NotApproved: + _logger.Debug("Current member is unapproved, redirect to error page"); + SetPublishedContentAsOtherPage(request, publicAccessAttempt.Result.NoAccessNodeId); + break; + case PublicAccessStatus.AccessAccepted: + _logger.Debug("Current member has access"); + break; + } + } + else + { + _logger.Debug("EnsurePublishedContentAccess: Page is not protected"); + } + } + + private static void SetPublishedContentAsOtherPage(IPublishedRequest request, int errorPageId) + { + if (errorPageId != request.PublishedContent.Id) + request.PublishedContent = request.UmbracoContext.PublishedSnapshot.Content.GetById(errorPageId); + } + + /// + /// Finds a template for the current node, if any. + /// + private void FindTemplate(IPublishedRequest request) + { + // 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? + + if (request.PublishedContent == null) + { + request.TemplateModel = null; + 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 + || (_webRoutingSettings.InternalRedirectPreservesTemplate && request.IsInternalRedirectPublishedContent); + var altTemplate = useAltTemplate + ? _requestAccessor.GetRequestValue(Core.Constants.Conventions.Url.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 (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( + _fileService, + _contentTypeService, + _webRoutingSettings.DisableAlternativeTemplates, + _webRoutingSettings.ValidateAlternativeTemplates, + altTemplate)) + { + // allowed, use + var template = _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) + { + _logger.Debug("FindTemplate: No template was found."); + + // 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 + { + _logger.Debug("FindTemplate: Running with template id={TemplateId} alias={TemplateAlias}", request.TemplateModel.Id, request.TemplateModel.Alias); + } + } + + private ITemplate GetTemplateModel(int? templateId) + { + if (templateId.HasValue == false || templateId.Value == default) + { + _logger.Debug("GetTemplateModel: No template."); + return null; + } + + _logger.Debug("GetTemplateModel: Get template id={TemplateId}", templateId); + + if (templateId == null) + throw new InvalidOperationException("The template is not set, the page cannot render."); + + var template = _fileService.GetTemplate(templateId.Value); + if (template == null) + throw new InvalidOperationException("The template with Id " + templateId + " does not exist, the page cannot render."); + _logger.Debug("GetTemplateModel: Got template id={TemplateId} alias={TemplateAlias}", template.Id, template.Alias); + return template; + } + + /// + /// Follows external redirection through umbracoRedirect document property. + /// + /// As per legacy, if the redirect does not work, we just ignore it. + private void FollowExternalRedirect(IPublishedRequest request) + { + if (request.HasPublishedContent == false) return; + + // don't try to find a redirect if the property doesn't exist + if (request.PublishedContent.HasProperty(Core.Constants.Conventions.Content.Redirect) == false) + return; + + var redirectId = request.PublishedContent.Value(_publishedValueFallback, Core.Constants.Conventions.Content.Redirect, defaultValue: -1); + var redirectUrl = "#"; + if (redirectId > 0) + { + redirectUrl = _publishedUrlProvider.GetUrl(redirectId); + } + else + { + // might be a UDI instead of an int Id + var redirectUdi = request.PublishedContent.Value(_publishedValueFallback, Core.Constants.Conventions.Content.Redirect); + if (redirectUdi != null) + redirectUrl = _publishedUrlProvider.GetUrl(redirectUdi.Guid); + } + if (redirectUrl != "#") + request.SetRedirect(redirectUrl); + } + + #endregion + } +} diff --git a/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs b/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs index 62b3b7f34c..ee74e56528 100644 --- a/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs +++ b/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs @@ -21,8 +21,11 @@ using Umbraco.Web.Common.Controllers; using System; using Umbraco.Web.Common.Middleware; using Umbraco.Web.Common.ModelBinding; +using Umbraco.Web.Common.Routing; +using Umbraco.Web.Common.Templates; using Umbraco.Web.Search; using Umbraco.Web.Security; +using Umbraco.Web.Templates; using Umbraco.Web.Trees; namespace Umbraco.Web.Common.Runtime @@ -99,7 +102,8 @@ namespace Umbraco.Web.Common.Runtime composition.RegisterUnique(); - + composition.RegisterUnique(); + composition.RegisterUnique(); } diff --git a/src/Umbraco.Web.Common/Templates/TemplateRenderer.cs b/src/Umbraco.Web.Common/Templates/TemplateRenderer.cs new file mode 100644 index 0000000000..72fb2c38bf --- /dev/null +++ b/src/Umbraco.Web.Common/Templates/TemplateRenderer.cs @@ -0,0 +1,198 @@ +using System; +using System.Globalization; +using System.IO; +using System.Linq; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.ViewEngines; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Core; +using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.Services; +using Umbraco.Core.Strings; +using Umbraco.Extensions; +using Umbraco.Web.Models; +using Umbraco.Web.Routing; +using Umbraco.Web.Templates; + +namespace Umbraco.Web.Common.Templates +{ + /// + /// This is used purely for the RenderTemplate functionality in Umbraco + /// + /// + /// This allows you to render an MVC template based purely off of a node id and an optional alttemplate id as string output. + /// + internal class TemplateRenderer : ITemplateRenderer + { + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly IPublishedRouter _publishedRouter; + private readonly IFileService _fileService; + private readonly ILocalizationService _languageService; + private readonly IWebRoutingSettings _webRoutingSettings; + private readonly IShortStringHelper _shortStringHelper; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly ICompositeViewEngine _viewEngine; + + public TemplateRenderer(IUmbracoContextAccessor umbracoContextAccessor, + IPublishedRouter publishedRouter, + IFileService fileService, + ILocalizationService textService, + IWebRoutingSettings webRoutingSettings, + IShortStringHelper shortStringHelper, + IHttpContextAccessor httpContextAccessor, + ICompositeViewEngine viewEngine) + { + _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); + _publishedRouter = publishedRouter ?? throw new ArgumentNullException(nameof(publishedRouter)); + _fileService = fileService ?? throw new ArgumentNullException(nameof(fileService)); + _languageService = textService ?? throw new ArgumentNullException(nameof(textService)); + _webRoutingSettings = webRoutingSettings ?? throw new ArgumentNullException(nameof(webRoutingSettings)); + _shortStringHelper = shortStringHelper ?? throw new ArgumentNullException(nameof(shortStringHelper)); + _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); + _viewEngine = viewEngine ?? throw new ArgumentNullException(nameof(viewEngine)); + } + + public void Render(int pageId, int? altTemplateId, StringWriter writer) + { + if (writer == null) throw new ArgumentNullException(nameof(writer)); + + var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + // instantiate a request and process + // important to use CleanedUmbracoUrl - lowercase path-only version of the current url, though this isn't going to matter + // terribly much for this implementation since we are just creating a doc content request to modify it's properties manually. + var contentRequest = _publishedRouter.CreateRequest(umbracoContext); + + var doc = contentRequest.UmbracoContext.Content.GetById(pageId); + + if (doc == null) + { + writer.Write("", pageId); + return; + } + + //in some cases the UmbracoContext will not have a PublishedRequest assigned to it if we are not in the + //execution of a front-end rendered page. In this case set the culture to the default. + //set the culture to the same as is currently rendering + if (umbracoContext.PublishedRequest == null) + { + var defaultLanguage = _languageService.GetAllLanguages().FirstOrDefault(); + contentRequest.Culture = defaultLanguage == null + ? CultureInfo.CurrentUICulture + : defaultLanguage.CultureInfo; + } + else + { + contentRequest.Culture = umbracoContext.PublishedRequest.Culture; + } + + //set the doc that was found by id + contentRequest.PublishedContent = doc; + //set the template, either based on the AltTemplate found or the standard template of the doc + var templateId = _webRoutingSettings.DisableAlternativeTemplates || !altTemplateId.HasValue + ? doc.TemplateId + : altTemplateId.Value; + if (templateId.HasValue) + contentRequest.TemplateModel = _fileService.GetTemplate(templateId.Value); + + //if there is not template then exit + if (contentRequest.HasTemplate == false) + { + if (altTemplateId.HasValue == false) + { + writer.Write("", doc.TemplateId); + } + else + { + writer.Write("", altTemplateId); + } + return; + } + + //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. + SaveExistingItems(out var oldPublishedRequest); + + try + { + //set the new items on context objects for this templates execution + SetNewItemsOnContextObjects(contentRequest); + + //Render the template + ExecuteTemplateRendering(writer, contentRequest); + } + finally + { + //restore items on context objects to continuing rendering the parent template + RestoreItems(oldPublishedRequest); + } + + } + + private void ExecuteTemplateRendering(TextWriter sw, IPublishedRequest request) + { + var httpContext = _httpContextAccessor.GetRequiredHttpContext(); + + var viewResult = _viewEngine.GetView(null, $"~/Views/{request.TemplateAlias}.cshtml", false); + + if (viewResult.Success == false) + { + throw new InvalidOperationException($"A view with the name {request.TemplateAlias} could not be found"); + } + + var modelMetadataProvider = httpContext.RequestServices.GetRequiredService(); + var tempDataProvider = httpContext.RequestServices.GetRequiredService(); + + var viewData = new ViewDataDictionary(modelMetadataProvider, new ModelStateDictionary()) + { + Model = new ContentModel(request.PublishedContent) + }; + + var writer = new StringWriter(); + var viewContext = new ViewContext( + new ActionContext(httpContext, httpContext.GetRouteData(), new ControllerActionDescriptor()), + viewResult.View, + viewData, + new TempDataDictionary(httpContext, tempDataProvider), + writer, + new HtmlHelperOptions() + ); + + + viewResult.View.RenderAsync(viewContext).GetAwaiter().GetResult(); + + var output = writer.GetStringBuilder().ToString(); + + sw.Write(output); + } + + private void SetNewItemsOnContextObjects(IPublishedRequest request) + { + //now, set the new ones for this page execution + _umbracoContextAccessor.UmbracoContext.PublishedRequest = request; + } + + /// + /// Save all items that we know are used for rendering execution to variables so we can restore after rendering + /// + private void SaveExistingItems(out IPublishedRequest oldPublishedRequest) + { + //Many objects require that these legacy items are in the http context items... before we render this template we need to first + //save the values in them so that we can re-set them after we render so the rest of the execution works as per normal + oldPublishedRequest = _umbracoContextAccessor.UmbracoContext.PublishedRequest; + } + + /// + /// Restores all items back to their context's to continue normal page rendering execution + /// + private void RestoreItems(IPublishedRequest oldPublishedRequest) + { + _umbracoContextAccessor.UmbracoContext.PublishedRequest = oldPublishedRequest; + } + } +} diff --git a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj index 9f6509cd82..561e68fe6e 100644 --- a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj +++ b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj @@ -31,5 +31,10 @@ <_Parameter1>Umbraco.Tests.UnitTests + + + + + diff --git a/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj b/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj index 8f7b78b128..db3fd4b085 100644 --- a/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj +++ b/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj @@ -61,6 +61,14 @@ Designer + + true + PreserveNewest + + + true + PreserveNewest + diff --git a/src/Umbraco.Web.UI.NetCore/appsettings.json b/src/Umbraco.Web.UI.NetCore/appsettings.json index 383cc86af9..8a1ccba072 100644 --- a/src/Umbraco.Web.UI.NetCore/appsettings.json +++ b/src/Umbraco.Web.UI.NetCore/appsettings.json @@ -69,13 +69,13 @@ }, "Content": { "Errors": { - "Error404": { - "default": "1047", - "en-US": "$site/error [@name = 'error']", - "en-UK": "8560867F-B88F-4C74-A9A4-679D8E5B3BFC" - } + "Error404": { + "default": "1047", + "en-US": "$site/error [@name = 'error']", + "en-UK": "8560867F-B88F-4C74-A9A4-679D8E5B3BFC" + } }, - "LoginBackgroundImage": "assets/img/login.jpg" + "LoginBackgroundImage": "assets/img/login.jpg" }, "RequestHandler": { "AddTrailingSlash": true, diff --git a/src/Umbraco.Web/Composing/Current.cs b/src/Umbraco.Web/Composing/Current.cs index 1ed217cc78..7ce8890018 100644 --- a/src/Umbraco.Web/Composing/Current.cs +++ b/src/Umbraco.Web/Composing/Current.cs @@ -11,6 +11,7 @@ using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.Hosting; using Umbraco.Core.Mapping; +using Umbraco.Core.Net; using Umbraco.Net; using Umbraco.Core.PackageActions; using Umbraco.Core.Packaging; @@ -121,7 +122,7 @@ namespace Umbraco.Web.Composing => Factory.GetInstance(); public static ITagQuery TagQuery => Factory.GetInstance(); - + public static IRuntimeMinifier RuntimeMinifier => Factory.GetInstance(); diff --git a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs index e30467f2b9..0b001e7229 100644 --- a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs @@ -188,10 +188,6 @@ namespace Umbraco.Web.Editors "mediaTypeApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( controller => controller.GetAllowedChildren(0)) }, - { - "macroRenderingApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( - controller => controller.GetMacroParameters(0)) - }, { "currentUserApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( diff --git a/src/Umbraco.Web/Runtime/WebInitialComposer.cs b/src/Umbraco.Web/Runtime/WebInitialComposer.cs index 0454f42b75..99a0c12339 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComposer.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComposer.cs @@ -4,6 +4,7 @@ using Microsoft.AspNet.SignalR; using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Dictionary; +using Umbraco.Core.Net; using Umbraco.Core.Runtime; using Umbraco.Core.Security; using Umbraco.Core.Services; @@ -42,7 +43,6 @@ namespace Umbraco.Web.Runtime composition.RegisterUnique(); - composition.RegisterUnique(); // register the umbraco helper - this is Transient! very important! // also, if not level.Run, we cannot really use the helper (during upgrade...) diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index e010a68ca8..fc92b6a747 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -304,7 +304,6 @@ - @@ -326,7 +325,6 @@ - @@ -346,7 +344,6 @@ - diff --git a/src/Umbraco.Web/UmbracoHelper.cs b/src/Umbraco.Web/UmbracoHelper.cs index 764647988a..469837c5f2 100644 --- a/src/Umbraco.Web/UmbracoHelper.cs +++ b/src/Umbraco.Web/UmbracoHelper.cs @@ -7,6 +7,8 @@ using Umbraco.Composing; using Umbraco.Core; using Umbraco.Core.Dictionary; using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Net; +using Umbraco.Core.Strings; using Umbraco.Core.Xml; using Umbraco.Web.Mvc; @@ -100,7 +102,7 @@ namespace Umbraco.Web /// /// If not specified, will use the template assigned to the node /// - public IHtmlString RenderTemplate(int contentId, int? altTemplateId = null) + public IHtmlEncodedString RenderTemplate(int contentId, int? altTemplateId = null) { return _componentRenderer.RenderTemplate(contentId, altTemplateId); } @@ -112,7 +114,7 @@ namespace Umbraco.Web /// /// The alias. /// - public IHtmlString RenderMacro(string alias) + public IHtmlEncodedString RenderMacro(string alias) { return _componentRenderer.RenderMacro(AssignedContentItem?.Id ?? 0, alias, null); } @@ -123,7 +125,7 @@ namespace Umbraco.Web /// The alias. /// The parameters. /// - public IHtmlString RenderMacro(string alias, object parameters) + public IHtmlEncodedString RenderMacro(string alias, object parameters) { return _componentRenderer.RenderMacro(AssignedContentItem?.Id ?? 0, alias, parameters?.ToDictionary()); } @@ -134,7 +136,7 @@ namespace Umbraco.Web /// The alias. /// The parameters. /// - public IHtmlString RenderMacro(string alias, IDictionary parameters) + public IHtmlEncodedString RenderMacro(string alias, IDictionary parameters) { return _componentRenderer.RenderMacro(AssignedContentItem?.Id ?? 0, alias, parameters); }