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)
|
internal static string FormatNoRoutesMatchedForPage(object p0)
|
||||||
=> string.Format(CultureInfo.CurrentCulture, GetString("NoRoutesMatchedForPage"), 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)
|
private static string GetString(string name, params string[] formatterNames)
|
||||||
{
|
{
|
||||||
var value = _resourceManager.GetString(name);
|
var value = _resourceManager.GetString(name);
|
||||||
|
|
|
||||||
|
|
@ -403,4 +403,7 @@
|
||||||
<data name="NoRoutesMatchedForPage" xml:space="preserve">
|
<data name="NoRoutesMatchedForPage" xml:space="preserve">
|
||||||
<value>No page named '{0}' matches the supplied values.</value>
|
<value>No page named '{0}' matches the supplied values.</value>
|
||||||
</data>
|
</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>
|
</root>
|
||||||
|
|
@ -2,6 +2,9 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Core;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Core.Internal;
|
||||||
using Microsoft.AspNetCore.Mvc.Routing;
|
using Microsoft.AspNetCore.Mvc.Routing;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
|
||||||
|
|
@ -413,7 +416,7 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
|
|
||||||
var routeValues = new RouteValueDictionary(values);
|
var routeValues = new RouteValueDictionary(values);
|
||||||
var ambientValues = urlHelper.ActionContext.RouteData.Values;
|
var ambientValues = urlHelper.ActionContext.RouteData.Values;
|
||||||
if (pageName == null)
|
if (string.IsNullOrEmpty(pageName))
|
||||||
{
|
{
|
||||||
if (!routeValues.ContainsKey("page") &&
|
if (!routeValues.ContainsKey("page") &&
|
||||||
ambientValues.TryGetValue("page", out var value))
|
ambientValues.TryGetValue("page", out var value))
|
||||||
|
|
@ -423,7 +426,7 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
routeValues["page"] = pageName;
|
routeValues["page"] = CalculatePageName(urlHelper.ActionContext, pageName);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!routeValues.ContainsKey("formaction") &&
|
if (!routeValues.ContainsKey("formaction") &&
|
||||||
|
|
@ -440,5 +443,24 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
host: host,
|
host: host,
|
||||||
fragment: fragment);
|
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.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Core.Internal;
|
||||||
using Microsoft.AspNetCore.Mvc.Razor.Internal;
|
using Microsoft.AspNetCore.Mvc.Razor.Internal;
|
||||||
using Microsoft.AspNetCore.Mvc.ViewEngines;
|
using Microsoft.AspNetCore.Mvc.ViewEngines;
|
||||||
using Microsoft.AspNetCore.Razor.Language;
|
using Microsoft.AspNetCore.Razor.Language;
|
||||||
|
|
@ -36,9 +37,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
||||||
private const string ControllerKey = "controller";
|
private const string ControllerKey = "controller";
|
||||||
private const string PageKey = "page";
|
private const string PageKey = "page";
|
||||||
|
|
||||||
private const string ParentDirectoryToken = "..";
|
|
||||||
private static readonly TimeSpan _cacheExpirationDuration = TimeSpan.FromMinutes(20);
|
private static readonly TimeSpan _cacheExpirationDuration = TimeSpan.FromMinutes(20);
|
||||||
private static readonly char[] _pathSeparators = new[] { '/', '\\' };
|
|
||||||
|
|
||||||
private readonly IRazorPageFactoryProvider _pageFactory;
|
private readonly IRazorPageFactoryProvider _pageFactory;
|
||||||
private readonly IRazorPageActivator _pageActivator;
|
private readonly IRazorPageActivator _pageActivator;
|
||||||
|
|
@ -100,41 +99,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
||||||
/// produces consistently cased results.
|
/// produces consistently cased results.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public static string GetNormalizedRouteValue(ActionContext context, string key)
|
public static string GetNormalizedRouteValue(ActionContext context, string key)
|
||||||
{
|
=> NormalizedRouteValue.GetNormalizedRouteValue(context, 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public RazorPageResult FindPage(ActionContext context, string pageName)
|
public RazorPageResult FindPage(ActionContext context, string pageName)
|
||||||
|
|
@ -345,59 +310,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
||||||
// path relative to currently-executing view, if any.
|
// path relative to currently-executing view, if any.
|
||||||
// Not yet executing a view. Start in app root.
|
// Not yet executing a view. Start in app root.
|
||||||
absolutePath = "/" + pagePath;
|
absolutePath = "/" + pagePath;
|
||||||
}
|
return ViewEnginePath.ResolvePath(absolutePath);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!RequiresPathResolution(pagePath))
|
return ViewEnginePath.CombinePath(executingFilePath, 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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// internal for tests
|
// 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.
|
// Though ./ViewName looks like a relative path, framework searches for that view using view locations.
|
||||||
return name.EndsWith(ViewExtension, StringComparison.OrdinalIgnoreCase);
|
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]
|
[Theory]
|
||||||
[InlineData(null, null, null, "/", null, "/")]
|
[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, "Hello", null, "/Hello")]
|
||||||
[InlineData("/", null, null, "", null, "/")]
|
[InlineData("/", null, null, "", null, "/")]
|
||||||
[InlineData("/hello/", null, null, "/world", null, "/hello/world")]
|
[InlineData("/hello/", null, null, "/world", null, "/hello/world")]
|
||||||
|
|
@ -1052,7 +1052,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
.Callback((UrlRouteContext context) => actual = context);
|
.Callback((UrlRouteContext context) => actual = context);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
urlHelper.Object.Page("TestPage");
|
urlHelper.Object.Page("/TestPage");
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
urlHelper.Verify();
|
urlHelper.Verify();
|
||||||
|
|
@ -1062,7 +1062,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
value =>
|
value =>
|
||||||
{
|
{
|
||||||
Assert.Equal("page", value.Key);
|
Assert.Equal("page", value.Key);
|
||||||
Assert.Equal("TestPage", value.Value);
|
Assert.Equal("/TestPage", value.Value);
|
||||||
});
|
});
|
||||||
Assert.Null(actual.Host);
|
Assert.Null(actual.Host);
|
||||||
Assert.Null(actual.Protocol);
|
Assert.Null(actual.Protocol);
|
||||||
|
|
@ -1100,7 +1100,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
.Callback((UrlRouteContext context) => actual = context);
|
.Callback((UrlRouteContext context) => actual = context);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
urlHelper.Object.Page("TestPage", values);
|
urlHelper.Object.Page("/TestPage", values);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
urlHelper.Verify();
|
urlHelper.Verify();
|
||||||
|
|
@ -1115,7 +1115,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
value =>
|
value =>
|
||||||
{
|
{
|
||||||
Assert.Equal("page", value.Key);
|
Assert.Equal("page", value.Key);
|
||||||
Assert.Equal("TestPage", value.Value);
|
Assert.Equal("/TestPage", value.Value);
|
||||||
});
|
});
|
||||||
Assert.Null(actual.Host);
|
Assert.Null(actual.Host);
|
||||||
Assert.Null(actual.Protocol);
|
Assert.Null(actual.Protocol);
|
||||||
|
|
@ -1132,7 +1132,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
.Callback((UrlRouteContext context) => actual = context);
|
.Callback((UrlRouteContext context) => actual = context);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
urlHelper.Object.Page("TestPage", new { id = 13 }, "https");
|
urlHelper.Object.Page("/TestPage", new { id = 13 }, "https");
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
urlHelper.Verify();
|
urlHelper.Verify();
|
||||||
|
|
@ -1147,7 +1147,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
value =>
|
value =>
|
||||||
{
|
{
|
||||||
Assert.Equal("page", value.Key);
|
Assert.Equal("page", value.Key);
|
||||||
Assert.Equal("TestPage", value.Value);
|
Assert.Equal("/TestPage", value.Value);
|
||||||
});
|
});
|
||||||
Assert.Equal("https", actual.Protocol);
|
Assert.Equal("https", actual.Protocol);
|
||||||
Assert.Null(actual.Host);
|
Assert.Null(actual.Host);
|
||||||
|
|
@ -1164,7 +1164,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
.Callback((UrlRouteContext context) => actual = context);
|
.Callback((UrlRouteContext context) => actual = context);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
urlHelper.Object.Page("TestPage", new { id = 13 }, "https", "mytesthost");
|
urlHelper.Object.Page("/TestPage", new { id = 13 }, "https", "mytesthost");
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
urlHelper.Verify();
|
urlHelper.Verify();
|
||||||
|
|
@ -1179,7 +1179,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
value =>
|
value =>
|
||||||
{
|
{
|
||||||
Assert.Equal("page", value.Key);
|
Assert.Equal("page", value.Key);
|
||||||
Assert.Equal("TestPage", value.Value);
|
Assert.Equal("/TestPage", value.Value);
|
||||||
});
|
});
|
||||||
Assert.Equal("https", actual.Protocol);
|
Assert.Equal("https", actual.Protocol);
|
||||||
Assert.Equal("mytesthost", actual.Host);
|
Assert.Equal("mytesthost", actual.Host);
|
||||||
|
|
@ -1196,7 +1196,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
.Callback((UrlRouteContext context) => actual = context);
|
.Callback((UrlRouteContext context) => actual = context);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
urlHelper.Object.Page("TestPage", new { id = 13 }, "https", "mytesthost", "#toc");
|
urlHelper.Object.Page("/TestPage", new { id = 13 }, "https", "mytesthost", "#toc");
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
urlHelper.Verify();
|
urlHelper.Verify();
|
||||||
|
|
@ -1211,7 +1211,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
value =>
|
value =>
|
||||||
{
|
{
|
||||||
Assert.Equal("page", value.Key);
|
Assert.Equal("page", value.Key);
|
||||||
Assert.Equal("TestPage", value.Value);
|
Assert.Equal("/TestPage", value.Value);
|
||||||
});
|
});
|
||||||
Assert.Equal("https", actual.Protocol);
|
Assert.Equal("https", actual.Protocol);
|
||||||
Assert.Equal("mytesthost", actual.Host);
|
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)
|
private static Mock<IUrlHelper> CreateMockUrlHelper(ActionContext context = null)
|
||||||
{
|
{
|
||||||
if (context == null)
|
if (context == null)
|
||||||
{
|
{
|
||||||
context = new ActionContext
|
context = GetActionContextForPage("/Page");
|
||||||
{
|
|
||||||
RouteData = new RouteData(),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var urlHelper = new Mock<IUrlHelper>();
|
var urlHelper = new Mock<IUrlHelper>();
|
||||||
|
|
@ -1513,6 +1633,27 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
return routeBuilder.Build();
|
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
|
private class PassThroughRouter : IRouter
|
||||||
{
|
{
|
||||||
public VirtualPathData GetVirtualPath(VirtualPathContext context)
|
public VirtualPathData GetVirtualPath(VirtualPathContext context)
|
||||||
|
|
|
||||||
|
|
@ -855,6 +855,54 @@ Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary`1[AspNetCore._InjectedP
|
||||||
Assert.Equal(expected, response.Headers.Location.ToString());
|
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)
|
private async Task AddAntiforgeryHeaders(HttpRequestMessage request)
|
||||||
{
|
{
|
||||||
var getResponse = await Client.GetAsync(request.RequestUri);
|
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