Wednesday, October 10, 2007

When IHTMLWindow2::get_document returns E_ACCESSDENIED

Internet Explorer extensions usually needs to access HTML elements. When extensions are initialized they get a IWebBrowser2 pointer representing the browser. Starting with this pointer one can get any HTML element in the web page but to do that we need to browse a hierarchy of frames first. The simplest web pages only have one frame and one document. Web pages containing <frame> or <iframe> have a hierarchy of frames, each frame having its own document.

Here are the objects involved and the corresponding interfaces:
browser      - IWebBrowser2
frame/iframe - IHTMLWindow2
document - IHTMLDocument2
element - IHTMLElement


The list bellow shows what method to call to get one object from another:
browser      -> document        IWebBrowser2::get_Document
document -> frame IHTMLDocument2::get_parentWindow
frame -> document IHTMLWindow2::get_document
frame -> parent frame IHTMLWindow2::get_parent
frame -> children frames IHTMLWindow2::get_frames


A normal call chain to get a HTML element is:
browser -> document -> frame -> child frame -> ... -> child frame -> document -> element

This will work almost all the time. The problems arise when different frames contain documents loaded from different internet domains. In this case IHTMLWindow2::get_document returns E_ACCESSDENIED when trying to get the document from the frame object. I think this happens to prevent cross frame scripting atacks.

Here is HtmlWindowToHtmlDocument function I wrote to be used instead IHTMLWindow2::get_document to bypass the restriction:



// Converts a IHTMLWindow2 object to a IHTMLDocument2. Returns NULL in case of failure.
// It takes into account accessing the DOM across frames loaded from different domains.

CComQIPtr<IHTMLDocument2> HtmlWindowToHtmlDocument(CComQIPtr<IHTMLWindow2> spWindow)
{
ATLASSERT(spWindow != NULL);

CComQIPtr<IHTMLDocument2> spDocument;
HRESULT hRes = spWindow->get_document(&spDocument);

if ((S_OK == hRes) && (spDocument != NULL))
{
// The html document was properly retrieved.
return spDocument;
}

// hRes could be E_ACCESSDENIED that means a security restriction that
// prevents scripting across frames that loads documents from different internet domains.

CComQIPtr<IWebBrowser2> spBrws = HtmlWindowToHtmlWebBrowser(spWindow);
if (spBrws == NULL)
{
return CComQIPtr<IHTMLDocument2>();
}

// Get the document object from the IWebBrowser2 object.
CComQIPtr<IDispatch> spDisp;
hRes = spBrws->get_Document(&spDisp);
spDocument = spDisp;

return spDocument;
}


// Converts a IHTMLWindow2 object to a IWebBrowser2. Returns NULL in case of failure.
CComQIPtr<IWebBrowser2> HtmlWindowToHtmlWebBrowser(CComQIPtr<IHTMLWindow2> spWindow)
{
ATLASSERT(spWindow != NULL);

CComQIPtr<IServiceProvider> spServiceProvider = spWindow;
if (spServiceProvider == NULL)
{
return CComQIPtr<IWebBrowser2>();
}

CComQIPtr<IWebBrowser2> spWebBrws;
HRESULT hRes = spServiceProvider->QueryService(IID_IWebBrowserApp, IID_IWebBrowser2, (void**)&spWebBrws);
if (hRes != S_OK)
{
return CComQIPtr<IWebBrowser2>();
}

return spWebBrws;
}
Here is the C# version of the code: "When IHTMLWindow2.document throws UnauthorizedAccessException".

This technique was successfully implemented and tested in My web automation library.

19 comments:

John said...

Hi, just been using your code which works great. Thanks...

However a couple of minor points (and i am by no means a com expert so feel free to ignore)

You sometimes use CComQIPtr when CComPtr will do.
I think it is generally thought best to not accept CComPtr as parameters or return values..

In which case if you are return straight interface pointers, remember to detach them...

Finally, you should use SUCCEEDED(hRes) instead of comparing against S_OK - but probably ok in this case...

CodeCentrix said...

Interesting questions!
For the first one: I try to avoid raw COM pointers as much as possible to prevent memory leaks and other kind of potential flaws (you should know how ugly a QueryInterface call might look). I think the opposite: it is generally best to not accept raw pointers as parameters or return values or in any other places :)
I know that AddRef/Release calls occur but generally this is not an issue. It is a small penalty to pay instead of working with more than one type of COM pointers (raw pointers, smart pointers).

As far as I know, the difference between CComQIPtr and CComPtr is that CComPtr does not automatically call QueryInterface in its assignment operator because prior to Visual C++ 5.0, there was no way to associate the GUID of an interface with its native C++ type (_uuidof keyword). That's why CComQIPtr template class has a second argument that is the interface GUID. I checked into ATL source code and it seems that now CComPtr does call Query interface inside its assignment operator (VS 2003). I always use CComQIPtr pointers. You can never know when you'll need a QueryInterface :)

For the second question: for this particular case there's no difference. But I got into habit of using SUCCEEDED macro instead of checking against S_OK because in COM methods can return more than one success HRESULT code.

Anonymous said...

How could this code be modified to work with an HTMLWindow2Class object?

When I get the windows from a frame, they come back as HTMLWindow2Class even if I try to cast them to an IHTMLWindow2 object.

Example:

int i = 0;
object objectOfI = i;
IHTMLWindow2 window = frames.item(ref objectOfI);

So far, I have not been able to get the IHTMLWindow2 object to work...

Any ideas?
Thanks in advance :)

CodeCentrix said...

