Ensure RazorPages in an area are not route-able through root based paths when root directory and area root directory overlap
Fixes #7147
This commit is contained in:
parent
64259fe51c
commit
7d64990a69
|
|
@ -73,14 +73,16 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
foreach (var viewDescriptor in GetViewDescriptors(_applicationManager))
|
foreach (var viewDescriptor in GetViewDescriptors(_applicationManager))
|
||||||
{
|
{
|
||||||
PageRouteModel model = null;
|
PageRouteModel model = null;
|
||||||
if (viewDescriptor.RelativePath.StartsWith(rootDirectory, StringComparison.OrdinalIgnoreCase))
|
// When RootDirectory and AreaRootDirectory overlap (e.g. RootDirectory = '/', AreaRootDirectory = '/Areas'), we
|
||||||
{
|
// only want to allow a page to be associated with the area route.
|
||||||
model = GetPageRouteModel(rootDirectory, viewDescriptor);
|
if (_pagesOptions.AllowAreas && viewDescriptor.RelativePath.StartsWith(areaRootDirectory, StringComparison.OrdinalIgnoreCase))
|
||||||
}
|
|
||||||
else if (_pagesOptions.AllowAreas && viewDescriptor.RelativePath.StartsWith(areaRootDirectory, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
model = GetAreaPageRouteModel(areaRootDirectory, viewDescriptor);
|
model = GetAreaPageRouteModel(areaRootDirectory, viewDescriptor);
|
||||||
}
|
}
|
||||||
|
else if (viewDescriptor.RelativePath.StartsWith(rootDirectory, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
model = GetPageRouteModel(rootDirectory, viewDescriptor);
|
||||||
|
}
|
||||||
|
|
||||||
if (model != null)
|
if (model != null)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
result = default;
|
result = default;
|
||||||
Debug.Assert(path.StartsWith("/", StringComparison.Ordinal));
|
Debug.Assert(path.StartsWith("/", StringComparison.Ordinal));
|
||||||
|
|
||||||
|
// 1. Parse the area name. This will be the first token we encounter.
|
||||||
var areaEndIndex = path.IndexOf('/', startIndex: 1);
|
var areaEndIndex = path.IndexOf('/', startIndex: 1);
|
||||||
if (areaEndIndex == -1 || areaEndIndex == path.Length)
|
if (areaEndIndex == -1 || areaEndIndex == path.Length)
|
||||||
{
|
{
|
||||||
|
|
@ -63,25 +64,37 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize the pages root directory so that it has a
|
|
||||||
var normalizedPagesRootDirectory = razorPagesOptions.RootDirectory.TrimStart('/');
|
|
||||||
if (!normalizedPagesRootDirectory.EndsWith("/", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
normalizedPagesRootDirectory += "/";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Compare(path, areaEndIndex + 1, normalizedPagesRootDirectory, 0, normalizedPagesRootDirectory.Length, StringComparison.OrdinalIgnoreCase) != 0)
|
|
||||||
{
|
|
||||||
logger.UnsupportedAreaPath(razorPagesOptions, path);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var areaName = path.Substring(1, areaEndIndex - 1);
|
var areaName = path.Substring(1, areaEndIndex - 1);
|
||||||
|
|
||||||
var pagePathIndex = areaEndIndex + normalizedPagesRootDirectory.Length;
|
string pageName;
|
||||||
Debug.Assert(path.EndsWith(RazorViewEngine.ViewExtension), $"{path} does not end in extension '{RazorViewEngine.ViewExtension}'.");
|
if (razorPagesOptions.RootDirectory == "/")
|
||||||
|
{
|
||||||
|
// When RootDirectory is "/", every thing past the area name is the page path.
|
||||||
|
Debug.Assert(path.EndsWith(RazorViewEngine.ViewExtension), $"{path} does not end in extension '{RazorViewEngine.ViewExtension}'.");
|
||||||
|
pageName = path.Substring(areaEndIndex, path.Length - areaEndIndex - RazorViewEngine.ViewExtension.Length);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Normalize the pages root directory so that it has a trailing slash. This ensures we're looking at a directory delimiter
|
||||||
|
// and not just the area name occuring as part of a segment.
|
||||||
|
Debug.Assert(razorPagesOptions.RootDirectory.StartsWith("/", StringComparison.Ordinal));
|
||||||
|
var normalizedPagesRootDirectory = razorPagesOptions.RootDirectory.Substring(1);
|
||||||
|
if (!normalizedPagesRootDirectory.EndsWith("/", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
normalizedPagesRootDirectory += "/";
|
||||||
|
}
|
||||||
|
|
||||||
var pageName = path.Substring(pagePathIndex, path.Length - pagePathIndex - RazorViewEngine.ViewExtension.Length);
|
Debug.Assert(normalizedPagesRootDirectory.Length > 0);
|
||||||
|
// If the pages root has a value i.e. it's not the app root "/", ensure that the area path contains this value.
|
||||||
|
if (string.Compare(path, areaEndIndex + 1, normalizedPagesRootDirectory, 0, normalizedPagesRootDirectory.Length, StringComparison.OrdinalIgnoreCase) != 0)
|
||||||
|
{
|
||||||
|
logger.UnsupportedAreaPath(razorPagesOptions, path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pageNameIndex = areaEndIndex + normalizedPagesRootDirectory.Length;
|
||||||
|
pageName = path.Substring(pageNameIndex, path.Length - pageNameIndex - RazorViewEngine.ViewExtension.Length);
|
||||||
|
}
|
||||||
|
|
||||||
var builder = new InplaceStringBuilder(areaEndIndex + pageName.Length);
|
var builder = new InplaceStringBuilder(areaEndIndex + pageName.Length);
|
||||||
builder.Append(path, 0, areaEndIndex);
|
builder.Append(path, 0, areaEndIndex);
|
||||||
|
|
|
||||||
|
|
@ -37,16 +37,25 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
|
|
||||||
public void OnProvidersExecuting(PageRouteModelProviderContext context)
|
public void OnProvidersExecuting(PageRouteModelProviderContext context)
|
||||||
{
|
{
|
||||||
AddPageModels(context);
|
// When RootDirectory and AreaRootDirectory overlap, e.g. RootDirectory = /, AreaRootDirectoryy = /Areas;
|
||||||
|
// we need to ensure that the page is only route-able via the area route. By adding area routes first,
|
||||||
|
// we'll ensure non area routes get skipped when it encounters an IsAlreadyRegistered check.
|
||||||
if (_pagesOptions.AllowAreas)
|
if (_pagesOptions.AllowAreas)
|
||||||
{
|
{
|
||||||
AddAreaPageModels(context);
|
AddAreaPageModels(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AddPageModels(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddPageModels(PageRouteModelProviderContext context)
|
private void AddPageModels(PageRouteModelProviderContext context)
|
||||||
{
|
{
|
||||||
|
var normalizedAreaRootDirectory = _pagesOptions.AreaRootDirectory;
|
||||||
|
if (!normalizedAreaRootDirectory.EndsWith("/", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
normalizedAreaRootDirectory += "/";
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var item in _project.EnumerateItems(_pagesOptions.RootDirectory))
|
foreach (var item in _project.EnumerateItems(_pagesOptions.RootDirectory))
|
||||||
{
|
{
|
||||||
if (!IsRouteable(item))
|
if (!IsRouteable(item))
|
||||||
|
|
@ -54,23 +63,29 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var relativePath = item.CombinedPath;
|
||||||
|
if (IsAlreadyRegistered(context, relativePath))
|
||||||
|
{
|
||||||
|
// A route for this file was already registered either by the CompiledPageRouteModel or as an area route.
|
||||||
|
// by this provider. Skip registering an additional entry.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (!PageDirectiveFeature.TryGetPageDirective(_logger, item, out var routeTemplate))
|
if (!PageDirectiveFeature.TryGetPageDirective(_logger, item, out var routeTemplate))
|
||||||
{
|
{
|
||||||
// .cshtml pages without @page are not RazorPages.
|
// .cshtml pages without @page are not RazorPages.
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var routeModel = new PageRouteModel(
|
if (_pagesOptions.AllowAreas && relativePath.StartsWith(normalizedAreaRootDirectory, StringComparison.OrdinalIgnoreCase))
|
||||||
relativePath: item.CombinedPath,
|
|
||||||
viewEnginePath: item.FilePathWithoutExtension);
|
|
||||||
|
|
||||||
if (IsAlreadyRegistered(context, routeModel))
|
|
||||||
{
|
{
|
||||||
// The CompiledPageRouteModelProvider (or another provider) already registered a PageRoute for this path.
|
// Ignore Razor pages that are under the area root directory when AllowAreas is enabled.
|
||||||
// Don't register a duplicate entry for this route.
|
// Conforming page paths will be added by AddAreaPageModels.
|
||||||
|
_logger.UnsupportedAreaPath(_pagesOptions, relativePath);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var routeModel = new PageRouteModel(relativePath, viewEnginePath: item.FilePathWithoutExtension);
|
||||||
PageSelectorModel.PopulateDefaults(routeModel, routeModel.ViewEnginePath, routeTemplate);
|
PageSelectorModel.PopulateDefaults(routeModel, routeModel.ViewEnginePath, routeTemplate);
|
||||||
context.RouteModels.Add(routeModel);
|
context.RouteModels.Add(routeModel);
|
||||||
}
|
}
|
||||||
|
|
@ -85,6 +100,14 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var relativePath = item.CombinedPath;
|
||||||
|
if (IsAlreadyRegistered(context, relativePath))
|
||||||
|
{
|
||||||
|
// A route for this file was already registered either by the CompiledPageRouteModel.
|
||||||
|
// Skip registering an additional entry.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (!PageDirectiveFeature.TryGetPageDirective(_logger, item, out var routeTemplate))
|
if (!PageDirectiveFeature.TryGetPageDirective(_logger, item, out var routeTemplate))
|
||||||
{
|
{
|
||||||
// .cshtml pages without @page are not RazorPages.
|
// .cshtml pages without @page are not RazorPages.
|
||||||
|
|
@ -96,9 +119,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var routeModel = new PageRouteModel(
|
var routeModel = new PageRouteModel(relativePath, viewEnginePath: areaResult.viewEnginePath)
|
||||||
relativePath: item.CombinedPath,
|
|
||||||
viewEnginePath: areaResult.viewEnginePath)
|
|
||||||
{
|
{
|
||||||
RouteValues =
|
RouteValues =
|
||||||
{
|
{
|
||||||
|
|
@ -106,25 +127,17 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (IsAlreadyRegistered(context, routeModel))
|
|
||||||
{
|
|
||||||
// The CompiledPageRouteModelProvider (or another provider) already registered a PageRoute for this path.
|
|
||||||
// Don't register a duplicate entry for this route.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
PageSelectorModel.PopulateDefaults(routeModel, areaResult.pageRoute, routeTemplate);
|
PageSelectorModel.PopulateDefaults(routeModel, areaResult.pageRoute, routeTemplate);
|
||||||
context.RouteModels.Add(routeModel);
|
context.RouteModels.Add(routeModel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsAlreadyRegistered(PageRouteModelProviderContext context, PageRouteModel routeModel)
|
private bool IsAlreadyRegistered(PageRouteModelProviderContext context, string relativePath)
|
||||||
{
|
{
|
||||||
for (var i = 0; i < context.RouteModels.Count; i++)
|
for (var i = 0; i < context.RouteModels.Count; i++)
|
||||||
{
|
{
|
||||||
var existingRouteModel = context.RouteModels[i];
|
var existingRouteModel = context.RouteModels[i];
|
||||||
if (string.Equals(existingRouteModel.ViewEnginePath, routeModel.ViewEnginePath, StringComparison.OrdinalIgnoreCase) &&
|
if (string.Equals(relativePath, existingRouteModel.RelativePath, StringComparison.OrdinalIgnoreCase))
|
||||||
string.Equals(existingRouteModel.RelativePath, existingRouteModel.RelativePath, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -175,6 +175,63 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OnProvidersExecuting_DoesNotAddAreaAndNonAreaRoutesForAPage()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var descriptors = new[]
|
||||||
|
{
|
||||||
|
GetDescriptor("/Areas/Accounts/Manage/Home.cshtml"),
|
||||||
|
GetDescriptor("/Areas/About.cshtml"),
|
||||||
|
GetDescriptor("/Contact.cshtml"),
|
||||||
|
};
|
||||||
|
var options = new RazorPagesOptions
|
||||||
|
{
|
||||||
|
AllowAreas = true,
|
||||||
|
AreaRootDirectory = "/Areas",
|
||||||
|
RootDirectory = "/",
|
||||||
|
};
|
||||||
|
var provider = new TestCompiledPageRouteModelProvider(descriptors, options);
|
||||||
|
var context = new PageRouteModelProviderContext();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
provider.OnProvidersExecuting(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Collection(context.RouteModels,
|
||||||
|
result =>
|
||||||
|
{
|
||||||
|
Assert.Equal("/Areas/Accounts/Manage/Home.cshtml", result.RelativePath);
|
||||||
|
Assert.Equal("/Manage/Home", result.ViewEnginePath);
|
||||||
|
Assert.Collection(result.Selectors,
|
||||||
|
selector => Assert.Equal("Accounts/Manage/Home", selector.AttributeRouteModel.Template));
|
||||||
|
Assert.Collection(result.RouteValues.OrderBy(k => k.Key),
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("area", kvp.Key);
|
||||||
|
Assert.Equal("Accounts", kvp.Value);
|
||||||
|
},
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("page", kvp.Key);
|
||||||
|
Assert.Equal("/Manage/Home", kvp.Value);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
result =>
|
||||||
|
{
|
||||||
|
Assert.Equal("/Contact.cshtml", result.RelativePath);
|
||||||
|
Assert.Equal("/Contact", result.ViewEnginePath);
|
||||||
|
Assert.Collection(result.Selectors,
|
||||||
|
selector => Assert.Equal("Contact", selector.AttributeRouteModel.Template));
|
||||||
|
Assert.Collection(result.RouteValues.OrderBy(k => k.Key),
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("page", kvp.Key);
|
||||||
|
Assert.Equal("/Contact", kvp.Value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void OnProvidersExecuting_AddsMultipleSelectorsForIndexPage_WithIndexAtRoot()
|
public void OnProvidersExecuting_AddsMultipleSelectorsForIndexPage_WithIndexAtRoot()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -171,6 +171,69 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OnProvidersExecuting_DoesNotAddAreaAndNonAreaRoutesForAPage()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var fileProvider = new TestFileProvider();
|
||||||
|
var conformingFileUnderAreasDirectory = fileProvider.AddFile("Categories.cshtml", "@page");
|
||||||
|
// We shouldn't add a route for this.
|
||||||
|
var nonConformingFileUnderAreasDirectory = fileProvider.AddFile("Home.cshtml", "@page");
|
||||||
|
var rootFile = fileProvider.AddFile("About.cshtml", "@page");
|
||||||
|
|
||||||
|
var productsDir = fileProvider.AddDirectoryContent("/Areas/Products", new[] { conformingFileUnderAreasDirectory });
|
||||||
|
var areasDir = fileProvider.AddDirectoryContent("/Areas", new IFileInfo[] { productsDir, nonConformingFileUnderAreasDirectory });
|
||||||
|
var rootDir = fileProvider.AddDirectoryContent("/", new IFileInfo[] { areasDir, rootFile });
|
||||||
|
|
||||||
|
var project = new TestRazorProject(fileProvider);
|
||||||
|
|
||||||
|
var optionsManager = Options.Create(new RazorPagesOptions
|
||||||
|
{
|
||||||
|
RootDirectory = "/",
|
||||||
|
AreaRootDirectory = "/Areas",
|
||||||
|
AllowAreas = true,
|
||||||
|
});
|
||||||
|
var provider = new RazorProjectPageRouteModelProvider(project, optionsManager, NullLoggerFactory.Instance);
|
||||||
|
var context = new PageRouteModelProviderContext();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
provider.OnProvidersExecuting(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Collection(context.RouteModels,
|
||||||
|
model =>
|
||||||
|
{
|
||||||
|
Assert.Equal("/Areas/Products/Categories.cshtml", model.RelativePath);
|
||||||
|
Assert.Equal("/Categories", model.ViewEnginePath);
|
||||||
|
Assert.Collection(model.Selectors,
|
||||||
|
selector => Assert.Equal("Products/Categories", selector.AttributeRouteModel.Template));
|
||||||
|
Assert.Collection(model.RouteValues.OrderBy(k => k.Key),
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("area", kvp.Key);
|
||||||
|
Assert.Equal("Products", kvp.Value);
|
||||||
|
},
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("page", kvp.Key);
|
||||||
|
Assert.Equal("/Categories", kvp.Value);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
model =>
|
||||||
|
{
|
||||||
|
Assert.Equal("/About.cshtml", model.RelativePath);
|
||||||
|
Assert.Equal("/About", model.ViewEnginePath);
|
||||||
|
Assert.Collection(model.Selectors,
|
||||||
|
selector => Assert.Equal("About", selector.AttributeRouteModel.Template));
|
||||||
|
Assert.Collection(model.RouteValues.OrderBy(k => k.Key),
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("page", kvp.Key);
|
||||||
|
Assert.Equal("/About", kvp.Value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void OnProvidersExecuting_AddsMultipleSelectorsForIndexPages()
|
public void OnProvidersExecuting_AddsMultipleSelectorsForIndexPages()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue