1 // Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. 2 3 using System.Collections.Generic; 4 using System.ComponentModel.DataAnnotations; 5 using System.Diagnostics; 6 using System.Diagnostics.CodeAnalysis; 7 using System.Globalization; 8 using System.Linq; 9 using System.Text; 10 using System.Web.Mvc; 11 using System.Web.WebPages.Html; 12 using System.Web.WebPages.Scope; 13 using Microsoft.Internal.Web.Utils; 14 15 namespace System.Web.WebPages 16 { 17 public sealed class ValidationHelper 18 { 19 private static readonly object _invalidCssClassKey = new object(); 20 private static readonly object _validCssClassKey = new object(); 21 private static IDictionary<object, object> _scopeOverride; 22 23 private readonly Dictionary<string, List<IValidator>> _validators = new Dictionary<string, List<IValidator>>(StringComparer.OrdinalIgnoreCase); 24 private readonly HttpContextBase _httpContext; 25 private readonly ModelStateDictionary _modelStateDictionary; 26 ValidationHelper(HttpContextBase httpContext, ModelStateDictionary modelStateDictionary)27 internal ValidationHelper(HttpContextBase httpContext, ModelStateDictionary modelStateDictionary) 28 { 29 Debug.Assert(httpContext != null); 30 Debug.Assert(modelStateDictionary != null); 31 32 _httpContext = httpContext; 33 _modelStateDictionary = modelStateDictionary; 34 } 35 36 public static string ValidCssClass 37 { 38 get 39 { 40 object value; 41 if (!Scope.TryGetValue(_validCssClassKey, out value)) 42 { 43 return null; 44 } 45 return value as string; 46 } 47 set { Scope[_validCssClassKey] = value; } 48 } 49 50 public static string InvalidCssClass 51 { 52 get 53 { 54 object value; 55 if (!Scope.TryGetValue(_invalidCssClassKey, out value)) 56 { 57 return HtmlHelper.DefaultValidationInputErrorCssClass; 58 } 59 return value as string; 60 } 61 set { Scope[_invalidCssClassKey] = value; } 62 } 63 64 [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This makes it easier for a user to read this value without knowing of this type.")] 65 public string FormField 66 { 67 get { return ModelStateDictionary.FormFieldKey; } 68 } 69 70 internal static IDictionary<object, object> Scope 71 { 72 get { return _scopeOverride ?? ScopeStorage.CurrentScope; } 73 } 74 RequireField(string field)75 public void RequireField(string field) 76 { 77 RequireField(field, errorMessage: null); 78 } 79 RequireField(string field, string errorMessage)80 public void RequireField(string field, string errorMessage) 81 { 82 if (String.IsNullOrEmpty(field)) 83 { 84 throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "field"); 85 } 86 Add(field, Validator.Required(errorMessage: errorMessage)); 87 } 88 RequireFields(params string[] fields)89 public void RequireFields(params string[] fields) 90 { 91 if (fields == null) 92 { 93 throw new ArgumentNullException("fields"); 94 } 95 foreach (var field in fields) 96 { 97 RequireField(field); 98 } 99 } 100 Add(string field, params IValidator[] validators)101 public void Add(string field, params IValidator[] validators) 102 { 103 if (String.IsNullOrEmpty(field)) 104 { 105 throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "field"); 106 } 107 if ((validators == null) || validators.Any(v => v == null)) 108 { 109 throw new ArgumentNullException("validators"); 110 } 111 112 AddFieldValidators(field, validators); 113 } 114 Add(IEnumerable<string> fields, params IValidator[] validators)115 public void Add(IEnumerable<string> fields, params IValidator[] validators) 116 { 117 if (fields == null) 118 { 119 throw new ArgumentNullException("fields"); 120 } 121 if (validators == null) 122 { 123 throw new ArgumentNullException("validators"); 124 } 125 foreach (var field in fields) 126 { 127 Add(field, validators); 128 } 129 } 130 AddFormError(string errorMessage)131 public void AddFormError(string errorMessage) 132 { 133 _modelStateDictionary.AddFormError(errorMessage); 134 } 135 IsValid(params string[] fields)136 public bool IsValid(params string[] fields) 137 { 138 // Don't need to validate fields as we treat empty fields as all in Validate. 139 return !Validate(fields).Any(); 140 } 141 Validate(params string[] fields)142 public IEnumerable<ValidationResult> Validate(params string[] fields) 143 { 144 IEnumerable<string> keys = fields; 145 if (fields == null || !fields.Any()) 146 { 147 // If no fields are present, validate all of them. 148 keys = _validators.Keys.Concat(new[] { FormField }); 149 } 150 return ValidateFieldsAndUpdateModelState(keys); 151 } 152 GetErrors(params string[] fields)153 public IEnumerable<string> GetErrors(params string[] fields) 154 { 155 // Don't need to validate fields as we treat empty fields as all in Validate. 156 return Validate(fields).Select(r => r.ErrorMessage); 157 } 158 For(string field)159 public HtmlString For(string field) 160 { 161 if (String.IsNullOrEmpty(field)) 162 { 163 throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "field"); 164 } 165 166 var clientRules = GetClientValidationRules(field); 167 return GenerateHtmlFromClientValidationRules(clientRules); 168 } 169 ClassFor(string field)170 public HtmlString ClassFor(string field) 171 { 172 if (_httpContext != null && String.Equals("POST", _httpContext.Request.HttpMethod, StringComparison.OrdinalIgnoreCase)) 173 { 174 string cssClass = IsValid(field) ? ValidationHelper.ValidCssClass : ValidationHelper.InvalidCssClass; 175 return cssClass == null ? null : new HtmlString(cssClass); 176 } 177 return null; 178 } 179 OverrideScope()180 internal static IDisposable OverrideScope() 181 { 182 _scopeOverride = new Dictionary<object, object>(); 183 return new DisposableAction(() => _scopeOverride = null); 184 } 185 GetUnobtrusiveValidationAttributes(string field)186 internal IDictionary<string, object> GetUnobtrusiveValidationAttributes(string field) 187 { 188 var clientRules = GetClientValidationRules(field); 189 var attributes = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase); 190 UnobtrusiveValidationAttributesGenerator.GetValidationAttributes(clientRules, attributes); 191 return attributes; 192 } 193 ValidateFieldsAndUpdateModelState(IEnumerable<string> fields)194 private IEnumerable<ValidationResult> ValidateFieldsAndUpdateModelState(IEnumerable<string> fields) 195 { 196 var validationContext = new ValidationContext(_httpContext, serviceProvider: null, items: null); 197 var validationResults = new List<ValidationResult>(); 198 foreach (var field in fields) 199 { 200 IEnumerable<ValidationResult> fieldResults = ValidateField(field, validationContext); 201 IEnumerable<string> errors = fieldResults.Select(c => c.ErrorMessage); 202 ModelState modelState = _modelStateDictionary[field]; 203 if (modelState != null && modelState.Errors.Any()) 204 { 205 errors = errors.Except(modelState.Errors, StringComparer.OrdinalIgnoreCase); 206 207 // If there were other validation errors that were added via ModelState, add them to the collection. 208 fieldResults = fieldResults.Concat(modelState.Errors.Select(e => new ValidationResult(e, new[] { field }))); 209 } 210 211 foreach (var errorMessage in errors) 212 { 213 // Only add errors that haven't been encountered before. This is to prevent from the same error message being duplicated 214 // if a call is made multiple times 215 _modelStateDictionary.AddError(field, errorMessage); 216 } 217 218 validationResults.AddRange(fieldResults); 219 } 220 return validationResults; 221 } 222 AddFieldValidators(string field, params IValidator[] validators)223 private void AddFieldValidators(string field, params IValidator[] validators) 224 { 225 List<IValidator> fieldValidators = null; 226 if (!_validators.TryGetValue(field, out fieldValidators)) 227 { 228 fieldValidators = new List<IValidator>(); 229 _validators[field] = fieldValidators; 230 } 231 foreach (var validator in validators) 232 { 233 fieldValidators.Add(validator); 234 } 235 } 236 ValidateField(string field, ValidationContext context)237 private IEnumerable<ValidationResult> ValidateField(string field, ValidationContext context) 238 { 239 List<IValidator> fieldValidators; 240 if (!_validators.TryGetValue(field, out fieldValidators)) 241 { 242 return Enumerable.Empty<ValidationResult>(); 243 } 244 context.MemberName = field; 245 return fieldValidators.Select(f => f.Validate(context)) 246 .Where(result => result != ValidationResult.Success); 247 } 248 GetClientValidationRules(string field)249 private IEnumerable<ModelClientValidationRule> GetClientValidationRules(string field) 250 { 251 List<IValidator> fieldValidators = null; 252 if (!_validators.TryGetValue(field, out fieldValidators)) 253 { 254 return Enumerable.Empty<ModelClientValidationRule>(); 255 } 256 257 return from item in fieldValidators 258 let clientRule = item.ClientValidationRule 259 where clientRule != null 260 select clientRule; 261 } 262 GenerateHtmlFromClientValidationRules(IEnumerable<ModelClientValidationRule> clientRules)263 internal static HtmlString GenerateHtmlFromClientValidationRules(IEnumerable<ModelClientValidationRule> clientRules) 264 { 265 if (!clientRules.Any()) 266 { 267 return null; 268 } 269 270 var attributes = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase); 271 UnobtrusiveValidationAttributesGenerator.GetValidationAttributes(clientRules, attributes); 272 273 var stringBuilder = new StringBuilder(); 274 foreach (var attribute in attributes) 275 { 276 string key = attribute.Key; 277 // Values are already html encoded. 278 string value = Convert.ToString(attribute.Value, CultureInfo.InvariantCulture); 279 stringBuilder.Append(key) 280 .Append("=\"") 281 .Append(value) 282 .Append('"') 283 .Append(' '); 284 } 285 286 // Trim trailing whitespace 287 if (stringBuilder.Length > 0) 288 { 289 stringBuilder.Length--; 290 } 291 292 return new HtmlString(stringBuilder.ToString()); 293 } 294 } 295 } 296