| // 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 "SendKeysCommandHandler.h" |
| #include <ctime> |
| #include <iomanip> |
| #include <UIAutomation.h> |
| #include "errorcodes.h" |
| #include "keycodes.h" |
| #include "logging.h" |
| #include "../Browser.h" |
| #include "../BrowserFactory.h" |
| #include "../Element.h" |
| #include "../IECommandExecutor.h" |
| #include "../InputManager.h" |
| #include "../StringUtilities.h" |
| #include "../VariantUtilities.h" |
| #include "../WindowUtilities.h" |
| |
| #define MAXIMUM_DIALOG_FIND_RETRIES 50 |
| #define MAXIMUM_CONTROL_FIND_RETRIES 10 |
| |
| const LPCTSTR fileDialogNames[] = { |
| _T("#32770"), |
| _T("ComboBoxEx32"), |
| _T("ComboBox"), |
| _T("Edit"), |
| NULL |
| }; |
| |
| namespace webdriver { |
| |
| std::wstring SendKeysCommandHandler::error_text = L""; |
| |
| struct DialogParentWindowInfo { |
| DWORD process_id; |
| const wchar_t* class_name; |
| HWND window_handle; |
| }; |
| |
| SendKeysCommandHandler::SendKeysCommandHandler(void) { |
| } |
| |
| SendKeysCommandHandler::~SendKeysCommandHandler(void) { |
| } |
| |
| void SendKeysCommandHandler::ExecuteInternal( |
| const IECommandExecutor& executor, |
| const ParametersMap& command_parameters, |
| Response* response) { |
| ParametersMap::const_iterator id_parameter_iterator = command_parameters.find("id"); |
| ParametersMap::const_iterator value_parameter_iterator = command_parameters.find("text"); |
| if (id_parameter_iterator == command_parameters.end()) { |
| response->SetErrorResponse(400, "Missing parameter in URL: id"); |
| return; |
| } else if (value_parameter_iterator == command_parameters.end()) { |
| response->SetErrorResponse(400, "Missing parameter: text"); |
| return; |
| } else { |
| std::string element_id = id_parameter_iterator->second.asString(); |
| |
| if (!value_parameter_iterator->second.isString()) { |
| response->SetErrorResponse(ERROR_INVALID_ARGUMENT, "parameter 'text' must be a string"); |
| return; |
| } |
| std::wstring keys = StringUtilities::ToWString(value_parameter_iterator->second.asString()); |
| |
| BrowserHandle browser_wrapper; |
| int status_code = executor.GetCurrentBrowser(&browser_wrapper); |
| if (status_code != WD_SUCCESS) { |
| response->SetErrorResponse(status_code, "Unable to get browser"); |
| return; |
| } |
| |
| ElementHandle initial_element; |
| status_code = this->GetElement(executor, element_id, &initial_element); |
| |
| if (status_code == WD_SUCCESS) { |
| ElementHandle element_wrapper = initial_element; |
| CComPtr<IHTMLOptionElement> option; |
| HRESULT hr = initial_element->element()->QueryInterface<IHTMLOptionElement>(&option); |
| if (SUCCEEDED(hr) && option) { |
| // If this is an <option> element, we want to operate on its parent |
| // <select> element. |
| CComPtr<IHTMLElement> parent_node; |
| hr = initial_element->element()->get_parentElement(&parent_node); |
| while (SUCCEEDED(hr) && parent_node) { |
| CComPtr<IHTMLSelectElement> select; |
| HRESULT select_hr = parent_node->QueryInterface<IHTMLSelectElement>(&select); |
| if (SUCCEEDED(select_hr) && select) { |
| IECommandExecutor& mutable_executor = const_cast<IECommandExecutor&>(executor); |
| mutable_executor.AddManagedElement(parent_node, &element_wrapper); |
| break; |
| } |
| hr = parent_node->get_parentElement(&parent_node); |
| } |
| } |
| |
| // Scroll the target element into view before executing the action |
| // sequence. |
| LocationInfo location = {}; |
| std::vector<LocationInfo> frame_locations; |
| element_wrapper->GetLocationOnceScrolledIntoView(executor.input_manager()->scroll_behavior(), |
| &location, |
| &frame_locations); |
| |
| if (this->IsFileUploadElement(element_wrapper)) { |
| if (executor.use_strict_file_interactability()) { |
| std::string upload_error_description = ""; |
| if (!this->IsElementInteractable(element_wrapper, |
| &upload_error_description)) { |
| response->SetErrorResponse(ERROR_ELEMENT_NOT_INTERACTABLE, |
| upload_error_description); |
| return; |
| } |
| } |
| this->UploadFile(browser_wrapper, element_wrapper, executor, keys, response); |
| return; |
| } |
| |
| std::string error_description = ""; |
| bool is_interactable = IsElementInteractable(element_wrapper, |
| &error_description); |
| if (!is_interactable) { |
| response->SetErrorResponse(ERROR_ELEMENT_NOT_INTERACTABLE, |
| error_description); |
| return; |
| } |
| |
| if (!this->VerifyPageHasFocus(browser_wrapper)) { |
| LOG(WARN) << "HTML rendering pane does not have the focus. Keystrokes may go to an unexpected UI element."; |
| } |
| if (!this->WaitUntilElementFocused(element_wrapper)) { |
| error_description = "Element cannot be interacted with via the keyboard because it is not focusable"; |
| response->SetErrorResponse(ERROR_ELEMENT_NOT_INTERACTABLE, |
| error_description); |
| return; |
| } |
| |
| Json::Value actions = this->CreateActionSequencePayload(executor, &keys); |
| |
| std::string error_info = ""; |
| status_code = executor.input_manager()->PerformInputSequence(browser_wrapper, actions, &error_info); |
| response->SetSuccessResponse(Json::Value::null); |
| return; |
| } else { |
| response->SetErrorResponse(status_code, "Element is no longer valid"); |
| return; |
| } |
| } |
| } |
| |
| bool SendKeysCommandHandler::IsElementInteractable(ElementHandle element_wrapper, |
| std::string* error_description) { |
| bool displayed; |
| int status_code = element_wrapper->IsDisplayed(true, &displayed); |
| if (status_code != WD_SUCCESS || !displayed) { |
| *error_description = "Element cannot be interacted with via the keyboard because it is not displayed"; |
| return false; |
| } |
| |
| if (!element_wrapper->IsEnabled()) { |
| *error_description = "Element cannot be interacted with via the keyboard because it is not enabled"; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| Json::Value SendKeysCommandHandler::CreateActionSequencePayload(const IECommandExecutor& executor, |
| std::wstring* keys) { |
| bool shift_pressed = executor.input_manager()->is_shift_pressed(); |
| bool control_pressed = executor.input_manager()->is_control_pressed(); |
| bool alt_pressed = executor.input_manager()->is_alt_pressed(); |
| |
| keys->push_back(static_cast<wchar_t>(WD_KEY_NULL)); |
| Json::Value key_array(Json::arrayValue); |
| for (size_t i = 0; i < keys->size(); ++i) { |
| std::wstring character = L""; |
| character.push_back(keys->at(i)); |
| if (IS_HIGH_SURROGATE(keys->at(i))) { |
| // We've converted the key string to a wstring, which contain |
| // wchar_t elements. On Windows, wchar_t is 16 bits, meaning |
| // the string has been encoded to UTF-16, which implies each |
| // Unicode code point will be either one wchar_t (where the |
| // value <= 0xFFFF), or two wchar_ts (where the code point is |
| // represented by a surrogate pair). In the latter case, we |
| // test for the first part of a surrogate pair, and if it is |
| // one, we grab the next wchar_t, and use the two together to |
| // represent a single Unicode "character." |
| ++i; |
| character.push_back(keys->at(i)); |
| } |
| |
| std::string single_key = StringUtilities::ToString(character); |
| |
| if (keys->at(i) == WD_KEY_SHIFT) { |
| Json::Value shift_key_value; |
| shift_key_value["value"] = single_key; |
| if (shift_pressed) { |
| shift_key_value["type"] = "keyUp"; |
| } else { |
| shift_key_value["type"] = "keyDown"; |
| shift_pressed = true; |
| } |
| key_array.append(shift_key_value); |
| continue; |
| } else if (keys->at(i) == WD_KEY_CONTROL) { |
| Json::Value control_key_value; |
| control_key_value["value"] = single_key; |
| if (control_pressed) { |
| control_key_value["type"] = "keyUp"; |
| } else { |
| control_key_value["type"] = "keyDown"; |
| control_pressed = true; |
| } |
| key_array.append(control_key_value); |
| continue; |
| } else if (keys->at(i) == WD_KEY_ALT) { |
| Json::Value alt_key_value; |
| alt_key_value["value"] = single_key; |
| if (alt_pressed) { |
| alt_key_value["type"] = "keyUp"; |
| } else { |
| alt_key_value["type"] = "keyDown"; |
| alt_pressed = true; |
| } |
| key_array.append(alt_key_value); |
| continue; |
| } |
| |
| Json::Value key_down_value; |
| key_down_value["type"] = "keyDown"; |
| key_down_value["value"] = single_key; |
| key_array.append(key_down_value); |
| |
| Json::Value key_up_value; |
| key_up_value["type"] = "keyUp"; |
| key_up_value["value"] = single_key; |
| key_array.append(key_up_value); |
| } |
| |
| Json::Value value; |
| value["type"] = "key"; |
| value["id"] = "send keys keyboard"; |
| value["actions"] = key_array; |
| |
| Json::Value actions(Json::arrayValue); |
| actions.append(value); |
| return actions; |
| } |
| |
| bool SendKeysCommandHandler::HasMultipleAttribute(ElementHandle element_wrapper) { |
| bool allows_multiple = false; |
| CComVariant multiple_value; |
| int status_code = element_wrapper->GetAttributeValue("multiple", |
| &multiple_value); |
| if (status_code == WD_SUCCESS && |
| VariantUtilities::VariantIsString(multiple_value)) { |
| if (0 == wcscmp(multiple_value.bstrVal, L"true")) { |
| allows_multiple = true; |
| } |
| } |
| return allows_multiple; |
| } |
| |
| void SendKeysCommandHandler::UploadFile(BrowserHandle browser_wrapper, |
| ElementHandle element_wrapper, |
| const IECommandExecutor& executor, |
| const std::wstring& keys, |
| Response* response) { |
| bool allows_multiple = this->HasMultipleAttribute(element_wrapper); |
| |
| std::vector<std::wstring> file_list; |
| StringUtilities::Split(keys, L"\n", &file_list); |
| |
| if (file_list.size() == 0) { |
| response->SetErrorResponse(EINVALIDARGUMENT, |
| "Upload file cannot be an empty string."); |
| return; |
| } |
| |
| if (!allows_multiple && file_list.size() > 1) { |
| response->SetErrorResponse(EINVALIDARGUMENT, |
| "Attempting to upload multiple files to file upload element without multiple attribute."); |
| return; |
| } |
| |
| std::wstring file_directory = L""; |
| std::wstring file_dialog_keys = L""; |
| std::vector<std::wstring>::const_iterator iterator = file_list.begin(); |
| for (; iterator < file_list.end(); ++iterator) { |
| std::wstring file_name = *iterator; |
| // Key sequence should be a path and file name. Check |
| // to see that the file exists before invoking the file |
| // selection dialog. Note that we also error if the file |
| // path passed in is valid, but is a directory instead of |
| // a file. |
| bool path_exists = ::PathFileExists(file_name.c_str()) == TRUE; |
| if (!path_exists) { |
| response->SetErrorResponse(EINVALIDARGUMENT, |
| "Attempting to upload file '" + StringUtilities::ToString(file_name) + "' which does not exist."); |
| return; |
| } |
| bool path_is_directory = ::PathIsDirectory(file_name.c_str()) == TRUE; |
| if (path_is_directory) { |
| response->SetErrorResponse(EINVALIDARGUMENT, |
| "Attempting to upload file '" + StringUtilities::ToString(file_name) + "' which is a directory."); |
| return; |
| } |
| |
| if (allows_multiple) { |
| std::vector<wchar_t> file_name_buffer; |
| StringUtilities::ToBuffer(file_name, &file_name_buffer); |
| ::PathRemoveFileSpec(&file_name_buffer[0]); |
| std::wstring current_file_directory = &file_name_buffer[0]; |
| if (file_directory.size() == 0) { |
| file_directory = current_file_directory; |
| } |
| |
| if (file_directory != current_file_directory) { |
| response->SetErrorResponse(EINVALIDARGUMENT, |
| "Attempting to upload multiple files, but all files must be in the same directory."); |
| return; |
| } |
| } |
| |
| if (allows_multiple && file_dialog_keys.size() > 0) { |
| file_dialog_keys.append(L" "); |
| } |
| if (allows_multiple && file_name.at(0) != L'\"') { |
| file_dialog_keys.append(L"\""); |
| } |
| file_dialog_keys.append(file_name); |
| if (allows_multiple && file_name.at(file_name.size() - 1) != L'\"') { |
| file_dialog_keys.append(L"\""); |
| } |
| } |
| |
| HWND window_handle = browser_wrapper->GetContentWindowHandle(); |
| HWND top_level_window_handle = browser_wrapper->GetTopLevelWindowHandle(); |
| |
| DWORD ie_process_id; |
| ::GetWindowThreadProcessId(window_handle, &ie_process_id); |
| |
| FileNameData key_data; |
| key_data.main = top_level_window_handle; |
| key_data.hwnd = window_handle; |
| key_data.text = file_dialog_keys.c_str(); |
| key_data.ieProcId = ie_process_id; |
| key_data.dialogTimeout = executor.file_upload_dialog_timeout(); |
| key_data.useLegacyDialogHandling = executor.use_legacy_file_upload_dialog_handling(); |
| |
| unsigned int thread_id; |
| HANDLE thread_handle = reinterpret_cast<HANDLE>(_beginthreadex(NULL, |
| 0, |
| &SendKeysCommandHandler::SetFileValue, |
| reinterpret_cast<void*>(&key_data), |
| 0, |
| &thread_id)); |
| |
| LOG(DEBUG) << "Clicking upload button and starting to handle file selection dialog"; |
| element_wrapper->element()->click(); |
| // We're now blocked until the dialog closes. |
| ::CloseHandle(thread_handle); |
| response->SetSuccessResponse(Json::Value::null); |
| } |
| |
| bool SendKeysCommandHandler::IsFileUploadElement(ElementHandle element_wrapper) { |
| CComPtr<IHTMLElement> element(element_wrapper->element()); |
| |
| CComPtr<IHTMLInputFileElement> file; |
| element->QueryInterface<IHTMLInputFileElement>(&file); |
| CComPtr<IHTMLInputElement> input; |
| element->QueryInterface<IHTMLInputElement>(&input); |
| CComBSTR element_type; |
| if (input) { |
| input->get_type(&element_type); |
| HRESULT hr = element_type.ToLower(); |
| if (FAILED(hr)) { |
| LOGHR(WARN, hr) << "Failed converting type attribute of <input> element to lowercase using ToLower() method of BSTR"; |
| } |
| } |
| bool is_file_element = (file != NULL) || |
| (input != NULL && element_type == L"file"); |
| return is_file_element; |
| } |
| |
| bool SendKeysCommandHandler::GetFileSelectionDialogCandidates(std::vector<HWND> parent_window_handles, IUIAutomation* ui_automation, IUIAutomationElementArray** dialog_candidates) { |
| LOG(INFO) << "using " << parent_window_handles.size() << " parent windows"; |
| CComVariant dialog_control_type(UIA_WindowControlTypeId); |
| CComPtr<IUIAutomationCondition> dialog_condition; |
| HRESULT hr = ui_automation->CreatePropertyCondition(UIA_ControlTypePropertyId, |
| dialog_control_type, |
| &dialog_condition); |
| if (FAILED(hr)) { |
| LOGHR(WARN, hr) << "Could not create condition to look for dialog"; |
| return false; |
| } |
| |
| bool found_candidate_dialogs = false; |
| int window_array_length = 0; |
| std::vector<HWND>::const_iterator handle_iterator = parent_window_handles.begin(); |
| for (; handle_iterator != parent_window_handles.end(); ++handle_iterator) { |
| CComPtr<IUIAutomationElement> parent_window; |
| hr = ui_automation->ElementFromHandle(*handle_iterator, &parent_window); |
| if (FAILED(hr)) { |
| LOGHR(WARN, hr) << "Did not get parent window UI Automation object"; |
| continue; |
| } |
| |
| CComPtr<IUIAutomationElementArray> current_dialog_candidates; |
| hr = parent_window->FindAll(TreeScope::TreeScope_Children, |
| dialog_condition, |
| ¤t_dialog_candidates); |
| if (FAILED(hr)) { |
| LOGHR(WARN, hr) << "Process of finding child dialogs of parent window failed"; |
| continue; |
| } |
| |
| if (!current_dialog_candidates) { |
| LOGHR(WARN, hr) << "Found no dialogs as children of parent window (null candidates)"; |
| continue; |
| } |
| |
| hr = current_dialog_candidates->get_Length(&window_array_length); |
| if (FAILED(hr)) { |
| LOGHR(WARN, hr) << "Could not get length of list of child dialogs of parent window"; |
| continue; |
| } |
| |
| if (window_array_length == 0) { |
| LOG(WARN) << "Found no dialogs as children of parent window (empty candidates)"; |
| continue; |
| } else { |
| // Use CComPtr::CopyTo() to increment the refcount, because when the |
| // current dialog candidates pointer goes out of scope, it will decrement |
| // the refcount, which will free the object when the refcount equals |
| // zero. |
| LOG(INFO) << "Found " << window_array_length << "children"; |
| current_dialog_candidates.CopyTo(dialog_candidates); |
| found_candidate_dialogs = true; |
| break; |
| } |
| } |
| |
| return found_candidate_dialogs; |
| } |
| |
| bool SendKeysCommandHandler::FillFileName(const wchar_t* file_name, |
| IUIAutomation* ui_automation, |
| IUIAutomationElement* file_selection_dialog) { |
| CComVariant file_name_combo_box_automation_id(L"1148"); |
| CComPtr<IUIAutomationCondition> file_name_combo_box_condition; |
| HRESULT hr = ui_automation->CreatePropertyCondition(UIA_AutomationIdPropertyId, |
| file_name_combo_box_automation_id, |
| &file_name_combo_box_condition); |
| if (FAILED(hr)) { |
| LOGHR(WARN, hr) << "Could not create condition to look for file selection combo box"; |
| return false; |
| } |
| |
| CComPtr<IUIAutomationElement> file_name_combo_box; |
| hr = file_selection_dialog->FindFirst(TreeScope::TreeScope_Children, |
| file_name_combo_box_condition, |
| &file_name_combo_box); |
| if (FAILED(hr)) { |
| LOGHR(WARN, hr) << "Failed to get file name combo box on current dialog, trying next dialog"; |
| return false; |
| } |
| |
| CComVariant edit_control_type(UIA_EditControlTypeId); |
| CComPtr<IUIAutomationCondition> file_name_edit_condition; |
| hr = ui_automation->CreatePropertyCondition(UIA_ControlTypePropertyId, |
| edit_control_type, |
| &file_name_edit_condition); |
| if (FAILED(hr)) { |
| LOGHR(WARN, hr) << "Could not create condition to look for file selection edit control"; |
| return false; |
| } |
| |
| CComPtr<IUIAutomationElement> file_name_edit_box; |
| hr = file_name_combo_box->FindFirst(TreeScope::TreeScope_Children, |
| file_name_edit_condition, |
| &file_name_edit_box); |
| if (FAILED(hr)) { |
| LOGHR(WARN, hr) << "Failed to get file name edit box from combo box on current dialog, trying next dialog"; |
| return false; |
| } |
| |
| CComPtr<IUIAutomationValuePattern> file_name_value_pattern; |
| hr = file_name_edit_box->GetCurrentPatternAs(UIA_ValuePatternId, |
| IID_PPV_ARGS(&file_name_value_pattern)); |
| if (FAILED(hr)) { |
| LOGHR(WARN, hr) << "Failed to get value pattern for file name edit box on current dialog, trying next dialog"; |
| return false; |
| } |
| |
| CComBSTR file_name_bstr(file_name); |
| hr = file_name_value_pattern->SetValue(file_name_bstr); |
| if (FAILED(hr)) { |
| LOGHR(WARN, hr) << "Failed to get set file name on current dialog, trying next dialog"; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool SendKeysCommandHandler::AcceptFileSelection(IUIAutomation* ui_automation, |
| IUIAutomationElement* file_selection_dialog) { |
| CComVariant open_button_automation_id(L"1"); |
| CComPtr<IUIAutomationCondition> open_button_condition; |
| HRESULT hr = ui_automation->CreatePropertyCondition(UIA_AutomationIdPropertyId, |
| open_button_automation_id, |
| &open_button_condition); |
| if (FAILED(hr)) { |
| LOGHR(WARN, hr) << "Could not create condition to look for open button"; |
| return false; |
| } |
| |
| CComPtr<IUIAutomationElement> open_button; |
| hr = file_selection_dialog->FindFirst(TreeScope::TreeScope_Children, |
| open_button_condition, |
| &open_button); |
| if (FAILED(hr)) { |
| LOGHR(WARN, hr) << "Failed to get open button on current dialog, trying next dialog"; |
| return false; |
| } |
| |
| CComPtr<IUIAutomationInvokePattern> open_button_invoke_pattern; |
| hr = open_button->GetCurrentPatternAs(UIA_InvokePatternId, |
| IID_PPV_ARGS(&open_button_invoke_pattern)); |
| if (FAILED(hr)) { |
| LOGHR(WARN, hr) << "Failed to get invoke pattern for open button on current dialog, trying next dialog"; |
| return false; |
| } |
| |
| hr = open_button_invoke_pattern->Invoke(); |
| if (FAILED(hr)) { |
| LOGHR(WARN, hr) << "Failed to click open button on current dialog, trying next dialog"; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool SendKeysCommandHandler::WaitForFileSelectionDialogClose(const int timeout, |
| IUIAutomationElement* file_selection_dialog) { |
| HWND dialog_window_handle; |
| HRESULT hr = file_selection_dialog->get_CurrentNativeWindowHandle(reinterpret_cast<UIA_HWND*>(&dialog_window_handle)); |
| if (FAILED(hr)) { |
| LOGHR(WARN, hr) << "Could not get window handle for file selection dialog"; |
| return false; |
| } |
| int max_retries = timeout / 100; |
| bool is_dialog_closed = ::IsWindow(dialog_window_handle) == FALSE; |
| while (!is_dialog_closed && --max_retries) { |
| ::Sleep(100); |
| is_dialog_closed = ::IsWindow(dialog_window_handle) == FALSE; |
| } |
| |
| return is_dialog_closed; |
| } |
| |
| bool SendKeysCommandHandler::FindFileSelectionErrorDialog(IUIAutomation* ui_automation, |
| IUIAutomationElement* file_selection_dialog, |
| IUIAutomationElement** error_dialog) { |
| CComVariant dialog_control_type(UIA_WindowControlTypeId); |
| CComPtr<IUIAutomationCondition> dialog_condition; |
| HRESULT hr = ui_automation->CreatePropertyCondition(UIA_ControlTypePropertyId, |
| dialog_control_type, |
| &dialog_condition); |
| if (FAILED(hr)) { |
| LOGHR(WARN, hr) << "Could not create condition to look for dialog"; |
| return false; |
| } |
| |
| hr = file_selection_dialog->FindFirst(TreeScope::TreeScope_Children, |
| dialog_condition, |
| error_dialog); |
| if (FAILED(hr)) { |
| LOGHR(WARN, hr) << "Could not find error dialog owned by file selection dialog"; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool SendKeysCommandHandler::DismissFileSelectionErrorDialog(IUIAutomation* ui_automation, |
| IUIAutomationElement* error_dialog) { |
| CComVariant error_dialog_text_automation_id(L"ContentText"); |
| CComPtr<IUIAutomationCondition> error_dialog_text_condition; |
| HRESULT hr = ui_automation->CreatePropertyCondition(UIA_AutomationIdPropertyId, |
| error_dialog_text_automation_id, |
| &error_dialog_text_condition); |
| if (FAILED(hr)) { |
| LOGHR(WARN, hr) << "Could not create condition to look for error text control"; |
| return false; |
| } |
| |
| CComPtr<IUIAutomationElement> error_dialog_text_control; |
| hr = error_dialog->FindFirst(TreeScope::TreeScope_Children, |
| error_dialog_text_condition, |
| &error_dialog_text_control); |
| if (FAILED(hr)) { |
| LOGHR(WARN, hr) << "Failed to get error message text control on error dialog"; |
| } |
| |
| CComBSTR error_dialog_text; |
| hr = error_dialog_text_control->get_CurrentName(&error_dialog_text); |
| if (FAILED(hr)) { |
| LOGHR(WARN, hr) << "Failed to get error message text from text control on error dialog"; |
| } |
| |
| error_text = error_dialog_text; |
| |
| CComVariant error_dialog_ok_button_automation_id(L"CommandButton_1"); |
| CComPtr<IUIAutomationCondition> error_dialog_ok_button_condition; |
| hr = ui_automation->CreatePropertyCondition(UIA_AutomationIdPropertyId, |
| error_dialog_ok_button_automation_id, |
| &error_dialog_ok_button_condition); |
| if (FAILED(hr)) { |
| LOGHR(WARN, hr) << "Could not create condition to look for error message OK button"; |
| return false; |
| } |
| |
| CComPtr<IUIAutomationElement> error_dialog_ok_button; |
| hr = error_dialog->FindFirst(TreeScope::TreeScope_Children, |
| error_dialog_ok_button_condition, |
| &error_dialog_ok_button); |
| if (FAILED(hr)) { |
| LOGHR(WARN, hr) << "Failed to get OK button on error dialog"; |
| return false; |
| } |
| |
| CComPtr<IUIAutomationInvokePattern> error_dialog_ok_button_invoke_pattern; |
| hr = error_dialog_ok_button->GetCurrentPatternAs(UIA_InvokePatternId, |
| IID_PPV_ARGS(&error_dialog_ok_button_invoke_pattern)); |
| if (FAILED(hr)) { |
| LOGHR(WARN, hr) << "Failed to get invoke pattern for OK button on error dialog"; |
| return false; |
| } |
| |
| hr = error_dialog_ok_button_invoke_pattern->Invoke(); |
| if (FAILED(hr)) { |
| LOGHR(WARN, hr) << "Failed to click OK button on error dialog"; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool SendKeysCommandHandler::DismissFileSelectionDialog(IUIAutomation* ui_automation, |
| IUIAutomationElement* file_selection_dialog) { |
| CComVariant cancel_button_automation_id(L"2"); |
| CComPtr<IUIAutomationCondition> cancel_button_condition; |
| HRESULT hr = ui_automation->CreatePropertyCondition(UIA_AutomationIdPropertyId, |
| cancel_button_automation_id, |
| &cancel_button_condition); |
| if (FAILED(hr)) { |
| LOGHR(WARN, hr) << "Could not create condition to look for cancel button"; |
| return false; |
| } |
| |
| CComPtr<IUIAutomationElement> cancel_button; |
| hr = file_selection_dialog->FindFirst(TreeScope::TreeScope_Children, |
| cancel_button_condition, |
| &cancel_button); |
| if (FAILED(hr)) { |
| LOGHR(WARN, hr) << "Failed to get cancel button on current dialog"; |
| return false; |
| } |
| |
| CComPtr<IUIAutomationInvokePattern> cancel_button_invoke_pattern; |
| hr = cancel_button->GetCurrentPatternAs(UIA_InvokePatternId, |
| IID_PPV_ARGS(&cancel_button_invoke_pattern)); |
| if (FAILED(hr)) { |
| LOGHR(WARN, hr) << "Failed to get invoke pattern for cancel button on current dialog"; |
| return false; |
| } |
| |
| hr = cancel_button_invoke_pattern->Invoke(); |
| if (FAILED(hr)) { |
| LOGHR(WARN, hr) << "Failed to cancel file selection dialog"; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| std::vector<HWND> SendKeysCommandHandler::FindWindowCandidates(FileNameData* file_data) { |
| // Find a dialog parent window with a class name of "Alternate |
| // Modal Top Most" belonging to the same process as the IE |
| // content process. If we find one, add it to the list of |
| // window handles that might be the file selection dialog's |
| // direct parent. |
| |
| DialogParentWindowInfo window_info; |
| window_info.process_id = file_data->ieProcId; |
| window_info.class_name = L"Alternate Modal Top Most"; |
| window_info.window_handle = NULL; |
| ::EnumWindows(&SendKeysCommandHandler::FindWindowWithClassNameAndProcess, |
| reinterpret_cast<LPARAM>(&window_info)); |
| std::vector<HWND> window_handles; |
| if (window_info.window_handle != NULL) { |
| LOG(INFO) << "found \"" << window_info.class_name << "\" " << window_info.window_handle; |
| window_handles.push_back(window_info.window_handle); |
| } |
| window_handles.push_back(file_data->main); |
| return window_handles; |
| } |
| |
| bool SendKeysCommandHandler::SendFileNameKeys(FileNameData* file_data) { |
| CComPtr<IUIAutomation> ui_automation; |
| HRESULT hr = ::CoCreateInstance(CLSID_CUIAutomation, |
| NULL, |
| CLSCTX_INPROC_SERVER, |
| IID_IUIAutomation, |
| reinterpret_cast<void**>(&ui_automation)); |
| |
| if (FAILED(hr)) { |
| LOGHR(WARN, hr) << "Unable to create global UI Automation object"; |
| error_text = L"The driver was unable to initialize the Windows UI Automation system. This is a Windows installation problem, not a driver problem."; |
| return false; |
| } |
| |
| std::vector<HWND> window_handles = FindWindowCandidates(file_data); |
| // Find all candidates for the file selection dialog. Retry until timeout. |
| int max_retries = file_data->dialogTimeout / 100; |
| CComPtr<IUIAutomationElementArray> dialog_candidates; |
| bool dialog_candidates_found = GetFileSelectionDialogCandidates(window_handles, |
| ui_automation, |
| &dialog_candidates); |
| while (!dialog_candidates_found && --max_retries) { |
| dialog_candidates.Release(); |
| ::Sleep(100); |
| window_handles = FindWindowCandidates(file_data); |
| dialog_candidates_found = GetFileSelectionDialogCandidates(window_handles, |
| ui_automation, |
| &dialog_candidates); |
| } |
| |
| if (!dialog_candidates_found) { |
| LOG(WARN) << "Did not find any dialogs after dialog timeout"; |
| error_text = L"The driver did not find the file selection dialog before the timeout."; |
| return false; |
| } |
| |
| // We must have a valid array of dialog candidates at this point, |
| // so we should be able to safely ignore checking for error values. |
| int window_array_length = 0; |
| hr = dialog_candidates->get_Length(&window_array_length); |
| |
| for (int i = 0; i < window_array_length; ++i) { |
| CComPtr<IUIAutomationElement> file_selection_dialog; |
| hr = dialog_candidates->GetElement(i, &file_selection_dialog); |
| if (FAILED(hr)) { |
| LOGHR(WARN, hr) << "Failed to get element " << i << " from list of child dialogs of IE main window, trying next dialog"; |
| continue; |
| } |
| if (!FillFileName(file_data->text, ui_automation, file_selection_dialog)) { |
| continue; |
| } |
| if (!AcceptFileSelection(ui_automation, file_selection_dialog)) { |
| continue; |
| } |
| if (WaitForFileSelectionDialogClose(file_data->dialogTimeout, |
| file_selection_dialog)) { |
| // Full success case. Break out of loop and return true. |
| break; |
| } |
| |
| // At this point, successfully found a file selection dialog, set its file name |
| // and attempted to accept the selection. However, the file selection dialog didn't |
| // close in a timely fashion, which indicates an error condition thrown up by the |
| // browser. Check for an error dialog, and if one is found, dismiss it and the file |
| // selection dialog so as not to hang the driver. |
| CComPtr<IUIAutomationElement> error_dialog; |
| if (!FindFileSelectionErrorDialog(ui_automation, |
| file_selection_dialog, |
| &error_dialog)) { |
| error_text = L"The driver found the file selection dialog, set the file information, and clicked the open button, but the dialog did not close in a timely manner."; |
| return false; |
| } |
| |
| if (!DismissFileSelectionErrorDialog(ui_automation, error_dialog)) { |
| return false; |
| } |
| |
| if (!DismissFileSelectionDialog(ui_automation, file_selection_dialog)) { |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| BOOL CALLBACK SendKeysCommandHandler::FindWindowWithClassNameAndProcess(HWND hwnd, LPARAM arg) { |
| DialogParentWindowInfo* process_win_info = reinterpret_cast<DialogParentWindowInfo*>(arg); |
| size_t number_of_characters = wcsnlen(process_win_info->class_name, 255); |
| std::vector<wchar_t> class_name(number_of_characters + 1); |
| if (::GetClassName(hwnd, &class_name[0], static_cast<int>(class_name.size())) == 0) { |
| // No match found. Skip |
| return TRUE; |
| } |
| |
| if (wcscmp(process_win_info->class_name, &class_name[0]) != 0) { |
| return TRUE; |
| } else { |
| DWORD process_id = NULL; |
| ::GetWindowThreadProcessId(hwnd, &process_id); |
| if (process_win_info->process_id == process_id) { |
| // Once we've found the first dialog (#32770) window |
| // for the process we want, we can stop. |
| process_win_info->window_handle = hwnd; |
| return FALSE; |
| } |
| } |
| |
| return TRUE; |
| } |
| unsigned int WINAPI SendKeysCommandHandler::SetFileValue(void *file_data) { |
| FileNameData* data = reinterpret_cast<FileNameData*>(file_data); |
| ::Sleep(100); |
| |
| bool file_name_successfully_sent = false; |
| unsigned int return_value = 0; |
| // On a separate thread, so must reinitialize COM. |
| HRESULT hr = ::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); |
| if (FAILED(hr)) { |
| return_value = 1; |
| LOGHR(WARN, hr) << "COM library initialization encountered an error"; |
| error_text = L"The driver could not initialize COM on the thread used to handle the file selection dialog."; |
| } else { |
| file_name_successfully_sent = SendFileNameKeys(data); |
| ::CoUninitialize(); |
| if (!file_name_successfully_sent) { |
| return_value = 1; |
| } |
| } |
| |
| // TODO: Remove this and the supporting functions once feedback |
| // on the use of UI Automation is completed. |
| if (!file_name_successfully_sent && data->useLegacyDialogHandling) { |
| LegacySelectFile(data); |
| } |
| |
| return return_value; |
| } |
| |
| bool SendKeysCommandHandler::LegacySelectFile(FileNameData* data) { |
| HWND ie_main_window_handle = data->main; |
| HWND dialog_window_handle = ::GetLastActivePopup(ie_main_window_handle); |
| |
| int max_retries = data->dialogTimeout / 100; |
| if (!dialog_window_handle || |
| (dialog_window_handle == ie_main_window_handle)) { |
| LOG(DEBUG) << "No dialog directly owned by the top-level window found. " |
| << "Beginning search for dialog. Will search for " |
| << max_retries << " attempts at 100ms intervals."; |
| // No dialog directly owned by the top-level window. |
| // Look for a dialog belonging to the same process as |
| // the IE server window. This isn't perfect, but it's |
| // all we have for now. |
| while ((dialog_window_handle == ie_main_window_handle) && --max_retries) { |
| ::Sleep(100); |
| ProcessWindowInfo process_win_info; |
| process_win_info.dwProcessId = data->ieProcId; |
| ::EnumWindows(&BrowserFactory::FindDialogWindowForProcess, |
| reinterpret_cast<LPARAM>(&process_win_info)); |
| if (process_win_info.hwndBrowser != NULL) { |
| dialog_window_handle = process_win_info.hwndBrowser; |
| } |
| } |
| } |
| |
| if (!dialog_window_handle || |
| (dialog_window_handle == ie_main_window_handle)) { |
| LOG(DEBUG) << "Did not find dialog owned by IE process. " |
| << "Searching again using GetLastActivePopup API."; |
| max_retries = data->dialogTimeout / 100; |
| while ((dialog_window_handle == ie_main_window_handle) && --max_retries) { |
| ::Sleep(100); |
| dialog_window_handle = ::GetLastActivePopup(ie_main_window_handle); |
| } |
| } |
| |
| if (!dialog_window_handle || |
| (dialog_window_handle == ie_main_window_handle)) { |
| LOG(WARN) << "No dialog found"; |
| return false; |
| } |
| |
| LOG(DEBUG) << "Found file upload dialog with handle " |
| << StringUtilities::Format("0x%08X", dialog_window_handle) |
| << ". Window has caption '" |
| << WindowUtilities::GetWindowCaption(dialog_window_handle) |
| << "' Starting to look for file name edit control."; |
| return LegacySendKeysToFileUploadAlert(dialog_window_handle, data->text); |
| } |
| |
| bool SendKeysCommandHandler::LegacySendKeysToFileUploadAlert( |
| HWND dialog_window_handle, |
| const wchar_t* value) { |
| HWND edit_field_window_handle = NULL; |
| int max_wait = MAXIMUM_CONTROL_FIND_RETRIES; |
| while (!edit_field_window_handle && --max_wait) { |
| WindowUtilities::Wait(200); |
| edit_field_window_handle = dialog_window_handle; |
| for (int i = 1; fileDialogNames[i]; ++i) { |
| std::wstring child_window_class = fileDialogNames[i]; |
| edit_field_window_handle = WindowUtilities::GetChildWindow( |
| edit_field_window_handle, |
| child_window_class); |
| if (!edit_field_window_handle) { |
| LOG(WARN) << "Didn't find window with class name '" |
| << LOGWSTRING(child_window_class) |
| << "' during attempt " |
| << MAXIMUM_CONTROL_FIND_RETRIES - max_wait |
| << " of " << MAXIMUM_CONTROL_FIND_RETRIES << "."; |
| } |
| } |
| } |
| |
| if (edit_field_window_handle) { |
| // Attempt to set the value, looping until we succeed. |
| LOG(DEBUG) << "Found edit control on file selection dialog"; |
| const wchar_t* filename = value; |
| size_t expected = wcslen(filename); |
| size_t curr = 0; |
| |
| max_wait = MAXIMUM_CONTROL_FIND_RETRIES; |
| while ((expected != curr) && --max_wait) { |
| ::SendMessage(edit_field_window_handle, |
| WM_SETTEXT, |
| 0, |
| reinterpret_cast<LPARAM>(filename)); |
| WindowUtilities::Wait(1000); |
| curr = ::SendMessage(edit_field_window_handle, WM_GETTEXTLENGTH, 0, 0); |
| } |
| |
| if (expected != curr) { |
| LOG(WARN) << "Did not send the expected number of characters to the " |
| << "file name control. Expected: " << expected << ", " |
| << "Actual: " << curr; |
| } |
| |
| max_wait = MAXIMUM_DIALOG_FIND_RETRIES; |
| bool triedToDismiss = false; |
| for (int i = 0; i < max_wait; i++) { |
| HWND open_button_window_handle = ::GetDlgItem(dialog_window_handle, |
| IDOK); |
| if (open_button_window_handle) { |
| LRESULT total = 0; |
| total += ::SendMessage(open_button_window_handle, |
| WM_LBUTTONDOWN, |
| 0, |
| 0); |
| total += ::SendMessage(open_button_window_handle, |
| WM_LBUTTONUP, |
| 0, |
| 0); |
| |
| // SendMessage should return zero for those messages if properly |
| // processed. |
| if (total == 0) { |
| triedToDismiss = true; |
| // Sometimes IE10 doesn't dismiss this dialog after the messages |
| // are received, even though the messages were processed |
| // successfully. If not, try again, just in case. |
| if (!::IsWindow(dialog_window_handle)) { |
| LOG(DEBUG) << "Dialog successfully dismissed"; |
| return true; |
| } |
| } |
| |
| WindowUtilities::Wait(200); |
| } |
| else if (triedToDismiss) { |
| // Probably just a slow close |
| LOG(DEBUG) << "Did not find OK button, but did previously. Assume dialog dismiss worked."; |
| return true; |
| } |
| } |
| |
| LOG(ERROR) << "Unable to set value of file input dialog"; |
| return false; |
| } |
| |
| LOG(WARN) << "No edit found"; |
| return false; |
| } |
| |
| bool SendKeysCommandHandler::VerifyPageHasFocus(BrowserHandle browser_wrapper) { |
| HWND browser_pane_window_handle = browser_wrapper->GetContentWindowHandle(); |
| DWORD proc; |
| DWORD thread_id = ::GetWindowThreadProcessId(browser_pane_window_handle, &proc); |
| GUITHREADINFO info; |
| info.cbSize = sizeof(GUITHREADINFO); |
| ::GetGUIThreadInfo(thread_id, &info); |
| |
| if (info.hwndFocus != browser_pane_window_handle) { |
| LOG(INFO) << "Focus is on a UI element other than the HTML viewer pane."; |
| // The focus is on a UI element other than the HTML viewer pane (like |
| // the address bar, for instance). This has implications for certain |
| // keystrokes, like backspace. We need to set the focus to the HTML |
| // viewer pane. |
| // N.B. The SetFocus() API should *NOT* cause the IE browser window to |
| // magically appear in the foreground. If that is not true, we will need |
| // to find some other solution. |
| // Send an explicit WM_KILLFOCUS message to free up SetFocus() to place the |
| // focus on the correct window. While SetFocus() is supposed to already do |
| // this, it seems to not work entirely correctly. |
| ::SendMessage(info.hwndFocus, WM_KILLFOCUS, NULL, NULL); |
| DWORD current_thread_id = ::GetCurrentThreadId(); |
| ::AttachThreadInput(current_thread_id, thread_id, TRUE); |
| HWND previous_focus = ::SetFocus(browser_pane_window_handle); |
| if (previous_focus == NULL) { |
| LOGERR(WARN) << "SetFocus API call failed"; |
| } |
| ::AttachThreadInput(current_thread_id, thread_id, FALSE); |
| ::GetGUIThreadInfo(thread_id, &info); |
| } |
| return info.hwndFocus == browser_pane_window_handle; |
| } |
| |
| bool SendKeysCommandHandler::WaitUntilElementFocused(ElementHandle element_wrapper) { |
| // Check we have focused the element. |
| CComPtr<IHTMLElement> element = element_wrapper->element(); |
| bool has_focus = false; |
| CComPtr<IDispatch> dispatch; |
| element->get_document(&dispatch); |
| CComPtr<IHTMLDocument2> document; |
| dispatch->QueryInterface<IHTMLDocument2>(&document); |
| |
| // If the element we want is already the focused element, we're done. |
| CComPtr<IHTMLElement> active_element; |
| if (document->get_activeElement(&active_element) == S_OK) { |
| if (active_element.IsEqualObject(element)) { |
| if (this->IsContentEditable(element)) { |
| this->SetElementFocus(element); |
| } |
| return true; |
| } |
| } |
| |
| this->SetElementFocus(element); |
| |
| // Hard-coded 1 second timeout here. Possible TODO is make this adjustable. |
| clock_t max_wait = clock() + CLOCKS_PER_SEC; |
| for (int i = clock(); i < max_wait; i = clock()) { |
| WindowUtilities::Wait(1); |
| CComPtr<IHTMLElement> active_wait_element; |
| if (document->get_activeElement(&active_wait_element) == S_OK && active_wait_element != NULL) { |
| CComPtr<IHTMLElement2> element2; |
| element->QueryInterface<IHTMLElement2>(&element2); |
| CComPtr<IHTMLElement2> active_wait_element2; |
| active_wait_element->QueryInterface<IHTMLElement2>(&active_wait_element2); |
| if (element2.IsEqualObject(active_wait_element2)) { |
| this->SetInsertionPoint(element); |
| has_focus = true; |
| break; |
| } |
| } |
| } |
| |
| return has_focus; |
| } |
| |
| bool SendKeysCommandHandler::SetInsertionPoint(IHTMLElement* element) { |
| CComPtr<IHTMLTxtRange> range; |
| CComPtr<IHTMLInputTextElement> input_element; |
| HRESULT hr = element->QueryInterface<IHTMLInputTextElement>(&input_element); |
| if (SUCCEEDED(hr) && input_element) { |
| input_element->createTextRange(&range); |
| } else { |
| CComPtr<IHTMLTextAreaElement> text_area_element; |
| hr = element->QueryInterface<IHTMLTextAreaElement>(&text_area_element); |
| if (SUCCEEDED(hr) && text_area_element) { |
| text_area_element->createTextRange(&range); |
| } else { |
| bool is_content_editable = this->IsContentEditable(element); |
| if (is_content_editable) { |
| CComPtr<IDispatch> dispatch; |
| hr = element->get_document(&dispatch); |
| if (dispatch) { |
| CComPtr<IHTMLDocument2> doc; |
| hr = dispatch->QueryInterface<IHTMLDocument2>(&doc); |
| if (doc) { |
| CComPtr<IHTMLElement> body; |
| hr = doc->get_body(&body); |
| if (body) { |
| CComPtr<IHTMLBodyElement> body_element; |
| hr = body->QueryInterface<IHTMLBodyElement>(&body_element); |
| if (body_element) { |
| hr = body_element->createTextRange(&range); |
| range->moveToElementText(element); |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| if (range) { |
| range->collapse(VARIANT_FALSE); |
| range->select(); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| bool SendKeysCommandHandler::IsContentEditable(IHTMLElement* element) { |
| CComPtr<IHTMLElement3> element3; |
| element->QueryInterface<IHTMLElement3>(&element3); |
| VARIANT_BOOL is_content_editable_variant = VARIANT_FALSE; |
| if (element3) { |
| element3->get_isContentEditable(&is_content_editable_variant); |
| } |
| return is_content_editable_variant == VARIANT_TRUE; |
| } |
| |
| void SendKeysCommandHandler::SetElementFocus(IHTMLElement* element) { |
| CComPtr<IHTMLElement2> element2; |
| element->QueryInterface<IHTMLElement2>(&element2); |
| element2->focus(); |
| } |
| |
| } // namespace webdriver |