I assume you use C# language. In this case please check my last post:When IHTMLWindow2.document throws UnauthorizedAccessException

nd said...

Thanks for your wonderful article. I want to use the idea you've explored in yout article to reach the IWebBrowser2 or a running IE.

Though I have a valid CComPtr<IHTMLDocument2> spHTMLDoc2, as the put_bgColor() works fine, the get_parentWindow() doesn't work and returns a 0x00000000 in the spHTMLWindow2.

Can you please help me here? I'm a newbie to COM.


spHTMLDoc2->put_bgColor(CComVariant("blue"));

CComPtr<IHTMLWindow2> spHTMLWindow2;

hr = spHTMLDoc2->get_parentWindow(&spHTMLWindow2);

nd said...

Sorry for the typos in the previous post. I meant to say -

"I want to use the idea you've explored in your article to reach the IWebBrowser2 *of* a running IE."

jasond said...

I've been using this for a while, and just found the identity operator '===' doesn't work on the objects that are part of the frame.

var doc = window.external.getMainWindow().frames[0].document;
alert( document.body === document.body);

That alerts 'false'.

Any idea why this would be broken when using this technique to get the frame?

CodeCentrix said...

Could you explain in more detail what are you doing? What type of object is window.external.getMainWindow()? How do you set window.external object?

jasond said...

I've used IDocHostUIHandler.GetExternal to supply my 'object' to javascript. My getMainWindow() on that object uses your technique to get a window and return that to the script. If I then use the '===' operator on the elements of the document in that window, the operator always indicates 'false'.

CodeCentrix said...

You should use QueryService technique only when you have a cross domain security restrictions. In this case, it seems that a whole new document object is created so the the === operator returns false. I don't know why this happens.

Anonymous said...

Thank you.I just need the C sharp code.

Anonymous said...

Great code it works for me perfectly! Here is my code:

Dim htmlDoc As mshtml.HTMLDocument = WebBrowser1.Document.DomDocument
Dim htmlWnd As mshtml.IHTMLWindow2 = Nothing
Dim FrameDoc As mshtml.IHTMLDocument2 = Nothing
Dim fls As mshtml.HTMLEmbed = Nothing
For n As Integer = 0 To htmlDoc.frames.length - 1
htmlWnd = htmlDoc.frames.item(n)
FrameDoc = CodecentrixSample.CrossFrameIE.GetDocumentFromWindow(htmlWnd)
If FrameDoc.title.ToLower.Trim = IFrameTitle.ToLower.Trim Then
...
Exit For
End If
Next

Anonymous said...

Hello. And Bye.

shadow said...

Good day..

I know it's quite a long shot, but I have this problem.. And I've ran out of places to ask for help for..

I am supposed to make my webbrowser control scroll depending on what scrollType I pass. Well, everything worked fine... that is, in IE6.
My code crashes in IE7 at get_parentWindow, get_scrollHeight (and perhaps also in scrollBy). They all return access violations.

Trying other solutions on the net didn't help, IE7 still crashes with calling functions (like get_scrollTop and etc)... I already tried scrolling using IHTMLElement2. But no luck... I also tried IHTMLDocument3->get_documentElement and used that for scrolling, still... no sunshine in sight.

well, it probably is an IE thing, but do u by chance know a way to work around this? it's really not good to just restrict users from using my scrolling functions if they don't use ie6.

i am kinda desperate now.
will be forever grateful for the help.

shadow said...
This comment has been removed by the author.
shadow said...

//error trapping had been removed
BOOL CBrowserControl::Scroll(LONG scrollType)
{
CComPtr<IDispatch> spDoc;
long scrollPos;

m_spWebBrowser->get_Document(&spDoc);

IHTMLDocument2 *pDocument = NULL;
HRESULT hresult = spDoc->QueryInterface( IID_IHTMLDocument2, (void**)&pDocument );
//hresult is S_OK
IHTMLElement *pBody = NULL;
hresult = pDocument->get_body( &pBody );
IHTMLTextContainer *pElement = NULL;
hresult = pBody->QueryInterface(IID_IHTMLTextContainer,(void**)&pE lement);
//hresults are S_OK

IHTMLWindow2 *pWindow = NULL;
hresult = pDocument->get_parentWindow( &pWindow );
//the crash happens here. cannot read hresult anymore since an access violation happens upon calling the function

long scrollwidth, scrollheight;
pElement->get_scrollHeight(&scrollheight);
//a crash here occurs too if i try to comment the code above
pElement->get_scrollWidth(&scrollwidth);
//still crashes here

long page;
m_spWebBrowser->get_Height(&page);

switch(scrollType)
{
case StepDown:
scrollPos = page/5;
break;
case StepUp:
scrollPos = -page/5;
break;
case PageDown:
scrollPos = page;
break;
case PageUp:
scrollPos = -page;
break;
case ScrollBottom:
scrollPos = scrollheight;
break;
case ScrollTop:
scrollPos = -scrollheight;
break;
}
pWindow->scrollBy( 0, scrollPos );
return true;
}

CodeCentrix said...

Do you make use of threads in your code?

shadow said...

The entire application is not really my code. I'm only tasked to add this functionality... There is however use of the CCriticalSection in some areas, but it's not here in the class. The BrowserControl also has a TEventHandler.

Aakash said...

Hi CodeCentrix, thanks for the nice blog.
I am facing a problem with modifying the DOM.
I read on MSDN that QueryInterface and IDispatch interface are required to modify the DOM of current loaded htmldocument. But I am unable to get the reference of these method and interface in C#. Could you please advise what I am missing here.