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;
+ }
+ }
+}