blob: 3ce4a3869d2b65c7f2fa368e1e2c4f849c994b63 [file] [log] [blame]
// Copyright 2011 Software Freedom Conservancy
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "Browser.h"
#include "logging.h"
#include <comutil.h>
#include "Alert.h"
namespace webdriver {
Browser::Browser(IWebBrowser2* browser, HWND hwnd, HWND session_handle) : DocumentHost(hwnd, session_handle) {
LOG(TRACE) << "Entering Browser::Browser";
this->is_navigation_started_ = false;
this->browser_ = browser;
this->AttachEvents();
}
Browser::~Browser(void) {
this->DetachEvents();
}
void __stdcall Browser::BeforeNavigate2(IDispatch* pObject,
VARIANT* pvarUrl,
VARIANT* pvarFlags,
VARIANT* pvarTargetFrame,
VARIANT* pvarData,
VARIANT* pvarHeaders,
VARIANT_BOOL* pbCancel) {
LOG(TRACE) << "Entering Browser::BeforeNavigate2";
}
void __stdcall Browser::OnQuit() {
LOG(TRACE) << "Entering Browser::OnQuit";
this->PostQuitMessage();
}
void __stdcall Browser::NewWindow3(IDispatch** ppDisp,
VARIANT_BOOL* pbCancel,
DWORD dwFlags,
BSTR bstrUrlContext,
BSTR bstrUrl) {
LOG(TRACE) << "Entering Browser::NewWindow3";
// Handle the NewWindow3 event to allow us to immediately hook
// the events of the new browser window opened by the user action.
// This will not allow us to handle windows created by the JavaScript
// showModalDialog function().
IWebBrowser2* browser;
LPSTREAM message_payload;
::SendMessage(this->executor_handle(),
WD_BROWSER_NEW_WINDOW,
NULL,
reinterpret_cast<LPARAM>(&message_payload));
HRESULT hr = ::CoGetInterfaceAndReleaseStream(message_payload,
IID_IWebBrowser2,
reinterpret_cast<void**>(&browser));
*ppDisp = browser;
}
void __stdcall Browser::DocumentComplete(IDispatch* pDisp, VARIANT* URL) {
LOG(TRACE) << "Entering Browser::DocumentComplete";
// Flag the browser as navigation having started.
this->is_navigation_started_ = true;
// DocumentComplete fires last for the top-level frame. If it fires
// for the top-level frame and the focused_frame_window_ member variable
// is not NULL, we assume we have navigated from within a frameset to a
// link that has a target of "_top", which replaces the frameset with the
// target page. On a top-level navigation, we are supposed to reset the
// focused frame to the top-level, so we do that here.
// NOTE: This is a possible source of unreliability if the above
// assumptions turn out to be wrong and/or the event firing doesn't work
// the way we expect it to.
CComPtr<IDispatch> dispatch(this->browser_);
if (dispatch.IsEqualObject(pDisp)) {
if (this->focused_frame_window() != NULL) {
LOG(DEBUG) << "DocumentComplete happened from within a frameset";
this->SetFocusedFrameByElement(NULL);
}
::PostMessage(this->executor_handle(), WD_REFRESH_MANAGED_ELEMENTS, NULL, NULL);
}
}
void Browser::GetDocument(IHTMLDocument2** doc) {
LOG(TRACE) << "Entering Browser::GetDocument";
CComPtr<IHTMLWindow2> window;
if (this->focused_frame_window() == NULL) {
LOG(INFO) << "No child frame focus. Focus is on top-level frame";
CComPtr<IDispatch> dispatch;
HRESULT hr = this->browser_->get_Document(&dispatch);
if (FAILED(hr)) {
LOGHR(WARN, hr) << "Unable to get document, IWebBrowser2::get_Document call failed";
return;
}
CComPtr<IHTMLDocument2> dispatch_doc;
hr = dispatch->QueryInterface(&dispatch_doc);
if (FAILED(hr)) {
LOGHR(WARN, hr) << "Have document but cannot cast, IDispatch::QueryInterface call failed";
return;
}
dispatch_doc->get_parentWindow(&window);
} else {
window = this->focused_frame_window();
}
if (window) {
bool result = this->GetDocumentFromWindow(window, doc);
if (!result) {
LOG(WARN) << "Cannot get document";
}
} else {
LOG(WARN) << "No window is found";
}
}
std::string Browser::GetTitle() {
LOG(TRACE) << "Entering Browser::GetTitle";
CComPtr<IDispatch> dispatch;
HRESULT hr = this->browser_->get_Document(&dispatch);
if (FAILED(hr)) {
LOGHR(WARN, hr) << "Unable to get document, IWebBrowser2::get_Document call failed";
return "";
}
CComPtr<IHTMLDocument2> doc;
hr = dispatch->QueryInterface(&doc);
if (FAILED(hr)) {
LOGHR(WARN, hr) << "Have document but cannot cast, IDispatch::QueryInterface call failed";
return "";
}
CComBSTR title;
hr = doc->get_title(&title);
if (FAILED(hr)) {
LOGHR(WARN, hr) << "Unable to get document title, call to IHTMLDocument2::get_title failed";
return "";
}
std::string title_string = CW2A(title, CP_UTF8);
return title_string;
}
HWND Browser::GetWindowHandle() {
LOG(TRACE) << "Entering Browser::GetWindowHandle";
// If, for some reason, the window handle is no longer valid,
// set the member variable to NULL so that we can reacquire
// the valid window handle. Note that this can happen when
// browsing from one type of content to another, like from
// HTML to a transformed XML page that renders content.
if (!::IsWindow(this->window_handle())) {
LOG(INFO) << "Flushing window handle as it is no longer valid";
this->set_window_handle(NULL);
}
if (this->window_handle() == NULL) {
LOG(INFO) << "Restore window handle from tab";
this->set_window_handle(this->GetTabWindowHandle());
}
return this->window_handle();
}
std::string Browser::GetWindowName() {
LOG(TRACE) << "Entering Browser::GetWindowName";
CComPtr<IDispatch> dispatch;
HRESULT hr = this->browser_->get_Document(&dispatch);
if (FAILED(hr)) {
LOGHR(WARN, hr) << "Unable to get document, IWebBrowser2::get_Document call failed";
return "";
}
CComQIPtr<IHTMLDocument2> doc(dispatch);
if (!doc) {
LOGHR(WARN, hr) << "Have document but cannot cast, IDispatch::QueryInterface call failed";
return "";
}
CComPtr<IHTMLWindow2> window;
hr = doc->get_parentWindow(&window);
if (FAILED(hr)) {
LOGHR(WARN, hr) << "Unable to get parent window, call to IHTMLDocument2::get_parentWindow failed";
return "";
}
std::string name = "";
CComBSTR window_name;
hr = window->get_name(&window_name);
if (window_name) {
name = CW2A(window_name, CP_UTF8);
} else {
LOG(WARN) << "Unable to get window name, IHTMLWindow2::get_name failed or returned a NULL value";
}
return name;
}
long Browser::GetWidth() {
LOG(TRACE) << "Entering Browser::GetWidth";
long width = 0;
this->browser_->get_Width(&width);
return width;
}
long Browser::GetHeight() {
LOG(TRACE) << "Entering Browser::GetHeight";
long height = 0;
this->browser_->get_Height(&height);
return height;
}
void Browser::SetWidth(long width) {
LOG(TRACE) << "Entering Browser::SetWidth";
this->browser_->put_Width(width);
}
void Browser::SetHeight(long height) {
LOG(TRACE) << "Entering Browser::SetHeight";
this->browser_->put_Height(height);
}
void Browser::AttachEvents() {
LOG(TRACE) << "Entering Browser::AttachEvents";
CComQIPtr<IDispatch> dispatch(this->browser_);
CComPtr<IUnknown> unknown(dispatch);
HRESULT hr = this->DispEventAdvise(unknown);
}
void Browser::DetachEvents() {
LOG(TRACE) << "Entering Browser::DetachEvents";
CComQIPtr<IDispatch> dispatch(this->browser_);
CComPtr<IUnknown> unknown(dispatch);
HRESULT hr = this->DispEventUnadvise(unknown);
}
void Browser::Close() {
LOG(TRACE) << "Entering Browser::Close";
// Closing the browser, so having focus on a frame doesn't
// make any sense.
this->SetFocusedFrameByElement(NULL);
HRESULT hr = this->browser_->Quit();
if (FAILED(hr)) {
LOGHR(WARN, hr) << "Call to IWebBrowser2::Quit failed";
}
}
int Browser::NavigateToUrl(const std::string& url) {
LOG(TRACE) << "Entring Browser::NavigateToUrl";
std::wstring wide_url = CA2W(url.c_str(), CP_UTF8);
CComVariant url_variant(wide_url.c_str());
CComVariant dummy;
// TODO: check HRESULT for error
HRESULT hr = this->browser_->Navigate2(&url_variant,
&dummy,
&dummy,
&dummy,
&dummy);
if (FAILED(hr)) {
LOGHR(WARN, hr) << "Call to IWebBrowser2::Navigate2 failed";
return EUNHANDLEDERROR;
}
this->set_wait_required(true);
return SUCCESS;
}
int Browser::NavigateBack() {
LOG(TRACE) << "Entering Browser::NavigateBack";
LPSTREAM stream;
HRESULT hr = ::CoMarshalInterThreadInterfaceInStream(IID_IWebBrowser2, this->browser_, &stream);
unsigned int thread_id = 0;
HANDLE thread_handle = reinterpret_cast<HANDLE>(_beginthreadex(NULL,
0,
&Browser::GoBackThreadProc,
(void *)stream,
0,
&thread_id));
if (thread_handle != NULL) {
::CloseHandle(thread_handle);
}
this->set_wait_required(true);
return SUCCESS;
}
unsigned int WINAPI Browser::GoBackThreadProc(LPVOID param) {
HRESULT hr = ::CoInitialize(NULL);
IWebBrowser2* browser;
LPSTREAM message_payload = reinterpret_cast<LPSTREAM>(param);
hr = ::CoGetInterfaceAndReleaseStream(message_payload,
IID_IWebBrowser2,
reinterpret_cast<void**>(&browser));
hr = browser->GoBack();
return 0;
}
int Browser::NavigateForward() {
LOG(TRACE) << "Entering Browser::NavigateForward";
LPSTREAM stream;
HRESULT hr = ::CoMarshalInterThreadInterfaceInStream(IID_IWebBrowser2, this->browser_, &stream);
unsigned int thread_id = 0;
HANDLE thread_handle = reinterpret_cast<HANDLE>(_beginthreadex(NULL,
0,
&Browser::GoForwardThreadProc,
(void *)stream,
0,
&thread_id));
if (thread_handle != NULL) {
::CloseHandle(thread_handle);
}
this->set_wait_required(true);
return SUCCESS;
}
unsigned int WINAPI Browser::GoForwardThreadProc(LPVOID param) {
HRESULT hr = ::CoInitialize(NULL);
IWebBrowser2* browser;
LPSTREAM message_payload = reinterpret_cast<LPSTREAM>(param);
hr = ::CoGetInterfaceAndReleaseStream(message_payload,
IID_IWebBrowser2,
reinterpret_cast<void**>(&browser));
hr = browser->GoForward();
return 0;
}
int Browser::Refresh() {
LOG(TRACE) << "Entering Browser::Refresh";
HRESULT hr = this->browser_->Refresh();
if (FAILED(hr)) {
LOGHR(WARN, hr) << "Call to IWebBrowser2::Refresh failed";
}
this->set_wait_required(true);
return SUCCESS;
}
HWND Browser::GetTopLevelWindowHandle() {
LOG(TRACE) << "Entering Browser::GetTopLevelWindowHandle";
HWND top_level_window_handle = NULL;
this->browser_->get_HWND(reinterpret_cast<SHANDLE_PTR*>(&top_level_window_handle));
return top_level_window_handle;
}
bool Browser::IsBusy() {
VARIANT_BOOL is_busy(VARIANT_FALSE);
HRESULT hr = this->browser_->get_Busy(&is_busy);
return SUCCEEDED(hr) && is_busy == VARIANT_TRUE;
}
bool Browser::Wait() {
LOG(TRACE) << "Entering Browser::Wait";
bool is_navigating = true;
LOG(DEBUG) << "Navigate Events Completed.";
this->is_navigation_started_ = false;
HWND dialog = this->GetActiveDialogWindowHandle();
if (dialog != NULL) {
LOG(DEBUG) << "Found alert. Aborting wait.";
this->set_wait_required(false);
return true;
}
// Navigate events completed. Waiting for browser.Busy != false...
is_navigating = this->is_navigation_started_;
if (is_navigating || this->IsBusy()) {
LOG(DEBUG) << "Browser busy property is true.";
return false;
}
// Waiting for browser.ReadyState == READYSTATE_COMPLETE...;
is_navigating = this->is_navigation_started_;
READYSTATE ready_state;
HRESULT hr = this->browser_->get_ReadyState(&ready_state);
if (is_navigating || FAILED(hr) || ready_state != READYSTATE_COMPLETE) {
LOG(DEBUG) << "readyState is not 'Complete'.";
return false;
}
// Waiting for document property != null...
is_navigating = this->is_navigation_started_;
CComQIPtr<IDispatch> document_dispatch;
hr = this->browser_->get_Document(&document_dispatch);
if (is_navigating && FAILED(hr) && !document_dispatch) {
LOG(DEBUG) << "Get Document failed.";
return false;
}
// Waiting for document to complete...
CComPtr<IHTMLDocument2> doc;
hr = document_dispatch->QueryInterface(&doc);
if (SUCCEEDED(hr)) {
LOG(DEBUG) << "Waiting for document to complete...";
is_navigating = this->IsDocumentNavigating(doc);
}
if (!is_navigating) {
LOG(DEBUG) << "Not in navigating state";
this->set_wait_required(false);
}
return !is_navigating;
}
bool Browser::IsDocumentNavigating(IHTMLDocument2* doc) {
LOG(TRACE) << "Entering Browser::IsDocumentNavigating";
bool is_navigating = true;
// Starting WaitForDocumentComplete()
is_navigating = this->is_navigation_started_;
CComBSTR ready_state;
HRESULT hr = doc->get_readyState(&ready_state);
if (FAILED(hr) || is_navigating || _wcsicmp(ready_state, L"complete") != 0) {
LOG(DEBUG) << "readyState is not complete. ";
return true;
} else {
is_navigating = false;
}
// document.readyState == complete
is_navigating = this->is_navigation_started_;
CComPtr<IHTMLFramesCollection2> frames;
hr = doc->get_frames(&frames);
if (is_navigating || FAILED(hr)) {
LOG(DEBUG) << "Could not get frames, navigation has started or call to IHTMLDocument2::get_frames failed";
return true;
}
if (frames != NULL) {
long frame_count = 0;
hr = frames->get_length(&frame_count);
CComVariant index;
index.vt = VT_I4;
for (long i = 0; i < frame_count; ++i) {
// Waiting on each frame
index.lVal = i;
CComVariant result;
hr = frames->item(&index, &result);
if (FAILED(hr)) {
LOGHR(DEBUG, hr) << "Could not get frame item for index " << i << ", call to IHTMLFramesCollection2::item failed";
return true;
}
CComQIPtr<IHTMLWindow2> window(result.pdispVal);
if (!window) {
// Frame is not an HTML frame.
continue;
}
CComPtr<IHTMLDocument2> frame_document;
bool is_valid_frame_document = this->GetDocumentFromWindow(window,
&frame_document);
is_navigating = this->is_navigation_started_;
if (is_navigating) {
break;
}
// Recursively call to wait for the frame document to complete
if (is_valid_frame_document) {
is_navigating = this->IsDocumentNavigating(frame_document);
if (is_navigating) {
break;
}
}
}
} else {
LOG(DEBUG) << "IHTMLDocument2.get_frames() returned empty collection";
}
return is_navigating;
}
bool Browser::GetDocumentFromWindow(IHTMLWindow2* window,
IHTMLDocument2** doc) {
LOG(TRACE) << "Entering Browser::GetDocumentFromWindow";
HRESULT hr = window->get_document(doc);
if (SUCCEEDED(hr)) {
return true;
}
if (hr == E_ACCESSDENIED) {
// Cross-domain documents may throw Access Denied. If so,
// get the document through the IWebBrowser2 interface.
CComPtr<IWebBrowser2> window_browser;
CComQIPtr<IServiceProvider> service_provider(window);
hr = service_provider->QueryService(IID_IWebBrowserApp, &window_browser);
if (FAILED(hr)) {
LOGHR(WARN, hr) << "Unable to get browser, call to IServiceProvider::QueryService failed for IID_IWebBrowserApp";
return false;
}
CComQIPtr<IDispatch> document_dispatch;
hr = window_browser->get_Document(&document_dispatch);
if (FAILED(hr) || hr == S_FALSE) {
LOGHR(WARN, hr) << "Unable to get document, call to IWebBrowser2::get_Document failed";
return false;
}
hr = document_dispatch->QueryInterface(doc);
if (FAILED(hr)) {
LOGHR(WARN, hr) << "Unable to query document, call to IDispatch::QueryInterface failed.";
return false;
}
return true;
} else {
LOGHR(WARN, hr) << "Unable to get main document, IHTMLWindow2::get_document returned other than E_ACCESSDENIED";
}
return false;
}
HWND Browser::GetTabWindowHandle() {
LOG(TRACE) << "Entering Browser::GetTabWindowHandle";
HWND hwnd = NULL;
CComQIPtr<IServiceProvider> service_provider;
HRESULT hr = this->browser_->QueryInterface(IID_IServiceProvider,
reinterpret_cast<void**>(&service_provider));
if (SUCCEEDED(hr)) {
CComPtr<IOleWindow> window;
hr = service_provider->QueryService(SID_SShellBrowser,
IID_IOleWindow,
reinterpret_cast<void**>(&window));
if (SUCCEEDED(hr)) {
// This gets the TabWindowClass window in IE 7 and 8,
// and the top-level window frame in IE 6. The window
// we need is the InternetExplorer_Server window.
window->GetWindow(&hwnd);
hwnd = this->FindContentWindowHandle(hwnd);
} else {
LOGHR(WARN, hr) << "Unable to get window, call to IOleWindow::QueryService for SID_SShellBrowser failed";
}
} else {
LOGHR(WARN, hr) << "Unable to get service, call to IWebBrowser2::QueryInterface for IID_IServiceProvider failed";
}
return hwnd;
}
HWND Browser::GetActiveDialogWindowHandle() {
LOG(TRACE) << "Entering Browser::GetActiveDialogWindowHandle";
HWND active_dialog_handle = NULL;
DWORD process_id;
::GetWindowThreadProcessId(this->GetWindowHandle(), &process_id);
ProcessWindowInfo process_win_info;
process_win_info.dwProcessId = process_id;
process_win_info.hwndBrowser = NULL;
::EnumWindows(&BrowserFactory::FindDialogWindowForProcess,
reinterpret_cast<LPARAM>(&process_win_info));
if (process_win_info.hwndBrowser != NULL) {
active_dialog_handle = process_win_info.hwndBrowser;
this->CheckDialogType(active_dialog_handle);
}
return active_dialog_handle;
}
void Browser::CheckDialogType(HWND dialog_window_handle) {
LOG(TRACE) << "Entering Browser::CheckDialogType";
vector<char> window_class_name(34);
if (GetClassNameA(dialog_window_handle, &window_class_name[0], 34)) {
if (strcmp(HTML_DIALOG_WINDOW_CLASS,
&window_class_name[0]) == 0) {
HWND content_window_handle = this->FindContentWindowHandle(dialog_window_handle);
::PostMessage(this->executor_handle(),
WD_NEW_HTML_DIALOG,
NULL,
reinterpret_cast<LPARAM>(content_window_handle));
}
}
}
} // namespace webdriver