parent
297196baa0
commit
f568d3c2bc
|
|
@ -0,0 +1,57 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Core.Internal
|
||||
{
|
||||
public static class NormalizedRouteValue
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the case-normalized route value for the specified route <paramref name="key"/>.
|
||||
/// </summary>
|
||||
/// <param name="context">The <see cref="ActionContext"/>.</param>
|
||||
/// <param name="key">The route key to lookup.</param>
|
||||
/// <returns>The value corresponding to the key.</returns>
|
||||
/// <remarks>
|
||||
/// The casing of a route value in <see cref="ActionContext.RouteData"/> is determined by the client.
|
||||
/// This making constructing paths for view locations in a case sensitive file system unreliable. Using the
|
||||
/// <see cref="Abstractions.ActionDescriptor.RouteValues"/> to get route values
|
||||
/// produces consistently cased results.
|
||||
/// </remarks>
|
||||
public static string GetNormalizedRouteValue(ActionContext context, string key)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (key == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(key));
|
||||
}
|
||||
|
||||
if (!context.RouteData.Values.TryGetValue(key, out var routeValue))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var actionDescriptor = context.ActionDescriptor;
|
||||
string normalizedValue = null;
|
||||
|
||||
if (actionDescriptor.RouteValues.TryGetValue(key, out var value) &&
|
||||
!string.IsNullOrEmpty(value))
|
||||
{
|
||||
normalizedValue = value;
|
||||
}
|
||||
|
||||
var stringRouteValue = routeValue?.ToString();
|
||||
if (string.Equals(normalizedValue, stringRouteValue, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return normalizedValue;
|
||||
}
|
||||
|
||||
return stringRouteValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
// 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 System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Core.Internal
|
||||
{
|
||||
public static class ViewEnginePath
|
||||
{
|
||||
private const string ParentDirectoryToken = "..";
|
||||
private static readonly char[] _pathSeparators = new[] { '/', '\\' };
|
||||
|
||||
public static string CombinePath(string first, string second)
|
||||
{
|
||||
Debug.Assert(!string.IsNullOrEmpty(first));
|
||||
|
||||
if (second.StartsWith("/", StringComparison.Ordinal))
|
||||
{
|
||||
// "second" is already an app-rooted path. Return it as-is.
|
||||
return second;
|
||||
}
|
||||
|
||||
string result;
|
||||
|
||||
// Get directory name (including final slash) but do not use Path.GetDirectoryName() to preserve path
|
||||
// normalization.
|
||||
var index = first.LastIndexOf('/');
|
||||
Debug.Assert(index >= 0);
|
||||
|
||||
if (index == first.Length - 1)
|
||||
{
|
||||
// If the first ends in a trailing slash e.g. "/Home/", assume it's a directory.
|
||||
result = first + second;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = first.Substring(0, index + 1) + second;
|
||||
}
|
||||
|
||||
return ResolvePath(result);
|
||||
}
|
||||
|
||||
public static string ResolvePath(string path)
|
||||
{
|
||||
if (!RequiresPathResolution(path))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
var pathSegments = new List<StringSegment>();
|
||||
var tokenizer = new StringTokenizer(path, _pathSeparators);
|
||||
foreach (var segment in tokenizer)
|
||||
{
|
||||
if (segment.Length == 0)
|
||||
{
|
||||
// Ignore multiple directory separators
|
||||
continue;
|
||||
}
|
||||
if (segment.Equals(ParentDirectoryToken, StringComparison.Ordinal))
|
||||
{
|
||||
if (pathSegments.Count == 0)
|
||||
{
|
||||
// Don't resolve the path if we ever escape the file system root. We can't reason about it in a
|
||||
// consistent way.
|
||||
return path;
|
||||
}
|
||||
pathSegments.RemoveAt(pathSegments.Count - 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
pathSegments.Add(segment);
|
||||
}
|
||||
}
|
||||
|
||||
var builder = new StringBuilder();
|
||||
for (var i = 0; i < pathSegments.Count; i++)
|
||||
{
|
||||
var segment = pathSegments[i];
|
||||
builder.Append('/');
|
||||
builder.Append(segment.Buffer, segment.Offset, segment.Length);
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static bool RequiresPathResolution(string path)
|
||||
{
|
||||
return path.IndexOf(ParentDirectoryToken, StringComparison.Ordinal) != -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1284,6 +1284,20 @@ namespace Microsoft.AspNetCore.Mvc.Core
|
|||
internal static string FormatNoRoutesMatchedForPage(object p0)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("NoRoutesMatchedForPage"), p0);
|
||||
|
||||
/// <summary>
|
||||
/// The relative page path '{0}' can only can only be used while executing a Razor Page. Specify a root relative path with a leading '/' to generate a URL outside of a Razor Page.
|
||||
/// </summary>
|
||||
internal static string UrlHelper_RelativePagePathIsNotSupported
|
||||
{
|
||||
get => GetString("UrlHelper_RelativePagePathIsNotSupported");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The relative page path '{0}' can only can only be used while executing a Razor Page. Specify a root relative path with a leading '/' to generate a URL outside of a Razor Page.
|
||||
/// </summary>
|
||||
internal static string FormatUrlHelper_RelativePagePathIsNotSupported(object p0)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("UrlHelper_RelativePagePathIsNotSupported"), p0);
|
||||
|
||||
private static string GetString(string name, params string[] formatterNames)
|
||||
{
|
||||
var value = _resourceManager.GetString(name);
|
||||
|
|
|
|||
|
|
@ -403,4 +403,7 @@
|
|||
<data name="NoRoutesMatchedForPage" xml:space="preserve">
|
||||
<value>No page named '{0}' matches the supplied values.</value>
|
||||
</data>
|
||||
<data name="UrlHelper_RelativePagePathIsNotSupported" xml:space="preserve">
|
||||
<value>The relative page path '{0}' can only can only be used while executing a Razor Page. Specify a root relative path with a leading '/' to generate a URL outside of a Razor Page.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -2,6 +2,9 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.AspNetCore.Mvc.Core;
|
||||
using Microsoft.AspNetCore.Mvc.Core.Internal;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
|
||||
|
|
@ -413,7 +416,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
|
||||
var routeValues = new RouteValueDictionary(values);
|
||||
var ambientValues = urlHelper.ActionContext.RouteData.Values;
|
||||
if (pageName == null)
|
||||
if (string.IsNullOrEmpty(pageName))
|
||||
{
|
||||
if (!routeValues.ContainsKey("page") &&
|
||||
ambientValues.TryGetValue("page", out var value))
|
||||
|
|
@ -423,7 +426,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
}
|
||||
else
|
||||
{
|
||||
routeValues["page"] = pageName;
|
||||
routeValues["page"] = CalculatePageName(urlHelper.ActionContext, pageName);
|
||||
}
|
||||
|
||||
if (!routeValues.ContainsKey("formaction") &&
|
||||
|
|
@ -440,5 +443,24 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
host: host,
|
||||
fragment: fragment);
|
||||
}
|
||||
|
||||
private static object CalculatePageName(ActionContext actionContext, string pageName)
|
||||
{
|
||||
Debug.Assert(pageName.Length > 0);
|
||||
// Paths not qualified with a leading slash are treated as relative to the current page.
|
||||
if (pageName[0] != '/')
|
||||
{
|
||||
var currentPagePath = NormalizedRouteValue.GetNormalizedRouteValue(actionContext, "page");
|
||||
if (string.IsNullOrEmpty(currentPagePath))
|
||||
{
|
||||
// Disallow the use sibling page routing, a Razor page specific feature, from a non-page action.
|
||||
throw new InvalidOperationException(Resources.FormatUrlHelper_RelativePagePathIsNotSupported(pageName));
|
||||
}
|
||||
|
||||
return ViewEnginePath.CombinePath(currentPagePath, pageName);
|
||||
}
|
||||
|
||||
return pageName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ using System.Globalization;
|
|||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using Microsoft.AspNetCore.Mvc.Core.Internal;
|
||||
using Microsoft.AspNetCore.Mvc.Razor.Internal;
|
||||
using Microsoft.AspNetCore.Mvc.ViewEngines;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
|
|
@ -36,9 +37,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
|||
private const string ControllerKey = "controller";
|
||||
private const string PageKey = "page";
|
||||
|
||||
private const string ParentDirectoryToken = "..";
|
||||
private static readonly TimeSpan _cacheExpirationDuration = TimeSpan.FromMinutes(20);
|
||||
private static readonly char[] _pathSeparators = new[] { '/', '\\' };
|
||||
|
||||
private readonly IRazorPageFactoryProvider _pageFactory;
|
||||
private readonly IRazorPageActivator _pageActivator;
|
||||
|
|
@ -100,41 +99,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
|||
/// produces consistently cased results.
|
||||
/// </remarks>
|
||||
public static string GetNormalizedRouteValue(ActionContext context, string key)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (key == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(key));
|
||||
}
|
||||
|
||||
object routeValue;
|
||||
if (!context.RouteData.Values.TryGetValue(key, out routeValue))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var actionDescriptor = context.ActionDescriptor;
|
||||
string normalizedValue = null;
|
||||
|
||||
string value;
|
||||
if (actionDescriptor.RouteValues.TryGetValue(key, out value) &&
|
||||
!string.IsNullOrEmpty(value))
|
||||
{
|
||||
normalizedValue = value;
|
||||
}
|
||||
|
||||
var stringRouteValue = routeValue?.ToString();
|
||||
if (string.Equals(normalizedValue, stringRouteValue, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return normalizedValue;
|
||||
}
|
||||
|
||||
return stringRouteValue;
|
||||
}
|
||||
=> NormalizedRouteValue.GetNormalizedRouteValue(context, key);
|
||||
|
||||
/// <inheritdoc />
|
||||
public RazorPageResult FindPage(ActionContext context, string pageName)
|
||||
|
|
@ -345,59 +310,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
|||
// path relative to currently-executing view, if any.
|
||||
// Not yet executing a view. Start in app root.
|
||||
absolutePath = "/" + pagePath;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Get directory name (including final slash) but do not use Path.GetDirectoryName() to preserve path
|
||||
// normalization.
|
||||
var index = executingFilePath.LastIndexOf('/');
|
||||
Debug.Assert(index >= 0);
|
||||
absolutePath = executingFilePath.Substring(0, index + 1) + pagePath;
|
||||
if (!RequiresPathResolution(pagePath))
|
||||
{
|
||||
return absolutePath;
|
||||
}
|
||||
return ViewEnginePath.ResolvePath(absolutePath);
|
||||
}
|
||||
|
||||
if (!RequiresPathResolution(pagePath))
|
||||
{
|
||||
return absolutePath;
|
||||
}
|
||||
|
||||
var pathSegments = new List<StringSegment>();
|
||||
var tokenizer = new StringTokenizer(absolutePath, _pathSeparators);
|
||||
foreach (var segment in tokenizer)
|
||||
{
|
||||
if (segment.Length == 0)
|
||||
{
|
||||
// Ignore multiple directory separators
|
||||
continue;
|
||||
}
|
||||
if (segment.Equals(ParentDirectoryToken, StringComparison.Ordinal))
|
||||
{
|
||||
if (pathSegments.Count == 0)
|
||||
{
|
||||
// Don't resolve the path if we ever escape the file system root. We can't reason about it in a
|
||||
// consistent way.
|
||||
return absolutePath;
|
||||
}
|
||||
pathSegments.RemoveAt(pathSegments.Count - 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
pathSegments.Add(segment);
|
||||
}
|
||||
}
|
||||
|
||||
var builder = new StringBuilder();
|
||||
for (var i = 0; i < pathSegments.Count; i++)
|
||||
{
|
||||
var segment = pathSegments[i];
|
||||
builder.Append('/');
|
||||
builder.Append(segment.Buffer, segment.Offset, segment.Length);
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
return ViewEnginePath.CombinePath(executingFilePath, pagePath);
|
||||
}
|
||||
|
||||
// internal for tests
|
||||
|
|
@ -585,10 +501,5 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
|||
// Though ./ViewName looks like a relative path, framework searches for that view using view locations.
|
||||
return name.EndsWith(ViewExtension, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool RequiresPathResolution(string path)
|
||||
{
|
||||
return path.IndexOf(ParentDirectoryToken, StringComparison.Ordinal) != -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -941,7 +941,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
|
||||
[Theory]
|
||||
[InlineData(null, null, null, "/", null, "/")]
|
||||
[InlineData(null, null, null, "/Hello", null, "/Hello" )]
|
||||
[InlineData(null, null, null, "/Hello", null, "/Hello")]
|
||||
[InlineData(null, null, null, "Hello", null, "/Hello")]
|
||||
[InlineData("/", null, null, "", null, "/")]
|
||||
[InlineData("/hello/", null, null, "/world", null, "/hello/world")]
|
||||
|
|
@ -1052,7 +1052,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
.Callback((UrlRouteContext context) => actual = context);
|
||||
|
||||
// Act
|
||||
urlHelper.Object.Page("TestPage");
|
||||
urlHelper.Object.Page("/TestPage");
|
||||
|
||||
// Assert
|
||||
urlHelper.Verify();
|
||||
|
|
@ -1062,7 +1062,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
value =>
|
||||
{
|
||||
Assert.Equal("page", value.Key);
|
||||
Assert.Equal("TestPage", value.Value);
|
||||
Assert.Equal("/TestPage", value.Value);
|
||||
});
|
||||
Assert.Null(actual.Host);
|
||||
Assert.Null(actual.Protocol);
|
||||
|
|
@ -1100,7 +1100,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
.Callback((UrlRouteContext context) => actual = context);
|
||||
|
||||
// Act
|
||||
urlHelper.Object.Page("TestPage", values);
|
||||
urlHelper.Object.Page("/TestPage", values);
|
||||
|
||||
// Assert
|
||||
urlHelper.Verify();
|
||||
|
|
@ -1115,7 +1115,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
value =>
|
||||
{
|
||||
Assert.Equal("page", value.Key);
|
||||
Assert.Equal("TestPage", value.Value);
|
||||
Assert.Equal("/TestPage", value.Value);
|
||||
});
|
||||
Assert.Null(actual.Host);
|
||||
Assert.Null(actual.Protocol);
|
||||
|
|
@ -1132,7 +1132,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
.Callback((UrlRouteContext context) => actual = context);
|
||||
|
||||
// Act
|
||||
urlHelper.Object.Page("TestPage", new { id = 13 }, "https");
|
||||
urlHelper.Object.Page("/TestPage", new { id = 13 }, "https");
|
||||
|
||||
// Assert
|
||||
urlHelper.Verify();
|
||||
|
|
@ -1147,7 +1147,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
value =>
|
||||
{
|
||||
Assert.Equal("page", value.Key);
|
||||
Assert.Equal("TestPage", value.Value);
|
||||
Assert.Equal("/TestPage", value.Value);
|
||||
});
|
||||
Assert.Equal("https", actual.Protocol);
|
||||
Assert.Null(actual.Host);
|
||||
|
|
@ -1164,7 +1164,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
.Callback((UrlRouteContext context) => actual = context);
|
||||
|
||||
// Act
|
||||
urlHelper.Object.Page("TestPage", new { id = 13 }, "https", "mytesthost");
|
||||
urlHelper.Object.Page("/TestPage", new { id = 13 }, "https", "mytesthost");
|
||||
|
||||
// Assert
|
||||
urlHelper.Verify();
|
||||
|
|
@ -1179,7 +1179,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
value =>
|
||||
{
|
||||
Assert.Equal("page", value.Key);
|
||||
Assert.Equal("TestPage", value.Value);
|
||||
Assert.Equal("/TestPage", value.Value);
|
||||
});
|
||||
Assert.Equal("https", actual.Protocol);
|
||||
Assert.Equal("mytesthost", actual.Host);
|
||||
|
|
@ -1196,7 +1196,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
.Callback((UrlRouteContext context) => actual = context);
|
||||
|
||||
// Act
|
||||
urlHelper.Object.Page("TestPage", new { id = 13 }, "https", "mytesthost", "#toc");
|
||||
urlHelper.Object.Page("/TestPage", new { id = 13 }, "https", "mytesthost", "#toc");
|
||||
|
||||
// Assert
|
||||
urlHelper.Verify();
|
||||
|
|
@ -1211,7 +1211,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
value =>
|
||||
{
|
||||
Assert.Equal("page", value.Key);
|
||||
Assert.Equal("TestPage", value.Value);
|
||||
Assert.Equal("/TestPage", value.Value);
|
||||
});
|
||||
Assert.Equal("https", actual.Protocol);
|
||||
Assert.Equal("mytesthost", actual.Host);
|
||||
|
|
@ -1354,14 +1354,134 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
});
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Sibling", "/Dir1/Dir2/Sibling")]
|
||||
[InlineData("Dir3/Sibling", "/Dir1/Dir2/Dir3/Sibling")]
|
||||
[InlineData("Dir4/Dir5/Index", "/Dir1/Dir2/Dir4/Dir5/Index")]
|
||||
public void Page_CalculatesPathRelativeToViewEnginePath_WhenNotRooted(string pageName, string expected)
|
||||
{
|
||||
// Arrange
|
||||
UrlRouteContext actual = null;
|
||||
var routeData = new RouteData();
|
||||
var actionContext = GetActionContextForPage("/Dir1/Dir2/About");
|
||||
|
||||
var urlHelper = CreateMockUrlHelper(actionContext);
|
||||
urlHelper.Setup(h => h.RouteUrl(It.IsAny<UrlRouteContext>()))
|
||||
.Callback((UrlRouteContext context) => actual = context);
|
||||
|
||||
// Act
|
||||
urlHelper.Object.Page(pageName);
|
||||
|
||||
// Assert
|
||||
urlHelper.Verify();
|
||||
Assert.NotNull(actual);
|
||||
Assert.Null(actual.RouteName);
|
||||
Assert.Collection(Assert.IsType<RouteValueDictionary>(actual.Values),
|
||||
value =>
|
||||
{
|
||||
Assert.Equal("page", value.Key);
|
||||
Assert.Equal(expected, value.Value);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Page_CalculatesPathRelativeToViewEnginePath_ForIndexPagePaths()
|
||||
{
|
||||
// Arrange
|
||||
var expected = "/Dir1/Dir2/Sibling";
|
||||
UrlRouteContext actual = null;
|
||||
var actionContext = GetActionContextForPage("/Dir1/Dir2/");
|
||||
|
||||
var urlHelper = CreateMockUrlHelper(actionContext);
|
||||
urlHelper.Setup(h => h.RouteUrl(It.IsAny<UrlRouteContext>()))
|
||||
.Callback((UrlRouteContext context) => actual = context);
|
||||
|
||||
// Act
|
||||
urlHelper.Object.Page("Sibling");
|
||||
|
||||
// Assert
|
||||
urlHelper.Verify();
|
||||
Assert.NotNull(actual);
|
||||
Assert.Null(actual.RouteName);
|
||||
Assert.Collection(Assert.IsType<RouteValueDictionary>(actual.Values),
|
||||
value =>
|
||||
{
|
||||
Assert.Equal("page", value.Key);
|
||||
Assert.Equal(expected, value.Value);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Page_CalculatesPathRelativeToViewEnginePath_WhenNotRooted_ForPageAtRoot()
|
||||
{
|
||||
// Arrange
|
||||
var expected = "/SiblingName";
|
||||
UrlRouteContext actual = null;
|
||||
var routeData = new RouteData();
|
||||
var actionContext = new ActionContext
|
||||
{
|
||||
ActionDescriptor = new ActionDescriptor
|
||||
{
|
||||
RouteValues = new Dictionary<string, string>
|
||||
{
|
||||
{ "page", "/Home" },
|
||||
},
|
||||
},
|
||||
RouteData = new RouteData
|
||||
{
|
||||
Values =
|
||||
{
|
||||
[ "page" ] = "/Home"
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var urlHelper = CreateMockUrlHelper(actionContext);
|
||||
urlHelper.Setup(h => h.RouteUrl(It.IsAny<UrlRouteContext>()))
|
||||
.Callback((UrlRouteContext context) => actual = context);
|
||||
|
||||
// Act
|
||||
urlHelper.Object.Page("SiblingName");
|
||||
|
||||
// Assert
|
||||
urlHelper.Verify();
|
||||
Assert.NotNull(actual);
|
||||
Assert.Null(actual.RouteName);
|
||||
Assert.Collection(Assert.IsType<RouteValueDictionary>(actual.Values),
|
||||
value =>
|
||||
{
|
||||
Assert.Equal("page", value.Key);
|
||||
Assert.Equal(expected, value.Value);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Page_Throws_IfRouteValueDoesNotIncludePageKey()
|
||||
{
|
||||
// Arrange
|
||||
var expected = "SiblingName";
|
||||
UrlRouteContext actual = null;
|
||||
var routeData = new RouteData();
|
||||
var actionContext = new ActionContext
|
||||
{
|
||||
RouteData = new RouteData(),
|
||||
};
|
||||
|
||||
var urlHelper = CreateMockUrlHelper(actionContext);
|
||||
urlHelper.Setup(h => h.RouteUrl(It.IsAny<UrlRouteContext>()))
|
||||
.Callback((UrlRouteContext context) => actual = context);
|
||||
|
||||
// Act & Assert
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => urlHelper.Object.Page(expected));
|
||||
Assert.Equal($"The relative page path '{expected}' can only can only be used while executing a Razor Page. " +
|
||||
"Specify a root relative path with a leading '/' to generate a URL outside of a Razor Page.", ex.Message);
|
||||
}
|
||||
|
||||
private static Mock<IUrlHelper> CreateMockUrlHelper(ActionContext context = null)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
context = new ActionContext
|
||||
{
|
||||
RouteData = new RouteData(),
|
||||
};
|
||||
context = GetActionContextForPage("/Page");
|
||||
}
|
||||
|
||||
var urlHelper = new Mock<IUrlHelper>();
|
||||
|
|
@ -1513,6 +1633,27 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
return routeBuilder.Build();
|
||||
}
|
||||
|
||||
private static ActionContext GetActionContextForPage(string page)
|
||||
{
|
||||
return new ActionContext
|
||||
{
|
||||
ActionDescriptor = new ActionDescriptor
|
||||
{
|
||||
RouteValues = new Dictionary<string, string>
|
||||
{
|
||||
{ "page", page },
|
||||
},
|
||||
},
|
||||
RouteData = new RouteData
|
||||
{
|
||||
Values =
|
||||
{
|
||||
[ "page" ] = page
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private class PassThroughRouter : IRouter
|
||||
{
|
||||
public VirtualPathData GetVirtualPath(VirtualPathContext context)
|
||||
|
|
|
|||
|
|
@ -855,6 +855,54 @@ Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary`1[AspNetCore._InjectedP
|
|||
Assert.Equal(expected, response.Headers.Location.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RedirectToSibling_Works()
|
||||
{
|
||||
// Arrange
|
||||
var expected = "/Pages/Redirects/Redirect/10";
|
||||
var response = await Client.GetAsync("/Pages/Redirects/RedirectToSibling/RedirectToRedirect");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
|
||||
Assert.Equal(expected, response.Headers.Location.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RedirectToSibling_RedirectsToIndexPage_WithoutIndexSegment()
|
||||
{
|
||||
// Arrange
|
||||
var expected = "/Pages/Redirects";
|
||||
var response = await Client.GetAsync("/Pages/Redirects/RedirectToSibling/RedirectToIndex");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
|
||||
Assert.Equal(expected, response.Headers.Location.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RedirectToSibling_RedirectsToSubDirectory()
|
||||
{
|
||||
// Arrange
|
||||
var expected = "/Pages/Redirects/SubDir/SubDirPage";
|
||||
var response = await Client.GetAsync("/Pages/Redirects/RedirectToSibling/RedirectToSubDir");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
|
||||
Assert.Equal(expected, response.Headers.Location.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RedirectToSibling_RedirectsToParentDirectory()
|
||||
{
|
||||
// Arrange
|
||||
var expected = "/Pages/Conventions/AuthFolder";
|
||||
var response = await Client.GetAsync("/Pages/Redirects/RedirectToSibling/RedirectToParent");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
|
||||
Assert.Equal(expected, response.Headers.Location.ToString());
|
||||
}
|
||||
|
||||
private async Task AddAntiforgeryHeaders(HttpRequestMessage request)
|
||||
{
|
||||
var getResponse = await Client.GetAsync(request.RequestUri);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
@page "{formaction?}"
|
||||
@functions
|
||||
{
|
||||
public IActionResult OnGetRedirectToIndex() => RedirectToPage("Index");
|
||||
|
||||
public IActionResult OnGetRedirectToRedirect() => RedirectToPage("Redirect", new { id = 10 });
|
||||
|
||||
public IActionResult OnGetRedirectToSubDir() => RedirectToPage("SubDir/SubDirPage");
|
||||
|
||||
public IActionResult OnGetRedirectToParent() => RedirectToPage("../Conventions/AuthFolder/Index");
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
@page
|
||||
Loading…
Reference in New Issue