65.9K
CodeProject 正在变化。 阅读更多。
Home

操作 Internet Explorer 地址栏中的按钮

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (15投票s)

2011年3月13日

CPOL

12分钟阅读

viewsIcon

52440

downloadIcon

2097

如何控制托管Internet Explorer地址栏中按钮的工具栏。

引言

虽然Internet Explorer提供了良好的扩展性支持,但它不像Firefox那样全面。在构建Firefox扩展时,一个简单的任务是在地址栏旁边添加自定义按钮,但在Internet Explorer中,这在官方是不可能的。如果我们要跨不同浏览器保留扩展的相同用户界面,这可能会成为一个问题。

解决方案是可能的,但这严重依赖于未记录的结构和Internet Explorer的行为。本文将展示这项任务可能有多么肮脏和复杂。

为了理解本文,您应该熟悉编程BHO(浏览器辅助对象),因为BHO的基础知识在此不作赘述。

潜入Internet Explorer的地址栏

查找地址栏窗口

首先,我们应该探索Internet Explorer的窗口结构。Microsoft Spy++是这项工作的完美工具。要定位工具栏窗口,只需使用Spy++的窗口搜索工具(搜索 -> 查找窗口...),然后将其拖放到地址编辑框旁边的工具栏上。请注意,“搜索”按钮(放大镜)不是工具栏的一部分,而是地址组合框。

选择工具栏窗口后,Spy++会向我们显示此窗口层次结构

正如我们所见,其中两个窗口对我们来说很有趣

  • 第一个接收并处理用户单击按钮时从工具栏发送的WM_COMMAND消息。其类名为“Address Band Root”。
  • 第二个是我们要操作的工具栏本身。其类名为“ToolbarWindow32”。

这些窗口的查找由barlib.cpp中的FindAddressBar函数完成。

BOOL FindAddressBar(
  HWND mainWnd, HWND* addressBarWnd, HWND* cmdTargetWnd)
{
  mainWnd = ::FindWindowEx( mainWnd, NULL, TEXT( "WorkerW" ), NULL );
  mainWnd = ::FindWindowEx( mainWnd, NULL, TEXT( "ReBarWindow32" ), NULL );

  *cmdTargetWnd = ::FindWindowEx
    mainWnd, NULL, TEXT( "Address Band Root" ), NULL );

  if( *cmdTargetWnd  )
    *addressBarWnd = ::FindWindowEx(
      *cmdTargetWnd, NULL, TEXT( "ToolbarWindow32" ), L"Page Control" );

  return cmdTargetWnd != NULL;
}

此函数获取指向主浏览器窗口的句柄,并存储已找到窗口的句柄。如果无法找到所需窗口,则返回FALSE。此过程仅适用于Internet Explorer 7或更高版本。早期版本的Internet Explorer具有相似的窗口结构,但并不完全相同,因此此函数不兼容。

查找浏览器的主窗口是一项相当简单的任务。我们只需调用浏览器对象的get_HWND即可。

HWND parent ;
_webBrowser2->get_HWND( (SHANDLE_PTR*)&parent );

HWND addressBarWnd, cmdTargetWnd;
if( ::FindAddressBar( parent, &addressBarWnd, &cmdTargetWnd ) )
  _proxy = new CAddressBarAccessProxy(
    g_hInst, addressBarWnd, cmdTargetWnd );

窗口子类化

现在我们有了工具栏的句柄,我们可以像标准工具栏一样操作它,但我们也需要对其进行子类化以获得精细控制。要做到这一点,我们只需要用我们自己的函数替换处理发送到窗口的消息的函数,但我们需要存储旧函数,以便它可以处理我们不关心的消息。

使用GWLP_WNDPROC作为第二个参数的GetWindowLongPtr API调用用于获取当前处理消息的函数。GetWindowLongPtr调用设置新的消息处理程序。我们的函数必须为我们不想处理的所有消息调用旧函数。要做到这一点,我们需要调用CallWindowProc并将其传递指向我们替换的旧函数的指针以及有关接收消息的其他信息。

仅对工具栏窗口进行子类化是不够的,因为用户单击按钮时生成的WM_COMMAND消息会发送到另一个窗口。为了能够处理新添加按钮上的用户单击,我们还需要对工具栏的父窗口进行子类化。

