Add browser action button in internet explorer BHO
bho, browser-action, c#, internet-explorer, winapi
Solution
EDIT: https://github.com/somanuell/SoBrowserAction
Here is a screen shot of my work in progress.
The things I did:
1. Escaping from the protected mode
The BHO Registration must update the `HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Internet Explorer\Low Rights\ElevationPolicy` key. See Understanding and Working in Protected Mode Internet Explorer.
I choose the process way because it's noted as "best practice" and is easier to debug, but the `RunDll32Policy` may do the trick, too.
Locate the `rgs` file containing your BHO registry settings. It's the one containing the upadte to the Registry Key `'Browser Helper Object'`. Add to that file the following:
HKLM {
NoRemove SOFTWARE {
NoRemove Microsoft {
NoRemove 'Internet Explorer' {
NoRemove 'Low Rights' {
NoRemove ElevationPolicy {
ForceRemove '{AE6E5BFE-B965-41B5-AC70-D7069E555C76}' {
val AppName = s 'SoBrowserActionInjector.exe'
val AppPath = s '%MODULEPATH%'
val Policy = d '3'
}
}
}
}
}
}
}
The GUID must be a new one, don't use mine, use a GUID Generator. The `3` value for policy ensures that the broker process will be launched as a medium integrity process. The `%MODULEPATH%`macro is NOT a predefined one.
Why use a macro? You may avoid that new code in your RGS file, provided that your MSI contains that update to the registry. As dealing with MSI may be painful, it's often easier to provide a "full self registering" package. But if you don't use a macro, you then can't allow the user to choose the installation directory. Using a macro permits to dynamically update the registry with the correct installation directory.
How to make the macro works? Locate the `DECLARE_REGISTRY_RESOURCEID` macro in the header of your BHO class and comment it out. Add the following function definition in that header:
static HRESULT WINAPI UpdateRegistry( BOOL bRegister ) throw() {
ATL::_ATL_REGMAP_ENTRY regMapEntries[2];
memset( ®MapEntries[1], 0, sizeof(ATL::_ATL_REGMAP_ENTRY));
regMapEntries[0].szKey = L"MODULEPATH";
regMapEntries[0].szData = sm_szModulePath;
return ATL::_pAtlModule->UpdateRegistryFromResource(IDR_CSOBABHO, bRegister,
regMapEntries);
}
That code is borrowed from the ATL implementation for `DECLARE_REGISTRY_RESOURCEID` (in my case it's the one shipped with VS2010, check your version of ATL and update code if necessary). The `IDR_CSOBABHO` macro is the resource ID of the `REGISTRY` resource adding the RGS in your RC file.
The `sm_szModulePath` variable must contains the installation path of the broker process EXE. I choose to make it a public static member variable of my BHO class. One simple way to set it up is in the `DllMain` function. When `regsvr32` load your Dll, `DllMain` is called, and the registry is updated with the good path.
extern "C" BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved) {
if ( dwReason == DLL_PROCESS_ATTACH ) {
DWORD dwCopied = GetModuleFileName( hInstance,
CCSoBABHO::sm_szModulePath,
sizeof( CCSoBABHO::sm_szModulePath ) /
sizeof( wchar_t ) );
if ( dwCopied ) {
wchar_t * pLastAntiSlash = wcsrchr( CCSoBABHO::sm_szModulePath, L'\\' );
if ( pLastAntiSlash ) *( pLastAntiSlash ) = 0;
}
}
return _AtlModule.DllMain(dwReason, lpReserved);
}
Many thanks to Mladen Janković.
How to lauch the Broker process?
One possible place is in the `SetSite` implementation. It will be lauched many times, but we will deal with that in the process itself. We will see later that the broker process may benefit from receiving as argument the HWND for the hosting IEFrame. This can be done with the `IWebBrowser2::get_HWND` method. I suppose here that your already have an `IWebBrowser2*` member.
STDMETHODIMP CCSoBABHO::SetSite( IUnknown* pUnkSite ) {
if ( pUnkSite ) {
HRESULT hr = pUnkSite->QueryInterface( IID_IWebBrowser2, (void**)&m_spIWebBrowser2 );
if ( SUCCEEDED( hr ) && m_spIWebBrowser2 ) {
SHANDLE_PTR hWndIEFrame;
hr = m_spIWebBrowser2->get_HWND( &hWndIEFrame );
if ( SUCCEEDED( hr ) ) {
wchar_t szExeName[] = L"SoBrowserActionInjector.exe";
wchar_t szFullPath[ MAX_PATH ];
wcscpy_s( szFullPath, sm_szModulePath );
wcscat_s( szFullPath, L"\\" );
wcscat_s( szFullPath, szExeName );
STARTUPINFO si;
memset( &si, 0, sizeof( si ) );
si.cb = sizeof( si );
PROCESS_INFORMATION pi;
wchar_t szCommandLine[ 64 ];
swprintf_s( szCommandLine, L"%.48s %d", szExeName, (int)hWndIEFrame );
BOOL bWin32Success = CreateProcess( szFullPath, szCommandLine, NULL,
NULL, FALSE, 0, NULL, NULL, &si, &pi );
if ( bWin32Success ) {
CloseHandle( pi.hThread );
CloseHandle( pi.hProcess );
}
}
}
[...]
2. Injecting the IEFrame threads
It appears that this may be the most complex part, because there are many ways to do it, each one with pros and cons.
The broker process, the "injector", may be a short lived one, with one simple argument (a HWND or a TID), which will have to deal with a unique IEFrame, if not already processed by a previous instance.
Rather, the "injector" may be a long lived, eventually never ending, process which will have to continually watch the Desktop, processing new IEFrames as they appear. Unicity of the process may be guaranteed by a Named Mutex.
For the time being, I will try to go with a KISS principle (Keep It Simple, Stupid). That is: a short lived injector. I know for sure that this will lead to special handling, in the BHO, for the case of a Tab Drag And Drop'ed to the Desktop, but I will see that later.
Going that route involves a Dll injection that survives the end of the injector, but I will delegate this to the Dll itself.
Here is the code for the injector process. It installs a `WH_CALLWNDPROCRET` hook for the thread hosting the IEFrame, use `SendMessage` (with a specific registered message) to immediatly trigger the Dll injection, and then removes the hook and terminates. The BHO Dll must export a `CallWndRetProc` callback named `HookCallWndProcRet`. Error paths are omitted.
#include <Windows.h>
#include <stdlib.h>
typedef LRESULT (CALLBACK *PHOOKCALLWNDPROCRET)( int nCode, WPARAM wParam, LPARAM lParam );
PHOOKCALLWNDPROCRET g_pHookCallWndProcRet;
HMODULE g_hDll;
UINT g_uiRegisteredMsg;
int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE, char * pszCommandLine, int ) {
HWND hWndIEFrame = (HWND)atoi( pszCommandLine );
wchar_t szFullPath[ MAX_PATH ];
DWORD dwCopied = GetModuleFileName( NULL, szFullPath,
sizeof( szFullPath ) / sizeof( wchar_t ) );
if ( dwCopied ) {
wchar_t * pLastAntiSlash = wcsrchr( szFullPath, L'\\' );
if ( pLastAntiSlash ) *( pLastAntiSlash + 1 ) = 0;
wcscat_s( szFullPath, L"SoBrowserActionBHO.dll" );
g_hDll = LoadLibrary( szFullPath );
if ( g_hDll ) {
g_pHookCallWndProcRet = (PHOOKCALLWNDPROCRET)GetProcAddress( g_hDll,
"HookCallWndProcRet" );
if ( g_pHookCallWndProcRet ) {
g_uiRegisteredMsg = RegisterWindowMessage( L"SOBA_MSG" );
if ( g_uiRegisteredMsg ) {
DWORD dwTID = GetWindowThreadProcessId( hWndIEFrame, NULL );
if ( dwTID ) {
HHOOK hHook = SetWindowsHookEx( WH_CALLWNDPROCRET,
g_pHookCallWndProcRet,
g_hDll, dwTID );
if ( hHook ) {
SendMessage( hWndIEFrame, g_uiRegisteredMsg, 0, 0 );
UnhookWindowsHookEx( hHook );
}
}
}
}
}
}
if ( g_hDll ) FreeLibrary( g_hDll );
return 0;
}
3. Surviving Injection: "hook me harder"
The temporary loading of the Dll in the main IE process is sufficient to add a new button to the Toolbar. But being able to monitor the `WM_COMMAND` for that new button requires more: a permanently loaded Dll and a hook still in place despite the end of the hooking process. A simple solution is to hook the thread again, passing the Dll instance handle.
As each tab opening will lead to a new BHO instantiation, thus a new injector process, the hook function must have a way to know if the current thread is already hooked (I don't want to just add a hook for each tab opening, that's not clean)
Thread Local Storage is the way to go:
- Allocate a TLS index in `DllMain`, for `DLL_PROCESS_ATTACH`.
- Store the new `HHOOK` as TLS data, and use that to know if the thread is already hooked
- Unhook if necessary, when `DLL_THREAD_DETACH`
- Free the TLS index in `DLL_PROCESS_DETACH`
That leads to the following code:
// DllMain
// -------
if ( dwReason == DLL_PROCESS_ATTACH ) {
CCSoBABHO::sm_dwTlsIndex = TlsAlloc();
[...]
} else if ( dwReason == DLL_THREAD_DETACH ) {
CCSoBABHO::UnhookIfHooked();
} else if ( dwReason == DLL_PROCESS_DETACH ) {
CCSoBABHO::UnhookIfHooked();
if ( CCSoBABHO::sm_dwTlsIndex != TLS_OUT_OF_INDEXES )
TlsFree( CCSoBABHO::sm_dwTlsIndex );
}
// BHO Class Static functions
// --------------------------
void CCSoBABHO::HookIfNotHooked( void ) {
if ( sm_dwTlsIndex == TLS_OUT_OF_INDEXES ) return;
HHOOK hHook = reinterpret_cast<HHOOK>( TlsGetValue( sm_dwTlsIndex ) );
if ( hHook ) return;
hHook = SetWindowsHookEx( WH_CALLWNDPROCRET, HookCallWndProcRet,
sm_hModule, GetCurrentThreadId() );
TlsSetValue( sm_dwTlsIndex, hHook );
return;
}
void CCSoBABHO::UnhookIfHooked( void ) {
if ( sm_dwTlsIndex == TLS_OUT_OF_INDEXES ) return;
HHOOK hHook = reinterpret_cast<HHOOK>( TlsGetValue( sm_dwTlsIndex ) );
if ( UnhookWindowsHookEx( hHook ) ) TlsSetValue( sm_dwTlsIndex, 0 );
}
We now have a nearly complete hook function:
LRESULT CALLBACK CCSoBABHO::HookCallWndProcRet( int nCode, WPARAM wParam,
LPARAM lParam ) {
if ( nCode == HC_ACTION ) {
if ( sm_uiRegisteredMsg == 0 )
sm_uiRegisteredMsg = RegisterWindowMessage( L"SOBA_MSG" );
if ( sm_uiRegisteredMsg ) {
PCWPRETSTRUCT pcwprets = reinterpret_cast<PCWPRETSTRUCT>( lParam );
if ( pcwprets && ( pcwprets->message == sm_uiRegisteredMsg ) ) {
HookIfNotHooked();
HWND hWndTB = FindThreadToolBarForIE9( pcwprets->hwnd );
if ( hWndTB ) {
AddBrowserActionForIE9( pcwprets->hwnd, hWndTB );
}
}
}
}
return CallNextHookEx( 0, nCode, wParam, lParam);
}
The code for `AddBrowserActionForIE9` will be edited later.
For IE9, getting the TB is pretty simple:
HWND FindThreadToolBarForIE9( HWND hWndIEFrame ) {
HWND hWndWorker = FindWindowEx( hWndIEFrame, NULL,
L"WorkerW", NULL );
if ( hWndWorker ) {
HWND hWndRebar= FindWindowEx( hWndWorker, NULL,
L"ReBarWindow32", NULL );
if ( hWndRebar ) {
HWND hWndBand = FindWindowEx( hWndRebar, NULL,
L"ControlBandClass", NULL );
if ( hWndBand ) {
return FindWindowEx( hWndBand, NULL,
L"ToolbarWindow32", NULL );
}
}
}
return 0;
}
4. Processing the Tool Bar
That part may be largely improved:
- I just created a black and white bitmap, and all was fine, that is: the black pixels where transparent. Each time I tried to add some colors and/or grey levels, the results were awful. I am not fluent, at all, with those "bitmap in toolbar magics"
- The size of the bitmap should depends on the current size of the other bitmaps already in the toolbar. I just used two bitmaps (one "normal", and one "big")
- It may be possible to optimize the part which force IE to "redraw" the new state of the toolbar, with a lesser width for the address bar. It works, there is a quick "redraw" phase involving the whole IE Main Window.
See my other answer to the question, as I am currently unable to edit the answer with code format working.
Problem
So. I'm working on a BHO in IE and I want to add a browser action like this: In internet explorer it would look something like The only tutorials and docs I've found were on creating toolbar items. None mentioned this option. I know this is possible because crossrider let you do this exact thing. I just don't know how. I can't find any documentation on how I would implement this in a BHO. Any pointers are very welcome. I tagged this with C# as a C# solution would probably be simpler but a C++ solution, or any other solution that works is also very welcome.