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