typedef LRESULT (CALLBACK *WndProcPtr)(
  HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

/*...*/

WndProcPtr oldAddrBarWndProc = NULL;
WndProcPtr oldCmdTargetWndProc = NULL;

void SubclassAddressBar(HWND addressBarWnd, HWND cmdTargetWnd)
{
  // subclassing toolbar window
  oldAddrBarWndProc = (WndProcPtr)::GetWindowLongPtr(
    addressBarWnd, GWLP_WNDPROC );

  ::SetWindowLongPtr(
    addressBarWnd, GWLP_WNDPROC, (LONG_PTR)AddrBarWndProcS );

  // subclassing window that handles WM_COMMAND messages
  // sent from toolbar
  oldCmdTargetWndProc = (WndProcPtr)::GetWindowLongPtr(
    _cmdTargetWnd, GWLP_WNDPROC );

  ::SetWindowLongPtr(
    _cmdTargetWnd, GWLP_WNDPROC, (LONG_PTR)CmdTargetWndProcS );
}

/*...*/

LRESULT CAddressBarAccessServer::AddrBarWndProcS(
  HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
  // here we should handle messages directed to the toolbar
  // that we are interested in

  return CallWindowProc( _instance->_oldAddrBarWndProc,
    hwnd, uMsg, wParam, lParam );
}

LRESULT CmdTargetWndProcS(
  HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
  // here we should handle WM_COMMAND messages sent by the toolbar

  return CallWindowProc( oldCmdTargetWndProc,
    hwnd, uMsg, wParam, lParam );
}

启用受保护模式的Internet Explorer 8+

Internet Explorer 8引入了受保护模式,其后果之一是每个选项卡都有自己的进程,该进程与托管浏览器主窗口(地址栏是其一部分)的进程分开。这是对我们的第一个坏消息。我们的BHO托管在每个选项卡的进程中,这意味着我们必须跨进程边界才能访问浏览器的工具栏。这就是(音乐响起)DLL注入发挥作用的地方。

现在,一个直接的实现是让我们的BHO将另一个DLL注入到浏览器的主进程中,但事实证明,这并不那么简单。第二个坏消息是新版Windows(Vista和7)中每个进程都有的称为完整性级别的概念。有几个可能的级别,我们在此不讨论,但主要原则是较低完整性级别的进程无法干扰较高完整性级别的进程。Internet Explorer以最低完整性级别生成选项卡进程,而托管主窗口的进程以中等级别运行。

幸运的是,好消息从这里开始。当选项卡进程生成新进程时,它以中等级别创建——与我们要访问的进程相同。所以要使DLL注入生效,我们只需要创建一个代理进程,其唯一目的是将DLL注入到浏览器的进程中。一切都很好,但是当在受保护模式下从父进程的级别启动更高级别的进程时,系统会提示用户是否应继续。

可以通过为代理进程创建提升策略来抑制此提示。提升策略是位于HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Internet Explorer\Low Rights\ElevationPolicy下的注册表键和值的集合。策略的创建是在我们BHO的注册脚本中完成的,稍后将对此进行描述。

实现

负责浏览器工具栏操作的所有代码都隔离在一个名为ieb_lib的独立静态库中。该库是BHO的DLL的一部分。它也与代表包装器的辅助DLL链接,以便可以将其注入到浏览器的主进程中。

ieb_start.exe代表代理进程,该进程将包装器DLL注入到浏览器的主进程中。

DLL 注入

注入开始于启动代理进程,该进程执行实际的注入。

BOOL InjectDll(HINSTANCE bhoDll, DWORD procID)
{
  STARTUPINFO startInfo;
  ::ZeroMemory( &startInfo, sizeof( startInfo ) );
  startInfo.cb = sizeof( startInfo );
  startInfo.dwFlags |= STARTF_USESHOWWINDOW;
  startInfo.wShowWindow = FALSE;

  PROCESS_INFORMATION processInfo;
  ::ZeroMemory( &processInfo, sizeof( processInfo ) );

  TCHAR params[ MAX_PATH ];
  _itow_s( procID, params, MAX_PATH, 10 );

  TCHAR path[ MAX_PATH ];
  if( !::GetModuleFileName( bhoDll, path, MAX_PATH ) )
    return FALSE;

#ifdef UNICODE
  wchar_t* sp = wcsrchr( path, L'\\' ) + 1;
#elif
  char* sp = strrchr( path, '\\' ) + 1; 
#endif

  lstrcpy( sp, TEXT( "ieb_start.exe" ) );

  if( !::CreateProcess( path, params, NULL, NULL, FALSE,
    CREATE_NO_WINDOW, NULL, NULL, &startInfo, &processInfo ) )
    return FALSE;

  ::WaitForSingleObject( processInfo.hProcess, INFINITE );

  ::CloseHandle( processInfo.hThread );
  ::CloseHandle( processInfo.hProcess );

  return TRUE;
}

正如我们所见,新进程以通常的方式启动,但没有可见窗口。还应该注意的是,我们将浏览器主进程的ID传递给代理,以便它知道要将包装器DLL注入到哪里。

DLL注入的基本原理是在远程进程中分配内存(使用VirtualAllocEx API调用),并将我们要加载的DLL的路径存储在那里(使用WriteProcessMemory调用)。接下来,我们应该使用CreateRemoteThread API调用在目标进程中创建一个线程。我们将LoadLibrary作为该线程的入口点,并将通过调用VirtualAllocEx获得的地址作为参数传递。

BOOL InjectDll(DWORD processId, TCHAR* dllName)
{
  if( !DebugPrivileges( TRUE ) )
    return FALSE;

  HANDLE process = ::OpenProcess( PROCESS_ALL_ACCESS, FALSE, processId );
  DWORD error = ::GetLastError();
  if( !process )
    return FALSE;

  TCHAR path[ MAX_PATH ];
  if( !::GetModuleFileName( NULL, path, MAX_PATH ) )
  {
    ::CloseHandle( process );
    return FALSE;
  }

#ifdef UNICODE
  wchar_t* sp = wcsrchr( path, L'\\' ) + 1;
  wcscpy_s( sp, path + MAX_PATH - sp, dllName );
#elif
  char* sp = strrchr( path, '\\' ) + 1; 
  strcpy_s( sp, path + MAX_PATH - sp, dllName );
#endif

  LPVOID address = ::VirtualAllocEx(
    process, NULL, sizeof( path ), MEM_COMMIT, PAGE_READWRITE );

  if( !address )
    return FALSE;

  if( !::WriteProcessMemory(
    process, address, path, sizeof( path ), NULL ) )
  {
    ::VirtualFreeEx( process, address, sizeof( path ), MEM_RELEASE );
    ::CloseHandle( process );
    return FALSE;
  }

  HANDLE thread = ::CreateRemoteThread( process, NULL, 0,
    (LPTHREAD_START_ROUTINE)::GetProcAddress(
    ::GetModuleHandle( L"Kernel32" ), "LoadLibraryW" ),
    address, 0, NULL );

  if( !thread )
  {
    ::VirtualFreeEx( process, address, sizeof( path ), MEM_RELEASE );
    ::CloseHandle( process );
    return FALSE;
  }

  ::WaitForSingleObject( thread, INFINITE );

  ::VirtualFreeEx( process, address, sizeof( path ), MEM_RELEASE );

  ::CloseHandle( thread );
  ::CloseHandle( process );

  DebugPrivileges( FALSE );

  return TRUE;
}

代理进程的静默提升

要以中等完整性级别启动代理进程而不出现提示,我们需要创建一个提升策略。每个策略都必须有其GUID。要创建策略,我们必须创建一个新的注册表项,其名称为策略GUID。在新项中,我们应该设置三个值:

  • AppPathDWORD)- 代理进程的路径。
  • AppNameREG_SZ)- 可执行文件的文件名。
  • PolicyREG_SZ)- 定义Internet Explorer如何启动代理进程;我们将此值设置为“3”,指示Explorer静默启动代理进程。

