File: MS\Internal\Documents\PeoplePickerWrapper.cs
Web Access
Project: src\src\Microsoft.DotNet.Wpf\src\PresentationUI\PresentationUI_wgljbex5_wpftmp.csproj (PresentationUI)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
// 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();
            queryInitParams.cbStruct = 
                (uint)Marshal.SizeOf(typeof(UnsafeNativeMethods.QueryInitParams));
            queryInitParams.dwFlags = 0;
            queryInitParams.pDefaultScope = null;
            queryInitParams.pDefaultSaveLocation = null;
            queryInitParams.pUserName = null;
            queryInitParams.pPassword = null;
            queryInitParams.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();
                openQueryWindowParams.cbStruct = 
                    (uint)Marshal.SizeOf(typeof(UnsafeNativeMethods.OpenQueryWindowParams));
                openQueryWindowParams.dwFlags = UnsafeNativeMethods.OQWF_DEFAULTFORM | 
                                                UnsafeNativeMethods.OQWF_OKCANCEL | 
                                                UnsafeNativeMethods.OQWF_SHOWOPTIONAL |
                                                UnsafeNativeMethods.OQWF_REMOVEFORMS |
                                                UnsafeNativeMethods.OQWF_HIDEMENUS;
                openQueryWindowParams.clsidHandler = UnsafeNativeMethods.CLSID_DsQuery;
                openQueryWindowParams.pHandlerParameters = queryInitParamsPtr; 
                openQueryWindowParams.clsidDefaultForm = 
                    UnsafeNativeMethods.CLSID_DsFindPeople; //Bring up the people picker
                openQueryWindowParams.pPersistQuery = IntPtr.Zero;      //We aren't persisting this query anywhere
                openQueryWindowParams.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("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("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
    }
}