From ca017eced27a0493eed7cec443991795ddfadf17 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Wed, 19 Apr 2017 14:18:58 -0700 Subject: [PATCH] Add page to AnchorTagHelper and FormTagHelper Fixes #6088 --- .../AnchorTagHelper.cs | 170 ++++++++++-------- .../FormActionTagHelper.cs | 102 ++++++----- .../FormTagHelper.cs | 102 ++++++----- .../Properties/Resources.Designer.cs | 56 ++---- .../Resources.resx | 14 +- .../ViewFeatures/DefaultHtmlGenerator.cs | 72 ++++++++ .../DefaultHtmlGeneratorExtensions.cs | 1 + .../ViewFeatures/IHtmlGenerator.cs | 61 +++++++ .../RedirectToPageResultTest.cs | 10 +- .../RazorPagesTest.cs | 48 +++++ .../RazorPagesWithBasePathTest.cs | 55 ++++++ .../AnchorTagHelperTest.cs | 142 ++++++++++++++- .../FormActionTagHelperTest.cs | 84 ++++++++- .../FormTagHelperTest.cs | 131 +++++++++++++- .../Pages/Routes/RouteUsingDefaultName.cshtml | 2 + .../Pages/Routes/Sibling.cshtml | 1 + .../Pages/Routes/_ViewImports.cshtml | 1 + .../Pages/TagHelper/CrossPost.cshtml | 5 + .../Pages/TagHelper/FormAction.cshtml | 4 + .../Pages/TagHelper/PathTraversalLinks.cshtml | 5 + .../Pages/TagHelper/SelfPost.cshtml | 5 + .../Pages/TagHelper/SiblingLinks.cshtml | 5 + .../Pages/TagHelper/SubDir/SubDirPage.cshtml | 2 + .../Pages/TagHelper/SubDirectoryLinks.cshtml | 5 + .../Pages/TagHelper/_ViewImports.cshtml | 1 + 25 files changed, 855 insertions(+), 229 deletions(-) create mode 100644 test/WebSites/RazorPagesWebSite/Pages/Routes/RouteUsingDefaultName.cshtml create mode 100644 test/WebSites/RazorPagesWebSite/Pages/Routes/Sibling.cshtml create mode 100644 test/WebSites/RazorPagesWebSite/Pages/Routes/_ViewImports.cshtml create mode 100644 test/WebSites/RazorPagesWebSite/Pages/TagHelper/CrossPost.cshtml create mode 100644 test/WebSites/RazorPagesWebSite/Pages/TagHelper/FormAction.cshtml create mode 100644 test/WebSites/RazorPagesWebSite/Pages/TagHelper/PathTraversalLinks.cshtml create mode 100644 test/WebSites/RazorPagesWebSite/Pages/TagHelper/SelfPost.cshtml create mode 100644 test/WebSites/RazorPagesWebSite/Pages/TagHelper/SiblingLinks.cshtml create mode 100644 test/WebSites/RazorPagesWebSite/Pages/TagHelper/SubDir/SubDirPage.cshtml create mode 100644 test/WebSites/RazorPagesWebSite/Pages/TagHelper/SubDirectoryLinks.cshtml create mode 100644 test/WebSites/RazorPagesWebSite/Pages/TagHelper/_ViewImports.cshtml diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/AnchorTagHelper.cs b/src/Microsoft.AspNetCore.Mvc.TagHelpers/AnchorTagHelper.cs index 8ef8ae97c6..d7de3ea208 100644 --- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/AnchorTagHelper.cs +++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/AnchorTagHelper.cs @@ -16,6 +16,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers [HtmlTargetElement("a", Attributes = ActionAttributeName)] [HtmlTargetElement("a", Attributes = ControllerAttributeName)] [HtmlTargetElement("a", Attributes = AreaAttributeName)] + [HtmlTargetElement("a", Attributes = PageAttributeName)] [HtmlTargetElement("a", Attributes = FragmentAttributeName)] [HtmlTargetElement("a", Attributes = HostAttributeName)] [HtmlTargetElement("a", Attributes = ProtocolAttributeName)] @@ -27,6 +28,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers private const string ActionAttributeName = "asp-action"; private const string ControllerAttributeName = "asp-controller"; private const string AreaAttributeName = "asp-area"; + private const string PageAttributeName = "asp-page"; private const string FragmentAttributeName = "asp-fragment"; private const string HostAttributeName = "asp-host"; private const string ProtocolAttributeName = "asp-protocol"; @@ -59,24 +61,40 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers /// /// The name of the action method. /// - /// Must be null if is non-null. + /// + /// Must be null if or is non-null. + /// [HtmlAttributeName(ActionAttributeName)] public string Action { get; set; } /// /// The name of the controller. /// - /// Must be null if is non-null. + /// + /// Must be null if or is non-null. + /// [HtmlAttributeName(ControllerAttributeName)] public string Controller { get; set; } /// /// The name of the area. /// - /// Must be null if is non-null. + /// + /// Must be null if or is non-null. + /// [HtmlAttributeName(AreaAttributeName)] public string Area { get; set; } + /// + /// The name of the page. + /// + /// + /// Must be null if or , + /// or is non-null. + /// + [HtmlAttributeName(PageAttributeName)] + public string Page { get; set; } + /// /// The protocol for the URL, such as "http" or "https". /// @@ -99,7 +117,8 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers /// Name of the route. /// /// - /// Must be null if or is non-null. + /// Must be null if one of , , + /// or is non-null. /// [HtmlAttributeName(RouteAttributeName)] public string Route { get; set; } @@ -134,12 +153,6 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers /// /// Does nothing if user provides an href attribute. - /// - /// Thrown if href attribute is provided and , , - /// , , , or are - /// non-null or if the user provided asp-route-* attributes. Also thrown if - /// and one or both of and are non-null. - /// public override void Process(TagHelperContext context, TagHelperOutput output) { if (context == null) @@ -158,6 +171,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers if (Action != null || Controller != null || Area != null || + Page != null || Route != null || Protocol != null || Host != null || @@ -167,7 +181,9 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers // User specified an href and one of the bound attributes; can't determine the href attribute. throw new InvalidOperationException( Resources.FormatAnchorTagHelper_CannotOverrideHref( + Href, "", + RouteValuesPrefix, ActionAttributeName, ControllerAttributeName, AreaAttributeName, @@ -175,72 +191,84 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers ProtocolAttributeName, HostAttributeName, FragmentAttributeName, - RouteValuesPrefix, - Href)); + PageAttributeName)); } + + return; + } + + var routeLink = Route != null; + var actionLink = Controller != null || Action != null; + var pageLink = Page != null; + + if ((routeLink && actionLink) || (routeLink && pageLink) || (actionLink && pageLink)) + { + var message = string.Join( + Environment.NewLine, + Resources.FormatCannotDetermineAttributeFor(Href, ""), + RouteAttributeName, + ControllerAttributeName + ", " + ActionAttributeName, + PageAttributeName); + + throw new InvalidOperationException(message); + } + + RouteValueDictionary routeValues = null; + if (_routeValues != null && _routeValues.Count > 0) + { + routeValues = new RouteValueDictionary(_routeValues); + } + + if (Area != null) + { + // Unconditionally replace any value from asp-route-area. + if (routeValues == null) + { + routeValues = new RouteValueDictionary(); + } + routeValues["area"] = Area; + } + + TagBuilder tagBuilder; + if (pageLink) + { + tagBuilder = Generator.GeneratePageLink( + ViewContext, + linkText: string.Empty, + pageName: Page, + protocol: Protocol, + hostname: Host, + fragment: Fragment, + routeValues: routeValues, + htmlAttributes: null); + } + else if (routeLink) + { + tagBuilder = Generator.GenerateRouteLink( + ViewContext, + linkText: string.Empty, + routeName: Route, + protocol: Protocol, + hostName: Host, + fragment: Fragment, + routeValues: routeValues, + htmlAttributes: null); } else { - RouteValueDictionary routeValues = null; - if (_routeValues != null && _routeValues.Count > 0) - { - routeValues = new RouteValueDictionary(_routeValues); - } - - if (Area != null) - { - if (routeValues == null) - { - routeValues = new RouteValueDictionary(); - } - - // Unconditionally replace any value from asp-route-area. - routeValues["area"] = Area; - } - - TagBuilder tagBuilder; - if (Route == null) - { - tagBuilder = Generator.GenerateActionLink( - ViewContext, - linkText: string.Empty, - actionName: Action, - controllerName: Controller, - protocol: Protocol, - hostname: Host, - fragment: Fragment, - routeValues: routeValues, - htmlAttributes: null); - } - else if (Action != null || Controller != null) - { - // Route and Action or Controller were specified. Can't determine the href attribute. - throw new InvalidOperationException( - Resources.FormatAnchorTagHelper_CannotDetermineHrefRouteActionOrControllerSpecified( - "", - RouteAttributeName, - ActionAttributeName, - ControllerAttributeName, - Href)); - } - else - { - tagBuilder = Generator.GenerateRouteLink( - ViewContext, - linkText: string.Empty, - routeName: Route, - protocol: Protocol, - hostName: Host, - fragment: Fragment, - routeValues: routeValues, - htmlAttributes: null); - } - - if (tagBuilder != null) - { - output.MergeAttributes(tagBuilder); - } + tagBuilder = Generator.GenerateActionLink( + ViewContext, + linkText: string.Empty, + actionName: Action, + controllerName: Controller, + protocol: Protocol, + hostname: Host, + fragment: Fragment, + routeValues: routeValues, + htmlAttributes: null); } + + output.MergeAttributes(tagBuilder); } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/FormActionTagHelper.cs b/src/Microsoft.AspNetCore.Mvc.TagHelpers/FormActionTagHelper.cs index 1b7cc1e401..5c2b8201ca 100644 --- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/FormActionTagHelper.cs +++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/FormActionTagHelper.cs @@ -18,6 +18,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers [HtmlTargetElement("button", Attributes = ActionAttributeName)] [HtmlTargetElement("button", Attributes = ControllerAttributeName)] [HtmlTargetElement("button", Attributes = AreaAttributeName)] + [HtmlTargetElement("button", Attributes = PageAttributeName)] [HtmlTargetElement("button", Attributes = FragmentAttributeName)] [HtmlTargetElement("button", Attributes = RouteAttributeName)] [HtmlTargetElement("button", Attributes = RouteValuesDictionaryName)] @@ -25,6 +26,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers [HtmlTargetElement("input", Attributes = ImageActionAttributeSelector, TagStructure = TagStructure.WithoutEndTag)] [HtmlTargetElement("input", Attributes = ImageControllerAttributeSelector, TagStructure = TagStructure.WithoutEndTag)] [HtmlTargetElement("input", Attributes = ImageAreaAttributeSelector, TagStructure = TagStructure.WithoutEndTag)] + [HtmlTargetElement("input", Attributes = ImagePageAttributeSelector, TagStructure = TagStructure.WithoutEndTag)] [HtmlTargetElement("input", Attributes = ImageFragmentAttributeSelector, TagStructure = TagStructure.WithoutEndTag)] [HtmlTargetElement("input", Attributes = ImageRouteAttributeSelector, TagStructure = TagStructure.WithoutEndTag)] [HtmlTargetElement("input", Attributes = ImageRouteValuesDictionarySelector, TagStructure = TagStructure.WithoutEndTag)] @@ -32,6 +34,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers [HtmlTargetElement("input", Attributes = SubmitActionAttributeSelector, TagStructure = TagStructure.WithoutEndTag)] [HtmlTargetElement("input", Attributes = SubmitControllerAttributeSelector, TagStructure = TagStructure.WithoutEndTag)] [HtmlTargetElement("input", Attributes = SubmitAreaAttributeSelector, TagStructure = TagStructure.WithoutEndTag)] + [HtmlTargetElement("input", Attributes = SubmitPageAttributeSelector, TagStructure = TagStructure.WithoutEndTag)] [HtmlTargetElement("input", Attributes = SubmitFragmentAttributeSelector, TagStructure = TagStructure.WithoutEndTag)] [HtmlTargetElement("input", Attributes = SubmitRouteAttributeSelector, TagStructure = TagStructure.WithoutEndTag)] [HtmlTargetElement("input", Attributes = SubmitRouteValuesDictionarySelector, TagStructure = TagStructure.WithoutEndTag)] @@ -41,6 +44,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers private const string ActionAttributeName = "asp-action"; private const string AreaAttributeName = "asp-area"; private const string ControllerAttributeName = "asp-controller"; + private const string PageAttributeName = "asp-page"; private const string FragmentAttributeName = "asp-fragment"; private const string RouteAttributeName = "asp-route"; private const string RouteValuesDictionaryName = "asp-all-route-data"; @@ -50,6 +54,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers private const string ImageTypeSelector = "[type=image], "; private const string ImageActionAttributeSelector = ImageTypeSelector + ActionAttributeName; private const string ImageAreaAttributeSelector = ImageTypeSelector + AreaAttributeName; + private const string ImagePageAttributeSelector = ImageTypeSelector + PageAttributeName; private const string ImageFragmentAttributeSelector = ImageTypeSelector + FragmentAttributeName; private const string ImageControllerAttributeSelector = ImageTypeSelector + ControllerAttributeName; private const string ImageRouteAttributeSelector = ImageTypeSelector + RouteAttributeName; @@ -59,6 +64,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers private const string SubmitTypeSelector = "[type=submit], "; private const string SubmitActionAttributeSelector = SubmitTypeSelector + ActionAttributeName; private const string SubmitAreaAttributeSelector = SubmitTypeSelector + AreaAttributeName; + private const string SubmitPageAttributeSelector = SubmitTypeSelector + PageAttributeName; private const string SubmitFragmentAttributeSelector = SubmitTypeSelector + FragmentAttributeName; private const string SubmitControllerAttributeSelector = SubmitTypeSelector + ControllerAttributeName; private const string SubmitRouteAttributeSelector = SubmitTypeSelector + RouteAttributeName; @@ -106,6 +112,12 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers [HtmlAttributeName(AreaAttributeName)] public string Area { get; set; } + /// + /// The name of the page. + /// + [HtmlAttributeName(PageAttributeName)] + public string Page { get; set; } + /// /// Gets or sets the URL fragment. /// @@ -185,51 +197,59 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers RouteValuesPrefix, FormAction)); } + + return; + } + + var routeLink = Route != null; + var actionLink = Controller != null || Action != null; + var pageLink = Page != null; + + if ((routeLink && actionLink) || (routeLink && pageLink) || (actionLink && pageLink)) + { + var message = string.Join( + Environment.NewLine, + Resources.FormatCannotDetermineAttributeFor(FormAction, '<' + output.TagName + '>'), + RouteAttributeName, + ControllerAttributeName + ", " + ActionAttributeName, + PageAttributeName); + + throw new InvalidOperationException(message); + } + + RouteValueDictionary routeValues = null; + if (_routeValues != null && _routeValues.Count > 0) + { + routeValues = new RouteValueDictionary(_routeValues); + } + + if (Area != null) + { + if (routeValues == null) + { + routeValues = new RouteValueDictionary(); + } + + // Unconditionally replace any value from asp-route-area. + routeValues["area"] = Area; + } + + var urlHelper = UrlHelperFactory.GetUrlHelper(ViewContext); + string url; + if (pageLink) + { + url = urlHelper.Page(Page, routeValues, protocol: null, host: null, fragment: Fragment); + } + else if (routeLink) + { + url = urlHelper.RouteUrl(Route, routeValues, protocol: null, host: null, fragment: Fragment); } else { - RouteValueDictionary routeValues = null; - if (_routeValues != null && _routeValues.Count > 0) - { - routeValues = new RouteValueDictionary(_routeValues); - } - - if (Area != null) - { - if (routeValues == null) - { - routeValues = new RouteValueDictionary(); - } - - // Unconditionally replace any value from asp-route-area. - routeValues["area"] = Area; - } - - if (Route == null) - { - var urlHelper = UrlHelperFactory.GetUrlHelper(ViewContext); - var url = urlHelper.Action(Action, Controller, routeValues, protocol: null, host: null, fragment: Fragment); - output.Attributes.SetAttribute(FormAction, url); - } - else if (Action != null || Controller != null) - { - // Route and Action or Controller were specified. Can't determine the formaction attribute. - throw new InvalidOperationException( - Resources.FormatFormActionTagHelper_CannotDetermineFormActionRouteActionOrControllerSpecified( - output.TagName, - RouteAttributeName, - ActionAttributeName, - ControllerAttributeName, - FormAction, - FragmentAttributeName)); - } - else - { - var urlHelper = UrlHelperFactory.GetUrlHelper(ViewContext); - var url = urlHelper.RouteUrl(Route, routeValues, protocol: null, host: null, fragment: Fragment); - output.Attributes.SetAttribute(FormAction, url); - } + url = urlHelper.Action(Action, Controller, routeValues, protocol: null, host: null, fragment: Fragment); } + + output.Attributes.SetAttribute(FormAction, url); } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/FormTagHelper.cs b/src/Microsoft.AspNetCore.Mvc.TagHelpers/FormTagHelper.cs index 3b00906fcb..02ae810309 100644 --- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/FormTagHelper.cs +++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/FormTagHelper.cs @@ -7,6 +7,7 @@ using System.ComponentModel; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Razor.TagHelpers; +using Microsoft.AspNetCore.Routing; namespace Microsoft.AspNetCore.Mvc.TagHelpers { @@ -16,6 +17,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers [HtmlTargetElement("form", Attributes = ActionAttributeName)] [HtmlTargetElement("form", Attributes = AntiforgeryAttributeName)] [HtmlTargetElement("form", Attributes = AreaAttributeName)] + [HtmlTargetElement("form", Attributes = PageAttributeName)] [HtmlTargetElement("form", Attributes = FragmentAttributeName)] [HtmlTargetElement("form", Attributes = ControllerAttributeName)] [HtmlTargetElement("form", Attributes = RouteAttributeName)] @@ -26,6 +28,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers private const string ActionAttributeName = "asp-action"; private const string AntiforgeryAttributeName = "asp-antiforgery"; private const string AreaAttributeName = "asp-area"; + private const string PageAttributeName = "asp-page"; private const string FragmentAttributeName = "asp-fragment"; private const string ControllerAttributeName = "asp-controller"; private const string RouteAttributeName = "asp-route"; @@ -72,6 +75,12 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers [HtmlAttributeName(AreaAttributeName)] public string Area { get; set; } + /// + /// The name of the page. + /// + [HtmlAttributeName(PageAttributeName)] + public string Page { get; set; } + /// /// Whether the antiforgery token should be generated. /// @@ -156,6 +165,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers if (Action != null || Controller != null || Area != null || + Page != null || Fragment != null || Route != null || (_routeValues != null && _routeValues.Count > 0)) @@ -163,14 +173,15 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers // User also specified bound attributes we cannot use. throw new InvalidOperationException( Resources.FormatFormTagHelper_CannotOverrideAction( - "
", HtmlActionAttributeName, + "", + RouteValuesPrefix, ActionAttributeName, ControllerAttributeName, FragmentAttributeName, AreaAttributeName, RouteAttributeName, - RouteValuesPrefix)); + PageAttributeName)); } // User is using the FormTagHelper like a normal tag. Antiforgery default should be false to @@ -179,22 +190,33 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers } else { - IDictionary routeValues = null; + var routeLink = Route != null; + var actionLink = Controller != null || Action != null; + var pageLink = Page != null; + + if ((routeLink && actionLink) || (routeLink && pageLink) || (actionLink && pageLink)) + { + var message = string.Join( + Environment.NewLine, + Resources.FormatCannotDetermineAttributeFor(HtmlActionAttributeName, ""), + RouteAttributeName, + ControllerAttributeName + ", " + ActionAttributeName, + PageAttributeName); + + throw new InvalidOperationException(message); + } + + RouteValueDictionary routeValues = null; if (_routeValues != null && _routeValues.Count > 0) { - // Convert from Dictionary to Dictionary. - routeValues = new Dictionary(_routeValues.Count, StringComparer.OrdinalIgnoreCase); - foreach (var routeValue in _routeValues) - { - routeValues.Add(routeValue.Key, routeValue.Value); - } + routeValues = new RouteValueDictionary(_routeValues); } if (Area != null) { if (routeValues == null) { - routeValues = new Dictionary(StringComparer.OrdinalIgnoreCase); + routeValues = new RouteValueDictionary(); } // Unconditionally replace any value from asp-route-area. @@ -202,7 +224,27 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers } TagBuilder tagBuilder; - if (Route == null) + if (pageLink) + { + tagBuilder = Generator.GeneratePageForm( + ViewContext, + Page, + routeValues, + Fragment, + method: null, + htmlAttributes: null); + } + else if (routeLink) + { + tagBuilder = Generator.GenerateRouteForm( + ViewContext, + Route, + routeValues, + Fragment, + method: null, + htmlAttributes: null); + } + else { tagBuilder = Generator.GenerateForm( ViewContext, @@ -213,42 +255,14 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers method: null, htmlAttributes: null); } - else if (Action != null || Controller != null) + + output.MergeAttributes(tagBuilder); + if (tagBuilder.HasInnerHtml) { - // Route and Action or Controller were specified. Can't determine the action attribute. - throw new InvalidOperationException( - Resources.FormatFormTagHelper_CannotDetermineActionWithRouteAndActionOrControllerSpecified( - "", - RouteAttributeName, - ActionAttributeName, - ControllerAttributeName, - HtmlActionAttributeName, - FragmentAttributeName)); - } - else - { - tagBuilder = Generator.GenerateRouteForm( - ViewContext, - Route, - routeValues, - Fragment, - method: null, - htmlAttributes: null); + output.PostContent.AppendHtml(tagBuilder.InnerHtml); } - if (tagBuilder != null) - { - output.MergeAttributes(tagBuilder); - if (tagBuilder.HasInnerHtml) - { - output.PostContent.AppendHtml(tagBuilder.InnerHtml); - } - } - - if (string.Equals(Method, "get", StringComparison.OrdinalIgnoreCase)) - { - antiforgeryDefault = false; - } + antiforgeryDefault = !string.Equals(Method, "get", StringComparison.OrdinalIgnoreCase); } if (Antiforgery ?? antiforgeryDefault) diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Mvc.TagHelpers/Properties/Resources.Designer.cs index 699ce21a9f..64b24c6483 100644 --- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/Properties/Resources.Designer.cs @@ -11,21 +11,21 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers = new ResourceManager("Microsoft.AspNetCore.Mvc.TagHelpers.Resources", typeof(Resources).GetTypeInfo().Assembly); /// - /// Cannot determine an '{4}' attribute for {0}. An {0} with a specified '{1}' must not have an '{2}' or '{3}' attribute. + /// Cannot determine the '{0}' attribute for {1}. The following attributes are mutually exclusive: /// - internal static string AnchorTagHelper_CannotDetermineHrefRouteActionOrControllerSpecified + internal static string CannotDetermineAttributeFor { - get => GetString("AnchorTagHelper_CannotDetermineHrefRouteActionOrControllerSpecified"); + get => GetString("CannotDetermineAttributeFor"); } /// - /// Cannot determine an '{4}' attribute for {0}. An {0} with a specified '{1}' must not have an '{2}' or '{3}' attribute. + /// Cannot determine the '{0}' attribute for {1}. The following attributes are mutually exclusive: /// - internal static string FormatAnchorTagHelper_CannotDetermineHrefRouteActionOrControllerSpecified(object p0, object p1, object p2, object p3, object p4) - => string.Format(CultureInfo.CurrentCulture, GetString("AnchorTagHelper_CannotDetermineHrefRouteActionOrControllerSpecified"), p0, p1, p2, p3, p4); + internal static string FormatCannotDetermineAttributeFor(object p0, object p1) + => string.Format(CultureInfo.CurrentCulture, GetString("CannotDetermineAttributeFor"), p0, p1); /// - /// Cannot override the '{9}' attribute for {0}. An {0} with a specified '{9}' must not have attributes starting with '{8}' or an '{1}', '{2}', '{3}', '{4}', '{5}', '{6}', or '{7}' attribute. + /// Cannot override the '{0}' attribute for {1}. An {1} with a specified '{0}' must not have attributes starting with '{2}' or an '{3}', '{4}', '{5}', '{6}', '{7}', '{8}', '{9}', or '{10}' attribute. /// internal static string AnchorTagHelper_CannotOverrideHref { @@ -33,13 +33,13 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers } /// - /// Cannot override the '{9}' attribute for {0}. An {0} with a specified '{9}' must not have attributes starting with '{8}' or an '{1}', '{2}', '{3}', '{4}', '{5}', '{6}', or '{7}' attribute. + /// Cannot override the '{0}' attribute for {1}. An {1} with a specified '{0}' must not have attributes starting with '{2}' or an '{3}', '{4}', '{5}', '{6}', '{7}', '{8}', '{9}', or '{10}' attribute. /// - internal static string FormatAnchorTagHelper_CannotOverrideHref(object p0, object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9) - => string.Format(CultureInfo.CurrentCulture, GetString("AnchorTagHelper_CannotOverrideHref"), p0, p1, p2, p3, p4, p5, p6, p7, p8, p9); + internal static string FormatAnchorTagHelper_CannotOverrideHref(object p0, object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9, object p10) + => string.Format(CultureInfo.CurrentCulture, GetString("AnchorTagHelper_CannotOverrideHref"), p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10); /// - /// Cannot override the '{1}' attribute for {0}. A {0} with a specified '{1}' must not have attributes starting with '{7}' or an '{2}', '{3}', '{4}', '{5}', or '{6}' attribute. + /// Cannot override the '{0}' attribute for {1}. A {1} with a specified '{0}' must not have attributes starting with '{2}' or an '{3}', '{4}', '{5}', '{6}', '{7}' or '{8}' attribute. /// internal static string FormTagHelper_CannotOverrideAction { @@ -47,10 +47,10 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers } /// - /// Cannot override the '{1}' attribute for {0}. A {0} with a specified '{1}' must not have attributes starting with '{7}' or an '{2}', '{3}', '{4}', '{5}', or '{6}' attribute. + /// Cannot override the '{0}' attribute for {1}. A {1} with a specified '{0}' must not have attributes starting with '{2}' or an '{3}', '{4}', '{5}', '{6}', '{7}' or '{8}' attribute. /// - internal static string FormatFormTagHelper_CannotOverrideAction(object p0, object p1, object p2, object p3, object p4, object p5, object p6, object p7) - => string.Format(CultureInfo.CurrentCulture, GetString("FormTagHelper_CannotOverrideAction"), p0, p1, p2, p3, p4, p5, p6, p7); + internal static string FormatFormTagHelper_CannotOverrideAction(object p0, object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8) + => string.Format(CultureInfo.CurrentCulture, GetString("FormTagHelper_CannotOverrideAction"), p0, p1, p2, p3, p4, p5, p6, p7, p8); ///
/// Unexpected '{1}' expression result type '{2}' for {0}. '{1}' must be of type '{3}' or '{4}' that can be parsed as a '{3}' if '{5}' is '{6}'. @@ -136,20 +136,6 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers internal static string FormatTagHelperOutput_AttributeDoesNotExist(object p0, object p1) => string.Format(CultureInfo.CurrentCulture, GetString("TagHelperOutput_AttributeDoesNotExist"), p0, p1); - /// - /// Cannot determine an '{4}' attribute for {0}. A {0} with a specified '{1}' must not have an '{2}', '{3}', or '{5}' attribute. - /// - internal static string FormTagHelper_CannotDetermineActionWithRouteAndActionOrControllerSpecified - { - get => GetString("FormTagHelper_CannotDetermineActionWithRouteAndActionOrControllerSpecified"); - } - - /// - /// Cannot determine an '{4}' attribute for {0}. A {0} with a specified '{1}' must not have an '{2}', '{3}', or '{5}' attribute. - /// - internal static string FormatFormTagHelper_CannotDetermineActionWithRouteAndActionOrControllerSpecified(object p0, object p1, object p2, object p3, object p4, object p5) - => string.Format(CultureInfo.CurrentCulture, GetString("FormTagHelper_CannotDetermineActionWithRouteAndActionOrControllerSpecified"), p0, p1, p2, p3, p4, p5); - /// /// The '{0}' property of '{1}' must not be null. /// @@ -178,20 +164,6 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers internal static string FormatFormActionTagHelper_CannotOverrideFormAction(object p0, object p1, object p2, object p3, object p4, object p5, object p6, object p7) => string.Format(CultureInfo.CurrentCulture, GetString("FormActionTagHelper_CannotOverrideFormAction"), p0, p1, p2, p3, p4, p5, p6, p7); - /// - /// Cannot determine a '{4}' attribute for <{0}>. <{0}> elements with a specified '{1}' must not have an '{2}', '{3}', or '{5}' attribute. - /// - internal static string FormActionTagHelper_CannotDetermineFormActionRouteActionOrControllerSpecified - { - get => GetString("FormActionTagHelper_CannotDetermineFormActionRouteActionOrControllerSpecified"); - } - - /// - /// Cannot determine a '{4}' attribute for <{0}>. <{0}> elements with a specified '{1}' must not have an '{2}', '{3}', or '{5}' attribute. - /// - internal static string FormatFormActionTagHelper_CannotDetermineFormActionRouteActionOrControllerSpecified(object p0, object p1, object p2, object p3, object p4, object p5) - => string.Format(CultureInfo.CurrentCulture, GetString("FormActionTagHelper_CannotDetermineFormActionRouteActionOrControllerSpecified"), p0, p1, p2, p3, p4, p5); - private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/Resources.resx b/src/Microsoft.AspNetCore.Mvc.TagHelpers/Resources.resx index 48ad0dca73..b41c161bcd 100644 --- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/Resources.resx +++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/Resources.resx @@ -117,14 +117,14 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Cannot determine an '{4}' attribute for {0}. An {0} with a specified '{1}' must not have an '{2}' or '{3}' attribute. + + Cannot determine the '{0}' attribute for {1}. The following attributes are mutually exclusive: - Cannot override the '{9}' attribute for {0}. An {0} with a specified '{9}' must not have attributes starting with '{8}' or an '{1}', '{2}', '{3}', '{4}', '{5}', '{6}', or '{7}' attribute. + Cannot override the '{0}' attribute for {1}. An {1} with a specified '{0}' must not have attributes starting with '{2}' or an '{3}', '{4}', '{5}', '{6}', '{7}', '{8}', '{9}', or '{10}' attribute. - Cannot override the '{1}' attribute for {0}. A {0} with a specified '{1}' must not have attributes starting with '{7}' or an '{2}', '{3}', '{4}', '{5}', or '{6}' attribute. + Cannot override the '{0}' attribute for {1}. A {1} with a specified '{0}' must not have attributes starting with '{2}' or an '{3}', '{4}', '{5}', '{6}', '{7}' or '{8}' attribute. Unexpected '{1}' expression result type '{2}' for {0}. '{1}' must be of type '{3}' or '{4}' that can be parsed as a '{3}' if '{5}' is '{6}'. @@ -144,16 +144,10 @@ The attribute '{0}' does not exist in the {1}. - - Cannot determine an '{4}' attribute for {0}. A {0} with a specified '{1}' must not have an '{2}', '{3}', or '{5}' attribute. - The '{0}' property of '{1}' must not be null. Cannot override the '{7}' attribute for <{0}>. <{0}> elements with a specified '{7}' must not have attributes starting with '{6}' or an '{1}', '{2}', '{3}', '{4}', or '{5}' attribute. - - Cannot determine a '{4}' attribute for <{0}>. <{0}> elements with a specified '{1}' must not have an '{2}', '{3}', or '{5}' attribute. - \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/DefaultHtmlGenerator.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/DefaultHtmlGenerator.cs index ab292f1512..f97cb2fc35 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/DefaultHtmlGenerator.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/DefaultHtmlGenerator.cs @@ -142,6 +142,32 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures return GenerateLink(linkText, url, htmlAttributes); } + /// + public virtual TagBuilder GeneratePageLink( + ViewContext viewContext, + string linkText, + string pageName, + string protocol, + string hostname, + string fragment, + object routeValues, + object htmlAttributes) + { + if (viewContext == null) + { + throw new ArgumentNullException(nameof(viewContext)); + } + + if (linkText == null) + { + throw new ArgumentNullException(nameof(linkText)); + } + + var urlHelper = _urlHelperFactory.GetUrlHelper(viewContext); + var url = urlHelper.Page(pageName, routeValues, protocol, hostname, fragment); + return GenerateLink(linkText, url, htmlAttributes); + } + /// public virtual IHtmlContent GenerateAntiforgery(ViewContext viewContext) { @@ -281,6 +307,52 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures return GenerateFormCore(viewContext, action, method, htmlAttributes); } + /// + public virtual TagBuilder GeneratePageForm( + ViewContext viewContext, + string pageName, + object routeValues, + string fragment, + string method, + object htmlAttributes) + { + if (viewContext == null) + { + throw new ArgumentNullException(nameof(viewContext)); + } + + var defaultMethod = false; + if (string.IsNullOrEmpty(method)) + { + defaultMethod = true; + } + else if (string.Equals(method, "post", StringComparison.OrdinalIgnoreCase)) + { + defaultMethod = true; + } + + string action; + if (pageName == null && routeValues == null && defaultMethod) + { + // Submit to the original URL in the special case that user called the BeginForm() overload without + // parameters (except for the htmlAttributes parameter). Also reachable in the even-more-unusual case + // that user called another BeginForm() overload with default argument values. + var request = viewContext.HttpContext.Request; + action = request.PathBase + request.Path + request.QueryString; + if (fragment != null) + { + action += "#" + fragment; + } + } + else + { + var urlHelper = _urlHelperFactory.GetUrlHelper(viewContext); + action = urlHelper.Page(pageName, values: routeValues,protocol: null, host: null, fragment: fragment); + } + + return GenerateFormCore(viewContext, action, method, htmlAttributes); + } + /// public TagBuilder GenerateRouteForm( ViewContext viewContext, diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/DefaultHtmlGeneratorExtensions.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/DefaultHtmlGeneratorExtensions.cs index fd58824cd0..17f98ce2e3 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/DefaultHtmlGeneratorExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/DefaultHtmlGeneratorExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. 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.AspNetCore.Mvc.Rendering; namespace Microsoft.AspNetCore.Mvc.ViewFeatures diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/IHtmlGenerator.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/IHtmlGenerator.cs index 6d7510dafc..19f5dcfa20 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/IHtmlGenerator.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/IHtmlGenerator.cs @@ -54,6 +54,38 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures object routeValues, object htmlAttributes); + /// + /// Generate a <a> element for a link to an action. + /// + /// The instance for the current scope. + /// The text to insert inside the element. + /// The page name. + /// The protocol (scheme) for the generated link. + /// The hostname for the generated link. + /// The fragment for the genrated link. + /// + /// An that contains the parameters for a route. The parameters are retrieved through + /// reflection by examining the properties of the . This is typically + /// created using initializer syntax. Alternatively, an + /// instance containing the route parameters. + /// + /// + /// An that contains the HTML attributes for the element. Alternatively, an + /// instance containing the HTML attributes. + /// + /// + /// A instance for the <a> element. + /// + TagBuilder GeneratePageLink( + ViewContext viewContext, + string linkText, + string pageName, + string protocol, + string hostname, + string fragment, + object routeValues, + object htmlAttributes); + /// /// Generate an <input type="hidden".../> element containing an antiforgery token. /// @@ -124,6 +156,35 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures string method, object htmlAttributes); + /// + /// Generate a <form> element. When the user submits the form, the page with name + /// will process the request. + /// + /// A instance for the current scope. + /// The name of the action method. + /// + /// An that contains the parameters for a route. The parameters are retrieved through + /// reflection by examining the properties of the . This is typically + /// created using initializer syntax. Alternatively, an + /// instance containing the route parameters. + /// + /// The url fragment. + /// The HTTP method for processing the form, either GET or POST. + /// + /// An that contains the HTML attributes for the element. Alternatively, an + /// instance containing the HTML attributes. + /// + /// + /// A instance for the </form> element. + /// + TagBuilder GeneratePageForm( + ViewContext viewContext, + string pageName, + object routeValues, + string fragment, + string method, + object htmlAttributes); + /// /// Generate a <form> element. The route with name generates the /// <form>'s action attribute value. diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/RedirectToPageResultTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/RedirectToPageResultTest.cs index fe7b3b7672..f53099c9cd 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/RedirectToPageResultTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/RedirectToPageResultTest.cs @@ -33,7 +33,7 @@ namespace Microsoft.AspNetCore.Mvc var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); var urlHelper = GetUrlHelper(actionContext, returnValue: null); - var result = new RedirectToPageResult("some-page", new Dictionary()) + var result = new RedirectToPageResult("/some-page", new Dictionary()) { UrlHelper = urlHelper, }; @@ -41,7 +41,7 @@ namespace Microsoft.AspNetCore.Mvc // Act & Assert await ExceptionAssert.ThrowsAsync( () => result.ExecuteResultAsync(actionContext), - "No page named 'some-page' matches the supplied values."); + "No page named '/some-page' matches the supplied values."); } [Theory] @@ -65,7 +65,7 @@ namespace Microsoft.AspNetCore.Mvc new ActionDescriptor()); var urlHelper = GetUrlHelper(actionContext, expectedUrl); - var result = new RedirectToPageResult("MyPage", new { id = 10, test = "value" }, permanentRedirect) + var result = new RedirectToPageResult("/MyPage", new { id = 10, test = "value" }, permanentRedirect) { UrlHelper = urlHelper, }; @@ -99,7 +99,7 @@ namespace Microsoft.AspNetCore.Mvc .Callback((UrlRouteContext c) => context = c) .Returns("some-value"); var values = new { test = "test-value" }; - var result = new RedirectToPageResult("MyPage", values, true, "test-fragment") + var result = new RedirectToPageResult("/MyPage", values, true, "test-fragment") { UrlHelper = urlHelper.Object, Protocol = "ftp", @@ -120,7 +120,7 @@ namespace Microsoft.AspNetCore.Mvc value => { Assert.Equal("page", value.Key); - Assert.Equal("MyPage", value.Value); + Assert.Equal("/MyPage", value.Value); }); Assert.Equal("ftp", context.Protocol); Assert.Equal("test-fragment", context.Fragment); diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs index e62c4e1398..6f05910072 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs @@ -903,6 +903,54 @@ Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary`1[AspNetCore._InjectedP Assert.Equal(expected, response.Headers.Location.ToString()); } + [Fact] + public async Task TagHelpers_SupportSiblingRoutes() + { + // Arrange +var expected = +@" + +"; + + // Act + var response = await Client.GetStringAsync("/Pages/TagHelper/SiblingLinks"); + + // Assert + Assert.Equal(expected, response.Trim()); + } + + [Fact] + public async Task TagHelpers_SupportSubDirectoryRoutes() + { + // Arrange +var expected = +@"
+
+"; + + // Act + var response = await Client.GetStringAsync("/Pages/TagHelper/SubDirectoryLinks"); + + // Assert + Assert.Equal(expected, response.Trim()); + } + + [Fact] + public async Task TagHelpers_SupportsPathNavigation() + { + // Arrange +var expected = +@"
+
+"; + + // Act + var response = await Client.GetStringAsync("/Pages/TagHelper/PathTraversalLinks"); + + // Assert + Assert.Equal(expected, response.Trim()); + } + private async Task AddAntiforgeryHeaders(HttpRequestMessage request) { var getResponse = await Client.GetAsync(request.RequestUri); diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs index dd0728b6ff..2458e23263 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs @@ -143,6 +143,48 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests Assert.Equal(expected, response.Trim()); } + [Fact] + public async Task FormTagHelper_WithPage_GeneratesLinksToSelf() + { + //Arrange + var expected = "
"; + + // Act + var response = await Client.GetStringAsync("/TagHelper/SelfPost"); + + // Assert + Assert.Contains(expected, response.Trim()); + } + + [Fact] + public async Task FormTagHelper_WithPage_AllowsPostingToAnotherPage() + { + //Arrange + var expected = ""; + + // Act + var response = await Client.GetStringAsync("/TagHelper/CrossPost"); + + // Assert + Assert.Contains(expected, response.Trim()); + } + + [Fact] + public async Task FormActionTagHelper_WithPage_AllowsPostingToAnotherPage() + { + //Arrange + var expected = +@"