这些键和值应在BHO的注册过程中添加。要做到这一点,我们需要修改注册脚本(IEBarBHO.rgs文件)并添加以下代码:

HKLM
{
  NoRemove SOFTWARE
  {
    NoRemove Microsoft
    {
      NoRemove 'Internet Explorer'
      {
        NoRemove 'Low Rights'
        {
          NoRemove ElevationPolicy
          {
            ForceRemove '{c6c528cd-8c93-494d-8583-38821b575da9}'
            {
              val AppPath = s '%MODULEPATH%'
              val AppName = s 'ieb_start.exe'
              val Policy = d '3'
            }
          }
        }
      }
    }
  }
}

可替换参数%MODULEPATH%用于定位代理的可执行文件。要使其正常工作,我们需要修改我们的BHO类,并删除DECLARE_REGISTRY_RESOURCEID宏提供的默认UpdateRegistry方法,并提供我们自己的方法。

extern TCHAR g_ModulePath[ MAX_PATH ];

_ATL_REGMAP_ENTRY CIEBarBHO::RegEntries[] =
{
 { OLESTR( "MODULEPATH" ), g_ModulePath },
 { NULL, NULL }
};

HRESULT CIEBarBHO::UpdateRegistry(BOOL bRegister)
{
  return ATL::_pAtlModule->UpdateRegistryFromResource(
    IDR_IEBARBHO, bRegister, RegEntries );
}

