From 0e22ad78613c2c4222336af282b75e1151a45df9 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Mon, 3 Feb 2020 17:15:23 +0000 Subject: [PATCH 01/19] =?UTF-8?q?malcontent-control:=20Use=20g=5Fsignal=5F?= =?UTF-8?q?connect=5Fobject()=20when=20we=20don=E2=80=99t=20hold=20ref?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The application doesn’t hold a ref to some of the widgets it holds a pointer to, since their ownership is controlled by the main window. The main window’s lifecycle is controlled by the application, but its dispose cycle runs at a slightly different time. Hence, we should disconnect from the widget signals when we can, but without holding a strong ref. --- malcontent-control/application.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/malcontent-control/application.c b/malcontent-control/application.c index d2b69a4..7a0ca34 100644 --- a/malcontent-control/application.c +++ b/malcontent-control/application.c @@ -148,9 +148,9 @@ mct_application_activate (GApplication *application) self->error_message = GTK_LABEL (gtk_builder_get_object (builder, "error_message")); /* Connect signals. */ - g_signal_connect (self->user_selector, "notify::user", - G_CALLBACK (user_selector_notify_user_cb), - self); + g_signal_connect_object (self->user_selector, "notify::user", + G_CALLBACK (user_selector_notify_user_cb), + self, 0 /* flags */); g_signal_connect (self->user_manager, "notify::is-loaded", G_CALLBACK (user_manager_notify_is_loaded_cb), self); From 93d18ed2a99eeae7098bdde12b54841995c8e007 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Tue, 28 Jan 2020 13:56:00 +0000 Subject: [PATCH 02/19] libmalcontent: Add type macros for boxed types These were accidentally missed out before. Signed-off-by: Philip Withnall --- libmalcontent/app-filter.h | 1 + libmalcontent/session-limits.h | 1 + 2 files changed, 2 insertions(+) diff --git a/libmalcontent/app-filter.h b/libmalcontent/app-filter.h index cc92e23..1860fe0 100644 --- a/libmalcontent/app-filter.h +++ b/libmalcontent/app-filter.h @@ -70,6 +70,7 @@ typedef enum */ typedef struct _MctAppFilter MctAppFilter; GType mct_app_filter_get_type (void); +#define MCT_TYPE_APP_FILTER mct_app_filter_get_type () MctAppFilter *mct_app_filter_ref (MctAppFilter *filter); void mct_app_filter_unref (MctAppFilter *filter); diff --git a/libmalcontent/session-limits.h b/libmalcontent/session-limits.h index 1ac356c..d8fb5f1 100644 --- a/libmalcontent/session-limits.h +++ b/libmalcontent/session-limits.h @@ -44,6 +44,7 @@ G_BEGIN_DECLS */ typedef struct _MctSessionLimits MctSessionLimits; GType mct_session_limits_get_type (void); +#define MCT_TYPE_SESSION_LIMITS mct_session_limits_get_type () MctSessionLimits *mct_session_limits_ref (MctSessionLimits *limits); void mct_session_limits_unref (MctSessionLimits *limits); From bffab0942fe6703f2ab31d74c1ce82c54bca5626 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Tue, 28 Jan 2020 14:16:40 +0000 Subject: [PATCH 03/19] malcontent-control: Move the app restrictions into a separate dialogue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rather than having a scrollable listbox within a scrollable list of widgets, move the listbox out to a separate dialogue. This involves separating out all the code to query the apps, to get and to set the app filter, from `MctUserControls` out into the new `MctRestrictApplicationsSelector`. Most of it is unchanged, aside from its interaction with the filter: the filter is now provided to the widget by the calling code, rather than being queried by the widget itself. The widget’s status can be queried into an `MctAppFilterBuilder`, rather than being used to set the app filter directly. This commit redesigns the appearance of the relevant widgets in the main window so that they follow the new list-box-like visual design. A following commit will apply similar changes to the other widgest in the main screen. Signed-off-by: Philip Withnall --- malcontent-control/application.c | 68 +- .../malcontent-control.gresource.xml | 4 +- malcontent-control/meson.build | 6 + .../restrict-applications-dialog.c | 382 ++++++++++ .../restrict-applications-dialog.h | 50 ++ .../restrict-applications-dialog.ui | 56 ++ .../restrict-applications-selector.c | 665 ++++++++++++++++++ .../restrict-applications-selector.h | 45 ++ .../restrict-applications-selector.ui | 32 + malcontent-control/user-controls.c | 438 ++---------- malcontent-control/user-controls.ui | 329 +++++---- po/POTFILES.in | 4 + 12 files changed, 1550 insertions(+), 529 deletions(-) create mode 100644 malcontent-control/restrict-applications-dialog.c create mode 100644 malcontent-control/restrict-applications-dialog.h create mode 100644 malcontent-control/restrict-applications-dialog.ui create mode 100644 malcontent-control/restrict-applications-selector.c create mode 100644 malcontent-control/restrict-applications-selector.h create mode 100644 malcontent-control/restrict-applications-selector.ui diff --git a/malcontent-control/application.c b/malcontent-control/application.c index 7a0ca34..5e30b11 100644 --- a/malcontent-control/application.c +++ b/malcontent-control/application.c @@ -178,6 +178,46 @@ mct_application_class_init (MctApplicationClass *klass) application_class->activate = mct_application_activate; } +static void +update_main_stack (MctApplication *self) +{ + gboolean is_user_manager_loaded; + const gchar *new_page_name, *old_page_name; + GtkWidget *new_focus_widget; + + /* The implementation of #ActUserManager guarantees that once is-loaded is + * true, it is never reset to false. */ + g_object_get (self->user_manager, "is-loaded", &is_user_manager_loaded, NULL); + + /* Handle any loading errors. */ + if (is_user_manager_loaded && act_user_manager_no_service (self->user_manager)) + { + gtk_label_set_label (self->error_title, + _("Failed to load user data from the system")); + gtk_label_set_label (self->error_message, + _("Please make sure that the AccountsService is installed and enabled.")); + + new_page_name = "error"; + new_focus_widget = NULL; + } + else if (is_user_manager_loaded) + { + new_page_name = "controls"; + new_focus_widget = GTK_WIDGET (self->user_selector); + } + else + { + new_page_name = "loading"; + new_focus_widget = NULL; + } + + old_page_name = gtk_stack_get_visible_child_name (self->main_stack); + gtk_stack_set_visible_child_name (self->main_stack, new_page_name); + + if (new_focus_widget != NULL && !g_str_equal (old_page_name, new_page_name)) + gtk_widget_grab_focus (new_focus_widget); +} + static void user_selector_notify_user_cb (GObject *obj, GParamSpec *pspec, @@ -198,34 +238,8 @@ user_manager_notify_is_loaded_cb (GObject *obj, gpointer user_data) { MctApplication *self = MCT_APPLICATION (user_data); - ActUserManager *user_manager = ACT_USER_MANAGER (obj); - gboolean is_loaded; - const gchar *new_page_name; - /* The implementation of #ActUserManager guarantees that once is-loaded is - * true, it is never reset to false. */ - g_object_get (user_manager, "is-loaded", &is_loaded, NULL); - - /* Handle any loading errors. */ - if (is_loaded && act_user_manager_no_service (user_manager)) - { - gtk_label_set_label (self->error_title, - _("Failed to load user data from the system")); - gtk_label_set_label (self->error_message, - _("Please make sure that the AccountsService is installed and enabled.")); - - new_page_name = "error"; - } - else if (is_loaded) - { - new_page_name = "controls"; - } - else - { - new_page_name = "loading"; - } - - gtk_stack_set_visible_child_name (self->main_stack, new_page_name); + update_main_stack (self); } /** diff --git a/malcontent-control/malcontent-control.gresource.xml b/malcontent-control/malcontent-control.gresource.xml index 516a033..8f4d8e9 100644 --- a/malcontent-control/malcontent-control.gresource.xml +++ b/malcontent-control/malcontent-control.gresource.xml @@ -1,10 +1,12 @@ - + carousel.css carousel.ui main.ui + restrict-applications-dialog.ui + restrict-applications-selector.ui user-controls.ui user-selector.ui diff --git a/malcontent-control/meson.build b/malcontent-control/meson.build index 4e66a6e..82a73b4 100644 --- a/malcontent-control/meson.build +++ b/malcontent-control/meson.build @@ -19,6 +19,10 @@ malcontent_control = executable('malcontent-control', 'gs-content-rating.c', 'gs-content-rating.h', 'main.c', + 'restrict-applications-dialog.c', + 'restrict-applications-dialog.h', + 'restrict-applications-selector.c', + 'restrict-applications-selector.h', 'user-controls.c', 'user-controls.h', 'user-image.c', @@ -90,6 +94,8 @@ if xmllint.found() files( 'carousel.ui', 'main.ui', + 'restrict-applications-dialog.ui', + 'restrict-applications-selector.ui', 'user-controls.ui', 'user-selector.ui', ), diff --git a/malcontent-control/restrict-applications-dialog.c b/malcontent-control/restrict-applications-dialog.c new file mode 100644 index 0000000..7d20ff1 --- /dev/null +++ b/malcontent-control/restrict-applications-dialog.c @@ -0,0 +1,382 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright © 2020 Endless Mobile, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * + * Authors: + * - Philip Withnall + */ + +#include +#include +#include +#include +#include +#include + +#include "restrict-applications-dialog.h" +#include "restrict-applications-selector.h" + + +static void update_description (MctRestrictApplicationsDialog *self); + +/** + * MctRestrictApplicationsDialog: + * + * The ‘Restrict Applications’ dialog is a dialog which shows the available + * applications on the system alongside a column of toggle switches, which + * allows the given user to be prevented from running each application. + * + * The dialog contains a single #MctRestrictApplicationsSelector. It takes a + * #MctRestrictApplicationsDialog:user and + * #MctRestrictApplicationsDialog:app-filter as input to set up the UI, and + * returns its output as set of modifications to a given #MctAppFilterBuilder + * using mct_restrict_applications_dialog_build_app_filter(). + * + * Since: 0.5.0 + */ +struct _MctRestrictApplicationsDialog +{ + GtkDialog parent_instance; + + MctRestrictApplicationsSelector *selector; + GtkLabel *description; + + MctAppFilter *app_filter; /* (owned) (not nullable) */ + ActUser *user; /* (owned) (nullable) */ +}; + +G_DEFINE_TYPE (MctRestrictApplicationsDialog, mct_restrict_applications_dialog, GTK_TYPE_DIALOG) + +typedef enum +{ + PROP_APP_FILTER = 1, + PROP_USER, +} MctRestrictApplicationsDialogProperty; + +static GParamSpec *properties[PROP_USER + 1]; + +static void +mct_restrict_applications_dialog_constructed (GObject *obj) +{ + MctRestrictApplicationsDialog *self = MCT_RESTRICT_APPLICATIONS_DIALOG (obj); + + g_assert (self->app_filter != NULL); + g_assert (self->user == NULL || ACT_IS_USER (self->user)); + + G_OBJECT_CLASS (mct_restrict_applications_dialog_parent_class)->constructed (obj); +} + +static void +mct_restrict_applications_dialog_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + MctRestrictApplicationsDialog *self = MCT_RESTRICT_APPLICATIONS_DIALOG (object); + + switch ((MctRestrictApplicationsDialogProperty) prop_id) + { + case PROP_APP_FILTER: + g_value_set_boxed (value, self->app_filter); + break; + + case PROP_USER: + g_value_set_object (value, self->user); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +mct_restrict_applications_dialog_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + MctRestrictApplicationsDialog *self = MCT_RESTRICT_APPLICATIONS_DIALOG (object); + + switch ((MctRestrictApplicationsDialogProperty) prop_id) + { + case PROP_APP_FILTER: + mct_restrict_applications_dialog_set_app_filter (self, g_value_get_boxed (value)); + break; + + case PROP_USER: + mct_restrict_applications_dialog_set_user (self, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +mct_restrict_applications_dialog_dispose (GObject *object) +{ + MctRestrictApplicationsDialog *self = (MctRestrictApplicationsDialog *)object; + + g_clear_pointer (&self->app_filter, mct_app_filter_unref); + g_clear_object (&self->user); + + G_OBJECT_CLASS (mct_restrict_applications_dialog_parent_class)->dispose (object); +} + +static void +mct_restrict_applications_dialog_class_init (MctRestrictApplicationsDialogClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->constructed = mct_restrict_applications_dialog_constructed; + object_class->get_property = mct_restrict_applications_dialog_get_property; + object_class->set_property = mct_restrict_applications_dialog_set_property; + object_class->dispose = mct_restrict_applications_dialog_dispose; + + /** + * MctRestrictApplicationsDialog:app-filter: (not nullable) + * + * The user’s current app filter, used to set up the dialog. As app filters + * are immutable, it is not updated as the dialog is changed. Use + * mct_restrict_applications_dialog_build_app_filter() to build the new app + * filter. + * + * Since: 0.5.0 + */ + properties[PROP_APP_FILTER] = + g_param_spec_boxed ("app-filter", + "App Filter", + "The user’s current app filter, used to set up the dialog.", + MCT_TYPE_APP_FILTER, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS | + G_PARAM_EXPLICIT_NOTIFY); + + /** + * MctRestrictApplicationsDialog:user: (nullable) + * + * The currently selected user account, or %NULL if no user is selected. + * + * Since: 0.5.0 + */ + properties[PROP_USER] = + g_param_spec_object ("user", + "User", + "The currently selected user account, or %NULL if no user is selected.", + ACT_TYPE_USER, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS | + G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, G_N_ELEMENTS (properties), properties); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/freedesktop/MalcontentControl/ui/restrict-applications-dialog.ui"); + + gtk_widget_class_bind_template_child (widget_class, MctRestrictApplicationsDialog, selector); + gtk_widget_class_bind_template_child (widget_class, MctRestrictApplicationsDialog, description); +} + +static void +mct_restrict_applications_dialog_init (MctRestrictApplicationsDialog *self) +{ + /* Ensure the types used in the UI are registered. */ + g_type_ensure (MCT_TYPE_RESTRICT_APPLICATIONS_SELECTOR); + + gtk_widget_init_template (GTK_WIDGET (self)); +} + +static const gchar * +get_user_display_name (ActUser *user) +{ + const gchar *display_name; + + g_return_val_if_fail (ACT_IS_USER (user), _("unknown")); + + display_name = act_user_get_real_name (user); + if (display_name != NULL) + return display_name; + + display_name = act_user_get_user_name (user); + if (display_name != NULL) + return display_name; + + /* Translators: this is the full name for an unknown user account. */ + return _("unknown"); +} + +static void +update_description (MctRestrictApplicationsDialog *self) +{ + g_autofree gchar *description = NULL; + + if (self->user == NULL) + { + gtk_widget_hide (GTK_WIDGET (self->description)); + return; + } + + /* Translators: the placeholder is a user’s full name */ + description = g_strdup_printf (_("Allow %s to use the following installed applications."), + get_user_display_name (self->user)); + gtk_label_set_text (self->description, description); + gtk_widget_show (GTK_WIDGET (self->description)); +} + +/** + * mct_restrict_applications_dialog_new: + * @app_filter: (transfer none): the initial app filter configuration to show + * @user: (transfer none) (nullable): the user to show the app filter for + * + * Create a new #MctRestrictApplicationsDialog widget. + * + * Returns: (transfer full): a new restricted applications editing dialog + * Since: 0.5.0 + */ +MctRestrictApplicationsDialog * +mct_restrict_applications_dialog_new (MctAppFilter *app_filter, + ActUser *user) +{ + g_return_val_if_fail (app_filter != NULL, NULL); + g_return_val_if_fail (user == NULL || ACT_IS_USER (user), NULL); + + return g_object_new (MCT_TYPE_RESTRICT_APPLICATIONS_DIALOG, + "app-filter", app_filter, + "user", user, + NULL); +} + +/** + * mct_restrict_applications_dialog_get_app_filter: + * @self: an #MctRestrictApplicationsDialog + * + * Get the value of #MctRestrictApplicationsDialog:app-filter. If the property + * was originally set to %NULL, this will be the empty app filter. + * + * Returns: (transfer none) (not nullable): the initial app filter used to + * populate the dialog + * Since: 0.5.0 + */ +MctAppFilter * +mct_restrict_applications_dialog_get_app_filter (MctRestrictApplicationsDialog *self) +{ + g_return_val_if_fail (MCT_IS_RESTRICT_APPLICATIONS_DIALOG (self), NULL); + + return self->app_filter; +} + +/** + * mct_restrict_applications_dialog_set_app_filter: + * @self: an #MctRestrictApplicationsDialog + * @app_filter: (nullable) (transfer none): the app filter to configure the dialog + * from, or %NULL to use an empty app filter + * + * Set the value of #MctRestrictApplicationsDialog:app-filter. + * + * Since: 0.5.0 + */ +void +mct_restrict_applications_dialog_set_app_filter (MctRestrictApplicationsDialog *self, + MctAppFilter *app_filter) +{ + g_autoptr(MctAppFilter) owned_app_filter = NULL; + + g_return_if_fail (MCT_IS_RESTRICT_APPLICATIONS_DIALOG (self)); + + /* Default app filter, typically for when we’re instantiated by #GtkBuilder. */ + if (app_filter == NULL) + { + g_auto(MctAppFilterBuilder) builder = MCT_APP_FILTER_BUILDER_INIT (); + owned_app_filter = mct_app_filter_builder_end (&builder); + app_filter = owned_app_filter; + } + + if (app_filter == self->app_filter) + return; + + g_clear_pointer (&self->app_filter, mct_app_filter_unref); + self->app_filter = mct_app_filter_ref (app_filter); + + mct_restrict_applications_selector_set_app_filter (self->selector, self->app_filter); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_APP_FILTER]); +} + +/** + * mct_restrict_applications_dialog_get_user: + * @self: an #MctRestrictApplicationsDialog + * + * Get the value of #MctRestrictApplicationsDialog:user. + * + * Returns: (transfer none) (nullable): the user the dialog is configured for, + * or %NULL if unknown + * Since: 0.5.0 + */ +ActUser * +mct_restrict_applications_dialog_get_user (MctRestrictApplicationsDialog *self) +{ + g_return_val_if_fail (MCT_IS_RESTRICT_APPLICATIONS_DIALOG (self), NULL); + + return self->user; +} + +/** + * mct_restrict_applications_dialog_set_user: + * @self: an #MctRestrictApplicationsDialog + * @user: (nullable) (transfer none): the user to configure the dialog for, + * or %NULL if unknown + * + * Set the value of #MctRestrictApplicationsDialog:user. + * + * Since: 0.5.0 + */ +void +mct_restrict_applications_dialog_set_user (MctRestrictApplicationsDialog *self, + ActUser *user) +{ + g_return_if_fail (MCT_IS_RESTRICT_APPLICATIONS_DIALOG (self)); + g_return_if_fail (user == NULL || ACT_IS_USER (user)); + + if (g_set_object (&self->user, user)) + { + update_description (self); + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_USER]); + } +} + +/** + * mct_restrict_applications_dialog_build_app_filter: + * @self: an #MctRestrictApplicationsDialog + * @builder: an existing #MctAppFilterBuilder to modify + * + * Get the app filter settings currently configured in the dialog, by modifying + * the given @builder. + * + * Typically this will be called in the handler for #GtkDialog::response. + * + * Since: 0.5.0 + */ +void +mct_restrict_applications_dialog_build_app_filter (MctRestrictApplicationsDialog *self, + MctAppFilterBuilder *builder) +{ + g_return_if_fail (MCT_IS_RESTRICT_APPLICATIONS_DIALOG (self)); + g_return_if_fail (builder != NULL); + + mct_restrict_applications_selector_build_app_filter (self->selector, builder); +} diff --git a/malcontent-control/restrict-applications-dialog.h b/malcontent-control/restrict-applications-dialog.h new file mode 100644 index 0000000..b4a5ed3 --- /dev/null +++ b/malcontent-control/restrict-applications-dialog.h @@ -0,0 +1,50 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright © 2020 Endless Mobile, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * + * Authors: + * - Philip Withnall + */ + +#pragma once + +#include +#include +#include +#include +#include + + +G_BEGIN_DECLS + +#define MCT_TYPE_RESTRICT_APPLICATIONS_DIALOG (mct_restrict_applications_dialog_get_type ()) +G_DECLARE_FINAL_TYPE (MctRestrictApplicationsDialog, mct_restrict_applications_dialog, MCT, RESTRICT_APPLICATIONS_DIALOG, GtkDialog) + +MctRestrictApplicationsDialog *mct_restrict_applications_dialog_new (MctAppFilter *app_filter, + ActUser *user); + +MctAppFilter *mct_restrict_applications_dialog_get_app_filter (MctRestrictApplicationsDialog *self); +void mct_restrict_applications_dialog_set_app_filter (MctRestrictApplicationsDialog *self, + MctAppFilter *app_filter); + +ActUser *mct_restrict_applications_dialog_get_user (MctRestrictApplicationsDialog *self); +void mct_restrict_applications_dialog_set_user (MctRestrictApplicationsDialog *self, + ActUser *user); + +void mct_restrict_applications_dialog_build_app_filter (MctRestrictApplicationsDialog *self, + MctAppFilterBuilder *builder); + +G_END_DECLS diff --git a/malcontent-control/restrict-applications-dialog.ui b/malcontent-control/restrict-applications-dialog.ui new file mode 100644 index 0000000..e95ff36 --- /dev/null +++ b/malcontent-control/restrict-applications-dialog.ui @@ -0,0 +1,56 @@ + + + + + + diff --git a/malcontent-control/restrict-applications-selector.c b/malcontent-control/restrict-applications-selector.c new file mode 100644 index 0000000..b6ddb83 --- /dev/null +++ b/malcontent-control/restrict-applications-selector.c @@ -0,0 +1,665 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright © 2020 Endless Mobile, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * + * Authors: + * - Philip Withnall + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "restrict-applications-selector.h" + + +#define WEB_BROWSERS_CONTENT_TYPE "x-scheme-handler/http" + +static void app_info_changed_cb (GAppInfoMonitor *monitor, + gpointer user_data); +static void reload_apps (MctRestrictApplicationsSelector *self); +static GtkWidget *create_row_for_app_cb (gpointer item, + gpointer user_data); + +/** + * MctRestrictApplicationsSelector: + * + * The ‘Restrict Applications’ selector is a list box which shows the available + * applications on the system alongside a column of toggle switches, which + * allows the given user to be prevented from running each application. + * + * The selector takes an #MctRestrictApplicationsSelector:app-filter as input + * to set up the UI, and returns its output as set of modifications to a given + * #MctAppFilterBuilder using + * mct_restrict_applications_selector_build_app_filter(). + * + * Since: 0.5.0 + */ +struct _MctRestrictApplicationsSelector +{ + GtkBox parent_instance; + + GtkListBox *listbox; + + GListStore *apps; /* (owned) */ + GAppInfoMonitor *app_info_monitor; /* (owned) */ + gulong app_info_monitor_changed_id; + GHashTable *blacklisted_apps; /* (owned) (element-type GAppInfo) */ + + MctAppFilter *app_filter; /* (owned) */ + + FlatpakInstallation *system_installation; /* (owned) */ + FlatpakInstallation *user_installation; /* (owned) */ +}; + +G_DEFINE_TYPE (MctRestrictApplicationsSelector, mct_restrict_applications_selector, GTK_TYPE_BOX) + +typedef enum +{ + PROP_APP_FILTER = 1, +} MctRestrictApplicationsSelectorProperty; + +static GParamSpec *properties[PROP_APP_FILTER + 1]; + +enum { + SIGNAL_CHANGED, +}; + +static guint signals[SIGNAL_CHANGED + 1]; + +static void +mct_restrict_applications_selector_constructed (GObject *obj) +{ + MctRestrictApplicationsSelector *self = MCT_RESTRICT_APPLICATIONS_SELECTOR (obj); + + /* Default app filter, typically for when we’re instantiated by #GtkBuilder. */ + if (self->app_filter == NULL) + { + g_auto(MctAppFilterBuilder) builder = MCT_APP_FILTER_BUILDER_INIT (); + self->app_filter = mct_app_filter_builder_end (&builder); + } + + g_assert (self->app_filter != NULL); + + G_OBJECT_CLASS (mct_restrict_applications_selector_parent_class)->constructed (obj); +} + +static void +mct_restrict_applications_selector_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + MctRestrictApplicationsSelector *self = MCT_RESTRICT_APPLICATIONS_SELECTOR (object); + + switch ((MctRestrictApplicationsSelectorProperty) prop_id) + { + case PROP_APP_FILTER: + g_value_set_boxed (value, self->app_filter); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +mct_restrict_applications_selector_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + MctRestrictApplicationsSelector *self = MCT_RESTRICT_APPLICATIONS_SELECTOR (object); + + switch ((MctRestrictApplicationsSelectorProperty) prop_id) + { + case PROP_APP_FILTER: + mct_restrict_applications_selector_set_app_filter (self, g_value_get_boxed (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +mct_restrict_applications_selector_dispose (GObject *object) +{ + MctRestrictApplicationsSelector *self = (MctRestrictApplicationsSelector *)object; + + g_clear_pointer (&self->blacklisted_apps, g_hash_table_unref); + g_clear_object (&self->apps); + + if (self->app_info_monitor != NULL && self->app_info_monitor_changed_id != 0) + { + g_signal_handler_disconnect (self->app_info_monitor, self->app_info_monitor_changed_id); + self->app_info_monitor_changed_id = 0; + } + g_clear_object (&self->app_info_monitor); + g_clear_pointer (&self->app_filter, mct_app_filter_unref); + g_clear_object (&self->system_installation); + g_clear_object (&self->user_installation); + + G_OBJECT_CLASS (mct_restrict_applications_selector_parent_class)->dispose (object); +} + +static void +mct_restrict_applications_selector_class_init (MctRestrictApplicationsSelectorClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->constructed = mct_restrict_applications_selector_constructed; + object_class->get_property = mct_restrict_applications_selector_get_property; + object_class->set_property = mct_restrict_applications_selector_set_property; + object_class->dispose = mct_restrict_applications_selector_dispose; + + /** + * MctRestrictApplicationsSelector:app-filter: (not nullable) + * + * The user’s current app filter, used to set up the selector. As app filters + * are immutable, it is not updated as the selector is changed. Use + * mct_restrict_applications_selector_build_app_filter() to build the new app + * filter. + * + * Since: 0.5.0 + */ + properties[PROP_APP_FILTER] = + g_param_spec_boxed ("app-filter", + "App Filter", + "The user’s current app filter, used to set up the selector.", + MCT_TYPE_APP_FILTER, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS | + G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, G_N_ELEMENTS (properties), properties); + + /** + * MctRestrictApplicationsSelector::changed: + * + * Emitted whenever an application in the list is blocked or unblocked. + * + * Since: 0.5.0 + */ + signals[SIGNAL_CHANGED] = + g_signal_new ("changed", + MCT_TYPE_RESTRICT_APPLICATIONS_SELECTOR, + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/freedesktop/MalcontentControl/ui/restrict-applications-selector.ui"); + + gtk_widget_class_bind_template_child (widget_class, MctRestrictApplicationsSelector, listbox); +} + +static void +mct_restrict_applications_selector_init (MctRestrictApplicationsSelector *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + self->apps = g_list_store_new (G_TYPE_APP_INFO); + + self->app_info_monitor = g_app_info_monitor_get (); + self->app_info_monitor_changed_id = + g_signal_connect (self->app_info_monitor, "changed", + (GCallback) app_info_changed_cb, self); + + gtk_list_box_bind_model (self->listbox, + G_LIST_MODEL (self->apps), + create_row_for_app_cb, + self, + NULL); + + self->blacklisted_apps = g_hash_table_new_full (g_direct_hash, + g_direct_equal, + g_object_unref, + NULL); + + self->system_installation = flatpak_installation_new_system (NULL, NULL); + self->user_installation = flatpak_installation_new_user (NULL, NULL); +} + +static void +on_switch_active_changed_cb (GtkSwitch *s, + GParamSpec *pspec, + gpointer user_data) +{ + MctRestrictApplicationsSelector *self = MCT_RESTRICT_APPLICATIONS_SELECTOR (user_data); + GAppInfo *app; + gboolean allowed; + + app = g_object_get_data (G_OBJECT (s), "GAppInfo"); + allowed = gtk_switch_get_active (s); + + if (allowed) + { + gboolean removed; + + g_debug ("Removing ‘%s’ from blacklisted apps", g_app_info_get_id (app)); + + removed = g_hash_table_remove (self->blacklisted_apps, app); + g_assert (removed); + } + else + { + gboolean added; + + g_debug ("Blacklisting ‘%s’", g_app_info_get_id (app)); + + added = g_hash_table_add (self->blacklisted_apps, g_object_ref (app)); + g_assert (added); + } + + g_signal_emit (self, signals[SIGNAL_CHANGED], 0); +} + +static GtkWidget * +create_row_for_app_cb (gpointer item, + gpointer user_data) +{ + MctRestrictApplicationsSelector *self = MCT_RESTRICT_APPLICATIONS_SELECTOR (user_data); + GAppInfo *app = G_APP_INFO (item); + g_autoptr(GIcon) icon = NULL; + GtkWidget *box, *w; + gboolean allowed; + const gchar *app_name; + gint size; + + app_name = g_app_info_get_name (app); + + g_assert (G_IS_DESKTOP_APP_INFO (app)); + + icon = g_app_info_get_icon (app); + if (icon == NULL) + icon = g_themed_icon_new ("application-x-executable"); + else + g_object_ref (icon); + + box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 12); + gtk_container_set_border_width (GTK_CONTAINER (box), 12); + gtk_widget_set_margin_end (box, 12); + + /* Icon */ + w = gtk_image_new_from_gicon (icon, GTK_ICON_SIZE_DIALOG); + gtk_icon_size_lookup (GTK_ICON_SIZE_DND, &size, NULL); + gtk_image_set_pixel_size (GTK_IMAGE (w), size); + gtk_container_add (GTK_CONTAINER (box), w); + + /* App name label */ + w = g_object_new (GTK_TYPE_LABEL, + "label", app_name, + "hexpand", TRUE, + "xalign", 0.0, + NULL); + gtk_container_add (GTK_CONTAINER (box), w); + + /* Switch */ + w = g_object_new (GTK_TYPE_SWITCH, + "valign", GTK_ALIGN_CENTER, + NULL); + gtk_container_add (GTK_CONTAINER (box), w); + + gtk_widget_show_all (box); + + /* Fetch status from AccountService */ + allowed = mct_app_filter_is_appinfo_allowed (self->app_filter, app); + + gtk_switch_set_active (GTK_SWITCH (w), allowed); + g_object_set_data_full (G_OBJECT (w), "GAppInfo", g_object_ref (app), g_object_unref); + + if (allowed) + g_hash_table_remove (self->blacklisted_apps, app); + else + g_hash_table_add (self->blacklisted_apps, g_object_ref (app)); + + g_signal_connect (w, "notify::active", G_CALLBACK (on_switch_active_changed_cb), self); + + return box; +} + +static gint +compare_app_info_cb (gconstpointer a, + gconstpointer b, + gpointer user_data) +{ + GAppInfo *app_a = (GAppInfo*) a; + GAppInfo *app_b = (GAppInfo*) b; + + return g_utf8_collate (g_app_info_get_display_name (app_a), + g_app_info_get_display_name (app_b)); +} + +static gint +app_compare_id_length_cb (gconstpointer a, + gconstpointer b) +{ + GAppInfo *info_a = (GAppInfo *) a, *info_b = (GAppInfo *) b; + const gchar *id_a, *id_b; + + id_a = g_app_info_get_id (info_a); + id_b = g_app_info_get_id (info_b); + + if (id_a == NULL && id_b == NULL) + return 0; + else if (id_a == NULL) + return -1; + else if (id_b == NULL) + return 1; + + return strlen (id_a) - strlen (id_b); +} + +static void +reload_apps (MctRestrictApplicationsSelector *self) +{ + GList *iter, *apps; + g_autoptr(GHashTable) seen_flatpak_ids = NULL; + g_autoptr(GHashTable) seen_executables = NULL; + + apps = g_app_info_get_all (); + + /* Sort the apps by increasing length of #GAppInfo ID. When coupled with the + * deduplication of flatpak IDs and executable paths, below, this should ensure that we + * pick the ‘base’ app out of any set with matching prefixes and identical app IDs (in + * case of flatpak apps) or executables (for non-flatpak apps), and show only that. + * + * This is designed to avoid listing all the components of LibreOffice for example, + * which all share an app ID and hence have the same entry in the parental controls + * app filter. */ + apps = g_list_sort (apps, app_compare_id_length_cb); + seen_flatpak_ids = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + seen_executables = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + + g_list_store_remove_all (self->apps); + + for (iter = apps; iter; iter = iter->next) + { + GAppInfo *app; + const gchar *app_name; + const gchar * const *supported_types; + + app = iter->data; + app_name = g_app_info_get_name (app); + + supported_types = g_app_info_get_supported_types (app); + + if (!G_IS_DESKTOP_APP_INFO (app) || + !g_app_info_should_show (app) || + app_name[0] == '\0' || + /* Endless' link apps have the "eos-link" prefix, and should be ignored too */ + g_str_has_prefix (g_app_info_get_id (app), "eos-link") || + /* FIXME: Only list flatpak apps and apps with X-Parental-Controls + * key set for now; we really need a system-wide MAC to be able to + * reliably support blacklisting system programs. */ + (!g_desktop_app_info_has_key (G_DESKTOP_APP_INFO (app), "X-Flatpak") && + !g_desktop_app_info_has_key (G_DESKTOP_APP_INFO (app), "X-Parental-Controls")) || + /* Web browsers are special cased */ + (supported_types && g_strv_contains (supported_types, WEB_BROWSERS_CONTENT_TYPE))) + { + continue; + } + + if (g_desktop_app_info_has_key (G_DESKTOP_APP_INFO (app), "X-Flatpak")) + { + g_autofree gchar *flatpak_id = NULL; + + flatpak_id = g_desktop_app_info_get_string (G_DESKTOP_APP_INFO (app), "X-Flatpak"); + g_debug ("Processing app ‘%s’ (Exec=%s, X-Flatpak=%s)", + g_app_info_get_id (app), + g_app_info_get_executable (app), + flatpak_id); + + /* Have we seen this flatpak ID before? */ + if (!g_hash_table_add (seen_flatpak_ids, g_steal_pointer (&flatpak_id))) + { + g_debug (" → Skipping ‘%s’ due to seeing its flatpak ID already", + g_app_info_get_id (app)); + continue; + } + } + else if (g_desktop_app_info_has_key (G_DESKTOP_APP_INFO (app), "X-Parental-Controls")) + { + g_autofree gchar *parental_controls_type = NULL; + g_autofree gchar *executable = NULL; + + parental_controls_type = g_desktop_app_info_get_string (G_DESKTOP_APP_INFO (app), + "X-Parental-Controls"); + /* Ignore X-Parental-Controls=none */ + if (g_strcmp0 (parental_controls_type, "none") == 0) + continue; + + executable = g_strdup (g_app_info_get_executable (app)); + g_debug ("Processing app ‘%s’ (Exec=%s, X-Parental-Controls=%s)", + g_app_info_get_id (app), + executable, + parental_controls_type); + + /* Have we seen this executable before? */ + if (!g_hash_table_add (seen_executables, g_steal_pointer (&executable))) + { + g_debug (" → Skipping ‘%s’ due to seeing its executable already", + g_app_info_get_id (app)); + continue; + } + } + + g_list_store_insert_sorted (self->apps, + app, + compare_app_info_cb, + self); + } + + g_list_free_full (apps, g_object_unref); +} + +static void +app_info_changed_cb (GAppInfoMonitor *monitor, + gpointer user_data) +{ + MctRestrictApplicationsSelector *self = MCT_RESTRICT_APPLICATIONS_SELECTOR (user_data); + + reload_apps (self); +} + +/* Will return %NULL if @flatpak_id is not installed. */ +static gchar * +get_flatpak_ref_for_app_id (MctRestrictApplicationsSelector *self, + const gchar *flatpak_id, + GCancellable *cancellable) +{ + g_autoptr(FlatpakInstalledRef) ref = NULL; + g_autoptr(GError) local_error = NULL; + + g_assert (self->system_installation != NULL); + g_assert (self->user_installation != NULL); + + /* FIXME technically this does local file I/O and should be async */ + ref = flatpak_installation_get_current_installed_app (self->user_installation, + flatpak_id, + cancellable, + &local_error); + + if (local_error != NULL && + !g_error_matches (local_error, FLATPAK_ERROR, FLATPAK_ERROR_NOT_INSTALLED)) + { + g_warning ("Error searching for Flatpak ref: %s", local_error->message); + return NULL; + } + + g_clear_error (&local_error); + + if (!ref || !flatpak_installed_ref_get_is_current (ref)) + { + /* FIXME technically this does local file I/O and should be async */ + ref = flatpak_installation_get_current_installed_app (self->system_installation, + flatpak_id, + cancellable, + &local_error); + if (local_error != NULL) + { + if (!g_error_matches (local_error, FLATPAK_ERROR, FLATPAK_ERROR_NOT_INSTALLED)) + g_warning ("Error searching for Flatpak ref: %s", local_error->message); + return NULL; + } + } + + return flatpak_ref_format_ref (FLATPAK_REF (ref)); +} + +/** + * mct_restrict_applications_selector_new: + * @app_filter: (transfer none): app filter to configure the selector from initially + * + * Create a new #MctRestrictApplicationsSelector widget. + * + * Returns: (transfer full): a new restricted applications selector + * Since: 0.5.0 + */ +MctRestrictApplicationsSelector * +mct_restrict_applications_selector_new (MctAppFilter *app_filter) +{ + g_return_val_if_fail (app_filter != NULL, NULL); + + return g_object_new (MCT_TYPE_RESTRICT_APPLICATIONS_SELECTOR, + "app-filter", app_filter, + NULL); +} + +/** + * mct_restrict_applications_selector_build_app_filter: + * @self: an #MctRestrictApplicationsSelector + * @builder: an existing #MctAppFilterBuilder to modify + * + * Get the app filter settings currently configured in the selector, by modifying + * the given @builder. + * + * Since: 0.5.0 + */ +void +mct_restrict_applications_selector_build_app_filter (MctRestrictApplicationsSelector *self, + MctAppFilterBuilder *builder) +{ + GDesktopAppInfo *app; + GHashTableIter iter; + + g_return_if_fail (MCT_IS_RESTRICT_APPLICATIONS_SELECTOR (self)); + g_return_if_fail (builder != NULL); + + g_hash_table_iter_init (&iter, self->blacklisted_apps); + while (g_hash_table_iter_next (&iter, (gpointer) &app, NULL)) + { + g_autofree gchar *flatpak_id = NULL; + + flatpak_id = g_desktop_app_info_get_string (app, "X-Flatpak"); + if (flatpak_id) + flatpak_id = g_strstrip (flatpak_id); + + if (flatpak_id) + { + g_autofree gchar *flatpak_ref = get_flatpak_ref_for_app_id (self, flatpak_id, NULL); + + if (!flatpak_ref) + { + g_warning ("Skipping blacklisting Flatpak ID ‘%s’ due to it not being installed", flatpak_id); + continue; + } + + g_debug ("\t\t → Blacklisting Flatpak ref: %s", flatpak_ref); + mct_app_filter_builder_blacklist_flatpak_ref (builder, flatpak_ref); + } + else + { + const gchar *executable = g_app_info_get_executable (G_APP_INFO (app)); + g_autofree gchar *path = g_find_program_in_path (executable); + + if (!path) + { + g_warning ("Skipping blacklisting executable ‘%s’ due to it not being found", executable); + continue; + } + + g_debug ("\t\t → Blacklisting path: %s", path); + mct_app_filter_builder_blacklist_path (builder, path); + } + } +} + +/** + * mct_restrict_applications_selector_get_app_filter: + * @self: an #MctRestrictApplicationsSelector + * + * Get the value of #MctRestrictApplicationsSelector:app-filter. If the property + * was originally set to %NULL, this will be the empty app filter. + * + * Returns: (transfer none) (not nullable): the initial app filter used to + * populate the selector + * Since: 0.5.0 + */ +MctAppFilter * +mct_restrict_applications_selector_get_app_filter (MctRestrictApplicationsSelector *self) +{ + g_return_val_if_fail (MCT_IS_RESTRICT_APPLICATIONS_SELECTOR (self), NULL); + + return self->app_filter; +} + +/** + * mct_restrict_applications_selector_set_app_filter: + * @self: an #MctRestrictApplicationsSelector + * @app_filter: (nullable) (transfer none): the app filter to configure the selector + * from, or %NULL to use an empty app filter + * + * Set the value of #MctRestrictApplicationsSelector:app-filter. + * + * This will overwrite any user changes to the selector, so they should be saved + * first using mct_restrict_applications_selector_build_app_filter() if desired. + * + * Since: 0.5.0 + */ +void +mct_restrict_applications_selector_set_app_filter (MctRestrictApplicationsSelector *self, + MctAppFilter *app_filter) +{ + g_autoptr(MctAppFilter) owned_app_filter = NULL; + + g_return_if_fail (MCT_IS_RESTRICT_APPLICATIONS_SELECTOR (self)); + + /* Default app filter, typically for when we’re instantiated by #GtkBuilder. */ + if (app_filter == NULL) + { + g_auto(MctAppFilterBuilder) builder = MCT_APP_FILTER_BUILDER_INIT (); + owned_app_filter = mct_app_filter_builder_end (&builder); + app_filter = owned_app_filter; + } + + if (app_filter == self->app_filter) + return; + + g_clear_pointer (&self->app_filter, mct_app_filter_unref); + self->app_filter = mct_app_filter_ref (app_filter); + + reload_apps (self); + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_APP_FILTER]); +} diff --git a/malcontent-control/restrict-applications-selector.h b/malcontent-control/restrict-applications-selector.h new file mode 100644 index 0000000..702a594 --- /dev/null +++ b/malcontent-control/restrict-applications-selector.h @@ -0,0 +1,45 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright © 2020 Endless Mobile, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * + * Authors: + * - Philip Withnall + */ + +#pragma once + +#include +#include +#include +#include +#include + + +G_BEGIN_DECLS + +#define MCT_TYPE_RESTRICT_APPLICATIONS_SELECTOR (mct_restrict_applications_selector_get_type ()) +G_DECLARE_FINAL_TYPE (MctRestrictApplicationsSelector, mct_restrict_applications_selector, MCT, RESTRICT_APPLICATIONS_SELECTOR, GtkBox) + +MctRestrictApplicationsSelector *mct_restrict_applications_selector_new (MctAppFilter *app_filter); + +MctAppFilter *mct_restrict_applications_selector_get_app_filter (MctRestrictApplicationsSelector *self); +void mct_restrict_applications_selector_set_app_filter (MctRestrictApplicationsSelector *self, + MctAppFilter *app_filter); + +void mct_restrict_applications_selector_build_app_filter (MctRestrictApplicationsSelector *self, + MctAppFilterBuilder *builder); + +G_END_DECLS diff --git a/malcontent-control/restrict-applications-selector.ui b/malcontent-control/restrict-applications-selector.ui new file mode 100644 index 0000000..6b5bdd9 --- /dev/null +++ b/malcontent-control/restrict-applications-selector.ui @@ -0,0 +1,32 @@ + + + + + + diff --git a/malcontent-control/user-controls.c b/malcontent-control/user-controls.c index 5cf6cae..1cc07a9 100644 --- a/malcontent-control/user-controls.c +++ b/malcontent-control/user-controls.c @@ -28,6 +28,7 @@ #include #include "gs-content-rating.h" +#include "restrict-applications-dialog.h" #include "user-controls.h" @@ -44,12 +45,9 @@ struct _MctUserControls GtkSwitch *allow_system_installation_switch; GtkSwitch *allow_user_installation_switch; GtkSwitch *allow_web_browsers_switch; - GtkListBox *listbox; GtkButton *restriction_button; GtkPopover *restriction_popover; - - FlatpakInstallation *system_installation; /* (owned) */ - FlatpakInstallation *user_installation; /* (owned) */ + MctRestrictApplicationsDialog *restrict_applications_dialog; GSimpleActionGroup *action_group; /* (owned) */ @@ -58,11 +56,6 @@ struct _MctUserControls GPermission *permission; /* (owned) (nullable) */ gulong permission_allowed_id; - GAppInfoMonitor *app_info_monitor; /* (owned) */ - - GHashTable *blacklisted_apps; /* (owned) */ - GListStore *apps; /* (owned) */ - GCancellable *cancellable; /* (owned) */ MctManager *manager; /* (owned) */ MctAppFilter *filter; /* (owned) */ @@ -72,12 +65,6 @@ struct _MctUserControls }; static gboolean blacklist_apps_cb (gpointer data); -static void app_info_changed_cb (GAppInfoMonitor *monitor, - gpointer user_data); - -static gint compare_app_info_cb (gconstpointer a, - gconstpointer b, - gpointer user_data); static void on_allow_installation_switch_active_changed_cb (GtkSwitch *s, GParamSpec *pspec, @@ -87,6 +74,17 @@ static void on_allow_web_browsers_switch_active_changed_cb (GtkSwitch *s, GParamSpec *pspec, MctUserControls *self); +static void on_restrict_applications_button_clicked_cb (GtkButton *button, + gpointer user_data); + +static gboolean on_restrict_applications_dialog_delete_event_cb (GtkWidget *widget, + GdkEvent *event, + gpointer user_data); + +static void on_restrict_applications_dialog_response_cb (GtkDialog *dialog, + gint response_id, + gpointer user_data); + static void on_set_age_action_activated (GSimpleAction *action, GVariant *param, gpointer user_data); @@ -146,139 +144,6 @@ static const gchar * const oars_categories[] = /* Auxiliary methods */ -static gint -app_compare_id_length_cb (gconstpointer a, - gconstpointer b) -{ - GAppInfo *info_a = (GAppInfo *) a, *info_b = (GAppInfo *) b; - const gchar *id_a, *id_b; - - id_a = g_app_info_get_id (info_a); - id_b = g_app_info_get_id (info_b); - - if (id_a == NULL && id_b == NULL) - return 0; - else if (id_a == NULL) - return -1; - else if (id_b == NULL) - return 1; - - return strlen (id_a) - strlen (id_b); -} - -static void -reload_apps (MctUserControls *self) -{ - GList *iter, *apps; - g_autoptr(GHashTable) seen_flatpak_ids = NULL; - g_autoptr(GHashTable) seen_executables = NULL; - - apps = g_app_info_get_all (); - - /* Sort the apps by increasing length of #GAppInfo ID. When coupled with the - * deduplication of flatpak IDs and executable paths, below, this should ensure that we - * pick the ‘base’ app out of any set with matching prefixes and identical app IDs (in - * case of flatpak apps) or executables (for non-flatpak apps), and show only that. - * - * This is designed to avoid listing all the components of LibreOffice for example, - * which all share an app ID and hence have the same entry in the parental controls - * app filter. */ - apps = g_list_sort (apps, app_compare_id_length_cb); - seen_flatpak_ids = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); - seen_executables = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); - - g_list_store_remove_all (self->apps); - - for (iter = apps; iter; iter = iter->next) - { - GAppInfo *app; - const gchar *app_name; - const gchar * const *supported_types; - - app = iter->data; - app_name = g_app_info_get_name (app); - - supported_types = g_app_info_get_supported_types (app); - - if (!G_IS_DESKTOP_APP_INFO (app) || - !g_app_info_should_show (app) || - app_name[0] == '\0' || - /* Endless' link apps have the "eos-link" prefix, and should be ignored too */ - g_str_has_prefix (g_app_info_get_id (app), "eos-link") || - /* FIXME: Only list flatpak apps and apps with X-Parental-Controls - * key set for now; we really need a system-wide MAC to be able to - * reliably support blacklisting system programs. See - * https://phabricator.endlessm.com/T25080. */ - (!g_desktop_app_info_has_key (G_DESKTOP_APP_INFO (app), "X-Flatpak") && - !g_desktop_app_info_has_key (G_DESKTOP_APP_INFO (app), "X-Parental-Controls")) || - /* Web browsers are special cased */ - (supported_types && g_strv_contains (supported_types, WEB_BROWSERS_CONTENT_TYPE))) - { - continue; - } - - if (g_desktop_app_info_has_key (G_DESKTOP_APP_INFO (app), "X-Flatpak")) - { - g_autofree gchar *flatpak_id = NULL; - - flatpak_id = g_desktop_app_info_get_string (G_DESKTOP_APP_INFO (app), "X-Flatpak"); - g_debug ("Processing app ‘%s’ (Exec=%s, X-Flatpak=%s)", - g_app_info_get_id (app), - g_app_info_get_executable (app), - flatpak_id); - - /* Have we seen this flatpak ID before? */ - if (!g_hash_table_add (seen_flatpak_ids, g_steal_pointer (&flatpak_id))) - { - g_debug (" → Skipping ‘%s’ due to seeing its flatpak ID already", - g_app_info_get_id (app)); - continue; - } - } - else if (g_desktop_app_info_has_key (G_DESKTOP_APP_INFO (app), "X-Parental-Controls")) - { - g_autofree gchar *parental_controls_type = NULL; - g_autofree gchar *executable = NULL; - - parental_controls_type = g_desktop_app_info_get_string (G_DESKTOP_APP_INFO (app), - "X-Parental-Controls"); - /* Ignore X-Parental-Controls=none */ - if (g_strcmp0 (parental_controls_type, "none") == 0) - continue; - - executable = g_strdup (g_app_info_get_executable (app)); - g_debug ("Processing app ‘%s’ (Exec=%s, X-Parental-Controls=%s)", - g_app_info_get_id (app), - executable, - parental_controls_type); - - /* Have we seen this executable before? */ - if (!g_hash_table_add (seen_executables, g_steal_pointer (&executable))) - { - g_debug (" → Skipping ‘%s’ due to seeing its executable already", - g_app_info_get_id (app)); - continue; - } - } - - g_list_store_insert_sorted (self->apps, - app, - compare_app_info_cb, - self); - } - - g_list_free_full (apps, g_object_unref); -} - -static void -app_info_changed_cb (GAppInfoMonitor *monitor, - gpointer user_data) -{ - MctUserControls *self = MCT_USER_CONTROLS (user_data); - - reload_apps (self); -} - static GsContentRatingSystem get_content_rating_system (ActUser *user) { @@ -320,6 +185,18 @@ update_app_filter (MctUserControls *self) g_clear_pointer (&self->filter, mct_app_filter_unref); + if (self->user == NULL) + return; + + /* FIXME: It’s expected that, unless authorised already, a user cannot read + * another user’s app filter. accounts-service currently (incorrectly) ignores + * the missing ‘interactive’ flag and prompts the user for permission if so, + * so don’t query at all in that case. */ + if (act_user_get_uid (self->user) != getuid () && + (self->permission == NULL || + !g_permission_get_allowed (self->permission))) + return; + /* FIXME: make it asynchronous */ self->filter = mct_manager_get_app_filter (self->manager, act_user_get_uid (self->user), @@ -329,26 +206,9 @@ update_app_filter (MctUserControls *self) if (error) { - /* It's expected that a non-admin user can't read another user's parental - * controls info unless the panel has been unlocked; ignore such an - * error. - */ - if (act_user_get_uid (self->user) != getuid () && - self->permission != NULL && - !g_permission_get_allowed (self->permission) && - g_error_matches (error, MCT_MANAGER_ERROR, MCT_MANAGER_ERROR_PERMISSION_DENIED)) - { - g_clear_error (&error); - g_debug ("Not enough permissions to retrieve app filter for user '%s'", - act_user_get_user_name (self->user)); - } - else - { - g_warning ("Error retrieving app filter for user '%s': %s", - act_user_get_user_name (self->user), - error->message); - } - + g_warning ("Error retrieving app filter for user '%s': %s", + act_user_get_user_name (self->user), + error->message); return; } @@ -551,55 +411,10 @@ setup_parental_control_settings (MctUserControls *self) gtk_widget_set_sensitive (GTK_WIDGET (self), is_authorized); - g_hash_table_remove_all (self->blacklisted_apps); - update_oars_level (self); update_categories_from_language (self); update_allow_app_installation (self); update_allow_web_browsers (self); - reload_apps (self); -} - -/* Will return %NULL if @flatpak_id is not installed. */ -static gchar * -get_flatpak_ref_for_app_id (MctUserControls *self, - const gchar *flatpak_id) -{ - g_autoptr(FlatpakInstalledRef) ref = NULL; - g_autoptr(GError) error = NULL; - - g_assert (self->system_installation != NULL); - g_assert (self->user_installation != NULL); - - ref = flatpak_installation_get_current_installed_app (self->user_installation, - flatpak_id, - self->cancellable, - &error); - - if (error && - !g_error_matches (error, FLATPAK_ERROR, FLATPAK_ERROR_NOT_INSTALLED)) - { - g_warning ("Error searching for Flatpak ref: %s", error->message); - return NULL; - } - - g_clear_error (&error); - - if (!ref || !flatpak_installed_ref_get_is_current (ref)) - { - ref = flatpak_installation_get_current_installed_app (self->system_installation, - flatpak_id, - self->cancellable, - &error); - if (error) - { - if (!g_error_matches (error, FLATPAK_ERROR, FLATPAK_ERROR_NOT_INSTALLED)) - g_warning ("Error searching for Flatpak ref: %s", error->message); - return NULL; - } - } - - return flatpak_ref_format_ref (FLATPAK_REF (ref)); } /* Callbacks */ @@ -611,8 +426,6 @@ blacklist_apps_cb (gpointer data) g_autoptr(MctAppFilter) new_filter = NULL; g_autoptr(GError) error = NULL; MctUserControls *self = data; - GDesktopAppInfo *app; - GHashTableIter iter; gboolean allow_web_browsers; gsize i; @@ -624,43 +437,7 @@ blacklist_apps_cb (gpointer data) g_debug ("\t → Blacklisting apps"); - g_hash_table_iter_init (&iter, self->blacklisted_apps); - while (g_hash_table_iter_next (&iter, (gpointer) &app, NULL)) - { - g_autofree gchar *flatpak_id = NULL; - - flatpak_id = g_desktop_app_info_get_string (app, "X-Flatpak"); - if (flatpak_id) - flatpak_id = g_strstrip (flatpak_id); - - if (flatpak_id) - { - g_autofree gchar *flatpak_ref = get_flatpak_ref_for_app_id (self, flatpak_id); - - if (!flatpak_ref) - { - g_warning ("Skipping blacklisting Flatpak ID ‘%s’ due to it not being installed", flatpak_id); - continue; - } - - g_debug ("\t\t → Blacklisting Flatpak ref: %s", flatpak_ref); - mct_app_filter_builder_blacklist_flatpak_ref (&builder, flatpak_ref); - } - else - { - const gchar *executable = g_app_info_get_executable (G_APP_INFO (app)); - g_autofree gchar *path = g_find_program_in_path (executable); - - if (!path) - { - g_warning ("Skipping blacklisting executable ‘%s’ due to it not being found", executable); - continue; - } - - g_debug ("\t\t → Blacklisting path: %s", path); - mct_app_filter_builder_blacklist_path (&builder, path); - } - } + mct_restrict_applications_dialog_build_app_filter (self->restrict_applications_dialog, &builder); /* Maturity level */ @@ -758,114 +535,51 @@ on_allow_web_browsers_switch_active_changed_cb (GtkSwitch *s, } static void -on_switch_active_changed_cb (GtkSwitch *s, - GParamSpec *pspec, - MctUserControls *self) +on_restrict_applications_button_clicked_cb (GtkButton *button, + gpointer user_data) { - GAppInfo *app; - gboolean allowed; + MctUserControls *self = MCT_USER_CONTROLS (user_data); + GtkWidget *toplevel; - app = g_object_get_data (G_OBJECT (s), "GAppInfo"); - allowed = gtk_switch_get_active (s); + /* Show the restrict applications dialogue modally, making sure to update its + * state first. */ + toplevel = gtk_widget_get_toplevel (GTK_WIDGET (self)); + if (GTK_IS_WINDOW (toplevel)) + gtk_window_set_transient_for (GTK_WINDOW (self->restrict_applications_dialog), + GTK_WINDOW (toplevel)); - if (allowed) - { - gboolean removed; + mct_restrict_applications_dialog_set_user (self->restrict_applications_dialog, self->user); + mct_restrict_applications_dialog_set_app_filter (self->restrict_applications_dialog, self->filter); - g_debug ("Removing '%s' from blacklisted apps", g_app_info_get_id (app)); + gtk_widget_show (GTK_WIDGET (self->restrict_applications_dialog)); +} - removed = g_hash_table_remove (self->blacklisted_apps, app); - g_assert (removed); - } - else - { - gboolean added; +static gboolean +on_restrict_applications_dialog_delete_event_cb (GtkWidget *widget, + GdkEvent *event, + gpointer user_data) +{ + MctUserControls *self = MCT_USER_CONTROLS (user_data); - g_debug ("Blacklisting '%s'", g_app_info_get_id (app)); - - added = g_hash_table_add (self->blacklisted_apps, g_object_ref (app)); - g_assert (added); - } + /* When the ‘Restrict Applications’ dialogue is closed, don’t destroy it, + * since it contains the app filter settings which we’ll want to reuse next + * time the dialogue is shown or the app filter is saved. */ + gtk_widget_hide (GTK_WIDGET (self->restrict_applications_dialog)); + /* Schedule an update to the saved state. */ schedule_update_blacklisted_apps (self); + + return TRUE; } -static GtkWidget * -create_row_for_app_cb (gpointer item, - gpointer user_data) +static void +on_restrict_applications_dialog_response_cb (GtkDialog *dialog, + gint response_id, + gpointer user_data) { - g_autoptr(GIcon) icon = NULL; - MctUserControls *self; - GtkWidget *box, *w; - GAppInfo *app; - gboolean allowed; - const gchar *app_name; - gint size; + MctUserControls *self = MCT_USER_CONTROLS (user_data); - self = MCT_USER_CONTROLS (user_data); - app = item; - app_name = g_app_info_get_name (app); - - g_assert (G_IS_DESKTOP_APP_INFO (app)); - - icon = g_app_info_get_icon (app); - if (icon == NULL) - icon = g_themed_icon_new ("application-x-executable"); - else - g_object_ref (icon); - - box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 12); - gtk_container_set_border_width (GTK_CONTAINER (box), 12); - gtk_widget_set_margin_end (box, 12); - - /* Icon */ - w = gtk_image_new_from_gicon (icon, GTK_ICON_SIZE_DIALOG); - gtk_icon_size_lookup (GTK_ICON_SIZE_DND, &size, NULL); - gtk_image_set_pixel_size (GTK_IMAGE (w), size); - gtk_container_add (GTK_CONTAINER (box), w); - - /* App name label */ - w = g_object_new (GTK_TYPE_LABEL, - "label", app_name, - "hexpand", TRUE, - "xalign", 0.0, - NULL); - gtk_container_add (GTK_CONTAINER (box), w); - - /* Switch */ - w = g_object_new (GTK_TYPE_SWITCH, - "valign", GTK_ALIGN_CENTER, - NULL); - gtk_container_add (GTK_CONTAINER (box), w); - - gtk_widget_show_all (box); - - /* Fetch status from AccountService */ - allowed = mct_app_filter_is_appinfo_allowed (self->filter, app); - - gtk_switch_set_active (GTK_SWITCH (w), allowed); - g_object_set_data_full (G_OBJECT (w), "GAppInfo", g_object_ref (app), g_object_unref); - - if (allowed) - g_hash_table_remove (self->blacklisted_apps, app); - else if (!allowed) - g_hash_table_add (self->blacklisted_apps, g_object_ref (app)); - - g_signal_connect (w, "notify::active", G_CALLBACK (on_switch_active_changed_cb), self); - - return box; -} - -static gint -compare_app_info_cb (gconstpointer a, - gconstpointer b, - gpointer user_data) -{ - GAppInfo *app_a = (GAppInfo*) a; - GAppInfo *app_b = (GAppInfo*) b; - - return g_utf8_collate (g_app_info_get_display_name (app_a), - g_app_info_get_display_name (app_b)); + on_restrict_applications_dialog_delete_event_cb (GTK_WIDGET (dialog), NULL, self); } static void @@ -923,11 +637,8 @@ mct_user_controls_finalize (GObject *object) g_cancellable_cancel (self->cancellable); g_clear_object (&self->action_group); - g_clear_object (&self->apps); g_clear_object (&self->cancellable); - g_clear_object (&self->system_installation); g_clear_object (&self->user); - g_clear_object (&self->user_installation); if (self->permission != NULL && self->permission_allowed_id != 0) { @@ -936,10 +647,8 @@ mct_user_controls_finalize (GObject *object) } g_clear_object (&self->permission); - g_clear_pointer (&self->blacklisted_apps, g_hash_table_unref); g_clear_pointer (&self->filter, mct_app_filter_unref); g_clear_object (&self->manager); - g_clear_object (&self->app_info_monitor); G_OBJECT_CLASS (mct_user_controls_parent_class)->finalize (object); } @@ -1038,10 +747,13 @@ mct_user_controls_class_init (MctUserControlsClass *klass) gtk_widget_class_bind_template_child (widget_class, MctUserControls, allow_web_browsers_switch); gtk_widget_class_bind_template_child (widget_class, MctUserControls, restriction_button); gtk_widget_class_bind_template_child (widget_class, MctUserControls, restriction_popover); - gtk_widget_class_bind_template_child (widget_class, MctUserControls, listbox); + gtk_widget_class_bind_template_child (widget_class, MctUserControls, restrict_applications_dialog); gtk_widget_class_bind_template_callback (widget_class, on_allow_installation_switch_active_changed_cb); gtk_widget_class_bind_template_callback (widget_class, on_allow_web_browsers_switch_active_changed_cb); + gtk_widget_class_bind_template_callback (widget_class, on_restrict_applications_button_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, on_restrict_applications_dialog_delete_event_cb); + gtk_widget_class_bind_template_callback (widget_class, on_restrict_applications_dialog_response_cb); } static void @@ -1050,11 +762,12 @@ mct_user_controls_init (MctUserControls *self) g_autoptr(GDBusConnection) system_bus = NULL; g_autoptr(GError) error = NULL; + /* Ensure the types used in the UI are registered. */ + g_type_ensure (MCT_TYPE_RESTRICT_APPLICATIONS_DIALOG); + gtk_widget_init_template (GTK_WIDGET (self)); self->selected_age = (guint) -1; - self->system_installation = flatpak_installation_new_system (NULL, NULL); - self->user_installation = flatpak_installation_new_user (NULL, NULL); self->cancellable = g_cancellable_new (); @@ -1079,19 +792,6 @@ mct_user_controls_init (MctUserControls *self) G_ACTION_GROUP (self->action_group)); gtk_popover_bind_model (self->restriction_popover, G_MENU_MODEL (self->age_menu), NULL); - self->blacklisted_apps = g_hash_table_new_full (g_direct_hash, g_direct_equal, g_object_unref, NULL); - - self->apps = g_list_store_new (G_TYPE_APP_INFO); - - self->app_info_monitor = g_app_info_monitor_get (); - g_signal_connect_object (self->app_info_monitor, "changed", - (GCallback) app_info_changed_cb, self, 0); - - gtk_list_box_bind_model (self->listbox, - G_LIST_MODEL (self->apps), - create_row_for_app_cb, - self, - NULL); g_object_bind_property (self->allow_user_installation_switch, "active", self->allow_system_installation_switch, "sensitive", diff --git a/malcontent-control/user-controls.ui b/malcontent-control/user-controls.ui index 880c207..4cc4dd0 100644 --- a/malcontent-control/user-controls.ui +++ b/malcontent-control/user-controls.ui @@ -18,9 +18,6 @@ - - - 0 @@ -29,22 +26,187 @@ - + True - 0.0 - Prevent this user from opening some apps by turning them off below. - True - True - listbox - - - - - - - + False + True + 0 + in + + + True + False + True + none + False + + + True + True + False + False + + + True + False + center + 12 + 12 + 8 + 8 + 4 + 4 + + + True + False + start + True + end + 0 + Block _Web Browsers + True + allow_web_browsers_switch + + + + + + 0 + 0 + + + + + True + False + start + True + end + 0 + Prevents the user from running web browsers, but limited web content may still be available in other applications + + + + + + + + + + 0 + 1 + + + + + True + True + end + center + + + + 1 + 0 + 2 + + + + + + + + + True + True + False + False + + + True + False + center + 12 + 12 + 8 + 8 + 4 + 4 + + + True + False + start + True + end + 0 + _Restrict Applications + True + restrict_applications_button + + + + + + 0 + 0 + + + + + True + False + start + True + end + 0 + Prevents particular applications from being used + + + + + + + + + + 0 + 1 + + + + + True + True + end + center + none + + + + True + pan-end-symbolic + 4 + + + + + 1 + 0 + 2 + + + + + + + + 1 @@ -52,115 +214,6 @@ - - - True - True - never - 100 - 400 - True - etched-in - - - - - True - none - - - - - - 2 - 0 - 2 - - - - - - - True - 0.0 - Restrict Web Browsers - - - - - - - - - 3 - 0 - - - - - - True - 0.0 - Prevent this user from running web browsers by turning them off below. Note that if the computer is connected to the internet, limited web content may still be available in other applications. - True - 55 - True - allow_web_browsers_switch - - - - - - - - - - 4 - 0 - - - - - - True - 12 - - - - True - 1.0 - Web _Browsers - True - True - allow_web_browsers_switch - - - - - - - - - - True - True - start - - - - - - - 5 - 0 - 2 - - - @@ -173,7 +226,7 @@ - 6 + 2 0 @@ -211,7 +264,7 @@ - 7 + 3 0 2 @@ -250,7 +303,7 @@ - 8 + 4 0 2 @@ -291,7 +344,7 @@ - 9 + 5 0 2 @@ -318,10 +371,22 @@ horizontal - + + + + + + + False + True + True + 1 + + + diff --git a/po/POTFILES.in b/po/POTFILES.in index f5380f0..aaa1ca0 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -7,6 +7,10 @@ malcontent-control/gs-content-rating.c malcontent-control/main.ui malcontent-control/org.freedesktop.MalcontentControl.appdata.xml.in malcontent-control/org.freedesktop.MalcontentControl.desktop.in +malcontent-control/restrict-applications-dialog.c +malcontent-control/restrict-applications-dialog.ui +malcontent-control/restrict-applications-selector.c +malcontent-control/restrict-applications-selector.ui malcontent-control/user-controls.c malcontent-control/user-controls.ui pam/pam_malcontent.c From 08ab378053aa4549cad3d682f00adb081a951f6f Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Tue, 28 Jan 2020 14:26:14 +0000 Subject: [PATCH 04/19] malcontent-control: Redesign other controls on main window MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Following the redesign of the app filter controls, redesign the rest of the controls on the window to match the list-box-like new style. This doesn’t change their functionality. Signed-off-by: Philip Withnall --- malcontent-control/user-controls.ui | 361 +++++++++++++++++++--------- 1 file changed, 252 insertions(+), 109 deletions(-) diff --git a/malcontent-control/user-controls.ui b/malcontent-control/user-controls.ui index 4cc4dd0..92bfbe8 100644 --- a/malcontent-control/user-controls.ui +++ b/malcontent-control/user-controls.ui @@ -232,124 +232,267 @@ - + True - 12 - + False + True + 0 + in - - - 1.0 - App _Installation - True - True - allow_user_installation_switch - - - - - - - - - + True + False True - start - + none + False + + + + True + False + False + + + True + False + center + 12 + 12 + 8 + 8 + 4 + 4 + + + True + False + start + True + end + 0 + Application _Installation + True + allow_user_installation_switch + + + + + + 0 + 0 + + + + + True + False + start + True + end + 0 + Restricts the user from installing applications + + + + + + + + + + 0 + 1 + + + + + True + True + end + center + + + + 1 + 0 + 2 + + + + + + + + + + + True + False + False + + + True + False + center + 12 + 12 + 8 + 8 + 4 + 4 + + + True + False + start + True + end + 0 + Application Installation for _Others + True + allow_system_installation_switch + + + + + + 0 + 0 + + + + + True + False + start + True + end + 0 + Restricts the user from installing applications for all users + + + + + + + + + + 0 + 1 + + + + + True + True + end + center + + + + 1 + 0 + 2 + + + + + + + + + + True + True + False + False + + + True + False + center + 12 + 12 + 8 + 8 + 4 + 4 + + + True + False + start + True + end + 0 + Application _Suitability + True + restriction_button + + + + + + + 0 + 0 + + + + + True + False + start + True + end + 0 + Restricts the applications the user can browse or install to those suitable for certain ages + + + + + + + + + + 0 + 1 + + + + + True + True + end + center + restriction_popover + + + 1 + 0 + 2 + + + + + + - 3 0 - 2 - - - - True - 12 - - - - - 1.0 - Install Apps for All _Users - True - True - allow_system_installation_switch - - - - - - - - - - True - True - start - - - - - - - 4 - 0 - 2 - - - - - - True - 12 - - - - True - 1.0 - Show Apps _Suitable For - True - True - restriction_button - - - - - - - - - - - True - True - start - restriction_popover - 200 - - - - - - 5 - 0 - 2 - - - @@ -375,9 +518,9 @@ - - - + + + From 629e7aac15a6a38e6f32bfa0ac5e34bf39676d36 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Tue, 28 Jan 2020 14:20:20 +0000 Subject: [PATCH 05/19] malcontent-control: Relabel a group of controls This fits in better with the new design, and is more noun-like than verb-like. Signed-off-by: Philip Withnall --- malcontent-control/user-controls.ui | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/malcontent-control/user-controls.ui b/malcontent-control/user-controls.ui index 92bfbe8..2439051 100644 --- a/malcontent-control/user-controls.ui +++ b/malcontent-control/user-controls.ui @@ -9,12 +9,12 @@ 12 start - + True 0.0 - Restrict Apps + Application Usage Restrictions @@ -214,13 +214,13 @@ - + True 12 0.0 - App Center Restrictions + Software Installation Restrictions From d56b19da1a8fc68e38a0875eaaae41cebe577fbd Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Tue, 28 Jan 2020 14:25:09 +0000 Subject: [PATCH 06/19] =?UTF-8?q?malcontent-control:=20Relabel=20=E2=80=98?= =?UTF-8?q?no=20restriction=E2=80=99=20entry=20for=20OARS=20settings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ‘All Ages’ is perhaps a bit clearer than ‘No Restriction’. Signed-off-by: Philip Withnall --- malcontent-control/user-controls.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/malcontent-control/user-controls.c b/malcontent-control/user-controls.c index 1cc07a9..02e7acd 100644 --- a/malcontent-control/user-controls.c +++ b/malcontent-control/user-controls.c @@ -237,7 +237,7 @@ update_categories_from_language (MctUserControls *self) g_menu_remove_all (self->age_menu); disabled_action = g_strdup_printf ("permissions.set-age(uint32 %u)", oars_disabled_age); - g_menu_append (self->age_menu, _("No Restriction"), disabled_action); + g_menu_append (self->age_menu, _("All Ages"), disabled_action); for (i = 0; entries[i] != NULL; i++) { @@ -310,7 +310,7 @@ update_oars_level (MctUserControls *self) /* Unrestricted? */ if (rating_age_category == NULL || all_categories_unset) - rating_age_category = _("No Restriction"); + rating_age_category = _("All Ages"); gtk_button_set_label (self->restriction_button, rating_age_category); } @@ -603,7 +603,7 @@ on_set_age_action_activated (GSimpleAction *action, /* Update the button */ if (age == oars_disabled_age) - gtk_button_set_label (self->restriction_button, _("No Restriction")); + gtk_button_set_label (self->restriction_button, _("All Ages")); for (i = 0; age != oars_disabled_age && entries[i] != NULL; i++) { From 36162c2c230a02b27c6309565e5765b767abe4de Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Tue, 28 Jan 2020 14:27:12 +0000 Subject: [PATCH 07/19] malcontent-control: Tweak default window height to match new controls A fairly arbitrary decision which seems to match the new controls a bit better, removing the large whitespace gap at the bottom of the window. Signed-off-by: Philip Withnall --- malcontent-control/main.ui | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/malcontent-control/main.ui b/malcontent-control/main.ui index 6d3d026..31f0d48 100644 --- a/malcontent-control/main.ui +++ b/malcontent-control/main.ui @@ -4,7 +4,7 @@ 500 - 700 + 600 True @@ -30,7 +30,7 @@ True never - 450 + 350 True From 8badee7fa9803ca124261cacaf66ef6e5f72b31d Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Tue, 28 Jan 2020 14:30:26 +0000 Subject: [PATCH 08/19] malcontent-control: Add polkit policy support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an unlock screen to the application, which is shown on startup if the current user doesn’t have permission to view the parental controls of other users. It requests permission using a new polkit action which implies the various accounts-service actions we need. This adds a dependency on `polkit-gobject-1`. Signed-off-by: Philip Withnall --- malcontent-control/application.c | 91 ++++++++++++++++++- malcontent-control/main.ui | 55 +++++++++++ malcontent-control/meson.build | 19 ++++ ...rg.freedesktop.MalcontentControl.policy.in | 18 ++++ po/POTFILES.in | 1 + 5 files changed, 179 insertions(+), 5 deletions(-) create mode 100644 malcontent-control/org.freedesktop.MalcontentControl.policy.in diff --git a/malcontent-control/application.c b/malcontent-control/application.c index 5e30b11..8197789 100644 --- a/malcontent-control/application.c +++ b/malcontent-control/application.c @@ -27,6 +27,7 @@ #include #include #include +#include #include "application.h" #include "user-controls.h" @@ -39,6 +40,12 @@ static void user_selector_notify_user_cb (GObject *obj, static void user_manager_notify_is_loaded_cb (GObject *obj, GParamSpec *pspec, gpointer user_data); +static void permission_new_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data); +static void permission_notify_allowed_cb (GObject *obj, + GParamSpec *pspec, + gpointer user_data); /** @@ -53,13 +60,19 @@ struct _MctApplication { GtkApplication parent_instance; + GCancellable *cancellable; /* (owned) */ + ActUserManager *user_manager; /* (owned) */ + GPermission *permission; /* (owned) */ + GError *permission_error; /* (nullable) (owned) */ + MctUserSelector *user_selector; MctUserControls *user_controls; GtkStack *main_stack; GtkLabel *error_title; GtkLabel *error_message; + GtkLockButton *lock_button; }; G_DEFINE_TYPE (MctApplication, mct_application, GTK_TYPE_APPLICATION) @@ -67,7 +80,7 @@ G_DEFINE_TYPE (MctApplication, mct_application, GTK_TYPE_APPLICATION) static void mct_application_init (MctApplication *self) { - /* Nothing to do here. */ + self->cancellable = g_cancellable_new (); } static void @@ -93,6 +106,8 @@ mct_application_dispose (GObject *object) { MctApplication *self = MCT_APPLICATION (object); + g_cancellable_cancel (self->cancellable); + if (self->user_manager != NULL) { g_signal_handlers_disconnect_by_func (self->user_manager, @@ -100,6 +115,16 @@ mct_application_dispose (GObject *object) g_clear_object (&self->user_manager); } + if (self->permission != NULL) + { + g_signal_handlers_disconnect_by_func (self->permission, + permission_notify_allowed_cb, self); + g_clear_object (&self->permission); + } + + g_clear_error (&self->permission_error); + g_clear_object (&self->cancellable); + G_OBJECT_CLASS (mct_application_parent_class)->dispose (object); } @@ -126,6 +151,11 @@ mct_application_activate (GApplication *application) g_type_ensure (MCT_TYPE_USER_CONTROLS); g_type_ensure (MCT_TYPE_USER_SELECTOR); + /* Start loading the permission */ + polkit_permission_new ("org.freedesktop.MalcontentControl.administration", + NULL, self->cancellable, + permission_new_cb, self); + builder = gtk_builder_new (); g_assert (self->user_manager == NULL); @@ -146,6 +176,7 @@ mct_application_activate (GApplication *application) self->user_controls = MCT_USER_CONTROLS (gtk_builder_get_object (builder, "user_controls")); self->error_title = GTK_LABEL (gtk_builder_get_object (builder, "error_title")); self->error_message = GTK_LABEL (gtk_builder_get_object (builder, "error_message")); + self->lock_button = GTK_LOCK_BUTTON (gtk_builder_get_object (builder, "lock_button")); /* Connect signals. */ g_signal_connect_object (self->user_selector, "notify::user", @@ -181,16 +212,19 @@ mct_application_class_init (MctApplicationClass *klass) static void update_main_stack (MctApplication *self) { - gboolean is_user_manager_loaded; + gboolean is_user_manager_loaded, is_permission_loaded, has_permission; const gchar *new_page_name, *old_page_name; GtkWidget *new_focus_widget; /* The implementation of #ActUserManager guarantees that once is-loaded is * true, it is never reset to false. */ g_object_get (self->user_manager, "is-loaded", &is_user_manager_loaded, NULL); + is_permission_loaded = (self->permission != NULL || self->permission_error != NULL); + has_permission = (self->permission != NULL && g_permission_get_allowed (self->permission)); - /* Handle any loading errors. */ - if (is_user_manager_loaded && act_user_manager_no_service (self->user_manager)) + /* Handle any loading errors (including those from getting the permission). */ + if ((is_user_manager_loaded && act_user_manager_no_service (self->user_manager)) || + self->permission_error != NULL) { gtk_label_set_label (self->error_title, _("Failed to load user data from the system")); @@ -200,7 +234,15 @@ update_main_stack (MctApplication *self) new_page_name = "error"; new_focus_widget = NULL; } - else if (is_user_manager_loaded) + else if (is_permission_loaded && !has_permission) + { + gtk_lock_button_set_permission (self->lock_button, self->permission); + mct_user_controls_set_permission (self->user_controls, self->permission); + + new_page_name = "unlock"; + new_focus_widget = GTK_WIDGET (self->lock_button); + } + else if (is_permission_loaded && is_user_manager_loaded) { new_page_name = "controls"; new_focus_widget = GTK_WIDGET (self->user_selector); @@ -242,6 +284,45 @@ user_manager_notify_is_loaded_cb (GObject *obj, update_main_stack (self); } +static void +permission_new_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + MctApplication *self = MCT_APPLICATION (user_data); + g_autoptr(GPermission) permission = NULL; + g_autoptr(GError) local_error = NULL; + + permission = polkit_permission_new_finish (result, &local_error); + if (permission == NULL) + { + g_assert (self->permission_error == NULL); + self->permission_error = g_steal_pointer (&local_error); + g_debug ("Error getting permission: %s", self->permission_error->message); + } + else + { + g_assert (self->permission == NULL); + self->permission = g_steal_pointer (&permission); + + g_signal_connect (self->permission, "notify::allowed", + G_CALLBACK (permission_notify_allowed_cb), self); + } + + /* Recalculate the UI. */ + update_main_stack (self); +} + +static void +permission_notify_allowed_cb (GObject *obj, + GParamSpec *pspec, + gpointer user_data) +{ + MctApplication *self = MCT_APPLICATION (user_data); + + update_main_stack (self); +} + /** * mct_application_new: * diff --git a/malcontent-control/main.ui b/malcontent-control/main.ui index 31f0d48..9c8bf9c 100644 --- a/malcontent-control/main.ui +++ b/malcontent-control/main.ui @@ -48,6 +48,61 @@ + + + True + vertical + True + True + + + True + vertical + 12 + 18 + + + True + Permission Required + + + + + + static + + + + + + + True + Permission is required to view and change parental controls settings for other users. + True + + + static + + + + + + + True + center + True + True + True + + + + + + + unlock + + + True diff --git a/malcontent-control/meson.build b/malcontent-control/meson.build index 82a73b4..528ed3c 100644 --- a/malcontent-control/meson.build +++ b/malcontent-control/meson.build @@ -37,6 +37,7 @@ malcontent_control = executable('malcontent-control', dependency('gobject-2.0', version: '>= 2.54'), dependency('gtk+-3.0'), dependency('flatpak'), + dependency('polkit-gobject-1'), libmalcontent_dep, ], include_directories: root_inc, @@ -104,6 +105,24 @@ if xmllint.found() ) endif +policy_file = i18n.merge_file('policy-file', + input: '@0@.policy.in'.format(application_id), + output: '@0@.policy'.format(application_id), + po_dir: join_paths(meson.source_root(), 'po'), + install: true, + install_dir: join_paths(get_option('datadir'), 'polkit-1', 'actions'), +) +if xmllint.found() + test( + 'validate-policy', xmllint, + args: [ + '--nonet', '--noblanks', '--noout', + policy_file, + ], + suite: ['malcontent-control'], + ) +endif + # FIXME: Add icons and tests #subdir('icons') #subdir('tests') diff --git a/malcontent-control/org.freedesktop.MalcontentControl.policy.in b/malcontent-control/org.freedesktop.MalcontentControl.policy.in new file mode 100644 index 0000000..b2a93ac --- /dev/null +++ b/malcontent-control/org.freedesktop.MalcontentControl.policy.in @@ -0,0 +1,18 @@ + + + + + The Malcontent Project + https://gitlab.freedesktop.org/pwithnall/malcontent + + + Manage parental controls + Authentication is required to read and change user parental controls + + no + no + auth_admin_keep + + com.endlessm.ParentalControls.AppFilter.ReadAny com.endlessm.ParentalControls.AppFilter.ChangeAny + + diff --git a/po/POTFILES.in b/po/POTFILES.in index aaa1ca0..d26b86e 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -7,6 +7,7 @@ malcontent-control/gs-content-rating.c malcontent-control/main.ui malcontent-control/org.freedesktop.MalcontentControl.appdata.xml.in malcontent-control/org.freedesktop.MalcontentControl.desktop.in +malcontent-control/org.freedesktop.MalcontentControl.policy.in malcontent-control/restrict-applications-dialog.c malcontent-control/restrict-applications-dialog.ui malcontent-control/restrict-applications-selector.c From f9a11b22b32024f06fbc0ca60a1c7e3f5781ee1f Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Tue, 28 Jan 2020 15:04:52 +0000 Subject: [PATCH 09/19] malcontent-control: Update the app filter after permissions have changed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After the user’s current set of permissions have changed, they may now be able to query the app filter whereas previously they weren’t allowed to. So try re-querying it. Signed-off-by: Philip Withnall --- malcontent-control/user-controls.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/malcontent-control/user-controls.c b/malcontent-control/user-controls.c index 02e7acd..7a10f01 100644 --- a/malcontent-control/user-controls.c +++ b/malcontent-control/user-controls.c @@ -833,6 +833,7 @@ on_permission_allowed_cb (GObject *obj, { MctUserControls *self = MCT_USER_CONTROLS (user_data); + update_app_filter (self); setup_parental_control_settings (self); } @@ -872,6 +873,7 @@ mct_user_controls_set_permission (MctUserControls *self, } /* Handle changes. */ + update_app_filter (self); setup_parental_control_settings (self); g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_PERMISSION]); From a05ac751a123785b0cc597dba03b19e7d25d53db Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Tue, 28 Jan 2020 15:07:18 +0000 Subject: [PATCH 10/19] malcontent-control: Add a main stack page for when there are no users Point the admin towards gnome-control-center so they can add child user accounts. Signed-off-by: Philip Withnall --- malcontent-control/application.c | 36 ++++++++++++++++--- malcontent-control/main.ui | 60 ++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 5 deletions(-) diff --git a/malcontent-control/application.c b/malcontent-control/application.c index 8197789..e97db71 100644 --- a/malcontent-control/application.c +++ b/malcontent-control/application.c @@ -46,6 +46,8 @@ static void permission_new_cb (GObject *source_object, static void permission_notify_allowed_cb (GObject *obj, GParamSpec *pspec, gpointer user_data); +static void user_accounts_panel_button_clicked_cb (GtkButton *button, + gpointer user_data); /** @@ -73,6 +75,7 @@ struct _MctApplication GtkLabel *error_title; GtkLabel *error_message; GtkLockButton *lock_button; + GtkButton *user_accounts_panel_button; }; G_DEFINE_TYPE (MctApplication, mct_application, GTK_TYPE_APPLICATION) @@ -177,11 +180,15 @@ mct_application_activate (GApplication *application) self->error_title = GTK_LABEL (gtk_builder_get_object (builder, "error_title")); self->error_message = GTK_LABEL (gtk_builder_get_object (builder, "error_message")); self->lock_button = GTK_LOCK_BUTTON (gtk_builder_get_object (builder, "lock_button")); + self->user_accounts_panel_button = GTK_BUTTON (gtk_builder_get_object (builder, "user_accounts_panel_button")); /* Connect signals. */ g_signal_connect_object (self->user_selector, "notify::user", G_CALLBACK (user_selector_notify_user_cb), self, 0 /* flags */); + g_signal_connect_object (self->user_accounts_panel_button, "clicked", + G_CALLBACK (user_accounts_panel_button_clicked_cb), + self, 0 /* flags */); g_signal_connect (self->user_manager, "notify::is-loaded", G_CALLBACK (user_manager_notify_is_loaded_cb), self); @@ -215,12 +222,14 @@ update_main_stack (MctApplication *self) gboolean is_user_manager_loaded, is_permission_loaded, has_permission; const gchar *new_page_name, *old_page_name; GtkWidget *new_focus_widget; + ActUser *selected_user; /* The implementation of #ActUserManager guarantees that once is-loaded is * true, it is never reset to false. */ g_object_get (self->user_manager, "is-loaded", &is_user_manager_loaded, NULL); is_permission_loaded = (self->permission != NULL || self->permission_error != NULL); has_permission = (self->permission != NULL && g_permission_get_allowed (self->permission)); + selected_user = mct_user_selector_get_user (self->user_selector); /* Handle any loading errors (including those from getting the permission). */ if ((is_user_manager_loaded && act_user_manager_no_service (self->user_manager)) || @@ -234,6 +243,11 @@ update_main_stack (MctApplication *self) new_page_name = "error"; new_focus_widget = NULL; } + else if (is_user_manager_loaded && selected_user == NULL) + { + new_page_name = "no-other-users"; + new_focus_widget = GTK_WIDGET (self->user_accounts_panel_button); + } else if (is_permission_loaded && !has_permission) { gtk_lock_button_set_permission (self->lock_button, self->permission); @@ -244,6 +258,8 @@ update_main_stack (MctApplication *self) } else if (is_permission_loaded && is_user_manager_loaded) { + mct_user_controls_set_user (self->user_controls, selected_user); + new_page_name = "controls"; new_focus_widget = GTK_WIDGET (self->user_selector); } @@ -265,13 +281,9 @@ user_selector_notify_user_cb (GObject *obj, GParamSpec *pspec, gpointer user_data) { - MctUserSelector *selector = MCT_USER_SELECTOR (obj); MctApplication *self = MCT_APPLICATION (user_data); - ActUser *user; - user = mct_user_selector_get_user (selector); - - mct_user_controls_set_user (self->user_controls, user); + update_main_stack (self); } static void @@ -323,6 +335,20 @@ permission_notify_allowed_cb (GObject *obj, update_main_stack (self); } +static void +user_accounts_panel_button_clicked_cb (GtkButton *button, + gpointer user_data) +{ + g_autoptr(GError) local_error = NULL; + + if (!g_spawn_command_line_async ("gnome-control-center user-accounts", &local_error)) + { + g_warning ("Error opening GNOME Control Center: %s", + local_error->message); + return; + } +} + /** * mct_application_new: * diff --git a/malcontent-control/main.ui b/malcontent-control/main.ui index 9c8bf9c..7305a7e 100644 --- a/malcontent-control/main.ui +++ b/malcontent-control/main.ui @@ -103,6 +103,66 @@ + + + True + vertical + True + True + + + True + vertical + 12 + 18 + + + True + No Child Users Configured + + + + + + static + + + + + + + True + No child users are currently set up on the system. Create one before setting up their parental controls. + True + + + static + + + + + + + True + Create _Child User + center + True + True + True + True + + + + + + + + no-other-users + + + True From ee342d374b73b06ece11a7d9b657f8d0f9999dc8 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Tue, 28 Jan 2020 15:01:35 +0000 Subject: [PATCH 11/19] malcontent-control: Add MctUserSelector:show-administrators property MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We don’t want to show administrators in the parental controls app, since child accounts are not administrators (if they are, they are too powerful to be constrained by parental controls). Signed-off-by: Philip Withnall --- malcontent-control/user-selector.c | 42 ++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/malcontent-control/user-selector.c b/malcontent-control/user-selector.c index 046fc93..8d7d80b 100644 --- a/malcontent-control/user-selector.c +++ b/malcontent-control/user-selector.c @@ -63,6 +63,7 @@ struct _MctUserSelector ActUserManager *user_manager; /* (owned) */ ActUser *user; /* (owned) */ + gboolean show_administrators; }; G_DEFINE_TYPE (MctUserSelector, mct_user_selector, GTK_TYPE_BOX) @@ -71,9 +72,10 @@ typedef enum { PROP_USER = 1, PROP_USER_MANAGER, + PROP_SHOW_ADMINISTRATORS, } MctUserSelectorProperty; -static GParamSpec *properties[PROP_USER_MANAGER + 1]; +static GParamSpec *properties[PROP_SHOW_ADMINISTRATORS + 1]; static void mct_user_selector_constructed (GObject *obj) @@ -117,6 +119,10 @@ mct_user_selector_get_property (GObject *object, g_value_set_object (value, self->user_manager); break; + case PROP_SHOW_ADMINISTRATORS: + g_value_set_boolean (value, self->show_administrators); + break; + default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } @@ -142,6 +148,11 @@ mct_user_selector_set_property (GObject *object, self->user_manager = g_value_dup_object (value); break; + case PROP_SHOW_ADMINISTRATORS: + self->show_administrators = g_value_get_boolean (value); + reload_users (self, NULL); + break; + default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } @@ -211,6 +222,21 @@ mct_user_selector_class_init (MctUserSelectorClass *klass) G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + /** + * MctUserSelector:show-administrators: + * + * Whether to show administrators in the list, or hide them. + * + * Since: 0.5.0 + */ + properties[PROP_SHOW_ADMINISTRATORS] = + g_param_spec_boolean ("show-administrators", + "Show Administrators?", + "Whether to show administrators in the list, or hide them.", + TRUE, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS); + g_object_class_install_properties (object_class, G_N_ELEMENTS (properties), properties); gtk_widget_class_set_template_from_resource (widget_class, "/org/freedesktop/MalcontentControl/ui/user-selector.ui"); @@ -223,6 +249,8 @@ mct_user_selector_class_init (MctUserSelectorClass *klass) static void mct_user_selector_init (MctUserSelector *self) { + self->show_administrators = TRUE; + /* Ensure the types used in the UI are registered. */ g_type_ensure (MCT_TYPE_CAROUSEL); @@ -351,6 +379,14 @@ reload_users (MctUserSelector *self, for (l = list; l; l = l->next) { user = l->data; + + if (act_user_get_account_type (user) == ACT_USER_ACCOUNT_TYPE_ADMINISTRATOR && + !self->show_administrators) + { + g_debug ("Ignoring administrator %s", get_real_or_user_name (user)); + continue; + } + g_debug ("Adding user %s", get_real_or_user_name (user)); user_added_cb (self->user_manager, user, self); } @@ -410,7 +446,9 @@ user_added_cb (ActUserManager *user_manager, MctUserSelector *self = MCT_USER_SELECTOR (user_data); GtkWidget *item, *widget; - if (act_user_is_system_account (user)) + if (act_user_is_system_account (user) || + (act_user_get_account_type (user) == ACT_USER_ACCOUNT_TYPE_ADMINISTRATOR && + !self->show_administrators)) return; g_debug ("User added: %u %s", (guint) act_user_get_uid (user), get_real_or_user_name (user)); From 2ab9924d8dd660b4549cbca6ca1a6e59339527aa Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Tue, 28 Jan 2020 15:03:00 +0000 Subject: [PATCH 12/19] malcontent-control: Hide administrator accounts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Children can’t be administrator accounts, otherwise applying parental controls to them would be pointless and ineffective. So hide the administrator accounts from the parental controls app. Signed-off-by: Philip Withnall --- malcontent-control/main.ui | 1 + 1 file changed, 1 insertion(+) diff --git a/malcontent-control/main.ui b/malcontent-control/main.ui index 7305a7e..7c6af80 100644 --- a/malcontent-control/main.ui +++ b/malcontent-control/main.ui @@ -17,6 +17,7 @@ True user_manager + False From 00bb439f6e7c0d7b5c5422dc71b3037ace430967 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Wed, 29 Jan 2020 17:36:36 +0000 Subject: [PATCH 13/19] malcontent-control: Fix use of an uninitialised variable in the carousel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `dest_x` is not set if `gtk_widget_translate_coordinates()` fails, which it can do before the widget is realised. This fixes a valgrind warning, but doesn’t change any user-visible behaviour as far as I can tell. This has been upstreamed to gnome-control-center as https://gitlab.gnome.org/GNOME/gnome-control-center/merge_requests/691. Signed-off-by: Philip Withnall --- malcontent-control/carousel.c | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/malcontent-control/carousel.c b/malcontent-control/carousel.c index 2a10ee4..c69fc9f 100644 --- a/malcontent-control/carousel.c +++ b/malcontent-control/carousel.c @@ -97,12 +97,13 @@ mct_carousel_item_get_x (MctCarouselItem *item, widget = GTK_WIDGET (item); width = gtk_widget_get_allocated_width (widget); - gtk_widget_translate_coordinates (widget, - parent, - width / 2, - 0, - &dest_x, - NULL); + if (!gtk_widget_translate_coordinates (widget, + parent, + width / 2, + 0, + &dest_x, + NULL)) + return 0; return CLAMP (dest_x - ARROW_SIZE, 0, From 695ee1023547063b97141e4220e9224ca4cb279e Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Wed, 29 Jan 2020 17:37:12 +0000 Subject: [PATCH 14/19] malcontent-control: Fix use-after-free when closing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sometimes, when closing the application, `flush_update_blacklisted_apps()` would be called after `MctRestrictApplicationsSelector` had been destroyed, leading to a critical. This was because the `MctRestrictApplicationsDialog` was being disposed early due to its `destroy-with-parent` property being set. The dispose function of `MctUserControls` was run several times due to GTK calling `g_object_run_dispose()`, and the critical would be emitted the second time. Make the dispose function’s call to `flush_update_blacklisted_apps()` be safe for multiple dispose calls, and ensure the dialog isn’t destroyed too early. Signed-off-by: Philip Withnall --- malcontent-control/user-controls.c | 12 +++++++++++- malcontent-control/user-controls.ui | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/malcontent-control/user-controls.c b/malcontent-control/user-controls.c index 7a10f01..ee1f8ca 100644 --- a/malcontent-control/user-controls.c +++ b/malcontent-control/user-controls.c @@ -62,6 +62,7 @@ struct _MctUserControls guint selected_age; /* @oars_disabled_age to disable OARS */ guint blacklist_apps_source_id; + gboolean flushed_on_dispose; }; static gboolean blacklist_apps_cb (gpointer data); @@ -650,6 +651,9 @@ mct_user_controls_finalize (GObject *object) g_clear_pointer (&self->filter, mct_app_filter_unref); g_clear_object (&self->manager); + /* Hopefully we don’t have data loss. */ + g_assert (self->flushed_on_dispose); + G_OBJECT_CLASS (mct_user_controls_parent_class)->finalize (object); } @@ -659,7 +663,13 @@ mct_user_controls_dispose (GObject *object) { MctUserControls *self = (MctUserControls *)object; - flush_update_blacklisted_apps (self); + /* Since GTK calls g_object_run_dispose(), dispose() may be called multiple + * times. We definitely want to save any unsaved changes, but don’t need to + * do it multiple times, and after the first g_object_run_dispose() call, + * none of our child widgets are still around to extract data from anyway. */ + if (!self->flushed_on_dispose) + flush_update_blacklisted_apps (self); + self->flushed_on_dispose = TRUE; G_OBJECT_CLASS (mct_user_controls_parent_class)->dispose (object); } diff --git a/malcontent-control/user-controls.ui b/malcontent-control/user-controls.ui index 2439051..4db1404 100644 --- a/malcontent-control/user-controls.ui +++ b/malcontent-control/user-controls.ui @@ -527,7 +527,7 @@ False True - True + False 1 From 97dba87bdc0bcba457c5bcb3c2ba71e6e2649a69 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Fri, 31 Jan 2020 11:51:35 +0000 Subject: [PATCH 15/19] docs: Update README to mention malcontent-control and its dependencies Signed-off-by: Philip Withnall --- README.md | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 8b75c1a..56f646e 100644 --- a/README.md +++ b/README.md @@ -11,12 +11,14 @@ being too violent. It provides an [accounts-service](https://gitlab.freedesktop.org/accountsservice/accountsservice) vendor extension for storing an app filter to -restrict the child’s access to certain applications; and a simple library for -accessing and applying the app filter. This results in the policy being stored -in `/var/lib/AccountsService/users/${user}`, which is a key file readable and -writable only by the accounts-service daemon. Access to the data is mediated -through accounts-service’s D-Bus interface, which libmalcontent is a client -library for. +restrict the child’s access to certain applications; a simple library for +accessing and applying the app filter; and a UI program (`malcontent-control`) +for viewing and changing the parental controls settings on users. + +The parental controls policy is stored in `/var/lib/AccountsService/users/${user}`, +which is a key file readable and writable only by the accounts-service daemon. +Access to the data is mediated through accounts-service’s D-Bus interface, which +libmalcontent is a client library for. All the library APIs are currently unstable and are likely to change wildly. @@ -74,15 +76,26 @@ $ flatpak run org.freedesktop.Bustle error: Running app/org.freedesktop.Bustle/x86_64/stable is not allowed by the policy set by your administrator ``` +Development +----------- + +When developing malcontent, you should be able to run an uninstalled version of +`malcontent-client` or `malcontent-control`, as long as the polkit files from +`accounts-service/` and `malcontent-control/org.freedesktop.MalcontentControl.policy.in` +have been installed system-wide (typically under `/usr/share/polkit-1`) where +your system copy of polkitd can find them. + Dependencies ------------ * accounts-service * dbus-daemon + * flatpak * gio-2.0 ≥ 2.60 * gio-unix-2.0 ≥ 2.60 * glib-2.0 ≥ 2.60 * gobject-2.0 ≥ 2.60 + * gtk+-3.0 * polkit-gobject-1 Licensing From 32c7435b8d5ffb74d55e246ad182f48cc4361d22 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Fri, 31 Jan 2020 11:52:38 +0000 Subject: [PATCH 16/19] malcontent-control: Update an issue link in a FIXME comment Since this code has moved out of our downstream g-c-c fork, the issue tracking is now upstream, so update an issue link. Signed-off-by: Philip Withnall --- malcontent-control/user-controls.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/malcontent-control/user-controls.c b/malcontent-control/user-controls.c index ee1f8ca..1f40cb0 100644 --- a/malcontent-control/user-controls.c +++ b/malcontent-control/user-controls.c @@ -110,7 +110,7 @@ static const GActionEntry actions[] = { }; /* FIXME: Factor this out and rely on code from libappstream-glib or gnome-software - * to do it. See: https://phabricator.endlessm.com/T24986 */ + * to do it. See: https://gitlab.freedesktop.org/pwithnall/malcontent/issues/7 */ static const gchar * const oars_categories[] = { "violence-cartoon", From 7fd8705f33b3e438be287db6528afa2b6a87bd52 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Fri, 31 Jan 2020 11:53:39 +0000 Subject: [PATCH 17/19] malcontent-control: Treat lack of GPermission as lack of permissions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rather than assuming that having no GPermission means we do have permissions, which was a little confusing and didn’t match other points in the code. Signed-off-by: Philip Withnall --- malcontent-control/user-controls.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/malcontent-control/user-controls.c b/malcontent-control/user-controls.c index 1f40cb0..a4bb8f4 100644 --- a/malcontent-control/user-controls.c +++ b/malcontent-control/user-controls.c @@ -408,7 +408,7 @@ setup_parental_control_settings (MctUserControls *self) if (self->permission != NULL) is_authorized = g_permission_get_allowed (G_PERMISSION (self->permission)); else - is_authorized = TRUE; + is_authorized = FALSE; gtk_widget_set_sensitive (GTK_WIDGET (self), is_authorized); From 591f63890b964bed64f7de414c00e9bf98b8eafd Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Mon, 3 Feb 2020 17:33:04 +0000 Subject: [PATCH 18/19] =?UTF-8?q?malcontent-control:=20Fix=20=E2=80=98Bloc?= =?UTF-8?q?k=20Web=20Browsers=E2=80=99=20label=20sense?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The switch is actually controlling whether to allow web browsers, not block them, and is enabled by default. Signed-off-by: Philip Withnall --- malcontent-control/user-controls.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/malcontent-control/user-controls.ui b/malcontent-control/user-controls.ui index 4db1404..4042174 100644 --- a/malcontent-control/user-controls.ui +++ b/malcontent-control/user-controls.ui @@ -64,7 +64,7 @@ True end 0 - Block _Web Browsers + Allow _Web Browsers True allow_web_browsers_switch From 4054913dc296960f9dcb5a40dc039909196e60c3 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Mon, 3 Feb 2020 17:49:21 +0000 Subject: [PATCH 19/19] malcontent-control: Fix losing user changes when apps are installed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If the user has the restrict applications dialogue open and has made some changes, then installs/uninstalls a flatpak (for example) from gnome-software in another window, then the list of apps in the restrict applications dialogue will be reloaded and the user’s changes will be lost. Prevent that by not reloading when the set of installed apps changes. This is not a long-term solution: ideally we’d diff the changes against the list of apps in the restrict applications dialogue and only update what’s changed. Signed-off-by: Philip Withnall --- malcontent-control/restrict-applications-selector.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/malcontent-control/restrict-applications-selector.c b/malcontent-control/restrict-applications-selector.c index b6ddb83..6fb9d4b 100644 --- a/malcontent-control/restrict-applications-selector.c +++ b/malcontent-control/restrict-applications-selector.c @@ -478,9 +478,14 @@ static void app_info_changed_cb (GAppInfoMonitor *monitor, gpointer user_data) { + /* FIXME: We should update the list of apps here, but we can’t call + * reload_apps() because that will dump and reload the entire list, losing + * any changes the user has already made to the set of switches. We need + * something more fine-grained. MctRestrictApplicationsSelector *self = MCT_RESTRICT_APPLICATIONS_SELECTOR (user_data); reload_apps (self); + */ } /* Will return %NULL if @flatpak_id is not installed. */