How do I call eval() in IE from C++? - c++

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.

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;
}

Related

Missing data in Windows file properties dialog when opened by ShellExecuteEx

I want to show the Windows file properties dialog for a file from my C++ code (on Windows 7, using VS 2012). I found the following code in this answer (which also contains a full MCVE). I also tried calling CoInitializeEx() first, as mentioned in the documentation of ShellExecuteEx():
// Whether I initialize COM or not doesn't seem to make a difference.
CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
SHELLEXECUTEINFO info = {0};
info.cbSize = sizeof info;
info.lpFile = L"D:\\Test.txt";
info.nShow = SW_SHOW;
info.fMask = SEE_MASK_INVOKEIDLIST;
info.lpVerb = L"properties";
ShellExecuteEx(&info);
This code works, i.e. the properties dialog is shown and ShellExecuteEx() returns TRUE. However, in the Details tab, the size property is wrong and the date properties are missing:
The rest of the properties in the Details tab (e.g. the file attributes) are correct. Strangely, the size and date properties are shown correctly in the General tab (left-most tab).
If I open the properties window via the Windows Explorer (file → right-click → Properties), then all properties in the Details tab are shown correctly:
I tried it with several files and file types (e.g. txt, rtf, pdf) on different drives and on three different PCs (1x German 64-bit Windows 7, 1x English 64-bit Windows 7, 1x English 32-bit Windows 7). I always get the same result, even if I run my program as administrator. On (64-bit) Windows 8.1 the code is working for me, though.
My original program in which I discovered the problem is an MFC application, but I see the same problem if I put the above code into a console application.
What do I have to do to show the correct values in the Details tab on Windows 7? Is it even possible?
As Raymond Chen suggested, replacing the path with a PIDL (SHELLEXECUTEINFO::lpIDList) makes the properties dialog correctly show the size and date fields under Windows 7 when invoked through ShellExecuteEx().
It seems that the Windows 7 implementation of ShellExecuteEx() is buggy since newer versions of the OS do not have an issue with SHELLEXCUTEINFO::lpFile.
There is another solution possible that involves creating an instance of IContextMenu and calling the IContextMenu::InvokeCommand() method. I guess this is what ShellExecuteEx() does under the hood. Scroll down to Solution 2 for example code.
Solution 1 - using a PIDL with ShellExecuteEx
#include <atlcom.h> // CComHeapPtr
#include <shlobj.h> // SHParseDisplayName()
#include <shellapi.h> // ShellExecuteEx()
// CComHeapPtr is a smart pointer that automatically calls CoTaskMemFree() when
// the current scope ends.
CComHeapPtr<ITEMIDLIST> pidl;
SFGAOF sfgao = 0;
// Convert the path into a PIDL.
HRESULT hr = ::SHParseDisplayName( L"D:\\Test.txt", nullptr, &pidl, 0, &sfgao );
if( SUCCEEDED( hr ) )
{
// Show the properties dialog of the file.
SHELLEXECUTEINFO info{ sizeof(info) };
info.hwnd = GetSafeHwnd();
info.nShow = SW_SHOWNORMAL;
info.fMask = SEE_MASK_INVOKEIDLIST;
info.lpIDList = pidl;
info.lpVerb = L"properties";
if( ! ::ShellExecuteEx( &info ) )
{
// Make sure you don't put ANY code before the call to ::GetLastError()
// otherwise the last error value might be invalidated!
DWORD err = ::GetLastError();
// TODO: Do your error handling here.
}
}
else
{
// TODO: Do your error handling here
}
This code works for me under both Win 7 and Win 10 (other versions not tested) when called from a button click handler of a simple dialog-based MFC application.
It also works for console applications if you set info.hwnd to NULL (simply remove the line info.hwnd = GetSafeHwnd(); from the example code as it is already initialized with 0). In the SHELLEXECUTEINFO reference it is stated that the hwnd member is optional.
Don't forget the mandatory call to CoInitialize() or CoInitializeEx() at the startup of your application and CoUninitialize() at shutdown to properly initialize and deinitialize COM.
Notes:
CComHeapPtr is a smart pointer included in ATL that automatically calls CoTaskMemFree() when the scope ends. It's an ownership-transferring pointer with semantics similar to the deprecated std::auto_ptr. That is, when you assign a CComHeapPtr object to another one, or use the constructor that has a CComHeapPtr parameter, the original object will become a NULL pointer.
CComHeapPtr<ITEMIDLIST> pidl2( pidl1 ); // pidl1 allocated somewhere before
// Now pidl1 can't be used anymore to access the ITEMIDLIST object.
// It has transferred ownership to pidl2!
I'm still using it because it is ready to use out-of-the-box and plays well together with the COM APIs.
Solution 2 - using IContextMenu
The following code requires Windows Vista or newer as I'm using the "modern" IShellItem API.
I wrapped the code into a function ShowPropertiesDialog() that takes a window handle and a filesystem path. If any error occurs, the function throws a std::system_error exception.
#include <atlcom.h>
#include <string>
#include <system_error>
/// Show the shell properties dialog for the given filesystem object.
/// \exception Throws std::system_error in case of any error.
void ShowPropertiesDialog( HWND hwnd, const std::wstring& path )
{
using std::system_error;
using std::system_category;
if( path.empty() )
throw system_error( std::make_error_code( std::errc::invalid_argument ),
"Invalid empty path" );
// SHCreateItemFromParsingName() returns only a generic error (E_FAIL) if
// the path is incorrect. We can do better:
if( ::GetFileAttributesW( path.c_str() ) == INVALID_FILE_ATTRIBUTES )
{
// Make sure you don't put ANY code before the call to ::GetLastError()
// otherwise the last error value might be invalidated!
DWORD err = ::GetLastError();
throw system_error( static_cast<int>( err ), system_category(), "Invalid path" );
}
// Create an IShellItem from the path.
// IShellItem basically is a wrapper for an IShellFolder and a child PIDL, simplifying many tasks.
CComPtr<IShellItem> pItem;
HRESULT hr = ::SHCreateItemFromParsingName( path.c_str(), nullptr, IID_PPV_ARGS( &pItem ) );
if( FAILED( hr ) )
throw system_error( hr, system_category(), "Could not get IShellItem object" );
// Bind to the IContextMenu of the item.
CComPtr<IContextMenu> pContextMenu;
hr = pItem->BindToHandler( nullptr, BHID_SFUIObject, IID_PPV_ARGS( &pContextMenu ) );
if( FAILED( hr ) )
throw system_error( hr, system_category(), "Could not get IContextMenu object" );
// Finally invoke the "properties" verb of the context menu.
CMINVOKECOMMANDINFO cmd{ sizeof(cmd) };
cmd.lpVerb = "properties";
cmd.hwnd = hwnd;
cmd.nShow = SW_SHOWNORMAL;
hr = pContextMenu->InvokeCommand( &cmd );
if( FAILED( hr ) )
throw system_error( hr, system_category(),
"Could not invoke the \"properties\" verb from the context menu" );
}
In the following I show an example of how to use ShowPropertiesDialog() from a button handler of a CDialog-derived class. Actually ShowPropertiesDialog() is independent from MFC, as it just needs a window handle, but OP mentioned that he wants to use the code in an MFC app.
#include <sstream>
#include <codecvt>
// Convert a multi-byte (ANSI) string returned from std::system_error::what()
// to Unicode (UTF-16).
std::wstring MultiByteToWString( const std::string& s )
{
std::wstring_convert< std::codecvt< wchar_t, char, std::mbstate_t >> conv;
try { return conv.from_bytes( s ); }
catch( std::range_error& ) { return {}; }
}
// A button click handler.
void CMyDialog::OnPropertiesButtonClicked()
{
std::wstring path( L"c:\\temp\\test.txt" );
// The code also works for the following paths:
//std::wstring path( L"c:\\temp" );
//std::wstring path( L"C:\\" );
//std::wstring path( L"\\\\127.0.0.1\\share" );
//std::wstring path( L"\\\\127.0.0.1\\share\\test.txt" );
try
{
ShowPropertiesDialog( GetSafeHwnd(), path );
}
catch( std::system_error& e )
{
std::wostringstream msg;
msg << L"Could not open the properties dialog for:\n" << path << L"\n\n"
<< MultiByteToWString( e.what() ) << L"\n"
<< L"Error code: " << e.code();
AfxMessageBox( msg.str().c_str(), MB_ICONERROR );
}
}