g_ModulePathDllMain函数中更新,它存储BHO的DLL所在的路径。我们还需要从类定义中删除DECLARE_REGISTRY_RESOURCEID宏的调用。

进程间通信

由于UIPI(用户界面权限隔离)的限制,我们无法在不同完整性级别运行的进程之间发送Windows消息,因此我们应该使用另一种IPC形式。

本示例使用文件映射在选项卡进程和浏览器主进程之间提供共享内存;对于更高级的通信形式,可以使用其他IPC技术。创建通信对象时,一个重要问题是谁拥有它。如果它是一个中等完整性级别的进程(主进程),较低完整性级别的进程(选项卡进程)将无法访问它,除非它们专门降低对象完整性(使用SetNamedSecurityInfoW)。更简单的方法是让选项卡进程创建通信对象。这样,两个进程都可以访问该对象而无需额外的麻烦。只有可保护的对象(如文件映射、管道等)会受到完整性检查,而套接字则不会。

代码

现在我们已经讨论完与DLL注入相关的细节,我们可以介绍实际的代码,这些代码允许我们操作浏览器的工具栏。

服务器

CAddressBarAccessServer类对浏览器工具栏进行子类化,并提供操作它的接口。SendMessagePostMessages方法向工具栏窗口发送/发布消息。

LRESULT SendMessage(UINT uMsg, WPARAM wParam, LPARAM lParam)
  { return ::SendMessage( _addressBarWnd, uMsg, wParam, lParam ); }
LRESULT PostMessage(UINT uMsg, WPARAM wParam, LPARAM lParam)
  { return ::PostMessage( _addressBarWnd, uMsg, wParam, lParam ); }

CmdTargetWndProc方法应修改为处理用户单击按钮时从工具栏发送的WM_COMMAND消息。该方法还将当前选项卡的代理通知其收到的消息。AddrBarWndProc方法应处理发送到工具栏本身的消息。

BOOL CAddressBarAccessServer::CmdTargetWndProc(
     LRESULT* lResult, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
  switch( uMsg )
  {
    /* TODO: ADD TOOLBAR COMMAND MESSAGE HANDLING THAT */
    /* IS EXECUTED WITHIN THE PROCESS WHICH HOSTS TOOLBAR */
    /* RETURN TRUE IF MESSAGE SHOULD NOT BE PROCESSED FURTHER */
  }

  if( uMsg == WM_COMMAND )
  {
    // should we use IPC or access client proxy directly
    if( _currentProcessID != ::GetCurrentProcessId() )
    {
      ATL::CComCritSecLock<CComAutoCriticalSection> lock(
        _clientHandlersLock, true );

      // send notification to listening thread
      // of process which owns client proxy
      ClienMessageHandlersMap::iterator it =
        _clientMessageHandlers.find( _currentProcessID );
      if( it != _clientMessageHandlers.end() )
      {
        ForwardedMessage msg(
          _currentProxyClient, uMsg, wParam, lParam );
        it->second.first->Write( &msg, sizeof( msg ), FALSE );
      }
    }
    else
      ( (CAddressBarAccessProxy*)_currentProxyClient )->
        CmdTargetWndProc( lResult, uMsg, wParam, lParam );
    }
    return FALSE;
}

BOOL CAddressBarAccessServer::AddrBarWndProc(
  LRESULT* lResult, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
  switch( uMsg )
  {
    /* TODO: ADD TOOLBAR MESSAGE HANDLING THAT */
    /* IS EXECUTED WITHIN THE PROCESS WHICH HOSTS TOOLBAR */
    /* RETURN TRUE IF MESSAGE SHOULD NOT BE PROCESSED FURTHER */
  }

  return FALSE;
}

如果我们想处理发送到工具栏和来自工具栏的消息,我们需要修改这两个方法。但请注意,它们可能在与拥有我们BHO的进程不同的进程中执行。

