blob: 0f1c7120a3729239e5c960e2abdcbb7cff43a771 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/accessibility/accessibility_event_recorder.h"
#include <oleacc.h>
#include <stdint.h>
#include <wrl/client.h>
#include <string>
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_piece.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/win/scoped_bstr.h"
#include "base/win/scoped_variant.h"
#include "content/browser/accessibility/accessibility_tree_formatter_utils_win.h"
#include "content/browser/accessibility/browser_accessibility_manager.h"
#include "content/browser/accessibility/browser_accessibility_win.h"
#include "third_party/iaccessible2/ia2_api_all.h"
#include "ui/base/win/atl_module.h"
#include "ui/gfx/win/hwnd_util.h"
namespace content {
namespace {
std::string RoleVariantToString(const base::win::ScopedVariant& role) {
if (role.type() == VT_I4) {
return base::UTF16ToUTF8(IAccessibleRoleToString(V_I4(role.ptr())));
} else if (role.type() == VT_BSTR) {
return base::UTF16ToUTF8(
base::string16(V_BSTR(role.ptr()), SysStringLen(V_BSTR(role.ptr()))));
}
return std::string();
}
HRESULT QueryIAccessible2(IAccessible* accessible, IAccessible2** accessible2) {
Microsoft::WRL::ComPtr<IServiceProvider> service_provider;
HRESULT hr = accessible->QueryInterface(service_provider.GetAddressOf());
return SUCCEEDED(hr) ?
service_provider->QueryService(IID_IAccessible2, accessible2) : hr;
}
HRESULT QueryIAccessibleText(IAccessible* accessible,
IAccessibleText** accessible_text) {
Microsoft::WRL::ComPtr<IServiceProvider> service_provider;
HRESULT hr = accessible->QueryInterface(service_provider.GetAddressOf());
return SUCCEEDED(hr) ?
service_provider->QueryService(IID_IAccessibleText, accessible_text) : hr;
}
std::string BstrToUTF8(BSTR bstr) {
base::string16 str16(bstr, SysStringLen(bstr));
// IAccessibleText returns the text you get by appending all static text
// children, with an "embedded object character" for each non-text child.
// Pretty-print the embedded object character as <obj> so that test output
// is human-readable.
base::StringPiece16 embedded_character(
&BrowserAccessibilityComWin::kEmbeddedCharacter, 1);
base::ReplaceChars(str16, embedded_character, L"<obj>", &str16);
return base::UTF16ToUTF8(str16);
}
std::string AccessibilityEventToStringUTF8(int32_t event_id) {
return base::UTF16ToUTF8(AccessibilityEventToString(event_id));
}
} // namespace
class AccessibilityEventRecorderWin : public AccessibilityEventRecorder {
public:
AccessibilityEventRecorderWin(
BrowserAccessibilityManager* manager,
base::ProcessId pid,
const base::StringPiece& application_name_match_pattern);
~AccessibilityEventRecorderWin() override;
// Callback registered by SetWinEventHook. Just calls OnWinEventHook.
static CALLBACK void WinEventHookThunk(HWINEVENTHOOK handle,
DWORD event,
HWND hwnd,
LONG obj_id,
LONG child_id,
DWORD event_thread,
DWORD event_time);
private:
// Called by the thunk registered by SetWinEventHook. Retrieves accessibility
// info about the node the event was fired on and appends a string to
// the event log.
void OnWinEventHook(HWINEVENTHOOK handle,
DWORD event,
HWND hwnd,
LONG obj_id,
LONG child_id,
DWORD event_thread,
DWORD event_time);
// Wrapper around AccessibleObjectFromWindow because the function call
// inexplicably flakes sometimes on build/trybots.
HRESULT AccessibleObjectFromWindowWrapper(
HWND hwnd, DWORD dwId, REFIID riid, void **ppvObject);
HWINEVENTHOOK win_event_hook_handle_;
static AccessibilityEventRecorderWin* instance_;
DISALLOW_COPY_AND_ASSIGN(AccessibilityEventRecorderWin);
};
// static
AccessibilityEventRecorderWin* AccessibilityEventRecorderWin::instance_ =
nullptr;
// static
std::unique_ptr<AccessibilityEventRecorder> AccessibilityEventRecorder::Create(
BrowserAccessibilityManager* manager,
base::ProcessId pid,
const base::StringPiece& application_name_match_pattern) {
if (!application_name_match_pattern.empty()) {
LOG(FATAL) << "Recording accessibility events from an application name "
"match pattern not supported on this platform yet.";
}
return std::make_unique<AccessibilityEventRecorderWin>(
manager, pid, application_name_match_pattern);
}
// static
CALLBACK void AccessibilityEventRecorderWin::WinEventHookThunk(
HWINEVENTHOOK handle,
DWORD event,
HWND hwnd,
LONG obj_id,
LONG child_id,
DWORD event_thread,
DWORD event_time) {
if (instance_) {
instance_->OnWinEventHook(handle, event, hwnd, obj_id, child_id,
event_thread, event_time);
}
}
AccessibilityEventRecorderWin::AccessibilityEventRecorderWin(
BrowserAccessibilityManager* manager,
base::ProcessId pid,
const base::StringPiece& application_name_match_pattern)
: AccessibilityEventRecorder(manager) {
CHECK(!instance_) << "There can be only one instance of"
<< " AccessibilityEventRecorder at a time.";
// For now, just use out of context events when running as a utility to watch
// events (no BrowserAccessibilityManager), because otherwise Chrome events
// are not getting reported. Being in context is better so that for
// TEXT_REMOVED and TEXT_INSERTED events, we can query the text that was
// inserted or removed and include that in the log.
int context = manager ? WINEVENT_INCONTEXT : WINEVENT_OUTOFCONTEXT;
win_event_hook_handle_ =
SetWinEventHook(EVENT_MIN, EVENT_MAX, GetModuleHandle(NULL),
&AccessibilityEventRecorderWin::WinEventHookThunk, pid,
0, // Hook all threads
context);
CHECK(win_event_hook_handle_);
instance_ = this;
}
AccessibilityEventRecorderWin::~AccessibilityEventRecorderWin() {
UnhookWinEvent(win_event_hook_handle_);
instance_ = nullptr;
}
void AccessibilityEventRecorderWin::OnWinEventHook(
HWINEVENTHOOK handle,
DWORD event,
HWND hwnd,
LONG obj_id,
LONG child_id,
DWORD event_thread,
DWORD event_time) {
Microsoft::WRL::ComPtr<IAccessible> browser_accessible;
HRESULT hr = AccessibleObjectFromWindowWrapper(
hwnd, obj_id, IID_IAccessible,
reinterpret_cast<void**>(browser_accessible.GetAddressOf()));
if (FAILED(hr)) {
// Note: our event hook will pick up some superfluous events we
// don't care about, so it's safe to just ignore these failures.
// Same below for other HRESULT checks.
VLOG(1) << "Ignoring result " << hr << " from AccessibleObjectFromWindow";
return;
}
base::win::ScopedVariant childid_variant(child_id);
Microsoft::WRL::ComPtr<IDispatch> dispatch;
hr = browser_accessible->get_accChild(childid_variant,
dispatch.GetAddressOf());
if (hr != S_OK || !dispatch) {
VLOG(1) << "Ignoring result " << hr << " and result " << dispatch.Get()
<< " from get_accChild";
return;
}
Microsoft::WRL::ComPtr<IAccessible> iaccessible;
hr = dispatch.CopyTo(iaccessible.GetAddressOf());
if (FAILED(hr)) {
VLOG(1) << "Ignoring result " << hr << " from QueryInterface";
return;
}
std::string event_str = AccessibilityEventToStringUTF8(event);
if (event_str.empty()) {
VLOG(1) << "Ignoring event " << event;
return;
}
base::win::ScopedVariant childid_self(CHILDID_SELF);
base::win::ScopedVariant role;
iaccessible->get_accRole(childid_self, role.Receive());
base::win::ScopedVariant state;
iaccessible->get_accState(childid_self, state.Receive());
int ia_state = V_I4(state.ptr());
std::string hwnd_class_name = base::UTF16ToUTF8(gfx::GetClassName(hwnd));
// Caret is special:
// Log all caret events that occur, with their window class, so that we can
// test to make sure they are only occurring on the desired window class.
if (ROLE_SYSTEM_CARET == V_I4(role.ptr())) {
base::string16 state_str = IAccessibleStateToString(ia_state);
std::string log = base::StringPrintf(
"%s role=ROLE_SYSTEM_CARET %ls window_class=%s", event_str.c_str(),
state_str.c_str(), hwnd_class_name.c_str());
OnEvent(log);
return;
}
if (only_web_events_) {
if (hwnd_class_name != "Chrome_RenderWidgetHostHWND")
return;
Microsoft::WRL::ComPtr<IServiceProvider> service_provider;
hr = iaccessible->QueryInterface(service_provider.GetAddressOf());
if (FAILED(hr))
return;
Microsoft::WRL::ComPtr<IAccessible> content_document;
hr = service_provider->QueryService(GUID_IAccessibleContentDocument,
content_document.GetAddressOf());
if (FAILED(hr))
return;
}
base::win::ScopedBstr name_bstr;
iaccessible->get_accName(childid_self, name_bstr.Receive());
base::win::ScopedBstr value_bstr;
iaccessible->get_accValue(childid_self, value_bstr.Receive());
// Avoid flakiness. Events fired on a WINDOW are out of the control
// of a test.
if (role.type() == VT_I4 && ROLE_SYSTEM_WINDOW == V_I4(role.ptr())) {
VLOG(1) << "Ignoring event " << event << " on ROLE_SYSTEM_WINDOW";
return;
}
// Avoid flakiness. The "offscreen" state depends on whether the browser
// window is frontmost or not, and "hottracked" depends on whether the
// mouse cursor happens to be over the element.
ia_state &= (~STATE_SYSTEM_OFFSCREEN & ~STATE_SYSTEM_HOTTRACKED);
// The "readonly" state is set on almost every node and doesn't typically
// change, so filter it out to keep the output less verbose.
ia_state &= ~STATE_SYSTEM_READONLY;
AccessibleStates ia2_state = 0;
Microsoft::WRL::ComPtr<IAccessible2> iaccessible2;
hr = QueryIAccessible2(iaccessible.Get(), iaccessible2.GetAddressOf());
bool has_ia2 = SUCCEEDED(hr) && iaccessible2;
base::string16 html_tag;
base::string16 obj_class;
base::string16 html_id;
if (has_ia2) {
iaccessible2->get_states(&ia2_state);
base::win::ScopedBstr attributes_bstr;
if (S_OK == iaccessible2->get_attributes(attributes_bstr.Receive())) {
std::vector<base::string16> ia2_attributes = base::SplitString(
base::string16(attributes_bstr, attributes_bstr.Length()),
base::string16(1, ';'), base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
for (base::string16& attr : ia2_attributes) {
if (base::StringPiece16(attr).starts_with(L"class:"))
obj_class = attr.substr(6); // HTML or view class
if (base::StringPiece16(attr).starts_with(L"id:")) {
html_id = base::string16(L"#");
html_id += attr.substr(3);
}
if (base::StringPiece16(attr).starts_with(L"tag:")) {
html_tag = attr.substr(4);
}
}
}
}
std::string log = base::StringPrintf("%s on", event_str.c_str());
if (!html_tag.empty()) {
// HTML node with tag
log += base::StringPrintf(
" <%s%s%s%s>", base::UTF16ToUTF8(html_tag).c_str(),
base::UTF16ToUTF8(html_id).c_str(), obj_class.empty() ? "" : ".",
base::UTF16ToUTF8(obj_class).c_str());
} else if (!obj_class.empty()) {
// Non-HTML node with class
log +=
base::StringPrintf(" class=%s", base::UTF16ToUTF8(obj_class).c_str());
}
log += base::StringPrintf(" role=%s", RoleVariantToString(role).c_str());
if (name_bstr.Length() > 0)
log += base::StringPrintf(" name=\"%s\"", BstrToUTF8(name_bstr).c_str());
if (value_bstr.Length() > 0) {
bool is_document =
role.type() == VT_I4 && ROLE_SYSTEM_DOCUMENT == V_I4(role.ptr());
// Don't show actual document value, which is a URL, in order to avoid
// machine-based differences in tests.
log += is_document ? " value~=[doc-url]"
: base::StringPrintf(" value=\"%s\"",
BstrToUTF8(value_bstr).c_str());
}
log += " ";
log += base::UTF16ToUTF8(IAccessibleStateToString(ia_state));
log += " ";
log += base::UTF16ToUTF8(IAccessible2StateToString(ia2_state));
// Group position, e.g. L3, 5 of 7
LONG group_level, similar_items_in_group, position_in_group;
if (has_ia2 &&
iaccessible2->get_groupPosition(&group_level, &similar_items_in_group,
&position_in_group) == S_OK) {
if (group_level)
log += base::StringPrintf(" level=%ld", group_level);
if (similar_items_in_group) {
log += base::StringPrintf(" %ld of %ld", position_in_group,
similar_items_in_group);
}
}
// For TEXT_REMOVED and TEXT_INSERTED events, query the text that was
// inserted or removed and include that in the log.
Microsoft::WRL::ComPtr<IAccessibleText> accessible_text;
hr = QueryIAccessibleText(iaccessible.Get(), accessible_text.GetAddressOf());
if (SUCCEEDED(hr)) {
if (event == IA2_EVENT_TEXT_REMOVED) {
IA2TextSegment old_text;
if (SUCCEEDED(accessible_text->get_oldText(&old_text))) {
log += base::StringPrintf(" old_text={'%s' start=%ld end=%ld}",
BstrToUTF8(old_text.text).c_str(),
old_text.start, old_text.end);
}
}
if (event == IA2_EVENT_TEXT_INSERTED) {
IA2TextSegment new_text;
if (SUCCEEDED(accessible_text->get_newText(&new_text))) {
log += base::StringPrintf(" new_text={'%s' start=%ld end=%ld}",
BstrToUTF8(new_text.text).c_str(),
new_text.start, new_text.end);
}
}
}
log = base::UTF16ToUTF8(
base::CollapseWhitespace(base::UTF8ToUTF16(log), true));
OnEvent(log);
}
HRESULT AccessibilityEventRecorderWin::AccessibleObjectFromWindowWrapper(
HWND hwnd, DWORD dw_id, REFIID riid, void** ppv_object) {
HRESULT hr = ::AccessibleObjectFromWindow(hwnd, dw_id, riid, ppv_object);
if (SUCCEEDED(hr))
return hr;
if (!manager_) // No manager when outside of Chrome tests.
return E_FAIL;
// The above call to ::AccessibleObjectFromWindow fails for unknown
// reasons every once in a while on the bots. Work around it by grabbing
// the object directly from the BrowserAccessibilityManager.
HWND accessibility_hwnd =
manager_->delegate()->AccessibilityGetAcceleratedWidget();
if (accessibility_hwnd != hwnd)
return E_FAIL;
IAccessible* obj = ToBrowserAccessibilityComWin(manager_->GetRoot());
obj->AddRef();
*ppv_object = obj;
return S_OK;
}
} // namespace content