ImpersonateLoggedOnUser is successful but secondary process is still run in the initial context

I have an installer that tries to (re)start my application in the current user context after the installation is done.
The installer runs in the SYSTEM context and before launching the application it attempts (and theoretically succeeds) to impersonate the current user. However, when I look in the task manager, I see that my application is running in the SYSTEM context.
This is (a snippet from) my code:
TCHAR szUsername[128] = _T("");
DWORD dwUsernameSize = 128;
GetUserName(szUsername, &dwUsernameSize);
// Lets the calling process impersonate the security context of a logged-on user.
if (!ImpersonateLoggedOnUser(hToken))
{
throw Win32Exception(GetLastError(), _T("Failed to impersonate current user"));
}
TCHAR szUsername2[128] = _T("");
DWORD dwUsernameSize2 = 128;
GetUserName(szUsername2, &dwUsernameSize2);
MLOGD(_T("ProcessUtils::StartProcessInCurrentUserContext: Successfully impersonated %s"), szUsername2);
ProcessUtils::StartProcess(sExeName, lstParams, sWorkingDir, bWaitToFinish, errCode);
ProcessUtils::StartProcess is a wrapper around CreateProcess.
szUsername contains SYSTEM and szUsername2 contains the current user. So ImpersonateLoggedOnUser is successful.
However, as mentioned above, the process is started in the SYSTEM context, not the current user one.
I'm not sure how helpful this might be, but my installer is written in NSIS and it's calling the function that contains the code from above via a plugin written in C/C++.
Does anyone know why my application doesn't start in the current user context?
Win32 CreateProcess creates a process in the same security context as the caller which is SYSTEM (even though you are impersonating).
Think you need to be calling CreateProcessAsUser.
I had a very similar problem a couple of years ago when I was also
working on an installer application. After A LOT of frustration, caused
by failed attempts to start an application in the context of the current
user using CreateProcessAsUser, I've finally given up. After a thorough
search on the web, I've found a briliant implementation that uses
IShellDispatch2 interface. Here is an example:
#include <Windows.h>
#include <exdisp.h>
#include <Shobjidl.h>
#include <Shlwapi.h>
#include <comutil.h>
#include <SHLGUID.h>
#include <cstdlib>
#include <iostream>
#pragma comment(lib, "Shlwapi.lib")
#pragma comment(lib, "comsuppw.lib")
bool ShellExecuteAsCurrentUser(const TCHAR *pcOperation, const TCHAR *pcFileName, const TCHAR *pcParameters,
const TCHAR *pcsDirectory, const DWORD dwShow)
{
bool bSuccess = false;
IShellWindows *psw = NULL;
HRESULT hr = CoCreateInstance(CLSID_ShellWindows, NULL, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&psw));
if(SUCCEEDED(hr))
{
HWND hwnd = 0;
IDispatch* pdisp = NULL;
_variant_t vEmpty;
if(S_OK == psw->FindWindowSW(&vEmpty, &vEmpty, SWC_DESKTOP, reinterpret_cast<long*>(&hwnd), SWFO_NEEDDISPATCH, &pdisp))
{
if((hwnd != NULL) && (hwnd != INVALID_HANDLE_VALUE))
{
IShellBrowser *psb;
hr = IUnknown_QueryService(pdisp, SID_STopLevelBrowser, IID_PPV_ARGS(&psb));
if(SUCCEEDED(hr))
{
IShellView *psv = NULL;
hr = psb->QueryActiveShellView(&psv);
if(SUCCEEDED(hr))
{
IDispatch *pdispBackground = NULL;
HRESULT hr = psv->GetItemObject(SVGIO_BACKGROUND, IID_PPV_ARGS(&pdispBackground));
if(SUCCEEDED(hr))
{
IShellFolderViewDual *psfvd = NULL;
hr = pdispBackground->QueryInterface(IID_PPV_ARGS(&psfvd));
if(SUCCEEDED(hr))
{
IDispatch *pdisp = NULL;
hr = psfvd->get_Application(&pdisp);
if(SUCCEEDED(hr))
{
IShellDispatch2 *psd;
hr = pdisp->QueryInterface(IID_PPV_ARGS(&psd));
if(SUCCEEDED(hr))
{
_variant_t verb(pcOperation);
_variant_t file(pcFileName);
_variant_t para(pcParameters);
_variant_t dir(pcsDirectory);
_variant_t show(dwShow);
if(SUCCEEDED(psd->ShellExecute(file.bstrVal, para, vEmpty, verb, show)))
bSuccess = true;
psd->Release();
psd = NULL;
}
pdisp->Release();
pdisp = NULL;
}
}
pdispBackground->Release();
pdispBackground = NULL;
}
psv->Release();
psv = NULL;
}
psb->Release();
psb = NULL;
}
}
pdisp->Release();
pdisp = NULL;
}
psw->Release();
psw = NULL;
}
return bSuccess;
}
int main(int argc, char *argv[])
{
CoInitialize(NULL);
if(ShellExecuteAsCurrentUser(L"open", L"notepad", nullptr, nullptr, SW_SHOWNORMAL))
std::cout << "SUCCESS" << std::endl;
CoUninitialize();
return 0;
}
This is just a quick demo, the implementation of ShellExecuteAsCurrentUser can be
improved by using smart pointers for COM interfaces and some refactoring. This method
worked for me on versions WinXP SP3 - Win 8.1, not sure if it works on Windows 10. For
more details, check the authors github page:
https://github.com/lordmulder/stdutils/tree/master/Contrib/StdUtils
If you had read the documentation for CreateProcess, you would have found the answer to your question in the first three sentences:
Creates a new process and its primary thread. The new process runs in the security context of the calling process.
If the calling process is impersonating another user, the new process uses the token for the calling process, not the impersonation token.
There really isn't much else to say; the behaviour you describe is as documented. If you want to create a process as another user, you must use CreateProcessAsUser or one of the related functions.

