From 652da168dbe6b1bc1a9c05b8f6c11d5f3695280a Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska Date: Tue, 28 Apr 2020 16:00:29 +0200 Subject: [PATCH] Migrating ActionResults and a dependency class --- .../ActionResults/JsonNetResult.cs | 55 ++++++ .../RedirectToUmbracoPageResult.cs | 174 ++++++++++++++++++ .../RedirectToUmbracoUrlResult.cs | 43 +++++ .../ActionResults/UmbracoPageResult.cs | 168 +++++++++++++++++ src/Umbraco.Web.Website/RouteDefinition.cs | 29 +++ 5 files changed, 469 insertions(+) create mode 100644 src/Umbraco.Web.BackOffice/ActionResults/JsonNetResult.cs create mode 100644 src/Umbraco.Web.Website/ActionResults/RedirectToUmbracoPageResult.cs create mode 100644 src/Umbraco.Web.Website/ActionResults/RedirectToUmbracoUrlResult.cs create mode 100644 src/Umbraco.Web.Website/ActionResults/UmbracoPageResult.cs create mode 100644 src/Umbraco.Web.Website/RouteDefinition.cs diff --git a/src/Umbraco.Web.BackOffice/ActionResults/JsonNetResult.cs b/src/Umbraco.Web.BackOffice/ActionResults/JsonNetResult.cs new file mode 100644 index 0000000000..24f4ec51b8 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/ActionResults/JsonNetResult.cs @@ -0,0 +1,55 @@ +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); + + writer.Flush(); + } + + return Task.CompletedTask; + } + } +} diff --git a/src/Umbraco.Web.Website/ActionResults/RedirectToUmbracoPageResult.cs b/src/Umbraco.Web.Website/ActionResults/RedirectToUmbracoPageResult.cs new file mode 100644 index 0000000000..dfc28f1412 --- /dev/null +++ b/src/Umbraco.Web.Website/ActionResults/RedirectToUmbracoPageResult.cs @@ -0,0 +1,174 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Web; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Composing; +using Umbraco.Core.IO; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Web.Routing; + +namespace Umbraco.Web.Website.ActionResults +{ + 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.UmbracoContext.Content.GetById(_pageId); + + return _publishedContent; + } + } + + /// + /// Creates a new RedirectToUmbracoResult + /// + /// + /// + public RedirectToUmbracoPageResult(int pageId, IPublishedUrlProvider publishedUrlProvider) + { + _pageId = pageId; + _publishedUrlProvider = publishedUrlProvider; + } + + /// + /// Creates a new RedirectToUmbracoResult + /// + /// + /// + /// + public RedirectToUmbracoPageResult(int pageId, NameValueCollection queryStringValues, IPublishedUrlProvider publishedUrlProvider) + { + _pageId = pageId; + _queryStringValues = queryStringValues; + _publishedUrlProvider = publishedUrlProvider; + } + + /// + /// Creates a new RedirectToUmbracoResult + /// + /// + /// + /// + public RedirectToUmbracoPageResult(int pageId, string queryString, IPublishedUrlProvider publishedUrlProvider) + { + _pageId = pageId; + _queryStringValues = ParseQueryString(queryString); + _publishedUrlProvider = publishedUrlProvider; + } + + /// + /// 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 tempData = httpContext.RequestServices.GetRequiredService(); + 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..929cdb85e6 --- /dev/null +++ b/src/Umbraco.Web.Website/ActionResults/RedirectToUmbracoUrlResult.cs @@ -0,0 +1,43 @@ +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 tempData = context.HttpContext.RequestServices.GetRequiredService(); + 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..5019dbd7c2 --- /dev/null +++ b/src/Umbraco.Web.Website/ActionResults/UmbracoPageResult.cs @@ -0,0 +1,168 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +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; + +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 routeDef = (RouteDefinition)routeData.DataTokens[Constants.Web.UmbracoRouteDefinitionDataToken]; + routeData.Values["action"] = routeDef.ActionName; + + 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, routeDef); + + 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")) + { + 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 tempData = context.HttpContext.RequestServices.GetRequiredService(); + + targetController.TempData = tempData; + targetController.TempData.Save(); + } + + /// + /// Creates a controller using the controller factory + /// + private static Controller CreateController(ControllerContext context, IControllerFactory factory, RouteDefinition routeDef) + { + if (!(factory.CreateController(context) is Controller controller)) + throw new InvalidOperationException("Could not create controller with name " + routeDef.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/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; } + } +}