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