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