diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/FormTagHelper.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/FormTagHelper.cs index 7584d2b295..3c0b46d24c 100644 --- a/src/Microsoft.AspNet.Mvc.TagHelpers/FormTagHelper.cs +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/FormTagHelper.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.AspNet.Mvc.Rendering; using Microsoft.AspNet.Razor.Runtime.TagHelpers; +using Microsoft.Framework.OptionsModel; namespace Microsoft.AspNet.Mvc.TagHelpers { @@ -28,6 +29,10 @@ namespace Microsoft.AspNet.Mvc.TagHelpers [Activate] protected internal IHtmlGenerator Generator { get; set; } + // Protected to ensure subclasses are correctly activated. Internal for ease of use when testing. + [Activate] + protected internal IOptions Options { get; set; } + /// /// The name of the action method. /// @@ -108,7 +113,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers } } - if (AntiForgery ?? antiForgeryDefault) + if (AntiForgery ?? Options.Options.GenerateAntiForgeryToken ?? antiForgeryDefault) { var antiForgeryTagBuilder = Generator.GenerateAntiForgery(ViewContext); if (antiForgeryTagBuilder != null) diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/FormTagHelperOptions.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/FormTagHelperOptions.cs new file mode 100644 index 0000000000..34de7730b6 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/FormTagHelperOptions.cs @@ -0,0 +1,21 @@ +// 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. + +namespace Microsoft.AspNet.Mvc.TagHelpers +{ + /// + /// Options pertaining to the default behavior of instances. + /// + public class FormTagHelperOptions + { + /// + /// Whether the anti-forgery token should be generated by default for all instances of + /// . Can be overridden on any given instance. + /// + /// + /// Defaults to null, which indicates a token will only be generated if the action + /// attribute was not explicitly defined. + /// + public bool? GenerateAntiForgeryToken { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/ITagHelperOptionsCollection.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/ITagHelperOptionsCollection.cs new file mode 100644 index 0000000000..80e0f3646f --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/ITagHelperOptionsCollection.cs @@ -0,0 +1,18 @@ +// 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.Razor.Runtime.TagHelpers; + +namespace Microsoft.Framework.DependencyInjection +{ + /// + /// Used for adding options pertaining to s to an . + /// + public interface ITagHelperOptionsCollection + { + /// + /// The . + /// + IServiceCollection Services { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/TagHelperOptionsCollection.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/TagHelperOptionsCollection.cs new file mode 100644 index 0000000000..a40f2155cb --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/TagHelperOptionsCollection.cs @@ -0,0 +1,29 @@ +// 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.Razor.Runtime.TagHelpers; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.Internal; + +namespace Microsoft.AspNet.Mvc.TagHelpers +{ + /// + /// Used for adding options pertaining to s to an . + /// + public class TagHelperOptionsCollection : ITagHelperOptionsCollection + { + /// + /// Creates a new ; + /// + /// The instance to add the options to. + public TagHelperOptionsCollection([NotNull] IServiceCollection serviceCollection) + { + Services = serviceCollection; + } + + /// + /// The . + /// + public IServiceCollection Services { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/TagHelperOptionsCollectionExtensions.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/TagHelperOptionsCollectionExtensions.cs new file mode 100644 index 0000000000..c673b09380 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/TagHelperOptionsCollectionExtensions.cs @@ -0,0 +1,46 @@ +// 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 Microsoft.AspNet.Mvc.TagHelpers; +using Microsoft.Framework.ConfigurationModel; +using Microsoft.Framework.Internal; + +namespace Microsoft.Framework.DependencyInjection +{ + /// + /// Extension methods for . + /// + public static class TagHelperOptionsCollectionExtensions + { + /// + /// Configures options for the from an . + /// + /// The instance this method extends. + /// An to get the options from. + /// The . + public static ITagHelperOptionsCollection ConfigureForm( + [NotNull] this ITagHelperOptionsCollection collection, + [NotNull] IConfiguration configuration) + { + collection.Services.Configure(configuration); + + return collection; + } + + /// + /// Configures options for the using a delegate. + /// + /// The instance this method extends. + /// The options setup delegate. + /// The . + public static ITagHelperOptionsCollection ConfigureForm( + [NotNull] this ITagHelperOptionsCollection collection, + [NotNull] Action setupAction) + { + collection.Services.Configure(setupAction); + + return collection; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/TagHelperServiceCollectionExtensions.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/TagHelperServiceCollectionExtensions.cs new file mode 100644 index 0000000000..0b694020a0 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/TagHelperServiceCollectionExtensions.cs @@ -0,0 +1,27 @@ +// 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.TagHelpers; +using Microsoft.AspNet.Razor.Runtime.TagHelpers; +using Microsoft.Framework.Internal; + +namespace Microsoft.Framework.DependencyInjection +{ + /// + /// Extension methods for . + /// + public static class TagHelperServiceCollectionExtensions + { + /// + /// Creates an which can be used to add options pertaining to + /// s to the . + /// + /// The instance this method extends. + /// The . + public static ITagHelperOptionsCollection ConfigureTagHelpers( + [NotNull] this IServiceCollection serviceCollection) + { + return new TagHelperOptionsCollection(serviceCollection); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/AntiForgeryTestHelper.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/AntiForgeryTestHelper.cs index d5b4377f86..67ef8a9d9d 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/AntiForgeryTestHelper.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/AntiForgeryTestHelper.cs @@ -2,6 +2,7 @@ // 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.IO; using System.Linq; using System.Net.Http; @@ -13,15 +14,27 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests { public static string RetrieveAntiForgeryToken(string htmlContent, string actionUrl) { + return RetrieveAntiForgeryTokens( + htmlContent, + attribute => attribute.Value.EndsWith(actionUrl, StringComparison.OrdinalIgnoreCase)) + .FirstOrDefault(); + } + + public static IEnumerable RetrieveAntiForgeryTokens( + string htmlContent, + Func predicate = null) + { + predicate = predicate ?? (_ => true); htmlContent = "" + htmlContent + ""; var reader = new StringReader(htmlContent); var htmlDocument = XDocument.Load(reader); + foreach (var form in htmlDocument.Descendants("form")) { foreach (var attribute in form.Attributes()) { - if (string.Equals(attribute.Name.LocalName, "action", StringComparison.OrdinalIgnoreCase) && - attribute.Value.EndsWith(actionUrl, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(attribute.Name.LocalName, "action", StringComparison.OrdinalIgnoreCase) + && predicate(attribute)) { foreach (var input in form.Descendants("input")) { @@ -30,14 +43,12 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests input.Attribute("name").Value == "__RequestVerificationToken" && input.Attribute("type").Value == "hidden") { - return input.Attributes("value").First().Value; + yield return input.Attributes("value").First().Value; } } } } } - - return null; } public static string RetrieveAntiForgeryCookie(HttpResponseMessage response) diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Form.Options.AntiForgery.False.html b/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Form.Options.AntiForgery.False.html new file mode 100644 index 0000000000..b7be3e8e24 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Form.Options.AntiForgery.False.html @@ -0,0 +1,15 @@ + + + + + Form + + + +

Form Tag Helper Test

+ +
+
+
+ + \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Form.Options.AntiForgery.True.html b/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Form.Options.AntiForgery.True.html new file mode 100644 index 0000000000..36cbabb5d5 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Form.Options.AntiForgery.True.html @@ -0,0 +1,15 @@ + + + + + Form + + + +

Form Tag Helper Test

+ +
+
+
+ + \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Form.Options.AntiForgery.null.html b/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Form.Options.AntiForgery.null.html new file mode 100644 index 0000000000..36cbabb5d5 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Form.Options.AntiForgery.null.html @@ -0,0 +1,15 @@ + + + + + Form + + + +

Form Tag Helper Test

+ +
+
+
+ + \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/MvcTagHelpersTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/MvcTagHelpersTest.cs index b937fe5d8b..f9b0a81ea8 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/MvcTagHelpersTest.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/MvcTagHelpersTest.cs @@ -3,13 +3,18 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Mvc.ModelBinding; +using Microsoft.AspNet.Mvc.Rendering; using Microsoft.AspNet.TestHost; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.Internal; using MvcTagHelpersWebSite; using Xunit; @@ -334,5 +339,41 @@ Products: Book1, Book2 (1)"; Products: Laptops (3)"; Assert.Equal(expected3, response4.Trim()); } + + [Theory] + [InlineData(null)] + [InlineData(true)] + [InlineData(false)] + public async Task FormTagHelper_GeneratesExpectedContent(bool? optionsAntiForgery) + { + // Arrange + var newServices = new ServiceCollection(); + newServices.ConfigureTagHelpers().ConfigureForm(options => options.GenerateAntiForgeryToken = optionsAntiForgery); + var serviceProvider = TestHelper.CreateServices("MvcTagHelpersWebSite", newServices); + var server = TestServer.Create(serviceProvider, _app); + var client = server.CreateClient(); + var expectedMediaType = MediaTypeHeaderValue.Parse("text/html; charset=utf-8"); + + // The K runtime compiles every file under compiler/resources as a resource at runtime with the same name + // as the file name, in order to update a baseline you just need to change the file in that folder. + var resourceName = string.Format( + "compiler/resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Form.Options.AntiForgery.{0}.html", + optionsAntiForgery?.ToString() ?? "null" + ); + var expectedContent = await _resourcesAssembly.ReadResourceAsStringAsync(resourceName); + + // Act + // The host is not important as everything runs in memory and tests are isolated from each other. + var response = await client.GetAsync("http://localhost/MvcTagHelper_Home/Form"); + var responseContent = await response.Content.ReadAsStringAsync(); + + var forgeryTokens = AntiForgeryTestHelper.RetrieveAntiForgeryTokens(responseContent); + expectedContent = string.Format(expectedContent, forgeryTokens.ToArray()); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(expectedMediaType, response.Content.Headers.ContentType); + Assert.Equal(expectedContent.Trim(), responseContent.Trim()); + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/FormTagHelperTest.cs b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/FormTagHelperTest.cs index cf24d16e39..15b031250e 100644 --- a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/FormTagHelperTest.cs +++ b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/FormTagHelperTest.cs @@ -10,6 +10,7 @@ using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Mvc.Rendering; using Microsoft.AspNet.Razor.Runtime.TagHelpers; using Microsoft.AspNet.Routing; +using Microsoft.Framework.OptionsModel; using Microsoft.Framework.WebEncoders; using Moq; using Xunit; @@ -89,12 +90,22 @@ namespace Microsoft.AspNet.Mvc.TagHelpers } [Theory] - [InlineData(true, "")] - [InlineData(false, "")] - [InlineData(null, "")] - public async Task ProcessAsync_GeneratesAntiForgeryCorrectly(bool? antiForgery, string expectedPostContent) + [InlineData(null, true, "")] + [InlineData(null, false, "")] + [InlineData(null, null, "")] + [InlineData(true, true, "")] + [InlineData(true, false, "")] + [InlineData(true, null, "")] + [InlineData(false, true, "")] + [InlineData(false, false, "")] + [InlineData(false, null, "")] + public async Task ProcessAsync_GeneratesAntiForgeryCorrectly( + bool? optionsAntiForgery, + bool? antiForgery, + string expectedPostContent) { // Arrange + var options = MakeOptions(new FormTagHelperOptions { GenerateAntiForgeryToken = optionsAntiForgery }); var viewContext = CreateViewContext(); var context = new TagHelperContext( allAttributes: new Dictionary(), @@ -128,6 +139,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers Action = "Index", AntiForgery = antiForgery, Generator = generator.Object, + Options = options, ViewContext = viewContext, }; @@ -268,9 +280,11 @@ namespace Microsoft.AspNet.Mvc.TagHelpers public async Task ProcessAsync_RestoresBoundAttributesIfActionIsSpecified(string htmlAction) { // Arrange + var options = MakeOptions(); var formTagHelper = new FormTagHelper { - Method = "POST" + Method = "POST", + Options = options }; var output = new TagHelperOutput("form", attributes: new Dictionary @@ -309,14 +323,22 @@ namespace Microsoft.AspNet.Mvc.TagHelpers } [Theory] - [InlineData(true, "")] - [InlineData(false, "")] - [InlineData(null, "")] + [InlineData(null, true, "")] + [InlineData(null, false, "")] + [InlineData(null, null, "")] + [InlineData(true, true, "")] + [InlineData(true, false, "")] + [InlineData(true, null, "")] + [InlineData(false, true, "")] + [InlineData(false, false, "")] + [InlineData(false, null, "")] public async Task ProcessAsync_SupportsAntiForgeryIfActionIsSpecified( + bool? optionsAntiForgery, bool? antiForgery, string expectedPostContent) { // Arrange + var options = MakeOptions(new FormTagHelperOptions { GenerateAntiForgeryToken = optionsAntiForgery }); var viewContext = CreateViewContext(); var generator = new Mock(); @@ -326,6 +348,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers { AntiForgery = antiForgery, Generator = generator.Object, + Options = options, ViewContext = viewContext, }; @@ -398,6 +421,15 @@ namespace Microsoft.AspNet.Mvc.TagHelpers Assert.Equal(expectedErrorMessage, ex.Message); } + private static IOptions MakeOptions(TOptions options = null) + where TOptions : class, new() + { + var optionsAccessor = new Mock>(); + optionsAccessor.Setup(o => o.Options).Returns(options ?? new TOptions()); + + return optionsAccessor.Object; + } + private static ViewContext CreateViewContext() { var actionContext = new ActionContext( diff --git a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/TagHelperOptionsCollectionExtensionsTest.cs b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/TagHelperOptionsCollectionExtensionsTest.cs new file mode 100644 index 0000000000..ebb59cebe4 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/TagHelperOptionsCollectionExtensionsTest.cs @@ -0,0 +1,52 @@ +// 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.Collections.Generic; +using Microsoft.Framework.ConfigurationModel; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.OptionsModel; +using Xunit; + +namespace Microsoft.AspNet.Mvc.TagHelpers.Test +{ + public class TagHelperOptionsCollectionExtensionsTest + { + public static TheoryData ConfigureForm_GetsOptionsFromConfigurationCorrectly_Data + { + get + { + return new TheoryData + { + { "true", true }, + { "false", false }, + { "True", true }, + { "False", false }, + { "TRue", true }, + { "FAlse", false }, + { null, null } + }; + } + } + + [Theory] + [MemberData(nameof(ConfigureForm_GetsOptionsFromConfigurationCorrectly_Data))] + public void ConfigureForm_GetsOptionsFromConfigurationCorrectly(string configValue, bool? expectedValue) + { + // Arrange + var configValues = new Dictionary + { + { $"{nameof(FormTagHelperOptions.GenerateAntiForgeryToken)}", configValue } + }; + var config = new Configuration(new MemoryConfigurationSource(configValues)); + var services = new ServiceCollection().AddOptions(); + services.ConfigureTagHelpers().ConfigureForm(config); + var serviceProvider = services.BuildServiceProvider(); + + // Act + var options = serviceProvider.GetService>().Options; + + // Assert + Assert.Equal(expectedValue, options.GenerateAntiForgeryToken); + } + } +} \ No newline at end of file diff --git a/test/WebSites/MvcTagHelpersWebSite/Controllers/MvcTagHelper_HomeController.cs b/test/WebSites/MvcTagHelpersWebSite/Controllers/MvcTagHelper_HomeController.cs index 391e29270f..478b2baf3b 100644 --- a/test/WebSites/MvcTagHelpersWebSite/Controllers/MvcTagHelper_HomeController.cs +++ b/test/WebSites/MvcTagHelpersWebSite/Controllers/MvcTagHelper_HomeController.cs @@ -166,5 +166,10 @@ namespace MvcTagHelpersWebSite.Controllers { return View(); } + + public IActionResult Form() + { + return View(); + } } } diff --git a/test/WebSites/MvcTagHelpersWebSite/Views/MvcTagHelper_Home/Form.cshtml b/test/WebSites/MvcTagHelpersWebSite/Views/MvcTagHelper_Home/Form.cshtml new file mode 100644 index 0000000000..f8446f2ef5 --- /dev/null +++ b/test/WebSites/MvcTagHelpersWebSite/Views/MvcTagHelper_Home/Form.cshtml @@ -0,0 +1,17 @@ +@using Microsoft.Framework.DependencyInjection +@addTagHelper "*, Microsoft.AspNet.Mvc.TagHelpers" + + + + + Form + + + +

Form Tag Helper Test

+ +
+
+
+ + \ No newline at end of file