SetCurrentProxy设置将收到来自工具栏的消息通知的代理。

void CAddressBarAccessServer::SetCurrentProxy(
  DWORD processID, UINT_PTR proxyClient)
{
  ATL::CComCritSecLock<CComAutoCriticalSection> lock(
    _clientHandlersLock, true );

  // store current proxy
  _currentProcessID = processID;
  _currentProxyClient = proxyClient;
}

Load方法执行子类化。此方法应修改以从资源DLL加载必需的图像并将按钮插入工具栏。Unload方法移除子类化。

void CAddressBarAccessServer::Load(
  HWND addressBarWnd, HWND cmdTargetWnd,
  DWORD processID, UINT_PTR proxyClient)
{
  ATL::CComCritSecLock<CComAutoCriticalSection> lock(
    _clientHandlersLock, true );

  if( processID != ::GetCurrentProcessId() &&
   _clientMessageHandlers.find( processID )==
   _clientMessageHandlers.end() )
  {
    // add IPC channel for proxy if it is in a different process
    _clientMessageHandlers[ processID ] =  ClienMessageHandlersEntry(
      new CCommChannel( TEXT( "IeBarMsgPoint" ), processID ), 1 );

    if( _clientMessageHandlers.size() == 1 )
    {
      _currentProcessID = processID;
      _currentProxyClient = proxyClient;
    }
  }

  // do it only the first tab of the browser is initialized
  if( ++_tabCounter == 1 )
  {
    _addressBarWnd = addressBarWnd;
    _cmdTargetWnd = cmdTargetWnd;

    // subclass windows
    _oldAddrBarWndProc = (WndProcPtr)::GetWindowLongPtr(
      _addressBarWnd, GWLP_WNDPROC );
    ::SetWindowLongPtr(
      _addressBarWnd, GWLP_WNDPROC, (LONG_PTR)AddrBarWndProcS );
    _oldCmdTargetWndProc = (WndProcPtr)::GetWindowLongPtr(
      _cmdTargetWnd, GWLP_WNDPROC );
    ::SetWindowLongPtr(
      _cmdTargetWnd, GWLP_WNDPROC, (LONG_PTR)CmdTargetWndProcS );
        
    // get toolbar's image lists
    _imageList = (HIMAGELIST)::SendMessage(
      addressBarWnd, TB_GETIMAGELIST, (WPARAM)0, (LPARAM)0 );
    _hotImageList = (HIMAGELIST)::SendMessage(
      addressBarWnd, TB_GETHOTIMAGELIST, (WPARAM)0, (LPARAM)0 );
    _pressedImageList = (HIMAGELIST)::SendMessage(
      addressBarWnd, TB_GETPRESSEDIMAGELIST, (WPARAM)0, (LPARAM)0 );

    // add required buttons
    InitButtons();

    lock.Unlock();

    // refreshes size of the toolbar
    ::SendMessage( addressBarWnd, WM_SIZE, 0, 0 );
    ::SendMessage( cmdTargetWnd, WM_SIZE, 0, 0 );
  }
}

void CAddressBarAccessServer::Unload(
     DWORD processID, UINT_PTR proxyClient)
{
  ATL::CComCritSecLock<CComAutoCriticalSection> lock(
    _clientHandlersLock, true );

  if( processID != ::GetCurrentProcessId() )
  {
    // destory IPC channel between proxy and server
    // if they are in different processes
    ClienMessageHandlersEntry& entry =
      _clientMessageHandlers[ processID ];
    if( --entry.second == 0 )
    {
        delete entry.first;
        _clientMessageHandlers.erase( processID );
    }
  }

  // if there's no more tabs when should clear changes made to toolbar
  if( --_tabCounter == 0 )
  {
    // reverese subclassing
    ::SetWindowLongPtr(
      _addressBarWnd, GWLP_WNDPROC, (LONG_PTR)_oldAddrBarWndProc );
    ::SetWindowLongPtr(
      _cmdTargetWnd, GWLP_WNDPROC, (LONG_PTR)_oldCmdTargetWndProc );

    _addressBarWnd = _cmdTargetWnd = NULL;

    // remove buttons
    for( ButtonsMap::iterator it = _buttons.begin();
      it != _buttons.end(); ++it )
      it->second.Destroy();

    _buttons.clear();

    // destory IPC channel which receives requests
    if( _channel )
    {
      delete _channel;
      _channel = NULL;
    }
  }
}

