1 // Licensed to the .NET Foundation under one or more agreements.
2 // The .NET Foundation licenses this file to you under the MIT license.
3 // See the LICENSE file in the project root for more information.
4 
5 using System.Collections;
6 using System.DirectoryServices.Interop;
7 
8 namespace System.DirectoryServices
9 {
10     /// <devdoc>
11     /// Holds a collection of values for a multi-valued property.
12     /// </devdoc>
13     public class PropertyValueCollection : CollectionBase
14     {
15         internal enum UpdateType
16         {
17             Add = 0,
18             Delete = 1,
19             Update = 2,
20             None = 3
21         }
22 
23         private readonly DirectoryEntry _entry;
24         private UpdateType _updateType = UpdateType.None;
25         private readonly ArrayList _changeList = null;
26         private readonly bool _allowMultipleChange = false;
27         private readonly bool _needNewBehavior = false;
28 
PropertyValueCollection(DirectoryEntry entry, string propertyName)29         internal PropertyValueCollection(DirectoryEntry entry, string propertyName)
30         {
31             _entry = entry;
32             PropertyName = propertyName;
33             PopulateList();
34             ArrayList tempList = new ArrayList();
35             _changeList = ArrayList.Synchronized(tempList);
36             _allowMultipleChange = entry.allowMultipleChange;
37             string tempPath = entry.Path;
38             if (tempPath == null || tempPath.Length == 0)
39             {
40                 // user does not specify path, so we bind to default naming context using LDAP provider.
41                 _needNewBehavior = true;
42             }
43             else
44             {
45                 if (tempPath.StartsWith("LDAP:", StringComparison.Ordinal))
46                     _needNewBehavior = true;
47             }
48         }
49 
50         public object this[int index]
51         {
52             get => List[index];
53             set
54             {
55                 if (_needNewBehavior && !_allowMultipleChange)
56                     throw new NotSupportedException();
57                 else
58                 {
59                     List[index] = value;
60                 }
61             }
62         }
63 
64         public string PropertyName { get; }
65 
66         public object Value
67         {
68             get
69             {
70                 if (this.Count == 0)
71                     return null;
72                 else if (this.Count == 1)
73                     return List[0];
74                 else
75                 {
76                     object[] objectArray = new object[this.Count];
77                     List.CopyTo(objectArray, 0);
78                     return objectArray;
79                 }
80             }
81 
82             set
83             {
84                 try
85                 {
86                     this.Clear();
87                 }
88                 catch (System.Runtime.InteropServices.COMException e)
89                 {
90                     if (e.ErrorCode != unchecked((int)0x80004005) || (value == null))
91                         // WinNT provider throws E_FAIL when null value is specified though actually ADS_PROPERTY_CLEAR option is used, need to catch exception
92                         // here. But at the same time we don't want to catch the exception if user explicitly sets the value to null.
93                         throw;
94                 }
95 
96                 if (value == null)
97                     return;
98 
99                 // we could not do Clear and Add, we have to bypass the existing collection cache
100                 _changeList.Clear();
101 
102                 if (value is Array)
103                 {
104                     // byte[] is a special case, we will follow what ADSI is doing, it must be an octet string. So treat it as a single valued attribute
105                     if (value is byte[])
106                         _changeList.Add(value);
107                     else if (value is object[])
108                         _changeList.AddRange((object[])value);
109                     else
110                     {
111                         //Need to box value type array elements.
112                         object[] objArray = new object[((Array)value).Length];
113                         ((Array)value).CopyTo(objArray, 0);
114                         _changeList.AddRange((object[])objArray);
115                     }
116                 }
117                 else
118                     _changeList.Add(value);
119 
120                 object[] allValues = new object[_changeList.Count];
121                 _changeList.CopyTo(allValues, 0);
122                 _entry.AdsObject.PutEx((int)AdsPropertyOperation.Update, PropertyName, allValues);
123 
124                 _entry.CommitIfNotCaching();
125 
126                 // populate the new context
127                 PopulateList();
128             }
129         }
130 
131         /// <devdoc>
132         /// Appends the value to the set of values for this property.
133         /// </devdoc>
134         public int Add(object value) => List.Add(value);
135 
136         /// <devdoc>
137         /// Appends the values to the set of values for this property.
138         /// </devdoc>
AddRange(object[] value)139         public void AddRange(object[] value)
140         {
141             if (value == null)
142             {
143                 throw new ArgumentNullException("value");
144             }
145             for (int i = 0; ((i) < (value.Length)); i = ((i) + (1)))
146             {
147                 this.Add(value[i]);
148             }
149         }
150 
151         /// <devdoc>
152         /// Appends the values to the set of values for this property.
153         /// </devdoc>
AddRange(PropertyValueCollection value)154         public void AddRange(PropertyValueCollection value)
155         {
156             if (value == null)
157             {
158                 throw new ArgumentNullException("value");
159             }
160             int currentCount = value.Count;
161             for (int i = 0; i < currentCount; i = ((i) + (1)))
162             {
163                 this.Add(value[i]);
164             }
165         }
166 
167         public bool Contains(object value) => List.Contains(value);
168 
169         /// <devdoc>
170         /// Copies the elements of this instance into an <see cref='System.Array'/>,
171         /// starting at a particular index into the given <paramref name="array"/>.
172         /// </devdoc>
CopyTo(object[] array, int index)173         public void CopyTo(object[] array, int index)
174         {
175             List.CopyTo(array, index);
176         }
177 
178         public int IndexOf(object value) => List.IndexOf(value);
179 
Insert(int index, object value)180         public void Insert(int index, object value) => List.Insert(index, value);
181 
PopulateList()182         private void PopulateList()
183         {
184             //No need to fill the cache here, when GetEx is calles, an implicit
185             //call to GetInfo will be called against an uninitialized property
186             //cache. Which is exactly what FillCache does.
187             //entry.FillCache(propertyName);
188             object var;
189             int unmanagedResult = _entry.AdsObject.GetEx(PropertyName, out var);
190             if (unmanagedResult != 0)
191             {
192                 //  property not found (IIS provider returns 0x80005006, other provides return 0x8000500D).
193                 if ((unmanagedResult == unchecked((int)0x8000500D)) || (unmanagedResult == unchecked((int)0x80005006)))
194                 {
195                     return;
196                 }
197                 else
198                 {
199                     throw COMExceptionHelper.CreateFormattedComException(unmanagedResult);
200                 }
201             }
202             if (var is ICollection)
203                 InnerList.AddRange((ICollection)var);
204             else
205                 InnerList.Add(var);
206         }
207 
208         /// <devdoc>
209         /// Removes the value from the collection.
210         /// </devdoc>
Remove(object value)211         public void Remove(object value)
212         {
213             if (_needNewBehavior)
214             {
215                 try
216                 {
217                     List.Remove(value);
218                 }
219                 catch (ArgumentException)
220                 {
221                     // exception is thrown because value does not exist in the current cache, but it actually might do exist just because it is a very
222                     // large multivalued attribute, the value has not been downloaded yet.
223                     OnRemoveComplete(0, value);
224                 }
225             }
226             else
227                 List.Remove(value);
228         }
229 
OnClearComplete()230         protected override void OnClearComplete()
231         {
232             if (_needNewBehavior && !_allowMultipleChange && _updateType != UpdateType.None && _updateType != UpdateType.Update)
233             {
234                 throw new InvalidOperationException(SR.DSPropertyValueSupportOneOperation);
235             }
236             _entry.AdsObject.PutEx((int)AdsPropertyOperation.Clear, PropertyName, null);
237             _updateType = UpdateType.Update;
238             try
239             {
240                 _entry.CommitIfNotCaching();
241             }
242             catch (System.Runtime.InteropServices.COMException e)
243             {
244                 // On ADSI 2.5 if property has not been assigned any value before,
245                 // then IAds::SetInfo() in CommitIfNotCaching returns bad HREsult 0x8007200A, which we ignore.
246                 if (e.ErrorCode != unchecked((int)0x8007200A))    //  ERROR_DS_NO_ATTRIBUTE_OR_VALUE
247                     throw;
248             }
249         }
250 
OnInsertComplete(int index, object value)251         protected override void OnInsertComplete(int index, object value)
252         {
253             if (_needNewBehavior)
254             {
255                 if (!_allowMultipleChange)
256                 {
257                     if (_updateType != UpdateType.None && _updateType != UpdateType.Add)
258                     {
259                         throw new InvalidOperationException(SR.DSPropertyValueSupportOneOperation);
260                     }
261 
262                     _changeList.Add(value);
263 
264                     object[] allValues = new object[_changeList.Count];
265                     _changeList.CopyTo(allValues, 0);
266                     _entry.AdsObject.PutEx((int)AdsPropertyOperation.Append, PropertyName, allValues);
267 
268                     _updateType = UpdateType.Add;
269                 }
270                 else
271                 {
272                     _entry.AdsObject.PutEx((int)AdsPropertyOperation.Append, PropertyName, new object[] { value });
273                 }
274             }
275             else
276             {
277                 object[] allValues = new object[InnerList.Count];
278                 InnerList.CopyTo(allValues, 0);
279                 _entry.AdsObject.PutEx((int)AdsPropertyOperation.Update, PropertyName, allValues);
280             }
281             _entry.CommitIfNotCaching();
282         }
283 
OnRemoveComplete(int index, object value)284         protected override void OnRemoveComplete(int index, object value)
285         {
286             if (_needNewBehavior)
287             {
288                 if (!_allowMultipleChange)
289                 {
290                     if (_updateType != UpdateType.None && _updateType != UpdateType.Delete)
291                     {
292                         throw new InvalidOperationException(SR.DSPropertyValueSupportOneOperation);
293                     }
294 
295                     _changeList.Add(value);
296                     object[] allValues = new object[_changeList.Count];
297                     _changeList.CopyTo(allValues, 0);
298                     _entry.AdsObject.PutEx((int)AdsPropertyOperation.Delete, PropertyName, allValues);
299 
300                     _updateType = UpdateType.Delete;
301                 }
302                 else
303                 {
304                     _entry.AdsObject.PutEx((int)AdsPropertyOperation.Delete, PropertyName, new object[] { value });
305                 }
306             }
307             else
308             {
309                 object[] allValues = new object[InnerList.Count];
310                 InnerList.CopyTo(allValues, 0);
311                 _entry.AdsObject.PutEx((int)AdsPropertyOperation.Update, PropertyName, allValues);
312             }
313 
314             _entry.CommitIfNotCaching();
315         }
316 
OnSetComplete(int index, object oldValue, object newValue)317         protected override void OnSetComplete(int index, object oldValue, object newValue)
318         {
319             // no need to consider the not allowing accumulative change case as it does not support Set
320             if (Count <= 1)
321             {
322                 _entry.AdsObject.Put(PropertyName, newValue);
323             }
324             else
325             {
326                 if (_needNewBehavior)
327                 {
328                     _entry.AdsObject.PutEx((int)AdsPropertyOperation.Delete, PropertyName, new object[] { oldValue });
329                     _entry.AdsObject.PutEx((int)AdsPropertyOperation.Append, PropertyName, new object[] { newValue });
330                 }
331                 else
332                 {
333                     object[] allValues = new object[InnerList.Count];
334                     InnerList.CopyTo(allValues, 0);
335                     _entry.AdsObject.PutEx((int)AdsPropertyOperation.Update, PropertyName, allValues);
336                 }
337             }
338 
339             _entry.CommitIfNotCaching();
340         }
341     }
342 }
343