diff --git a/src/Umbraco.Core/Configuration/Grid/GridConfig.cs b/src/Umbraco.Core/Configuration/Grid/GridConfig.cs index 72c720e3d6..363dc7b048 100644 --- a/src/Umbraco.Core/Configuration/Grid/GridConfig.cs +++ b/src/Umbraco.Core/Configuration/Grid/GridConfig.cs @@ -1,7 +1,5 @@ -using System.IO; -using Umbraco.Core.Cache; +using Umbraco.Core.Cache; using Umbraco.Core.Hosting; -using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Manifest; using Umbraco.Core.Serialization; @@ -10,9 +8,9 @@ namespace Umbraco.Core.Configuration.Grid { public class GridConfig : IGridConfig { - public GridConfig(AppCaches appCaches, IIOHelper ioHelper, IManifestParser manifestParser, IJsonSerializer jsonSerializer, IHostingEnvironment hostingEnvironment) + public GridConfig(AppCaches appCaches, IManifestParser manifestParser, IJsonSerializer jsonSerializer, IHostingEnvironment hostingEnvironment, ILogger logger) { - EditorsConfig = new GridEditorsConfig(appCaches, ioHelper, manifestParser, jsonSerializer, hostingEnvironment.IsDebugMode); + EditorsConfig = new GridEditorsConfig(appCaches, hostingEnvironment, manifestParser, jsonSerializer, logger); } public IGridEditorsConfig EditorsConfig { get; } diff --git a/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs b/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs index 410c83ff1a..6cf985fecb 100644 --- a/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs +++ b/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs @@ -1,9 +1,8 @@ using System; using System.Collections.Generic; using System.IO; -using Umbraco.Composing; using Umbraco.Core.Cache; -using Umbraco.Core.IO; +using Umbraco.Core.Hosting; using Umbraco.Core.Logging; using Umbraco.Core.Manifest; using Umbraco.Core.PropertyEditors; @@ -14,18 +13,19 @@ namespace Umbraco.Core.Configuration.Grid internal class GridEditorsConfig : IGridEditorsConfig { private readonly AppCaches _appCaches; - private readonly IIOHelper _ioHelper; + private readonly IHostingEnvironment _hostingEnvironment; private readonly IManifestParser _manifestParser; - private readonly bool _isDebug; - private readonly IJsonSerializer _jsonSerializer; - public GridEditorsConfig(AppCaches appCaches, IIOHelper ioHelper, IManifestParser manifestParser,IJsonSerializer jsonSerializer, bool isDebug) + private readonly IJsonSerializer _jsonSerializer; + private readonly ILogger _logger; + + public GridEditorsConfig(AppCaches appCaches, IHostingEnvironment hostingEnvironment, IManifestParser manifestParser,IJsonSerializer jsonSerializer, ILogger logger) { _appCaches = appCaches; - _ioHelper = ioHelper; + _hostingEnvironment = hostingEnvironment; _manifestParser = manifestParser; _jsonSerializer = jsonSerializer; - _isDebug = isDebug; + _logger = logger; } public IEnumerable Editors @@ -34,7 +34,7 @@ namespace Umbraco.Core.Configuration.Grid { List GetResult() { - var configFolder = new DirectoryInfo(_ioHelper.MapPath(Constants.SystemDirectories.Config)); + var configFolder = new DirectoryInfo(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Config)); var editors = new List(); var gridConfig = Path.Combine(configFolder.FullName, "grid.editors.config.js"); if (File.Exists(gridConfig)) @@ -47,7 +47,7 @@ namespace Umbraco.Core.Configuration.Grid } catch (Exception ex) { - Current.Logger.Error(ex, "Could not parse the contents of grid.editors.config.js into a JSON array '{Json}", sourceString); + _logger.Error(ex, "Could not parse the contents of grid.editors.config.js into a JSON array '{Json}", sourceString); } } @@ -61,7 +61,7 @@ namespace Umbraco.Core.Configuration.Grid } //cache the result if debugging is disabled - var result = _isDebug + var result = _hostingEnvironment.IsDebugMode ? GetResult() : _appCaches.RuntimeCache.GetCacheItem>(typeof(GridEditorsConfig) + ".Editors",GetResult, TimeSpan.FromMinutes(10)); diff --git a/src/Umbraco.Tests.Common/TestHelperBase.cs b/src/Umbraco.Tests.Common/TestHelperBase.cs index ce20e723df..b79fb67fe7 100644 --- a/src/Umbraco.Tests.Common/TestHelperBase.cs +++ b/src/Umbraco.Tests.Common/TestHelperBase.cs @@ -16,7 +16,6 @@ using Umbraco.Net; using Umbraco.Core.Persistence; using Umbraco.Core.Serialization; using Umbraco.Core.Strings; -using Umbraco.Core.Sync; using Umbraco.Web; using Umbraco.Web.Routing; diff --git a/src/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj b/src/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj index e203d378ef..1b49f4d7bc 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj +++ b/src/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj @@ -30,6 +30,7 @@ + diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj b/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj index c5732870f3..f2cb3489cc 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj @@ -21,6 +21,7 @@ + diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Controllers/SurfaceControllerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Controllers/SurfaceControllerTests.cs new file mode 100644 index 0000000000..1b4630c2da --- /dev/null +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Controllers/SurfaceControllerTests.cs @@ -0,0 +1,236 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Moq; +using NUnit.Framework; +using Umbraco.Core.Cache; +using Umbraco.Core.Hosting; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Services; +using Umbraco.Tests.Common; +using Umbraco.Tests.Common.Builders; +using Umbraco.Tests.Testing; +using Umbraco.Web; +using Umbraco.Web.PublishedCache; +using Umbraco.Web.Routing; +using Umbraco.Web.Website; +using Umbraco.Web.Website.Controllers; + +namespace Umbraco.Tests.Integration +{ + [TestFixture] + [UmbracoTest(WithApplication = true)] + public class SurfaceControllerTests + { + private IUmbracoContextAccessor _umbracoContextAccessor; + + [SetUp] + public void SetUp() + { + _umbracoContextAccessor = new TestUmbracoContextAccessor(); + } + + [Test] + public void Can_Construct_And_Get_Result() + { + var httpContextAccessor = Mock.Of(); + var hostingEnvironment = Mock.Of(); + var globalSettings = new GlobalSettingsBuilder().Build(); + + var umbracoContextFactory = new UmbracoContextFactory( + _umbracoContextAccessor, + Mock.Of(), + new TestVariationContextAccessor(), + new TestDefaultCultureAccessor(), + globalSettings, + Mock.Of(), + hostingEnvironment, + new UriUtility(hostingEnvironment), + httpContextAccessor, + Mock.Of(), + Mock.Of()); + + var umbracoContextReference = umbracoContextFactory.EnsureUmbracoContext(); + var umbracoContext = umbracoContextReference.UmbracoContext; + + var umbracoContextAccessor = new TestUmbracoContextAccessor(umbracoContext); + + var ctrl = new TestSurfaceController(umbracoContextAccessor, Mock.Of(), Mock.Of()); + + var result = ctrl.Index(); + + Assert.IsNotNull(result); + } + + [Test] + public void Umbraco_Context_Not_Null() + { + var globalSettings = new GlobalSettingsBuilder().Build(); + var httpContextAccessor = Mock.Of(); + var hostingEnvironment = Mock.Of(); + + var umbracoContextFactory = new UmbracoContextFactory( + _umbracoContextAccessor, + Mock.Of(), + new TestVariationContextAccessor(), + new TestDefaultCultureAccessor(), + globalSettings, + Mock.Of(), + hostingEnvironment, + new UriUtility(hostingEnvironment), + httpContextAccessor, + Mock.Of(), + Mock.Of()); + + var umbracoContextReference = umbracoContextFactory.EnsureUmbracoContext(); + var umbCtx = umbracoContextReference.UmbracoContext; + + var umbracoContextAccessor = new TestUmbracoContextAccessor(umbCtx); + + var ctrl = new TestSurfaceController(umbracoContextAccessor, Mock.Of(), Mock.Of()); + + Assert.IsNotNull(ctrl.UmbracoContext); + } + + [Test] + public void Can_Lookup_Content() + { + var publishedSnapshot = new Mock(); + publishedSnapshot.Setup(x => x.Members).Returns(Mock.Of()); + var content = new Mock(); + content.Setup(x => x.Id).Returns(2); + + var publishedSnapshotService = new Mock(); + var httpContextAccessor = Mock.Of(); + var hostingEnvironment = Mock.Of(); + var globalSettings = new GlobalSettingsBuilder().Build(); + + var umbracoContextFactory = new UmbracoContextFactory( + _umbracoContextAccessor, + publishedSnapshotService.Object, + new TestVariationContextAccessor(), + new TestDefaultCultureAccessor(), + globalSettings, + Mock.Of(), + hostingEnvironment, + new UriUtility(hostingEnvironment), + httpContextAccessor, + Mock.Of(), + Mock.Of()); + + var umbracoContextReference = umbracoContextFactory.EnsureUmbracoContext(); + var umbracoContext = umbracoContextReference.UmbracoContext; + + var umbracoContextAccessor = new TestUmbracoContextAccessor(umbracoContext); + + var publishedContentQuery = Mock.Of(query => query.Content(2) == content.Object); + + var ctrl = new TestSurfaceController(umbracoContextAccessor, publishedContentQuery, Mock.Of()); + var result = ctrl.GetContent(2) as PublishedContentResult; + + Assert.IsNotNull(result); + Assert.IsNotNull(result.Content); + Assert.AreEqual(2, result.Content.Id); + } + + + [Test] + public void Mock_Current_Page() + { + var globalSettings = new GlobalSettingsBuilder().Build(); + var httpContextAccessor = Mock.Of(); + var hostingEnvironment = Mock.Of(); + + var umbracoContextFactory = new UmbracoContextFactory( + _umbracoContextAccessor, + Mock.Of(), + new TestVariationContextAccessor(), + new TestDefaultCultureAccessor(), + globalSettings, + Mock.Of(), + hostingEnvironment, + new UriUtility(hostingEnvironment), + httpContextAccessor, + Mock.Of(), + Mock.Of()); + + var umbracoContextReference = umbracoContextFactory.EnsureUmbracoContext(); + var umbracoContext = umbracoContextReference.UmbracoContext; + + var umbracoContextAccessor = new TestUmbracoContextAccessor(umbracoContext); + + var content = Mock.Of(publishedContent => publishedContent.Id == 12345); + + var publishedRequestMock = new Mock(); + publishedRequestMock.Setup(x => x.PublishedContent).Returns(content); + + var routeDefinition = new RouteDefinition + { + PublishedRequest = publishedRequestMock.Object + }; + + var routeData = new RouteData(); + routeData.DataTokens.Add(Core.Constants.Web.UmbracoRouteDefinitionDataToken, routeDefinition); + + var ctrl = new TestSurfaceController(umbracoContextAccessor, Mock.Of(), Mock.Of()); + ctrl.ControllerContext = new ControllerContext() + { + HttpContext = Mock.Of(), + RouteData = routeData + }; + + var result = ctrl.GetContentFromCurrentPage() as PublishedContentResult; + + Assert.AreEqual(12345, result.Content.Id); + } + + + public class TestSurfaceController : SurfaceController + { + private readonly IPublishedContentQuery _publishedContentQuery; + + public TestSurfaceController(IUmbracoContextAccessor umbracoContextAccessor, IPublishedContentQuery publishedContentQuery, IPublishedUrlProvider publishedUrlProvider) + : base(umbracoContextAccessor, null, ServiceContext.CreatePartial(), AppCaches.Disabled, null, null, publishedUrlProvider) + { + _publishedContentQuery = publishedContentQuery; + } + + public IActionResult Index() + { + // ReSharper disable once Mvc.ViewNotResolved + return View(); + } + + public IActionResult GetContent(int id) + { + var content = _publishedContentQuery.Content(id); + + return new PublishedContentResult(content); + } + + public IActionResult GetContentFromCurrentPage() + { + var content = CurrentPage; + + return new PublishedContentResult(content); + } + } + + public class PublishedContentResult : IActionResult + { + public IPublishedContent Content { get; set; } + + public PublishedContentResult(IPublishedContent content) + { + Content = content; + } + + public Task ExecuteResultAsync(ActionContext context) + { + return Task.CompletedTask; + } + } + } +} diff --git a/src/Umbraco.Web.BackOffice/ActionResults/JsonNetResult.cs b/src/Umbraco.Web.BackOffice/ActionResults/JsonNetResult.cs new file mode 100644 index 0000000000..db5c7a8510 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/ActionResults/JsonNetResult.cs @@ -0,0 +1,52 @@ +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; + +namespace Umbraco.Web.BackOffice.ActionResults +{ + /// + /// Custom json result using newtonsoft json.net + /// + public class JsonNetResult : IActionResult + { + public Encoding ContentEncoding { get; set; } + public string ContentType { get; set; } + public object Data { get; set; } + + public JsonSerializerSettings SerializerSettings { get; set; } + public Formatting Formatting { get; set; } + + public JsonNetResult() + { + SerializerSettings = new JsonSerializerSettings(); + } + + public Task ExecuteResultAsync(ActionContext context) + { + if (context is null) + throw new ArgumentNullException(nameof(context)); + + var response = context.HttpContext.Response; + + response.ContentType = string.IsNullOrEmpty(ContentType) == false + ? ContentType + : System.Net.Mime.MediaTypeNames.Application.Json; + + if (!(ContentEncoding is null)) + response.Headers.Add(Microsoft.Net.Http.Headers.HeaderNames.ContentEncoding, ContentEncoding.ToString()); + + if (!(Data is null)) + { + using var bodyWriter = new StreamWriter(response.Body); + using var writer = new JsonTextWriter(bodyWriter) { Formatting = Formatting }; + var serializer = JsonSerializer.Create(SerializerSettings); + serializer.Serialize(writer, Data); + } + + return Task.CompletedTask; + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs index 429b2e625a..d776749e6b 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs @@ -1,9 +1,18 @@ -using System.Threading.Tasks; +using System; +using System.Globalization; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; +using Umbraco.Core; using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.Grid; using Umbraco.Core.Hosting; +using Umbraco.Core.Services; using Umbraco.Core.WebAssets; using Umbraco.Net; +using Umbraco.Web.BackOffice.ActionResults; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.ActionResults; using Umbraco.Web.WebAssets; @@ -16,13 +25,19 @@ namespace Umbraco.Web.BackOffice.Controllers private readonly IGlobalSettings _globalSettings; private readonly IHostingEnvironment _hostingEnvironment; private readonly IUmbracoApplicationLifetime _umbracoApplicationLifetime; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly ILocalizedTextService _textService; + private readonly IGridConfig _gridConfig; - public BackOfficeController(IRuntimeMinifier runtimeMinifier, IGlobalSettings globalSettings, IHostingEnvironment hostingEnvironment, IUmbracoApplicationLifetime umbracoApplicationLifetime) + public BackOfficeController(IRuntimeMinifier runtimeMinifier, IGlobalSettings globalSettings, IHostingEnvironment hostingEnvironment, IUmbracoApplicationLifetime umbracoApplicationLifetime, IUmbracoContextAccessor umbracoContextAccessor, ILocalizedTextService textService, IGridConfig gridConfig) { _runtimeMinifier = runtimeMinifier; _globalSettings = globalSettings; _hostingEnvironment = hostingEnvironment; _umbracoApplicationLifetime = umbracoApplicationLifetime; + _umbracoContextAccessor = umbracoContextAccessor; + _textService = textService; + _gridConfig = gridConfig ?? throw new ArgumentNullException(nameof(gridConfig)); } // GET @@ -42,5 +57,53 @@ namespace Umbraco.Web.BackOffice.Controllers return new JavaScriptResult(result); } + + /// + /// Get the json localized text for a given culture or the culture for the current user + /// + /// + /// + [HttpGet] + public JsonNetResult LocalizedText(string culture = null) + { + var securityHelper = _umbracoContextAccessor.GetRequiredUmbracoContext().Security; + var isAuthenticated = securityHelper.IsAuthenticated(); + + var cultureInfo = string.IsNullOrWhiteSpace(culture) + //if the user is logged in, get their culture, otherwise default to 'en' + ? isAuthenticated + //current culture is set at the very beginning of each request + ? Thread.CurrentThread.CurrentCulture + : CultureInfo.GetCultureInfo(_globalSettings.DefaultUILanguage) + : CultureInfo.GetCultureInfo(culture); + + var allValues = _textService.GetAllStoredValues(cultureInfo); + var pathedValues = allValues.Select(kv => + { + var slashIndex = kv.Key.IndexOf('/'); + var areaAlias = kv.Key.Substring(0, slashIndex); + var valueAlias = kv.Key.Substring(slashIndex + 1); + return new + { + areaAlias, + valueAlias, + value = kv.Value + }; + }); + + var nestedDictionary = pathedValues + .GroupBy(pv => pv.areaAlias) + .ToDictionary(pv => pv.Key, pv => + pv.ToDictionary(pve => pve.valueAlias, pve => pve.value)); + + return new JsonNetResult { Data = nestedDictionary, Formatting = Formatting.None }; + } + + //[UmbracoAuthorize(Order = 0)] TODO: Re-implement UmbracoAuthorizeAttribute + [HttpGet] + public JsonNetResult GetGridConfig() + { + return new JsonNetResult { Data = _gridConfig.EditorsConfig.Editors, Formatting = Formatting.None }; + } } } diff --git a/src/Umbraco.Web.Common/Security/WebSecurity.cs b/src/Umbraco.Web.Common/Security/WebSecurity.cs index 5f54d2e9ee..e87720dc86 100644 --- a/src/Umbraco.Web.Common/Security/WebSecurity.cs +++ b/src/Umbraco.Web.Common/Security/WebSecurity.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Text; +using Umbraco.Composing; using Umbraco.Core; using Umbraco.Core.Models.Membership; using Umbraco.Web.Security; @@ -11,46 +12,46 @@ namespace Umbraco.Web.Common.Security public class WebSecurity : IWebSecurity { - public IUser CurrentUser => throw new NotImplementedException(); + public IUser CurrentUser => new User(Current.Configs.Global()); public ValidateRequestAttempt AuthorizeRequest(bool throwExceptions = false) { - throw new NotImplementedException(); + return ValidateRequestAttempt.Success; } public void ClearCurrentLogin() { - throw new NotImplementedException(); + //throw new NotImplementedException(); } public Attempt GetUserId() { - throw new NotImplementedException(); + return Attempt.Succeed(-1); } public bool IsAuthenticated() { - throw new NotImplementedException(); + return true; } public double PerformLogin(int userId) { - throw new NotImplementedException(); + return 100; } public bool UserHasSectionAccess(string section, IUser user) { - throw new NotImplementedException(); + return true; } public bool ValidateCurrentUser() { - throw new NotImplementedException(); + return true; } public ValidateRequestAttempt ValidateCurrentUser(bool throwExceptions, bool requiresApproval = true) { - throw new NotImplementedException(); + return ValidateRequestAttempt.Success; } } } diff --git a/src/Umbraco.Web.UI/config/grid.editors.config.js b/src/Umbraco.Web.UI.NetCore/Config/grid.editors.config.js similarity index 100% rename from src/Umbraco.Web.UI/config/grid.editors.config.js rename to src/Umbraco.Web.UI.NetCore/Config/grid.editors.config.js diff --git a/src/Umbraco.Web.UI.NetCore/Startup.cs b/src/Umbraco.Web.UI.NetCore/Startup.cs index 3a86978e17..a1f0fdae23 100644 --- a/src/Umbraco.Web.UI.NetCore/Startup.cs +++ b/src/Umbraco.Web.UI.NetCore/Startup.cs @@ -1,6 +1,7 @@ using System; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -42,6 +43,13 @@ namespace Umbraco.Web.UI.BackOffice { options.ShouldProfile = request => false; // WebProfiler determine and start profiling. We should not use the MiniProfilerMiddleware to also profile }); + + // If using Kestrel: https://stackoverflow.com/a/55196057 + services.Configure(options => + { + options.AllowSynchronousIO = true; + }); + } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 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 966cb9c86e..8e5df46c55 100644 --- a/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj +++ b/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj @@ -63,6 +63,7 @@ + diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 5e66765379..8c0b98787c 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -142,7 +142,6 @@ True Settings.settings - diff --git a/src/Umbraco.Web.Website/ActionResults/RedirectToUmbracoPageResult.cs b/src/Umbraco.Web.Website/ActionResults/RedirectToUmbracoPageResult.cs new file mode 100644 index 0000000000..d271e63e2f --- /dev/null +++ b/src/Umbraco.Web.Website/ActionResults/RedirectToUmbracoPageResult.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections.Specialized; +using System.Linq; +using System.Threading.Tasks; +using System.Web; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Core; +using Umbraco.Core.IO; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Web.Routing; + +namespace Umbraco.Web.Website.ActionResults +{ + /// + /// Redirects to an Umbraco page by Id or Entity + /// + public class RedirectToUmbracoPageResult : IActionResult + { + private IPublishedContent _publishedContent; + private readonly int _pageId; + private readonly NameValueCollection _queryStringValues; + private readonly IPublishedUrlProvider _publishedUrlProvider; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private string _url; + + private string Url + { + get + { + if (!string.IsNullOrWhiteSpace(_url)) return _url; + + if (PublishedContent is null) + throw new InvalidOperationException($"Cannot redirect, no entity was found for id {PageId}"); + + var result = _publishedUrlProvider.GetUrl(PublishedContent.Id); + + if (result == "#") + throw new InvalidOperationException( + $"Could not route to entity with id {PageId}, the NiceUrlProvider could not generate a URL"); + + _url = result; + + return _url; + } + } + + private int PageId => _pageId; + + private IPublishedContent PublishedContent + { + get + { + if (!(_publishedContent is null)) return _publishedContent; + + //need to get the URL for the page + _publishedContent = _umbracoContextAccessor.GetRequiredUmbracoContext().Content.GetById(_pageId); + + return _publishedContent; + } + } + + /// + /// Creates a new RedirectToUmbracoResult + /// + /// + /// + public RedirectToUmbracoPageResult(int pageId, IPublishedUrlProvider publishedUrlProvider, IUmbracoContextAccessor umbracoContextAccessor) + { + _pageId = pageId; + _publishedUrlProvider = publishedUrlProvider; + _umbracoContextAccessor = umbracoContextAccessor; + } + + /// + /// Creates a new RedirectToUmbracoResult + /// + /// + /// + /// + public RedirectToUmbracoPageResult(int pageId, NameValueCollection queryStringValues, IPublishedUrlProvider publishedUrlProvider, IUmbracoContextAccessor umbracoContextAccessor) + { + _pageId = pageId; + _queryStringValues = queryStringValues; + _publishedUrlProvider = publishedUrlProvider; + _umbracoContextAccessor = umbracoContextAccessor; + } + + /// + /// Creates a new RedirectToUmbracoResult + /// + /// + /// + /// + public RedirectToUmbracoPageResult(int pageId, string queryString, IPublishedUrlProvider publishedUrlProvider, IUmbracoContextAccessor umbracoContextAccessor) + { + _pageId = pageId; + _queryStringValues = ParseQueryString(queryString); + _publishedUrlProvider = publishedUrlProvider; + _umbracoContextAccessor = umbracoContextAccessor; + } + + /// + /// Creates a new RedirectToUmbracoResult + /// + /// + /// + /// + public RedirectToUmbracoPageResult(IPublishedContent publishedContent, IPublishedUrlProvider publishedUrlProvider, IUmbracoContextAccessor umbracoContextAccessor) + { + _publishedContent = publishedContent; + _pageId = publishedContent.Id; + _publishedUrlProvider = publishedUrlProvider; + _umbracoContextAccessor = umbracoContextAccessor; + } + + /// + /// Creates a new RedirectToUmbracoResult + /// + /// + /// + /// + /// + public RedirectToUmbracoPageResult(IPublishedContent publishedContent, NameValueCollection queryStringValues, IPublishedUrlProvider publishedUrlProvider, IUmbracoContextAccessor umbracoContextAccessor) + { + _publishedContent = publishedContent; + _pageId = publishedContent.Id; + _queryStringValues = queryStringValues; + _publishedUrlProvider = publishedUrlProvider; + _umbracoContextAccessor = umbracoContextAccessor; + } + + /// + /// Creates a new RedirectToUmbracoResult + /// + /// + /// + /// + /// + public RedirectToUmbracoPageResult(IPublishedContent publishedContent, string queryString, IPublishedUrlProvider publishedUrlProvider, IUmbracoContextAccessor umbracoContextAccessor) + { + _publishedContent = publishedContent; + _pageId = publishedContent.Id; + _queryStringValues = ParseQueryString(queryString); + _publishedUrlProvider = publishedUrlProvider; + _umbracoContextAccessor = umbracoContextAccessor; + } + + public Task ExecuteResultAsync(ActionContext context) + { + if (context is null) throw new ArgumentNullException(nameof(context)); + + var httpContext = context.HttpContext; + var ioHelper = httpContext.RequestServices.GetRequiredService(); + var destinationUrl = ioHelper.ResolveUrl(Url); + + if (!(_queryStringValues is null) && _queryStringValues.Count > 0) + { + destinationUrl += "?" + string.Join("&", + _queryStringValues.AllKeys.Select(x => x + "=" + HttpUtility.UrlEncode(_queryStringValues[x]))); + } + + var tempDataDictionaryFactory = context.HttpContext.RequestServices.GetRequiredService(); + var tempData = tempDataDictionaryFactory.GetTempData(context.HttpContext); + tempData?.Keep(); + + httpContext.Response.Redirect(destinationUrl); + + return Task.CompletedTask; + } + + private NameValueCollection ParseQueryString(string queryString) + { + return !string.IsNullOrEmpty(queryString) ? HttpUtility.ParseQueryString(queryString) : null; + } + } +} diff --git a/src/Umbraco.Web.Website/ActionResults/RedirectToUmbracoUrlResult.cs b/src/Umbraco.Web.Website/ActionResults/RedirectToUmbracoUrlResult.cs new file mode 100644 index 0000000000..6a7b4d678d --- /dev/null +++ b/src/Umbraco.Web.Website/ActionResults/RedirectToUmbracoUrlResult.cs @@ -0,0 +1,44 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.Extensions.DependencyInjection; + +namespace Umbraco.Web.Website.ActionResults +{ + /// + /// Redirects to the current URL rendering an Umbraco page including it's query strings + /// + /// + /// This is useful if you need to redirect + /// to the current page but the current page is actually a rewritten URL normally done with something like + /// Server.Transfer. It is also handy if you want to persist the query strings. + /// + public class RedirectToUmbracoUrlResult : IActionResult + { + private readonly IUmbracoContext _umbracoContext; + + /// + /// Creates a new RedirectToUmbracoResult + /// + /// + public RedirectToUmbracoUrlResult(IUmbracoContext umbracoContext) + { + _umbracoContext = umbracoContext; + } + + public Task ExecuteResultAsync(ActionContext context) + { + if (context is null) throw new ArgumentNullException(nameof(context)); + + var destinationUrl = _umbracoContext.OriginalRequestUrl.PathAndQuery; + var tempDataDictionaryFactory = context.HttpContext.RequestServices.GetRequiredService(); + var tempData = tempDataDictionaryFactory.GetTempData(context.HttpContext); + tempData?.Keep(); + + context.HttpContext.Response.Redirect(destinationUrl); + + return Task.CompletedTask; + } + } +} diff --git a/src/Umbraco.Web.Website/ActionResults/UmbracoPageResult.cs b/src/Umbraco.Web.Website/ActionResults/UmbracoPageResult.cs new file mode 100644 index 0000000000..62942541e9 --- /dev/null +++ b/src/Umbraco.Web.Website/ActionResults/UmbracoPageResult.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Filters; +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.Logging; +using Umbraco.Extensions; + +namespace Umbraco.Web.Website.ActionResults +{ + /// + /// Used by posted forms to proxy the result to the page in which the current URL matches on + /// + public class UmbracoPageResult : IActionResult + { + private readonly IProfilingLogger _profilingLogger; + + public UmbracoPageResult(IProfilingLogger profilingLogger) + { + _profilingLogger = profilingLogger; + } + + public Task ExecuteResultAsync(ActionContext context) + { + var routeData = context.RouteData; + + ResetRouteData(routeData); + ValidateRouteData(routeData); + + var factory = context.HttpContext.RequestServices.GetRequiredService(); + Controller controller = null; + + if (!(context is ControllerContext controllerContext)) + return Task.FromCanceled(new System.Threading.CancellationToken()); + + try + { + controller = CreateController(controllerContext, factory); + + CopyControllerData(controllerContext, controller); + + ExecuteControllerAction(controllerContext, controller); + } + finally + { + CleanupController(controllerContext, controller, factory); + } + + return Task.CompletedTask; + } + + /// + /// Executes the controller action + /// + private void ExecuteControllerAction(ControllerContext context, Controller controller) + { + using (_profilingLogger.TraceDuration("Executing Umbraco RouteDefinition controller", "Finished")) + { + //TODO I do not think this will work, We need to test this, when we can, in the .NET Core executable. + var aec = new ActionExecutingContext(context, new List(), new Dictionary(), controller); + var actionExecutedDelegate = CreateActionExecutedDelegate(aec); + + controller.OnActionExecutionAsync(aec, actionExecutedDelegate); + } + } + + /// + /// Creates action execution delegate from ActionExecutingContext + /// + private static ActionExecutionDelegate CreateActionExecutedDelegate(ActionExecutingContext context) + { + var actionExecutedContext = new ActionExecutedContext(context, context.Filters, context.Controller) + { + Result = context.Result, + }; + return () => Task.FromResult(actionExecutedContext); + } + + /// + /// Since we could be returning the current page from a surface controller posted values in which the routing values are changed, we + /// need to revert these values back to nothing in order for the normal page to render again. + /// + private static void ResetRouteData(RouteData routeData) + { + routeData.DataTokens["area"] = null; + routeData.DataTokens["Namespaces"] = null; + } + + /// + /// Validate that the current page execution is not being handled by the normal umbraco routing system + /// + private static void ValidateRouteData(RouteData routeData) + { + if (routeData.DataTokens.ContainsKey(Constants.Web.UmbracoRouteDefinitionDataToken) == false) + { + throw new InvalidOperationException("Can only use " + typeof(UmbracoPageResult).Name + + " in the context of an Http POST when using a SurfaceController form"); + } + } + + /// + /// Ensure ModelState, ViewData and TempData is copied across + /// + private static void CopyControllerData(ControllerContext context, Controller controller) + { + controller.ViewData.ModelState.Merge(context.ModelState); + + foreach (var d in controller.ViewData) + controller.ViewData[d.Key] = d.Value; + + // We cannot simply merge the temp data because during controller execution it will attempt to 'load' temp data + // but since it has not been saved, there will be nothing to load and it will revert to nothing, so the trick is + // to Save the state of the temp data first then it will automatically be picked up. + // http://issues.umbraco.org/issue/U4-1339 + + var targetController = controller; + var tempDataDictionaryFactory = context.HttpContext.RequestServices.GetRequiredService(); + var tempData = tempDataDictionaryFactory.GetTempData(context.HttpContext); + + targetController.TempData = tempData; + targetController.TempData.Save(); + } + + /// + /// Creates a controller using the controller factory + /// + private static Controller CreateController(ControllerContext context, IControllerFactory factory) + { + if (!(factory.CreateController(context) is Controller controller)) + throw new InvalidOperationException("Could not create controller with name " + context.ActionDescriptor.ControllerName + "."); + + return controller; + } + + /// + /// Cleans up the controller by releasing it using the controller factory, and by disposing it. + /// + private static void CleanupController(ControllerContext context, Controller controller, IControllerFactory factory) + { + if (!(controller is null)) + factory.ReleaseController(context, controller); + + controller?.DisposeIfDisposable(); + } + + private class DummyView : IView + { + public DummyView(string path) + { + Path = path; + } + + public Task RenderAsync(ViewContext context) + { + return Task.CompletedTask; + } + + public string Path { get; } + } + } +} diff --git a/src/Umbraco.Web.Website/Controllers/SurfaceController.cs b/src/Umbraco.Web.Website/Controllers/SurfaceController.cs new file mode 100644 index 0000000000..161fe1750f --- /dev/null +++ b/src/Umbraco.Web.Website/Controllers/SurfaceController.cs @@ -0,0 +1,181 @@ +using System; +using System.Collections.Specialized; +using Umbraco.Core; +using Umbraco.Core.Cache; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Persistence; +using Umbraco.Core.Services; +using Umbraco.Web.Common.Controllers; +using Umbraco.Web.Routing; +using Umbraco.Web.Website.ActionResults; + +namespace Umbraco.Web.Website.Controllers +{ + /// + /// Provides a base class for front-end add-in controllers. + /// + // TODO: Migrate MergeModelStateToChildAction and MergeParentContextViewData action filters + // [MergeModelStateToChildAction] + // [MergeParentContextViewData] + public abstract class SurfaceController : PluginController + { + private readonly IPublishedUrlProvider _publishedUrlProvider; + + /// + /// Gets the current page. + /// + protected virtual IPublishedContent CurrentPage + { + get + { + var routeDefAttempt = TryGetRouteDefinitionFromAncestorViewContexts(); + if (routeDefAttempt.Success == false) + throw routeDefAttempt.Exception; + + var routeDef = routeDefAttempt.Result; + return routeDef.PublishedRequest.PublishedContent; + } + } + + protected SurfaceController(IUmbracoContextAccessor umbracoContextAccessor, IUmbracoDatabaseFactory databaseFactory, ServiceContext services, AppCaches appCaches, ILogger logger, IProfilingLogger profilingLogger, IPublishedUrlProvider publishedUrlProvider) + : base(umbracoContextAccessor, databaseFactory, services, appCaches, logger, profilingLogger) + { + _publishedUrlProvider = publishedUrlProvider; + } + + /// + /// Redirects to the Umbraco page with the given id + /// + /// + /// + protected RedirectToUmbracoPageResult RedirectToUmbracoPage(int pageId) + { + return new RedirectToUmbracoPageResult(pageId, _publishedUrlProvider, UmbracoContextAccessor); + } + + /// + /// Redirects to the Umbraco page with the given id and passes provided querystring + /// + /// + /// + /// + protected RedirectToUmbracoPageResult RedirectToUmbracoPage(int pageId, NameValueCollection queryStringValues) + { + return new RedirectToUmbracoPageResult(pageId, queryStringValues, _publishedUrlProvider, UmbracoContextAccessor); + } + + /// + /// Redirects to the Umbraco page with the given id and passes provided querystring + /// + /// + /// + /// + protected RedirectToUmbracoPageResult RedirectToUmbracoPage(int pageId, string queryString) + { + return new RedirectToUmbracoPageResult(pageId, queryString, _publishedUrlProvider, UmbracoContextAccessor); + } + + /// + /// Redirects to the Umbraco page with the given published content + /// + /// + /// + protected RedirectToUmbracoPageResult RedirectToUmbracoPage(IPublishedContent publishedContent) + { + return new RedirectToUmbracoPageResult(publishedContent, _publishedUrlProvider, UmbracoContextAccessor); + } + + /// + /// Redirects to the Umbraco page with the given published content and passes provided querystring + /// + /// + /// + /// + protected RedirectToUmbracoPageResult RedirectToUmbracoPage(IPublishedContent publishedContent, NameValueCollection queryStringValues) + { + return new RedirectToUmbracoPageResult(publishedContent, queryStringValues, _publishedUrlProvider, UmbracoContextAccessor); + } + + /// + /// Redirects to the Umbraco page with the given published content and passes provided querystring + /// + /// + /// + /// + protected RedirectToUmbracoPageResult RedirectToUmbracoPage(IPublishedContent publishedContent, string queryString) + { + return new RedirectToUmbracoPageResult(publishedContent, queryString, _publishedUrlProvider, UmbracoContextAccessor); + } + + /// + /// Redirects to the currently rendered Umbraco page + /// + /// + protected RedirectToUmbracoPageResult RedirectToCurrentUmbracoPage() + { + return new RedirectToUmbracoPageResult(CurrentPage, _publishedUrlProvider, UmbracoContextAccessor); + } + + /// + /// Redirects to the currently rendered Umbraco page and passes provided querystring + /// + /// + /// + protected RedirectToUmbracoPageResult RedirectToCurrentUmbracoPage(NameValueCollection queryStringValues) + { + return new RedirectToUmbracoPageResult(CurrentPage, queryStringValues, _publishedUrlProvider, UmbracoContextAccessor); + } + + /// + /// Redirects to the currently rendered Umbraco page and passes provided querystring + /// + /// + /// + protected RedirectToUmbracoPageResult RedirectToCurrentUmbracoPage(string queryString) + { + return new RedirectToUmbracoPageResult(CurrentPage, queryString, _publishedUrlProvider, UmbracoContextAccessor); + } + + /// + /// Redirects to the currently rendered Umbraco URL + /// + /// + /// + /// This is useful if you need to redirect + /// to the current page but the current page is actually a rewritten URL normally done with something like + /// Server.Transfer.* + /// + protected RedirectToUmbracoUrlResult RedirectToCurrentUmbracoUrl() + { + return new RedirectToUmbracoUrlResult(UmbracoContext); + } + + /// + /// Returns the currently rendered Umbraco page + /// + /// + protected UmbracoPageResult CurrentUmbracoPage() + { + return new UmbracoPageResult(ProfilingLogger); + } + + /// + /// we need to recursively find the route definition based on the parent view context + /// + /// + private Attempt TryGetRouteDefinitionFromAncestorViewContexts() + { + var currentContext = ControllerContext; + while (!(currentContext is null)) + { + var currentRouteData = currentContext.RouteData; + if (currentRouteData.DataTokens.ContainsKey(Core.Constants.Web.UmbracoRouteDefinitionDataToken)) + return Attempt.Succeed((RouteDefinition)currentRouteData.DataTokens[Core.Constants.Web.UmbracoRouteDefinitionDataToken]); + } + + return Attempt.Fail( + new InvalidOperationException("Cannot find the Umbraco route definition in the route values, the request must be made in the context of an Umbraco request")); + } + } +} diff --git a/src/Umbraco.Web.Website/RouteDefinition.cs b/src/Umbraco.Web.Website/RouteDefinition.cs new file mode 100644 index 0000000000..02eab6ae77 --- /dev/null +++ b/src/Umbraco.Web.Website/RouteDefinition.cs @@ -0,0 +1,29 @@ +using System; +using Umbraco.Web.Routing; + +namespace Umbraco.Web.Website +{ + /// + /// Represents the data required to route to a specific controller/action during an Umbraco request + /// + public class RouteDefinition + { + public string ControllerName { get; set; } + public string ActionName { get; set; } + + /// + /// The Controller type found for routing to + /// + public Type ControllerType { get; set; } + + /// + /// Everything related to the current content request including the requested content + /// + public IPublishedRequest PublishedRequest { get; set; } + + /// + /// Gets/sets whether the current request has a hijacked route/user controller routed for it + /// + public bool HasHijackedRoute { get; set; } + } +} diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 7e3638d96a..825b67767a 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -194,6 +194,7 @@ namespace Umbraco.Web.Editors /// /// /// + /// Migrated already to .Net Core [HttpGet] public JsonNetResult LocalizedText(string culture = null) { @@ -239,7 +240,8 @@ namespace Umbraco.Web.Editors return JavaScript(result); } - + + /// Migrated already to .Net Core [UmbracoAuthorize(Order = 0)] [HttpGet] public JsonNetResult GetGridConfig() diff --git a/src/Umbraco.Web/Mvc/JsonNetResult.cs b/src/Umbraco.Web/Mvc/JsonNetResult.cs index da6780451e..c086b5f375 100644 --- a/src/Umbraco.Web/Mvc/JsonNetResult.cs +++ b/src/Umbraco.Web/Mvc/JsonNetResult.cs @@ -13,6 +13,7 @@ namespace Umbraco.Web.Mvc /// /// Custom json result using newtonsoft json.net /// + /// Migrated already to .Net Core public class JsonNetResult : ActionResult { public Encoding ContentEncoding { get; set; } diff --git a/src/Umbraco.Web/Mvc/RedirectToUmbracoPageResult.cs b/src/Umbraco.Web/Mvc/RedirectToUmbracoPageResult.cs index 46d6019860..e379f569e0 100644 --- a/src/Umbraco.Web/Mvc/RedirectToUmbracoPageResult.cs +++ b/src/Umbraco.Web/Mvc/RedirectToUmbracoPageResult.cs @@ -14,6 +14,7 @@ namespace Umbraco.Web.Mvc /// /// Redirects to an Umbraco page by Id or Entity /// + /// Migrated already to .Net Core public class RedirectToUmbracoPageResult : ActionResult { private IPublishedContent _publishedContent; diff --git a/src/Umbraco.Web/Mvc/RedirectToUmbracoUrlResult.cs b/src/Umbraco.Web/Mvc/RedirectToUmbracoUrlResult.cs index 3690e98790..6f97bff534 100644 --- a/src/Umbraco.Web/Mvc/RedirectToUmbracoUrlResult.cs +++ b/src/Umbraco.Web/Mvc/RedirectToUmbracoUrlResult.cs @@ -11,6 +11,7 @@ namespace Umbraco.Web.Mvc /// to the current page but the current page is actually a rewritten URL normally done with something like /// Server.Transfer. It is also handy if you want to persist the query strings. /// + /// Migrated already to .Net Core public class RedirectToUmbracoUrlResult : ActionResult { private readonly IUmbracoContext _umbracoContext; diff --git a/src/Umbraco.Web/Mvc/RouteDefinition.cs b/src/Umbraco.Web/Mvc/RouteDefinition.cs index 94f97e7e11..45e759fd66 100644 --- a/src/Umbraco.Web/Mvc/RouteDefinition.cs +++ b/src/Umbraco.Web/Mvc/RouteDefinition.cs @@ -7,6 +7,7 @@ namespace Umbraco.Web.Mvc /// /// Represents the data required to route to a specific controller/action during an Umbraco request /// + /// Migrated already to .Net Core public class RouteDefinition { public string ControllerName { get; set; } diff --git a/src/Umbraco.Web/Mvc/SurfaceController.cs b/src/Umbraco.Web/Mvc/SurfaceController.cs index 877a3c31d8..b9c857a8b3 100644 --- a/src/Umbraco.Web/Mvc/SurfaceController.cs +++ b/src/Umbraco.Web/Mvc/SurfaceController.cs @@ -13,6 +13,8 @@ namespace Umbraco.Web.Mvc /// /// Provides a base class for front-end add-in controllers. /// + /// Migrated already to .Net Core without MergeModelStateToChildAction and MergeParentContextViewData action filters + /// TODO: Migrate MergeModelStateToChildAction and MergeParentContextViewData action filters [MergeModelStateToChildAction] [MergeParentContextViewData] public abstract class SurfaceController : PluginController diff --git a/src/Umbraco.Web/Mvc/UmbracoPageResult.cs b/src/Umbraco.Web/Mvc/UmbracoPageResult.cs index 75804b47bb..30c990a981 100644 --- a/src/Umbraco.Web/Mvc/UmbracoPageResult.cs +++ b/src/Umbraco.Web/Mvc/UmbracoPageResult.cs @@ -10,6 +10,7 @@ namespace Umbraco.Web.Mvc /// /// Used by posted forms to proxy the result to the page in which the current URL matches on /// + /// Migrated already to .Net Core public class UmbracoPageResult : ActionResult { private readonly IProfilingLogger _profilingLogger;