Added LocalRedirectresult
- Fixes #3346 - Added helper method in controller - Added relevant tests
This commit is contained in:
parent
b6d7012c27
commit
eb398c811d
|
|
@ -30,8 +30,8 @@ namespace Microsoft.AspNet.Mvc
|
|||
string Content(string contentPath);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a value that indicates whether the URL is local. An URL with an absolute path is considered local
|
||||
/// if it does not have a host/authority part. URLs using the virtual paths ('~/') are also local.
|
||||
/// Returns a value that indicates whether the URL is local. A URL with an absolute path is considered local
|
||||
/// if it does not have a host/authority part. URLs using virtual paths ('~/') are also local.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL.</param>
|
||||
/// <returns><c>true</c> if the URL is local; otherwise, <c>false</c>.</returns>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,104 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.AspNet.Mvc.Core;
|
||||
using Microsoft.AspNet.Mvc.Logging;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="ActionResult"/> that returns a redirect to the supplied local URL.
|
||||
/// </summary>
|
||||
public class LocalRedirectResult : ActionResult
|
||||
{
|
||||
private string _localUrl;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LocalRedirectResult"/> class with the values
|
||||
/// provided.
|
||||
/// </summary>
|
||||
/// <param name="localUrl">The local URL to redirect to.</param>
|
||||
public LocalRedirectResult(string localUrl)
|
||||
: this(localUrl, permanent: false)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LocalRedirectResult"/> class with the values
|
||||
/// provided.
|
||||
/// </summary>
|
||||
/// <param name="url">The local URL to redirect to.</param>
|
||||
/// <param name="permanent">Specifies whether the redirect should be permanent (301) or temporary (302).</param>
|
||||
public LocalRedirectResult(string localUrl, bool permanent)
|
||||
{
|
||||
if (string.IsNullOrEmpty(localUrl))
|
||||
{
|
||||
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(localUrl));
|
||||
}
|
||||
|
||||
Permanent = permanent;
|
||||
Url = localUrl;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value that specifies that the redirect should be permanent if true or temporary if false.
|
||||
/// </summary>
|
||||
public bool Permanent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the local URL to redirect to.
|
||||
/// </summary>
|
||||
public string Url
|
||||
{
|
||||
get
|
||||
{
|
||||
return _localUrl;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(value));
|
||||
}
|
||||
|
||||
_localUrl = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="IUrlHelper"/> for this result.
|
||||
/// </summary>
|
||||
public IUrlHelper UrlHelper { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void ExecuteResult(ActionContext context)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
var loggerFactory = context.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger<LocalRedirectResult>();
|
||||
|
||||
var urlHelper = GetUrlHelper(context);
|
||||
|
||||
if (!urlHelper.IsLocalUrl(Url))
|
||||
{
|
||||
throw new InvalidOperationException(Resources.UrlNotLocal);
|
||||
}
|
||||
|
||||
var destinationUrl = urlHelper.Content(Url);
|
||||
logger.LocalRedirectResultExecuting(destinationUrl);
|
||||
context.HttpContext.Response.Redirect(destinationUrl, Permanent);
|
||||
}
|
||||
|
||||
private IUrlHelper GetUrlHelper(ActionContext context)
|
||||
{
|
||||
return UrlHelper ?? context.HttpContext.RequestServices.GetRequiredService<IUrlHelper>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.Logging
|
||||
{
|
||||
public static class LocalRedirectResultLoggerExtensions
|
||||
{
|
||||
private static Action<ILogger, string, Exception> _localRedirectResultExecuting;
|
||||
|
||||
static LocalRedirectResultLoggerExtensions()
|
||||
{
|
||||
_localRedirectResultExecuting = LoggerMessage.Define<string>(
|
||||
LogLevel.Information,
|
||||
1,
|
||||
"Executing LocalRedirectResult, redirecting to {Destination}.");
|
||||
}
|
||||
|
||||
public static void LocalRedirectResultExecuting(this ILogger logger, string destination)
|
||||
{
|
||||
_localRedirectResultExecuting(logger, destination, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -650,11 +650,6 @@ namespace Microsoft.AspNet.Mvc.Core
|
|||
return string.Format(CultureInfo.CurrentCulture, GetString("FileResult_InvalidPath"), p0);
|
||||
}
|
||||
|
||||
internal static string FormatFileResult_PathNotRooted(object p0)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, GetString("FileResult_PathNotRooted"), p0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The input was not valid.
|
||||
/// </summary>
|
||||
|
|
@ -1023,6 +1018,38 @@ namespace Microsoft.AspNet.Mvc.Core
|
|||
return string.Format(CultureInfo.CurrentCulture, GetString("HttpResponseStreamWriter_InvalidBufferSize"), p0, p1, p2, p3, p4);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Path '{0}' was not rooted.
|
||||
/// </summary>
|
||||
internal static string FileResult_PathNotRooted
|
||||
{
|
||||
get { return GetString("FileResult_PathNotRooted"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Path '{0}' was not rooted.
|
||||
/// </summary>
|
||||
internal static string FormatFileResult_PathNotRooted(object p0)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, GetString("FileResult_PathNotRooted"), p0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The supplied URL is not local. A URL with an absolute path is considered local if it does not have a host/authority part. URLs using virtual paths ('~/') are also local.
|
||||
/// </summary>
|
||||
internal static string UrlNotLocal
|
||||
{
|
||||
get { return GetString("UrlNotLocal"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The supplied URL is not local. A URL with an absolute path is considered local if it does not have a host/authority part. URLs using virtual paths ('~/') are also local.
|
||||
/// </summary>
|
||||
internal static string FormatUrlNotLocal()
|
||||
{
|
||||
return GetString("UrlNotLocal");
|
||||
}
|
||||
|
||||
private static string GetString(string name, params string[] formatterNames)
|
||||
{
|
||||
var value = _resourceManager.GetString(name);
|
||||
|
|
|
|||
|
|
@ -319,4 +319,7 @@
|
|||
<value>Path '{0}' was not rooted.</value>
|
||||
<comment>{0} is the path which wasn't rooted</comment>
|
||||
</data>
|
||||
<data name="UrlNotLocal" xml:space="preserve">
|
||||
<value>The supplied URL is not local. A URL with an absolute path is considered local if it does not have a host/authority part. URLs using virtual paths ('~/') are also local.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -613,6 +613,40 @@ namespace Microsoft.AspNet.Mvc
|
|||
return new RedirectResult(url, permanent: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="LocalRedirectResult"/> object that redirects to
|
||||
/// the specified local <paramref name="localUrl"/>.
|
||||
/// </summary>
|
||||
/// <param name="localUrl">The local URL to redirect to.</param>
|
||||
/// <returns>The created <see cref="LocalRedirectResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual LocalRedirectResult LocalRedirect(string localUrl)
|
||||
{
|
||||
if (string.IsNullOrEmpty(localUrl))
|
||||
{
|
||||
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(localUrl));
|
||||
}
|
||||
|
||||
return new LocalRedirectResult(localUrl);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="LocalRedirectResult"/> object with <see cref="LocalRedirectResult.Permanent"/>
|
||||
/// set to true using the specified <paramref name="localUrl"/>.
|
||||
/// </summary>
|
||||
/// <param name="localUrl">The local URL to redirect to.</param>
|
||||
/// <returns>The created <see cref="LocalRedirectResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual LocalRedirectResult LocalRedirectPermanent(string localUrl)
|
||||
{
|
||||
if (string.IsNullOrEmpty(localUrl))
|
||||
{
|
||||
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(localUrl));
|
||||
}
|
||||
|
||||
return new LocalRedirectResult(localUrl, permanent: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Redirects to the specified action using the <paramref name="actionName"/>.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,133 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.AspNet.Http;
|
||||
using Microsoft.AspNet.Mvc.Abstractions;
|
||||
using Microsoft.AspNet.Mvc.Infrastructure;
|
||||
using Microsoft.AspNet.Mvc.Routing;
|
||||
using Microsoft.AspNet.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
{
|
||||
public class LocalRedirectResultTest
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_WithParameterUrl_SetsResultUrlAndNotPermanent()
|
||||
{
|
||||
// Arrange
|
||||
var url = "/test/url";
|
||||
|
||||
// Act
|
||||
var result = new LocalRedirectResult(url);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Permanent);
|
||||
Assert.Same(url, result.Url);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithParameterUrlAndPermanent_SetsResultUrlAndPermanent()
|
||||
{
|
||||
// Arrange
|
||||
var url = "/test/url";
|
||||
|
||||
// Act
|
||||
var result = new LocalRedirectResult(url, permanent: true);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Permanent);
|
||||
Assert.Same(url, result.Url);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Execute_ReturnsExpectedValues()
|
||||
{
|
||||
// Arrange
|
||||
var appRoot = "/";
|
||||
var contentPath = "~/Home/About";
|
||||
var expectedPath = "/Home/About";
|
||||
var httpResponse = new Mock<HttpResponse>();
|
||||
httpResponse.Setup(o => o.Redirect(expectedPath, false))
|
||||
.Verifiable();
|
||||
|
||||
var httpContext = GetHttpContext(appRoot, contentPath, expectedPath, httpResponse.Object);
|
||||
var actionContext = GetActionContext(httpContext);
|
||||
var result = new LocalRedirectResult(contentPath);
|
||||
|
||||
// Act
|
||||
result.ExecuteResult(actionContext);
|
||||
|
||||
// Assert
|
||||
httpResponse.Verify();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("", "Home/About", "/Home/About")]
|
||||
[InlineData("/myapproot", "http://www.example.com", "/test")]
|
||||
public void Execute_Throws_ForNonLocalUrl(
|
||||
string appRoot,
|
||||
string contentPath,
|
||||
string expectedPath)
|
||||
{
|
||||
// Arrange
|
||||
var httpResponse = new Mock<HttpResponse>();
|
||||
httpResponse.Setup(o => o.Redirect(expectedPath, false))
|
||||
.Verifiable();
|
||||
|
||||
var httpContext = GetHttpContext(appRoot, contentPath, expectedPath, httpResponse.Object);
|
||||
var actionContext = GetActionContext(httpContext);
|
||||
var result = new LocalRedirectResult(contentPath);
|
||||
|
||||
// Act & Assert
|
||||
var exception = Assert.Throws<InvalidOperationException>(() => result.ExecuteResult(actionContext));
|
||||
Assert.Equal(
|
||||
"The supplied URL is not local. A URL with an absolute path is considered local if it does not " +
|
||||
"have a host/authority part. URLs using virtual paths ('~/') are also local.",
|
||||
exception.Message);
|
||||
}
|
||||
|
||||
private static ActionContext GetActionContext(HttpContext httpContext)
|
||||
{
|
||||
var routeData = new RouteData();
|
||||
routeData.Routers.Add(new Mock<IRouter>().Object);
|
||||
|
||||
return new ActionContext(httpContext, routeData, new ActionDescriptor());
|
||||
}
|
||||
|
||||
private static IServiceProvider GetServiceProvider(IUrlHelper urlHelper)
|
||||
{
|
||||
var serviceCollection = new ServiceCollection();
|
||||
serviceCollection.AddInstance<IUrlHelper>(urlHelper);
|
||||
serviceCollection.AddTransient<ILoggerFactory, LoggerFactory>();
|
||||
return serviceCollection.BuildServiceProvider();
|
||||
}
|
||||
|
||||
private static HttpContext GetHttpContext(
|
||||
string appRoot,
|
||||
string contentPath,
|
||||
string expectedPath,
|
||||
HttpResponse response)
|
||||
{
|
||||
var httpContext = new Mock<HttpContext>();
|
||||
var actionContext = GetActionContext(httpContext.Object);
|
||||
var actionContextAccessor = new ActionContextAccessor() { ActionContext = actionContext };
|
||||
var mockActionSelector = new Mock<IActionSelector>();
|
||||
var urlHelper = new UrlHelper(actionContextAccessor, mockActionSelector.Object);
|
||||
var serviceProvider = GetServiceProvider(urlHelper);
|
||||
|
||||
httpContext.Setup(o => o.Response)
|
||||
.Returns(response);
|
||||
httpContext.SetupGet(o => o.RequestServices)
|
||||
.Returns(serviceProvider);
|
||||
httpContext.Setup(o => o.Request.PathBase)
|
||||
.Returns(new PathString(appRoot));
|
||||
|
||||
return httpContext.Object;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -107,6 +107,51 @@ namespace Microsoft.AspNet.Mvc.Test
|
|||
() => controller.Redirect(url: url), "url");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LocalRedirect_WithParameterUrl_SetsLocalRedirectResultWithSameUrl()
|
||||
{
|
||||
// Arrange
|
||||
var controller = new TestableController();
|
||||
var url = "/test/url";
|
||||
|
||||
// Act
|
||||
var result = controller.LocalRedirect(url);
|
||||
|
||||
// Assert
|
||||
Assert.IsType<LocalRedirectResult>(result);
|
||||
Assert.False(result.Permanent);
|
||||
Assert.Same(url, result.Url);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LocalRedirectPermanent_WithParameterUrl_SetsLocalRedirectResultPermanentWithSameUrl()
|
||||
{
|
||||
// Arrange
|
||||
var controller = new TestableController();
|
||||
var url = "/test/url";
|
||||
|
||||
// Act
|
||||
var result = controller.LocalRedirectPermanent(url);
|
||||
|
||||
// Assert
|
||||
Assert.IsType<LocalRedirectResult>(result);
|
||||
Assert.True(result.Permanent);
|
||||
Assert.Same(url, result.Url);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
public void LocalRedirect_WithParameter_NullOrEmptyUrl_Throws(string url)
|
||||
{
|
||||
// Arrange
|
||||
var controller = new TestableController();
|
||||
|
||||
// Act & Assert
|
||||
ExceptionAssert.ThrowsArgumentNullOrEmpty(
|
||||
() => controller.LocalRedirect(localUrl: url), "localUrl");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
|
|
|
|||
Loading…
Reference in New Issue