Skip to content

Commit

Permalink
Updating IE to be able to close Windows 10 credentials dialog
Browse files Browse the repository at this point in the history
When Microsoft updated Windows 10's credentials prompt dialog, they
made it a dialog that is not a standard alert dialog and therefore
not able to be automated using standard Windows automation techniques.
With a recent preview release, it became possible to manipulate this
dialog using the UI Automation feature of Windows. The driver will now
be able to accept or dismiss this credentials dialog now. Please note
that there is still no supported solution for entering credentials, as
there is no support for that feature in the W3C WebDriver Specification.
  • Loading branch information
jimevans committed Feb 25, 2019
1 parent c183a9c commit 83b2736
Show file tree
Hide file tree
Showing 13 changed files with 180 additions and 53 deletions.
176 changes: 133 additions & 43 deletions cpp/iedriver/Alert.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,14 @@
// limitations under the License.

#include "Alert.h"

#include <UIAutomation.h>

#include "errorcodes.h"
#include "logging.h"
#include "DocumentHost.h"
#include "StringUtilities.h"
#include "WebDriverConstants.h"

#define INVALID_CONTROL_ID -1

Expand All @@ -42,11 +46,20 @@ Alert::Alert(std::shared_ptr<DocumentHost> browser, HWND handle) {
}
}

std::vector<HWND> text_boxes;
::EnumChildWindows(this->alert_handle_,
&Alert::FindTextBoxes,
reinterpret_cast<LPARAM>(&text_boxes));
this->is_security_alert_ = text_boxes.size() > 1;
std::vector<char> window_class(30);
::GetClassNameA(handle, &window_class[0], 30);

if (strcmp(&window_class[0], SECURITY_DIALOG_WINDOW_CLASS) == 0) {
this->is_standard_alert_ = false;
this->is_standard_control_alert_ = false;
this->is_security_alert_ = true;
} else {
std::vector<HWND> text_boxes;
::EnumChildWindows(this->alert_handle_,
&Alert::FindTextBoxes,
reinterpret_cast<LPARAM>(&text_boxes));
this->is_security_alert_ = text_boxes.size() > 1;
}
}


