1 /* gtkatspicache.c: AT-SPI object cache
2  *
3  * Copyright 2020  holder
4  *
5  * SPDX-License-Identifier: LGPL-2.1-or-later
6  *
7  * This library is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU Lesser General Public
9  * License as published by the Free Software Foundation; either
10  * version 2.1 of the License, or (at your option) any later version.
11  *
12  * This library is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15  * Lesser General Public License for more details.
16  *
17  * You should have received a copy of the GNU Lesser General Public
18  * License along with this library; if not, see <http://www.gnu.org/licenses/>.
19  */
20 
21 #include "config.h"
22 
23 #include "gtkatspicacheprivate.h"
24 
25 #include "gtkatspicontextprivate.h"
26 #include "gtkatspirootprivate.h"
27 #include "gtkatspiutilsprivate.h"
28 #include "gtkdebug.h"
29 
30 #include "a11y/atspi/atspi-accessible.h"
31 #include "a11y/atspi/atspi-application.h"
32 #include "a11y/atspi/atspi-cache.h"
33 
34 /* Cached item:
35  *
36  *  (so): object ref
37  *  (so): application ref
38  *  (so): parent ref
39  *    - parent.role == application ? desktop ref : null ref
40  *  i: index in parent, or -1 for transient widgets/menu items
41  *  i: child count, or -1 for defunct/menus
42  *  as: interfaces
43  *  s: name
44  *  u: role
45  *  s: description
46  *  au: state set
47  */
48 #define ITEM_SIGNATURE          "(so)(so)(so)iiassusau"
49 #define GET_ITEMS_SIGNATURE     "a(" ITEM_SIGNATURE ")"
50 
51 struct _GtkAtSpiCache
52 {
53   GObject parent_instance;
54 
55   char *cache_path;
56   GDBusConnection *connection;
57 
58   /* HashTable<str, GtkAtSpiContext> */
59   GHashTable *contexts_by_path;
60 
61   /* HashTable<GtkAtSpiContext, str> */
62   GHashTable *contexts_to_path;
63 
64   /* Re-entrancy guard */
65   gboolean in_get_items;
66 
67   GtkAtSpiRoot *root;
68 };
69 
70 enum
71 {
72   PROP_CACHE_PATH = 1,
73   PROP_CONNECTION,
74 
75   N_PROPS
76 };
77 
78 static GParamSpec *obj_props[N_PROPS];
79 
G_DEFINE_TYPE(GtkAtSpiCache,gtk_at_spi_cache,G_TYPE_OBJECT)80 G_DEFINE_TYPE (GtkAtSpiCache, gtk_at_spi_cache, G_TYPE_OBJECT)
81 
82 static void
83 gtk_at_spi_cache_finalize (GObject *gobject)
84 {
85   GtkAtSpiCache *self = GTK_AT_SPI_CACHE (gobject);
86 
87   g_clear_pointer (&self->contexts_to_path, g_hash_table_unref);
88   g_clear_pointer (&self->contexts_by_path, g_hash_table_unref);
89   g_clear_object (&self->connection);
90   g_free (self->cache_path);
91 
92   G_OBJECT_CLASS (gtk_at_spi_cache_parent_class)->finalize (gobject);
93 }
94 
95 static void
gtk_at_spi_cache_set_property(GObject * gobject,guint prop_id,const GValue * value,GParamSpec * pspec)96 gtk_at_spi_cache_set_property (GObject      *gobject,
97                                guint         prop_id,
98                                const GValue *value,
99                                GParamSpec   *pspec)
100 {
101   GtkAtSpiCache *self = GTK_AT_SPI_CACHE (gobject);
102 
103   switch (prop_id)
104     {
105     case PROP_CACHE_PATH:
106       g_free (self->cache_path);
107       self->cache_path = g_value_dup_string (value);
108       break;
109 
110     case PROP_CONNECTION:
111       g_clear_object (&self->connection);
112       self->connection = g_value_dup_object (value);
113       break;
114 
115     default:
116       G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
117     }
118 }
119 
120 static void
collect_object(GtkAtSpiCache * self,GVariantBuilder * builder,GtkAtSpiContext * context)121 collect_object (GtkAtSpiCache   *self,
122                 GVariantBuilder *builder,
123                 GtkAtSpiContext *context)
124 {
125   g_variant_builder_add (builder, "@(so)", gtk_at_spi_context_to_ref (context));
126 
127   GtkAtSpiRoot *root = gtk_at_spi_context_get_root (context);
128   g_variant_builder_add (builder, "@(so)", gtk_at_spi_root_to_ref (root));
129 
130   g_variant_builder_add (builder, "@(so)", gtk_at_spi_context_get_parent_ref (context));
131 
132   g_variant_builder_add (builder, "i", gtk_at_spi_context_get_index_in_parent (context));
133   g_variant_builder_add (builder, "i", gtk_at_spi_context_get_child_count (context));
134 
135   g_variant_builder_add (builder, "@as", gtk_at_spi_context_get_interfaces (context));
136 
137   char *name = gtk_at_context_get_name (GTK_AT_CONTEXT (context));
138   g_variant_builder_add (builder, "s", name ? name : "");
139   g_free (name);
140 
141   guint atspi_role = gtk_atspi_role_for_context (GTK_AT_CONTEXT (context));
142   g_variant_builder_add (builder, "u", atspi_role);
143 
144   char *description = gtk_at_context_get_description (GTK_AT_CONTEXT (context));
145   g_variant_builder_add (builder, "s", description ? description : "");
146   g_free (description);
147 
148   g_variant_builder_add (builder, "@au", gtk_at_spi_context_get_states (context));
149 }
150 
151 static void
collect_root(GtkAtSpiCache * self,GVariantBuilder * builder)152 collect_root (GtkAtSpiCache   *self,
153               GVariantBuilder *builder)
154 {
155   g_variant_builder_add (builder, "@(so)", gtk_at_spi_root_to_ref (self->root));
156   g_variant_builder_add (builder, "@(so)", gtk_at_spi_root_to_ref (self->root));
157 
158   g_variant_builder_add (builder, "@(so)", gtk_at_spi_null_ref ());
159 
160   g_variant_builder_add (builder, "i", -1);
161   g_variant_builder_add (builder, "i", 0);
162 
163   GVariantBuilder interfaces = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE ("as"));
164 
165   g_variant_builder_add (&interfaces, "s", atspi_accessible_interface.name);
166   g_variant_builder_add (&interfaces, "s", atspi_application_interface.name);
167   g_variant_builder_add (builder, "@as", g_variant_builder_end (&interfaces));
168 
169   g_variant_builder_add (builder, "s", g_get_prgname () ? g_get_prgname () : "Unnamed");
170 
171   g_variant_builder_add (builder, "u", ATSPI_ROLE_APPLICATION);
172 
173   g_variant_builder_add (builder, "s", g_get_application_name () ? g_get_application_name () : "No description");
174 
175   GVariantBuilder states = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE ("au"));
176   g_variant_builder_add (&states, "u", 0);
177   g_variant_builder_add (&states, "u", 0);
178   g_variant_builder_add (builder, "@au", g_variant_builder_end (&states));
179 }
180 
181 static void
collect_cached_objects(GtkAtSpiCache * self,GVariantBuilder * builder)182 collect_cached_objects (GtkAtSpiCache   *self,
183                         GVariantBuilder *builder)
184 {
185   GHashTable *collection = g_hash_table_new (NULL, NULL);
186   GHashTableIter iter;
187   gpointer key_p, value_p;
188 
189   /* Serializing the contexts might re-enter, and end up modifying the hash
190    * table, so we take a snapshot here and return the items we have at the
191    * moment of the GetItems() call
192    */
193   g_hash_table_iter_init (&iter, self->contexts_by_path);
194   while (g_hash_table_iter_next (&iter, &key_p, &value_p))
195     g_hash_table_add (collection, value_p);
196 
197   g_variant_builder_open (builder, G_VARIANT_TYPE ("(" ITEM_SIGNATURE ")"));
198   collect_root (self, builder);
199   g_variant_builder_close (builder);
200 
201   g_hash_table_iter_init (&iter, collection);
202   while (g_hash_table_iter_next (&iter, &key_p, &value_p))
203     {
204       g_variant_builder_open (builder, G_VARIANT_TYPE ("(" ITEM_SIGNATURE ")"));
205 
206       GtkAtSpiContext *context = value_p;
207 
208       collect_object (self, builder, context);
209 
210       g_variant_builder_close (builder);
211     }
212 
213   g_hash_table_unref (collection);
214 }
215 
216 static void
emit_add_accessible(GtkAtSpiCache * self,GtkAtSpiContext * context)217 emit_add_accessible (GtkAtSpiCache   *self,
218                      GtkAtSpiContext *context)
219 {
220   GtkATContext *at_context = GTK_AT_CONTEXT (context);
221 
222   /* If the context is hidden, we don't need to update the cache */
223   if (gtk_at_context_has_accessible_state (at_context, GTK_ACCESSIBLE_STATE_HIDDEN))
224     {
225       GtkAccessibleValue *is_hidden =
226         gtk_at_context_get_accessible_state (at_context, GTK_ACCESSIBLE_STATE_HIDDEN);
227 
228       if (gtk_boolean_accessible_value_get (is_hidden))
229         return;
230     }
231 
232   GVariantBuilder builder = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE ("(" ITEM_SIGNATURE ")"));
233 
234   collect_object (self, &builder, context);
235 
236   g_dbus_connection_emit_signal (self->connection,
237                                  NULL,
238                                  self->cache_path,
239                                  "org.a11y.atspi.Cache",
240                                  "AddAccessible",
241                                  g_variant_new ("(@(" ITEM_SIGNATURE "))",
242                                    g_variant_builder_end (&builder)),
243                                  NULL);
244 }
245 
246 static void
emit_remove_accessible(GtkAtSpiCache * self,GtkAtSpiContext * context)247 emit_remove_accessible (GtkAtSpiCache   *self,
248                         GtkAtSpiContext *context)
249 {
250   GtkATContext *at_context = GTK_AT_CONTEXT (context);
251 
252   /* If the context is hidden, we don't need to update the cache */
253   if (gtk_at_context_has_accessible_state (at_context, GTK_ACCESSIBLE_STATE_HIDDEN))
254     {
255       GtkAccessibleValue *is_hidden =
256         gtk_at_context_get_accessible_state (at_context, GTK_ACCESSIBLE_STATE_HIDDEN);
257 
258       if (gtk_boolean_accessible_value_get (is_hidden))
259         return;
260     }
261 
262   GVariant *ref = gtk_at_spi_context_to_ref (context);
263 
264   g_dbus_connection_emit_signal (self->connection,
265                                  NULL,
266                                  self->cache_path,
267                                  "org.a11y.atspi.Cache",
268                                  "RemoveAccessible",
269                                  g_variant_new ("(@(so))", ref),
270                                  NULL);
271 }
272 
273 static void
handle_cache_method(GDBusConnection * connection,const gchar * sender,const gchar * object_path,const gchar * interface_name,const gchar * method_name,GVariant * parameters,GDBusMethodInvocation * invocation,gpointer user_data)274 handle_cache_method (GDBusConnection       *connection,
275                      const gchar           *sender,
276                      const gchar           *object_path,
277                      const gchar           *interface_name,
278                      const gchar           *method_name,
279                      GVariant              *parameters,
280                      GDBusMethodInvocation *invocation,
281                      gpointer               user_data)
282 {
283   GtkAtSpiCache *self = user_data;
284 
285   GTK_NOTE (A11Y,
286             g_message ("[Cache] Method '%s' on interface '%s' for object '%s' from '%s'\n",
287                        method_name, interface_name, object_path, sender));
288 
289 
290   if (g_strcmp0 (method_name, "GetItems") == 0)
291     {
292       GVariantBuilder builder = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE ("(" GET_ITEMS_SIGNATURE ")"));
293       GVariant *items;
294 
295       /* Prevent the emission os signals while collecting accessible
296        * objects as the result of walking the accessible tree
297        */
298       self->in_get_items = TRUE;
299 
300       g_variant_builder_open (&builder, G_VARIANT_TYPE (GET_ITEMS_SIGNATURE));
301       collect_cached_objects (self, &builder);
302       g_variant_builder_close (&builder);
303       items = g_variant_builder_end (&builder);
304 
305       self->in_get_items = FALSE;
306 
307       GTK_NOTE (A11Y,
308                 g_message ("Returning %lu items\n", g_variant_n_children (items)));
309 
310       g_dbus_method_invocation_return_value (invocation, items);
311     }
312 }
313 
314 static GVariant *
handle_cache_get_property(GDBusConnection * connection,const gchar * sender,const gchar * object_path,const gchar * interface_name,const gchar * property_name,GError ** error,gpointer user_data)315 handle_cache_get_property (GDBusConnection       *connection,
316                            const gchar           *sender,
317                            const gchar           *object_path,
318                            const gchar           *interface_name,
319                            const gchar           *property_name,
320                            GError               **error,
321                            gpointer               user_data)
322 {
323   GVariant *res = NULL;
324 
325   g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
326                "Unknown property '%s'", property_name);
327 
328   return res;
329 }
330 
331 
332 static const GDBusInterfaceVTable cache_vtable = {
333   handle_cache_method,
334   handle_cache_get_property,
335   NULL,
336 };
337 
338 static void
gtk_at_spi_cache_constructed(GObject * gobject)339 gtk_at_spi_cache_constructed (GObject *gobject)
340 {
341   GtkAtSpiCache *self = GTK_AT_SPI_CACHE (gobject);
342 
343   g_assert (self->connection);
344   g_assert (self->cache_path);
345 
346   g_dbus_connection_register_object (self->connection,
347                                      self->cache_path,
348                                      (GDBusInterfaceInfo *) &atspi_cache_interface,
349                                      &cache_vtable,
350                                      self,
351                                      NULL,
352                                      NULL);
353 
354   GTK_NOTE (A11Y, g_message ("Cache registered at %s", self->cache_path));
355 
356   G_OBJECT_CLASS (gtk_at_spi_cache_parent_class)->constructed (gobject);
357 }
358 
359 static void
gtk_at_spi_cache_class_init(GtkAtSpiCacheClass * klass)360 gtk_at_spi_cache_class_init (GtkAtSpiCacheClass *klass)
361 {
362   GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
363 
364   gobject_class->constructed = gtk_at_spi_cache_constructed;
365   gobject_class->set_property = gtk_at_spi_cache_set_property;
366   gobject_class->finalize = gtk_at_spi_cache_finalize;
367 
368   obj_props[PROP_CACHE_PATH] =
369     g_param_spec_string ("cache-path", NULL, NULL,
370                          NULL,
371                          G_PARAM_WRITABLE |
372                          G_PARAM_CONSTRUCT_ONLY |
373                          G_PARAM_STATIC_STRINGS);
374 
375   obj_props[PROP_CONNECTION] =
376     g_param_spec_object ("connection", NULL, NULL,
377                          G_TYPE_DBUS_CONNECTION,
378                          G_PARAM_WRITABLE |
379                          G_PARAM_CONSTRUCT_ONLY |
380                          G_PARAM_STATIC_STRINGS);
381 
382   g_object_class_install_properties (gobject_class, N_PROPS, obj_props);
383 }
384 
385 static void
gtk_at_spi_cache_init(GtkAtSpiCache * self)386 gtk_at_spi_cache_init (GtkAtSpiCache *self)
387 {
388   self->contexts_by_path = g_hash_table_new_full (g_str_hash, g_str_equal,
389                                                   g_free,
390                                                   NULL);
391   self->contexts_to_path = g_hash_table_new (NULL, NULL);
392 }
393 
394 GtkAtSpiCache *
gtk_at_spi_cache_new(GDBusConnection * connection,const char * cache_path,GtkAtSpiRoot * root)395 gtk_at_spi_cache_new (GDBusConnection *connection,
396                       const char      *cache_path,
397                       GtkAtSpiRoot    *root)
398 {
399   GtkAtSpiCache *cache;
400 
401   g_return_val_if_fail (G_IS_DBUS_CONNECTION (connection), NULL);
402   g_return_val_if_fail (cache_path != NULL, NULL);
403 
404   cache = g_object_new (GTK_TYPE_AT_SPI_CACHE,
405                         "connection", connection,
406                         "cache-path", cache_path,
407                         NULL);
408   cache->root = root;
409 
410   return cache;
411 }
412 
413 void
gtk_at_spi_cache_add_context(GtkAtSpiCache * self,GtkAtSpiContext * context)414 gtk_at_spi_cache_add_context (GtkAtSpiCache   *self,
415                               GtkAtSpiContext *context)
416 {
417   g_return_if_fail (GTK_IS_AT_SPI_CACHE (self));
418   g_return_if_fail (GTK_IS_AT_SPI_CONTEXT (context));
419 
420   const char *path = gtk_at_spi_context_get_context_path (context);
421   if (path == NULL)
422     return;
423 
424   if (g_hash_table_contains (self->contexts_by_path, path))
425     return;
426 
427   char *path_key = g_strdup (path);
428   g_hash_table_insert (self->contexts_by_path, path_key, context);
429   g_hash_table_insert (self->contexts_to_path, context, path_key);
430 
431   GTK_NOTE (A11Y, g_message ("Adding context '%s' to cache", path_key));
432 
433   /* GetItems is safe from re-entrancy, but we still don't want to
434    * emit an unnecessary signal while we're collecting ATContexts
435    */
436   if (!self->in_get_items)
437     emit_add_accessible (self, context);
438 }
439 
440 void
gtk_at_spi_cache_remove_context(GtkAtSpiCache * self,GtkAtSpiContext * context)441 gtk_at_spi_cache_remove_context (GtkAtSpiCache   *self,
442                                  GtkAtSpiContext *context)
443 {
444   g_return_if_fail (GTK_IS_AT_SPI_CACHE (self));
445   g_return_if_fail (GTK_IS_AT_SPI_CONTEXT (context));
446 
447   const char *path = gtk_at_spi_context_get_context_path (context);
448   if (!g_hash_table_contains (self->contexts_by_path, path))
449     return;
450 
451   emit_remove_accessible (self, context);
452 
453   /* The order is important: the value in contexts_by_path is the
454    * key in contexts_to_path
455    */
456   g_hash_table_remove (self->contexts_to_path, context);
457   g_hash_table_remove (self->contexts_by_path, path);
458 
459   GTK_NOTE (A11Y, g_message ("Removing context '%s' from cache", path));
460 }
461