Using WH_CBT hooks out of the local process thread...

0

I have an application that saves HTML from an IE windows using features of Interop.ShDocVw.dll. I have a need to save a PDF file, but the output goes to an iframe element which is not exposed in the interop. I found that I can trigger the save as option in IE using the IWebBrowser2.ExecWB(). I was thinking that it should be possible (maybe) to put a hook in the browser thread that would allow me to change the file name and path and click the Save button. While I can put a WH_CBT hook into the local process, the hook appears either NOT to be in the IE process, or I am getting a hook into something other than the Save As dialog. I am not sure how to check exactly what process I am in when I get a hook, that might help identify what I am doing wrong.

What I am seeing is that my CBTProc method does not fire a HCBT_CREATEWND code when I put the WH_CBT hook in. I do get HCBT_ACTIVATE codes firing in the hook, but without trapping the wParam on the HCBT_CREATEWND, I have no way to know that the window has been created so that I can try to SendMsg for the text change and button click.

What I am thinking is that you cannot do this due to the way windows security works, but I am not familiar with the way of the (Windows) force by any stretch of the imagination, so I don't really know. I know we can get at and save HTML, these PDF files have been quite elusive. It would seem that it should be possible considering what I do with HTML, but I need some guidance to determine this. It sure looks like it should work. My C# code provided should anyone care to point out something I am overlooking, or to tell me that it can't be done... (Hopefully not the latter.)

// This class is compiled into it's own DLL 
public class CIeSaveAs
{
    #region Unmanaged Hook Methods

    [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
    private static extern int SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hInstance, int threadId);

