13

With the advent of IE11, IHTMLWindow2::execScript() is deprecated. The recommended approach is to use eval() instead. I'm automating IE via its C++ COM interfaces, and I have been unable to find how to accomplish this. Can someone point me to the example I've obviously missed in my searching? If it's not possible to execute code via eval, what's the appropriate way to inject JavaScript code into a running instance of Internet Explorer now that execScript is no longer available?

EDIT: Any solution that will work for the project I'm working on must work out-of-process. I am not using a Browser Helper Object (BHO), or any type of IE plugin. Thus, any solution that involves an interface that cannot be properly marshaled cross-process won't work for me.

JimEvans
  • 27,201
  • 7
  • 83
  • 108
  • 1
    My guess is that you'd do it like you would in JavaScript, by adding a new script element to the DOM of the page, but I'm checking with the IE dev team... – EricLaw Aug 20 '13 at 22:46
  • 1
    @JimEvans, I don't have IE11 installed to try, but the following works for IE10 using `eval`: `CComDispatchDriver window = m_window; /* of IHTMLWindow2 */ window.Invoke1(L"eval", &CComVariant(L"alert(true)"));` – noseratio Aug 21 '13 at 00:20

1 Answers1

14

I have now verified the eval approach works consistently with IE9, IE10 and IE11 (error checks skipped for breavity):

CComVariant result;
CComDispatchDriver disp = m_htmlWindow; // of IHTMLWindow2
disp.Invoke1(L"eval", &CComVariant(L"confirm('See this?')"), &result);
result.ChangeType(VT_BSTR);
MessageBoxW(V_BSTR(&result));

Feels even better than execScript, because it actually returns the result. It works also in C# with WinForms' WebBrowser:

var result = webBrowser1.Document.InvokeScript("eval", new object[] { "confirm('see this?')" });
MessageBox.Show(result.ToString());

That said, execScript still works for IE11 Preview:

CComVariant result;
m_htmlWindow->execScript(CComBSTR(L"confirm('See this too?')"), CComBSTR(L"JavaScript"), &result);
result.ChangeType(VT_BSTR);
MessageBoxW(V_BSTR(&result));

And it still discards the result, as it always did.

A bit off-topic, but you don't have to stick with eval for this. This approach allows to execute any named method available inside the namespace of the JavaScript window object of the loaded page (via IDispatch interface). You may call your own function and pass a live COM object into it, rather than a string parameter, e.g.:

// JavaScript
function AlertUser(user)
{
  alert(user.name);
  return user.age;
}

// C++
CComDispatchDriver disp = m_htmlWindow; // of IHTMLWindow2
disp.Invoke1(L"AlertUser", &CComVariant(userObject), &result);

I'd prefer the above direct call to eval where possible.

[EDITED]

It takes some tweaks to make this approach work for out-of-process calls. As @JimEvans pointed out in the comments, Invoke was returning error 0x80020006 ("Unknown name"). However, a test HTA app worked just fine, what made me think to try IDispatchEx::GetDispId for name resolution. That indeed worked (error checks skipped):

CComDispatchDriver dispWindow;
htmlWindow->QueryInterface(&dispWindow);

CComPtr<IDispatchEx> dispexWindow;
htmlWindow->QueryInterface(&dispexWindow);

DISPID dispidEval = -1;
dispexWindow->GetDispID(CComBSTR("eval"), fdexNameCaseSensitive, &dispidEval);
dispWindow.Invoke1(dispidEval, &CComVariant("function DoAlert(text) { alert(text); }")); // inject

DISPID dispidDoAlert = -1;
dispexWindow->GetDispID(CComBSTR("DoAlert"), fdexNameCaseSensitive, &dispidDoAlert) );
dispWindow.Invoke1(dispidDoAlert, &CComVariant("Hello, World!")); // call

The full C++ test app is here: http://pastebin.com/ccZr0cG2

[UPDATE]

