Branch data Line data Source code
1 : : /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
2 : : *
3 : : * Copyright © 2020 Endless Mobile, Inc.
4 : : *
5 : : * SPDX-License-Identifier: GPL-2.0-or-later
6 : : *
7 : : * This program is free software; you can redistribute it and/or modify
8 : : * it under the terms of the GNU General Public License as published by
9 : : * the Free Software Foundation; either version 2 of the License, or
10 : : * (at your option) any later version.
11 : : *
12 : : * This program 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
15 : : * GNU General Public License for more details.
16 : : *
17 : : * You should have received a copy of the GNU General Public License
18 : : * along with this program; if not, see <http://www.gnu.org/licenses/>.
19 : : *
20 : : * Authors:
21 : : * - Philip Withnall <withnall@endlessm.com>
22 : : */
23 : :
24 : : #include "config.h"
25 : :
26 : : #include <flatpak.h>
27 : : #include <gio/gdesktopappinfo.h>
28 : : #include <gio/gio.h>
29 : : #include <glib.h>
30 : : #include <glib-object.h>
31 : : #include <glib/gi18n-lib.h>
32 : : #include <gtk/gtk.h>
33 : : #include <adwaita.h>
34 : : #include <libmalcontent/app-filter.h>
35 : :
36 : : #include "restrict-applications-selector.h"
37 : :
38 : :
39 : : #define WEB_BROWSERS_CONTENT_TYPE "x-scheme-handler/http"
40 : :
41 : : static void app_info_changed_cb (GAppInfoMonitor *monitor,
42 : : gpointer user_data);
43 : : static void reload_apps (MctRestrictApplicationsSelector *self);
44 : : static GtkWidget *create_row_for_app_cb (gpointer item,
45 : : gpointer user_data);
46 : : static char *app_info_dup_name (GAppInfo *app_info);
47 : :
48 : : /**
49 : : * MctRestrictApplicationsSelector:
50 : : *
51 : : * The ‘Restrict Applications’ selector is a list box which shows the available
52 : : * applications on the system alongside a column of toggle switches, which
53 : : * allows the given user to be prevented from running each application.
54 : : *
55 : : * The selector takes an
56 : : * [property@MalcontentUi.RestrictApplicationsSelector:app-filter] as input
57 : : * to set up the UI, and returns its output as set of modifications to a given
58 : : * [type@Malcontent.AppFilterBuilder] using
59 : : * [method@MalcontentUi.RestrictApplicationsSelector.build_app_filter].
60 : : *
61 : : * Search terms may be applied using
62 : : * [property@MalcontentUi.RestrictApplicationsSelector:search].
63 : : * These will filter the list of displayed apps so that only ones matching the
64 : : * search terms (by name, using UTF-8 normalisation and casefolding) will be
65 : : * displayed.
66 : : *
67 : : * Since: 0.5.0
68 : : */
69 : : struct _MctRestrictApplicationsSelector
70 : : {
71 : : GtkBox parent_instance;
72 : :
73 : : GtkListBox *listbox;
74 : :
75 : : GList *cached_apps; /* (nullable) (owned) (element-type GAppInfo) */
76 : : GListStore *apps;
77 : : GtkFilterListModel *filtered_apps;
78 : : GtkStringFilter *search_filter;
79 : : GAppInfoMonitor *app_info_monitor; /* (owned) */
80 : : gulong app_info_monitor_changed_id;
81 : : GHashTable *blocklisted_apps; /* (owned) (element-type GAppInfo) */
82 : :
83 : : MctAppFilter *app_filter; /* (owned) */
84 : :
85 : : FlatpakInstallation *system_installation; /* (owned) */
86 : : FlatpakInstallation *user_installation; /* (owned) */
87 : :
88 : : gchar *search; /* (nullable) (owned) */
89 : : };
90 : :
91 [ + + + - : 4 : G_DEFINE_TYPE (MctRestrictApplicationsSelector, mct_restrict_applications_selector, GTK_TYPE_BOX)
+ + ]
92 : :
93 : : typedef enum
94 : : {
95 : : PROP_APP_FILTER = 1,
96 : : PROP_SEARCH,
97 : : } MctRestrictApplicationsSelectorProperty;
98 : :
99 : : static GParamSpec *properties[PROP_SEARCH + 1];
100 : :
101 : : enum {
102 : : SIGNAL_CHANGED,
103 : : };
104 : :
105 : : static guint signals[SIGNAL_CHANGED + 1];
106 : :
107 : : static void
108 : 0 : mct_restrict_applications_selector_constructed (GObject *obj)
109 : : {
110 : 0 : MctRestrictApplicationsSelector *self = MCT_RESTRICT_APPLICATIONS_SELECTOR (obj);
111 : :
112 : : /* Default app filter, typically for when we’re instantiated by #GtkBuilder. */
113 [ # # ]: 0 : if (self->app_filter == NULL)
114 : : {
115 : 0 : g_auto(MctAppFilterBuilder) builder = MCT_APP_FILTER_BUILDER_INIT ();
116 : 0 : self->app_filter = mct_app_filter_builder_end (&builder);
117 : : }
118 : :
119 : 0 : g_assert (self->app_filter != NULL);
120 : :
121 : : /* Load the apps. */
122 : 0 : reload_apps (self);
123 : :
124 : 0 : G_OBJECT_CLASS (mct_restrict_applications_selector_parent_class)->constructed (obj);
125 : 0 : }
126 : :
127 : : static void
128 : 0 : mct_restrict_applications_selector_get_property (GObject *object,
129 : : guint prop_id,
130 : : GValue *value,
131 : : GParamSpec *pspec)
132 : : {
133 : 0 : MctRestrictApplicationsSelector *self = MCT_RESTRICT_APPLICATIONS_SELECTOR (object);
134 : :
135 [ # # # ]: 0 : switch ((MctRestrictApplicationsSelectorProperty) prop_id)
136 : : {
137 : 0 : case PROP_APP_FILTER:
138 : 0 : g_value_set_boxed (value, self->app_filter);
139 : 0 : break;
140 : 0 : case PROP_SEARCH:
141 : 0 : g_value_set_string (value, self->search);
142 : 0 : break;
143 : :
144 : 0 : default:
145 : 0 : G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
146 : : }
147 : 0 : }
148 : :
149 : : static void
150 : 0 : mct_restrict_applications_selector_set_property (GObject *object,
151 : : guint prop_id,
152 : : const GValue *value,
153 : : GParamSpec *pspec)
154 : : {
155 : 0 : MctRestrictApplicationsSelector *self = MCT_RESTRICT_APPLICATIONS_SELECTOR (object);
156 : :
157 [ # # # ]: 0 : switch ((MctRestrictApplicationsSelectorProperty) prop_id)
158 : : {
159 : 0 : case PROP_APP_FILTER:
160 : 0 : mct_restrict_applications_selector_set_app_filter (self, g_value_get_boxed (value));
161 : 0 : break;
162 : 0 : case PROP_SEARCH:
163 : 0 : mct_restrict_applications_selector_set_search (self, g_value_get_string (value));
164 : 0 : break;
165 : :
166 : 0 : default:
167 : 0 : G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
168 : : }
169 : 0 : }
170 : :
171 : : static void
172 : 0 : mct_restrict_applications_selector_dispose (GObject *object)
173 : : {
174 : 0 : MctRestrictApplicationsSelector *self = (MctRestrictApplicationsSelector *)object;
175 : :
176 [ # # ]: 0 : g_clear_pointer (&self->blocklisted_apps, g_hash_table_unref);
177 [ # # # # ]: 0 : g_clear_list (&self->cached_apps, g_object_unref);
178 : :
179 [ # # # # ]: 0 : if (self->app_info_monitor != NULL && self->app_info_monitor_changed_id != 0)
180 : : {
181 : 0 : g_signal_handler_disconnect (self->app_info_monitor, self->app_info_monitor_changed_id);
182 : 0 : self->app_info_monitor_changed_id = 0;
183 : : }
184 [ # # ]: 0 : g_clear_object (&self->app_info_monitor);
185 [ # # ]: 0 : g_clear_pointer (&self->app_filter, mct_app_filter_unref);
186 [ # # ]: 0 : g_clear_object (&self->system_installation);
187 [ # # ]: 0 : g_clear_object (&self->user_installation);
188 [ # # ]: 0 : g_clear_pointer (&self->search, g_free);
189 : :
190 : 0 : G_OBJECT_CLASS (mct_restrict_applications_selector_parent_class)->dispose (object);
191 : 0 : }
192 : :
193 : : static void
194 : 0 : mct_restrict_applications_selector_realize (GtkWidget *widget)
195 : : {
196 : 0 : g_autoptr(GtkCssProvider) provider = NULL;
197 : :
198 : 0 : GTK_WIDGET_CLASS (mct_restrict_applications_selector_parent_class)->realize (widget);
199 : :
200 : 0 : provider = gtk_css_provider_new ();
201 : 0 : gtk_css_provider_load_from_resource (provider,
202 : : "/org/freedesktop/MalcontentUi/ui/restricts-switch.css");
203 : 0 : gtk_style_context_add_provider_for_display (gtk_widget_get_display (widget),
204 : 0 : GTK_STYLE_PROVIDER (provider),
205 : : GTK_STYLE_PROVIDER_PRIORITY_APPLICATION - 1);
206 : 0 : }
207 : :
208 : : static void
209 : 1 : mct_restrict_applications_selector_class_init (MctRestrictApplicationsSelectorClass *klass)
210 : : {
211 : 1 : GObjectClass *object_class = G_OBJECT_CLASS (klass);
212 : 1 : GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
213 : :
214 : 1 : object_class->constructed = mct_restrict_applications_selector_constructed;
215 : 1 : object_class->get_property = mct_restrict_applications_selector_get_property;
216 : 1 : object_class->set_property = mct_restrict_applications_selector_set_property;
217 : 1 : object_class->dispose = mct_restrict_applications_selector_dispose;
218 : :
219 : 1 : widget_class->realize = mct_restrict_applications_selector_realize;
220 : :
221 : : /**
222 : : * MctRestrictApplicationsSelector:app-filter: (not nullable)
223 : : *
224 : : * The user’s current app filter, used to set up the selector.
225 : : *
226 : : * As app filters are immutable, it is not updated as the selector is changed.
227 : : * Use [method@MalcontentUi.RestrictApplicationsSelector.build_app_filter] to
228 : : * build the new app filter.
229 : : *
230 : : * Since: 0.5.0
231 : : */
232 : 1 : properties[PROP_APP_FILTER] =
233 : 1 : g_param_spec_boxed ("app-filter", NULL, NULL,
234 : : MCT_TYPE_APP_FILTER,
235 : : G_PARAM_READWRITE |
236 : : G_PARAM_STATIC_STRINGS |
237 : : G_PARAM_EXPLICIT_NOTIFY);
238 : :
239 : : /**
240 : : * MctRestrictApplicationsSelector:search: (nullable)
241 : : *
242 : : * Search terms to filter the displayed list of apps by, or `NULL` to not
243 : : * filter the search.
244 : : *
245 : : * Since: 0.12.0
246 : : */
247 : 1 : properties[PROP_SEARCH] =
248 : 1 : g_param_spec_string ("search", NULL, NULL,
249 : : NULL,
250 : : G_PARAM_READWRITE |
251 : : G_PARAM_STATIC_STRINGS |
252 : : G_PARAM_EXPLICIT_NOTIFY);
253 : :
254 : 1 : g_object_class_install_properties (object_class, G_N_ELEMENTS (properties), properties);
255 : :
256 : : /**
257 : : * MctRestrictApplicationsSelector::changed:
258 : : *
259 : : * Emitted whenever an application in the list is blocked or unblocked.
260 : : *
261 : : * Since: 0.5.0
262 : : */
263 : 1 : signals[SIGNAL_CHANGED] =
264 : 1 : g_signal_new ("changed",
265 : : MCT_TYPE_RESTRICT_APPLICATIONS_SELECTOR,
266 : : G_SIGNAL_RUN_LAST,
267 : : 0,
268 : : NULL, NULL,
269 : : g_cclosure_marshal_VOID__VOID,
270 : : G_TYPE_NONE, 0);
271 : :
272 : 1 : gtk_widget_class_set_template_from_resource (widget_class, "/org/freedesktop/MalcontentUi/ui/restrict-applications-selector.ui");
273 : :
274 : 1 : gtk_widget_class_bind_template_child (widget_class, MctRestrictApplicationsSelector, listbox);
275 : 1 : gtk_widget_class_bind_template_child (widget_class, MctRestrictApplicationsSelector, apps);
276 : 1 : gtk_widget_class_bind_template_child (widget_class, MctRestrictApplicationsSelector, filtered_apps);
277 : 1 : gtk_widget_class_bind_template_child (widget_class, MctRestrictApplicationsSelector, search_filter);
278 : :
279 : 1 : gtk_widget_class_bind_template_callback (widget_class, app_info_dup_name);
280 : 1 : }
281 : :
282 : : static void
283 : 0 : mct_restrict_applications_selector_init (MctRestrictApplicationsSelector *self)
284 : : {
285 : :
286 : 0 : gtk_widget_init_template (GTK_WIDGET (self));
287 : :
288 : 0 : self->app_info_monitor = g_app_info_monitor_get ();
289 : 0 : self->app_info_monitor_changed_id =
290 : 0 : g_signal_connect (self->app_info_monitor, "changed",
291 : : (GCallback) app_info_changed_cb, self);
292 : :
293 : 0 : gtk_list_box_bind_model (self->listbox,
294 : 0 : G_LIST_MODEL (self->filtered_apps),
295 : : create_row_for_app_cb,
296 : : self,
297 : : NULL);
298 : :
299 : 0 : self->blocklisted_apps = g_hash_table_new_full (g_direct_hash,
300 : : g_direct_equal,
301 : : g_object_unref,
302 : : NULL);
303 : :
304 : 0 : self->system_installation = flatpak_installation_new_system (NULL, NULL);
305 : 0 : self->user_installation = flatpak_installation_new_user (NULL, NULL);
306 : 0 : }
307 : :
308 : : static void
309 : 0 : on_switch_active_changed_cb (GtkSwitch *s,
310 : : GParamSpec *pspec,
311 : : gpointer user_data)
312 : : {
313 : 0 : MctRestrictApplicationsSelector *self = MCT_RESTRICT_APPLICATIONS_SELECTOR (user_data);
314 : : GAppInfo *app;
315 : : gboolean allowed;
316 : :
317 : 0 : app = g_object_get_data (G_OBJECT (s), "GAppInfo");
318 : 0 : allowed = !gtk_switch_get_active (s);
319 : :
320 [ # # ]: 0 : if (allowed)
321 : : {
322 : : gboolean removed;
323 : :
324 : 0 : g_debug ("Removing ‘%s’ from blocklisted apps", g_app_info_get_id (app));
325 : :
326 : 0 : removed = g_hash_table_remove (self->blocklisted_apps, app);
327 : 0 : g_assert (removed);
328 : : }
329 : : else
330 : : {
331 : : gboolean added;
332 : :
333 : 0 : g_debug ("Blocklisting ‘%s’", g_app_info_get_id (app));
334 : :
335 : 0 : added = g_hash_table_add (self->blocklisted_apps, g_object_ref (app));
336 : 0 : g_assert (added);
337 : : }
338 : :
339 : 0 : g_signal_emit (self, signals[SIGNAL_CHANGED], 0);
340 : 0 : }
341 : :
342 : : static void
343 : 0 : update_listbox_row_switch (MctRestrictApplicationsSelector *self,
344 : : GtkSwitch *w)
345 : : {
346 : 0 : GAppInfo *app = g_object_get_data (G_OBJECT (w), "GAppInfo");
347 [ # # # # ]: 0 : gboolean allowed = mct_app_filter_is_appinfo_allowed (self->app_filter, app) && !g_hash_table_contains (self->blocklisted_apps, app);
348 : :
349 : 0 : gtk_switch_set_active (w, !allowed);
350 : :
351 [ # # ]: 0 : if (allowed)
352 : 0 : g_hash_table_remove (self->blocklisted_apps, app);
353 : : else
354 : 0 : g_hash_table_add (self->blocklisted_apps, g_object_ref (app));
355 : 0 : }
356 : :
357 : : static GtkWidget *
358 : 0 : create_row_for_app_cb (gpointer item,
359 : : gpointer user_data)
360 : : {
361 : 0 : MctRestrictApplicationsSelector *self = MCT_RESTRICT_APPLICATIONS_SELECTOR (user_data);
362 : 0 : GAppInfo *app = G_APP_INFO (item);
363 : 0 : g_autoptr(GIcon) icon = NULL;
364 : : GtkWidget *row, *w;
365 : : const gchar *app_name;
366 : :
367 : 0 : app_name = g_app_info_get_name (app);
368 : :
369 : 0 : g_assert (G_IS_DESKTOP_APP_INFO (app));
370 : :
371 : 0 : icon = g_app_info_get_icon (app);
372 [ # # ]: 0 : if (icon == NULL)
373 : 0 : icon = g_themed_icon_new ("application-x-executable");
374 : : else
375 : 0 : g_object_ref (icon);
376 : :
377 : 0 : row = adw_action_row_new ();
378 : :
379 : : /* Icon */
380 : 0 : w = gtk_image_new_from_gicon (icon);
381 : 0 : gtk_image_set_icon_size (GTK_IMAGE (w), GTK_ICON_SIZE_LARGE);
382 : 0 : adw_action_row_add_prefix (ADW_ACTION_ROW (row), w);
383 : :
384 : : /* App name label */
385 : 0 : adw_preferences_row_set_title (ADW_PREFERENCES_ROW (row), app_name);
386 : :
387 : : /* Switch */
388 : 0 : w = g_object_new (GTK_TYPE_SWITCH,
389 : : "valign", GTK_ALIGN_CENTER,
390 : : NULL);
391 : 0 : gtk_widget_add_css_class (w, "restricts");
392 : 0 : adw_action_row_add_suffix (ADW_ACTION_ROW (row), w);
393 : 0 : adw_action_row_set_activatable_widget (ADW_ACTION_ROW (row), w);
394 : :
395 : 0 : gtk_widget_set_focusable (GTK_WIDGET (row), FALSE);
396 : :
397 : : /* Fetch status from AccountService */
398 : 0 : g_object_set_data (G_OBJECT (row), "GtkSwitch", w);
399 : 0 : g_object_set_data_full (G_OBJECT (w), "GAppInfo", g_object_ref (app), g_object_unref);
400 : 0 : update_listbox_row_switch (self, GTK_SWITCH (w));
401 : 0 : g_signal_connect (w, "notify::active", G_CALLBACK (on_switch_active_changed_cb), self);
402 : :
403 : 0 : return row;
404 : : }
405 : :
406 : : static char *
407 : 0 : app_info_dup_name (GAppInfo *app_info)
408 : : {
409 : 0 : return g_strdup (g_app_info_get_name (app_info));
410 : : }
411 : :
412 : : static gint
413 : 0 : compare_app_info_cb (gconstpointer a,
414 : : gconstpointer b,
415 : : gpointer user_data)
416 : : {
417 : 0 : GAppInfo *app_a = (GAppInfo*) a;
418 : 0 : GAppInfo *app_b = (GAppInfo*) b;
419 : :
420 : 0 : return g_utf8_collate (g_app_info_get_name (app_a),
421 : 0 : g_app_info_get_name (app_b));
422 : : }
423 : :
424 : : static gint
425 : 0 : app_compare_id_length_cb (gconstpointer a,
426 : : gconstpointer b)
427 : : {
428 : 0 : GAppInfo *info_a = (GAppInfo *) a, *info_b = (GAppInfo *) b;
429 : : const gchar *id_a, *id_b;
430 : : gsize id_a_len, id_b_len;
431 : :
432 : 0 : id_a = g_app_info_get_id (info_a);
433 : 0 : id_b = g_app_info_get_id (info_b);
434 : :
435 [ # # # # ]: 0 : if (id_a == NULL && id_b == NULL)
436 : 0 : return 0;
437 [ # # ]: 0 : else if (id_a == NULL)
438 : 0 : return -1;
439 [ # # ]: 0 : else if (id_b == NULL)
440 : 0 : return 1;
441 : :
442 : 0 : id_a_len = strlen (id_a);
443 : 0 : id_b_len = strlen (id_b);
444 [ # # ]: 0 : if (id_a_len == id_b_len)
445 : 0 : return strcmp (id_a, id_b);
446 : : else
447 : 0 : return id_a_len - id_b_len;
448 : : }
449 : :
450 : : /* Elements in @added_out and @removed_out are valid as long as @old_apps and
451 : : * @new_apps are valid.
452 : : *
453 : : * Both lists have to be sorted the same before calling this function. */
454 : : static void
455 : 0 : diff_app_lists (GList *old_apps,
456 : : GList *new_apps,
457 : : GPtrArray **added_out,
458 : : GPtrArray **removed_out)
459 : : {
460 [ # # ]: 0 : g_autoptr(GPtrArray) added = g_ptr_array_new_with_free_func (NULL);
461 [ # # ]: 0 : g_autoptr(GPtrArray) removed = g_ptr_array_new_with_free_func (NULL);
462 : : GList *o, *n;
463 : :
464 : 0 : g_return_if_fail (added_out != NULL);
465 : 0 : g_return_if_fail (removed_out != NULL);
466 : :
467 [ # # # # ]: 0 : for (o = old_apps, n = new_apps; o != NULL || n != NULL;)
468 : : {
469 : : int comparison;
470 : :
471 [ # # ]: 0 : if (o == NULL)
472 : 0 : comparison = 1;
473 [ # # ]: 0 : else if (n == NULL)
474 : 0 : comparison = -1;
475 : : else
476 : 0 : comparison = app_compare_id_length_cb (o->data, n->data);
477 : :
478 [ # # ]: 0 : if (comparison < 0)
479 : : {
480 : 0 : g_ptr_array_add (removed, o->data);
481 : 0 : o = o->next;
482 : : }
483 [ # # ]: 0 : else if (comparison > 0)
484 : : {
485 : 0 : g_ptr_array_add (added, n->data);
486 : 0 : n = n->next;
487 : : }
488 : : else
489 : : {
490 : 0 : o = o->next;
491 : 0 : n = n->next;
492 : : }
493 : : }
494 : :
495 : 0 : *added_out = g_steal_pointer (&added);
496 : 0 : *removed_out = g_steal_pointer (&removed);
497 : : }
498 : :
499 : : /* This is quite expensive to call, as there’s no way to avoid calling
500 : : * g_app_info_get_all() to see if anything’s changed; and that’s quite expensive. */
501 : : static void
502 : 0 : reload_apps (MctRestrictApplicationsSelector *self)
503 : : {
504 : 0 : g_autolist(GAppInfo) old_apps = NULL;
505 : 0 : g_autolist(GAppInfo) new_apps = NULL;
506 : 0 : g_autoptr(GPtrArray) added_apps = NULL, removed_apps = NULL;
507 : 0 : g_autoptr(GHashTable) seen_flatpak_ids = NULL;
508 : 0 : g_autoptr(GHashTable) seen_executables = NULL;
509 : :
510 : 0 : old_apps = g_steal_pointer (&self->cached_apps);
511 : 0 : new_apps = g_app_info_get_all ();
512 : :
513 : : /* Sort the apps by increasing length of #GAppInfo ID. When coupled with the
514 : : * deduplication of flatpak IDs and executable paths, below, this should ensure that we
515 : : * pick the ‘base’ app out of any set with matching prefixes and identical app IDs (in
516 : : * case of flatpak apps) or executables (for non-flatpak apps), and show only that.
517 : : *
518 : : * This is designed to avoid listing all the components of LibreOffice for example,
519 : : * which all share an app ID and hence have the same entry in the parental controls
520 : : * app filter.
521 : : *
522 : : * Then diff the old and new lists so that the code below doesn’t end up
523 : : * removing more rows than are necessary, and hence potentially losing
524 : : * in-progress user input. */
525 : 0 : new_apps = g_list_sort (new_apps, app_compare_id_length_cb);
526 : 0 : diff_app_lists (old_apps, new_apps, &added_apps, &removed_apps);
527 : :
528 : 0 : g_debug ("%s: Diffed old and new app lists: %u apps added, %u apps removed",
529 : : G_STRFUNC, added_apps->len, removed_apps->len);
530 : :
531 : 0 : seen_flatpak_ids = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
532 : 0 : seen_executables = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
533 : :
534 : : /* Remove items first. */
535 [ # # ]: 0 : for (guint i = 0; i < removed_apps->len; i++)
536 : : {
537 : 0 : GAppInfo *app = removed_apps->pdata[i];
538 : : guint pos;
539 : : gboolean found;
540 : :
541 : 0 : found = g_list_store_find_with_equal_func (self->apps, app,
542 : : (GEqualFunc) g_app_info_equal, &pos);
543 : :
544 : : /* The app being removed may have not passed the condition checks below
545 : : * to have been added to self->apps. */
546 [ # # ]: 0 : if (!found)
547 : 0 : continue;
548 : :
549 : 0 : g_debug ("Removing app ‘%s’", g_app_info_get_id (app));
550 : 0 : g_list_store_remove (self->apps, pos);
551 : : }
552 : :
553 : : /* Now add the new items. */
554 [ # # ]: 0 : for (guint i = 0; i < added_apps->len; i++)
555 : : {
556 : 0 : GAppInfo *app = added_apps->pdata[i];
557 : : const gchar *app_name;
558 : : const gchar * const *supported_types;
559 : :
560 : 0 : app_name = g_app_info_get_name (app);
561 : :
562 : 0 : supported_types = g_app_info_get_supported_types (app);
563 : :
564 [ # # # # : 0 : if (!G_IS_DESKTOP_APP_INFO (app) ||
# # # # #
# ]
565 : 0 : !g_app_info_should_show (app) ||
566 [ # # # # ]: 0 : app_name[0] == '\0' ||
567 : : /* FIXME: Only list flatpak apps and apps with X-Parental-Controls
568 : : * key set for now; we really need a system-wide MAC to be able to
569 : : * reliably support blocklisting system programs. */
570 [ # # ]: 0 : (!g_desktop_app_info_has_key (G_DESKTOP_APP_INFO (app), "X-Flatpak") &&
571 [ # # ]: 0 : !g_desktop_app_info_has_key (G_DESKTOP_APP_INFO (app), "X-Parental-Controls")) ||
572 : : /* Web browsers are special cased */
573 [ # # ]: 0 : (supported_types && g_strv_contains (supported_types, WEB_BROWSERS_CONTENT_TYPE)))
574 : : {
575 : 0 : continue;
576 : : }
577 : :
578 [ # # ]: 0 : if (g_desktop_app_info_has_key (G_DESKTOP_APP_INFO (app), "X-Flatpak"))
579 : : {
580 [ # # ]: 0 : g_autofree gchar *flatpak_id = NULL;
581 : :
582 : 0 : flatpak_id = g_desktop_app_info_get_string (G_DESKTOP_APP_INFO (app), "X-Flatpak");
583 : 0 : g_debug ("Processing app ‘%s’ (Exec=%s, X-Flatpak=%s)",
584 : : g_app_info_get_id (app),
585 : : g_app_info_get_executable (app),
586 : : flatpak_id);
587 : :
588 : : /* Have we seen this flatpak ID before? */
589 [ # # ]: 0 : if (!g_hash_table_add (seen_flatpak_ids, g_steal_pointer (&flatpak_id)))
590 : : {
591 : 0 : g_debug (" → Skipping ‘%s’ due to seeing its flatpak ID already",
592 : : g_app_info_get_id (app));
593 : 0 : continue;
594 : : }
595 : : }
596 [ # # ]: 0 : else if (g_desktop_app_info_has_key (G_DESKTOP_APP_INFO (app), "X-Parental-Controls"))
597 : : {
598 [ # # ]: 0 : g_autofree gchar *parental_controls_type = NULL;
599 [ # # ]: 0 : g_autofree gchar *executable = NULL;
600 : :
601 : 0 : parental_controls_type = g_desktop_app_info_get_string (G_DESKTOP_APP_INFO (app),
602 : : "X-Parental-Controls");
603 : : /* Ignore X-Parental-Controls=none */
604 [ # # ]: 0 : if (g_strcmp0 (parental_controls_type, "none") == 0)
605 : 0 : continue;
606 : :
607 : 0 : executable = g_strdup (g_app_info_get_executable (app));
608 : 0 : g_debug ("Processing app ‘%s’ (Exec=%s, X-Parental-Controls=%s)",
609 : : g_app_info_get_id (app),
610 : : executable,
611 : : parental_controls_type);
612 : :
613 : : /* Have we seen this executable before? */
614 [ # # ]: 0 : if (!g_hash_table_add (seen_executables, g_steal_pointer (&executable)))
615 : : {
616 : 0 : g_debug (" → Skipping ‘%s’ due to seeing its executable already",
617 : : g_app_info_get_id (app));
618 : 0 : continue;
619 : : }
620 : : }
621 : :
622 : 0 : g_list_store_insert_sorted (self->apps,
623 : : app,
624 : : compare_app_info_cb,
625 : : self);
626 : : }
627 : :
628 : : /* Update the cache for next time. */
629 : 0 : self->cached_apps = g_steal_pointer (&new_apps);
630 : 0 : }
631 : :
632 : : static void
633 : 0 : app_info_changed_cb (GAppInfoMonitor *monitor,
634 : : gpointer user_data)
635 : : {
636 : 0 : MctRestrictApplicationsSelector *self = MCT_RESTRICT_APPLICATIONS_SELECTOR (user_data);
637 : :
638 : 0 : reload_apps (self);
639 : 0 : }
640 : :
641 : : /* Will return %NULL if @flatpak_id is not installed. */
642 : : static gchar *
643 : 0 : get_flatpak_ref_for_app_id (MctRestrictApplicationsSelector *self,
644 : : const gchar *flatpak_id,
645 : : GCancellable *cancellable)
646 : : {
647 : 0 : g_autoptr(FlatpakInstalledRef) ref = NULL;
648 : 0 : g_autoptr(GError) local_error = NULL;
649 : :
650 : 0 : g_assert (self->system_installation != NULL);
651 : 0 : g_assert (self->user_installation != NULL);
652 : :
653 : : /* FIXME technically this does local file I/O and should be async */
654 : 0 : ref = flatpak_installation_get_current_installed_app (self->user_installation,
655 : : flatpak_id,
656 : : cancellable,
657 : : &local_error);
658 : :
659 [ # # # # ]: 0 : if (local_error != NULL &&
660 : 0 : !g_error_matches (local_error, FLATPAK_ERROR, FLATPAK_ERROR_NOT_INSTALLED))
661 : : {
662 : 0 : g_warning ("Error searching for Flatpak ref: %s", local_error->message);
663 : 0 : return NULL;
664 : : }
665 : :
666 : 0 : g_clear_error (&local_error);
667 : :
668 [ # # # # ]: 0 : if (!ref || !flatpak_installed_ref_get_is_current (ref))
669 : : {
670 : : /* FIXME technically this does local file I/O and should be async */
671 : 0 : ref = flatpak_installation_get_current_installed_app (self->system_installation,
672 : : flatpak_id,
673 : : cancellable,
674 : : &local_error);
675 [ # # ]: 0 : if (local_error != NULL)
676 : : {
677 [ # # ]: 0 : if (!g_error_matches (local_error, FLATPAK_ERROR, FLATPAK_ERROR_NOT_INSTALLED))
678 : 0 : g_warning ("Error searching for Flatpak ref: %s", local_error->message);
679 : 0 : return NULL;
680 : : }
681 : : }
682 : :
683 : 0 : return flatpak_ref_format_ref (FLATPAK_REF (ref));
684 : : }
685 : :
686 : : /**
687 : : * mct_restrict_applications_selector_new:
688 : : * @app_filter: (transfer none): app filter to configure the selector from initially
689 : : *
690 : : * Create a new [class@MalcontentUi.RestrictApplicationsSelector] widget.
691 : : *
692 : : * Returns: (transfer full): a new restricted applications selector
693 : : * Since: 0.5.0
694 : : */
695 : : MctRestrictApplicationsSelector *
696 : 0 : mct_restrict_applications_selector_new (MctAppFilter *app_filter)
697 : : {
698 : 0 : g_return_val_if_fail (app_filter != NULL, NULL);
699 : :
700 : 0 : return g_object_new (MCT_TYPE_RESTRICT_APPLICATIONS_SELECTOR,
701 : : "app-filter", app_filter,
702 : : NULL);
703 : : }
704 : :
705 : : /**
706 : : * mct_restrict_applications_selector_build_app_filter:
707 : : * @self: a restricted applications selector widget
708 : : * @builder: an existing [type@Malcontent.AppFilterBuilder] to modify
709 : : *
710 : : * Get the app filter settings currently configured in the selector, by modifying
711 : : * the given @builder.
712 : : *
713 : : * Since: 0.5.0
714 : : */
715 : : void
716 : 0 : mct_restrict_applications_selector_build_app_filter (MctRestrictApplicationsSelector *self,
717 : : MctAppFilterBuilder *builder)
718 : : {
719 : : GDesktopAppInfo *app;
720 : : GHashTableIter iter;
721 : :
722 : 0 : g_return_if_fail (MCT_IS_RESTRICT_APPLICATIONS_SELECTOR (self));
723 : 0 : g_return_if_fail (builder != NULL);
724 : :
725 : 0 : g_hash_table_iter_init (&iter, self->blocklisted_apps);
726 [ # # ]: 0 : while (g_hash_table_iter_next (&iter, (gpointer) &app, NULL))
727 : : {
728 [ # # ]: 0 : g_autofree gchar *flatpak_id = NULL;
729 : :
730 : 0 : flatpak_id = g_desktop_app_info_get_string (app, "X-Flatpak");
731 [ # # ]: 0 : if (flatpak_id)
732 : 0 : flatpak_id = g_strstrip (flatpak_id);
733 : :
734 [ # # ]: 0 : if (flatpak_id)
735 : : {
736 [ # # ]: 0 : g_autofree gchar *flatpak_ref = get_flatpak_ref_for_app_id (self, flatpak_id, NULL);
737 : :
738 [ # # ]: 0 : if (!flatpak_ref)
739 : : {
740 : 0 : g_warning ("Skipping blocklisting Flatpak ID ‘%s’ due to it not being installed", flatpak_id);
741 : 0 : continue;
742 : : }
743 : :
744 : 0 : g_debug ("\t\t → Blocklisting Flatpak ref: %s", flatpak_ref);
745 : 0 : mct_app_filter_builder_blocklist_flatpak_ref (builder, flatpak_ref);
746 : : }
747 : : else
748 : : {
749 : 0 : const gchar *executable = g_app_info_get_executable (G_APP_INFO (app));
750 [ # # # # ]: 0 : g_autofree gchar *path = (executable != NULL) ? g_find_program_in_path (executable) : NULL;
751 : :
752 [ # # ]: 0 : if (!path)
753 : : {
754 : 0 : g_warning ("Skipping blocklisting executable ‘%s’ due to it not being found", executable);
755 : 0 : continue;
756 : : }
757 : :
758 : 0 : g_debug ("\t\t → Blocklisting path: %s", path);
759 : 0 : mct_app_filter_builder_blocklist_path (builder, path);
760 : : }
761 : : }
762 : : }
763 : :
764 : : /**
765 : : * mct_restrict_applications_selector_get_app_filter:
766 : : * @self: a restricted applications selector widget
767 : : *
768 : : * Get the value of
769 : : * [property@MalcontentUi.RestrictApplicationsSelector:app-filter].
770 : : *
771 : : * If the property was originally set to `NULL`, this will be the empty app
772 : : * filter.
773 : : *
774 : : * Returns: (transfer none) (not nullable): the initial app filter used to
775 : : * populate the selector
776 : : * Since: 0.5.0
777 : : */
778 : : MctAppFilter *
779 : 0 : mct_restrict_applications_selector_get_app_filter (MctRestrictApplicationsSelector *self)
780 : : {
781 : 0 : g_return_val_if_fail (MCT_IS_RESTRICT_APPLICATIONS_SELECTOR (self), NULL);
782 : :
783 : 0 : return self->app_filter;
784 : : }
785 : :
786 : : /**
787 : : * mct_restrict_applications_selector_set_app_filter:
788 : : * @self: a restricted applications selector widget
789 : : * @app_filter: (nullable) (transfer none): the app filter to configure the
790 : : * selector from, or `NULL` to use an empty app filter
791 : : *
792 : : * Set the value of
793 : : * [property@MalcontentUi.RestrictApplicationsSelector:app-filter].
794 : : *
795 : : * This will overwrite any user changes to the selector, so they should be saved
796 : : * first using
797 : : * [method@MalcontentUi.RestrictApplicationsSelector.build_app_filter] if
798 : : * desired.
799 : : *
800 : : * Since: 0.5.0
801 : : */
802 : : void
803 : 0 : mct_restrict_applications_selector_set_app_filter (MctRestrictApplicationsSelector *self,
804 : : MctAppFilter *app_filter)
805 : : {
806 [ # # ]: 0 : g_autoptr(MctAppFilter) owned_app_filter = NULL;
807 : : guint n_apps;
808 : :
809 : 0 : g_return_if_fail (MCT_IS_RESTRICT_APPLICATIONS_SELECTOR (self));
810 : :
811 : : /* Default app filter, typically for when we’re instantiated by #GtkBuilder. */
812 [ # # ]: 0 : if (app_filter == NULL)
813 : : {
814 : 0 : g_auto(MctAppFilterBuilder) builder = MCT_APP_FILTER_BUILDER_INIT ();
815 : 0 : owned_app_filter = mct_app_filter_builder_end (&builder);
816 : 0 : app_filter = owned_app_filter;
817 : : }
818 : :
819 [ # # ]: 0 : if (app_filter == self->app_filter)
820 : 0 : return;
821 : :
822 [ # # ]: 0 : g_clear_pointer (&self->app_filter, mct_app_filter_unref);
823 : 0 : self->app_filter = mct_app_filter_ref (app_filter);
824 : :
825 : : /* Update the status of each app row. */
826 : 0 : n_apps = g_list_model_get_n_items (G_LIST_MODEL (self->filtered_apps));
827 : :
828 [ # # ]: 0 : for (guint i = 0; i < n_apps; i++)
829 : : {
830 : : GtkListBoxRow *row;
831 : : GtkWidget *w;
832 : :
833 : : /* Navigate the widget hierarchy set up in create_row_for_app_cb(). */
834 : 0 : row = gtk_list_box_get_row_at_index (self->listbox, i);
835 : 0 : g_assert (row != NULL && GTK_IS_LIST_BOX_ROW (row));
836 : :
837 : 0 : w = g_object_get_data (G_OBJECT (row), "GtkSwitch");
838 : 0 : g_assert (w != NULL && GTK_IS_SWITCH (w));
839 : :
840 : 0 : update_listbox_row_switch (self, GTK_SWITCH (w));
841 : : }
842 : :
843 : 0 : g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_APP_FILTER]);
844 : : }
845 : :
846 : : /**
847 : : * mct_restrict_applications_selector_get_search:
848 : : * @self: a restricted applications selector widget
849 : : *
850 : : * Get the value of [property@MalcontentUi.RestrictApplicationsSelector:search].
851 : : *
852 : : * Returns: (nullable): current search terms, or `NULL` if no search filtering
853 : : * is active
854 : : * Since: 0.12.0
855 : : */
856 : : const gchar *
857 : 0 : mct_restrict_applications_selector_get_search (MctRestrictApplicationsSelector *self)
858 : : {
859 : 0 : g_return_val_if_fail (MCT_IS_RESTRICT_APPLICATIONS_SELECTOR (self), NULL);
860 : :
861 : 0 : return self->search;
862 : : }
863 : :
864 : : /**
865 : : * mct_restrict_applications_selector_set_search:
866 : : * @self: a restricted applications selector widget
867 : : * @search: (nullable): search terms, or `NULL` to not filter the app list
868 : : *
869 : : * Set the value of [property@MalcontentUi.RestrictApplicationsSelector:search],
870 : : * or clear it to `NULL`.
871 : : *
872 : : * Since: 0.12.0
873 : : */
874 : : void
875 : 0 : mct_restrict_applications_selector_set_search (MctRestrictApplicationsSelector *self,
876 : : const gchar *search)
877 : : {
878 : 0 : g_return_if_fail (MCT_IS_RESTRICT_APPLICATIONS_SELECTOR (self));
879 : :
880 : : /* Squash empty search terms down to nothing. */
881 [ # # # # ]: 0 : if (search != NULL && *search == '\0')
882 : 0 : search = NULL;
883 : :
884 [ # # ]: 0 : if (g_strcmp0 (search, self->search) == 0)
885 : 0 : return;
886 : :
887 [ # # ]: 0 : g_clear_pointer (&self->search, g_free);
888 : 0 : self->search = g_strdup (search);
889 : :
890 : 0 : gtk_string_filter_set_search (self->search_filter, self->search);
891 : 0 : g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SEARCH]);
892 : : }
|