// Licensed to the Software Freedom Conservancy (SFC) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The SFC licenses this file // to you 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 "DocumentHost.h" #include #include #include "errorcodes.h" #include "logging.h" #include "BrowserCookie.h" #include "BrowserFactory.h" #include "CookieManager.h" #include "HookProcessor.h" #include "messages.h" #include "RegistryUtilities.h" #include "Script.h" #include "StringUtilities.h" namespace webdriver { DocumentHost::DocumentHost(HWND hwnd, HWND executor_handle) { LOG(TRACE) << "Entering DocumentHost::DocumentHost"; // NOTE: COM should be initialized on this thread, so we // could use CoCreateGuid() and StringFromGUID2() instead. UUID guid; RPC_WSTR guid_string = NULL; RPC_STATUS status = ::UuidCreate(&guid); if (status != RPC_S_OK) { // If we encounter an error, not bloody much we can do about it. // Just log it and continue. LOG(WARN) << "UuidCreate returned a status other then RPC_S_OK: " << status; } status = ::UuidToString(&guid, &guid_string); if (status != RPC_S_OK) { // If we encounter an error, not bloody much we can do about it. // Just log it and continue. LOG(WARN) << "UuidToString returned a status other then RPC_S_OK: " << status; } // RPC_WSTR is currently typedef'd in RpcDce.h (pulled in by rpc.h) // as unsigned short*. It needs to be typedef'd as wchar_t* wchar_t* cast_guid_string = reinterpret_cast(guid_string); this->browser_id_ = StringUtilities::ToString(cast_guid_string); ::RpcStringFree(&guid_string); this->window_handle_ = hwnd; this->executor_handle_ = executor_handle; this->script_executor_handle_ = NULL; this->is_closing_ = false; this->wait_required_ = false; this->is_awaiting_new_process_ = false; this->focused_frame_window_ = NULL; this->cookie_manager_ = new CookieManager(); if (this->window_handle_ != NULL) { this->cookie_manager_->Initialize(this->window_handle_); } } DocumentHost::~DocumentHost(void) { delete this->cookie_manager_; } std::string DocumentHost::GetCurrentUrl() { LOG(TRACE) << "Entering DocumentHost::GetCurrentUrl"; CComPtr doc; this->GetDocument(&doc); if (!doc) { LOG(WARN) << "Unable to get document object, DocumentHost::GetDocument returned NULL. " << "Attempting to get URL from IWebBrowser2 object"; return this->GetBrowserUrl(); } CComBSTR url; HRESULT hr = doc->get_URL(&url); if (FAILED(hr)) { LOGHR(WARN, hr) << "Unable to get current URL, call to IHTMLDocument2::get_URL failed"; return ""; } std::wstring converted_url(url, ::SysStringLen(url)); std::string current_url = StringUtilities::ToString(converted_url); return current_url; } std::string DocumentHost::GetPageSource() { LOG(TRACE) << "Entering DocumentHost::GetPageSource"; CComPtr doc; this->GetDocument(&doc); if (!doc) { LOG(WARN) << "Unable to get document object, DocumentHost::GetDocument did not return a valid IHTMLDocument2 pointer"; return ""; } CComPtr doc3; HRESULT hr = doc->QueryInterface(&doc3); if (FAILED(hr) || !doc3) { LOG(WARN) << "Unable to get document object, QueryInterface to IHTMLDocument3 failed"; return ""; } CComPtr document_element; hr = doc3->get_documentElement(&document_element); if (FAILED(hr) || !document_element) { LOGHR(WARN, hr) << "Unable to get document element from page, call to IHTMLDocument3::get_documentElement failed"; return ""; } CComBSTR html; hr = document_element->get_outerHTML(&html); if (FAILED(hr)) { LOGHR(WARN, hr) << "Have document element but cannot read source, call to IHTMLElement::get_outerHTML failed"; return ""; } std::wstring converted_html = html; std::string page_source = StringUtilities::ToString(converted_html); return page_source; } void DocumentHost::Restore(void) { if (this->IsFullScreen()) { this->SetFullScreen(false); } HWND window_handle = this->GetTopLevelWindowHandle(); if (::IsZoomed(window_handle) || ::IsIconic(window_handle)) { ::ShowWindow(window_handle, SW_RESTORE); } } int DocumentHost::SetFocusedFrameByElement(IHTMLElement* frame_element) { LOG(TRACE) << "Entering DocumentHost::SetFocusedFrameByElement"; HRESULT hr = S_OK; if (!frame_element) { this->focused_frame_window_ = NULL; return WD_SUCCESS; } CComPtr interim_result; CComPtr object_element; hr = frame_element->QueryInterface(&object_element); if (SUCCEEDED(hr) && object_element) { CComPtr object_disp; object_element->get_contentDocument(&object_disp); if (!object_disp) { LOG(WARN) << "Cannot get IDispatch interface from IHTMLObjectElement4 element"; return ENOSUCHFRAME; } CComPtr object_doc; object_disp->QueryInterface(&object_doc); if (!object_doc) { LOG(WARN) << "Cannot get IHTMLDocument2 document from IDispatch reference"; return ENOSUCHFRAME; } hr = object_doc->get_parentWindow(&interim_result); if (FAILED(hr)) { LOGHR(WARN, hr) << "Cannot get parentWindow from IHTMLDocument2, call to IHTMLDocument2::get_parentWindow failed"; return ENOSUCHFRAME; } } else { CComPtr frame_base; frame_element->QueryInterface(&frame_base); if (!frame_base) { LOG(WARN) << "IHTMLElement is not a FRAME or IFRAME element"; return ENOSUCHFRAME; } hr = frame_base->get_contentWindow(&interim_result); if (FAILED(hr)) { LOGHR(WARN, hr) << "Cannot get contentWindow from IHTMLFrameBase2, call to IHTMLFrameBase2::get_contentWindow failed"; return ENOSUCHFRAME; } } this->focused_frame_window_ = interim_result; return WD_SUCCESS; } int DocumentHost::SetFocusedFrameByName(const std::string& frame_name) { LOG(TRACE) << "Entering DocumentHost::SetFocusedFrameByName"; CComVariant frame_identifier = StringUtilities::ToWString(frame_name).c_str(); return this->SetFocusedFrameByIdentifier(frame_identifier); } int DocumentHost::SetFocusedFrameByIndex(const int frame_index) { LOG(TRACE) << "Entering DocumentHost::SetFocusedFrameByIndex"; CComVariant frame_identifier; frame_identifier.vt = VT_I4; frame_identifier.lVal = frame_index; return this->SetFocusedFrameByIdentifier(frame_identifier); } void DocumentHost::SetFocusedFrameToParent() { LOG(TRACE) << "Entering DocumentHost::SetFocusedFrameToParent"; // Three possible outcomes. // Outcome 1: Already at top-level browsing context. No-op. if (this->focused_frame_window_ != NULL) { CComPtr parent_window; HRESULT hr = this->focused_frame_window_->get_parent(&parent_window); if (FAILED(hr)) { LOGHR(WARN, hr) << "IHTMLWindow2::get_parent call failed."; } CComPtr top_window; hr = this->focused_frame_window_->get_top(&top_window); if (FAILED(hr)) { LOGHR(WARN, hr) << "IHTMLWindow2::get_top call failed."; } if (top_window.IsEqualObject(parent_window)) { // Outcome 2: Focus is on a frame one level deep, making the // parent the top-level browsing context. Set focused frame // pointer to NULL. this->focused_frame_window_ = NULL; } else { // Outcome 3: Focus is on a frame more than one level deep. // Set focused frame pointer to parent frame. this->focused_frame_window_ = parent_window; } } } int DocumentHost::SetFocusedFrameByIdentifier(VARIANT frame_identifier) { LOG(TRACE) << "Entering DocumentHost::SetFocusedFrameByIdentifier"; CComPtr doc; this->GetDocument(&doc); CComPtr frames; HRESULT hr = doc->get_frames(&frames); if (!frames) { LOG(WARN) << "No frames in document are set, IHTMLDocument2::get_frames returned NULL"; return ENOSUCHFRAME; } long length = 0; frames->get_length(&length); if (!length) { LOG(WARN) << "No frames in document are found IHTMLFramesCollection2::get_length returned 0"; return ENOSUCHFRAME; } // Find the frame CComVariant frame_holder; hr = frames->item(&frame_identifier, &frame_holder); if (FAILED(hr)) { LOGHR(WARN, hr) << "Error retrieving frame holder, call to IHTMLFramesCollection2::item failed"; return ENOSUCHFRAME; } CComPtr interim_result; frame_holder.pdispVal->QueryInterface(&interim_result); if (!interim_result) { LOG(WARN) << "Error retrieving frame, IDispatch cannot be cast to IHTMLWindow2"; return ENOSUCHFRAME; } this->focused_frame_window_ = interim_result; return WD_SUCCESS; } void DocumentHost::PostQuitMessage() { LOG(TRACE) << "Entering DocumentHost::PostQuitMessage"; LPSTR message_payload = new CHAR[this->browser_id_.size() + 1]; strcpy_s(message_payload, this->browser_id_.size() + 1, this->browser_id_.c_str()); ::PostMessage(this->executor_handle(), WD_BROWSER_QUIT, NULL, reinterpret_cast(message_payload)); } HWND DocumentHost::FindContentWindowHandle(HWND top_level_window_handle) { LOG(TRACE) << "Entering DocumentHost::FindContentWindowHandle"; ProcessWindowInfo process_window_info; process_window_info.pBrowser = NULL; process_window_info.hwndBrowser = NULL; DWORD process_id; ::GetWindowThreadProcessId(top_level_window_handle, &process_id); process_window_info.dwProcessId = process_id; ::EnumChildWindows(top_level_window_handle, &BrowserFactory::FindChildWindowForProcess, reinterpret_cast(&process_window_info)); return process_window_info.hwndBrowser; } int DocumentHost::GetDocumentMode(IHTMLDocument2* doc) { LOG(TRACE) << "Entering DocumentHost::GetDocumentMode"; CComPtr mode_doc; doc->QueryInterface(&mode_doc); if (!mode_doc) { LOG(DEBUG) << "QueryInterface for IHTMLDocument6 fails, so document mode must be 7 or less."; return 5; } CComVariant mode; HRESULT hr = mode_doc->get_documentMode(&mode); if (FAILED(hr)) { LOGHR(WARN, hr) << "get_documentMode failed."; return 5; } int document_mode = static_cast(mode.fltVal); return document_mode; } bool DocumentHost::IsStandardsMode(IHTMLDocument2* doc) { LOG(TRACE) << "Entering DocumentHost::IsStandardsMode"; CComPtr compatibility_mode_doc; doc->QueryInterface(&compatibility_mode_doc); if (!compatibility_mode_doc) { LOG(WARN) << "Unable to cast document to IHTMLDocument5. IE6 or greater is required."; return false; } CComBSTR compatibility_mode; HRESULT hr = compatibility_mode_doc->get_compatMode(&compatibility_mode); if (FAILED(hr)) { LOGHR(WARN, hr) << "Failed calling get_compatMode."; return false; } // Compatibility mode should be "BackCompat" for quirks mode, and // "CSS1Compat" for standards mode. Check for "BackCompat" because // that's less likely to change. return compatibility_mode != L"BackCompat"; } bool DocumentHost::GetDocumentDimensions(IHTMLDocument2* doc, LocationInfo* info) { LOG(TRACE) << "Entering DocumentHost::GetDocumentDimensions"; CComVariant document_height; CComVariant document_width; // In non-standards-compliant mode, the BODY element represents the canvas. // In standards-compliant mode, the HTML element represents the canvas. CComPtr canvas_element; if (!IsStandardsMode(doc)) { doc->get_body(&canvas_element); if (!canvas_element) { LOG(WARN) << "Unable to get canvas element from document in compatibility mode"; return false; } } else { CComPtr document_element_doc; doc->QueryInterface(&document_element_doc); if (!document_element_doc) { LOG(WARN) << "Unable to get IHTMLDocument3 handle from document."; return false; } // The root node should be the HTML element. document_element_doc->get_documentElement(&canvas_element); if (!canvas_element) { LOG(WARN) << "Could not retrieve document element."; return false; } CComPtr html_element; canvas_element->QueryInterface(&html_element); if (!html_element) { LOG(WARN) << "Document element is not the HTML element."; return false; } } canvas_element->getAttribute(CComBSTR("scrollHeight"), 0, &document_height); canvas_element->getAttribute(CComBSTR("scrollWidth"), 0, &document_width); info->height = document_height.lVal; info->width = document_width.lVal; return true; } bool DocumentHost::IsCrossZoneUrl(std::string url) { LOG(TRACE) << "Entering Browser::IsCrossZoneUrl"; std::wstring target_url = StringUtilities::ToWString(url); CComPtr parsed_url; HRESULT hr = ::CreateUri(target_url.c_str(), Uri_CREATE_IE_SETTINGS, 0, &parsed_url); if (FAILED(hr)) { // If we can't parse the URL, assume that it's invalid, and // therefore won't cross a Protected Mode boundary. return false; } bool is_protected_mode_browser = this->IsProtectedMode(); bool is_protected_mode_url = is_protected_mode_browser; if (url.find("about:blank") != 0) { // If the URL starts with "about:blank", it won't cross the Protected // Mode boundary, so skip checking if it's a Protected Mode URL. is_protected_mode_url = ::IEIsProtectedModeURL(target_url.c_str()) == S_OK; } bool is_cross_zone = is_protected_mode_browser != is_protected_mode_url; if (is_cross_zone) { LOG(DEBUG) << "Navigation across Protected Mode zone detected. URL: " << url << ", is URL Protected Mode: " << (is_protected_mode_url ? "true" : "false") << ", is IE in Protected Mode: " << (is_protected_mode_browser ? "true" : "false"); } return is_cross_zone; } bool DocumentHost::IsProtectedMode() { LOG(TRACE) << "Entering DocumentHost::IsProtectedMode"; HWND window_handle = this->GetBrowserWindowHandle(); HookSettings hook_settings; hook_settings.hook_procedure_name = "ProtectedModeWndProc"; hook_settings.hook_procedure_type = WH_CALLWNDPROC; hook_settings.window_handle = window_handle; hook_settings.communication_type = OneWay; HookProcessor hook; if (!hook.CanSetWindowsHook(window_handle)) { LOG(WARN) << "Cannot check Protected Mode because driver and browser are " << "not the same bit-ness."; return false; } hook.Initialize(hook_settings); HookProcessor::ResetFlag(); ::SendMessage(window_handle, WD_IS_BROWSER_PROTECTED_MODE, NULL, NULL); bool is_protected_mode = HookProcessor::GetFlagValue(); return is_protected_mode; } bool DocumentHost::SetFocusToBrowser() { LOG(TRACE) << "Entering DocumentHost::SetFocusToBrowser"; HWND top_level_window_handle = this->GetTopLevelWindowHandle(); HWND foreground_window = ::GetAncestor(::GetForegroundWindow(), GA_ROOT); if (foreground_window != top_level_window_handle) { LOG(TRACE) << "Top-level IE window is " << top_level_window_handle << " foreground window is " << foreground_window; CComPtr ui_automation; HRESULT hr = ::CoCreateInstance(CLSID_CUIAutomation, NULL, CLSCTX_INPROC_SERVER, IID_IUIAutomation, reinterpret_cast(&ui_automation)); if (SUCCEEDED(hr)) { LOG(TRACE) << "Using UI Automation to set window focus"; CComPtr parent_window; hr = ui_automation->ElementFromHandle(top_level_window_handle, &parent_window); if (SUCCEEDED(hr)) { CComPtr window_pattern; hr = parent_window->GetCurrentPatternAs(UIA_WindowPatternId, IID_PPV_ARGS(&window_pattern)); if (SUCCEEDED(hr) && window_pattern != nullptr) { BOOL is_topmost; hr = window_pattern->get_CurrentIsTopmost(&is_topmost); WindowVisualState visual_state; hr = window_pattern->get_CurrentWindowVisualState(&visual_state); if (visual_state == WindowVisualState::WindowVisualState_Maximized || visual_state == WindowVisualState::WindowVisualState_Normal) { parent_window->SetFocus(); window_pattern->SetWindowVisualState(visual_state); } } } } } foreground_window = ::GetAncestor(::GetForegroundWindow(), GA_ROOT); if (foreground_window != top_level_window_handle) { HWND content_window_handle = this->GetContentWindowHandle(); LOG(TRACE) << "Top-level IE window is " << top_level_window_handle << " foreground window is " << foreground_window; LOG(TRACE) << "Window still not in foreground; " << "attempting to use SetForegroundWindow API"; UINT_PTR lock_timeout = 0; DWORD process_id = 0; DWORD thread_id = ::GetWindowThreadProcessId(top_level_window_handle, &process_id); DWORD current_thread_id = ::GetCurrentThreadId(); DWORD current_process_id = ::GetCurrentProcessId(); if (current_thread_id != thread_id) { ::AttachThreadInput(current_thread_id, thread_id, TRUE); ::SystemParametersInfo(SPI_GETFOREGROUNDLOCKTIMEOUT, 0, &lock_timeout, 0); ::SystemParametersInfo(SPI_SETFOREGROUNDLOCKTIMEOUT, 0, 0, SPIF_SENDWININICHANGE | SPIF_UPDATEINIFILE); HookSettings hook_settings; hook_settings.hook_procedure_name = "AllowSetForegroundProc"; hook_settings.hook_procedure_type = WH_CALLWNDPROC; hook_settings.window_handle = content_window_handle; hook_settings.communication_type = OneWay; HookProcessor hook; if (!hook.CanSetWindowsHook(content_window_handle)) { LOG(WARN) << "Setting window focus may fail because driver and browser " << "are not the same bit-ness."; return false; } hook.Initialize(hook_settings); ::SendMessage(content_window_handle, WD_ALLOW_SET_FOREGROUND, NULL, NULL); hook.Dispose(); } ::SetForegroundWindow(top_level_window_handle); ::Sleep(100); if (current_thread_id != thread_id) { ::SystemParametersInfo(SPI_SETFOREGROUNDLOCKTIMEOUT, 0, reinterpret_cast(lock_timeout), SPIF_SENDWININICHANGE | SPIF_UPDATEINIFILE); ::AttachThreadInput(current_thread_id, thread_id, FALSE); } } foreground_window = ::GetAncestor(::GetForegroundWindow(), GA_ROOT); return foreground_window == top_level_window_handle; } } // namespace webdriver #ifdef __cplusplus extern "C" { #endif LRESULT CALLBACK ProtectedModeWndProc(int nCode, WPARAM wParam, LPARAM lParam) { CWPSTRUCT* call_window_proc_struct = reinterpret_cast(lParam); if (WD_IS_BROWSER_PROTECTED_MODE == call_window_proc_struct->message) { BOOL is_protected_mode = FALSE; HRESULT hr = ::IEIsProtectedModeProcess(&is_protected_mode); webdriver::HookProcessor::SetFlagValue(is_protected_mode == TRUE); } return ::CallNextHookEx(NULL, nCode, wParam, lParam); } LRESULT CALLBACK AllowSetForegroundProc(int nCode, WPARAM wParam, LPARAM lParam) { if ((nCode == HC_ACTION) && (wParam == PM_REMOVE)) { MSG* msg = reinterpret_cast(lParam); if (msg->message == WD_ALLOW_SET_FOREGROUND) { ::AllowSetForegroundWindow(ASFW_ANY); } } return CallNextHookEx(NULL, nCode, wParam, lParam); } #ifdef __cplusplus } #endif