| // 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 <IEPMapi.h> |
| #include <UIAutomation.h> |
| |
| #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<wchar_t*>(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<IHTMLDocument2> 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<IHTMLDocument2> doc; |
| this->GetDocument(&doc); |
| if (!doc) { |
| LOG(WARN) << "Unable to get document object, DocumentHost::GetDocument did not return a valid IHTMLDocument2 pointer"; |
| return ""; |
| } |
| |
| CComPtr<IHTMLDocument3> doc3; |
| HRESULT hr = doc->QueryInterface<IHTMLDocument3>(&doc3); |
| if (FAILED(hr) || !doc3) { |
| LOG(WARN) << "Unable to get document object, QueryInterface to IHTMLDocument3 failed"; |
| return ""; |
| } |
| |
| CComPtr<IHTMLElement> 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<IHTMLWindow2> interim_result; |
| CComPtr<IHTMLObjectElement4> object_element; |
| hr = frame_element->QueryInterface<IHTMLObjectElement4>(&object_element); |
| if (SUCCEEDED(hr) && object_element) { |
| CComPtr<IDispatch> object_disp; |
| object_element->get_contentDocument(&object_disp); |
| if (!object_disp) { |
| LOG(WARN) << "Cannot get IDispatch interface from IHTMLObjectElement4 element"; |
| return ENOSUCHFRAME; |
| } |
| |
| CComPtr<IHTMLDocument2> object_doc; |
| object_disp->QueryInterface<IHTMLDocument2>(&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<IHTMLFrameBase2> frame_base; |
| frame_element->QueryInterface<IHTMLFrameBase2>(&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<IHTMLWindow2> parent_window; |
| HRESULT hr = this->focused_frame_window_->get_parent(&parent_window); |
| if (FAILED(hr)) { |
| LOGHR(WARN, hr) << "IHTMLWindow2::get_parent call failed."; |
| } |
| CComPtr<IHTMLWindow2> 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<IHTMLDocument2> doc; |
| this->GetDocument(&doc); |
| |
| CComPtr<IHTMLFramesCollection2> 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<IHTMLWindow2> interim_result; |
| frame_holder.pdispVal->QueryInterface<IHTMLWindow2>(&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<LPARAM>(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<LPARAM>(&process_window_info)); |
| return process_window_info.hwndBrowser; |
| } |
| |
| int DocumentHost::GetDocumentMode(IHTMLDocument2* doc) { |
| LOG(TRACE) << "Entering DocumentHost::GetDocumentMode"; |
| CComPtr<IHTMLDocument6> mode_doc; |
| doc->QueryInterface<IHTMLDocument6>(&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<int>(mode.fltVal); |
| return document_mode; |
| } |
| |
| bool DocumentHost::IsStandardsMode(IHTMLDocument2* doc) { |
| LOG(TRACE) << "Entering DocumentHost::IsStandardsMode"; |
| CComPtr<IHTMLDocument5> compatibility_mode_doc; |
| doc->QueryInterface<IHTMLDocument5>(&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<IHTMLElement> 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<IHTMLDocument3> document_element_doc; |
| doc->QueryInterface<IHTMLDocument3>(&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<IHTMLHtmlElement> html_element; |
| canvas_element->QueryInterface<IHTMLHtmlElement>(&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<IUri> 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<IUIAutomation> ui_automation; |
| HRESULT hr = ::CoCreateInstance(CLSID_CUIAutomation, |
| NULL, |
| CLSCTX_INPROC_SERVER, |
| IID_IUIAutomation, |
| reinterpret_cast<void**>(&ui_automation)); |
| if (SUCCEEDED(hr)) { |
| LOG(TRACE) << "Using UI Automation to set window focus"; |
| CComPtr<IUIAutomationElement> parent_window; |
| hr = ui_automation->ElementFromHandle(top_level_window_handle, |
| &parent_window); |
| if (SUCCEEDED(hr)) { |
| CComPtr<IUIAutomationWindowPattern> 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<void*>(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<CWPSTRUCT*>(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<MSG*>(lParam); |
| if (msg->message == WD_ALLOW_SET_FOREGROUND) { |
| ::AllowSetForegroundWindow(ASFW_ANY); |
| } |
| } |
| |
| return CallNextHookEx(NULL, nCode, wParam, lParam); |
| } |
| |
| #ifdef __cplusplus |
| } |
| #endif |