Issue #1736 - Razor view searches in case-sensitive filesystems.

This commit is contained in:
sornaks 2015-03-21 15:57:10 -07:00 committed by Pranav K
parent 823e9f1516
commit 54c1fed254
6 changed files with 649 additions and 35 deletions

View File

@ -3,9 +3,9 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.AspNet.Mvc.Razor.Internal;
using Microsoft.Framework.Internal;
namespace Microsoft.AspNet.Mvc.Razor
@ -49,7 +49,9 @@ namespace Microsoft.AspNet.Mvc.Razor
{
var keyBuilder = new StringBuilder();
var routeValues = context.ActionContext.RouteData.Values;
var controller = routeValues.GetValueOrDefault<string>(RazorViewEngine.ControllerKey);
var controller = RazorViewEngine.GetNormalizedRouteValue(
context.ActionContext,
RazorViewEngine.ControllerKey);
// format is "{viewName}:{isPartial}:{controllerName}:{areaName}:"
keyBuilder.Append(context.ViewName)
@ -58,7 +60,7 @@ namespace Microsoft.AspNet.Mvc.Razor
.Append(CacheKeySeparator)
.Append(controller);
var area = routeValues.GetValueOrDefault<string>(RazorViewEngine.AreaKey);
var area = RazorViewEngine.GetNormalizedRouteValue(context.ActionContext, RazorViewEngine.AreaKey);
if (!string.IsNullOrEmpty(area))
{
keyBuilder.Append(CacheKeySeparator)

View File

@ -1,26 +0,0 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNet.Mvc;
using Microsoft.Framework.Internal;
namespace System.Collections.Generic
{
internal static class DictionaryExtensions
{
public static T GetValueOrDefault<T>([NotNull] this IDictionary<string, object> dictionary,
[NotNull] string key)
{
object valueAsObject;
if (dictionary.TryGetValue(key, out valueAsObject))
{
if (valueAsObject is T)
{
return (T)valueAsObject;
}
}
return default(T);
}
}
}

View File

@ -5,6 +5,8 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using Microsoft.AspNet.Mvc.Razor.Internal;
using Microsoft.AspNet.Mvc.Razor.OptionDescriptors;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.Framework.Internal;
@ -133,6 +135,62 @@ namespace Microsoft.AspNet.Mvc.Razor
return GetRazorPageResult(context, pageName, isPartial: true);
}
/// <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="ActionDescriptor.RouteValueDefaults"/> for attribute routes and
/// <see cref="ActionDescriptor.RouteConstraints"/> for traditional routes to get route values produces
/// consistently cased results.
/// </remarks>
internal static string GetNormalizedRouteValue(ActionContext context, string key)
{
object routeValue;
if (!context.RouteData.Values.TryGetValue(key, out routeValue))
{
return null;
}
var actionDescriptor = context.ActionDescriptor;
string normalizedValue = null;
if (actionDescriptor.AttributeRouteInfo != null)
{
object match;
if (actionDescriptor.RouteValueDefaults.TryGetValue(key, out match))
{
normalizedValue = match?.ToString();
}
}
else
{
// For traditional routes, lookup the key in RouteConstraints if the key is RequireKey.
var match = actionDescriptor.RouteConstraints.FirstOrDefault(
constraint => string.Equals(constraint.RouteKey, key, StringComparison.OrdinalIgnoreCase));
if (match != null && match.KeyHandling != RouteKeyHandling.CatchAll)
{
if (match.KeyHandling == RouteKeyHandling.DenyKey)
{
return null;
}
normalizedValue = match.RouteValue;
}
}
var stringRouteValue = routeValue?.ToString();
if (string.Equals(normalizedValue, stringRouteValue, StringComparison.OrdinalIgnoreCase))
{
return normalizedValue;
}
return stringRouteValue;
}
private RazorPageResult GetRazorPageResult(ActionContext context,
string pageName,
bool isPartial)
@ -164,8 +222,7 @@ namespace Microsoft.AspNet.Mvc.Razor
bool isPartial)
{
// Initialize the dictionary for the typical case of having controller and action tokens.
var routeValues = context.RouteData.Values;
var areaName = routeValues.GetValueOrDefault<string>(AreaKey);
var areaName = GetNormalizedRouteValue(context, AreaKey);
// Only use the area view location formats if we have an area token.
var viewLocations = !string.IsNullOrEmpty(areaName) ? AreaViewLocationFormats :
@ -204,7 +261,7 @@ namespace Microsoft.AspNet.Mvc.Razor
}
// 3. Use the expanded locations to look up a page.
var controllerName = routeValues.GetValueOrDefault<string>(ControllerKey);
var controllerName = GetNormalizedRouteValue(context, ControllerKey);
var searchedLocations = new List<string>();
foreach (var path in viewLocations)
{

View File

@ -0,0 +1,67 @@
// Copyright (c) Microsoft Open Technologies, Inc. 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.Threading.Tasks;
using Microsoft.AspNet.Builder;
using Microsoft.Framework.DependencyInjection;
using RazorEmbeddedViewsWebSite;
using Xunit;
namespace Microsoft.AspNet.Mvc.FunctionalTests
{
// The EmbeddedFileSystem used by RazorEmbeddedViewsWebSite performs case sensitive lookups for files.
// These tests verify that we correctly normalize route values when constructing view lookup paths.
public class RazorFileSystemCaseSensitivityTest
{
private const string SiteName = nameof(RazorEmbeddedViewsWebSite);
private readonly Action<IApplicationBuilder> _app = new Startup().Configure;
private readonly Action<IServiceCollection> _configureServices = new Startup().ConfigureServices;
[Fact]
public async Task RazorViewEngine_NormalizesActionName_WhenLookingUpViewPaths()
{
// Arrange
var expectedMessage = "Hello test-user, this is /RazorEmbeddedViews_Home";
var server = TestHelper.CreateServer(_app, SiteName, _configureServices);
var client = server.CreateClient();
// Act
var response = await client.GetStringAsync("http://localhost/RazorEmbeddedViews_Home/index?User=test-user");
// Assert
Assert.Equal(expectedMessage, response);
}
[Fact]
public async Task RazorViewEngine_NormalizesControllerRouteValue_WhenLookingUpViewPaths()
{
// Arrange
var expectedMessage = "Hello test-user, this is /razorembeddedviews_home";
var server = TestHelper.CreateServer(_app, SiteName, _configureServices);
var client = server.CreateClient();
// Act
var response = await client.GetStringAsync("http://localhost/razorembeddedviews_home?User=test-user");
// Assert
Assert.Equal(expectedMessage, response);
}
[Fact]
public async Task RazorViewEngine_NormalizesAreaRouteValue_WhenLookupViewPaths()
{
// Arrange
var expectedMessage = "Hello admin-user, this is /restricted/razorembeddedviews_admin/login";
var server = TestHelper.CreateServer(_app, SiteName, _configureServices);
var client = server.CreateClient();
var target = "http://localhost/restricted/razorembeddedviews_admin/login?AdminUser=admin-user";
// Act
var response = await client.GetStringAsync(target);
// Assert
Assert.Equal(expectedMessage, response);
}
}
}

View File

@ -126,6 +126,59 @@ namespace Microsoft.AspNet.Mvc.Razor
expanderContext,
"test3:1:controller3:area3:culture:fr:theme:sleek"
};
yield return new object[]
{
new ViewLocationExpanderContext(
GetActionContextWithActionDescriptor(
new Dictionary<string, object>()
{
{"controller", "MyController" },
},
new Dictionary<string, string>()
{
{"controller", "mycontroller" },
},
isAttributeRouted: true),
"test",
isPartial: false),
"test:0:mycontroller"
};
yield return new object[]
{
new ViewLocationExpanderContext(
GetActionContextWithActionDescriptor(
new Dictionary<string, object>()
{
{"controller", "MyController" },
},
new Dictionary<string, string>()
{
{"controller", "mycontroller" },
},
isAttributeRouted: true),
"test",
isPartial: false),
"test:0:mycontroller"
};
yield return new object[]
{
new ViewLocationExpanderContext(
GetActionContextWithActionDescriptor(
new Dictionary<string, object>()
{
{"controller", "mycontroller" },
},
new Dictionary<string, string>()
{
},
isAttributeRouted: true),
"test",
isPartial: false),
"test:0:mycontroller"
};
}
}
@ -150,7 +203,42 @@ namespace Microsoft.AspNet.Mvc.Razor
routeData.Values["area"] = area;
}
return new ActionContext(new DefaultHttpContext(), routeData, new ActionDescriptor());
var actionDesciptor = new ActionDescriptor();
actionDesciptor.RouteConstraints = new List<RouteDataActionConstraint>();
return new ActionContext(new DefaultHttpContext(), routeData, actionDesciptor);
}
private static ActionContext GetActionContextWithActionDescriptor(
IDictionary<string, object> routeValues,
IDictionary<string, string> routesInActionDescriptor,
bool isAttributeRouted)
{
var httpContext = new DefaultHttpContext();
var routeData = new RouteData();
foreach (var kvp in routeValues)
{
routeData.Values.Add(kvp.Key, kvp.Value);
}
var actionDescriptor = new ActionDescriptor();
if (isAttributeRouted)
{
actionDescriptor.AttributeRouteInfo = new Routing.AttributeRouteInfo();
foreach (var kvp in routesInActionDescriptor)
{
actionDescriptor.RouteValueDefaults.Add(kvp.Key, kvp.Value);
}
}
else
{
actionDescriptor.RouteConstraints = new List<RouteDataActionConstraint>();
foreach (var kvp in routesInActionDescriptor)
{
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(kvp.Key, kvp.Value));
}
}
return new ActionContext(httpContext, routeData, actionDescriptor);
}
}
}

