| // 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 |