Changes the umbraco route values to use http features intead of in route values which is much nicer, fixes the redirect to page result, tests a surface controller POST and it works, ensures the routing takes place before the form check, removes a bunch of old code

This commit is contained in:
Shannon
2021-02-03 15:47:27 +11:00
parent 2dc169457b
commit 0c26a82489
28 changed files with 313 additions and 902 deletions

View File

@@ -7,8 +7,6 @@ namespace Umbraco.Core
/// </summary>
public static class Web
{
public const string UmbracoRouteDefinitionDataToken = "umbraco-route-def";
/// <summary>
/// The preview cookie name
/// </summary>

View File

@@ -60,22 +60,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.ModelBinders
// Arrange
IPublishedContent pc = CreatePublishedContent();
ModelBindingContext bindingContext = CreateBindingContextForUmbracoRequest(typeof(ContentModel), pc);
bindingContext.ActionContext.RouteData.Values.Remove(Constants.Web.UmbracoRouteDefinitionDataToken);
// Act
await _contentModelBinder.BindModelAsync(bindingContext);
// Assert
Assert.False(bindingContext.Result.IsModelSet);
}
[Test]
public async Task Does_Not_Bind_Model_When_UmbracoToken_Has_Incorrect_Model()
{
// Arrange
IPublishedContent pc = CreatePublishedContent();
ModelBindingContext bindingContext = CreateBindingContextForUmbracoRequest(typeof(ContentModel), pc);
bindingContext.ActionContext.RouteData.Values[Constants.Web.UmbracoRouteDefinitionDataToken] = new NonContentModel();
bindingContext.ActionContext.HttpContext.Features.Set<UmbracoRouteValues>(null);
// Act
await _contentModelBinder.BindModelAsync(bindingContext);
@@ -220,9 +205,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.ModelBinders
var httpContext = new DefaultHttpContext();
var routeData = new RouteData();
routeData.Values.Add(Constants.Web.UmbracoRouteDefinitionDataToken, new UmbracoRouteValues(publishedRequest));
{
}
httpContext.Features.Set(new UmbracoRouteValues(publishedRequest));
var actionContext = new ActionContext(httpContext, routeData, new ActionDescriptor());
var metadataProvider = new EmptyModelMetadataProvider();

View File

@@ -3,6 +3,7 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Moq;
@@ -126,15 +127,15 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Website.Controllers
var routeDefinition = new UmbracoRouteValues(publishedRequest);
var routeData = new RouteData();
routeData.Values.Add(CoreConstants.Web.UmbracoRouteDefinitionDataToken, routeDefinition);
var httpContext = new DefaultHttpContext();
httpContext.Features.Set(routeDefinition);
var ctrl = new TestSurfaceController(umbracoContextAccessor, Mock.Of<IPublishedContentQuery>(), Mock.Of<IPublishedUrlProvider>())
{
ControllerContext = new ControllerContext()
{
HttpContext = Mock.Of<HttpContext>(),
RouteData = routeData
HttpContext = httpContext,
RouteData = new RouteData()
}
};

View File

@@ -21,7 +21,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Website.Routing
{
[TestFixture]
public class HijackedRouteEvaluatorTests
public class ControllerActionSearcherTests
{
private class TestActionDescriptorCollectionProvider : ActionDescriptorCollectionProvider
{
@@ -88,11 +88,11 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Website.Routing
[TestCase("Custom", "Render1", nameof(Render1Controller.Custom), true)]
public void Matches_Controller(string action, string controller, string resultAction, bool matches)
{
var evaluator = new HijackedRouteEvaluator(
new NullLogger<HijackedRouteEvaluator>(),
var query = new ControllerActionSearcher(
new NullLogger<ControllerActionSearcher>(),
GetActionDescriptors());
HijackedRouteResult result = evaluator.Evaluate(controller, action);
ControllerActionSearchResult result = query.Find<IRenderController>(controller, action);
Assert.AreEqual(matches, result.Success);
if (matches)
{

View File

@@ -1,5 +1,6 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Routing;
@@ -49,7 +50,9 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Website.Routing
TestHelper.GetHostingEnvironment(),
state,
routeValuesFactory ?? Mock.Of<IUmbracoRouteValuesFactory>(),
filter ?? Mock.Of<IRoutableDocumentFilter>(x => x.IsDocumentRequest(It.IsAny<string>()) == true));
filter ?? Mock.Of<IRoutableDocumentFilter>(x => x.IsDocumentRequest(It.IsAny<string>()) == true),
Mock.Of<IDataProtectionProvider>(),
Mock.Of<IControllerActionSearcher>());
return transformer;
}
@@ -74,7 +77,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Website.Routing
typeof(TestController));
private IUmbracoRouteValuesFactory GetRouteValuesFactory(IPublishedRequest request)
=> Mock.Of<IUmbracoRouteValuesFactory>(x => x.Create(It.IsAny<HttpContext>(), It.IsAny<RouteValueDictionary>(), It.IsAny<IPublishedRequest>()) == GetRouteValues(request));
=> Mock.Of<IUmbracoRouteValuesFactory>(x => x.Create(It.IsAny<HttpContext>(), It.IsAny<IPublishedRequest>()) == GetRouteValues(request));
private IPublishedRouter GetRouter(IPublishedRequest request)
=> Mock.Of<IPublishedRouter>(x => x.RouteRequestAsync(It.IsAny<IPublishedRequestBuilder>(), It.IsAny<RouteRequestOptions>()) == Task.FromResult(request));
@@ -140,6 +143,25 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Website.Routing
Assert.AreEqual(request, umbracoContext.PublishedRequest);
}
[Test]
public async Task Assigns_UmbracoRouteValues_To_HttpContext_Feature()
{
IUmbracoContext umbracoContext = GetUmbracoContext(true);
IPublishedRequest request = Mock.Of<IPublishedRequest>();
UmbracoRouteValueTransformer transformer = GetTransformerWithRunState(
Mock.Of<IUmbracoContextAccessor>(x => x.UmbracoContext == umbracoContext),
router: GetRouter(request),
routeValuesFactory: GetRouteValuesFactory(request));
var httpContext = new DefaultHttpContext();
RouteValueDictionary result = await transformer.TransformAsync(httpContext, new RouteValueDictionary());
UmbracoRouteValues routeVals = httpContext.Features.Get<UmbracoRouteValues>();
Assert.IsNotNull(routeVals);
Assert.AreEqual(routeVals.PublishedRequest, umbracoContext.PublishedRequest);
}
[Test]
public async Task Assigns_Values_To_RouteValueDictionary()
{

View File

@@ -39,8 +39,8 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Website.Routing
renderingDefaults,
Mock.Of<IShortStringHelper>(),
new UmbracoFeatures(),
new HijackedRouteEvaluator(
new NullLogger<HijackedRouteEvaluator>(),
new ControllerActionSearcher(
new NullLogger<ControllerActionSearcher>(),
Mock.Of<IActionDescriptorCollectionProvider>()),
publishedRouter.Object);
@@ -56,7 +56,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Website.Routing
UmbracoRouteValuesFactory factory = GetFactory(out Mock<IPublishedRouter> publishedRouter, out _);
UmbracoRouteValues result = factory.Create(new DefaultHttpContext(), new RouteValueDictionary(), request);
UmbracoRouteValues result = factory.Create(new DefaultHttpContext(), request);
// The request has content, no template, no hijacked route and no disabled template features so UpdateRequestToNotFound will be called
publishedRouter.Verify(m => m.UpdateRequestToNotFound(It.IsAny<IPublishedRequest>()), Times.Once);
@@ -73,11 +73,9 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Website.Routing
UmbracoRouteValuesFactory factory = GetFactory(out _, out UmbracoRenderingDefaults renderingDefaults);
var routeVals = new RouteValueDictionary();
UmbracoRouteValues result = factory.Create(new DefaultHttpContext(), routeVals, request);
UmbracoRouteValues result = factory.Create(new DefaultHttpContext(), request);
Assert.IsNotNull(result);
Assert.IsTrue(routeVals.ContainsKey(Constants.Web.UmbracoRouteDefinitionDataToken));
Assert.AreEqual(result, routeVals[Constants.Web.UmbracoRouteDefinitionDataToken]);
Assert.AreEqual(renderingDefaults.DefaultControllerType, result.ControllerType);
Assert.AreEqual(UmbracoRouteValues.DefaultActionName, result.ActionName);
Assert.IsNull(result.TemplateName);

View File

@@ -17,12 +17,13 @@ namespace Umbraco.Web.Common.Controllers
/// </summary>
protected UmbracoRouteValues GetUmbracoRouteValues(ResultExecutingContext context)
{
if (!context.RouteData.Values.TryGetValue(Core.Constants.Web.UmbracoRouteDefinitionDataToken, out var def))
UmbracoRouteValues routeVals = context.HttpContext.Features.Get<UmbracoRouteValues>();
if (routeVals == null)
{
throw new InvalidOperationException($"No route value found with key {Core.Constants.Web.UmbracoRouteDefinitionDataToken}");
throw new InvalidOperationException($"No {nameof(UmbracoRouteValues)} feature was found in the HttpContext");
}
return (UmbracoRouteValues)def;
return routeVals;
}
/// <summary>

View File

@@ -71,11 +71,11 @@ namespace Umbraco.Web.Common.Controllers
return _umbracoRouteValues;
}
_umbracoRouteValues = HttpContext.GetRouteValue(Core.Constants.Web.UmbracoRouteDefinitionDataToken) as UmbracoRouteValues;
_umbracoRouteValues = HttpContext.Features.Get<UmbracoRouteValues>();
if (_umbracoRouteValues == null)
{
throw new InvalidOperationException($"No route value found with key {Core.Constants.Web.UmbracoRouteDefinitionDataToken}");
throw new InvalidOperationException($"No {nameof(UmbracoRouteValues)} feature was found in the HttpContext");
}
return _umbracoRouteValues;

View File

@@ -29,7 +29,8 @@ namespace Umbraco.Web.Common.Localization
/// <inheritdoc/>
public override Task<ProviderCultureResult> DetermineProviderCultureResult(HttpContext httpContext)
{
if (httpContext.GetRouteValue(Core.Constants.Web.UmbracoRouteDefinitionDataToken) is UmbracoRouteValues routeValues)
UmbracoRouteValues routeValues = httpContext.Features.Get<UmbracoRouteValues>();
if (routeValues != null)
{
string culture = routeValues.PublishedRequest?.Culture;
if (culture != null)

View File

@@ -30,8 +30,8 @@ namespace Umbraco.Web.Common.ModelBinders
// only IPublishedContent will ever exist in the request so when this model binder is used as an IModelBinder
// in the aspnet pipeline it will really only support converting from IPublishedContent which is contained
// in the UmbracoRouteValues --> IContentModel
if (!bindingContext.ActionContext.RouteData.Values.TryGetValue(Core.Constants.Web.UmbracoRouteDefinitionDataToken, out var source)
|| !(source is UmbracoRouteValues umbracoRouteValues))
UmbracoRouteValues umbracoRouteValues = bindingContext.HttpContext.Features.Get<UmbracoRouteValues>();
if (umbracoRouteValues == null)
{
return Task.CompletedTask;
}

View File

@@ -1,5 +1,6 @@
using System;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Net;
using System.Web;
@@ -12,39 +13,43 @@ namespace Umbraco.Web.Common.Security
{
public class EncryptionHelper
{
// TODO: Decide if these belong here... I don't think so since this all has to do with surface controller routes
// could also just be injected too....
private static IDataProtector CreateDataProtector(IDataProtectionProvider dataProtectionProvider)
{
return dataProtectionProvider.CreateProtector(nameof(EncryptionHelper));
}
=> dataProtectionProvider.CreateProtector(nameof(EncryptionHelper));
public static string Decrypt(string encryptedString, IDataProtectionProvider dataProtectionProvider)
{
return CreateDataProtector(dataProtectionProvider).Unprotect(encryptedString);
}
=> CreateDataProtector(dataProtectionProvider).Unprotect(encryptedString);
public static string Encrypt(string plainString, IDataProtectionProvider dataProtectionProvider)
{
return CreateDataProtector(dataProtectionProvider).Protect(plainString);
}
=> CreateDataProtector(dataProtectionProvider).Protect(plainString);
/// <summary>
/// This is used in methods like BeginUmbracoForm and SurfaceAction to generate an encrypted string which gets submitted in a request for which
/// Umbraco can decrypt during the routing process in order to delegate the request to a specific MVC Controller.
/// </summary>
/// <param name="dataProtectionProvider"></param>
/// <param name="controllerName"></param>
/// <param name="controllerAction"></param>
/// <param name="area"></param>
/// <param name="additionalRouteVals"></param>
/// <returns></returns>
public static string CreateEncryptedRouteString(IDataProtectionProvider dataProtectionProvider, string controllerName, string controllerAction, string area, object additionalRouteVals = null)
{
if (dataProtectionProvider == null) throw new ArgumentNullException(nameof(dataProtectionProvider));
if (controllerName == null) throw new ArgumentNullException(nameof(controllerName));
if (string.IsNullOrEmpty(controllerName)) throw new ArgumentException("Value can't be empty.", nameof(controllerName));
if (controllerAction == null) throw new ArgumentNullException(nameof(controllerAction));
if (string.IsNullOrEmpty(controllerAction)) throw new ArgumentException("Value can't be empty.", nameof(controllerAction));
if (area == null) throw new ArgumentNullException(nameof(area));
if (dataProtectionProvider is null)
{
throw new ArgumentNullException(nameof(dataProtectionProvider));
}
if (string.IsNullOrEmpty(controllerName))
{
throw new ArgumentException($"'{nameof(controllerName)}' cannot be null or empty.", nameof(controllerName));
}
if (string.IsNullOrEmpty(controllerAction))
{
throw new ArgumentException($"'{nameof(controllerAction)}' cannot be null or empty.", nameof(controllerAction));
}
if (area is null)
{
throw new ArgumentNullException(nameof(area));
}
// need to create a params string as Base64 to put into our hidden field to use during the routes
var surfaceRouteParams = $"{ViewConstants.ReservedAdditionalKeys.Controller}={WebUtility.UrlEncode(controllerName)}&{ViewConstants.ReservedAdditionalKeys.Action}={WebUtility.UrlEncode(controllerAction)}&{ViewConstants.ReservedAdditionalKeys.Area}={area}";
@@ -54,22 +59,34 @@ namespace Umbraco.Web.Common.Security
if (additionalRouteVals != null)
{
if (additionalRouteVals is Dictionary<string, object> additionalRouteValsAsDictionary)
{
additionalRouteValsAsQuery = additionalRouteValsAsDictionary.ToQueryString();
else
additionalRouteValsAsQuery = additionalRouteVals.ToDictionary<object>().ToQueryString();
}
else
{
additionalRouteValsAsQuery = additionalRouteVals.ToDictionary<object>().ToQueryString();
}
}
else
{
additionalRouteValsAsQuery = null;
}
if (additionalRouteValsAsQuery.IsNullOrWhiteSpace() == false)
{
surfaceRouteParams += "&" + additionalRouteValsAsQuery;
}
return Encrypt(surfaceRouteParams, dataProtectionProvider);
}
public static bool DecryptAndValidateEncryptedRouteString(IDataProtectionProvider dataProtectionProvider, string encryptedString, out IDictionary<string, string> parts)
{
if (dataProtectionProvider == null) throw new ArgumentNullException(nameof(dataProtectionProvider));
if (dataProtectionProvider == null)
{
throw new ArgumentNullException(nameof(dataProtectionProvider));
}
string decryptedString;
try
{
@@ -81,22 +98,32 @@ namespace Umbraco.Web.Common.Security
parts = null;
return false;
}
var parsedQueryString = HttpUtility.ParseQueryString(decryptedString);
NameValueCollection parsedQueryString = HttpUtility.ParseQueryString(decryptedString);
parts = new Dictionary<string, string>();
foreach (var key in parsedQueryString.AllKeys)
{
parts[key] = parsedQueryString[key];
}
// validate all required keys exist
// the controller
if (parts.All(x => x.Key != ViewConstants.ReservedAdditionalKeys.Controller))
{
return false;
}
// the action
if (parts.All(x => x.Key != ViewConstants.ReservedAdditionalKeys.Action))
{
return false;
}
// the area
if (parts.All(x => x.Key != ViewConstants.ReservedAdditionalKeys.Area))
{
return false;
}
return true;
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
@@ -13,6 +14,7 @@ using Microsoft.Extensions.DependencyInjection;
using Umbraco.Core;
using Umbraco.Core.Logging;
using Umbraco.Extensions;
using Umbraco.Web.Common.Routing;
namespace Umbraco.Web.Website.ActionResults
{
@@ -33,13 +35,15 @@ namespace Umbraco.Web.Website.ActionResults
var routeData = context.RouteData;
ResetRouteData(routeData);
ValidateRouteData(routeData);
ValidateRouteData(context);
var factory = context.HttpContext.RequestServices.GetRequiredService<IControllerFactory>();
IControllerFactory factory = context.HttpContext.RequestServices.GetRequiredService<IControllerFactory>();
Controller controller = null;
if (!(context is ControllerContext controllerContext))
return Task.FromCanceled(new System.Threading.CancellationToken());
{
return Task.FromCanceled(CancellationToken.None);
}
try
{
@@ -97,9 +101,10 @@ namespace Umbraco.Web.Website.ActionResults
/// <summary>
/// Validate that the current page execution is not being handled by the normal umbraco routing system
/// </summary>
private static void ValidateRouteData(RouteData routeData)
private static void ValidateRouteData(ActionContext actionContext)
{
if (routeData.Values.ContainsKey(Constants.Web.UmbracoRouteDefinitionDataToken) == false)
UmbracoRouteValues umbracoRouteValues = actionContext.HttpContext.Features.Get<UmbracoRouteValues>();
if (umbracoRouteValues == null)
{
throw new InvalidOperationException("Can only use " + typeof(UmbracoPageResult).Name +
" in the context of an Http POST when using a SurfaceController form");
@@ -114,7 +119,9 @@ namespace Umbraco.Web.Website.ActionResults
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
@@ -135,7 +142,9 @@ namespace Umbraco.Web.Website.ActionResults
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;
}
@@ -146,7 +155,9 @@ namespace Umbraco.Web.Website.ActionResults
private static void CleanupController(ControllerContext context, Controller controller, IControllerFactory factory)
{
if (!(controller is null))
{
factory.ReleaseController(context, controller);
}
controller?.DisposeIfDisposable();
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Specialized;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Core;
using Umbraco.Core.Cache;
using Umbraco.Core.Logging;
@@ -38,14 +39,13 @@ namespace Umbraco.Web.Website.Controllers
{
get
{
var routeDefAttempt = TryGetRouteDefinitionFromAncestorViewContexts();
if (routeDefAttempt.Success == false)
UmbracoRouteValues umbracoRouteValues = HttpContext.Features.Get<UmbracoRouteValues>();
if (umbracoRouteValues == null)
{
throw routeDefAttempt.Exception;
throw new InvalidOperationException($"No {nameof(UmbracoRouteValues)} feature was found in the HttpContext");
}
var routeDef = routeDefAttempt.Result;
return routeDef.PublishedRequest.PublishedContent;
return umbracoRouteValues.PublishedRequest.PublishedContent;
}
}
@@ -101,24 +101,5 @@ namespace Umbraco.Web.Website.Controllers
/// </summary>
protected UmbracoPageResult CurrentUmbracoPage()
=> new UmbracoPageResult(ProfilingLogger);
/// <summary>
/// we need to recursively find the route definition based on the parent view context
/// </summary>
private Attempt<UmbracoRouteValues> TryGetRouteDefinitionFromAncestorViewContexts()
{
var currentContext = ControllerContext;
while (!(currentContext is null))
{
var currentRouteData = currentContext.RouteData;
if (currentRouteData.Values.ContainsKey(Constants.Web.UmbracoRouteDefinitionDataToken))
{
return Attempt.Succeed((UmbracoRouteValues)currentRouteData.Values[Constants.Web.UmbracoRouteDefinitionDataToken]);
}
}
return Attempt<UmbracoRouteValues>.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"));
}
}
}

View File

@@ -37,7 +37,7 @@ namespace Umbraco.Web.Website.DependencyInjection
builder.Services.AddDataProtection();
builder.Services.AddScoped<UmbracoRouteValueTransformer>();
builder.Services.AddSingleton<HijackedRouteEvaluator>();
builder.Services.AddSingleton<IControllerActionSearcher, ControllerActionSearcher>();
builder.Services.AddSingleton<IUmbracoRouteValuesFactory, UmbracoRouteValuesFactory>();
builder.Services.AddSingleton<IUmbracoRenderingDefaults, UmbracoRenderingDefaults>();
builder.Services.AddSingleton<IRoutableDocumentFilter, RoutableDocumentFilter>();

View File

@@ -243,15 +243,12 @@ namespace Umbraco.Extensions
return htmlHelper.ActionLink(actionName, metaData.ControllerName, routeVals);
}
#region BeginUmbracoForm
/// <summary>
/// Used for rendering out the Form for BeginUmbracoForm
/// </summary>
internal class UmbracoForm : MvcForm
{
private readonly ViewContext _viewContext;
private bool _disposed;
private readonly string _encryptedString;
private readonly string _controllerName;
@@ -272,15 +269,8 @@ namespace Umbraco.Extensions
_encryptedString = EncryptionHelper.CreateEncryptedRouteString(GetRequiredService<IDataProtectionProvider>(viewContext), controllerName, controllerAction, area, additionalRouteVals);
}
protected new void Dispose()
protected override void GenerateEndForm()
{
if (_disposed)
{
return;
}
_disposed = true;
// Detect if the call is targeting UmbRegisterController/UmbProfileController/UmbLoginStatusController/UmbLoginController and if it is we automatically output a AntiForgeryToken()
// We have a controllerName and area so we can match
if (_controllerName == "UmbRegister"
@@ -295,7 +285,7 @@ namespace Umbraco.Extensions
// write out the hidden surface form routes
_viewContext.Writer.Write("<input name=\"ufprt\" type=\"hidden\" value=\"" + _encryptedString + "\" />");
base.Dispose();
base.GenerateEndForm();
}
}
@@ -710,7 +700,6 @@ namespace Umbraco.Extensions
return theForm;
}
#endregion
#region If

View File

@@ -1,21 +1,16 @@
using System;
using System;
namespace Umbraco.Web.Website.Routing
{
/// <summary>
/// The result from evaluating if a route can be hijacked
/// The result from querying a controller/action in the existing routes
/// </summary>
public class HijackedRouteResult
public class ControllerActionSearchResult
{
/// <summary>
/// Returns a failed result
/// Initializes a new instance of the <see cref="ControllerActionSearchResult"/> class.
/// </summary>
public static HijackedRouteResult Failed() => new HijackedRouteResult(false, null, null, null);
/// <summary>
/// Initializes a new instance of the <see cref="HijackedRouteResult"/> class.
/// </summary>
public HijackedRouteResult(
private ControllerActionSearchResult(
bool success,
string controllerName,
Type controllerType,
@@ -28,7 +23,18 @@ namespace Umbraco.Web.Website.Routing
}
/// <summary>
/// Gets a value indicating if the route could be hijacked
/// Initializes a new instance of the <see cref="ControllerActionSearchResult"/> class.
/// </summary>
public ControllerActionSearchResult(
string controllerName,
Type controllerType,
string actionName)
: this(true, controllerName, controllerType, actionName)
{
}
/// <summary>
/// Gets a value indicating whether the route could be hijacked
/// </summary>
public bool Success { get; }
@@ -46,5 +52,10 @@ namespace Umbraco.Web.Website.Routing
/// Gets the Acton name
/// </summary>
public string ActionName { get; }
/// <summary>
/// Returns a failed result
/// </summary>
public static ControllerActionSearchResult Failed() => new ControllerActionSearchResult(false, null, null, null);
}
}

View File

@@ -11,19 +11,19 @@ using Umbraco.Web.Common.Controllers;
namespace Umbraco.Web.Website.Routing
{
/// <summary>
/// Determines if a custom controller can hijack the current route
/// Used to find a controller/action in the current available routes
/// </summary>
public class HijackedRouteEvaluator
public class ControllerActionSearcher : IControllerActionSearcher
{
private readonly ILogger<HijackedRouteEvaluator> _logger;
private readonly ILogger<ControllerActionSearcher> _logger;
private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider;
private const string DefaultActionName = nameof(RenderController.Index);
/// <summary>
/// Initializes a new instance of the <see cref="HijackedRouteEvaluator"/> class.
/// Initializes a new instance of the <see cref="ControllerActionSearcher"/> class.
/// </summary>
public HijackedRouteEvaluator(
ILogger<HijackedRouteEvaluator> logger,
public ControllerActionSearcher(
ILogger<ControllerActionSearcher> logger,
IActionDescriptorCollectionProvider actionDescriptorCollectionProvider)
{
_logger = logger;
@@ -33,7 +33,8 @@ namespace Umbraco.Web.Website.Routing
/// <summary>
/// Determines if a custom controller can hijack the current route
/// </summary>
public HijackedRouteResult Evaluate(string controller, string action)
/// <typeparam name="T">The controller type to find</typeparam>
public ControllerActionSearchResult Find<T>(string controller, string action)
{
IReadOnlyList<ControllerActionDescriptor> candidates = FindControllerCandidates(controller, action, DefaultActionName);
@@ -45,8 +46,8 @@ namespace Umbraco.Web.Website.Routing
{
ControllerActionDescriptor controllerDescriptor = customControllerCandidates[0];
// ensure the controller is of type IRenderController and ControllerBase
if (TypeHelper.IsTypeAssignableFrom<IRenderController>(controllerDescriptor.ControllerTypeInfo)
// ensure the controller is of type T and ControllerBase
if (TypeHelper.IsTypeAssignableFrom<T>(controllerDescriptor.ControllerTypeInfo)
&& TypeHelper.IsTypeAssignableFrom<ControllerBase>(controllerDescriptor.ControllerTypeInfo))
{
// now check if the custom action matches
@@ -61,8 +62,7 @@ namespace Umbraco.Web.Website.Routing
}
// it's a hijacked route with a custom controller, so return the the values
return new HijackedRouteResult(
true,
return new ControllerActionSearchResult(
controllerDescriptor.ControllerName,
controllerDescriptor.ControllerTypeInfo,
resultingAction);
@@ -73,7 +73,7 @@ namespace Umbraco.Web.Website.Routing
"The current Document Type {ContentTypeAlias} matches a locally declared controller of type {ControllerName}. Custom Controllers for Umbraco routing must implement '{UmbracoRenderController}' and inherit from '{UmbracoControllerBase}'.",
controller,
controllerDescriptor.ControllerTypeInfo.FullName,
typeof(IRenderController).FullName,
typeof(T).FullName,
typeof(ControllerBase).FullName);
// we cannot route to this custom controller since it is not of the correct type so we'll continue with the defaults
@@ -81,7 +81,7 @@ namespace Umbraco.Web.Website.Routing
}
}
return HijackedRouteResult.Failed();
return ControllerActionSearchResult.Failed();
}
/// <summary>

View File

@@ -0,0 +1,7 @@
namespace Umbraco.Web.Website.Routing
{
public interface IControllerActionSearcher
{
ControllerActionSearchResult Find<T>(string controller, string action);
}
}

View File

@@ -13,6 +13,6 @@ namespace Umbraco.Web.Website.Routing
/// <summary>
/// Creates <see cref="UmbracoRouteValues"/>
/// </summary>
UmbracoRouteValues Create(HttpContext httpContext, RouteValueDictionary values, IPublishedRequest request);
UmbracoRouteValues Create(HttpContext httpContext, IPublishedRequest request);
}
}

View File

@@ -1,14 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Umbraco.Core;
using Umbraco.Core.Configuration.Models;
using Umbraco.Core.Hosting;
using Umbraco.Extensions;
using Umbraco.Web.Common.Routing;
using Umbraco.Web.Common.Security;
using Umbraco.Web.Routing;
using Umbraco.Web.Website.Controllers;
using RouteDirection = Umbraco.Web.Routing.RouteDirection;
@@ -35,6 +43,10 @@ namespace Umbraco.Web.Website.Routing
private readonly IRuntimeState _runtime;
private readonly IUmbracoRouteValuesFactory _routeValuesFactory;
private readonly IRoutableDocumentFilter _routableDocumentFilter;
private readonly IDataProtectionProvider _dataProtectionProvider;
private readonly IControllerActionSearcher _controllerActionSearcher;
private const string ControllerToken = "controller";
private const string ActionToken = "action";
/// <summary>
/// Initializes a new instance of the <see cref="UmbracoRouteValueTransformer"/> class.
@@ -47,21 +59,25 @@ namespace Umbraco.Web.Website.Routing
IHostingEnvironment hostingEnvironment,
IRuntimeState runtime,
IUmbracoRouteValuesFactory routeValuesFactory,
IRoutableDocumentFilter routableDocumentFilter)
IRoutableDocumentFilter routableDocumentFilter,
IDataProtectionProvider dataProtectionProvider,
IControllerActionSearcher controllerActionSearcher)
{
if (globalSettings is null)
{
throw new System.ArgumentNullException(nameof(globalSettings));
throw new ArgumentNullException(nameof(globalSettings));
}
_logger = logger ?? throw new System.ArgumentNullException(nameof(logger));
_umbracoContextAccessor = umbracoContextAccessor ?? throw new System.ArgumentNullException(nameof(umbracoContextAccessor));
_publishedRouter = publishedRouter ?? throw new System.ArgumentNullException(nameof(publishedRouter));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor));
_publishedRouter = publishedRouter ?? throw new ArgumentNullException(nameof(publishedRouter));
_globalSettings = globalSettings.Value;
_hostingEnvironment = hostingEnvironment ?? throw new System.ArgumentNullException(nameof(hostingEnvironment));
_runtime = runtime ?? throw new System.ArgumentNullException(nameof(runtime));
_routeValuesFactory = routeValuesFactory ?? throw new System.ArgumentNullException(nameof(routeValuesFactory));
_routableDocumentFilter = routableDocumentFilter ?? throw new System.ArgumentNullException(nameof(routableDocumentFilter));
_hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment));
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
_routeValuesFactory = routeValuesFactory ?? throw new ArgumentNullException(nameof(routeValuesFactory));
_routableDocumentFilter = routableDocumentFilter ?? throw new ArgumentNullException(nameof(routableDocumentFilter));
_dataProtectionProvider = dataProtectionProvider;
_controllerActionSearcher = controllerActionSearcher;
}
/// <inheritdoc/>
@@ -87,23 +103,33 @@ namespace Umbraco.Web.Website.Routing
// Check if there is no existing content and return the no content controller
if (!_umbracoContextAccessor.UmbracoContext.Content.HasContent())
{
values["controller"] = ControllerExtensions.GetControllerName<RenderNoContentController>();
values["action"] = nameof(RenderNoContentController.Index);
values[ControllerToken] = ControllerExtensions.GetControllerName<RenderNoContentController>();
values[ActionToken] = nameof(RenderNoContentController.Index);
return await Task.FromResult(values);
return values;
}
IPublishedRequest publishedRequest = await RouteRequestAsync(_umbracoContextAccessor.UmbracoContext);
UmbracoRouteValues routeDef = _routeValuesFactory.Create(httpContext, values, publishedRequest);
UmbracoRouteValues umbracoRouteValues = _routeValuesFactory.Create(httpContext, publishedRequest);
values["controller"] = routeDef.ControllerName;
if (string.IsNullOrWhiteSpace(routeDef.ActionName) == false)
// Store the route values as a httpcontext feature
httpContext.Features.Set(umbracoRouteValues);
// Need to check if there is form data being posted back to an Umbraco URL
PostedDataProxyInfo postedInfo = GetFormInfo(httpContext, values);
if (postedInfo != null)
{
values["action"] = routeDef.ActionName;
return HandlePostedValues(postedInfo, httpContext, values);
}
return await Task.FromResult(values);
values[ControllerToken] = umbracoRouteValues.ControllerName;
if (string.IsNullOrWhiteSpace(umbracoRouteValues.ActionName) == false)
{
values[ActionToken] = umbracoRouteValues.ActionName;
}
return values;
}
private async Task<IPublishedRequest> RouteRequestAsync(IUmbracoContext umbracoContext)
@@ -123,5 +149,92 @@ namespace Umbraco.Web.Website.Routing
return routedRequest;
}
/// <summary>
/// Checks the request and query strings to see if it matches the definition of having a Surface controller
/// posted/get value, if so, then we return a PostedDataProxyInfo object with the correct information.
/// </summary>
private PostedDataProxyInfo GetFormInfo(HttpContext httpContext, RouteValueDictionary values)
{
if (httpContext is null)
{
throw new ArgumentNullException(nameof(httpContext));
}
// if it is a POST/GET then a value must be in the request
if (!httpContext.Request.Query.TryGetValue("ufprt", out StringValues encodedVal)
&& (!httpContext.Request.HasFormContentType || !httpContext.Request.Form.TryGetValue("ufprt", out encodedVal)))
{
return null;
}
if (!EncryptionHelper.DecryptAndValidateEncryptedRouteString(
_dataProtectionProvider,
encodedVal,
out IDictionary<string, string> decodedParts))
{
return null;
}
// Get all route values that are not the default ones and add them separately so they eventually get to action parameters
foreach (KeyValuePair<string, string> item in decodedParts.Where(x => ReservedAdditionalKeys.AllKeys.Contains(x.Key) == false))
{
values[item.Key] = item.Value;
}
// return the proxy info without the surface id... could be a local controller.
return new PostedDataProxyInfo
{
ControllerName = WebUtility.UrlDecode(decodedParts.First(x => x.Key == ReservedAdditionalKeys.Controller).Value),
ActionName = WebUtility.UrlDecode(decodedParts.First(x => x.Key == ReservedAdditionalKeys.Action).Value),
Area = WebUtility.UrlDecode(decodedParts.First(x => x.Key == ReservedAdditionalKeys.Area).Value),
};
}
private RouteValueDictionary HandlePostedValues(PostedDataProxyInfo postedInfo, HttpContext httpContext, RouteValueDictionary values)
{
// set the standard route values/tokens
values[ControllerToken] = postedInfo.ControllerName;
values[ActionToken] = postedInfo.ActionName;
ControllerActionSearchResult surfaceControllerQueryResult = _controllerActionSearcher.Find<SurfaceController>(postedInfo.ControllerName, postedInfo.ActionName);
if (surfaceControllerQueryResult == null || !surfaceControllerQueryResult.Success)
{
throw new InvalidOperationException("Could not find a Surface controller route in the RouteTable for controller name " + postedInfo.ControllerName);
}
// set the area if one is there.
if (!postedInfo.Area.IsNullOrWhiteSpace())
{
values["area"] = postedInfo.Area;
}
return values;
}
private class PostedDataProxyInfo
{
public string ControllerName { get; set; }
public string ActionName { get; set; }
public string Area { get; set; }
}
// Define reserved dictionary keys for controller, action and area specified in route additional values data
private static class ReservedAdditionalKeys
{
internal static readonly string[] AllKeys = new[]
{
Controller,
Action,
Area
};
internal const string Controller = "c";
internal const string Action = "a";
internal const string Area = "ar";
}
}
}

