Merge pull request #3389 from lambdageek/bug-43099
[mono.git] / mcs / class / referencesource / System.Web.DynamicData / DynamicData / DynamicValidator.cs
1 namespace System.Web.DynamicData {
2     using System;
3     using System.Collections.Generic;
4     using System.ComponentModel;
5     using System.ComponentModel.DataAnnotations;
6     using System.Diagnostics;
7     using System.Drawing;
8     using System.Globalization;
9     using System.Linq;
10     using System.Web;
11     using System.Web.Resources;
12     using System.Web.UI;
13     using System.Web.UI.WebControls;
14     using System.Web.DynamicData.Util;
15
16     /// <summary>
17     /// Validator that enforces model validation. It can be used either at the field level or the entity level
18     /// </summary>
19     [ToolboxBitmap(typeof(DynamicValidator), "DynamicValidator.bmp")]
20     public class DynamicValidator : BaseValidator {
21
22         private IDynamicDataSource _dataSource;
23         private Exception _exception;
24         private Dictionary<Type, bool> _ignoredModelValidationAttributes;
25
26         /// <summary>
27         /// The name of the column to be validated, or null for entity level validation
28         /// </summary>
29         [Browsable(false)]
30         [Themeable(false)]
31         public string ColumnName {
32             get {
33                 return (Column == null) ? String.Empty : Column.Name;
34             }
35         }
36
37         private IDynamicDataSource DynamicDataSource {
38             get {
39                 if (_dataSource == null) {
40                     // get data source for the parent container.
41                     _dataSource = this.FindDataSourceControl();
42                     // get the data source for the targeted data bound control.
43                     if (_dataSource == null) {
44                         Control c = NamingContainer.FindControl(ControlToValidate);
45                         if (c == null) {
46                             throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture,
47                                 DynamicDataResources.DynamicValidator_ControlNotFound, ControlToValidate, ID));
48                         }
49
50                         _dataSource = c.FindDataSourceControl();
51                     }
52                 }
53                 return _dataSource;
54             }
55         }
56
57         /// <summary>
58         /// The column to be validated, or null for entity level validation
59         /// </summary>
60         [Browsable(false)]
61         [Themeable(false)]
62         public MetaColumn Column { get; set; }
63
64         /// <summary>
65         /// The validation exception that occurred, if any
66         /// </summary>
67         protected virtual Exception ValidationException {
68             get {
69                 return _exception;
70             }
71             set {
72                 _exception = value;
73             }
74         }
75
76         /// <summary>
77         /// Overridden from base
78         /// </summary>
79         protected override bool ControlPropertiesValid() {
80             bool hasDataSource = DynamicDataSource != null;
81
82             // We can't call the base when there is no Column, because the control will be something like
83             // a GridView, which doesn't have a validation property and would cause the base to fail.
84             if (String.IsNullOrEmpty(ColumnName)) {
85                 return hasDataSource;
86             }
87
88             return base.ControlPropertiesValid();
89         }
90
91         /// <summary>
92         /// Overridden from base
93         /// </summary>
94         protected override bool EvaluateIsValid() {
95             // Check if there were some model exceptions (e.g. OnProductNameChanging throwing)
96             Exception e = ValidationException;
97             if (e != null) {
98                 ErrorMessage = HttpUtility.HtmlEncode(e.Message);
99                 return false;
100             }
101
102             if (Column == null)
103                 return true;
104
105
106             string controlValue = GetControlValidationValue(ControlToValidate);
107             if (controlValue == null) {
108                 // can return null if ControlToValidate is empty, or if the control is not written correctly to return a value
109                 // this does not mean that the value was null
110                 return true;
111             }
112
113             if (Column is MetaForeignKeyColumn || Column is MetaChildrenColumn) {
114                 // do not perform conversion or validation on relationship columns as controlValue is the serialized form
115                 // of a foreign key which would be useless to a validation attribute
116                 return true;
117             }
118
119             // Check if any of our validators want to fail the value
120             controlValue = (string)Column.ConvertEditedValue(controlValue);
121
122             object value;
123             if (!TryConvertControlValue(controlValue, Column.ColumnType, out value)) {
124                 ErrorMessage = HttpUtility.HtmlEncode(DynamicDataResources.DynamicValidator_CannotConvertValue);
125                 return false;
126             }
127
128             return ValueIsValid(value);
129         }
130
131         internal static bool TryConvertControlValue(string controlValue, Type columnType, out object value) {
132             try {
133                 if (controlValue == null) {
134                     value = null;
135                 } else if (columnType == typeof(string)) {
136                     value = controlValue;
137                 } else if (controlValue.Length != 0) {
138                     value = Misc.ChangeType(controlValue, columnType);
139                 } else {
140                     value = null;
141                 }
142                 return true;
143             } catch (Exception) {
144                 value = null;
145                 return false;
146             }
147         }
148
149         private bool ValueIsValid(object value) {
150             // Go through all the model validation attribute to make sure they're valid
151             foreach (var attrib in Column.Attributes.Cast<Attribute>().OfType<ValidationAttribute>()) {
152
153                 // Ignore it if it's found in the ignore list
154                 if (_ignoredModelValidationAttributes != null &&
155                     _ignoredModelValidationAttributes.ContainsKey(attrib.GetType())) {
156                     continue;
157                 }
158
159                 //DynamicValidator can not pass in a ValidationContext as it does
160                 //not have an easy way to get the data object row. Hence we will
161                 //not support attributes that require Validation Context (Ex : CompareAttribute).
162                 if (attrib.RequiresValidationContext) {
163                     continue;
164                 }
165
166                 if (!attrib.IsValid(value)) {
167                     ErrorMessage = HttpUtility.HtmlEncode(StringLocalizerUtil.GetLocalizedString(attrib, Column.DisplayName));
168                     return false;
169                 }
170             }
171
172             return true;
173         }
174
175         internal void SetIgnoredModelValidationAttributes(Dictionary<Type, bool> ignoredModelValidationAttributes) {
176             _ignoredModelValidationAttributes = ignoredModelValidationAttributes;
177         }
178
179         private void OnException(object sender, DynamicValidatorEventArgs e) {
180             ValidateException(e.Exception);
181         }
182
183         /// <summary>
184         /// Overridden from base
185         /// </summary>
186         [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2109:ReviewVisibleEventHandlers")]
187         protected override void OnInit(EventArgs e) {
188             base.OnInit(e);
189
190             // Don't do anything in Design mode
191             if (DesignMode)
192                 return;
193
194             IDynamicDataSource dataSource = DynamicDataSource;
195             if (dataSource != null) {
196                 // Register for datasource exception so that we're called if an error occurs
197                 // during an update/insert/delete
198                 dataSource.Exception += new EventHandler<DynamicValidatorEventArgs>(OnException);
199             }
200         }
201
202         /// <summary>
203         /// Called when an exception happens. Typically, this sets the ValidationException
204         /// </summary>
205         /// <param name="exception">The exception</param>
206         protected virtual void ValidateException(Exception exception) {
207             if (exception == null) {
208                 return;
209             }
210
211             ValidationException = null;
212             
213             // IDynamicValidatorExceptions are used by LinqDataSource to wrap exceptions caused by problems
214             // with setting model properties (columns), such as exceptions thrown from the OnXYZChanging
215             // methods
216             IDynamicValidatorException e = exception as IDynamicValidatorException;
217             if (e != null) {
218                 HandleDynamicValidatorException(e);
219             } else {
220                 // It's not a column specific exception.  e.g. it could be coming from
221                 // OnValidate (DLinq), or could be caused by a database error.
222                 // We only want to use it if it's a ValidationException, otherwise we
223                 // could end up displaying sensitive database errors to the end user
224                 if (Column == null && exception is ValidationException) {
225                     if (exception.InnerException == null) {
226                         ValidationException = exception;
227                     } else {
228                         ValidationException = exception.InnerException;
229                     }
230                 }
231             }
232         }
233
234         private void HandleDynamicValidatorException(IDynamicValidatorException e) {
235             if (Column == null) {
236                 // IDynamicValidatorException only applies to column exceptions
237                 return;
238             }
239
240             List<string> columnNames = GetValidationColumnNames(Column);
241
242             foreach (string name in columnNames) {
243                 // see if the exception wraps any child exceptions relevant to this column
244                 Exception inner;
245                 if (e.InnerExceptions.TryGetValue(name, out inner)) {
246                     // Stop as soon as we find the first exception.
247                     ValidationException = inner;
248                     return;
249                 }
250             }
251         }
252
253         /// <summary>
254         /// Get the names of all the columns that can throw an exception that will affect the setting of the 
255         /// value of the given column.
256         /// </summary>
257         private static List<string> GetValidationColumnNames(MetaColumn column) {
258             List<string> columnNames = new List<string>();
259             columnNames.Add(column.Name); // add it first so that it gets checked first
260             var fkColumn = column as MetaForeignKeyColumn;
261             if (fkColumn != null) {
262                 columnNames.AddRange(fkColumn.ForeignKeyNames);
263             }
264             return columnNames;
265         }
266     }
267 }