This update creates __execScript method on a window object of a child iframe, out-of-proc. The code to be injected was optimized to return the target window object for later use (no need to make a series of out-of-proc calls to obtain the iframe object, it's done in the context of the main window):

CComBSTR __execScriptCode(L"(window.__execScript = function(exp) { return eval(exp); }, window.self)");

Below is the code for C++ console app (pastebin), some error checks skipped for breavity. There's also a corresponding prototype in .HTA, which is more readable.

//
// http://stackoverflow.com/questions/18342200/how-do-i-call-eval-in-ie-from-c/18349546//
//

#include <tchar.h>
#include <ExDisp.h>
#include <mshtml.h>
#include <dispex.h>
#include <atlbase.h>
#include <atlcomcli.h>

#define _S(a) \
    { HRESULT hr = (a); if (FAILED(hr)) return hr; } 

#define disp_cast(disp) \
    ((CComDispatchDriver&)(void(static_cast<IDispatch*>(disp)), reinterpret_cast<CComDispatchDriver&>(disp)))

struct ComInit {
    ComInit() { ::CoInitialize(NULL); }
    ~ComInit() { CoUninitialize(); }
};

int _tmain(int argc, _TCHAR* argv[])
{
    ComInit comInit;

    CComPtr<IWebBrowser2> ie;
    _S( ie.CoCreateInstance(L"InternetExplorer.Application", NULL, CLSCTX_LOCAL_SERVER) );
    _S( ie->put_Visible(VARIANT_TRUE) );
    CComVariant ve;
    _S( ie->Navigate2(&CComVariant(L"http://jsfiddle.net/"), &ve, &ve, &ve, &ve) );

    // wait for page to finish loading
    for (;;)
    {
        Sleep(250);
        READYSTATE rs = READYSTATE_UNINITIALIZED;
        ie->get_ReadyState(&rs);
        if ( rs == READYSTATE_COMPLETE )
            break;
    }

    // inject __execScript into the main window

    CComPtr<IDispatch> dispDoc;
    _S( ie->get_Document(&dispDoc) );
    CComPtr<IHTMLDocument2> htmlDoc;
    _S( dispDoc->QueryInterface(&htmlDoc) );
    CComPtr<IHTMLWindow2> htmlWindow;
    _S( htmlDoc->get_parentWindow(&htmlWindow) );
    CComPtr<IDispatchEx> dispexWindow;
    _S( htmlWindow->QueryInterface(&dispexWindow) );

    CComBSTR __execScript("__execScript");
    CComBSTR __execScriptCode(L"(window.__execScript = function(exp) { return eval(exp); }, window.self)");

    DISPID dispid = -1;
    _S( dispexWindow->GetDispID(CComBSTR("eval"), fdexNameCaseSensitive, &dispid) );
    _S( disp_cast(dispexWindow).Invoke1(dispid, &CComVariant(__execScriptCode)) ); 

    // inject __execScript into the child frame

    WCHAR szCode[1024];
    wsprintfW(szCode, L"document.all.tags(\"iframe\")[0].contentWindow.eval(\"%ls\")", __execScriptCode.m_str);

    dispid = -1;
    _S( dispexWindow->GetDispID(__execScript, fdexNameCaseSensitive, &dispid) );
    CComVariant vIframe;
    _S( disp_cast(dispexWindow).Invoke1(dispid, &CComVariant(szCode), &vIframe) ); // inject __execScript and return the iframe's window object
    _S( vIframe.ChangeType(VT_DISPATCH) );

    CComPtr<IDispatchEx> dispexIframe;
    _S( V_DISPATCH(&vIframe)->QueryInterface(&dispexIframe) );

    dispid = -1;
    _S( dispexIframe->GetDispID(__execScript, fdexNameCaseSensitive, &dispid) );
    _S( disp_cast(dispexIframe).Invoke1(dispid, &CComVariant("alert(document.URL)")) ); // call the code inside child iframe

    return 0;
}
noseratio
  • 59,932
  • 34
  • 208
  • 486
  • You're absolutely right about calling any named function. That's exactly the approach my project takes. However, it's injecting that function _into_ the page where we are currently using `execScript`, and which needs to be replaced. N.B., we don't have any control whatsoever what pages our library may be used against, so we can't modify the source of the page to directly create the global function. – JimEvans Aug 21 '13 at 16:49
  • In this case you could use `eval` first to inject a new global function, the call it. It does work: `disp.Invoke1(L"eval", &CComVariant(L"function GlobalTest() { alert(true); }"));` – noseratio Aug 21 '13 at 17:00
  • Regarding your out-of-process concern, I haven't tried it out-of-proc, but I'm confident it will work as is. It's done via `IDispatch` which gets marshaled automatically. No extra plumbing needed. – noseratio Aug 21 '13 at 17:09
  • When I try using your code, or try using `GetIDsOfNames` on the IDispatch for `eval`, I receive an HRESULT of 0x80020006 ("Unknown name"), which leads me to believe that there's an in-process/out-of-process check in place. – JimEvans Aug 21 '13 at 17:36
  • Interesting. I've briefly tried automating IE from an .HTA app and `eval` still works out-of-proc: http://pastebin.com/cLR6FeJV. I'll try the same as a C++ app tomorrow. – noseratio Aug 21 '13 at 17:55
  • You were right, it did not work as is **for out-of-proc calls**. A solution was to use [IDispatchEx](http://msdn.microsoft.com/en-us/library/sky96ah7(v=vs.94).aspx) for name resolution. I've updated the answer. – noseratio Aug 21 '13 at 20:00
  • 1
    Your solution is fine as far as it goes, but it doesn't completely meet my needs. Attempting to execute script in the context of a frame/iframe won't work. `IDispatchEx::GetDispID` returns the same "Unknown name" HRESULT for the IHTMLWindow2 object associated with the frame. – JimEvans Aug 23 '13 at 13:48
  • 1
    Could you elaborate how you obtain the frame's window IHTMLWindow2 object out-of-proc? I've tried `browser.Document.all.tags("iframe")[0].contentWindow` and I'm getting *Permission denied* on the last step - `contentWindow` (IE10). – noseratio Aug 23 '13 at 14:21
  • 1
    That should work, and does in my environment, as long as you don't cross any Protected Mode boundaries along the way. The full code of my project is available [on GitHub](http://github.com/SeleniumHQ/selenium), in particular, in the IEDriverServer project therein. The `DocumentHost` class contains the code used to change frame focus. – JimEvans Aug 23 '13 at 14:44
  • It indeed works, the problem was that I tested it against a site with cross-domain frames. I just tried it against jsfiddle.net and `eval` still works: `ie.Document.all.tags("iframe")[0].contentWindow.eval("alert(true)")`, both HTA and C++. Perhaps, the frame's document must be fully navigated first for that to work. I'll be sure to check how it's done in IEDriverServer, just need a bit more of spare time. – noseratio Aug 23 '13 at 16:03
  • Well, right now, I'm using `execScript`, so this won't be an exact match. However, all script execution is localized to a single class, and I'd be happy to walk you through it at your convenience. – JimEvans Aug 23 '13 at 16:58
  • Here's another idea, an HTA prototype injecting into a child frame from the context of the main window: http://pastebin.com/1xK049DH. Here `eval` is called out-of-proc only for the main window. – noseratio Aug 24 '13 at 04:30
  • Based upon the fact that `eval` can actually return a result, here's an [improved version](http://pastebin.com/iM5829MM) of the above, which does: `var iframe = ie.Document.parentWindow.__execScript('document.all.tags("iframe")[0].contentWindow');`. I'm going to verify this with C++. – noseratio Aug 24 '13 at 06:51
  • Thought I'd link here a [C# version](http://stackoverflow.com/a/18546866/1768303) of `IDispatchEx` invoker , for relevancy. – noseratio Sep 02 '13 at 09:49
  • `GetDispID` with "eval" doesn't work in IE8. Using "Function" (the function constructor) works though. – Rob W Nov 11 '13 at 11:11
  • @Robw, I haven't tried it under IE8, but generally for `eval` to work, but there has to be at least one ` – noseratio Nov 11 '13 at 11:46
  • I have tried to use this approach and modified Selenium IEDriverServer to use "eval" in hope it will fix the access denied issues I see. Unfortunately, it did not. :( – wilx Apr 26 '17 at 11:27
  • 1
    @wilx i'm a little late, but you can keep using execScript: https://stackoverflow.com/a/31605264/1160796 – basher Nov 05 '18 at 23:01
  • @basher isn't it ironic how we're stuck with supporting IE11 forever? – noseratio Nov 05 '18 at 23:31
  • @noseratio I love me some automation with shDocVw.InternetExplorer. As painful as IE11 is, I'm afraid of what would happen if it turned into Edge. Things would start randomly breaking because someone installed X windows update. At least IE11 isn't changing. Do they even offer an edge version? btw - I learned the `.Navigate(); ... await xxx.Task; dostuff` from some of your code samples elsewhere on SO a while ago. I was using like `function x_documentcomplete { dostuff..` instead of the delegate with `await`. Holy hell you cleaned up my code. Way more readable now that it's serial. Thank you – basher Nov 06 '18 at 23:57
  • 1
    @basher, yeah they now have [WebDriver for Edge](https://blogs.windows.com/msedgedev/2018/06/14/webdriver-w3c-recommendation-feature-on-demand/) which they promise to keep updating along side with Edge. But there is another emerging trend: ElectronJS. That allows to abandon the IE legacy and wrap the whole web app with a Desktop envelop so it will run not only on Win7 but also on Mac an Linux, using the cutting-edge web standards (including `async/await` that you liked). [I'm jumping on that wagon, too](https://github.com/noseratio/electron-quick-start) ;-) – noseratio Nov 07 '18 at 00:24
  • @noseratio very nice, i'll have to check it out. Thanks! – basher Nov 07 '18 at 14:16