View File

@@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Routing;
using Umbraco.Core;
using Umbraco.Core.Strings;
using Umbraco.Extensions;
using Umbraco.Web.Common.Controllers;
using Umbraco.Web.Common.Routing;
using Umbraco.Web.Features;
using Umbraco.Web.Routing;
@@ -20,7 +21,7 @@ namespace Umbraco.Web.Website.Routing
private readonly IUmbracoRenderingDefaults _renderingDefaults;
private readonly IShortStringHelper _shortStringHelper;
private readonly UmbracoFeatures _umbracoFeatures;
private readonly HijackedRouteEvaluator _hijackedRouteEvaluator;
private readonly IControllerActionSearcher _controllerActionSearcher;
private readonly IPublishedRouter _publishedRouter;
private readonly Lazy<string> _defaultControllerName;
@@ -31,13 +32,13 @@ namespace Umbraco.Web.Website.Routing
IUmbracoRenderingDefaults renderingDefaults,
IShortStringHelper shortStringHelper,
UmbracoFeatures umbracoFeatures,
HijackedRouteEvaluator hijackedRouteEvaluator,
IControllerActionSearcher controllerActionSearcher,
IPublishedRouter publishedRouter)
{
_renderingDefaults = renderingDefaults;
_shortStringHelper = shortStringHelper;
_umbracoFeatures = umbracoFeatures;
_hijackedRouteEvaluator = hijackedRouteEvaluator;
_controllerActionSearcher = controllerActionSearcher;
_publishedRouter = publishedRouter;
_defaultControllerName = new Lazy<string>(() => ControllerExtensions.GetControllerName(_renderingDefaults.DefaultControllerType));
}
@@ -48,18 +49,13 @@ namespace Umbraco.Web.Website.Routing
protected string DefaultControllerName => _defaultControllerName.Value;
/// <inheritdoc/>
public UmbracoRouteValues Create(HttpContext httpContext, RouteValueDictionary values, IPublishedRequest request)
public UmbracoRouteValues Create(HttpContext httpContext, IPublishedRequest request)
{
if (httpContext is null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (values is null)
{
throw new ArgumentNullException(nameof(values));
}
if (request is null)
{
throw new ArgumentNullException(nameof(request));
@@ -90,9 +86,6 @@ namespace Umbraco.Web.Website.Routing
def = CheckNoTemplate(def);
// store the route definition
values.TryAdd(Constants.Web.UmbracoRouteDefinitionDataToken, def);
return def;
}
@@ -106,7 +99,7 @@ namespace Umbraco.Web.Website.Routing
var customControllerName = request.PublishedContent?.ContentType?.Alias;
if (customControllerName != null)
{
HijackedRouteResult hijackedResult = _hijackedRouteEvaluator.Evaluate(customControllerName, def.TemplateName);
ControllerActionSearchResult hijackedResult = _controllerActionSearcher.Find<IRenderController>(customControllerName, def.TemplateName);
if (hijackedResult.Success)
{
return new UmbracoRouteValues(

View File

@@ -1,278 +0,0 @@
using System;
using System.Collections.Specialized;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Umbraco.Core;
using Umbraco.Core.Models;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Web.Composing;
using Umbraco.Web.Routing;
namespace Umbraco.Web.Mvc
{
/// <summary>
/// Redirects to an Umbraco page by Id or Entity
/// </summary>
/// Migrated already to .Net Core
public class RedirectToUmbracoPageResult : ActionResult
{
private IPublishedContent _publishedContent;
private readonly int _pageId;
private readonly Guid _key;
private NameValueCollection _queryStringValues;
private IPublishedUrlProvider _publishedUrlProvider;
private string _url;
public string Url
{
get
{
if (!_url.IsNullOrWhiteSpace()) return _url;
if (PublishedContent == null)
{
throw new InvalidOperationException(string.Format("Cannot redirect, no entity was found for id {0}", _pageId));
}
var result = _publishedUrlProvider.GetUrl(PublishedContent.Id);
if (result != "#")
{
_url = result;
return _url;
}
throw new InvalidOperationException(string.Format("Could not route to entity with id {0}, the NiceUrlProvider could not generate a URL", _pageId));
}
}
public int PageId
{
get { return _pageId; }
}
public Guid Key
{
get { return _key; }
}
public IPublishedContent PublishedContent
{
get
{
if (_publishedContent != null) return _publishedContent;
if (_pageId != default(int))
{
_publishedContent = Current.UmbracoContext.Content.GetById(_pageId);
}
else if (_key != default(Guid))
{
_publishedContent = Current.UmbracoContext.Content.GetById(_key);
}
return _publishedContent;
}
}
/// <summary>
/// Creates a new RedirectToUmbracoResult
/// </summary>
/// <param name="pageId"></param>
public RedirectToUmbracoPageResult(int pageId)
: this(pageId, Current.PublishedUrlProvider)
{
}
/// <summary>
/// Creates a new RedirectToUmbracoResult
/// </summary>
/// <param name="pageId"></param>
/// <param name="queryStringValues"></param>
public RedirectToUmbracoPageResult(int pageId, NameValueCollection queryStringValues)
: this(pageId, queryStringValues, Current.PublishedUrlProvider)
{
}
/// <summary>
/// Creates a new RedirectToUmbracoResult
/// </summary>
/// <param name="pageId"></param>
/// <param name="queryString"></param>
public RedirectToUmbracoPageResult(int pageId, string queryString)
: this(pageId, queryString, Current.PublishedUrlProvider)
{
}
/// <summary>
/// Creates a new RedirectToUmbracoResult
/// </summary>
/// <param name="publishedContent"></param>
public RedirectToUmbracoPageResult(IPublishedContent publishedContent)
: this(publishedContent, Current.PublishedUrlProvider)
{
}
/// <summary>
/// Creates a new RedirectToUmbracoResult
/// </summary>
/// <param name="publishedContent"></param>
/// <param name="queryStringValues"></param>
public RedirectToUmbracoPageResult(IPublishedContent publishedContent, NameValueCollection queryStringValues)
: this(publishedContent, queryStringValues, Current.PublishedUrlProvider)
{
}
/// <summary>
/// Creates a new RedirectToUmbracoResult
/// </summary>
/// <param name="queryString"></param>
/// <param name="queryStringValues"></param>
public RedirectToUmbracoPageResult(IPublishedContent publishedContent, string queryString)
: this(publishedContent, queryString, Current.PublishedUrlProvider)
{
}
/// <summary>
/// Creates a new RedirectToUmbracoResult
/// </summary>
/// <param name="pageId"></param>
/// <param name="umbracoContextAccessor"></param>
public RedirectToUmbracoPageResult(int pageId, IPublishedUrlProvider publishedUrlProvider)
{
_pageId = pageId;
_publishedUrlProvider = publishedUrlProvider;
}
/// <summary>
/// Creates a new RedirectToUmbracoResult
/// </summary>
/// <param name="pageId"></param>
/// <param name="queryStringValues"></param>
/// <param name="umbracoContextAccessor"></param>
public RedirectToUmbracoPageResult(int pageId, NameValueCollection queryStringValues, IPublishedUrlProvider publishedUrlProvider)
{
_pageId = pageId;
_queryStringValues = queryStringValues;
_publishedUrlProvider = publishedUrlProvider;
}
/// <summary>
/// Creates a new RedirectToUmbracoResult
/// </summary>
/// <param name="pageId"></param>
/// <param name="queryString"></param>
/// <param name="umbracoContextAccessor"></param>
public RedirectToUmbracoPageResult(int pageId, string queryString, IPublishedUrlProvider publishedUrlProvider)
{
_pageId = pageId;
_queryStringValues = ParseQueryString(queryString);
_publishedUrlProvider = publishedUrlProvider;
}
/// <summary>
/// Creates a new RedirectToUmbracoResult
/// </summary>
/// <param name="key"></param>
/// <param name="umbracoContextAccessor"></param>
public RedirectToUmbracoPageResult(Guid key)
{
_key = key;
}
/// <summary>
/// Creates a new RedirectToUmbracoResult
/// </summary>
/// <param name="key"></param>
/// <param name="queryStringValues"></param>
public RedirectToUmbracoPageResult(Guid key, NameValueCollection queryStringValues)
{
_key = key;
_queryStringValues = queryStringValues;
}
/// <summary>
/// Creates a new RedirectToUmbracoResult
/// </summary>
/// <param name="key"></param>
/// <param name="queryString"></param>
public RedirectToUmbracoPageResult(Guid key, string queryString)
{
_key = key;
_queryStringValues = ParseQueryString(queryString);
}
/// <summary>
/// Creates a new RedirectToUmbracoResult
/// </summary>
/// <param name="publishedContent"></param>
/// <param name="umbracoContextAccessor"></param>
public RedirectToUmbracoPageResult(IPublishedContent publishedContent, IPublishedUrlProvider publishedUrlProvider)
{
_publishedContent = publishedContent;
_pageId = publishedContent.Id;
_publishedUrlProvider = publishedUrlProvider;
}
/// <summary>
/// Creates a new RedirectToUmbracoResult
/// </summary>
/// <param name="publishedContent"></param>
/// <param name="queryStringValues"></param>
/// <param name="umbracoContextAccessor"></param>
public RedirectToUmbracoPageResult(IPublishedContent publishedContent, NameValueCollection queryStringValues, IPublishedUrlProvider publishedUrlProvider)
{
_publishedContent = publishedContent;
_pageId = publishedContent.Id;
_queryStringValues = queryStringValues;
_publishedUrlProvider = publishedUrlProvider;
}
/// <summary>
/// Creates a new RedirectToUmbracoResult
/// </summary>
/// <param name="publishedContent"></param>
/// <param name="queryString"></param>
/// <param name="umbracoContextAccessor"></param>
public RedirectToUmbracoPageResult(IPublishedContent publishedContent, string queryString, IPublishedUrlProvider publishedUrlProvider)
{
_publishedContent = publishedContent;
_pageId = publishedContent.Id;
_queryStringValues = ParseQueryString(queryString);
_publishedUrlProvider = publishedUrlProvider;
}
public override void ExecuteResult(ControllerContext context)
{
if (context == null) throw new ArgumentNullException("context");
if (context.IsChildAction)
{
throw new InvalidOperationException("Cannot redirect from a Child Action");
}
var destinationUrl = UrlHelper.GenerateContentUrl(Url, context.HttpContext);
if (_queryStringValues != null && _queryStringValues.Count > 0)
{
destinationUrl = destinationUrl += "?" + string.Join("&",
_queryStringValues.AllKeys.Select(x => x + "=" + HttpUtility.UrlEncode(_queryStringValues[x])));
}
context.Controller.TempData.Keep();
context.HttpContext.Response.Redirect(destinationUrl, endResponse: false);
}
private NameValueCollection ParseQueryString(string queryString)
{
if (!string.IsNullOrEmpty(queryString))
{
return HttpUtility.ParseQueryString(queryString);
}
return null;
}
}
}

View File

@@ -1,43 +0,0 @@
using System;
using System.Web.Mvc;
namespace Umbraco.Web.Mvc
{
/// <summary>
/// Redirects to the current URL rendering an Umbraco page including it's query strings
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// Migrated already to .Net Core
public class RedirectToUmbracoUrlResult : ActionResult
{
private readonly IUmbracoContext _umbracoContext;
/// <summary>
/// Creates a new RedirectToUmbracoResult
/// </summary>
/// <param name="umbracoContext"></param>
public RedirectToUmbracoUrlResult(IUmbracoContext umbracoContext)
{
_umbracoContext = umbracoContext;
}
public override void ExecuteResult(ControllerContext context)
{
if (context == null) throw new ArgumentNullException("context");
if (context.IsChildAction)
{
throw new InvalidOperationException("Cannot redirect from a Child Action");
}
var destinationUrl = _umbracoContext.OriginalRequestUrl.PathAndQuery;
context.Controller.TempData.Keep();
context.HttpContext.Response.Redirect(destinationUrl, endResponse: false);
}
}
}

View File

@@ -3,16 +3,12 @@ using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using System.Web.SessionState;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Umbraco.Core;
using Umbraco.Core.Composing;
using Umbraco.Core.Strings;
using Umbraco.Web.Features;
using Umbraco.Web.Models;
using Umbraco.Web.Routing;
using Umbraco.Core.Strings;
using Current = Umbraco.Web.Composing.Current;
namespace Umbraco.Web.Mvc
@@ -27,29 +23,21 @@ namespace Umbraco.Web.Mvc
internal const string Area = "ar";
}
private readonly IControllerFactory _controllerFactory;
private readonly IShortStringHelper _shortStringHelper;
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
private readonly IUmbracoContext _umbracoContext;
public RenderRouteHandler(IUmbracoContextAccessor umbracoContextAccessor, IControllerFactory controllerFactory, IShortStringHelper shortStringHelper)
{
_umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor));
_controllerFactory = controllerFactory ?? throw new ArgumentNullException(nameof(controllerFactory));
_shortStringHelper = shortStringHelper ?? throw new ArgumentNullException(nameof(shortStringHelper));
}
public RenderRouteHandler(IUmbracoContext umbracoContext, IControllerFactory controllerFactory, IShortStringHelper shortStringHelper)
{
_umbracoContext = umbracoContext ?? throw new ArgumentNullException(nameof(umbracoContext));
_controllerFactory = controllerFactory ?? throw new ArgumentNullException(nameof(controllerFactory));
_shortStringHelper = shortStringHelper ?? throw new ArgumentNullException(nameof(shortStringHelper));
}
private IUmbracoContext UmbracoContext => _umbracoContext ?? _umbracoContextAccessor.UmbracoContext;
private UmbracoFeatures Features => Current.Factory.GetRequiredService<UmbracoFeatures>(); // TODO: inject
#region IRouteHandler Members
/// <summary>
@@ -74,21 +62,10 @@ namespace Umbraco.Web.Mvc
#endregion
private void UpdateRouteDataForRequest(ContentModel contentModel, RequestContext requestContext)
{
if (contentModel == null) throw new ArgumentNullException(nameof(contentModel));
if (requestContext == null) throw new ArgumentNullException(nameof(requestContext));
// requestContext.RouteData.DataTokens[Core.Constants.Web.UmbracoDataToken] = contentModel;
// the rest should not change -- it's only the published content that has changed
}
/// <summary>
/// Checks the request and query strings to see if it matches the definition of having a Surface controller
/// posted/get value, if so, then we return a PostedDataProxyInfo object with the correct information.
/// </summary>
/// <param name="requestContext"></param>
/// <returns></returns>
internal static PostedDataProxyInfo GetFormInfo(RequestContext requestContext)
{
if (requestContext == null) throw new ArgumentNullException(nameof(requestContext));
@@ -144,8 +121,6 @@ namespace Umbraco.Web.Mvc
/// Handles a posted form to an Umbraco URL and ensures the correct controller is routed to and that
/// the right DataTokens are set.
/// </summary>
/// <param name="requestContext"></param>
/// <param name="postedInfo"></param>
internal static IHttpHandler HandlePostedValues(RequestContext requestContext, PostedDataProxyInfo postedInfo)
{
if (requestContext == null) throw new ArgumentNullException(nameof(requestContext));

View File

@@ -10,9 +10,6 @@ using Umbraco.Web.Composing;
namespace Umbraco.Web.Mvc
{
/// <summary>
/// Provides a base class for front-end add-in controllers.
/// </summary>
/// Migrated already to .Net Core without MergeModelStateToChildAction and MergeParentContextViewData action filters
/// TODO: Migrate MergeModelStateToChildAction and MergeParentContextViewData action filters
[MergeModelStateToChildAction]
@@ -26,197 +23,5 @@ namespace Umbraco.Web.Mvc
: base(umbracoContextAccessor, databaseFactory, services, appCaches,profilingLogger)
{ }
/// <summary>
/// Redirects to the Umbraco page with the given id
/// </summary>
/// <param name="pageId"></param>
/// <returns></returns>
protected RedirectToUmbracoPageResult RedirectToUmbracoPage(int pageId)
{
return new RedirectToUmbracoPageResult(pageId, Current.PublishedUrlProvider);
}
/// <summary>
/// Redirects to the Umbraco page with the given id and passes provided querystring
/// </summary>
/// <param name="pageId"></param>
/// <param name="queryStringValues"></param>
/// <returns></returns>
protected RedirectToUmbracoPageResult RedirectToUmbracoPage(int pageId, NameValueCollection queryStringValues)
{
return new RedirectToUmbracoPageResult(pageId, queryStringValues, Current.PublishedUrlProvider);
}
/// <summary>
/// Redirects to the Umbraco page with the given id and passes provided querystring
/// </summary>
/// <param name="pageId"></param>
/// <param name="queryString"></param>
/// <returns></returns>
protected RedirectToUmbracoPageResult RedirectToUmbracoPage(int pageId, string queryString)
{
return new RedirectToUmbracoPageResult(pageId, queryString, Current.PublishedUrlProvider);
}
/// <summary>
/// Redirects to the Umbraco page with the given id
/// </summary>
/// <param name="pageId"></param>
/// <returns></returns>
protected RedirectToUmbracoPageResult RedirectToUmbracoPage(Guid key)
{
return new RedirectToUmbracoPageResult(key);
}
/// <summary>
/// Redirects to the Umbraco page with the given id and passes provided querystring
/// </summary>
/// <param name="pageId"></param>
/// <param name="queryStringValues"></param>
/// <returns></returns>
protected RedirectToUmbracoPageResult RedirectToUmbracoPage(Guid key, NameValueCollection queryStringValues)
{
return new RedirectToUmbracoPageResult(key, queryStringValues);
}
/// <summary>
/// Redirects to the Umbraco page with the given id and passes provided querystring
/// </summary>
/// <param name="pageId"></param>
/// <param name="queryString"></param>
/// <returns></returns>
protected RedirectToUmbracoPageResult RedirectToUmbracoPage(Guid key, string queryString)
{
return new RedirectToUmbracoPageResult(key, queryString);
}
/// <summary>
/// Redirects to the Umbraco page with the given id
/// </summary>
/// <param name="publishedContent"></param>
/// <returns></returns>
protected RedirectToUmbracoPageResult RedirectToUmbracoPage(IPublishedContent publishedContent)
{
return new RedirectToUmbracoPageResult(publishedContent, Current.PublishedUrlProvider);
}
/// <summary>
/// Redirects to the Umbraco page with the given id and passes provided querystring
/// </summary>
/// <param name="publishedContent"></param>
/// <param name="queryStringValues"></param>
/// <returns></returns>
protected RedirectToUmbracoPageResult RedirectToUmbracoPage(IPublishedContent publishedContent, NameValueCollection queryStringValues)
{
return new RedirectToUmbracoPageResult(publishedContent, queryStringValues, Current.PublishedUrlProvider);
}
/// <summary>
/// Redirects to the Umbraco page with the given id and passes provided querystring
/// </summary>
/// <param name="publishedContent"></param>
/// <param name="queryString"></param>
/// <returns></returns>
protected RedirectToUmbracoPageResult RedirectToUmbracoPage(IPublishedContent publishedContent, string queryString)
{
return new RedirectToUmbracoPageResult(publishedContent, queryString, Current.PublishedUrlProvider);
}
/// <summary>
/// Redirects to the currently rendered Umbraco page
/// </summary>
/// <returns></returns>
protected RedirectToUmbracoPageResult RedirectToCurrentUmbracoPage()
{
return new RedirectToUmbracoPageResult(CurrentPage, Current.PublishedUrlProvider);
}
/// <summary>
/// Redirects to the currently rendered Umbraco page and passes provided querystring
/// </summary>
/// <param name="queryStringValues"></param>
/// <returns></returns>
protected RedirectToUmbracoPageResult RedirectToCurrentUmbracoPage(NameValueCollection queryStringValues)
{
return new RedirectToUmbracoPageResult(CurrentPage, queryStringValues, Current.PublishedUrlProvider);
}
/// <summary>
/// Redirects to the currently rendered Umbraco page and passes provided querystring
/// </summary>
/// <param name="queryString"></param>
/// <returns></returns>
protected RedirectToUmbracoPageResult RedirectToCurrentUmbracoPage(string queryString)
{
return new RedirectToUmbracoPageResult(CurrentPage, queryString, Current.PublishedUrlProvider);
}
/// <summary>
/// Redirects to the currently rendered Umbraco URL
/// </summary>
/// <returns></returns>
/// <remarks>
/// 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.
/// </remarks>
protected RedirectToUmbracoUrlResult RedirectToCurrentUmbracoUrl()
{
return new RedirectToUmbracoUrlResult(UmbracoContext);
}
/// <summary>
/// Returns the currently rendered Umbraco page
/// </summary>
/// <returns></returns>
protected UmbracoPageResult CurrentUmbracoPage()
{
return new UmbracoPageResult(ProfilingLogger);
}
/// <summary>
/// Gets the current page.
/// </summary>
protected virtual IPublishedContent CurrentPage
{
get
{
var routeDefAttempt = TryGetRouteDefinitionFromAncestorViewContexts();
if (routeDefAttempt.Success == false)
throw routeDefAttempt.Exception;
var routeDef = routeDefAttempt.Result;
return routeDef.PublishedRequest.PublishedContent;
}
}
/// <summary>
/// we need to recursively find the route definition based on the parent view context
/// </summary>
/// <returns></returns>
/// <remarks>
/// We may have Child Actions within Child actions so we need to recursively look this up.
/// see: http://issues.umbraco.org/issue/U4-1844
/// </remarks>
private Attempt<RouteDefinition> TryGetRouteDefinitionFromAncestorViewContexts()
{
var currentContext = ControllerContext;
while (currentContext != null)
{
var currentRouteData = currentContext.RouteData;
if (currentRouteData.Values.ContainsKey(Core.Constants.Web.UmbracoRouteDefinitionDataToken))
{
return Attempt.Succeed((RouteDefinition)currentRouteData.Values[Core.Constants.Web.UmbracoRouteDefinitionDataToken]);
}
currentContext = currentContext.IsChildAction
? currentContext.ParentActionViewContext
: null;
}
return Attempt<RouteDefinition>.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"));
}
}
}

View File

@@ -1,140 +0,0 @@
using System;
using System.IO;
using System.Web.Mvc;
using System.Web.Routing;
using Umbraco.Core;
using Umbraco.Core.Logging;
namespace Umbraco.Web.Mvc
{
/// <summary>
/// Used by posted forms to proxy the result to the page in which the current URL matches on
/// </summary>
/// Migrated already to .Net Core
public class UmbracoPageResult : ActionResult
{
private readonly IProfilingLogger _profilingLogger;
public UmbracoPageResult(IProfilingLogger profilingLogger)
{
_profilingLogger = profilingLogger;
}
public override void ExecuteResult(ControllerContext context)
{
ResetRouteData(context.RouteData);
ValidateRouteData(context.RouteData);
var routeDef = (RouteDefinition)context.RouteData.Values[Umbraco.Core.Constants.Web.UmbracoRouteDefinitionDataToken];
var factory = ControllerBuilder.Current.GetControllerFactory();
context.RouteData.Values["action"] = routeDef.ActionName;
ControllerBase controller = null;
try
{
controller = CreateController(context, factory, routeDef);
CopyControllerData(context, controller);
ExecuteControllerAction(context, controller);
}
finally
{
CleanupController(controller, factory);
}
}
/// <summary>
/// Executes the controller action
/// </summary>
private void ExecuteControllerAction(ControllerContext context, IController controller)
{
using (_profilingLogger.TraceDuration<UmbracoPageResult>("Executing Umbraco RouteDefinition controller", "Finished"))
{
controller.Execute(context.RequestContext);
}
}
/// <summary>
/// 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.
/// </summary>
private static void ResetRouteData(RouteData routeData)
{
routeData.DataTokens["area"] = null;
routeData.DataTokens["Namespaces"] = null;
}
/// <summary>
/// Validate that the current page execution is not being handled by the normal umbraco routing system
/// </summary>
private static void ValidateRouteData(RouteData routeData)
{
if (routeData.Values.ContainsKey(Umbraco.Core.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");
}
}
/// <summary>
/// Ensure ModelState, ViewData and TempData is copied across
/// </summary>
private static void CopyControllerData(ControllerContext context, ControllerBase controller)
{
controller.ViewData.ModelState.Merge(context.Controller.ViewData.ModelState);
foreach (var d in context.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 as Controller;
var sourceController = context.Controller as Controller;
if (targetController != null && sourceController != null)
{
targetController.TempDataProvider = sourceController.TempDataProvider;
targetController.TempData = sourceController.TempData;
targetController.TempData.Save(sourceController.ControllerContext, sourceController.TempDataProvider);
}
}
/// <summary>
/// Creates a controller using the controller factory
/// </summary>
private static ControllerBase CreateController(ControllerContext context, IControllerFactory factory, RouteDefinition routeDef)
{
var controller = factory.CreateController(context.RequestContext, routeDef.ControllerName) as ControllerBase;
if (controller == null)
throw new InvalidOperationException("Could not create controller with name " + routeDef.ControllerName + ".");
return controller;
}
/// <summary>
/// Cleans up the controller by releasing it using the controller factory, and by disposing it.
/// </summary>
private static void CleanupController(IController controller, IControllerFactory factory)
{
if (controller != null)
factory.ReleaseController(controller);
if (controller != null)
controller.DisposeIfDisposable();
}
private class DummyView : IView
{
public void Render(ViewContext viewContext, TextWriter writer)
{
}
}
}
}

View File

@@ -1,40 +0,0 @@
using System.Web.Mvc;
using Umbraco.Core;
using Umbraco.Web.Composing;
namespace Umbraco.Web.Mvc
{
/// <summary>
/// If Umbraco.Core.UseHttps property in web.config is set to true, this filter will redirect any http access to https.
/// </summary>
public class UmbracoRequireHttpsAttribute : RequireHttpsAttribute
{
/// <summary>
/// If Umbraco.Core.UseHttps is true and we have a non-HTTPS request, handle redirect.
/// </summary>
/// <param name="filterContext">Filter context</param>
protected override void HandleNonHttpsRequest(AuthorizationContext filterContext)
{
// If Umbraco.Core.UseHttps is set, let base method handle redirect. Otherwise, we don't care.
if (/*Current.Configs.Global().UseHttps*/ false)
{
base.HandleNonHttpsRequest(filterContext);
}
}
/// <summary>
/// Check to see if HTTPS is currently being used if Umbraco.Core.UseHttps is true.
/// </summary>
/// <param name="filterContext">Filter context</param>
public override void OnAuthorization(AuthorizationContext filterContext)
{
// If umbracoSSL is set, let base method handle checking for HTTPS. Otherwise, we don't care.
if (/*Current.Configs.Global().UseHttps*/ false)
{
base.OnAuthorization(filterContext);
}
}
}
}

View File

@@ -180,10 +180,8 @@
<Compile Include="WebApi\SessionHttpControllerRouteHandler.cs" />
<Compile Include="WebApi\UmbracoApiControllerTypeCollectionBuilder.cs" />
<Compile Include="Runtime\WebInitialComponent.cs" />
<Compile Include="Mvc\UmbracoRequireHttpsAttribute.cs" />
<Compile Include="Mvc\ProfilingView.cs" />
<Compile Include="Mvc\NotFoundHandler.cs" />
<Compile Include="Mvc\RedirectToUmbracoUrlResult.cs" />
<Compile Include="Mvc\UmbracoVirtualNodeByIdRouteHandler.cs" />
<Compile Include="Mvc\EnsurePublishedContentRequestAttribute.cs" />
<Compile Include="Mvc\UmbracoVirtualNodeRouteHandler.cs" />
@@ -216,7 +214,6 @@
<Compile Include="Mvc\MergeModelStateToChildActionAttribute.cs" />
<Compile Include="Mvc\PluginController.cs" />
<Compile Include="Mvc\PostedDataProxyInfo.cs" />
<Compile Include="Mvc\RedirectToUmbracoPageResult.cs" />
<Compile Include="Mvc\Strings.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
@@ -224,7 +221,6 @@
</Compile>
<Compile Include="Mvc\SurfaceController.cs" />
<Compile Include="Mvc\PluginControllerAttribute.cs" />
<Compile Include="Mvc\UmbracoPageResult.cs" />
<Compile Include="RouteCollectionExtensions.cs" />
<Compile Include="UmbracoHelper.cs" />
<Compile Include="Mvc\ViewDataContainerExtensions.cs" />