AddButton将按钮插入工具栏。我们必须为按钮提供三种不同状态的图标:按钮处于非活动状态时、用户将鼠标悬停在按钮上时以及按下按钮时。

void AddButton(WORD id, HICON image, HICON hotImage, HICON pressedImage);

InitButtons方法初始化并插入所需的按钮到工具栏。我们应该修改此函数来插入我们自己的按钮。

void CAddressBarAccessServer::InitButtons()
{
  HINSTANCE module;
  GetModuleHandleEx(
    GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,
    (LPCWSTR)( &CAddressBarAccessServer::ProxyListen ), &module );

  /* INSERT BUTTONS TO TOOLBAR */

  HICON icon = ::LoadIcon( module, MAKEINTRESOURCE( IDI_ICON1 ) );
  HICON hotIcon = ::LoadIcon( module, MAKEINTRESOURCE( IDI_ICON2 ) );
  HICON pressedIcon = ::LoadIcon( module, MAKEINTRESOURCE( IDI_ICON2 ) );

  AddButton( 0xc001, icon, hotIcon, pressedIcon );
}

GetModuleHandleEx API调用获取包含应加载的资源的模块(图标)的句柄。如果存在选项卡隔离,它将返回包装器DLL(ieb_wrap.dll)的句柄;否则,它将返回BHO的DLL(ieb_bho.dll)的句柄。

当服务器在单独的进程中运行时,它会启动一个侦听线程,该线程读取IPC对象的请求并对其进行解释。ProxyListen方法是线程的入口点。此方法解包通过IPC发送的数据,并调用适当的服务器方法来处理来自代理的请求。

Proxy

CAddressBarAccessProxy类抽象了地址栏服务器的位置,因此BHO不必担心它是否与浏览器地址栏位于同一进程中。如果不存在选项卡隔离,代理对象将直接调用服务器的方法;否则,它将通过IPC将调用路由到位于主进程中的服务器。

SendMessagePostMessage方法仅将调用重定向到服务器对象的相应方法。SetCurrent方法通知服务器这是当前选项卡的代理服务器。

void CAddressBarAccessProxy::SetCurrent()
{
  if( _server )
    _server->SetCurrentProxy( ::GetCurrentProcessId(), (INT_PTR)this );
  else
  {
    SelectTabCmd cmd( ::GetCurrentProcessId(), (INT_PTR)this );
    _cmdChannel->Write( &cmd, sizeof( cmd ) );
  }
}

LRESULT CAddressBarAccessProxy::SendMessage(
  UINT uMsg, WPARAM wParam, LPARAM lParam)
{
  if( _server )
    return _server->SendMessage( uMsg, wParam, lParam );

  SendMessageCmd cmd ( uMsg, wParam, lParam );
  _cmdChannel->Write( &cmd, sizeof( cmd ) );

  LRESULT result;
  _cmdChannel->Read( &result, sizeof( result ), TRUE );

  return result;
}

LRESULT CAddressBarAccessProxy::PostMessage(
  UINT uMsg, WPARAM wParam, LPARAM lParam)
{
  if( _server )
    return _server->PostMessage( uMsg, wParam, lParam );

  PostMessageCmd cmd ( uMsg, wParam, lParam );
  _cmdChannel->Write( &cmd, sizeof( cmd ) );

  return 0;
}

要捕获选项卡更改,我们需要处理浏览器对象发送的DISPID_WINDOWSTATECHANGED事件。

STDMETHODIMP CIEBarBHO::Invoke(DISPID dispidMember, REFIID riid, 
  LCID lcid, WORD wFlags, DISPPARAMS* pDispParams, 
  VARIANT* pvarResult, EXCEPINFO* pExcepInfo, UINT* puArgErr)
{
  if( dispidMember == DISPID_WINDOWSTATECHANGED )
  {
    DWORD flags = pDispParams->rgvarg[1].intVal;
    DWORD valid = pDispParams->rgvarg[0].intVal;

    // check whether the event is raised because tab became active
    if( (valid & OLECMDIDF_WINDOWSTATE_USERVISIBLE) != 0 &&
      (flags & OLECMDIDF_WINDOWSTATE_USERVISIBLE) != 0 &&
      (valid & OLECMDIDF_WINDOWSTATE_ENABLED) != 0 &&
      (flags & OLECMDIDF_WINDOWSTATE_ENABLED) != 0 )
      _proxy->SetCurrent();
  }
  return S_OK;
}