Expand Down Expand Up @@ -121,6 +134,9 @@ int Alert::SetPassword(const std::string& password) {
int Alert::SendKeysInternal(const std::string& keys,
TextBoxFindInfo* text_box_find_info) {
LOG(TRACE) << "Entering Alert::SendKeysInternal";
if (!this->is_standard_alert_) {
return EUNSUPPORTEDOPERATION;
}
// Alert present, find the text box.
// Retry up to 10 times to find the dialog.
int max_wait = 10;
Expand Down Expand Up @@ -355,20 +371,27 @@ int Alert::ClickAlertButton(DialogButtonInfo button_info) {
button_info.button_control_id,
NULL);
} else {
// For non-standard alerts (that is, alerts that are not
// created by alert(), confirm() or prompt() JavaScript
// functions), we cheat. Sending the BN_CLICKED notification
// via WM_COMMAND makes the dialog think that the proper
// button was clicked, but it's not the same as sending the
// click message to the button. N.B., sending the BM_CLICK
// message to the button may fail if the dialog doesn't have
// focus, so we do it this way. Also, we send the notification
// to the immediate parent of the button, which, in turn,
// notifies the top-level dialog.
::SendMessage(::GetParent(button_info.button_handle),
WM_COMMAND,
MAKEWPARAM(0, BN_CLICKED),
reinterpret_cast<LPARAM>(button_info.button_handle));
if (button_info.use_accessibility) {
int status_code = ClickAlertButtonUsingAccessibility(button_info.accessibility_id);
if (status_code != WD_SUCCESS) {
return status_code;
}
} else {
// For non-standard alerts (that is, alerts that are not
// created by alert(), confirm() or prompt() JavaScript
// functions), we cheat. Sending the BN_CLICKED notification
// via WM_COMMAND makes the dialog think that the proper
// button was clicked, but it's not the same as sending the
// click message to the button. N.B., sending the BM_CLICK
// message to the button may fail if the dialog doesn't have
// focus, so we do it this way. Also, we send the notification
// to the immediate parent of the button, which, in turn,
// notifies the top-level dialog.
::SendMessage(::GetParent(button_info.button_handle),
WM_COMMAND,
MAKEWPARAM(0, BN_CLICKED),
reinterpret_cast<LPARAM>(button_info.button_handle));
}
}
// Hack to make sure alert is really closed, and browser
// is ready for the next operation. This may be a flawed
Expand All @@ -393,37 +416,104 @@ int Alert::ClickAlertButton(DialogButtonInfo button_info) {

Alert::DialogButtonInfo Alert::GetDialogButton(BUTTON_TYPE button_type) {
LOG(TRACE) << "Entering Alert::GetDialogButton";
DialogButtonFindInfo button_find_info;
button_find_info.button_handle = NULL;
button_find_info.button_control_id = this->is_standard_alert_ ? IDOK : INVALID_CONTROL_ID;
if (button_type == OK) {
button_find_info.match_proc = &Alert::IsOKButton;
} else {
button_find_info.match_proc = &Alert::IsCancelButton;
}

int max_wait = 10;
// Retry up to 10 times to find the dialog.
while ((button_find_info.button_handle == NULL) && --max_wait) {
::EnumChildWindows(this->alert_handle_,
&Alert::FindDialogButton,
reinterpret_cast<LPARAM>(&button_find_info));
if (button_find_info.button_handle == NULL) {
::Sleep(50);
// Return the simple version of the struct so that subclasses do not
// have to know anything about the function pointer definition.
DialogButtonInfo button_info;
if (this->is_standard_alert_ || !this->is_security_alert_) {
DialogButtonFindInfo button_find_info;
button_find_info.button_handle = NULL;
button_find_info.button_control_id = this->is_standard_alert_ ? IDOK : INVALID_CONTROL_ID;
if (button_type == OK) {
button_find_info.match_proc = &Alert::IsOKButton;
} else {
break;
button_find_info.match_proc = &Alert::IsCancelButton;
}

int max_wait = 10;
// Retry up to 10 times to find the dialog.
while ((button_find_info.button_handle == NULL) && --max_wait) {
::EnumChildWindows(this->alert_handle_,
&Alert::FindDialogButton,
reinterpret_cast<LPARAM>(&button_find_info));
if (button_find_info.button_handle == NULL) {
::Sleep(50);
} else {
break;
}
}
button_info.button_handle = button_find_info.button_handle;
button_info.button_control_id = button_find_info.button_control_id;
button_info.button_exists = button_find_info.button_handle != NULL;
button_info.accessibility_id = "";
button_info.use_accessibility = false;
} else {
button_info.button_handle = NULL;
button_info.button_control_id = 0;
button_info.button_exists = true;
button_info.accessibility_id = button_type == OK ? "OKButton" : "CancelButton";
button_info.use_accessibility = true;
}

// Use the simple version of the struct so that subclasses do not
// have to know anything about the function pointer definition.
DialogButtonInfo button_info;
button_info.button_handle = button_find_info.button_handle;
button_info.button_control_id = button_find_info.button_control_id;
button_info.button_exists = button_find_info.button_handle != NULL;
return button_info;
}

int Alert::ClickAlertButtonUsingAccessibility(const std::string& automation_id) {
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";
return EUNHANDLEDERROR;
}

CComPtr<IUIAutomationElement> parent_window;
hr = ui_automation->ElementFromHandle(this->alert_handle_,
&parent_window);
if (FAILED(hr)) {
LOGHR(WARN, hr) << "Unable to get automation object from window handle";
return EUNHANDLEDERROR;
}

CComVariant button_automation_id = automation_id.c_str();
CComPtr<IUIAutomationCondition> button_condition;
hr = ui_automation->CreatePropertyCondition(UIA_AutomationIdPropertyId,
button_automation_id,
&button_condition);
if (FAILED(hr)) {
LOGHR(WARN, hr) << "Unable to create button finding condition";
return EUNHANDLEDERROR;
}

CComPtr<IUIAutomationElement> button;
hr = parent_window->FindFirst(TreeScope::TreeScope_Children,
button_condition,
&button);
if (FAILED(hr)) {
LOGHR(WARN, hr) << "Unable to find button";
return EUNHANDLEDERROR;
}

CComPtr<IUIAutomationInvokePattern> button_invoke_pattern;
hr = button->GetCurrentPatternAs(UIA_InvokePatternId,
IID_PPV_ARGS(&button_invoke_pattern));
if (FAILED(hr)) {
LOGHR(WARN, hr) << "Unable to get invoke pattern on button";
return EUNHANDLEDERROR;
}

hr = button_invoke_pattern->Invoke();
if (FAILED(hr)) {
LOGHR(WARN, hr) << "Unable to invoke button";
return EUNHANDLEDERROR;
}

return WD_SUCCESS;
}

bool Alert::IsOKButton(HWND button_handle) {
int control_id = ::GetDlgCtrlID(button_handle);
if (control_id != 0) {
Expand Down
7 changes: 6 additions & 1 deletion cpp/iedriver/Alert.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ class Alert {
HWND button_handle;
int button_control_id;
bool button_exists;
std::string accessibility_id;
bool use_accessibility;
};

struct DialogButtonFindInfo {
Expand Down Expand Up @@ -78,10 +80,13 @@ class Alert {

DialogButtonInfo GetDialogButton(BUTTON_TYPE button_type);
int ClickAlertButton(DialogButtonInfo button_info);
int ClickAlertButtonUsingAccessibility(const std::string& automation_id);
std::string GetStandardDialogText(void);
std::string GetDirectUIDialogText(void);
HWND GetDirectUIChild(void);
IAccessible* GetChildWithRole(IAccessible* parent, long expected_role, int index);
IAccessible* GetChildWithRole(IAccessible* parent,
long expected_role,
int index);

static bool IsOKButton(HWND button_handle);
static bool IsCancelButton(HWND button_handle);
Expand Down
4 changes: 3 additions & 1 deletion cpp/iedriver/BrowserFactory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -987,6 +987,7 @@ BOOL CALLBACK BrowserFactory::FindDialogWindowForProcess(HWND hwnd, LPARAM arg)

// Could this be an dialog window?
// 7 == "#32770\0"
// 29 == "Credential Dialog Xaml Host\0"
// 34 == "Internet Explorer_TridentDlgFrame\0"
char name[34];
if (::GetClassNameA(hwnd, name, 34) == 0) {
Expand All @@ -995,7 +996,8 @@ BOOL CALLBACK BrowserFactory::FindDialogWindowForProcess(HWND hwnd, LPARAM arg)
}

if (strcmp(ALERT_WINDOW_CLASS, name) != 0 &&
strcmp(HTML_DIALOG_WINDOW_CLASS, name) != 0) {
strcmp(HTML_DIALOG_WINDOW_CLASS, name) != 0 &&
strcmp(SECURITY_DIALOG_WINDOW_CLASS, name) != 0) {
return TRUE;
} else {
// If the window style has the WS_DISABLED bit set or the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ void SendKeysToAlertCommandHandler::ExecuteInternal(
Alert dialog(browser_wrapper, alert_handle);
status_code = dialog.SendKeys(value);
if (status_code != WD_SUCCESS) {
if (status_code == EUNSUPPORTEDOPERATION) {
response->SetErrorResponse(status_code,
"Alert was not generated by the JavaScript alert(), confirm(), or prompt() functions");
return;
}
response->SetErrorResponse(ERROR_ELEMENT_NOT_INTERACTABLE,
"Modal dialog did not have a text box - maybe it was an alert");
return;
Expand Down
3 changes: 3 additions & 0 deletions cpp/iedriver/IECommandExecutor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1023,6 +1023,9 @@ bool IECommandExecutor::IsAlertActive(BrowserHandle browser, HWND* alert_handle)
if (strcmp(ALERT_WINDOW_CLASS, &window_class_name[0]) == 0) {
*alert_handle = dialog_handle;
return true;
} else if (strcmp(SECURITY_DIALOG_WINDOW_CLASS, &window_class_name[0]) == 0) {
*alert_handle = dialog_handle;
return true;
} else {
LOG(WARN) << "Found alert handle does not have a window class consistent with an alert";
}
Expand Down
8 changes: 4 additions & 4 deletions cpp/iedriver/IEDriver.rc
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ END
//

VS_VERSION_INFO VERSIONINFO
FILEVERSION 3,141,5,9
PRODUCTVERSION 3,141,5,9
FILEVERSION 3,141,5,10
PRODUCTVERSION 3,141,5,10
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
Expand All @@ -68,12 +68,12 @@ BEGIN
BEGIN
VALUE "CompanyName", "Software Freedom Conservancy"
VALUE "FileDescription", "Driver library for the IE driver"
VALUE "FileVersion", "3.141.5.9"
VALUE "FileVersion", "3.141.5.10"
VALUE "InternalName", "IEDriver.dll"
VALUE "LegalCopyright", "Copyright (C) 2019"
VALUE "OriginalFilename", "IEDriver.dll"
VALUE "ProductName", "Selenium WebDriver"
VALUE "ProductVersion", "3.141.5.9"
VALUE "ProductVersion", "3.141.5.10"
END
END
BLOCK "VarFileInfo"
Expand Down
1 change: 1 addition & 0 deletions cpp/iedriver/WebDriverConstants.h
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
// Window classes
#define ALERT_WINDOW_CLASS "#32770"
#define HTML_DIALOG_WINDOW_CLASS "Internet Explorer_TridentDlgFrame"
#define SECURITY_DIALOG_WINDOW_CLASS "Credential Dialog Xaml Host"

// Event names
#define WEBDRIVER_START_EVENT_NAME L"WD_START_EVENT"
Expand Down
13 changes: 13 additions & 0 deletions cpp/iedriverserver/CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@ available via the project downloads page. Changes in "revision" field indicate
private releases checked into the prebuilts directory of the source tree, but
not made generally available on the downloads page.

v3.141.5.10
===========
* (on behalf of Ben Kucera) Updated proxy settings to set proxy bypass
addresses. Fixes issue #6086.
* Updated to be able to close Windows 10 credentials dialog. When Microsoft
updated Windows 10's credentials prompt dialog, they made it a dialog
that is not a standard alert dialog and therefore not able to be
automated using standard Windows automation techniques. With a recent
preview release, it became possible to manipulate this dialog using
the UI Automation feature of Windows. The driver will now be able
to accept or dismiss this credentials dialog now. Please note that
there is still no supported solution for entering credentials, as
there is no support for that feature in the W3C WebDriver Specification.

v3.141.5.9
==========
Expand Down
8 changes: 4 additions & 4 deletions cpp/iedriverserver/IEDriverServer.rc
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ END
//

VS_VERSION_INFO VERSIONINFO
FILEVERSION 3,141,5,9
PRODUCTVERSION 3,141,5,9
FILEVERSION 3,141,5,10
PRODUCTVERSION 3,141,5,10
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
Expand All @@ -68,12 +68,12 @@ BEGIN
BEGIN
VALUE "CompanyName", "Software Freedom Conservancy"
VALUE "FileDescription", "Command line server for the IE driver"
VALUE "FileVersion", "3.141.5.9"
VALUE "FileVersion", "3.141.5.10"
VALUE "InternalName", "IEDriverServer.exe"
VALUE "LegalCopyright", "Copyright (C) 2019"
VALUE "OriginalFilename", "IEDriverServer.exe"
VALUE "ProductName", "Selenium WebDriver"
VALUE "ProductVersion", "3.141.5.9"
VALUE "ProductVersion", "3.141.5.10"
END
END
BLOCK "VarFileInfo"
Expand Down
Binary file modified cpp/prebuilt/Win32/Release/IEDriverServer.exe
Binary file not shown.
Binary file modified cpp/prebuilt/x64/Release/IEDriverServer.exe
Binary file not shown.
1 change: 1 addition & 0 deletions cpp/webdriver-server/errorcodes.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
#define ECLICKINTERCEPTED 33
#define EMOVETARGETOUTOFBOUNDS 34
#define ENOSUCHCOOKIE 35
#define EUNSUPPORTEDOPERATION 36
#define EINVALIDARGUMENT 62

#define ERROR_ELEMENT_CLICK_INTERCEPTED "element click intercepted"
Expand Down
Loading

0 comments on commit 83b2736

Please sign in to comment.