View File

@ -10,6 +10,7 @@ using Microsoft.AspNet.Routing;
using Microsoft.AspNet.Testing;
using Moq;
using Xunit;
using Microsoft.AspNet.Mvc.Routing;
namespace Microsoft.AspNet.Mvc.Razor.Test
{
@ -594,7 +595,151 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
Assert.Equal(expected, result.SearchedLocations);
}
[Fact]
[Theory]
// Looks in RouteValueDefaults
[InlineData(true)]
// Looks in RouteConstraints
[InlineData(false)]
public void FindPage_SelectsActionCaseInsensitively(bool isAttributeRouted)
{
// The ActionDescriptor contains "Foo" and the RouteData contains "foo"
// which matches the case of the constructor thus searching in the appropriate location.
// Arrange
var routeValues = new Dictionary<string, object>
{
{ "controller", "foo" }
};
var page = new Mock<IRazorPage>(MockBehavior.Strict).Object;
var pageFactory = new Mock<IRazorPageFactory>();
pageFactory.Setup(p => p.CreateInstance("/Views/Foo/details.cshtml"))
.Returns(page)
.Verifiable();
var viewEngine = CreateViewEngine(pageFactory.Object);
var routesInActionDescriptor = new Dictionary<string, string>()
{
{ "controller", "Foo" }
};
var context = GetActionContextWithActionDescriptor(routeValues, routesInActionDescriptor, isAttributeRouted);
// Act
var result = viewEngine.FindPage(context, "details");
// Assert
Assert.Equal("details", result.Name);
Assert.Same(page, result.Page);
Assert.Null(result.SearchedLocations);
pageFactory.Verify();
}
[Theory]
// Looks in RouteValueDefaults
[InlineData(true)]
// Looks in RouteConstraints
[InlineData(false)]
public void FindPage_LooksForPages_UsingActionDescriptor_Controller(bool isAttributeRouted)
{
// Arrange
var expected = new[]
{
"/Views/bar/foo.cshtml",
"/Views/Shared/foo.cshtml",
};
var routeValues = new Dictionary<string, object>
{
{ "controller", "Bar" }
};
var routesInActionDescriptor = new Dictionary<string, string>()
{
{ "controller", "bar" }
};
var page = Mock.Of<IRazorPage>();
var viewEngine = CreateViewEngine();
var context = GetActionContextWithActionDescriptor(routeValues, routesInActionDescriptor, isAttributeRouted);
// Act
var result = viewEngine.FindPage(context, "foo");
// Assert
Assert.Equal("foo", result.Name);
Assert.Null(result.Page);
Assert.Equal(expected, result.SearchedLocations);
}
[Theory]
// Looks in RouteValueDefaults
[InlineData(true)]
// Looks in RouteConstraints
[InlineData(false)]
public void FindPage_LooksForPages_UsingActionDescriptor_Areas(bool isAttributeRouted)
{
// Arrange
var expected = new[]
{
"/Areas/world/Views/bar/foo.cshtml",
"/Areas/world/Views/Shared/foo.cshtml",
"/Views/Shared/foo.cshtml"
};
var routeValues = new Dictionary<string, object>
{
{ "controller", "Bar" },
{ "area", "World" }
};
var routesInActionDescriptor = new Dictionary<string, string>()
{
{ "controller", "bar" },
{ "area", "world" }
};
var page = Mock.Of<IRazorPage>();
var viewEngine = CreateViewEngine();
var context = GetActionContextWithActionDescriptor(routeValues, routesInActionDescriptor, isAttributeRouted);
// Act
var result = viewEngine.FindPage(context, "foo");
// Assert
Assert.Equal("foo", result.Name);
Assert.Null(result.Page);
Assert.Equal(expected, result.SearchedLocations);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void FindPage_LooksForPages_UsesRouteValuesAsFallback(bool isAttributeRouted)
{
// Arrange
var expected = new[]
{
"/Views/foo/bar.cshtml",
"/Views/Shared/bar.cshtml",
};
var routeValues = new Dictionary<string, object>()
{
{ "controller", "foo" }
};
var page = Mock.Of<IRazorPage>();
var viewEngine = CreateViewEngine();
var context = GetActionContextWithActionDescriptor(routeValues, new Dictionary<string, string>(), isAttributeRouted);
// Act
var result = viewEngine.FindPage(context, "bar");
// Assert
Assert.Equal("bar", result.Name);
Assert.Null(result.Page);
Assert.Equal(expected, result.SearchedLocations);
}
[Fact]
public void AreaViewLocationFormats_ContainsExpectedLocations()
{
// Arrange
@ -626,6 +771,252 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
Assert.Equal(viewLocations, viewEngine.ViewLocationFormats);
}
[Fact]
public void GetNormalizedRouteValue_ReturnsValueFromRouteConstraints_IfKeyHandlingIsRequired()
{
// Arrange
var key = "some-key";
var actionDescriptor = new ActionDescriptor
{
RouteConstraints = new[]
{
new RouteDataActionConstraint(key, "Route-Value")
}
};
var actionContext = new ActionContext
{
ActionDescriptor = actionDescriptor,
RouteData = new RouteData()
};
actionContext.RouteData.Values[key] = "route-value";
// Act
var result = RazorViewEngine.GetNormalizedRouteValue(actionContext, key);
// Assert
Assert.Equal("Route-Value", result);
}
[Fact]
public void GetNormalizedRouteValue_ReturnsRouteValue_IfValueDoesNotMatchRouteConstraint()
{
// Arrange
var key = "some-key";
var actionDescriptor = new ActionDescriptor
{
RouteConstraints = new[]
{
new RouteDataActionConstraint(key, "different-value")
}
};
var actionContext = new ActionContext
{
ActionDescriptor = actionDescriptor,
RouteData = new RouteData()
};
actionContext.RouteData.Values[key] = "route-value";
// Act
var result = RazorViewEngine.GetNormalizedRouteValue(actionContext, key);
// Assert
Assert.Equal("route-value", result);
}
[Fact]
public void GetNormalizedRouteValue_ReturnsNull_IfRouteConstraintKeyHandlingIsDeny()
{
// Arrange
var key = "some-key";
var actionDescriptor = new ActionDescriptor
{
RouteConstraints = new[]
{
new RouteDataActionConstraint(key, routeValue: string.Empty)
}
};
var actionContext = new ActionContext
{
ActionDescriptor = actionDescriptor,
RouteData = new RouteData()
};
actionContext.RouteData.Values[key] = "route-value";
// Act
var result = RazorViewEngine.GetNormalizedRouteValue(actionContext, key);
// Assert
Assert.Null(result);
}
[Fact]
public void GetNormalizedRouteValue_ReturnsRouteDataValue_IfRouteConstraintKeyHandlingIsCatchAll()
{
// Arrange
var key = "some-key";
var actionDescriptor = new ActionDescriptor
{
RouteConstraints = new[]
{
RouteDataActionConstraint.CreateCatchAll(key)
}
};
var actionContext = new ActionContext
{
ActionDescriptor = actionDescriptor,
RouteData = new RouteData()
};
actionContext.RouteData.Values[key] = "route-value";
// Act
var result = RazorViewEngine.GetNormalizedRouteValue(actionContext, key);
// Assert
Assert.Equal("route-value", result);
}
[Fact]
public void GetNormalizedRouteValue_UsesRouteValueDefaults_IfAttributeRouted()
{
// Arrange
var key = "some-key";
var actionDescriptor = new ActionDescriptor
{
AttributeRouteInfo = new AttributeRouteInfo(),
};
actionDescriptor.RouteValueDefaults[key] = "Route-Value";
var actionContext = new ActionContext
{
ActionDescriptor = actionDescriptor,
RouteData = new RouteData()
};
actionContext.RouteData.Values[key] = "route-value";
// Act
var result = RazorViewEngine.GetNormalizedRouteValue(actionContext, key);
// Assert
Assert.Equal("Route-Value", result);
}
[Fact]
public void GetNormalizedRouteValue_UsesRouteValue_IfRouteValueDefaultsDoesNotMatchRouteValue()
{
// Arrange
var key = "some-key";
var actionDescriptor = new ActionDescriptor
{
AttributeRouteInfo = new AttributeRouteInfo(),
};
actionDescriptor.RouteValueDefaults[key] = "different-value";
var actionContext = new ActionContext
{
ActionDescriptor = actionDescriptor,
RouteData = new RouteData()
};
actionContext.RouteData.Values[key] = "route-value";
// Act
var result = RazorViewEngine.GetNormalizedRouteValue(actionContext, key);
// Assert
Assert.Equal("route-value", result);
}
[Fact]
public void GetNormalizedRouteValue_ConvertsRouteDefaultToStringValue_IfAttributeRouted()
{
using (new CultureReplacer())
{
// Arrange
var key = "some-key";
var actionDescriptor = new ActionDescriptor
{
AttributeRouteInfo = new AttributeRouteInfo(),
};
actionDescriptor.RouteValueDefaults[key] = 32;
var actionContext = new ActionContext
{
ActionDescriptor = actionDescriptor,
RouteData = new RouteData()
};
actionContext.RouteData.Values[key] = 32;
// Act
var result = RazorViewEngine.GetNormalizedRouteValue(actionContext, key);
// Assert
Assert.Equal("32", result);
}
}
[Fact]
public void GetNormalizedRouteValue_UsesRouteDataValue_IfKeyDoesNotExistInRouteDefaultValues()
{
// Arrange
var key = "some-key";
var actionDescriptor = new ActionDescriptor
{
AttributeRouteInfo = new AttributeRouteInfo(),
};
var actionContext = new ActionContext
{
ActionDescriptor = actionDescriptor,
RouteData = new RouteData()
};
actionContext.RouteData.Values[key] = "route-value";
// Act
var result = RazorViewEngine.GetNormalizedRouteValue(actionContext, key);
// Assert
Assert.Equal("route-value", result);
}
[Fact]
public void GetNormalizedRouteValue_ConvertsRouteValueToString()
{
using (new CultureReplacer())
{
// Arrange
var key = "some-key";
var actionDescriptor = new ActionDescriptor
{
AttributeRouteInfo = new AttributeRouteInfo(),
};
var actionContext = new ActionContext
{
ActionDescriptor = actionDescriptor,
RouteData = new RouteData()
};
actionContext.RouteData.Values[key] = 43;
// Act
var result = RazorViewEngine.GetNormalizedRouteValue(actionContext, key);
// Assert
Assert.Equal("43", result);
}
}
private RazorViewEngine CreateViewEngine(IRazorPageFactory pageFactory = null,
IRazorViewFactory viewFactory = null,
IEnumerable<IViewLocationExpander> expanders = null,
@ -673,7 +1064,42 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
routeData.Values.Add(kvp.Key, kvp.Value);
}
return new ActionContext(httpContext, routeData, new ActionDescriptor());
var actionDesciptor = new ActionDescriptor();
actionDesciptor.RouteConstraints = new List<RouteDataActionConstraint>();
return new ActionContext(httpContext, routeData, actionDesciptor);
}
private static ActionContext GetActionContextWithActionDescriptor(
IDictionary<string, object> routeValues,
IDictionary<string, string> routesInActionDescriptor,
bool isAttributeRouted)
{
var httpContext = new DefaultHttpContext();
var routeData = new RouteData();
foreach (var kvp in routeValues)
{
routeData.Values.Add(kvp.Key, kvp.Value);
}
var actionDescriptor = new ActionDescriptor();
if (isAttributeRouted)
{
actionDescriptor.AttributeRouteInfo = new AttributeRouteInfo();
foreach (var kvp in routesInActionDescriptor)
{
actionDescriptor.RouteValueDefaults.Add(kvp.Key, kvp.Value);
}
}
else
{
actionDescriptor.RouteConstraints = new List<RouteDataActionConstraint>();
foreach (var kvp in routesInActionDescriptor)
{
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(kvp.Key, kvp.Value));
}
}
return new ActionContext(httpContext, routeData, actionDescriptor);
}
private class OverloadedLocationViewEngine : RazorViewEngine