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))
|
||||
{
|
||||
PageRouteModel model = null;
|
||||
if (viewDescriptor.RelativePath.StartsWith(rootDirectory, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
model = GetPageRouteModel(rootDirectory, viewDescriptor);
|
||||
}
|
||||
else if (_pagesOptions.AllowAreas && viewDescriptor.RelativePath.StartsWith(areaRootDirectory, 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.
|
||||
if (_pagesOptions.AllowAreas && viewDescriptor.RelativePath.StartsWith(areaRootDirectory, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
model = GetAreaPageRouteModel(areaRootDirectory, viewDescriptor);
|
||||
}
|
||||
else if (viewDescriptor.RelativePath.StartsWith(rootDirectory, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
model = GetPageRouteModel(rootDirectory, viewDescriptor);
|
||||
}
|
||||
|
||||
if (model != null)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
|||
result = default;
|
||||
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);
|
||||
if (areaEndIndex == -1 || areaEndIndex == path.Length)
|
||||
{
|
||||
|
|
@ -63,25 +64,37 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
|||
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 pagePathIndex = areaEndIndex + normalizedPagesRootDirectory.Length;
|
||||
Debug.Assert(path.EndsWith(RazorViewEngine.ViewExtension), $"{path} does not end in extension '{RazorViewEngine.ViewExtension}'.");
|
||||
string pageName;
|
||||
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);
|
||||
builder.Append(path, 0, areaEndIndex);
|
||||
|
|
|
|||
|
|
@ -37,16 +37,25 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
|||
|
||||
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)
|
||||
{
|
||||
AddAreaPageModels(context);
|
||||
}
|
||||
|
||||
AddPageModels(context);
|
||||
}
|
||||
|
||||
private void AddPageModels(PageRouteModelProviderContext context)
|
||||
{
|
||||
var normalizedAreaRootDirectory = _pagesOptions.AreaRootDirectory;
|
||||
if (!normalizedAreaRootDirectory.EndsWith("/", StringComparison.Ordinal))
|
||||
{
|
||||
normalizedAreaRootDirectory += "/";
|
||||
}
|
||||
|
||||
foreach (var item in _project.EnumerateItems(_pagesOptions.RootDirectory))
|
||||
{
|
||||
if (!IsRouteable(item))
|
||||
|
|
@ -54,23 +63,29 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
|||
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))
|
||||
{
|
||||
// .cshtml pages without @page are not RazorPages.
|
||||
continue;
|
||||
}
|
||||
|
||||
var routeModel = new PageRouteModel(
|
||||
relativePath: item.CombinedPath,
|
||||
viewEnginePath: item.FilePathWithoutExtension);
|
||||
|
||||
if (IsAlreadyRegistered(context, routeModel))
|
||||
if (_pagesOptions.AllowAreas && relativePath.StartsWith(normalizedAreaRootDirectory, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// The CompiledPageRouteModelProvider (or another provider) already registered a PageRoute for this path.
|
||||
// Don't register a duplicate entry for this route.
|
||||
// Ignore Razor pages that are under the area root directory when AllowAreas is enabled.
|
||||
// Conforming page paths will be added by AddAreaPageModels.
|
||||
_logger.UnsupportedAreaPath(_pagesOptions, relativePath);
|
||||
continue;
|
||||
}
|
||||
|
||||
var routeModel = new PageRouteModel(relativePath, viewEnginePath: item.FilePathWithoutExtension);
|
||||
PageSelectorModel.PopulateDefaults(routeModel, routeModel.ViewEnginePath, routeTemplate);
|
||||
context.RouteModels.Add(routeModel);
|
||||
}
|
||||
|
|
@ -85,6 +100,14 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
|||
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))
|
||||
{
|
||||
// .cshtml pages without @page are not RazorPages.
|
||||
|
|
@ -96,9 +119,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
|||
continue;
|
||||
}
|
||||
|
||||
var routeModel = new PageRouteModel(
|
||||
relativePath: item.CombinedPath,
|
||||
viewEnginePath: areaResult.viewEnginePath)
|
||||
var routeModel = new PageRouteModel(relativePath, viewEnginePath: areaResult.viewEnginePath)
|
||||
{
|
||||
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);
|
||||
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++)
|
||||
{
|
||||
var existingRouteModel = context.RouteModels[i];
|
||||
if (string.Equals(existingRouteModel.ViewEnginePath, routeModel.ViewEnginePath, StringComparison.OrdinalIgnoreCase) &&
|
||||
string.Equals(existingRouteModel.RelativePath, existingRouteModel.RelativePath, StringComparison.OrdinalIgnoreCase))
|
||||
if (string.Equals(relativePath, existingRouteModel.RelativePath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
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]
|
||||
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]
|
||||
public void OnProvidersExecuting_AddsMultipleSelectorsForIndexPages()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue