RazorPages page directives missing quotes should alert user

Fixes #5868
This commit is contained in:
Pranav K 2017-06-22 11:56:16 -07:00
parent 9d138affa2
commit 0dfffd45c2
5 changed files with 121 additions and 133 deletions

View File

@ -2,9 +2,11 @@
// 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.IO;
using Microsoft.AspNetCore.Mvc.Razor.Extensions; using Microsoft.AspNetCore.Mvc.Razor.Extensions;
using Microsoft.AspNetCore.Mvc.RazorPages.Internal;
using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
{ {
@ -26,7 +28,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
builder.Features.Add(new PageDirectiveParserOptionsFeature()); builder.Features.Add(new PageDirectiveParserOptionsFeature());
}); });
public static bool TryGetPageDirective(RazorProjectItem projectItem, out string template) public static bool TryGetPageDirective(ILogger logger, RazorProjectItem projectItem, out string template)
{ {
if (projectItem == null) if (projectItem == null)
{ {
@ -34,35 +36,34 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
} }
var sourceDocument = RazorSourceDocument.ReadFrom(projectItem); var sourceDocument = RazorSourceDocument.ReadFrom(projectItem);
return TryGetPageDirective(sourceDocument, out template); return TryGetPageDirective(logger, sourceDocument, out template);
} }
public static bool TryGetPageDirective(Func<Stream> streamFactory, out string template) static bool TryGetPageDirective(
{ ILogger logger,
if (streamFactory == null) RazorSourceDocument sourceDocument,
{ out string template)
throw new ArgumentNullException(nameof(streamFactory));
}
using (var stream = streamFactory())
{
var sourceDocument = RazorSourceDocument.ReadFrom(stream, fileName: "Parse.cshtml");
return TryGetPageDirective(sourceDocument, out template);
}
}
private static bool TryGetPageDirective(RazorSourceDocument sourceDocument, out string template)
{ {
var codeDocument = RazorCodeDocument.Create(sourceDocument); var codeDocument = RazorCodeDocument.Create(sourceDocument);
PageDirectiveEngine.Process(codeDocument); PageDirectiveEngine.Process(codeDocument);
if (PageDirective.TryGetPageDirective(codeDocument.GetDocumentIntermediateNode(), out var pageDirective)) var documentIRNode = codeDocument.GetDocumentIntermediateNode();
if (PageDirective.TryGetPageDirective(documentIRNode, out var pageDirective))
{ {
template = pageDirective.RouteTemplate; template = pageDirective.RouteTemplate;
return true; return true;
} }
template = null; template = null;
var visitor = new Visitor();
visitor.Visit(documentIRNode);
if (visitor.MalformedPageDirective != null)
{
logger.MalformedPageDirective(sourceDocument.FilePath, visitor.MalformedPageDirective.Diagnostics);
return true;
}
return false; return false;
} }
@ -75,5 +76,18 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
options.ParseOnlyLeadingDirectives = true; options.ParseOnlyLeadingDirectives = true;
} }
} }
private class Visitor : IntermediateNodeWalker
{
public MalformedDirectiveIntermediateNode MalformedPageDirective { get; private set; }
public override void VisitMalformedDirective(MalformedDirectiveIntermediateNode node)
{
if (node.Descriptor == PageDirective.Directive)
{
MalformedPageDirective = node;
}
}
}
} }
} }

View File