CmdTargetWndProc是用于通知代理浏览器工具栏发送的WM_COMMAND的回调函数。只有当前选定选项卡的代理才会收到来自服务器的消息通知。如果我们想处理这些通知,应该覆盖此方法。

virtual BOOL CmdTargetWndProc(
  LRESULT* lResult, UINT uMsg, WPARAM wParam, LPARAM lParam)
  { return 0; }

当代理位于单独的进程中时,它会启动另一个线程,该线程侦听服务器在用户单击按钮后发送的关于WM_COMMAND消息的通知。MessageHandlerListner方法是线程的入口点,它通过调用CmdTargetWndProc方法来处理通知。

DWORD CAddressBarAccessProxy::MessageHandlerListner(LPVOID param)
{
  CCommChannel* channel = (CCommChannel*)param;

  char buffer[ CCommChannel::SECTION_SIZE ];
  ForwardedMessage* msg = (ForwardedMessage*)buffer;

  while(channel->Read( buffer, CCommChannel::SECTION_SIZE ) )
  {
    LRESULT result;
    ( (CAddressBarAccessProxy*)msg->_proxyClient )->CmdTargetWndProc(
      &result, msg->_uMsg, msg->_wParam, msg->_lParam );
  }
  return 0;
}

IPC

CCommChannel类封装了选项卡进程和浏览器主进程之间的IPC。最重要的函数是ReadWrite,它们分别读取和写入共享内存。Read方法在数据可用之前等待。Write方法等待共享内存可用(先前写入的数据必须先被读取),然后写入新数据,之后它发出新数据可用的信号。

BOOL CCommChannel::Read(
  LPVOID data, DWORD dataSize, BOOL response/* = FALSE*/)
{
  ::WaitForSingleObject(
    _events[ response ? RESPONSE_AVAILABLE : REQUEST_AVAILABLE ],
    INFINITE );

  LPVOID source =
    ::MapViewOfFile( _section, FILE_MAP_ALL_ACCESS, 0, 0, dataSize );
  if( !source )
  {
    if( !response )
      ::SetEvent( _events[ SERVER_AVAILABLE ] );
    return FALSE;
  }

  ::CopyMemory( data, source, dataSize );
  BOOL ok = ::UnmapViewOfFile( source );

  if( !response )
    ::SetEvent( _events[ SERVER_AVAILABLE ] );
  return ok;
}

BOOL CCommChannel::Write(
  LPVOID data, DWORD dataSize, BOOL response/* = FALSE*/)
{
  if( !response )
    ::WaitForSingleObject( _events[ SERVER_AVAILABLE ], INFINITE );

  LPVOID destination =
    ::MapViewOfFile( _section, FILE_MAP_ALL_ACCESS, 0, 0, dataSize );

  if( !destination )
  {
    if( !response )
      ::SetEvent( _events[ SERVER_AVAILABLE ] );
    return FALSE;
  }

  ::CopyMemory( destination, data, dataSize );
  if( ::UnmapViewOfFile( destination ) )
  {
    ::SetEvent(
      _events[ response ? RESPONSE_AVAILABLE : REQUEST_AVAILABLE ] );
    return TRUE;
  }
  else
  {
    ::SetEvent(
      _events[ response ? RESPONSE_AVAILABLE : SERVER_AVAILABLE ] );
    return FALSE;
  }
}

SetReady将一个段标记为可用于新写入。IsFirst指示当前进程是否是创建系统IPC对象的进程。

还有其他几种结构用于在服务器和代理之间使用IPC时打包和解包消息。

安装和卸载BHO

要注册BHO,我们应该执行regsvr32 ieb_bho.dll命令,要卸载,则执行regsvr32 /u ieb_bho.dll。这些命令需要管理员权限。所有DLL和EXE文件必须位于同一目录中。Internet Explorer可能会询问用户是否应允许加载我们的BHO。

结论

正如我们所见,将自己的按钮添加到地址栏并不像看起来那么容易。此外,依赖于未记录的行为可能会让你陷入麻烦,并且在每个浏览器版本甚至补丁中都会让你头疼。另一个问题是,这采用了某些杀毒软件可能认为有害的技术,特别是因为它在浏览器内部运行,而浏览器是攻击的常见目标。因此,此技术应谨慎使用,并且可能仅在受控环境中部署。

操作Internet Explorer地址栏中的按钮 - CodeProject - 代码之家
© . All rights reserved.