Problems accessing a COM interface in C++

What I want to do is access a COM interface and then call the "Open" method of that interface.
I have a sample code in Visual Basic which works fine, but I need to write it in C++ and I can't seem to get it to work.
First of all, this is the working VB code:
Dim CANapeApplication As CANAPELib.Application
CANapeApplication = CreateObject("CANape.Application")
Call CANapeApplication.Open("C:\Users\Public\Documents\Vector\CANape\12\Project", 0)
CANape.Application is the ProgID which selects the interface I need.
After reading some docs at msdn.microsoft.com and this question, I wrote this code:
void ErrorDescription(HRESULT hr); //Function to output a readable hr error
int InitCOM();
int OpenCANape();
// Declarations of variables used.
HRESULT hresult;
void **canApeAppPtr;
IDispatch *pdisp;
CLSID ClassID;
DISPID FAR dispid;
UINT nArgErr;
OLECHAR FAR* canApeWorkingDirectory = L"C:\\Users\\Public\\Documents\\Vector\\CANape\\12\\Project";
int main(){
// Instantiate CANape COM interface
if (InitCOM() != 0) {
std::cout << "init error";
return 1;
}
// Open CANape
if (OpenCANape() != 0) {
std::cout << "Failed to open CANape Project" << std::endl;
return 1;
}
CoUninitialize();
return 0;
}
void ErrorDescription(HRESULT hr) {
if(FACILITY_WINDOWS == HRESULT_FACILITY(hr))
hr = HRESULT_CODE(hr);
TCHAR* szErrMsg;
if(FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER|FORMAT_MESSAGE_FROM_SYSTEM,
NULL, hr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPTSTR)&szErrMsg, 0, NULL) != 0)
{
_tprintf(TEXT("%s"), szErrMsg);
LocalFree(szErrMsg);
} else
_tprintf( TEXT("[Could not find a description for error # %#x.]\n"), hr);
}
int InitCOM() {
// Initialize OLE DLLs.
hresult = OleInitialize(NULL);
if (!SUCCEEDED(hresult)) {
ErrorDescription(hresult);
return 1;
}
// Get CLSID from ProgID
//hresult = CLSIDFromProgID(OLESTR("CANape.Application"), &ClassID);
hresult = CLSIDFromProgID(OLESTR("CanapeCom.CanapeCom"), &ClassID);
if (!SUCCEEDED(hresult)) {
ErrorDescription(hresult);
return 1;
}
// OLE function CoCreateInstance starts application using GUID/CLSID
hresult = CoCreateInstance(ClassID, NULL, CLSCTX_LOCAL_SERVER,
IID_IDispatch, (void **)&pdisp);
if (!SUCCEEDED(hresult)) {
ErrorDescription(hresult);
return 1;
}
// Call QueryInterface to see if object supports IDispatch
hresult = pdisp->QueryInterface(IID_IDispatch, (void **)&pdisp);
if (!SUCCEEDED(hresult)) {
ErrorDescription(hresult);
return 1;
}
std::cout << "success" << std::endl;
return 0;
}
int OpenCANape() {
//Method name
OLECHAR *szMember = L"Open";
// Retrieve the dispatch identifier for the Open method
// Use defaults where possible
DISPID idFileExists;
hresult = pdisp->GetIDsOfNames(
IID_NULL,
&szMember,
1,
LOCALE_SYSTEM_DEFAULT,
&idFileExists);
if (!SUCCEEDED(hresult)) {
std::cout << "GetIDsOfNames: ";
ErrorDescription(hresult);
return 1;
}
unsigned int puArgErr = 0;
VARIANT VarResult;
VariantInit(&VarResult);
DISPPARAMS pParams;
memset(&pParams, 0, sizeof(DISPPARAMS));
pParams.cArgs = 2;
VARIANT Arguments[2];
VariantInit(&Arguments[0]);
pParams.rgvarg = Arguments;
pParams.cNamedArgs = 0;
pParams.rgvarg[0].vt = VT_BSTR;
pParams.rgvarg[0].bstrVal = SysAllocString(canApeWorkingDirectory);
pParams.rgvarg[1].vt = VT_INT;
pParams.rgvarg[1].intVal = 0; // debug mode
// Invoke the method. Use defaults where possible.
hresult = pdisp->Invoke(
dispid,
IID_NULL,
LOCALE_SYSTEM_DEFAULT,
DISPATCH_METHOD,
&pParams,
&VarResult,
NULL,
&puArgErr
);
SysFreeString(pParams.rgvarg[0].bstrVal);
if (!SUCCEEDED(hresult)) {
ErrorDescription(hresult);
return 1;
}
return 0;
}
There are several problems with this.
Using the ClassID received from CLSIDFromProgID as the first parameter of CoCreateInstance does not work, it returns the error: class not registered
If i use the ProgID CanapeCom.CanapeCom (I found it by looking in the Registry), CoCreateInstance works. However, when I use pdisp->GetIDsOfNames I get the error message: Unkown name. Which I think means that the method was not found. That seems logical because I've used a different ProgID, but I just can't figure out how to get to the interface I'm looking for.
I have also tried to use the resulting CLSID from CLSIDFromProgID(OLESTR("CANape.Application"), &ClassID); as the 4th argument of CoCreateInstance but that resulted in a "No such interface supported" error.
Do I need the dll file of the software? In the VB example the dll file is used to get the interface and then create a new object using the ProgID. I'm not sure if I need to do the same in C++ or how this should work.
I'm really stuck here and hope that someone can help me.
Thanks for your comments.
I've fixed the problem, although the solution is kind of embarrassing...
In my defense, I'm still a student and new to this kind of stuff.
I've used the Process Monitor to check what happens when I execute the VB script.
I saw that the CLSID used there is the ID returned by CLSIDFromProgID(OLESTR("CANape.Application"), &ClassID);, which meant that this had to be the right one and the problem had to be somewhere else. I've looked again at the CoCreateInstance and then took a look at the other parameters. Turns out that the context CLSCTX_LOCAL_SERVER was wrong, it has to be CLSCTX_INPROC_SERVER. I don't know why I've set it to local_server in the first place or why I've never questioned it. I wrote that part of the code a few days ago and then focused too much on the CLSID and IID rather than on the other parameters.
I've also taken the first comment from Alex into account and created a tlb file.
This is a simplified version of the code that works:
#import "CANape.tlb"
int _tmain(int argc, _TCHAR* argv[])
{
_bstr_t path = "C:\\Users\\Public\\Documents\\Vector\\CANape\\12\\Project";
CLSID idbpnt;
CoInitialize(NULL);
HRESULT hr = CLSIDFromProgID (L"CANape.Application", &idbpnt);
CANAPELib::IApplication *app;
hr = CoCreateInstance(idbpnt,NULL,CLSCTX_INPROC_SERVER,__uuidof(CANAPELib::IApplication),(LPVOID*)&app );
app->Open(path,0);
CoUninitialize();
return 0;
}

