Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Popup Window Blocker

0.00/5 (No votes)
1 May 2003 8  
Popup Blocker is a Browser Helper Object (BHO) that eliminates all popup and popunder windows, including those opened automatically by script.

Sample Image - PopupBlocker.jpg

Introduction

Why pay money for a popup window blocker when it's so easy to roll your own?  Popup Blocker is implemented in ATL as a browser helper object (BHO).  A BHO is a DLL that will attach itself to every new instance of Internet Explorer.  In the BHO you can intercept Internet Explorer events, and access the browser window and document object model (DOM).  This gives you a great deal of flexibility in modifying Internet Explorer behavior.

There are several advantages to using a BHO over an application that continually scans open windows for keywords in the title.  The BHO is event driven and does not run in a loop or use a timer, so it doesn't use any CPU cycles if nothing is happening.  The BHO will prevent popups from opening and downloading their content, so you save bandwidth.  The BHO can be written so as to eliminate the need for a keyword list or blacklist.

Also included, but not discussed in this article, is a Windows Installer setup program.

Creating the BHO

A minimal BHO is a COM server DLL that implements IObjectWithSite.  Create a new ATL Project and accept the default settings (attributed, DLL).  Add an ATL Simple Object, give it a name and on the Options tab select Aggregation: No and Support: IObjectWithSite

The only method on IObjectWithSite that must be implemented is SetSite().  IE will call SetSite with a pointer to IUnknown which we can query for a pointer to IWebBrowser2, which gives us the keys to the house.

//

// IOleObjectWithSite Methods

//

STDMETHODIMP CPub::SetSite(IUnknown *pUnkSite)
{
    if (!pUnkSite)
    {
        ATLTRACE(_T("SetSite(): pUnkSite is NULL\n"));
    }
    else
    {
        // Query pUnkSite for the IWebBrowser2 interface.

        m_spWebBrowser2 = pUnkSite;
        if (m_spWebBrowser2)
        {
            // Connect to the browser in order to handle events.

            HRESULT hr = ManageBrowserConnection(ConnType_Advise);
            if (FAILED(hr))
                ATLTRACE(_T("Failure sinking events from IWebBrowser2\n"));
        }
        else
        {
            ATLTRACE(_T("QI for IWebBrowser2 failed\n"));
        }
    }
    
    return S_OK;
}
        

Once we have the pointer to IWebBrowser2 we can hook up to the DWebBrowserEvents2 connection point in order to receive the NewWindow2 event.  This event is sent every time a new window is about to open and allows you to cancel the operation -- exactly what we want for a popup window blocker.  

//

// Funnel web browser events through this class

//

HRESULT CPub::ManageBrowserConnection(ConnectType eConnectType)
{
    if (eConnectType == ConnType_Unadvise && m_dwBrowserCookie == 0)
        return S_OK;    // not advised, nothing to do


    ATLASSERT(m_spWebBrowser2);
    if (!m_spWebBrowser2)
        return S_OK;

    CComQIPtr spCPC(m_spWebBrowser2);
    
    HRESULT hr = E_FAIL;
    if (spCPC)
    {
        CComPtr spCP;
        hr = spCPC->FindConnectionPoint(DIID_DWebBrowserEvents2, &spCP);
        if (SUCCEEDED(hr))
        {
            if (eConnectType == ConnType_Advise)
            {
                ATLASSERT(m_dwBrowserCookie == 0);
                hr = spCP->Advise((IDispatch*)this, &m_dwBrowserCookie);
            }
            else 
            {
                hr = spCP->Unadvise(m_dwBrowserCookie);
                m_dwBrowserCookie = 0;
            }
        }
    }
    return hr;
}

To create the event handler we have to derive our class from IDispatchImpl.

class ATL_NO_VTABLE CPub : 
    public IObjectWithSiteImpl<CPUB>,
    public IDispatchImpl
        

Then add the Invoke method as follows:

//

// IDispatch Methods

//

STDMETHODIMP CPub::Invoke(DISPID dispidMember, REFIID riid, LCID lcid,
	WORD wFlags, DISPPARAMS* pDispParams, VARIANT* pvarResult,
	EXCEPINFO*  pExcepInfo,  UINT* puArgErr)
{
    if (!pDispParams)
        return E_INVALIDARG;
    
    switch (dispidMember)
    {
        //

        // The parameters for this DISPID are as follows:

        // [0]: Cancel flag  - VT_BYREF|VT_BOOL

        // [1]: IDispatch* - Pointer to an IDispatch interface. 

        //

        // If you cancel here, ALL popups will be blocked.

        //

    case DISPID_NEWWINDOW2:
        // Set the cancel flag to block popups

        pDispParams->rgvarg[0].pvarVal->vt = VT_BOOL;
        pDispParams->rgvarg[0].pvarVal->boolVal = VARIANT_TRUE;
        break;
    default:
        break;
   }
   
   return S_OK;
}