@ -2,9 +2,11 @@
// 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.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
@ -14,6 +16,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
private static readonly Action<ILogger, string, string[], ModelValidationState, Exception> _handlerMethodExecuting; private static readonly Action<ILogger, string, string[], ModelValidationState, Exception> _handlerMethodExecuting;
private static readonly Action<ILogger, string, string, Exception> _handlerMethodExecuted; private static readonly Action<ILogger, string, string, Exception> _handlerMethodExecuted;
private static readonly Action<ILogger, object, Exception> _pageFilterShortCircuit; private static readonly Action<ILogger, object, Exception> _pageFilterShortCircuit;
private static readonly Action<ILogger, string, string[], Exception> _malformedPageDirective;
static PageLoggerExtensions() static PageLoggerExtensions()
{ {
@ -33,6 +36,11 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
LogLevel.Debug, LogLevel.Debug,
3, 3,
"Request was short circuited at page filter '{PageFilter}'."); "Request was short circuited at page filter '{PageFilter}'.");
_malformedPageDirective = LoggerMessage.Define<string, string[]>(
LogLevel.Warning,
104,
"The page directive at '{FilePath}' is malformed. Please fix the following issues: {Diagnostics}");
} }
public static void ExecutingHandlerMethod(this ILogger logger, PageContext context, HandlerMethodDescriptor handler, object[] arguments) public static void ExecutingHandlerMethod(this ILogger logger, PageContext context, HandlerMethodDescriptor handler, object[] arguments)
@ -76,5 +84,19 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{ {
_pageFilterShortCircuit(logger, filter, null); _pageFilterShortCircuit(logger, filter, null);
} }
public static void MalformedPageDirective(this ILogger logger, string filePath, IList<RazorDiagnostic> diagnostics)
{
if (logger.IsEnabled(LogLevel.Warning))
{
var messages = new string[diagnostics.Count];
for (var i = 0; i < diagnostics.Count; i++)
{
messages[i] = diagnostics[i].GetMessage();
}
_malformedPageDirective(logger, filePath, messages, null);
}
}
} }
} }

View File