How to automate an IE webapp that pops a modal HTML dialog?

[Revised yet again for clarity]
I have a C++ program which interacts with a website. The site is IE-specific, and so is my program.
I'm connecting to the running instance of IE in an ordinary way (out of process -- see code). Once I get the IWebBrowser2, I have no problem getting the IHTMLDocument2 and interacting with the individual IHTMLElement objects, filling in fields and clicking buttons.
But if the web page has javascript that calls window.showModalDialog, I'm stuck: I need to interact with the HTML elements in the popup, just like the other pages; but I can't seem to get its IWebBrowser2.
The popup is always titled "Web Page Dialog", and is a window of type Internet Explorer_TridentDlgFrame containing an Internet Explorer_Server. But I can't get the IWebBrowser2 from the Internet Explorer_Server window the way I can when it's a normal IE instance.
I can get the IHTMLDocument2Ptr, but when I try to get the IWebBrowser2 I get an HRESULT of E_NOINTERFACE.
The code is pretty standard stuff, and works fine if it's a 'normal' IE window
IHTMLDocument2Ptr pDoc;
LRESULT lRes;
/* hWndChild is an instance of class "Internet Explorer_Server" */
UINT nMsg = ::RegisterWindowMessage( "WM_HTML_GETOBJECT" );
::SendMessageTimeout( hWndChild, nMsg, 0L, 0L, SMTO_ABORTIFHUNG, 1000,
(DWORD*)&lRes );
LPFNOBJECTFROMLRESULT pfObjectFromLresult =
(LPFNOBJECTFROMLRESULT)::GetProcAddress( hInst, "ObjectFromLresult" );
if ( pfObjectFromLresult != NULL )
{
HRESULT hr;
hr = (*pfObjectFromLresult)( lRes, IID_IHTMLDocument, 0, (void**)&pDoc );
if ( SUCCEEDED(hr) ) {
IServiceProvider *pService;
hr = pDoc->QueryInterface(IID_IServiceProvider, (void **) &pService);
if ( SUCCEEDED(hr) )
{
hr = pService->QueryService(SID_SWebBrowserApp,
IID_IWebBrowser2, (void **) &pBrowser);
// This is where the problem occurs:
// hr == E_NOINTERFACE
}
}
}
In case it matters, this is Vista and IE8. (I emphasize this because both of these introduced breaking changes in my codebase, which had worked fine with XP/IE7.)
Once again, my goal is to get each IHTMLElement and interact with it. I don't have access to the source code of the application which I'm automating.
I'm considering sending keystrokes blindly to the Internet Explorer_Server window, but would rather not.
Edited to add:
Someone suggested getting the child windows and sending them messages, but I'm pretty sure that doesn't work with Internet Explorer_Server; according to Spy++, there's aren't any child windows. (This is not IE-specific. Java applets don't seem to have child windows, either.)
Update
In the comments, Simon Maurer said the above code worked for him, and just to make sure there were no typos he very generously posted a complete standalone app on pastebin. When I used his code, it failed in the same way in the same place, and I realized he thought I wanted to connect to the underlying page, not the popup. So I've edited the text above to remove that ambiguity.
I don't know why you want to get the IServiceProvider or IWebBrowser2 if you just want the IHTMLElement's. You can get them by calling the IHTMLDocument's get_all() method.
This code snippet shows you how this works:
#include <Windows.h>
#include <mshtml.h>
#include <Exdisp.h>
#include <atlbase.h>
#include <SHLGUID.h>
#include <oleacc.h>
#include <comdef.h>
#include <tchar.h>
HRESULT EnumElements(HINSTANCE hOleAccInst, HWND child)
{
HRESULT hr;
UINT nMsg = ::RegisterWindowMessage(_T("WM_HTML_GETOBJECT"));
LRESULT lRes = 0;
::SendMessageTimeout(child, nMsg, 0L, 0L, SMTO_ABORTIFHUNG, 1000, (PDWORD)&lRes);
LPFNOBJECTFROMLRESULT pfObjectFromLresult = (LPFNOBJECTFROMLRESULT)::GetProcAddress(hOleAccInst, "ObjectFromLresult");
if (pfObjectFromLresult == NULL)
return S_FALSE;
CComPtr<IHTMLDocument2> spDoc;
hr = (*pfObjectFromLresult)(lRes, IID_IHTMLDocument2, 0, (void**)&spDoc);
if (FAILED(hr)) return hr;
CComPtr<IHTMLElementCollection> spElementCollection;
hr = spDoc->get_all(&spElementCollection);
if (FAILED(hr)) return hr;
CComBSTR url;
spDoc->get_URL(&url);
printf("URL: %ws\n", url);
long lElementCount;
hr = spElementCollection->get_length(&lElementCount);
if (FAILED(hr)) return hr;
printf("Number of elements: %d", lElementCount);
VARIANT vIndex; vIndex.vt = VT_I4;
VARIANT vSubIndex; vSubIndex.vt = VT_I4; vSubIndex.lVal = 0;
for (vIndex.lVal = 0; vIndex.lVal < lElementCount; vIndex.lVal++)
{
CComPtr<IDispatch> spDispatchElement;
if (FAILED(spElementCollection->item(vIndex, vSubIndex, &spDispatchElement)))
continue;
CComPtr<IHTMLElement> spElement;
if (FAILED(spDispatchElement->QueryInterface(IID_IHTMLElement, (void**)&spElement)))
continue;
CComBSTR tagName;
if (SUCCEEDED(spElement->get_tagName(&tagName)))
{
printf("%ws\n", tagName);
}
}
return S_OK;
}
int _tmain(int argc, _TCHAR* argv[])
{
::CoInitialize(NULL);
HINSTANCE hInst = ::LoadLibrary(_T("OLEACC.DLL"));
if (hInst != NULL)
{
HRESULT hr = EnumElements(hInst, (HWND)0x000F05E4); // Handle to Internet Explorer_Server determined with Spy++ :)
::FreeLibrary(hInst);
}
::CoUninitialize();
return 0;
}
Above code works on both: a normal window or a modal window, just pass the correct HWND to the SendMessageTimeout function.
WARNING I use a hard-coded HWND value in this example, if you want to test it you should start an IE instance and get the HWND of the Internet Explorer_Server window using Spy++.
I also advise you to use CComPtr to avoid memory leaks.

How do I retrieve IPIEHTMLDocument2 interface on IE Mobile

I wrote an Active X plugin for IE7 which implements IObjectWithSite besides some other necessary interfaces (note no IOleClient). This interface is queried and called by IE7. During the SetSite() call I retrieve a pointer to IE7's site interface which I can use to retrieve the IHTMLDocument2 interface using the following approach:
IUnknown *site = pUnkSite; /* retrieved from IE7 during SetSite() call */
IServiceProvider *sp = NULL;
IHTMLWindow2 *win = NULL;
IHTMLDocument2 *doc = NULL;
if(site) {
site->QueryInterface(IID_IServiceProvider, (void **)&sp);
if(sp) {
sp->QueryService(IID_IHTMLWindow2, IID_IHTMLWindow2, (void **)&win);
if(win) {
win->get_document(&doc);
}
}
}
if(doc) {
/* found */
}
I tried a similiar approach on PIE as well using the following code, however, even the IPIEHTMLWindow2 interface cannot be acquired, so I'm stuck:
IUnknown *site = pUnkSite; /* retrieved from PIE during SetSite() call */
IPIEHTMLWindow2 *win = NULL;
IPIEHTMLDocument1 *tmp = NULL;
IPIEHTMLDocument2 *doc = NULL;
if(site) {
site->QueryInterface(__uuidof(*win), (void **)&win);
if(win) { /* never the case */
win->get_document(&tmp);
if(tmp) {
tmp->QueryInterface(__uuidof(*doc), (void **)&doc);
}
}
}
if(doc) {
/* found */
}
Using the IServiceProvider interface doesn't work either, so I already tested this.
Any ideas?
I found the following code in the Google Gears code, here. I copied the functions I think you need to here. The one you need is at the bottom (GetHtmlWindow2), but the other two are needed as well. Hopefully I didn't miss anything, but if I did the stuff you need is probably at the link.
#ifdef WINCE
// We can't get IWebBrowser2 for WinCE.
#else
HRESULT ActiveXUtils::GetWebBrowser2(IUnknown *site, IWebBrowser2 **browser2) {
CComQIPtr<IServiceProvider> service_provider = site;
if (!service_provider) { return E_FAIL; }
return service_provider->QueryService(SID_SWebBrowserApp,
IID_IWebBrowser2,
reinterpret_cast<void**>(browser2));
}
#endif
HRESULT ActiveXUtils::GetHtmlDocument2(IUnknown *site,
IHTMLDocument2 **document2) {
HRESULT hr;
#ifdef WINCE
// Follow path Window2 -> Window -> Document -> Document2
CComPtr<IPIEHTMLWindow2> window2;
hr = GetHtmlWindow2(site, &window2);
if (FAILED(hr) || !window2) { return false; }
CComQIPtr<IPIEHTMLWindow> window = window2;
CComPtr<IHTMLDocument> document;
hr = window->get_document(&document);
if (FAILED(hr) || !document) { return E_FAIL; }
return document->QueryInterface(__uuidof(*document2),
reinterpret_cast<void**>(document2));
#else
CComPtr<IWebBrowser2> web_browser2;
hr = GetWebBrowser2(site, &web_browser2);
if (FAILED(hr) || !web_browser2) { return E_FAIL; }
CComPtr<IDispatch> doc_dispatch;
hr = web_browser2->get_Document(&doc_dispatch);
if (FAILED(hr) || !doc_dispatch) { return E_FAIL; }
return doc_dispatch->QueryInterface(document2);
#endif
}
HRESULT ActiveXUtils::GetHtmlWindow2(IUnknown *site,
#ifdef WINCE
IPIEHTMLWindow2 **window2) {
// site is javascript IDispatch pointer.
return site->QueryInterface(__uuidof(*window2),
reinterpret_cast<void**>(window2));
#else
IHTMLWindow2 **window2) {
CComPtr<IHTMLDocument2> html_document2;
// To hook an event on a page's window object, follow the path
// IWebBrowser2->document->parentWindow->IHTMLWindow2
HRESULT hr = GetHtmlDocument2(site, &html_document2);
if (FAILED(hr) || !html_document2) { return E_FAIL; }
return html_document2->get_parentWindow(window2);
#endif
}
Well I was aware of the gears code already. The mechanism gears uses is based on a workaround through performing an explicit method call into the gears plugin from the gears loader to set the window object and use that as site interface instead of the IUnknown provided by IE Mobile in the SetSite call. Regarding to the gears code the Google engineers are aware of the same problem I'm asking and came up with this workaround I described.
However, I believe there must be another more "official" way of dealing with this issue since explicitely setting the site on an Active X control/plugin isn't very great. I'm going to ask the MS IE Mobile team directly now and will keep you informed once I get a solution. It might be a bug in IE Mobile which is the most likely thing I can imagine of, but who knows...
But thanks anyways for your response ;))