1 // Copyright 2018 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 <atk/atk.h>
8 #include <atk/atkutil.h>
9 #include <atspi/atspi.h>
10
11 #include "base/process/process_handle.h"
12 #include "base/stl_util.h"
13 #include "base/strings/pattern.h"
14 #include "base/strings/stringprintf.h"
15 #include "content/browser/accessibility/accessibility_tree_formatter_utils_auralinux.h"
16 #include "content/browser/accessibility/browser_accessibility_auralinux.h"
17 #include "content/browser/accessibility/browser_accessibility_manager.h"
18
19 #if defined(ATK_CHECK_VERSION) && ATK_CHECK_VERSION(2, 16, 0)
20 #define ATK_216
21 #endif
22
23 namespace content {
24
25 // This class has two distinct event recording code paths. When we are
26 // recording events in-process (typically this is used for
27 // DumpAccessibilityEvents tests), we use ATK's global event handlers. Since
28 // ATK doesn't support intercepting events from other processes, if we have a
29 // non-zero PID or an accessibility application name pattern, we use AT-SPI2
30 // directly to intercept events. Since AT-SPI2 should be capable of
31 // intercepting events in-process as well, eventually it would be nice to
32 // remove the ATK code path entirely.
33 class AccessibilityEventRecorderAuraLinux : public AccessibilityEventRecorder {
34 public:
35 explicit AccessibilityEventRecorderAuraLinux(
36 BrowserAccessibilityManager* manager,
37 base::ProcessId pid,
38 const AXTreeSelector& selector);
39 ~AccessibilityEventRecorderAuraLinux() override;
40
41 void ProcessATKEvent(const char* event,
42 unsigned int n_params,
43 const GValue* params);
44 void ProcessATSPIEvent(const AtspiEvent* event);
45
46 static gboolean OnATKEventReceived(GSignalInvocationHint* hint,
47 unsigned int n_params,
48 const GValue* params,
49 gpointer data);
50
51 private:
52 bool ShouldUseATSPI();
53
54 std::string AtkObjectToString(AtkObject* obj, bool include_name);
55 void AddATKEventListener(const char* event_name);
56 void AddATKEventListeners();
57 void RemoveATKEventListeners();
58 bool IncludeState(AtkStateType state_type);
59
60 void AddATSPIEventListeners();
61 void RemoveATSPIEventListeners();
62
63 AtspiEventListener* atspi_event_listener_ = nullptr;
64 base::ProcessId pid_;
65 base::StringPiece application_name_match_pattern_;
66 static AccessibilityEventRecorderAuraLinux* instance_;
67
68 DISALLOW_COPY_AND_ASSIGN(AccessibilityEventRecorderAuraLinux);
69 };
70
71 // static
72 AccessibilityEventRecorderAuraLinux*
73 AccessibilityEventRecorderAuraLinux::instance_ = nullptr;
74
75 // static
GetATKListenerIds()76 std::vector<unsigned int>& GetATKListenerIds() {
77 static base::NoDestructor<std::vector<unsigned int>> atk_listener_ids;
78 return *atk_listener_ids;
79 }
80
81 // static
OnATKEventReceived(GSignalInvocationHint * hint,unsigned int n_params,const GValue * params,gpointer data)82 gboolean AccessibilityEventRecorderAuraLinux::OnATKEventReceived(
83 GSignalInvocationHint* hint,
84 unsigned int n_params,
85 const GValue* params,
86 gpointer data) {
87 GSignalQuery query;
88 g_signal_query(hint->signal_id, &query);
89
90 if (instance_) {
91 // "add" and "remove" are details; not part of the signal name itself.
92 gchar* signal_name =
93 g_strcmp0(query.signal_name, "children-changed")
94 ? g_strdup(query.signal_name)
95 : g_strconcat(query.signal_name, ":",
96 g_quark_to_string(hint->detail), nullptr);
97 instance_->ProcessATKEvent(signal_name, n_params, params);
98 g_free(signal_name);
99 }
100 return true;
101 }
102
103 // static
Create(BrowserAccessibilityManager * manager,base::ProcessId pid,const AXTreeSelector & selector)104 std::unique_ptr<AccessibilityEventRecorder> AccessibilityEventRecorder::Create(
105 BrowserAccessibilityManager* manager,
106 base::ProcessId pid,
107 const AXTreeSelector& selector) {
108 return std::make_unique<AccessibilityEventRecorderAuraLinux>(manager, pid,
109 selector);
110 }
111
112 std::vector<AccessibilityEventRecorder::TestPass>
GetTestPasses()113 AccessibilityEventRecorder::GetTestPasses() {
114 // Both the Blink pass and native pass use the same recorder
115 return {
116 {"blink", &AccessibilityEventRecorder::Create},
117 {"linux", &AccessibilityEventRecorder::Create},
118 };
119 }
120
ShouldUseATSPI()121 bool AccessibilityEventRecorderAuraLinux::ShouldUseATSPI() {
122 return pid_ != base::GetCurrentProcId() ||
123 !application_name_match_pattern_.empty();
124 }
125
AccessibilityEventRecorderAuraLinux(BrowserAccessibilityManager * manager,base::ProcessId pid,const AXTreeSelector & selector)126 AccessibilityEventRecorderAuraLinux::AccessibilityEventRecorderAuraLinux(
127 BrowserAccessibilityManager* manager,
128 base::ProcessId pid,
129 const AXTreeSelector& selector)
130 : AccessibilityEventRecorder(manager),
131 pid_(pid),
132 application_name_match_pattern_(selector.pattern) {
133 CHECK(!instance_) << "There can be only one instance of"
134 << " AccessibilityEventRecorder at a time.";
135
136 if (ShouldUseATSPI()) {
137 AddATSPIEventListeners();
138 } else {
139 AddATKEventListeners();
140 }
141
142 instance_ = this;
143 }
144
~AccessibilityEventRecorderAuraLinux()145 AccessibilityEventRecorderAuraLinux::~AccessibilityEventRecorderAuraLinux() {
146 RemoveATSPIEventListeners();
147 instance_ = nullptr;
148 }
149
AddATKEventListener(const char * event_name)150 void AccessibilityEventRecorderAuraLinux::AddATKEventListener(
151 const char* event_name) {
152 unsigned id = atk_add_global_event_listener(OnATKEventReceived, event_name);
153 if (!id)
154 LOG(FATAL) << "atk_add_global_event_listener failed for " << event_name;
155
156 std::vector<unsigned int>& atk_listener_ids = GetATKListenerIds();
157 atk_listener_ids.push_back(id);
158 }
159
AddATKEventListeners()160 void AccessibilityEventRecorderAuraLinux::AddATKEventListeners() {
161 if (GetATKListenerIds().size() >= 1)
162 return;
163 GObject* gobject = G_OBJECT(g_object_new(G_TYPE_OBJECT, nullptr, nullptr));
164 g_object_unref(atk_no_op_object_new(gobject));
165 g_object_unref(gobject);
166
167 AddATKEventListener("ATK:AtkObject:state-change");
168 AddATKEventListener("ATK:AtkObject:focus-event");
169 AddATKEventListener("ATK:AtkObject:property-change");
170 AddATKEventListener("ATK:AtkObject:children-changed");
171 AddATKEventListener("ATK:AtkText:text-insert");
172 AddATKEventListener("ATK:AtkText:text-remove");
173 AddATKEventListener("ATK:AtkText:text-selection-changed");
174 AddATKEventListener("ATK:AtkText:text-caret-moved");
175 AddATKEventListener("ATK:AtkText:text-attributes-changed");
176 AddATKEventListener("ATK:AtkSelection:selection-changed");
177 AddATKEventListener("ATK:AtkTable:column-reordered");
178 AddATKEventListener("ATK:AtkTable:row-reordered");
179 }
180
RemoveATKEventListeners()181 void AccessibilityEventRecorderAuraLinux::RemoveATKEventListeners() {
182 std::vector<unsigned int>& atk_listener_ids = GetATKListenerIds();
183 for (const auto& id : atk_listener_ids)
184 atk_remove_global_event_listener(id);
185
186 atk_listener_ids.clear();
187 }
188
189 // Pruning states which are not supported on older bots makes it possible to
190 // run the events tests in more environments.
IncludeState(AtkStateType state_type)191 bool AccessibilityEventRecorderAuraLinux::IncludeState(
192 AtkStateType state_type) {
193 switch (state_type) {
194 #if defined(ATK_216)
195 case ATK_STATE_CHECKABLE:
196 case ATK_STATE_HAS_POPUP:
197 case ATK_STATE_READ_ONLY:
198 return false;
199 #endif
200 case ATK_STATE_LAST_DEFINED:
201 return false;
202 default:
203 return true;
204 }
205 }
206
AtkObjectToString(AtkObject * obj,bool include_name)207 std::string AccessibilityEventRecorderAuraLinux::AtkObjectToString(
208 AtkObject* obj,
209 bool include_name) {
210 std::string role = AtkRoleToString(atk_object_get_role(obj));
211 base::ReplaceChars(role, " ", "_", &role);
212 std::string str =
213 base::StringPrintf("role=ROLE_%s", base::ToUpperASCII(role).c_str());
214 // Getting the name breaks firing of name-change events. Allow disabling of
215 // logging the name in those situations.
216 if (include_name)
217 str += base::StringPrintf(" name='%s'", atk_object_get_name(obj));
218 return str;
219 }
220
ProcessATKEvent(const char * event,unsigned int n_params,const GValue * params)221 void AccessibilityEventRecorderAuraLinux::ProcessATKEvent(
222 const char* event,
223 unsigned int n_params,
224 const GValue* params) {
225 // If we don't have a root object, it means the tree is being destroyed.
226 if (!manager_->GetRoot()) {
227 RemoveATKEventListeners();
228 return;
229 }
230
231 bool log_name = true;
232 std::string event_name(event);
233 std::string log;
234 if (event_name.find("property-change") != std::string::npos) {
235 DCHECK_GE(n_params, 2u);
236 AtkPropertyValues* property_values =
237 static_cast<AtkPropertyValues*>(g_value_get_pointer(¶ms[1]));
238
239 if (g_strcmp0(property_values->property_name, "accessible-value") == 0) {
240 log += "VALUE-CHANGED:";
241 log +=
242 base::NumberToString(g_value_get_double(&property_values->new_value));
243 } else if (g_strcmp0(property_values->property_name, "accessible-name") ==
244 0) {
245 const char* new_name = g_value_get_string(&property_values->new_value);
246 log += "NAME-CHANGED:";
247 log += (new_name) ? new_name : "(null)";
248 } else if (g_strcmp0(property_values->property_name,
249 "accessible-description") == 0) {
250 const char* new_description =
251 g_value_get_string(&property_values->new_value);
252 log += "DESCRIPTION-CHANGED:";
253 log += (new_description) ? new_description : "(null)";
254 } else if (g_strcmp0(property_values->property_name, "accessible-parent") ==
255 0) {
256 log += "PARENT-CHANGED";
257 if (AtkObject* new_parent = static_cast<AtkObject*>(
258 g_value_get_object(&property_values->new_value)))
259 log += " PARENT:(" + AtkObjectToString(new_parent, log_name) + ")";
260 } else {
261 return;
262 }
263 } else if (event_name.find("children-changed") != std::string::npos) {
264 log_name = false;
265 log += base::ToUpperASCII(event);
266 // Despite this actually being a signed integer, it's defined as a uint.
267 int index = static_cast<int>(g_value_get_uint(¶ms[1]));
268 log += base::StringPrintf(" index:%d", index);
269 AtkObject* child = static_cast<AtkObject*>(g_value_get_pointer(¶ms[2]));
270 if (child)
271 log += " CHILD:(" + AtkObjectToString(child, log_name) + ")";
272 else
273 log += " CHILD:(NULL)";
274 } else if (event_name.find("focus-event") != std::string::npos) {
275 log += base::ToUpperASCII(event);
276 gchar* parameter = g_strdup_value_contents(¶ms[1]);
277 log += base::StringPrintf(":%s", parameter);
278 g_free(parameter);
279 } else {
280 log += base::ToUpperASCII(event);
281 if (event_name.find("state-change") != std::string::npos) {
282 std::string state_type = g_value_get_string(¶ms[1]);
283 log += ":" + base::ToUpperASCII(state_type);
284
285 gchar* parameter = g_strdup_value_contents(¶ms[2]);
286 log += base::StringPrintf(":%s", parameter);
287 g_free(parameter);
288
289 } else if (event_name.find("text-insert") != std::string::npos ||
290 event_name.find("text-remove") != std::string::npos) {
291 DCHECK_GE(n_params, 4u);
292 log += base::StringPrintf(
293 " (start=%i length=%i '%s')", g_value_get_int(¶ms[1]),
294 g_value_get_int(¶ms[2]), g_value_get_string(¶ms[3]));
295 }
296 }
297
298 AtkObject* obj = ATK_OBJECT(g_value_get_object(¶ms[0]));
299 log += " " + AtkObjectToString(obj, log_name);
300
301 std::string states;
302 AtkStateSet* state_set = atk_object_ref_state_set(obj);
303 for (int i = ATK_STATE_INVALID; i < ATK_STATE_LAST_DEFINED; i++) {
304 AtkStateType state_type = static_cast<AtkStateType>(i);
305 if (atk_state_set_contains_state(state_set, state_type) &&
306 IncludeState(state_type)) {
307 states += " " + base::ToUpperASCII(atk_state_type_get_name(state_type));
308 }
309 }
310 states = base::CollapseWhitespaceASCII(states, false);
311 base::ReplaceChars(states, " ", ",", &states);
312 log += base::StringPrintf(" %s", states.c_str());
313 g_object_unref(state_set);
314
315 OnEvent(log);
316 }
317
318 // This list is composed of the sorted event names taken from the list provided
319 // in the libatspi documentation at:
320 // https://developer.gnome.org/libatspi/stable/AtspiEventListener.html#atspi-event-listener-register
321 const char* const kEventNames[] = {
322 "object:active-descendant-changed",
323 "object:children-changed",
324 "object:column-deleted",
325 "object:column-inserted",
326 "object:column-reordered",
327 "object:model-changed",
328 "object:property-change",
329 "object:property-change:accessible-description",
330 "object:property-change:accessible-name",
331 "object:property-change:accessible-parent",
332 "object:property-change:accessible-role",
333 "object:property-change:accessible-table-caption",
334 "object:property-change:accessible-table-column-description",
335 "object:property-change:accessible-table-column-header",
336 "object:property-change:accessible-table-row-description",
337 "object:property-change:accessible-table-row-header",
338 "object:property-change:accessible-table-summary",
339 "object:property-change:accessible-value",
340 "object:row-deleted",
341 "object:row-inserted",
342 "object:row-reordered",
343 "object:selection-changed",
344 "object:state-changed",
345 "object:text-attributes-changed",
346 "object:text-caret-moved",
347 "object:text-changed",
348 "object:text-selection-changed",
349 "object:visible-data-changed",
350 "window:activate",
351 "window:close",
352 "window:create",
353 "window:deactivate",
354 "window:desktop-create",
355 "window:desktop-destroy",
356 "window:lower",
357 "window:maximize",
358 "window:minimize",
359 "window:move",
360 "window:raise",
361 "window:reparent",
362 "window:resize",
363 "window:restore",
364 "window:restyle",
365 "window:shade",
366 "window:unshade",
367 };
368
OnATSPIEventReceived(AtspiEvent * event,void * data)369 static void OnATSPIEventReceived(AtspiEvent* event, void* data) {
370 static_cast<AccessibilityEventRecorderAuraLinux*>(data)->ProcessATSPIEvent(
371 event);
372 g_boxed_free(ATSPI_TYPE_EVENT, static_cast<void*>(event));
373 }
374
AddATSPIEventListeners()375 void AccessibilityEventRecorderAuraLinux::AddATSPIEventListeners() {
376 atspi_init();
377 atspi_event_listener_ =
378 atspi_event_listener_new(OnATSPIEventReceived, this, nullptr);
379
380 GError* error = nullptr;
381 for (size_t i = 0; i < base::size(kEventNames); i++) {
382 atspi_event_listener_register(atspi_event_listener_, kEventNames[i],
383 &error);
384 if (error) {
385 LOG(ERROR) << "Could not register event listener for " << kEventNames[i];
386 g_clear_error(&error);
387 }
388 }
389 }
390
RemoveATSPIEventListeners()391 void AccessibilityEventRecorderAuraLinux::RemoveATSPIEventListeners() {
392 if (!atspi_event_listener_)
393 return;
394
395 GError* error = nullptr;
396 for (size_t i = 0; i < base::size(kEventNames); i++) {
397 atspi_event_listener_deregister(atspi_event_listener_, kEventNames[i],
398 nullptr);
399 if (error) {
400 LOG(ERROR) << "Could not deregister event listener for "
401 << kEventNames[i];
402 g_clear_error(&error);
403 }
404 }
405
406 g_object_unref(atspi_event_listener_);
407 atspi_event_listener_ = nullptr;
408 }
409
ProcessATSPIEvent(const AtspiEvent * event)410 void AccessibilityEventRecorderAuraLinux::ProcessATSPIEvent(
411 const AtspiEvent* event) {
412 GError* error = nullptr;
413
414 if (!application_name_match_pattern_.empty()) {
415 AtspiAccessible* application =
416 atspi_accessible_get_application(event->source, &error);
417 if (error || !application)
418 return;
419
420 char* application_name = atspi_accessible_get_name(application, &error);
421 g_object_unref(application);
422 if (error || !application_name) {
423 g_clear_error(&error);
424 return;
425 }
426
427 if (!base::MatchPattern(application_name,
428 application_name_match_pattern_)) {
429 return;
430 }
431 free(application_name);
432 }
433
434 if (pid_) {
435 int pid = atspi_accessible_get_process_id(event->source, &error);
436 if (!error && pid != pid_)
437 return;
438 g_clear_error(&error);
439 }
440
441 std::stringstream output;
442 output << event->type << " ";
443
444 GHashTable* attributes =
445 atspi_accessible_get_attributes(event->source, &error);
446 std::string html_tag, html_class, html_id;
447 if (!error && attributes) {
448 if (char* tag = static_cast<char*>(g_hash_table_lookup(attributes, "tag")))
449 html_tag = tag;
450 if (char* id = static_cast<char*>(g_hash_table_lookup(attributes, "id")))
451 html_id = id;
452 if (char* class_chars =
453 static_cast<char*>(g_hash_table_lookup(attributes, "class")))
454 html_class = std::string(".") + class_chars;
455 g_hash_table_unref(attributes);
456 }
457 g_clear_error(&error);
458
459 if (!html_tag.empty())
460 output << "<" << html_tag << html_id << html_class << ">";
461
462 AtspiRole role = atspi_accessible_get_role(event->source, &error);
463 output << "role=";
464 if (!error)
465 output << ATSPIRoleToString(role);
466 else
467 output << "#error";
468 g_clear_error(&error);
469
470 char* name = atspi_accessible_get_name(event->source, &error);
471 output << " name=";
472 if (!error && name)
473 output << name;
474 else
475 output << "#error";
476 g_clear_error(&error);
477 free(name);
478
479 AtspiStateSet* atspi_states = atspi_accessible_get_state_set(event->source);
480 GArray* state_array = atspi_state_set_get_states(atspi_states);
481 std::vector<std::string> states;
482 for (unsigned i = 0; i < state_array->len; i++) {
483 AtspiStateType state_type = g_array_index(state_array, AtspiStateType, i);
484 states.push_back(ATSPIStateToString(state_type));
485 }
486 g_array_free(state_array, TRUE);
487 g_object_unref(atspi_states);
488 output << " ";
489 std::copy(states.begin(), states.end(),
490 std::ostream_iterator<std::string>(output, ", "));
491
492 OnEvent(output.str());
493 }
494
495 } // namespace content
496