@ -4,6 +4,7 @@
using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
@ -12,13 +13,16 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{ {
private readonly RazorProject _project; private readonly RazorProject _project;
private readonly RazorPagesOptions _pagesOptions; private readonly RazorPagesOptions _pagesOptions;
private readonly ILogger _logger;
public RazorProjectPageApplicationModelProvider( public RazorProjectPageApplicationModelProvider(
RazorProject razorProject, RazorProject razorProject,
IOptions<RazorPagesOptions> pagesOptionsAccessor) IOptions<RazorPagesOptions> pagesOptionsAccessor,
ILoggerFactory loggerFactory)
{ {
_project = razorProject; _project = razorProject;
_pagesOptions = pagesOptionsAccessor.Value; _pagesOptions = pagesOptionsAccessor.Value;
_logger = loggerFactory.CreateLogger<RazorProjectPageApplicationModelProvider>();
} }
public int Order => -1000; public int Order => -1000;
@ -37,7 +41,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
continue; continue;
} }
if (!PageDirectiveFeature.TryGetPageDirective(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;

View File

@ -3,8 +3,12 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq;
using System.Text; using System.Text;
using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Testing;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
@ -15,126 +19,110 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
public void TryGetPageDirective_FindsTemplate() public void TryGetPageDirective_FindsTemplate()
{ {
// Arrange // Arrange
string template;
var projectItem = new TestRazorProjectItem(@"@page ""Some/Path/{value}"" var projectItem = new TestRazorProjectItem(@"@page ""Some/Path/{value}""
The rest of the thing"); The rest of the thing");
var sink = new TestSink();
var logger = new TestLogger("logger", sink, enabled: true);
// Act & Assert // Act & Assert
Assert.True(PageDirectiveFeature.TryGetPageDirective(projectItem, out template)); Assert.True(PageDirectiveFeature.TryGetPageDirective(logger, projectItem, out var template));
Assert.Equal("Some/Path/{value}", template); Assert.Equal("Some/Path/{value}", template);
Assert.Empty(sink.Writes);
} }
[Fact] [Fact]
public void TryGetPageDirective_NoNewLine() public void TryGetPageDirective_NoNewLine()
{ {
// Arrange // Arrange
string template;
var projectItem = new TestRazorProjectItem(@"@page ""Some/Path/{value}"""); var projectItem = new TestRazorProjectItem(@"@page ""Some/Path/{value}""");
var sink = new TestSink();
var logger = new TestLogger("logger", sink, enabled: true);
// Act & Assert // Act & Assert
Assert.True(PageDirectiveFeature.TryGetPageDirective(projectItem, out template)); Assert.True(PageDirectiveFeature.TryGetPageDirective(logger, projectItem, out var template));
Assert.Equal("Some/Path/{value}", template); Assert.Equal("Some/Path/{value}", template);
Assert.Empty(sink.Writes);
} }
[Fact] [Fact]
public void TryGetPageDirective_JunkBeforeDirective() public void TryGetPageDirective_JunkBeforeDirective()
{ {
// Arrange // Arrange
string template;
var projectItem = new TestRazorProjectItem(@"Not a directive @page ""Some/Path/{value}"""); var projectItem = new TestRazorProjectItem(@"Not a directive @page ""Some/Path/{value}""");
var sink = new TestSink();
var logger = new TestLogger("logger", sink, enabled: true);
// Act & Assert // Act & Assert
Assert.False(PageDirectiveFeature.TryGetPageDirective(projectItem, out template)); Assert.False(PageDirectiveFeature.TryGetPageDirective(logger, projectItem, out var template));
Assert.Null(template); Assert.Null(template);
Assert.Empty(sink.Writes);
} }
[Theory] [Theory]
[InlineData(@"""Some/Path/{value}")] [InlineData(@"""Some/Path/{value}")]
[InlineData(@"Some/Path/{value}""")] [InlineData(@"Some/Path/{value}""")]
public void TryGetPageDirective_RequiresBothQuotes(string inTemplate) public void TryGetPageDirective_WithoutBothQuotes_LogsWarning(string inTemplate)
{ {
// Arrange // Arrange
string template; var expected = "The page directive at 'Test.cshtml' is malformed. Please fix the following issues: The 'page' directive expects a string surrounded by double quotes.";
var sink = new TestSink();
var logger = new TestLogger("logger", sink, enabled: true);
var projectItem = new TestRazorProjectItem($@"@page {inTemplate}"); var projectItem = new TestRazorProjectItem($@"@page {inTemplate}");
// Act & Assert // Act & Assert
Assert.False(PageDirectiveFeature.TryGetPageDirective(projectItem, out template)); Assert.True(PageDirectiveFeature.TryGetPageDirective(logger, projectItem, out var template));
Assert.Null(template); Assert.Null(template);
Assert.Collection(sink.Writes,
log =>
{
Assert.Equal(LogLevel.Warning, log.LogLevel);
Assert.Equal(expected, log.State.ToString());
});
} }
[Fact] [Fact]
public void TryGetPageDirective_NoQuotesAroundPath_IsNotTemplate() public void TryGetPageDirective_NoQuotesAroundPath_LogsWarning()
{ {
// Arrange // Arrange
string template; var expected = "The page directive at 'Test.cshtml' is malformed. Please fix the following issues: The 'page' directive expects a string surrounded by double quotes.";
var sink = new TestSink();
var logger = new TestLogger("logger", sink, enabled: true);
var projectItem = new TestRazorProjectItem(@"@page Some/Path/{value}"); var projectItem = new TestRazorProjectItem(@"@page Some/Path/{value}");
// Act & Assert // Act & Assert
Assert.False(PageDirectiveFeature.TryGetPageDirective(projectItem, out template)); Assert.True(PageDirectiveFeature.TryGetPageDirective(logger, projectItem, out var template));
Assert.Null(template); Assert.Null(template);
} var logs = sink.Writes.Select(w => w.State.ToString().Trim()).ToList();
Assert.Collection(sink.Writes,
[Fact] log =>
public void TryGetPageDirective_WrongNewLine() {
{ Assert.Equal(LogLevel.Warning, log.LogLevel);
// Arrange Assert.Equal(expected, log.State.ToString());
var wrongNewLine = Environment.NewLine == "\r\n" ? "\n" : "\r\n"; });
string template;
var projectItem = new TestRazorProjectItem($"@page \"Some/Path/{{value}}\" {wrongNewLine}");
// Act & Assert
Assert.True(PageDirectiveFeature.TryGetPageDirective(projectItem, out template));
Assert.Equal("Some/Path/{value}", template);
} }
[Fact] [Fact]
public void TryGetPageDirective_NewLineBeforeDirective() public void TryGetPageDirective_NewLineBeforeDirective()
{ {
// Arrange // Arrange
string template;
var projectItem = new TestRazorProjectItem("\n @page \"Some/Path/{value}\""); var projectItem = new TestRazorProjectItem("\n @page \"Some/Path/{value}\"");
var sink = new TestSink();
var logger = new TestLogger("logger", sink, enabled: true);
// Act // Act
Assert.True(PageDirectiveFeature.TryGetPageDirective(projectItem, out template)); Assert.True(PageDirectiveFeature.TryGetPageDirective(logger, projectItem, out var template));
Assert.Equal("Some/Path/{value}", template); Assert.Equal("Some/Path/{value}", template);
} Assert.Empty(sink.Writes);
[Fact]
public void TryGetPageDirective_WhitespaceBeforeDirective()
{
// Arrange
string template;
var projectItem = new TestRazorProjectItem(@" @page ""Some/Path/{value}""
");
// Act & Assert
Assert.True(PageDirectiveFeature.TryGetPageDirective(projectItem, out template));
Assert.Equal("Some/Path/{value}", template);
}
[Fact]
public void TryGetPageDirective_JunkBeforeNewline()
{
// Arrange
string template;
var projectItem = new TestRazorProjectItem(@"@page ""Some/Path/{value}"" things that are not the path
a new line");
// Act & Assert
Assert.False(PageDirectiveFeature.TryGetPageDirective(projectItem, out template));
Assert.Null(template);
} }
[Fact] [Fact]
public void TryGetPageDirective_Directive_WithoutPathOrContent() public void TryGetPageDirective_Directive_WithoutPathOrContent()
{ {
// Arrange // Arrange
string template;
var projectItem = new TestRazorProjectItem(@"@page"); var projectItem = new TestRazorProjectItem(@"@page");
// Act & Assert // Act & Assert
Assert.True(PageDirectiveFeature.TryGetPageDirective(projectItem, out template)); Assert.True(PageDirectiveFeature.TryGetPageDirective(NullLogger.Instance, projectItem, out var template));
Assert.Null(template); Assert.Null(template);
} }
@ -142,59 +130,30 @@ a new line");
public void TryGetPageDirective_DirectiveWithContent_WithoutPath() public void TryGetPageDirective_DirectiveWithContent_WithoutPath()
{ {
// Arrange // Arrange
string template;
var projectItem = new TestRazorProjectItem(@"@page var projectItem = new TestRazorProjectItem(@"@page
Non-path things"); Non-path things");
var sink = new TestSink();
var logger = new TestLogger("logger", sink, enabled: true);
// Act & Assert // Act & Assert
Assert.True(PageDirectiveFeature.TryGetPageDirective(projectItem, out template)); Assert.True(PageDirectiveFeature.TryGetPageDirective(logger, projectItem, out var template));
Assert.Null(template); Assert.Null(template);
Assert.Empty(sink.Writes);
} }
[Fact] [Fact]
public void TryGetPageDirective_NoDirective() public void TryGetPageDirective_NoDirective()
{ {
// Arrange // Arrange
string template;
var projectItem = new TestRazorProjectItem(@"This is junk var projectItem = new TestRazorProjectItem(@"This is junk
Nobody will use it"); Nobody will use it");
var sink = new TestSink();
var logger = new TestLogger("logger", sink, enabled: true);
// Act & Assert // Act & Assert
Assert.False(PageDirectiveFeature.TryGetPageDirective(projectItem, out template)); Assert.False(PageDirectiveFeature.TryGetPageDirective(logger, projectItem, out var template));
Assert.Null(template); Assert.Null(template);
} Assert.Empty(sink.Writes);
[Fact]
public void TryGetPageDirective_EmptyStream()
{
// Arrange
string template;
var projectItem = new TestRazorProjectItem(string.Empty);
// Act
Assert.False(PageDirectiveFeature.TryGetPageDirective(projectItem, out template));
Assert.Null(template);
}
[Fact]
public void TryGetPageDirective_NullProject()
{
// Arrange
string template;
// Act & Assert
Assert.Throws<ArgumentNullException>(() => PageDirectiveFeature.TryGetPageDirective(projectItem: null, template: out template));
}
[Fact]
public void TryGetPageDirective_NullStream()
{
// Arrange
string template;
var projectItem = new TestRazorProjectItem(content: null);
// Act & Assert
Assert.Throws<ArgumentNullException>(() => PageDirectiveFeature.TryGetPageDirective(projectItem, out template));
} }
} }
@ -207,21 +166,9 @@ Nobody will use it");
_content = content; _content = content;
} }
public override string BasePath public override string BasePath => throw new NotImplementedException();
{
get
{
throw new NotImplementedException();
}
}
public override bool Exists public override bool Exists => throw new NotImplementedException();
{
get
{
throw new NotImplementedException();
}
}
public override string Path => "Test.cshtml"; public override string Path => "Test.cshtml";

View File

@ -5,6 +5,7 @@ using System;
using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
@ -26,7 +27,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
var optionsManager = new TestOptionsManager<RazorPagesOptions>(); var optionsManager = new TestOptionsManager<RazorPagesOptions>();
optionsManager.Value.RootDirectory = "/"; optionsManager.Value.RootDirectory = "/";
var provider = new RazorProjectPageApplicationModelProvider(project, optionsManager); var provider = new RazorProjectPageApplicationModelProvider(project, optionsManager, NullLoggerFactory.Instance);
var context = new PageApplicationModelProviderContext(); var context = new PageApplicationModelProviderContext();
// Act // Act
@ -60,7 +61,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
var optionsManager = new TestOptionsManager<RazorPagesOptions>(); var optionsManager = new TestOptionsManager<RazorPagesOptions>();
optionsManager.Value.RootDirectory = "/"; optionsManager.Value.RootDirectory = "/";
var provider = new RazorProjectPageApplicationModelProvider(project, optionsManager); var provider = new RazorProjectPageApplicationModelProvider(project, optionsManager, NullLoggerFactory.Instance);
var context = new PageApplicationModelProviderContext(); var context = new PageApplicationModelProviderContext();
// Act // Act
@ -98,7 +99,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
var optionsManager = new TestOptionsManager<RazorPagesOptions>(); var optionsManager = new TestOptionsManager<RazorPagesOptions>();
optionsManager.Value.RootDirectory = "/"; optionsManager.Value.RootDirectory = "/";
var provider = new RazorProjectPageApplicationModelProvider(project, optionsManager); var provider = new RazorProjectPageApplicationModelProvider(project, optionsManager, NullLoggerFactory.Instance);
var context = new PageApplicationModelProviderContext(); var context = new PageApplicationModelProviderContext();
// Act & Assert // Act & Assert
@ -124,7 +125,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
var optionsManager = new TestOptionsManager<RazorPagesOptions>(); var optionsManager = new TestOptionsManager<RazorPagesOptions>();
optionsManager.Value.RootDirectory = "/"; optionsManager.Value.RootDirectory = "/";
var provider = new RazorProjectPageApplicationModelProvider(project, optionsManager); var provider = new RazorProjectPageApplicationModelProvider(project, optionsManager, NullLoggerFactory.Instance);
var context = new PageApplicationModelProviderContext(); var context = new PageApplicationModelProviderContext();
// Act // Act
@ -162,7 +163,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
var optionsManager = new TestOptionsManager<RazorPagesOptions>(); var optionsManager = new TestOptionsManager<RazorPagesOptions>();
optionsManager.Value.RootDirectory = "/Pages"; optionsManager.Value.RootDirectory = "/Pages";
var provider = new RazorProjectPageApplicationModelProvider(project, optionsManager); var provider = new RazorProjectPageApplicationModelProvider(project, optionsManager, NullLoggerFactory.Instance);
var context = new PageApplicationModelProviderContext(); var context = new PageApplicationModelProviderContext();
// Act // Act