Once you get this much to compile, to make IE load the BHO you need to add the CLSID to the registry.

HKLM {
    SOFTWARE {
        Microsoft {    
            Windows {
                CurrentVersion {
                    Explorer {
                        'Browser Helper Objects'
                        {
                            {C68AE9C0-0909-4DDC-B661-C1AFB9F5AE53}
                        }
                    }
                }
            }
        }
}
        

If you've done everything right up to now, the BHO will load with every instance of IE and prevent all popup windows from opening. You can quickly test this by launching IE and selecting Open in New Window from the context menu. If the BHO is working properly the command should fail. 

One problem you may notice is that even with no open browser windows the linker will sometimes fail because some process is using the BHO, forcing you to reboot the computer to release it.  Needless to say, this can quickly become annoying.  What is happening is that Windows Explorer also uses IE for its GUI, and so also loads the BHO.  Since we want our BHO to load only when we launch IE, we need to make a change to DllMain so that it doesn't get loaded by Windows Explorer. This should eliminate the linker problem.

BOOL WINAPI DllMain(DWORD dwReason, LPVOID lpReserved) 
{
    if (dwReason == DLL_PROCESS_ATTACH)
    {
        // Don't attach to Windows Explorer

        TCHAR pszLoader[MAX_PATH];
        GetModuleFileName(NULL, pszLoader, MAX_PATH);
        CString sLoader = pszLoader;
        sLoader.MakeLower();
        if (sLoader.Find(_T("explorer.exe")) >= 0)
            return FALSE;

        g_hinstPub = _AtlBaseModule.m_hInst;
    }
    return __super::DllMain(dwReason, lpReserved);
}
        

So now that we have our basic BHO, it would be nice to be able to exert some kind of control over it.  For starters, we need to be able to enable or disable it.  Also, it would be nice if Open in New Window from the context menu worked normally.  There are probably a number of ways to do this, but I chose to add my own menu to the normal IE context menu.  To access the IE context menu we need to derive from the IDocHostUIHandler interface and implement the ShowContextMenu method.

class ATL_NO_VTABLE CPub : 
    public IObjectWithSiteImpl<CPUB>,
    public IDispatchImpl,
    public IDocHostUIHandler
        

Add the ShowContextMenu method:

HRESULT CPub::ShowContextMenu(DWORD dwID,
                              POINT *ppt,
                              IUnknown *pcmdTarget,
                              IDispatch *pdispObject) 
{
    // Return S_OK to tell MSHTML not to display its own menu

    // Return S_FALSE displays default MSHTML menu

    return S_OK;
}
        

In order to get IE to call our ShowContextMenu we use the ICustomDoc::SetUIHandler method. Typically, this is done when we get the NavigateComplete2 event because at that point the document has been created and we can access the browsers IHTMLDocument2 interface. However, because we need to get the default handlers for IDocHostUIHandler and IOleCommandTarget (more on that later), instead we will do it in DocumentComplete after the entire document has been loaded. So we add a case to our Invoke method:

//

	// The parameters for this DISPID:

	// [0]: URL navigated to - VT_BYREF|VT_VARIANT

	// [1]: An object that evaluates to the top-level or frame

	//      WebBrowser object corresponding to the event. 

	//

	// Fires after a navigation to a link is completed on either 

	// a window or frameSet element.

	//

case DISPID_NAVIGATECOMPLETE2:
	ATLTRACE(_T("(%ld) DISPID_NAVIGATECOMPLETE2\n"),
		::GetCurrentThreadId());
	{
		// Any new windows that might pop up after this

		// (due to script) should be blocked.

		m_bBlockNewWindow = TRUE;	// Reset


		if (!m_pWBDisp)
		{
			// This is the IDispatch* of the top-level browser

			m_pWBDisp = pDispParams->rgvarg[1].pdispVal;
		}
	}
	break;
	
case DISPID_DOCUMENTCOMPLETE:
	ATLTRACE(_T("(%ld) DISPID_DOCUMENTCOMPLETE\n"),
		::GetCurrentThreadId());
	if (m_pWBDisp && 
		m_pWBDisp == pDispParams->rgvarg[1].pdispVal)
	{
		// If the LPDISPATCH are same, that means

		// it is the final DocumentComplete. Reset m_pWBDisp.

		ATLTRACE(_T("(%ld) DISPID_DOCUMENTCOMPLETE (final)\n"),
			::GetCurrentThreadId());
		m_pWBDisp = NULL;

		CComPtr spDisp;
		HRESULT hr = m_spWebBrowser2->get_Document(&spDisp);
		if (SUCCEEDED(hr) && spDisp)
		{
			// If this is not an HTML document (e.g., it's a

			// Word doc or a PDF), don't sink.

			CComQIPtr spHTML(spDisp);
			if (spHTML)
			{
			  // Get pointers to default interfaces

			  CComQIPtr spOleObject(spDisp);
			  if (spOleObject)
			  {
			    CComPtr spClientSite;
			    hr = spOleObject->GetClientSite(&spClientSite);
			    if (SUCCEEDED(hr) && spClientSite)
			    {
			      m_spDefaultDocHostUIHandler = spClientSite;
			      m_spDefaultOleCommandTarget = spClientSite;
			    }
			  }

			  // Set this class to be the IDocHostUIHandler

			  CComQIPtr spCustomDoc(spDisp);
			  if (spCustomDoc)
			    spCustomDoc->SetUIHandler(this);
			}
		} 
	}
	break;

If you compile and run now, you should see that the IE context menu is disabled since we are always returning S_OK from the ShowContextMenu method. In the MSDN documentation there is an excellent article titled "WebBrowser Customization" that explains exactly, with sample code, how to add code to ShowContextMenu so as to replicate the original IE context menu, so I won't bother to talk about all that here.  What the article did not explain was how to add your own menu to that context menu.  Create your menu resource and then add this menu to the top of the context menu in the normal fashion. Nothing tricky here:

// Insert our menu at the top of the context menu

g_hPubMenu = LoadMenu(g_hinstPub, MAKEINTRESOURCE(IDR_PUBMENU));
if (g_hPubMenu)
{
    ::InsertMenu(hSubMenu, 0, MF_POPUP | MF_BYPOSITION,
	(UINT_PTR) g_hPubMenu, _T("Popup Blocker"));
    ::InsertMenu(hSubMenu, 1, MF_BYPOSITION | MF_SEPARATOR, NULL, NULL);
}
        

The tricky part comes when you run the BHO and notice that the all the items on the context menu work as you expect EXCEPT for those items on your menu, which are disabled.  This is because IE inconveniently disregards menu items it does not recognize and will not enable your menu items even if you tell it to do so with the EnableMenuItem command.  To fix this, we need to temporarily add our own window procedure just before the call to TrackPopupMenu so that we can handle messages for our menu, then we restore the original window procedure immediately after TrackPopupMenu returns.

LRESULT CALLBACK CtxMenuWndProc(HWND hwnd, UINT uMsg, WPARAM wParam,
	LPARAM lParam)
{
    if (uMsg == WM_INITMENUPOPUP)
    {
        if (wParam == (WPARAM) g_hPubMenu)
        {
            // This is our menu

            ::CheckMenuItem(g_hPubMenu,
		ID_ENABLEPOPUPBLOCKER, MF_BYCOMMAND | (g_bEnabled ?
			MF_CHECKED : MF_UNCHECKED));
            ::CheckMenuItem(g_hPubMenu, ID_PLAYSOUND,
		MF_BYCOMMAND | (g_bPlaySound ? MF_CHECKED : MF_UNCHECKED));
            return 0;
        }
    }
    return CallWindowProc(g_lpPrevWndProc, hwnd, uMsg, wParam, lParam);
}
            

If a message is not for our menu, we just pass it on to the original window procedure.  Now that the menu is working, you can add switches and dialogs to control the behavior of the BHO. In the code included with this article I added a checked menu item to enable/disable the BHO.  I also intercepted the normal Open in New Window command and set a flag temporarily disabling the BHO in order to restore normal behavior.

So at this point we have a popup blocker that works pretty well, but there are still a couple of glitches.  The first one that you will notice is that certain links seem to be disabled, that is nothing happens when they are clicked.  These are links that  pop up another window, usually through a bit of script.  The BHO happily suppresses these windows with no indication to the user.  To solve this problem, I added a way to temporarily override the BHO by holding down the CTRL key. You have to be careful here to not disable the SHIFT + Click shortcut used to open a new window (thanks to Matt Newman for pointing that one out!).

BOOL CPub::IsHotkeyDown()
{
	SHORT nState = GetAsyncKeyState(VK_CONTROL);
	BOOL bDown = (nState & 0x8000);
	if (!bDown)
	{
		// Allow shift+click to open a new window

		nState = GetAsyncKeyState(VK_SHIFT);
		bDown = (nState & 0x8000);
	}
	return bDown;
}
            

But how is the user supposed to know when to press the hot key? In my code I chose to play a sound every time a popup window is suppressed. That way the user has an indication that the BHO is in the act when a link appears to be broken.  Then all the user needs to do is hold down the CTRL key while clicking the link, and the link will work normally.

Now our popup blocker is working really well.  It blocks all popup windows and we can shut it off if we need to.  However, there are still a couple of problems to solve and you probably won't notice them right away.  For one, when browsing the web you will start to see these IE Script Error dialogs popping up:

IE Script Error dialog

This is even more annoying than the normal popup ads.  They occur because some scripts expect to have the popup window available and generate an error when it is not.  Because not all script writers add error handlers to their pages, IE acts as the default error handler and poups up its own cryptic dialog box.  Another problem is that some options on the Save As dialog under "Save as type" have disappeared, and there are various other UI problems when hosting IE -- like scroll bars going away when viewing help inside Visual Studio. 

To solve script error problem, I implemented IOleCommandTarget and added the methods for QueryStatus and Exec.  If I get an OLECMDID_SHOWSCRIPTERROR in Exec, I set pvaOut to VARIANT_TRUE to indicate that I wish to continue running scripts and return S_OK instead of the default to suppress the script error dialog.

class ATL_NO_VTABLE CPub : 
	public IObjectWithSiteImpl<CPUB>,
	public IDispatchImpl,
	public IDocHostUIHandler,
	public IOleCommandTarget
			
STDMETHOD(QueryStatus)(
	/*[in]*/ const GUID *pguidCmdGroup, 
	/*[in]*/ ULONG cCmds,
	/*[in,out][size_is(cCmds)]*/ OLECMD *prgCmds,
	/*[in,out]*/ OLECMDTEXT *pCmdText)
{
	return m_spDefaultOleCommandTarget->QueryStatus(pguidCmdGroup, cCmds,
		prgCmds, pCmdText);
}
	
STDMETHOD(Exec)(
	/*[in]*/ const GUID *pguidCmdGroup,
	/*[in]*/ DWORD nCmdID,
	/*[in]*/ DWORD nCmdExecOpt,
	/*[in]*/ VARIANTARG *pvaIn,
	/*[in,out]*/ VARIANTARG *pvaOut)
{
	if (nCmdID == OLECMDID_SHOWSCRIPTERROR)
	{
		// Don't show the error dialog, but

		// continue running scripts on the page.

		(*pvaOut).vt = VT_BOOL;
		(*pvaOut).boolVal = VARIANT_TRUE;
		return S_OK;
	}
	return m_spDefaultOleCommandTarget->Exec(pguidCmdGroup, nCmdID,
		nCmdExecOpt, pvaIn, pvaOut);
}
		

With our IOleCommandTarget implementation in place all unhandled script errors are gobbled up and the default IE Script Error dialogs never show up. 

I should mention that in my first implementation I was intercepting the HTMLWindowEvents2::onerror event which gets fired whenever an unhandled script error occurs.  It is indeed possible to do it that way, but you have to be careful to attach the handler to the top-level window.  If you attach to a child frame, then events in a sibling frame will not be caught (as pointed out by ferdo. Thanks!).  If you attach to the top-level window, then unhandled events will eventually bubble up to your handler.  In digging into that problem, I decided to scrap that method in favor of the IOleCommandTarget implementation just because I thought it was cleaner and easier to understand.

The Save As and UI problems are solved by simply calling the default handler from within each IDocHostUIHandler method . Using the pointer to the default handler we got during DISPID_DOCUMENTCOMPLETE, we call the default if we are not handling the method ourselves.

STDMETHOD(GetHostInfo)(DOCHOSTUIINFO FAR *pInfo)
{
	if (m_spDefaultDocHostUIHandler)
		return m_spDefaultDocHostUIHandler->GetHostInfo(pInfo);
	return S_OK;
}
		

It's not clear why these UI problems are not mentioned in MSDN documentation on BHOs (if they are, I missed them),  but they definately should be.

<shameless_plug>The latest version can be installed from the web at Osborn Technologies.</shameless_plug>

Improvements

There is still room for a number of improvements:

  • Make the sound and hotkeys user selectable.
  • Add an optional visual indicator that a popup window has been blocked.
  • Add a whitelist that will allow popups for an entire website or domain.

Revision History

  • April 26,2003: Fixed SHIFT+Click and untrapped script error problems.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here