Added LocalRedirectresult

- Fixes #3346
- Added helper method in controller
- Added relevant tests
This commit is contained in:
Ajay Bhargav Baaskaran 2015-10-21 13:14:47 -07:00
parent b6d7012c27
commit eb398c811d
8 changed files with 379 additions and 7 deletions

View File

@ -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>

View File

@ -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>();
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);

View File

@ -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>

View File

@ -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>

View File

@ -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;
}
}
}

View File

@ -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("")]