    [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
    private static extern bool UnhookWindowsHookEx(int idHook);

    [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
    private static extern int CallNextHookEx(int idHook, int nCode, IntPtr wParam, IntPtr lParam);

    #endregion

    #region Unmanaged calls to update Dialog

    [DllImport("user32.dll", CharSet = CharSet.Auto)]
    private static extern IntPtr SendMessage(IntPtr hWnd, int msg, int wParam, int lParam);

    [DllImport("user32.dll", CharSet = CharSet.Auto)]
    private static extern IntPtr GetDlgItem(IntPtr hWnd, int nIDDlgItem);

    [DllImport("user32.dll", CharSet = CharSet.Auto)]
    private static extern bool SetWindowText(IntPtr hWnd, string lpString);

    [DllImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool IsWindowVisible(IntPtr hWnd);

    #endregion

    [DllImport("kernel32.dll")]
    private static extern uint GetCurrentThreadId();

    #region Win32 structures requred to handle windows messages/state

    [StructLayout(LayoutKind.Sequential)]
    public struct CBT_CREATEWND
    {
        public IntPtr lpcs;
        int hwndInsertAfter;
    };
    [StructLayout(LayoutKind.Sequential)]
    public struct CREATESTRUCT
    {
        int lpCreateParams;
        int hInstance;
        int hMenu;
        int hwndParent;
        int cy;
        public int cx;
        int y;
        public int x;
        int style;
        int lpszName;
        public int lpszClass;
        int dwExStyle;
    }

    #endregion

    public delegate int HookProc(int nCode, IntPtr wParam, IntPtr lParam);

    private IntPtr _hwndSaveAsDlg;
    private string _filePath;

    private HookProc _hookProcedure;
    private int _hook = 0;

    //The public interface to CIeSaveAs
    public void SavePdf(IWebBrowser2 browser, string filePath)
    {
        _filePath = filePath;

        _hookProcedure = new HookProc(SaveAsHookProc);

        _hook = SetWindowsHookEx(5 /*WH_CBT*/, _hookProcedure, (IntPtr)0, (int)GetCurrentThreadId());

        if (_hook == 0 || browser == null) return;
        try
        {
            // open the browser Save As dialog
            browser.ExecWB(OLECMDID.OLECMDID_SAVEAS, OLECMDEXECOPT.OLECMDEXECOPT_DONTPROMPTUSER);
        }
        finally
        {
            UnhookWindowsHookEx(_hook);
        }
    }


    // The CBTProc method
    private int SaveAsHookProc(int nCode, IntPtr wParam, IntPtr lParam)
    {
        //Debug.WriteLine( string.Format( "nCode = {0}", nCode ) );
        switch (nCode)
        {
            case 3: // HCBT_CREATEWND
                CBT_CREATEWND cw = (CBT_CREATEWND)Marshal.PtrToStructure(lParam, typeof(CBT_CREATEWND));
                CREATESTRUCT cs = (CREATESTRUCT)Marshal.PtrToStructure(cw.lpcs, typeof(CREATESTRUCT));
                if (cs.lpszClass == 0x00008002)
                {
                    _hwndSaveAsDlg = (IntPtr)wParam; // Get hwnd of SaveAs dialog
                    cs.x = -2 * cs.cx; // Move dialog off screen
                }
                break;
            case 5: // HCBT_ACTIVATE
                IntPtr hwnd = (IntPtr)wParam;

                if (hwnd == _hwndSaveAsDlg && _hwndSaveAsDlg != (IntPtr)0)
                {
                    //Prepare a thread to act as required messages source.Also set File path and its save type.
                    var tpok = new ThreadToPressOk(hwnd, _filePath);

                    _hwndSaveAsDlg = (IntPtr)0;

                    new Thread(tpok.ThreadProc).Start();
                }
                break;
        }
        //This is required to pass control to next hook if exists on this process.
        return CallNextHookEx(_hook, nCode, wParam, lParam);
    }

    public class ThreadToPressOk
    {
        private IntPtr _hwndDialog;
        private string _filePath;
        private EnumBrowserFileSaveType _fileSaveType;

        public ThreadToPressOk(IntPtr hwnd, string filePath)
        {
            _hwndDialog = hwnd;
            _filePath = filePath;
            _fileSaveType = EnumBrowserFileSaveType.SAVETYPE_ARCHIVE;
        }


        public void ThreadProc()
        {
            //To avoid race condition, we are forcing this thread to wait until Saveas dialog is displayed.
            while (!IsWindowVisible(_hwndDialog))
            {
                Thread.Sleep(100);
                Application.DoEvents();
            }

            Application.DoEvents();

            //Get the handle to file path on the saveas dialog.
            IntPtr nameB = GetDlgItem(_hwndDialog, 0x047c);
            //Get the handle to saveas on the saveas dialog.
            IntPtr saveBtn = GetDlgItem(_hwndDialog, 0x0001);

            if (
                ((IntPtr)0 != nameB) &&
                ((IntPtr)0 != saveBtn) &&
                IsWindowVisible(_hwndDialog))
            {

                SetWindowText(nameB, _filePath);

                SendMessage(saveBtn, 0x00F5 /*BM_CLICK*/, 0, 0);
            }

            // Clean up GUI - we have clicked save button.
            //GC is going to do that cleanup job, so we are OK
            Application.DoEvents();

            //Terminate the thread.
            return;
        }
    }
}

I made a console app that gets a browser window with ShDocVw.dll that contains a PDF file, then call the above class from that app...

    class Program
{
    static void Main(string[] args)
    {

        Console.WriteLine("Creating new CIeSaveAs class...");
        var saveAs = new CIeSaveAs();
        Console.WriteLine("Invoke SavePdf...");

        var filePath = @"C:\test\a\b\test.pdf";

        InternetExplorer browser = null;
        var shell = new ShellWindows();
        foreach (InternetExplorer ie in shell)
        {
            var file = Path.GetFileNameWithoutExtension(ie.FullName).ToLower();
            if (file.Equals("iexplore"))
            {
                HTMLDocument document = ie.Document as HTMLDocument;
                if (document != null)
                {
                    var title = GetPropertyValue(document, "title").ToString();
                    if (title.StartsWith("CM/ECF"))
                    {
                        browser = ie;
                        break;
                    }
                }
            }
        }

        saveAs.SavePdf(browser, filePath);

        Console.WriteLine();
        Console.WriteLine("Press Enter to exit...");
        Console.ReadKey();

    }

    private static object GetPropertyValue(object obj, string propertyName)
    {
        int dispId = 0;
        DispatchUtility.TryGetDispId(obj, propertyName, out dispId);

        if (dispId > 0)
            return DispatchUtility.Invoke(obj, dispId, null);

        return null;
    }
}

Ok, here's the missing code that makes it compile...

   public static class DispatchUtility
{
    private const int S_OK = 0; //From WinError.h
    private const int LOCALE_SYSTEM_DEFAULT = 2 << 10; //From WinNT.h == 2048 == 0x800

    public static bool ImplementsIDispatch(object obj)
    {
        bool result = obj is IDispatchInfo;
        return result;
    }

    public static Type GetType(object obj, bool throwIfNotFound)
    {
        RequireReference(obj, "obj");
        Type result = GetType((IDispatchInfo)obj, throwIfNotFound);
        return result;
    }

    public static bool TryGetDispId(object obj, string name, out int dispId)
    {
        RequireReference(obj, "obj");
        bool result = TryGetDispId((IDispatchInfo)obj, name, out dispId);
        return result;
    }

    public static object Invoke(object obj, int dispId, object[] args)
    {
        string memberName = "[DispId=" + dispId + "]";
        object result = Invoke(obj, memberName, args);
        return result;
    }

    public static object Invoke(object obj, string memberName, object[] args)
    {
        RequireReference(obj, "obj");
        Type type = obj.GetType();
        object result = type.InvokeMember(memberName,
            BindingFlags.InvokeMethod | BindingFlags.GetProperty,
            null, obj, args, null);
        return result;
    }

    private static void RequireReference<T>(T value, string name) where T : class
    {
        if (value == null)
        {
            throw new ArgumentNullException(name);
        }
    }

    private static Type GetType(IDispatchInfo dispatch, bool throwIfNotFound)
    {
        RequireReference(dispatch, "dispatch");

        Type result = null;
        int typeInfoCount;
        int hr = dispatch.GetTypeInfoCount(out typeInfoCount);
        if (hr == S_OK && typeInfoCount > 0)
        {
            dispatch.GetTypeInfo(0, LOCALE_SYSTEM_DEFAULT, out result);
        }

        if (result == null && throwIfNotFound)
        {
            // If the GetTypeInfoCount called failed, throw an exception for that.
            Marshal.ThrowExceptionForHR(hr);

            // Otherwise, throw the same exception that Type.GetType would throw.
            throw new TypeLoadException();
        }

        return result;
    }

    private static bool TryGetDispId(IDispatchInfo dispatch, string name, out int dispId)
    {
        RequireReference(dispatch, "dispatch");
        RequireReference(name, "name");

        bool result = false;

        Guid iidNull = Guid.Empty;
        int hr = dispatch.GetDispId(ref iidNull, ref name, 1, LOCALE_SYSTEM_DEFAULT, out dispId);

        const int DISP_E_UNKNOWNNAME = unchecked((int)0x80020006); //From WinError.h
        const int DISPID_UNKNOWN = -1; //From OAIdl.idl
        if (hr == S_OK)
        {
            result = true;
        }
        else if (hr == DISP_E_UNKNOWNNAME && dispId == DISPID_UNKNOWN)
        {
            result = false;
        }
        else
        {
            Marshal.ThrowExceptionForHR(hr);
        }

        return result;
    }

    [ComImport]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    [Guid("00020400-0000-0000-C000-000000000046")]
    private interface IDispatchInfo
    {
        [PreserveSig]
        int GetTypeInfoCount(out int typeInfoCount);

        void GetTypeInfo(int typeInfoIndex, int lcid, [MarshalAs(UnmanagedType.CustomMarshaler,
            MarshalTypeRef = typeof(System.Runtime.InteropServices.CustomMarshalers.TypeToTypeInfoMarshaler))] out Type typeInfo);

        [PreserveSig]
        int GetDispId(ref Guid riid, ref string name, int nameCount, int lcid, out int dispId);

        // NOTE: The real IDispatch also has an Invoke method next, but we don't need it.
    }
}

Any insight would be much appreciated,

Kent

c#
setwindowshookex
asked on Stack Overflow Nov 2, 2017 by Kent

0 Answers

Nobody has answered this question yet.


User contributions licensed under CC BY-SA 3.0