| File: MS\Internal\Documents\PeoplePickerWrapper.cs | Web Access |
| Project: src\wpf\src\Microsoft.DotNet.Wpf\src\PresentationUI\PresentationUI.csproj (PresentationUI) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // Description: PeoplePickerWrapper provides a managed wrapper around // the unmanaged ActiveDirectory ICommonQuery COM object. using System; using System.Collections.Generic; using System.DirectoryServices; using System.Globalization; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.InteropServices.ComTypes; using System.Windows.TrustUI; namespace MS.Internal.Documents { /// <summary> /// PeoplePickerWrapper provides a managed wrapper around /// the unmanaged ActiveDirectory ICommonQuery COM object /// </summary> internal partial class PeoplePickerWrapper { //------------------------------------------------------ // // Constructors // //------------------------------------------------------ #region Constructors /// <summary> /// Constructs a new PeoplePickerWrapper object. /// </summary> internal PeoplePickerWrapper() { } #endregion Constructors //------------------------------------------------------ // // Internal Methods // //------------------------------------------------------ #region Internal Methods /// <summary> /// Invokes the "People Picker" dialog and returns a list of strings /// representing the email address of the users and groups selected therein. /// </summary> /// <param name="hWndParent">The parent window for this dialog. If null, window will /// be shown non-modally.</param> internal String[] Show(IntPtr hWndParent) { ValidateHWnd(hWndParent); IDataObject data = OpenQueryWindow(hWndParent); //If the data returned from OpenQueryWindow is null, //it means no data was entered in the dialog so we will //return null. Otherwise we need to extract the data //from the returned IDataObject object. if (data != null) { //Get a MemoryStream that contains the data contained in the //IDataObject (which is a raw form of a DsObjects struct). System.Windows.DataObject dataObject = new System.Windows.DataObject(data); System.IO.MemoryStream dsObjectStream = dataObject.GetData( UnsafeNativeMethods.CFSTR_DSOBJECTNAMES) as System.IO.MemoryStream; //Extract the data from that memory stream. These will come back as //ActiveDirectory paths in the form 'LDAP://CN=...' String[] ldapPaths = Array.Empty<String>(); //Get a wrapper for the DsObjectNames object our pointer points to. DsObjectNamesWrapper dsObjects = new DsObjectNamesWrapper(dsObjectStream); try { //Get the names from the DsObjectNamesWrapper (these are AD paths) ldapPaths = dsObjects.Names; } finally { dsObjects.Dispose(); dsObjectStream.Close(); } //Get a set of e-mail addresses from the paths using AD and return them. return GetEmailAddressesFromPaths(ldapPaths); } else { return Array.Empty<String>(); } } #endregion Internal Methods //------------------------------------------------------ // // Private Methods // //------------------------------------------------------ #region Private Methods /// <summary> /// Instantiates an ICommonQuery COM object and invokes OpenQueryWindow on it /// with the necessary parameters to bring up the "people picker" portion of /// the dialog. /// </summary> /// <returns></returns> /// <param name="hWndParent">The HWND for the ICommonQuery.OpenQueryWindow call which /// defines the parent of the dialog.</param> private IDataObject OpenQueryWindow(IntPtr hWndParent) { Type commonQueryType = Type.GetTypeFromCLSID(UnsafeNativeMethods.CLSID_CommonQuery); //Get an instance of the ICommonQuery COM object. UnsafeNativeMethods.ICommonQuery commonQueryInstance = Activator.CreateInstance(commonQueryType) as UnsafeNativeMethods.ICommonQuery; Invariant.Assert(commonQueryInstance != null, "Unable to create an instance of ICommonQuery."); //Set up the QueryInitParams -- this is essentially empty as we require no special flags, //default usernames, passwords or server information for our purposes. UnsafeNativeMethods.QueryInitParams queryInitParams = new UnsafeNativeMethods.QueryInitParams { cbStruct = (uint)Marshal.SizeOf(typeof(UnsafeNativeMethods.QueryInitParams)), dwFlags = 0, pDefaultScope = null, pDefaultSaveLocation = null, pUserName = null, pPassword = null, pServer = null }; //Allocate memory for our QueryInitParams structure that will be used in the //OpenQueryWindowParams structure. IntPtr queryInitParamsPtr = Marshal.AllocCoTaskMem(Marshal.SizeOf(queryInitParams)); //Now try to invoke the OpenQueryWindow method uint hresult = UnsafeNativeMethods.E_FAIL; IDataObject data = null; try { //Stuff the queryInitParams into a pointer (that we can assign to //OpenQueryWindowParams.pHandlerParameters below). Marshal.StructureToPtr(queryInitParams, queryInitParamsPtr, false /*fDeleteOld*/); //Set up the OpenQueryWindowParams. //We require the default form with: // - The specified "Find Users" default form // - "OK and Cancel" buttons shown // - Options enabled // - No "Find:" dropdown (for things other than users) // - No menus // - Single item selection UnsafeNativeMethods.OpenQueryWindowParams openQueryWindowParams = new UnsafeNativeMethods.OpenQueryWindowParams { cbStruct = (uint)Marshal.SizeOf(typeof(UnsafeNativeMethods.OpenQueryWindowParams)), dwFlags = UnsafeNativeMethods.OQWF_DEFAULTFORM | UnsafeNativeMethods.OQWF_OKCANCEL | UnsafeNativeMethods.OQWF_SHOWOPTIONAL | UnsafeNativeMethods.OQWF_REMOVEFORMS | UnsafeNativeMethods.OQWF_HIDEMENUS, clsidHandler = UnsafeNativeMethods.CLSID_DsQuery, pHandlerParameters = queryInitParamsPtr, clsidDefaultForm = UnsafeNativeMethods.CLSID_DsFindPeople, //Bring up the people picker pPersistQuery = IntPtr.Zero, //We aren't persisting this query anywhere pFormParameters = IntPtr.Zero //We aren't pre-populating the form }; //Invoke the OpenQueryWindow method on the ICommonQuery object, //which will invoke the dialog and return any entered data in the //"data" field. //OpenQueryWindow will not return until the dialog is closed. hresult = commonQueryInstance.OpenQueryWindow(hWndParent, ref openQueryWindowParams, out data); } finally { //Free the memory used for our QueryInitParams structure. Marshal.FreeCoTaskMem(queryInitParamsPtr); commonQueryInstance = null; } if (hresult == UnsafeNativeMethods.S_OK) { //The user pressed "OK," so we can return the selected data return data; } else if (hresult == UnsafeNativeMethods.S_FALSE) { //The user canceled out, so we just return null. return null; } else { //An error condition was reported, we throw an exception with the //hresult included. throw new InvalidOperationException( String.Format(CultureInfo.CurrentCulture, SR.PeoplePickerErrorConditionFromOpenQueryWindow, hresult)); } } /// <summary> /// Turns a set of AD paths (in the form 'LDAP://CN=...') into a set of /// e-mail addresses by looking up the paths in the Directory and retrieving /// the 'mail' property, if it exists for the given AD object. /// </summary> /// <param name="paths"></param> /// <returns></returns> private String[] GetEmailAddressesFromPaths(String[] paths) { List<String> addresses = new List<String>(paths.Length); for (int i = 0; i < paths.Length; i++) { PropertyValueCollection emailCollection = null; DirectoryEntry directoryEntry = null; //Create a DirectoryEntry pointing to the current path; //Attempt to retrieve the "mail" field. directoryEntry = new DirectoryEntry(paths[i]); emailCollection = directoryEntry.Properties[_adEmailAddressKey]; if (emailCollection != null && emailCollection.Count > 0) { //We have a non-empty e-mail collection; we will add the //first available e-mail address. String address = emailCollection[0] as String; if (address != null) { addresses.Add(address); } } } return addresses.ToArray(); } /// <summary> /// Verifies that the given parent HWND is either null or an RMPublishingDialog /// Windows Form. /// </summary> /// <param name="hWndParent"></param> /// private void ValidateHWnd(IntPtr hWndParent) { if( hWndParent != IntPtr.Zero ) { System.Windows.Forms.Control rmPublishingDialog = System.Windows.Forms.Control.FromHandle(hWndParent) as RMPublishingDialog; if (rmPublishingDialog == null) { throw new InvalidOperationException(SR.PeoplePickerInvalidParentWindow); } } } #endregion Private Methods //------------------------------------------------------ // // Private Fields // //------------------------------------------------------ #region Private Fields //The key name for the ActiveDirectory E-Mail address property private const String _adEmailAddressKey = "mail"; #region DsObjectNamesWrapper Class /// <summary> /// The DsObjectNamesWrapper class wraps a DsObjectNames Struct thus hiding /// the complexity of the unmanaged->managed mangling that's necessary to /// retrieve the object names we need therein. /// /// This is used in PeoplePickerWrapper.ParseDataFromHandle to simplify the code. /// </summary> private class DsObjectNamesWrapper : IDisposable { /// <summary> /// Static constructor for DsObjectNamesWrapper. /// </summary> static DsObjectNamesWrapper() { // Do not remove! (See above Security comment) } /// <summary> /// Constructs a new DsObjectNamesWrapper given a MemoryStream containing /// a raw representation of a DsObjectNames object. /// </summary> /// <param name="ptrToDsObjectNames">A pointer to a valid DsObjectNames struct</param> internal DsObjectNamesWrapper(System.IO.MemoryStream dataStream) { ArgumentNullException.ThrowIfNull(dataStream); //We need to get a pointer to this data for our DsObjectNamesWrapper //to wrap. //First we convert the stream to an array of bytes. byte[] data = dataStream.ToArray(); //Allocate memory to store an unmanaged copy of this data _ptrToDsObjectNames = Marshal.AllocHGlobal(data.Length); Invariant.Assert(_ptrToDsObjectNames != IntPtr.Zero, "Invalid pointer to DsObjectNames data."); //Marshal the data over to the unmanaged side Marshal.Copy(data, 0, _ptrToDsObjectNames, data.Length); //Get a DsObjectNames structure out of the pointer we //were handed. _dsObjectNames = Marshal.PtrToStructure<UnsafeNativeMethods.DsObjectNames>(_ptrToDsObjectNames); } /// <summary> /// Finalizer for DsObjectNamesWrapper, ensures that unmanaged resources are properly /// cleaned up. /// </summary> ~DsObjectNamesWrapper() { this.Dispose(); } /// <summary> /// The number of names in the DsObjectNames struct this class is wrapping. /// </summary> internal uint Count { get { ThrowIfDisposed(); return _dsObjectNames.cItems; } } /// <summary> /// An array of names contained within the DsObjectNames struct. /// </summary> internal String[] Names { get { ThrowIfDisposed(); if (_names == null) { _names = GetNamesFromDsObjectStruct(); } return _names; } } /// <summary> /// Gets the list of names from the struct. /// </summary> /// </summary> /// <returns></returns> private String[] GetNamesFromDsObjectStruct() { ThrowIfDisposed(); String[] names = new string[Count]; for (int i = 0; i < Count; i++) { names[i] = BuildStringForItemName(i); } return names; } /// <summary> /// Extracts the name for the specified entry number from the DsObjectNames data /// </summary> /// <param name="index">The index of the name to retrieve</param> /// <returns></returns> private String BuildStringForItemName(int index) { ThrowIfDisposed(); //Ensure we're within proper bounds. if (index < 0 || index >= Count) { throw new ArgumentOutOfRangeException(nameof(index)); } //First we have to get a DsObject out of the array (aObjects) of DsObjects. //The information in this struct will tell us where to find the name data //we want. UnsafeNativeMethods.DsObject dsObject = GetDsObjectForIndex(index); //The offset of the name from the start of the DsObjectNames structure is stored in //the corresponding DsObject's offsetName field, so the pointer to the beginning of the //string can be computed as: // // offset = StartOfStructAddress + dsObject.offsetName; IntPtr nameOffset = new IntPtr(_ptrToDsObjectNames.ToInt64() + dsObject.offsetName); //We marshal this pointer to a string and return it. return Marshal.PtrToStringAuto(nameOffset); } /// <summary> /// Retrieves a DsObject structure from the "array" in our DsObjectNames structure. /// Because marshalling cannot properly marshal a nested, variable sized /// array in a struct, we have to do this the hard way. /// That is, the "array" of DsObjects that's in our managed DsObjectNames structure /// definition is merely a pointer to a big chunk of data that we have to manually /// retrieve DsObjects from by manipulating pointers, rather than doing aObjects[i]. /// </summary> /// <param name="index">The index of the DsObject to retrieve</param> /// <returns></returns> private UnsafeNativeMethods.DsObject GetDsObjectForIndex(int index) { ThrowIfDisposed(); //Ensure we're within proper bounds. if (index < 0 || index >= Count) { throw new ArgumentOutOfRangeException(nameof(index)); } //Now we calculate the offset of the specified array index. //This is the address of the first element in the array, plus //the number of entries past that times the size of an entry, or: //offset = StartOfStructAddress + FirstArrayEntryOffset + index * SizeOfEntry IntPtr offset = new IntPtr(_ptrToDsObjectNames.ToInt64() + _dsObjectArrayFieldOffset + index * _sizeOfDsObject); //Marshal that to a DsObject structure. UnsafeNativeMethods.DsObject dsObject = Marshal.PtrToStructure<UnsafeNativeMethods.DsObject>(offset); return dsObject; } /// <summary> /// Implemented to deal with FxCop rule UseSafeHandleToEncapsulateNativeResources /// We can now assert that the code will not allow methods to be called after /// we are disposed. /// </summary> private void ThrowIfDisposed() { ObjectDisposedException.ThrowIf(_isDisposed, typeof(DsObjectNamesWrapper)); } /// <summary> /// Cleans up the unmanaged memory allocations made by this object. /// </summary> public void Dispose() { lock (this) { if (!_isDisposed) { Marshal.FreeHGlobal(_ptrToDsObjectNames); _isDisposed = true; GC.SuppressFinalize(this); } } } #region Private Fields //------------------------------------------------------ // // Private Fields // //------------------------------------------------------ /// <summary> /// Pointer to the DsObjectNames struct we're wrapping. /// We use this to calculate pointers to field offsets. /// </summary> private IntPtr _ptrToDsObjectNames; /// <summary> /// The list of names in the DsObjectNames struct /// </summary> private String[] _names; /// <summary> /// The DsObjectNames struct we're wrapping. /// </summary> private UnsafeNativeMethods.DsObjectNames _dsObjectNames; private bool _isDisposed; // Useful constants: /// <summary> /// The offset from the start of a DsObjectNames structure to the /// DsObjects array. /// </summary> private static readonly int _dsObjectArrayFieldOffset = Unsafe.SizeOf<Guid>() + sizeof(UInt32); /// <summary> /// The size of a DsObject. /// </summary> private static readonly int _sizeOfDsObject = Marshal.SizeOf(typeof(UnsafeNativeMethods.DsObject)); #endregion Private Fields } #endregion DsObjectNamesWrapper Class #endregion Private Fields } }