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