diff --git a/src/MvcSiteMapProvider/MvcSiteMapProvider/MvcSiteMapProvider.csproj b/src/MvcSiteMapProvider/MvcSiteMapProvider/MvcSiteMapProvider.csproj index 4fc6f9b6..faa9b324 100644 --- a/src/MvcSiteMapProvider/MvcSiteMapProvider/MvcSiteMapProvider.csproj +++ b/src/MvcSiteMapProvider/MvcSiteMapProvider/MvcSiteMapProvider.csproj @@ -199,6 +199,7 @@ + diff --git a/src/MvcSiteMapProvider/MvcSiteMapProvider/Resources/Messages.Designer.cs b/src/MvcSiteMapProvider/MvcSiteMapProvider/Resources/Messages.Designer.cs index 5877a758..ede258c5 100644 --- a/src/MvcSiteMapProvider/MvcSiteMapProvider/Resources/Messages.Designer.cs +++ b/src/MvcSiteMapProvider/MvcSiteMapProvider/Resources/Messages.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.33440 +// Runtime Version:4.0.30319.18408 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -383,6 +383,19 @@ internal static string SiteMapNodeActionAndURLNotSet { } } + /// + /// Looks up a localized string similar to The node with key '{0}' and title '{1}' does not have a valid value for Area. The current value is '{2}'. The Area field must be a valid C# identifier or be set to an empty string to indicate a non-area controller. + /// + ///A C# identifier must start with a Unicode letter, underscore, or ampersand and may be followed by zero or more Unicode letters, digits, or underscores. + /// + ///Please use the same value that is returned from the AreaName property of your AreaRegistration class.. + /// + internal static string SiteMapNodeAreaNameInvalid { + get { + return ResourceManager.GetString("SiteMapNodeAreaNameInvalid", resourceCulture); + } + } + /// /// Looks up a localized string similar to The '{0}' has already been set. Simultaneous use of both CanonicalUrl and CanonicalKey is not allowed.. /// @@ -392,6 +405,19 @@ internal static string SiteMapNodeCanonicalValueAlreadySet { } } + /// + /// Looks up a localized string similar to The node with key '{0}' and title '{1}' does not have a valid value for Controller. The current value is '{2}'. The Controller field must be a valid C# identifier and not end with the suffix 'Controller'. + /// + ///A C# identifier must start with a Unicode letter, underscore, or ampersand and may be followed by zero or more Unicode letters, digits, or underscores. + /// + ///If you are attempting to add an area to the controller field, do note that the 'AreaName/ControllerName' syntax is not supported by MvcSiteMapProvid [rest of string was truncated]";. + /// + internal static string SiteMapNodeControllerNameInvalid { + get { + return ResourceManager.GetString("SiteMapNodeControllerNameInvalid", resourceCulture); + } + } + /// /// Looks up a localized string similar to ParentKey: '{0}' | Controller: '{1}' | Action: '{2}' | Area: '{3}' | URL: '{4}' | Key: '{5}' | Source: '{6}'. /// diff --git a/src/MvcSiteMapProvider/MvcSiteMapProvider/Resources/Messages.resx b/src/MvcSiteMapProvider/MvcSiteMapProvider/Resources/Messages.resx index d93edd90..565040ba 100644 --- a/src/MvcSiteMapProvider/MvcSiteMapProvider/Resources/Messages.resx +++ b/src/MvcSiteMapProvider/MvcSiteMapProvider/Resources/Messages.resx @@ -341,4 +341,18 @@ The available values for HttpMethod are: {3} + + The node with key '{0}' and title '{1}' does not have a valid value for Area. The current value is '{2}'. The Area field must be a valid C# identifier or be set to an empty string to indicate a non-area controller. + +A C# identifier must start with a Unicode letter, underscore, or ampersand and may be followed by zero or more Unicode letters, digits, or underscores. + +Please use the same value that is returned from the AreaName property of your AreaRegistration class. + + + The node with key '{0}' and title '{1}' does not have a valid value for Controller. The current value is '{2}'. The Controller field must be a valid C# identifier and not end with the suffix 'Controller'. + +A C# identifier must start with a Unicode letter, underscore, or ampersand and may be followed by zero or more Unicode letters, digits, or underscores. + +If you are attempting to add an area to the controller field, do note that the 'AreaName/ControllerName' syntax is not supported by MvcSiteMapProvider. To set the area, use the 'Area' property or 'area' attribute. + \ No newline at end of file diff --git a/src/MvcSiteMapProvider/MvcSiteMapProvider/SiteMap.cs b/src/MvcSiteMapProvider/MvcSiteMapProvider/SiteMap.cs index 45ab4c9b..aa01daff 100644 --- a/src/MvcSiteMapProvider/MvcSiteMapProvider/SiteMap.cs +++ b/src/MvcSiteMapProvider/MvcSiteMapProvider/SiteMap.cs @@ -4,6 +4,7 @@ using System.Web.UI; using System.Web.Mvc; using System.Web.Routing; +using MvcSiteMapProvider.Text; using MvcSiteMapProvider.Web; using MvcSiteMapProvider.Web.Mvc; using MvcSiteMapProvider.Collections.Specialized; @@ -772,6 +773,8 @@ protected virtual ISiteMapNode ReturnNodeIfAccessible(ISiteMapNode node) protected virtual void AssertSiteMapNodeConfigurationIsValid(ISiteMapNode node) { ThrowIfTitleNotSet(node); + ThrowIfControllerNameInvalid(node); + ThrowIfAreaNameInvalid(node); ThrowIfActionAndUrlNotSet(node); ThrowIfHttpMethodInvalid(node); ThrowIfRouteValueIsPreservedRouteParameter(node); @@ -818,6 +821,28 @@ protected virtual void ThrowIfHttpMethodInvalid(ISiteMapNode node) } } + protected virtual void ThrowIfControllerNameInvalid(ISiteMapNode node) + { + if (!String.IsNullOrEmpty(node.Controller)) + { + if (!node.Controller.IsValidIdentifier() || node.Controller.EndsWith("Controller")) + { + throw new MvcSiteMapException(String.Format(Resources.Messages.SiteMapNodeControllerNameInvalid, node.Key, node.Title, node.Controller)); + } + } + } + + protected virtual void ThrowIfAreaNameInvalid(ISiteMapNode node) + { + if (!String.IsNullOrEmpty(node.Area)) + { + if (!node.Area.IsValidIdentifier()) + { + throw new MvcSiteMapException(String.Format(Resources.Messages.SiteMapNodeAreaNameInvalid, node.Key, node.Title, node.Area)); + } + } + } + #endregion } diff --git a/src/MvcSiteMapProvider/MvcSiteMapProvider/Text/StringExtensions.cs b/src/MvcSiteMapProvider/MvcSiteMapProvider/Text/StringExtensions.cs new file mode 100644 index 00000000..280ce633 --- /dev/null +++ b/src/MvcSiteMapProvider/MvcSiteMapProvider/Text/StringExtensions.cs @@ -0,0 +1,79 @@ +using System; +using System.Linq; +using System.Text.RegularExpressions; + +namespace MvcSiteMapProvider.Text +{ + public static class StringExtensions + { + // C# keywords: http://msdn.microsoft.com/en-us/library/x53a06bb(v=vs.71).aspx + private static string[] keywords = new[] + { + "abstract", "event", "new", "struct", + "as", "explicit", "null", "switch", + "base", "extern", "object", "this", + "bool", "false", "operator", "throw", + "breal", "finally", "out", "true", + "byte", "fixed", "override", "try", + "case", "float", "params", "typeof", + "catch", "for", "private", "uint", + "char", "foreach", "protected", "ulong", + "checked", "goto", "public", "unchekeced", + "class", "if", "readonly", "unsafe", + "const", "implicit", "ref", "ushort", + "continue", "in", "return", "using", + "decimal", "int", "sbyte", "virtual", + "default", "interface", "sealed", "volatile", + "delegate", "internal", "short", "void", + "do", "is", "sizeof", "while", + "double", "lock", "stackalloc", + "else", "long", "static", + "enum", "namespace", "string" + }; + + // definition of a valid C# identifier: http://msdn.microsoft.com/en-us/library/aa664670(v=vs.71).aspx + private const string formattingCharacter = @"\p{Cf}"; + private const string connectingCharacter = @"\p{Pc}"; + private const string decimalDigitCharacter = @"\p{Nd}"; + private const string combiningCharacter = @"\p{Mn}|\p{Mc}"; + private const string letterCharacter = @"\p{Lu}|\p{Ll}|\p{Lt}|\p{Lm}|\p{Lo}|\p{Nl}"; + private const string identifierPartCharacter = letterCharacter + "|" + + decimalDigitCharacter + "|" + + connectingCharacter + "|" + + combiningCharacter + "|" + + formattingCharacter; + private const string identifierPartCharacters = "(" + identifierPartCharacter + ")+"; + private const string identifierStartCharacter = "(" + letterCharacter + "|_)"; + private const string identifierOrKeyword = identifierStartCharacter + "(" + + identifierPartCharacters + ")*"; + private static Regex validIdentifierRegex = new Regex("^" + identifierOrKeyword + "$", RegexOptions.Compiled); + + + /// + /// Determines if a string matches a valid C# identifier according to the C# language specification (including Unicode support). + /// + /// The identifier being analyzed. + /// true if the identifier is valid, otherwise false. + /// Source: https://gist.github.com/LordDawnhunter/5245476 + public static bool IsValidIdentifier(this string identifier) + { + if (String.IsNullOrEmpty(identifier)) return false; + var normalizedIdentifier = identifier.Normalize(); + + // 1. check that the identifier match the validIdentifer regular expression and it's not a C# keyword + if (validIdentifierRegex.IsMatch(normalizedIdentifier) && !keywords.Contains(normalizedIdentifier)) + { + return true; + } + + // 2. check if the identifier starts with @ + if (normalizedIdentifier.StartsWith("@") && validIdentifierRegex.IsMatch(normalizedIdentifier.Substring(1))) + { + return true; + } + + // 3. it's not a valid identifier + return false; + } + } +}