1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "content/browser/accessibility/accessibility_event_recorder.h"
6
7 #include <oleacc.h>
8 #include <stdint.h>
9 #include <wrl/client.h>
10
11 #include <string>
12
13 #include "base/strings/string_number_conversions.h"
14 #include "base/strings/string_piece.h"
15 #include "base/strings/string_util.h"
16 #include "base/strings/stringprintf.h"
17 #include "base/strings/utf_string_conversions.h"
18 #include "base/win/scoped_bstr.h"
19 #include "base/win/scoped_variant.h"
20 #include "content/browser/accessibility/accessibility_event_recorder_uia_win.h"
21 #include "content/browser/accessibility/accessibility_tree_formatter_utils_win.h"
22 #include "content/browser/accessibility/browser_accessibility_manager.h"
23 #include "content/browser/accessibility/browser_accessibility_win.h"
24 #include "third_party/iaccessible2/ia2_api_all.h"
25 #include "ui/base/win/atl_module.h"
26 #include "ui/gfx/win/hwnd_util.h"
27
28 namespace content {
29
30 namespace {
31
RoleVariantToString(const base::win::ScopedVariant & role)32 std::string RoleVariantToString(const base::win::ScopedVariant& role) {
33 if (role.type() == VT_I4) {
34 return base::UTF16ToUTF8(IAccessibleRoleToString(V_I4(role.ptr())));
35 } else if (role.type() == VT_BSTR) {
36 return base::UTF16ToUTF8(
37 base::string16(V_BSTR(role.ptr()), SysStringLen(V_BSTR(role.ptr()))));
38 }
39 return std::string();
40 }
41
QueryIAccessible2(IAccessible * accessible,IAccessible2 ** accessible2)42 HRESULT QueryIAccessible2(IAccessible* accessible, IAccessible2** accessible2) {
43 Microsoft::WRL::ComPtr<IServiceProvider> service_provider;
44 HRESULT hr = accessible->QueryInterface(IID_PPV_ARGS(&service_provider));
45 return SUCCEEDED(hr)
46 ? service_provider->QueryService(IID_IAccessible2, accessible2)
47 : hr;
48 }
49
QueryIAccessibleText(IAccessible * accessible,IAccessibleText ** accessible_text)50 HRESULT QueryIAccessibleText(IAccessible* accessible,
51 IAccessibleText** accessible_text) {
52 Microsoft::WRL::ComPtr<IServiceProvider> service_provider;
53 HRESULT hr = accessible->QueryInterface(IID_PPV_ARGS(&service_provider));
54 return SUCCEEDED(hr) ? service_provider->QueryService(IID_IAccessibleText,
55 accessible_text)
56 : hr;
57 }
58
BstrToPrettyUTF8(BSTR bstr)59 std::string BstrToPrettyUTF8(BSTR bstr) {
60 base::string16 str16(bstr, SysStringLen(bstr));
61
62 // IAccessibleText returns the text you get by appending all static text
63 // children, with an "embedded object character" for each non-text child.
64 // Pretty-print the embedded object character as <obj> so that test output
65 // is human-readable.
66 base::StringPiece16 embedded_character(
67 &BrowserAccessibilityComWin::kEmbeddedCharacter, 1);
68 base::ReplaceChars(str16, embedded_character, L"<obj>", &str16);
69
70 return base::UTF16ToUTF8(str16);
71 }
72
AccessibilityEventToStringUTF8(int32_t event_id)73 std::string AccessibilityEventToStringUTF8(int32_t event_id) {
74 return base::UTF16ToUTF8(AccessibilityEventToString(event_id));
75 }
76
77 } // namespace
78
79 class AccessibilityEventRecorderWin : public AccessibilityEventRecorder {
80 public:
81 AccessibilityEventRecorderWin(
82 BrowserAccessibilityManager* manager,
83 base::ProcessId pid,
84 const base::StringPiece& application_name_match_pattern);
85 ~AccessibilityEventRecorderWin() override;
86
87 // Callback registered by SetWinEventHook. Just calls OnWinEventHook.
88 static CALLBACK void WinEventHookThunk(HWINEVENTHOOK handle,
89 DWORD event,
90 HWND hwnd,
91 LONG obj_id,
92 LONG child_id,
93 DWORD event_thread,
94 DWORD event_time);
95
96 private:
97 // Called by the thunk registered by SetWinEventHook. Retrieves accessibility
98 // info about the node the event was fired on and appends a string to
99 // the event log.
100 void OnWinEventHook(HWINEVENTHOOK handle,
101 DWORD event,
102 HWND hwnd,
103 LONG obj_id,
104 LONG child_id,
105 DWORD event_thread,
106 DWORD event_time);
107
108 // Wrapper around AccessibleObjectFromWindow because the function call
109 // inexplicably flakes sometimes on build/trybots.
110 HRESULT AccessibleObjectFromWindowWrapper(HWND hwnd,
111 DWORD dwId,
112 REFIID riid,
113 void** ppvObject);
114
115 HWINEVENTHOOK win_event_hook_handle_;
116 static AccessibilityEventRecorderWin* instance_;
117
118 DISALLOW_COPY_AND_ASSIGN(AccessibilityEventRecorderWin);
119 };
120
121 // static
122 AccessibilityEventRecorderWin* AccessibilityEventRecorderWin::instance_ =
123 nullptr;
124
125 // static
Create(BrowserAccessibilityManager * manager,base::ProcessId pid,const AXTreeSelector & selector)126 std::unique_ptr<AccessibilityEventRecorder> AccessibilityEventRecorder::Create(
127 BrowserAccessibilityManager* manager,
128 base::ProcessId pid,
129 const AXTreeSelector& selector) {
130 if (!selector.pattern.empty()) {
131 LOG(FATAL) << "Recording accessibility events from an application name "
132 "match pattern not supported on this platform yet.";
133 }
134
135 return std::make_unique<AccessibilityEventRecorderWin>(manager, pid,
136 selector.pattern);
137 }
138
139 std::vector<AccessibilityEventRecorder::TestPass>
GetTestPasses()140 AccessibilityEventRecorder::GetTestPasses() {
141 // In addition to the 'Blink' pass, Windows includes two accessibility APIs
142 // that need to be tested independently (MSAA & UIA); the Blink pass uses the
143 // same recorder as the MSAA pass.
144 return {
145 {"blink", &AccessibilityEventRecorder::Create},
146 {"win", &AccessibilityEventRecorder::Create},
147 {"uia", &AccessibilityEventRecorderUia::CreateUia},
148 };
149 }
150
151 // static
WinEventHookThunk(HWINEVENTHOOK handle,DWORD event,HWND hwnd,LONG obj_id,LONG child_id,DWORD event_thread,DWORD event_time)152 CALLBACK void AccessibilityEventRecorderWin::WinEventHookThunk(
153 HWINEVENTHOOK handle,
154 DWORD event,
155 HWND hwnd,
156 LONG obj_id,
157 LONG child_id,
158 DWORD event_thread,
159 DWORD event_time) {
160 if (instance_) {
161 instance_->OnWinEventHook(handle, event, hwnd, obj_id, child_id,
162 event_thread, event_time);
163 }
164 }
165
AccessibilityEventRecorderWin(BrowserAccessibilityManager * manager,base::ProcessId pid,const base::StringPiece & application_name_match_pattern)166 AccessibilityEventRecorderWin::AccessibilityEventRecorderWin(
167 BrowserAccessibilityManager* manager,
168 base::ProcessId pid,
169 const base::StringPiece& application_name_match_pattern)
170 : AccessibilityEventRecorder(manager) {
171 CHECK(!instance_) << "There can be only one instance of"
172 << " AccessibilityEventRecorder at a time.";
173 // For now, just use out of context events when running as a utility to watch
174 // events (no BrowserAccessibilityManager), because otherwise Chrome events
175 // are not getting reported. Being in context is better so that for
176 // TEXT_REMOVED and TEXT_INSERTED events, we can query the text that was
177 // inserted or removed and include that in the log.
178 int context = manager ? WINEVENT_INCONTEXT : WINEVENT_OUTOFCONTEXT;
179 win_event_hook_handle_ =
180 SetWinEventHook(EVENT_MIN, EVENT_MAX, GetModuleHandle(NULL),
181 &AccessibilityEventRecorderWin::WinEventHookThunk, pid,
182 0, // Hook all threads
183 context);
184 CHECK(win_event_hook_handle_);
185 instance_ = this;
186 }
187
~AccessibilityEventRecorderWin()188 AccessibilityEventRecorderWin::~AccessibilityEventRecorderWin() {
189 UnhookWinEvent(win_event_hook_handle_);
190 instance_ = nullptr;
191 }
192
OnWinEventHook(HWINEVENTHOOK handle,DWORD event,HWND hwnd,LONG obj_id,LONG child_id,DWORD event_thread,DWORD event_time)193 void AccessibilityEventRecorderWin::OnWinEventHook(HWINEVENTHOOK handle,
194 DWORD event,
195 HWND hwnd,
196 LONG obj_id,
197 LONG child_id,
198 DWORD event_thread,
199 DWORD event_time) {
200 Microsoft::WRL::ComPtr<IAccessible> browser_accessible;
201 HRESULT hr = AccessibleObjectFromWindowWrapper(
202 hwnd, obj_id, IID_PPV_ARGS(&browser_accessible));
203 if (FAILED(hr)) {
204 // Note: our event hook will pick up some superfluous events we
205 // don't care about, so it's safe to just ignore these failures.
206 // Same below for other HRESULT checks.
207 VLOG(1) << "Ignoring result " << hr << " from AccessibleObjectFromWindow";
208 return;
209 }
210
211 base::win::ScopedVariant childid_variant(child_id);
212 Microsoft::WRL::ComPtr<IDispatch> dispatch;
213 hr = browser_accessible->get_accChild(childid_variant, &dispatch);
214 if (hr != S_OK || !dispatch) {
215 VLOG(1) << "Ignoring result " << hr << " and result " << dispatch.Get()
216 << " from get_accChild";
217 return;
218 }
219
220 Microsoft::WRL::ComPtr<IAccessible> iaccessible;
221 hr = dispatch.As(&iaccessible);
222 if (FAILED(hr)) {
223 VLOG(1) << "Ignoring result " << hr << " from QueryInterface";
224 return;
225 }
226
227 std::string event_str = AccessibilityEventToStringUTF8(event);
228 if (event_str.empty()) {
229 VLOG(1) << "Ignoring event " << event;
230 return;
231 }
232
233 base::win::ScopedVariant childid_self(CHILDID_SELF);
234 base::win::ScopedVariant role;
235 iaccessible->get_accRole(childid_self, role.Receive());
236 base::win::ScopedVariant state;
237 iaccessible->get_accState(childid_self, state.Receive());
238 int ia_state = V_I4(state.ptr());
239 std::string hwnd_class_name = base::UTF16ToUTF8(gfx::GetClassName(hwnd));
240
241 // Caret is special:
242 // Log all caret events that occur, with their window class, so that we can
243 // test to make sure they are only occurring on the desired window class.
244 if (ROLE_SYSTEM_CARET == V_I4(role.ptr())) {
245 base::string16 state_str = IAccessibleStateToString(ia_state);
246 std::string log = base::StringPrintf(
247 "%s role=ROLE_SYSTEM_CARET %ls window_class=%s", event_str.c_str(),
248 state_str.c_str(), hwnd_class_name.c_str());
249 OnEvent(log);
250 return;
251 }
252
253 if (only_web_events_) {
254 if (hwnd_class_name != "Chrome_RenderWidgetHostHWND")
255 return;
256
257 Microsoft::WRL::ComPtr<IServiceProvider> service_provider;
258 hr = iaccessible->QueryInterface(IID_PPV_ARGS(&service_provider));
259 if (FAILED(hr))
260 return;
261
262 Microsoft::WRL::ComPtr<IAccessible> content_document;
263 hr = service_provider->QueryService(GUID_IAccessibleContentDocument,
264 IID_PPV_ARGS(&content_document));
265 if (FAILED(hr))
266 return;
267 }
268
269 base::win::ScopedBstr name_bstr;
270 iaccessible->get_accName(childid_self, name_bstr.Receive());
271 base::win::ScopedBstr value_bstr;
272 iaccessible->get_accValue(childid_self, value_bstr.Receive());
273
274 // Avoid flakiness. Events fired on a WINDOW are out of the control
275 // of a test.
276 if (role.type() == VT_I4 && ROLE_SYSTEM_WINDOW == V_I4(role.ptr())) {
277 VLOG(1) << "Ignoring event " << event << " on ROLE_SYSTEM_WINDOW";
278 return;
279 }
280
281 // Avoid flakiness. The "offscreen" state depends on whether the browser
282 // window is frontmost or not, and "hottracked" depends on whether the
283 // mouse cursor happens to be over the element.
284 ia_state &= (~STATE_SYSTEM_OFFSCREEN & ~STATE_SYSTEM_HOTTRACKED);
285
286 // The "readonly" state is set on almost every node and doesn't typically
287 // change, so filter it out to keep the output less verbose.
288 ia_state &= ~STATE_SYSTEM_READONLY;
289
290 AccessibleStates ia2_state = 0;
291 Microsoft::WRL::ComPtr<IAccessible2> iaccessible2;
292 hr = QueryIAccessible2(iaccessible.Get(), &iaccessible2);
293 bool has_ia2 = SUCCEEDED(hr) && iaccessible2;
294
295 base::string16 html_tag;
296 base::string16 obj_class;
297 base::string16 html_id;
298
299 if (has_ia2) {
300 iaccessible2->get_states(&ia2_state);
301 base::win::ScopedBstr attributes_bstr;
302 if (S_OK == iaccessible2->get_attributes(attributes_bstr.Receive())) {
303 std::vector<base::string16> ia2_attributes = base::SplitString(
304 base::string16(attributes_bstr.Get(), attributes_bstr.Length()),
305 base::string16(1, ';'), base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
306 for (base::string16& attr : ia2_attributes) {
307 if (base::StartsWith(attr, L"class:"))
308 obj_class = attr.substr(6); // HTML or view class
309 if (base::StartsWith(attr, L"id:")) {
310 html_id = base::string16(L"#");
311 html_id += attr.substr(3);
312 }
313 if (base::StartsWith(attr, L"tag:")) {
314 html_tag = attr.substr(4);
315 }
316 }
317 }
318 }
319
320 std::string log = base::StringPrintf("%s on", event_str.c_str());
321 if (!html_tag.empty()) {
322 // HTML node with tag
323 log += base::StringPrintf(
324 " <%s%s%s%s>", base::UTF16ToUTF8(html_tag).c_str(),
325 base::UTF16ToUTF8(html_id).c_str(), obj_class.empty() ? "" : ".",
326 base::UTF16ToUTF8(obj_class).c_str());
327 } else if (!obj_class.empty()) {
328 // Non-HTML node with class
329 log +=
330 base::StringPrintf(" class=%s", base::UTF16ToUTF8(obj_class).c_str());
331 }
332
333 log += base::StringPrintf(" role=%s", RoleVariantToString(role).c_str());
334 if (name_bstr.Length() > 0)
335 log += base::StringPrintf(" name=\"%s\"",
336 BstrToPrettyUTF8(name_bstr.Get()).c_str());
337 if (value_bstr.Length() > 0) {
338 bool is_document =
339 role.type() == VT_I4 && ROLE_SYSTEM_DOCUMENT == V_I4(role.ptr());
340 // Don't show actual document value, which is a URL, in order to avoid
341 // machine-based differences in tests.
342 log += is_document
343 ? " value~=[doc-url]"
344 : base::StringPrintf(" value=\"%s\"",
345 BstrToPrettyUTF8(value_bstr.Get()).c_str());
346 }
347 log += " ";
348 log += base::UTF16ToUTF8(IAccessibleStateToString(ia_state));
349 log += " ";
350 log += base::UTF16ToUTF8(IAccessible2StateToString(ia2_state));
351
352 // Group position, e.g. L3, 5 of 7
353 LONG group_level, similar_items_in_group, position_in_group;
354 if (has_ia2 &&
355 iaccessible2->get_groupPosition(&group_level, &similar_items_in_group,
356 &position_in_group) == S_OK) {
357 if (group_level)
358 log += base::StringPrintf(" level=%ld", group_level);
359 if (position_in_group)
360 log += base::StringPrintf(" PosInSet=%ld", position_in_group);
361 if (similar_items_in_group)
362 log += base::StringPrintf(" SetSize=%ld", similar_items_in_group);
363 }
364
365 // For TEXT_REMOVED and TEXT_INSERTED events, query the text that was
366 // inserted or removed and include that in the log.
367 Microsoft::WRL::ComPtr<IAccessibleText> accessible_text;
368 hr = QueryIAccessibleText(iaccessible.Get(), &accessible_text);
369 if (SUCCEEDED(hr)) {
370 if (event == IA2_EVENT_TEXT_REMOVED) {
371 IA2TextSegment old_text;
372 if (SUCCEEDED(accessible_text->get_oldText(&old_text))) {
373 log += base::StringPrintf(" old_text={'%s' start=%ld end=%ld}",
374 BstrToPrettyUTF8(old_text.text).c_str(),
375 old_text.start, old_text.end);
376 }
377 }
378 if (event == IA2_EVENT_TEXT_INSERTED) {
379 IA2TextSegment new_text;
380 if (SUCCEEDED(accessible_text->get_newText(&new_text))) {
381 log += base::StringPrintf(" new_text={'%s' start=%ld end=%ld}",
382 BstrToPrettyUTF8(new_text.text).c_str(),
383 new_text.start, new_text.end);
384 }
385 }
386 }
387
388 log =
389 base::UTF16ToUTF8(base::CollapseWhitespace(base::UTF8ToUTF16(log), true));
390 OnEvent(log);
391 }
392
AccessibleObjectFromWindowWrapper(HWND hwnd,DWORD dw_id,REFIID riid,void ** ppv_object)393 HRESULT AccessibilityEventRecorderWin::AccessibleObjectFromWindowWrapper(
394 HWND hwnd,
395 DWORD dw_id,
396 REFIID riid,
397 void** ppv_object) {
398 HRESULT hr = ::AccessibleObjectFromWindow(hwnd, dw_id, riid, ppv_object);
399 if (SUCCEEDED(hr))
400 return hr;
401
402 if (!manager_) // No manager when outside of Chrome tests.
403 return E_FAIL;
404
405 // The above call to ::AccessibleObjectFromWindow fails for unknown
406 // reasons every once in a while on the bots. Work around it by grabbing
407 // the object directly from the BrowserAccessibilityManager.
408 HWND accessibility_hwnd =
409 manager_->delegate()->AccessibilityGetAcceleratedWidget();
410 if (accessibility_hwnd != hwnd)
411 return E_FAIL;
412
413 IAccessible* obj = ToBrowserAccessibilityComWin(manager_->GetRoot());
414 obj->AddRef();
415 *ppv_object = obj;
416 return S_OK;
417 }
418
419 } // namespace content
420