Merge branch 'user-controls-without-user' into 'master'

Allow user controls to be used for not-yet-existing users

See merge request pwithnall/malcontent!24
This commit is contained in:
Philip Withnall 2020-02-14 12:16:56 +00:00
commit 33167e7767
7 changed files with 646 additions and 149 deletions

View File

@ -70,14 +70,14 @@ pkgconfig.generate(libmalcontent_ui,
libraries_private: libmalcontent_ui_private_deps, libraries_private: libmalcontent_ui_private_deps,
) )
gnome.generate_gir(libmalcontent_ui, libmalcontent_ui_gir = gnome.generate_gir(libmalcontent_ui,
sources: libmalcontent_ui_sources + libmalcontent_ui_headers + libmalcontent_ui_private_headers, sources: libmalcontent_ui_sources + libmalcontent_ui_headers + libmalcontent_ui_private_headers,
nsversion: libmalcontent_ui_api_version, nsversion: libmalcontent_ui_api_version,
namespace: 'MalcontentUi', namespace: 'MalcontentUi',
symbol_prefix: 'mct_', symbol_prefix: 'mct_',
identifier_prefix: 'Mct', identifier_prefix: 'Mct',
export_packages: 'libmalcontent-ui', export_packages: 'libmalcontent-ui',
includes: ['AccountsService-1.0', 'Gio-2.0', 'GObject-2.0', 'Gtk-3.0'], includes: ['AccountsService-1.0', 'Gio-2.0', 'GObject-2.0', 'Gtk-3.0', libmalcontent_gir[0]],
install: true, install: true,
dependencies: libmalcontent_ui_dep, dependencies: libmalcontent_ui_dep,
) )

View File

@ -19,7 +19,6 @@
* - Philip Withnall <withnall@endlessm.com> * - Philip Withnall <withnall@endlessm.com>
*/ */
#include <act/act.h>
#include <gio/gio.h> #include <gio/gio.h>
#include <glib.h> #include <glib.h>
#include <glib-object.h> #include <glib-object.h>
@ -55,7 +54,7 @@ struct _MctRestrictApplicationsDialog
GtkLabel *description; GtkLabel *description;
MctAppFilter *app_filter; /* (owned) (not nullable) */ MctAppFilter *app_filter; /* (owned) (not nullable) */
ActUser *user; /* (owned) (nullable) */ gchar *user_display_name; /* (owned) (nullable) */
}; };
G_DEFINE_TYPE (MctRestrictApplicationsDialog, mct_restrict_applications_dialog, GTK_TYPE_DIALOG) G_DEFINE_TYPE (MctRestrictApplicationsDialog, mct_restrict_applications_dialog, GTK_TYPE_DIALOG)
@ -63,10 +62,10 @@ G_DEFINE_TYPE (MctRestrictApplicationsDialog, mct_restrict_applications_dialog,
typedef enum typedef enum
{ {
PROP_APP_FILTER = 1, PROP_APP_FILTER = 1,
PROP_USER, PROP_USER_DISPLAY_NAME,
} MctRestrictApplicationsDialogProperty; } MctRestrictApplicationsDialogProperty;
static GParamSpec *properties[PROP_USER + 1]; static GParamSpec *properties[PROP_USER_DISPLAY_NAME + 1];
static void static void
mct_restrict_applications_dialog_constructed (GObject *obj) mct_restrict_applications_dialog_constructed (GObject *obj)
@ -74,7 +73,9 @@ mct_restrict_applications_dialog_constructed (GObject *obj)
MctRestrictApplicationsDialog *self = MCT_RESTRICT_APPLICATIONS_DIALOG (obj); MctRestrictApplicationsDialog *self = MCT_RESTRICT_APPLICATIONS_DIALOG (obj);
g_assert (self->app_filter != NULL); g_assert (self->app_filter != NULL);
g_assert (self->user == NULL || ACT_IS_USER (self->user)); g_assert (self->user_display_name == NULL ||
(*self->user_display_name != '\0' &&
g_utf8_validate (self->user_display_name, -1, NULL)));
G_OBJECT_CLASS (mct_restrict_applications_dialog_parent_class)->constructed (obj); G_OBJECT_CLASS (mct_restrict_applications_dialog_parent_class)->constructed (obj);
} }
@ -93,8 +94,8 @@ mct_restrict_applications_dialog_get_property (GObject *object,
g_value_set_boxed (value, self->app_filter); g_value_set_boxed (value, self->app_filter);
break; break;
case PROP_USER: case PROP_USER_DISPLAY_NAME:
g_value_set_object (value, self->user); g_value_set_string (value, self->user_display_name);
break; break;
default: default:
@ -116,8 +117,8 @@ mct_restrict_applications_dialog_set_property (GObject *object,
mct_restrict_applications_dialog_set_app_filter (self, g_value_get_boxed (value)); mct_restrict_applications_dialog_set_app_filter (self, g_value_get_boxed (value));
break; break;
case PROP_USER: case PROP_USER_DISPLAY_NAME:
mct_restrict_applications_dialog_set_user (self, g_value_get_object (value)); mct_restrict_applications_dialog_set_user_display_name (self, g_value_get_string (value));
break; break;
default: default:
@ -131,7 +132,7 @@ mct_restrict_applications_dialog_dispose (GObject *object)
MctRestrictApplicationsDialog *self = (MctRestrictApplicationsDialog *)object; MctRestrictApplicationsDialog *self = (MctRestrictApplicationsDialog *)object;
g_clear_pointer (&self->app_filter, mct_app_filter_unref); g_clear_pointer (&self->app_filter, mct_app_filter_unref);
g_clear_object (&self->user); g_clear_pointer (&self->user_display_name, g_free);
G_OBJECT_CLASS (mct_restrict_applications_dialog_parent_class)->dispose (object); G_OBJECT_CLASS (mct_restrict_applications_dialog_parent_class)->dispose (object);
} }
@ -168,19 +169,22 @@ mct_restrict_applications_dialog_class_init (MctRestrictApplicationsDialogClass
G_PARAM_EXPLICIT_NOTIFY); G_PARAM_EXPLICIT_NOTIFY);
/** /**
* MctRestrictApplicationsDialog:user: (nullable) * MctRestrictApplicationsDialog:user-display-name: (nullable)
* *
* The currently selected user account, or %NULL if no user is selected. * The display name for the currently selected user account, or %NULL if no
* user is selected. This will typically be the users full name (if known)
* or their username.
*
* If set, it must be valid UTF-8 and non-empty.
* *
* Since: 0.5.0 * Since: 0.5.0
*/ */
properties[PROP_USER] = properties[PROP_USER_DISPLAY_NAME] =
g_param_spec_object ("user", g_param_spec_string ("user-display-name",
"User", "User Display Name",
"The currently selected user account, or %NULL if no user is selected.", "The display name for the currently selected user account, or %NULL if no user is selected.",
ACT_TYPE_USER, NULL,
G_PARAM_READWRITE | G_PARAM_READWRITE |
G_PARAM_CONSTRUCT_ONLY |
G_PARAM_STATIC_STRINGS | G_PARAM_STATIC_STRINGS |
G_PARAM_EXPLICIT_NOTIFY); G_PARAM_EXPLICIT_NOTIFY);
@ -201,31 +205,12 @@ mct_restrict_applications_dialog_init (MctRestrictApplicationsDialog *self)
gtk_widget_init_template (GTK_WIDGET (self)); 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 static void
update_description (MctRestrictApplicationsDialog *self) update_description (MctRestrictApplicationsDialog *self)
{ {
g_autofree gchar *description = NULL; g_autofree gchar *description = NULL;
if (self->user == NULL) if (self->user_display_name == NULL)
{ {
gtk_widget_hide (GTK_WIDGET (self->description)); gtk_widget_hide (GTK_WIDGET (self->description));
return; return;
@ -233,7 +218,7 @@ update_description (MctRestrictApplicationsDialog *self)
/* Translators: the placeholder is a users full name */ /* Translators: the placeholder is a users full name */
description = g_strdup_printf (_("Allow %s to use the following installed applications."), description = g_strdup_printf (_("Allow %s to use the following installed applications."),
get_user_display_name (self->user)); self->user_display_name);
gtk_label_set_text (self->description, description); gtk_label_set_text (self->description, description);
gtk_widget_show (GTK_WIDGET (self->description)); gtk_widget_show (GTK_WIDGET (self->description));
} }
@ -241,7 +226,8 @@ update_description (MctRestrictApplicationsDialog *self)
/** /**
* mct_restrict_applications_dialog_new: * mct_restrict_applications_dialog_new:
* @app_filter: (transfer none): the initial app filter configuration to show * @app_filter: (transfer none): the initial app filter configuration to show
* @user: (transfer none) (nullable): the user to show the app filter for * @user_display_name: (transfer none) (nullable): the display name of the user
* to show the app filter for, or %NULL if no user is selected
* *
* Create a new #MctRestrictApplicationsDialog widget. * Create a new #MctRestrictApplicationsDialog widget.
* *
@ -250,14 +236,16 @@ update_description (MctRestrictApplicationsDialog *self)
*/ */
MctRestrictApplicationsDialog * MctRestrictApplicationsDialog *
mct_restrict_applications_dialog_new (MctAppFilter *app_filter, mct_restrict_applications_dialog_new (MctAppFilter *app_filter,
ActUser *user) const gchar *user_display_name)
{ {
g_return_val_if_fail (app_filter != NULL, NULL); g_return_val_if_fail (app_filter != NULL, NULL);
g_return_val_if_fail (user == NULL || ACT_IS_USER (user), NULL); g_return_val_if_fail (user_display_name == NULL ||
(*user_display_name != '\0' &&
g_utf8_validate (user_display_name, -1, NULL)), NULL);
return g_object_new (MCT_TYPE_RESTRICT_APPLICATIONS_DIALOG, return g_object_new (MCT_TYPE_RESTRICT_APPLICATIONS_DIALOG,
"app-filter", app_filter, "app-filter", app_filter,
"user", user, "user-display-name", user_display_name,
NULL); NULL);
} }
@ -318,45 +306,50 @@ mct_restrict_applications_dialog_set_app_filter (MctRestrictApplicationsDialog *
} }
/** /**
* mct_restrict_applications_dialog_get_user: * mct_restrict_applications_dialog_get_user_display_name:
* @self: an #MctRestrictApplicationsDialog * @self: an #MctRestrictApplicationsDialog
* *
* Get the value of #MctRestrictApplicationsDialog:user. * Get the value of #MctRestrictApplicationsDialog:user-display-name.
* *
* Returns: (transfer none) (nullable): the user the dialog is configured for, * Returns: (transfer none) (nullable): the display name of the user the dialog
* or %NULL if unknown * is configured for, or %NULL if unknown
* Since: 0.5.0 * Since: 0.5.0
*/ */
ActUser * const gchar *
mct_restrict_applications_dialog_get_user (MctRestrictApplicationsDialog *self) mct_restrict_applications_dialog_get_user_display_name (MctRestrictApplicationsDialog *self)
{ {
g_return_val_if_fail (MCT_IS_RESTRICT_APPLICATIONS_DIALOG (self), NULL); g_return_val_if_fail (MCT_IS_RESTRICT_APPLICATIONS_DIALOG (self), NULL);
return self->user; return self->user_display_name;
} }
/** /**
* mct_restrict_applications_dialog_set_user: * mct_restrict_applications_dialog_set_user_display_name:
* @self: an #MctRestrictApplicationsDialog * @self: an #MctRestrictApplicationsDialog
* @user: (nullable) (transfer none): the user to configure the dialog for, * @user_display_name: (nullable) (transfer none): the display name of the user
* or %NULL if unknown * to configure the dialog for, or %NULL if unknown
* *
* Set the value of #MctRestrictApplicationsDialog:user. * Set the value of #MctRestrictApplicationsDialog:user-display-name.
* *
* Since: 0.5.0 * Since: 0.5.0
*/ */
void void
mct_restrict_applications_dialog_set_user (MctRestrictApplicationsDialog *self, mct_restrict_applications_dialog_set_user_display_name (MctRestrictApplicationsDialog *self,
ActUser *user) const gchar *user_display_name)
{ {
g_return_if_fail (MCT_IS_RESTRICT_APPLICATIONS_DIALOG (self)); g_return_if_fail (MCT_IS_RESTRICT_APPLICATIONS_DIALOG (self));
g_return_if_fail (user == NULL || ACT_IS_USER (user)); g_return_if_fail (user_display_name == NULL ||
(*user_display_name != '\0' &&
g_utf8_validate (user_display_name, -1, NULL)));
if (g_set_object (&self->user, user)) if (g_strcmp0 (self->user_display_name, user_display_name) == 0)
{ return;
update_description (self);
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_USER]); g_clear_pointer (&self->user_display_name, g_free);
} self->user_display_name = g_strdup (user_display_name);
update_description (self);
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_USER_DISPLAY_NAME]);
} }
/** /**

View File

@ -21,7 +21,6 @@
#pragma once #pragma once
#include <act/act.h>
#include <glib.h> #include <glib.h>
#include <glib-object.h> #include <glib-object.h>
#include <gtk/gtk.h> #include <gtk/gtk.h>
@ -34,15 +33,15 @@ G_BEGIN_DECLS
G_DECLARE_FINAL_TYPE (MctRestrictApplicationsDialog, mct_restrict_applications_dialog, MCT, RESTRICT_APPLICATIONS_DIALOG, GtkDialog) G_DECLARE_FINAL_TYPE (MctRestrictApplicationsDialog, mct_restrict_applications_dialog, MCT, RESTRICT_APPLICATIONS_DIALOG, GtkDialog)
MctRestrictApplicationsDialog *mct_restrict_applications_dialog_new (MctAppFilter *app_filter, MctRestrictApplicationsDialog *mct_restrict_applications_dialog_new (MctAppFilter *app_filter,
ActUser *user); const gchar *user_display_name);
MctAppFilter *mct_restrict_applications_dialog_get_app_filter (MctRestrictApplicationsDialog *self); MctAppFilter *mct_restrict_applications_dialog_get_app_filter (MctRestrictApplicationsDialog *self);
void mct_restrict_applications_dialog_set_app_filter (MctRestrictApplicationsDialog *self, void mct_restrict_applications_dialog_set_app_filter (MctRestrictApplicationsDialog *self,
MctAppFilter *app_filter); MctAppFilter *app_filter);
ActUser *mct_restrict_applications_dialog_get_user (MctRestrictApplicationsDialog *self); const gchar *mct_restrict_applications_dialog_get_user_display_name (MctRestrictApplicationsDialog *self);
void mct_restrict_applications_dialog_set_user (MctRestrictApplicationsDialog *self, void mct_restrict_applications_dialog_set_user_display_name (MctRestrictApplicationsDialog *self,
ActUser *user); const gchar *user_display_name);
void mct_restrict_applications_dialog_build_app_filter (MctRestrictApplicationsDialog *self, void mct_restrict_applications_dialog_build_app_filter (MctRestrictApplicationsDialog *self,
MctAppFilterBuilder *builder); MctAppFilterBuilder *builder);

View File

@ -21,7 +21,6 @@
#pragma once #pragma once
#include <act/act.h>
#include <glib.h> #include <glib.h>
#include <glib-object.h> #include <glib-object.h>
#include <gtk/gtk.h> #include <gtk/gtk.h>

View File

@ -21,6 +21,7 @@
*/ */
#include <libmalcontent/malcontent.h> #include <libmalcontent/malcontent.h>
#include <locale.h>
#include <flatpak.h> #include <flatpak.h>
#include <gio/gio.h> #include <gio/gio.h>
#include <gio/gdesktopappinfo.h> #include <gio/gdesktopappinfo.h>
@ -37,6 +38,38 @@
/* The value which we store as an age to indicate that OARS filtering is disabled. */ /* The value which we store as an age to indicate that OARS filtering is disabled. */
static const guint32 oars_disabled_age = (guint32) -1; static const guint32 oars_disabled_age = (guint32) -1;
/**
* MctUserControls:
*
* A group of widgets which allow setting the parental controls for a given
* user.
*
* If #MctUserControls:user is set, the current parental controls settings for
* that user will be loaded and displayed, and any changes made via the controls
* will be automatically saved for that user (potentially after a short
* timeout).
*
* If #MctUserControls:user is unset (for example, if setting the parental
* controls for a user account which hasnt yet been created), the controls can
* be initialised by setting:
* * #MctUserControls:app-filter
* * #MctUserControls:user-account-type
* * #MctUserControls:user-locale
* * #MctUserControls:user-display-name
*
* When #MctUserControls:user is unset, changes made to the parental controls
* cannot be saved automatically, and must be queried using
* mct_user_controls_build_app_filter(), then saved by the calling code.
*
* As parental controls are system settings, privileges are needed to view and
* edit them (for the current user or for other users). These can be acquired
* using polkit. #MctUserControls:permission is used to query the current
* permissions for getting/setting parental controls. If its %NULL, or if
* permissions are not currently granted, the #MctUserControls will be
* insensitive.
*
* Since: 0.5.0
*/
struct _MctUserControls struct _MctUserControls
{ {
GtkGrid parent_instance; GtkGrid parent_instance;
@ -58,11 +91,15 @@ struct _MctUserControls
GCancellable *cancellable; /* (owned) */ GCancellable *cancellable; /* (owned) */
MctManager *manager; /* (owned) */ MctManager *manager; /* (owned) */
MctAppFilter *filter; /* (owned) */ MctAppFilter *filter; /* (owned) (nullable) */
guint selected_age; /* @oars_disabled_age to disable OARS */ guint selected_age; /* @oars_disabled_age to disable OARS */
guint blacklist_apps_source_id; guint blacklist_apps_source_id;
gboolean flushed_on_dispose; gboolean flushed_on_dispose;
ActUserAccountType user_account_type;
gchar *user_locale; /* (nullable) (owned) */
gchar *user_display_name; /* (nullable) (owned) */
}; };
static gboolean blacklist_apps_cb (gpointer data); static gboolean blacklist_apps_cb (gpointer data);
@ -96,14 +133,17 @@ static void on_permission_allowed_cb (GObject *obj,
G_DEFINE_TYPE (MctUserControls, mct_user_controls, GTK_TYPE_GRID) G_DEFINE_TYPE (MctUserControls, mct_user_controls, GTK_TYPE_GRID)
enum typedef enum
{ {
PROP_USER = 1, PROP_USER = 1,
PROP_PERMISSION, PROP_PERMISSION,
N_PROPS PROP_APP_FILTER,
}; PROP_USER_ACCOUNT_TYPE,
PROP_USER_LOCALE,
PROP_USER_DISPLAY_NAME,
} MctUserControlsProperty;
static GParamSpec *properties [N_PROPS]; static GParamSpec *properties[PROP_USER_DISPLAY_NAME + 1];
static const GActionEntry actions[] = { static const GActionEntry actions[] = {
{ "set-age", on_set_age_action_activated, "u", NULL, NULL, { 0, }} { "set-age", on_set_age_action_activated, "u", NULL, NULL, { 0, }}
@ -146,13 +186,53 @@ static const gchar * const oars_categories[] =
/* Auxiliary methods */ /* Auxiliary methods */
static GsContentRatingSystem static GsContentRatingSystem
get_content_rating_system (ActUser *user) get_content_rating_system (MctUserControls *self)
{ {
const gchar *user_language; if (self->user_locale == NULL)
return GS_CONTENT_RATING_SYSTEM_UNKNOWN;
user_language = act_user_get_language (user); return gs_utils_content_rating_system_from_locale (self->user_locale);
}
return gs_utils_content_rating_system_from_locale (user_language); static const gchar *
get_user_locale (ActUser *user)
{
const gchar *locale;
g_return_val_if_fail (ACT_IS_USER (user), "C");
/* accounts-service can return %NULL if loading over D-Bus failed. */
locale = act_user_get_language (user);
if (locale == NULL)
return NULL;
/* It can return the empty string if the user uses the system default locale. */
if (*locale == '\0')
locale = setlocale (LC_MESSAGES, NULL);
if (locale == NULL || *locale == '\0')
locale = "C";
return locale;
}
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 static void
@ -180,12 +260,10 @@ flush_update_blacklisted_apps (MctUserControls *self)
} }
static void static void
update_app_filter (MctUserControls *self) update_app_filter_from_user (MctUserControls *self)
{ {
g_autoptr(GError) error = NULL; g_autoptr(GError) error = NULL;
g_clear_pointer (&self->filter, mct_app_filter_unref);
if (self->user == NULL) if (self->user == NULL)
return; return;
@ -199,6 +277,7 @@ update_app_filter (MctUserControls *self)
return; return;
/* FIXME: make it asynchronous */ /* FIXME: make it asynchronous */
g_clear_pointer (&self->filter, mct_app_filter_unref);
self->filter = mct_manager_get_app_filter (self->manager, self->filter = mct_manager_get_app_filter (self->manager,
act_user_get_uid (self->user), act_user_get_uid (self->user),
MCT_MANAGER_GET_VALUE_FLAGS_NONE, MCT_MANAGER_GET_VALUE_FLAGS_NONE,
@ -226,7 +305,7 @@ update_categories_from_language (MctUserControls *self)
gsize i; gsize i;
g_autofree gchar *disabled_action = NULL; g_autofree gchar *disabled_action = NULL;
rating_system = get_content_rating_system (self->user); rating_system = get_content_rating_system (self);
rating_system_str = gs_content_rating_system_to_str (rating_system); rating_system_str = gs_content_rating_system_to_str (rating_system);
g_debug ("Using rating system %s", rating_system_str); g_debug ("Using rating system %s", rating_system_str);
@ -306,7 +385,7 @@ update_oars_level (MctUserControls *self)
g_debug ("Effective age for this user: %u; %s", maximum_age, g_debug ("Effective age for this user: %u; %s", maximum_age,
all_categories_unset ? "all categories unset" : "some categories set"); all_categories_unset ? "all categories unset" : "some categories set");
rating_system = get_content_rating_system (self->user); rating_system = get_content_rating_system (self);
rating_age_category = gs_utils_content_rating_age_to_str (rating_system, maximum_age); rating_age_category = gs_utils_content_rating_age_to_str (rating_system, maximum_age);
/* Unrestricted? */ /* Unrestricted? */
@ -323,7 +402,7 @@ update_allow_app_installation (MctUserControls *self)
gboolean allow_user_installation; gboolean allow_user_installation;
gboolean non_admin_user = TRUE; gboolean non_admin_user = TRUE;
if (act_user_get_account_type (self->user) == ACT_USER_ACCOUNT_TYPE_ADMINISTRATOR) if (self->user_account_type == ACT_USER_ACCOUNT_TYPE_ADMINISTRATOR)
non_admin_user = FALSE; non_admin_user = FALSE;
/* Admins are always allowed to install apps for all users. This behaviour is governed /* Admins are always allowed to install apps for all users. This behaviour is governed
@ -334,8 +413,8 @@ update_allow_app_installation (MctUserControls *self)
/* If user is admin, we are done here, bail out. */ /* If user is admin, we are done here, bail out. */
if (!non_admin_user) if (!non_admin_user)
{ {
g_debug ("User %s is administrator, hiding app installation controls", g_debug ("User %s is an administrator, hiding app installation controls",
act_user_get_user_name (self->user)); self->user_display_name);
return; return;
} }
@ -427,63 +506,16 @@ blacklist_apps_cb (gpointer data)
g_autoptr(MctAppFilter) new_filter = NULL; g_autoptr(MctAppFilter) new_filter = NULL;
g_autoptr(GError) error = NULL; g_autoptr(GError) error = NULL;
MctUserControls *self = data; MctUserControls *self = data;
gboolean allow_web_browsers;
gsize i;
self->blacklist_apps_source_id = 0; self->blacklist_apps_source_id = 0;
g_debug ("Building parental controls settings…"); if (self->user == NULL)
/* Blacklist */
g_debug ("\t → Blacklisting apps");
mct_restrict_applications_dialog_build_app_filter (self->restrict_applications_dialog, &builder);
/* Maturity level */
g_debug ("\t → Maturity level");
if (self->selected_age == oars_disabled_age)
g_debug ("\t\t → Disabled");
for (i = 0; self->selected_age != oars_disabled_age && oars_categories[i] != NULL; i++)
{ {
MctAppFilterOarsValue oars_value; g_debug ("Not saving app filter as user is unset");
const gchar *oars_category; return G_SOURCE_REMOVE;
oars_category = oars_categories[i];
oars_value = as_content_rating_id_csm_age_to_value (oars_category, self->selected_age);
g_debug ("\t\t → %s: %s", oars_category, oars_value_to_string (oars_value));
mct_app_filter_builder_set_oars_value (&builder, oars_category, oars_value);
}
/* Web browsers */
allow_web_browsers = gtk_switch_get_active (self->allow_web_browsers_switch);
g_debug ("\t → %s web browsers", allow_web_browsers ? "Enabling" : "Disabling");
if (!allow_web_browsers)
mct_app_filter_builder_blacklist_content_type (&builder, WEB_BROWSERS_CONTENT_TYPE);
/* App installation */
if (act_user_get_account_type (self->user) != ACT_USER_ACCOUNT_TYPE_ADMINISTRATOR)
{
gboolean allow_system_installation;
gboolean allow_user_installation;
allow_system_installation = gtk_switch_get_active (self->allow_system_installation_switch);
allow_user_installation = gtk_switch_get_active (self->allow_user_installation_switch);
g_debug ("\t → %s system installation", allow_system_installation ? "Enabling" : "Disabling");
g_debug ("\t → %s user installation", allow_user_installation ? "Enabling" : "Disabling");
mct_app_filter_builder_set_allow_user_installation (&builder, allow_user_installation);
mct_app_filter_builder_set_allow_system_installation (&builder, allow_system_installation);
} }
mct_user_controls_build_app_filter (self, &builder);
new_filter = mct_app_filter_builder_end (&builder); new_filter = mct_app_filter_builder_end (&builder);
/* FIXME: should become asynchronous */ /* FIXME: should become asynchronous */
@ -549,7 +581,7 @@ on_restrict_applications_button_clicked_cb (GtkButton *button,
gtk_window_set_transient_for (GTK_WINDOW (self->restrict_applications_dialog), gtk_window_set_transient_for (GTK_WINDOW (self->restrict_applications_dialog),
GTK_WINDOW (toplevel)); GTK_WINDOW (toplevel));
mct_restrict_applications_dialog_set_user (self->restrict_applications_dialog, self->user); mct_restrict_applications_dialog_set_user_display_name (self->restrict_applications_dialog, self->user_display_name);
mct_restrict_applications_dialog_set_app_filter (self->restrict_applications_dialog, self->filter); mct_restrict_applications_dialog_set_app_filter (self->restrict_applications_dialog, self->filter);
gtk_widget_show (GTK_WIDGET (self->restrict_applications_dialog)); gtk_widget_show (GTK_WIDGET (self->restrict_applications_dialog));
@ -598,7 +630,7 @@ on_set_age_action_activated (GSimpleAction *action,
self = MCT_USER_CONTROLS (user_data); self = MCT_USER_CONTROLS (user_data);
age = g_variant_get_uint32 (param); age = g_variant_get_uint32 (param);
rating_system = get_content_rating_system (self->user); rating_system = get_content_rating_system (self);
entries = gs_utils_content_rating_get_values (rating_system); entries = gs_utils_content_rating_get_values (rating_system);
ages = gs_utils_content_rating_get_ages (rating_system); ages = gs_utils_content_rating_get_ages (rating_system);
@ -640,6 +672,8 @@ mct_user_controls_finalize (GObject *object)
g_clear_object (&self->action_group); g_clear_object (&self->action_group);
g_clear_object (&self->cancellable); g_clear_object (&self->cancellable);
g_clear_object (&self->user); g_clear_object (&self->user);
g_clear_pointer (&self->user_locale, g_free);
g_clear_pointer (&self->user_display_name, g_free);
if (self->permission != NULL && self->permission_allowed_id != 0) if (self->permission != NULL && self->permission_allowed_id != 0)
{ {
@ -682,7 +716,7 @@ mct_user_controls_get_property (GObject *object,
{ {
MctUserControls *self = MCT_USER_CONTROLS (object); MctUserControls *self = MCT_USER_CONTROLS (object);
switch (prop_id) switch ((MctUserControlsProperty) prop_id)
{ {
case PROP_USER: case PROP_USER:
g_value_set_object (value, self->user); g_value_set_object (value, self->user);
@ -692,6 +726,22 @@ mct_user_controls_get_property (GObject *object,
g_value_set_object (value, self->permission); g_value_set_object (value, self->permission);
break; break;
case PROP_APP_FILTER:
g_value_set_boxed (value, self->filter);
break;
case PROP_USER_ACCOUNT_TYPE:
g_value_set_enum (value, self->user_account_type);
break;
case PROP_USER_LOCALE:
g_value_set_string (value, self->user_locale);
break;
case PROP_USER_DISPLAY_NAME:
g_value_set_string (value, self->user_display_name);
break;
default: default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
} }
@ -705,7 +755,7 @@ mct_user_controls_set_property (GObject *object,
{ {
MctUserControls *self = MCT_USER_CONTROLS (object); MctUserControls *self = MCT_USER_CONTROLS (object);
switch (prop_id) switch ((MctUserControlsProperty) prop_id)
{ {
case PROP_USER: case PROP_USER:
mct_user_controls_set_user (self, g_value_get_object (value)); mct_user_controls_set_user (self, g_value_get_object (value));
@ -715,6 +765,22 @@ mct_user_controls_set_property (GObject *object,
mct_user_controls_set_permission (self, g_value_get_object (value)); mct_user_controls_set_permission (self, g_value_get_object (value));
break; break;
case PROP_APP_FILTER:
mct_user_controls_set_app_filter (self, g_value_get_boxed (value));
break;
case PROP_USER_ACCOUNT_TYPE:
mct_user_controls_set_user_account_type (self, g_value_get_enum (value));
break;
case PROP_USER_LOCALE:
mct_user_controls_set_user_locale (self, g_value_get_string (value));
break;
case PROP_USER_DISPLAY_NAME:
mct_user_controls_set_user_display_name (self, g_value_get_string (value));
break;
default: default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
} }
@ -747,7 +813,92 @@ mct_user_controls_class_init (MctUserControlsClass *klass)
G_PARAM_STATIC_STRINGS | G_PARAM_STATIC_STRINGS |
G_PARAM_EXPLICIT_NOTIFY); G_PARAM_EXPLICIT_NOTIFY);
g_object_class_install_properties (object_class, N_PROPS, properties); /**
* MctUserControls:app-filter: (nullable)
*
* The users current app filter, used to set up the user controls. As app
* filters are immutable, it is not updated as the user controls are changed.
* Use mct_user_controls_build_app_filter() to build the new app filter.
*
* This may be %NULL if the app filter is unknown, or if querying it from
* #MctUserControls:user fails.
*
* Since: 0.5.0
*/
properties[PROP_APP_FILTER] =
g_param_spec_boxed ("app-filter",
"App Filter",
"The users current app filter, used to set up the user controls, or %NULL if unknown.",
MCT_TYPE_APP_FILTER,
G_PARAM_READWRITE |
G_PARAM_STATIC_STRINGS |
G_PARAM_EXPLICIT_NOTIFY);
/**
* MctUserControls:user-account-type:
*
* The type of the currently selected user account.
*
* Since: 0.5.0
*/
properties[PROP_USER_ACCOUNT_TYPE] =
g_param_spec_enum ("user-account-type",
"User Account Type",
"The type of the currently selected user account.",
/* FIXME: Not a typo here; libaccountsservice uses the wrong namespace.
* See: https://gitlab.freedesktop.org/accountsservice/accountsservice/issues/84 */
ACT_USER_TYPE_USER_ACCOUNT_TYPE,
ACT_USER_ACCOUNT_TYPE_STANDARD,
G_PARAM_READWRITE |
G_PARAM_STATIC_STRINGS |
G_PARAM_EXPLICIT_NOTIFY);
/**
* MctUserControls:user-locale: (nullable)
*
* The locale for the currently selected user account, or %NULL if no
* user is selected.
*
* If set, it must be in the format documented by [`setlocale()`](man:setlocale(3)):
* ```
* language[_territory][.codeset][@modifier]
* ```
* where `language` is an ISO 639 language code, `territory` is an ISO 3166
* country code, and `codeset` is a character set or encoding identifier like
* `ISO-8859-1` or `UTF-8`.
*
* Since: 0.5.0
*/
properties[PROP_USER_LOCALE] =
g_param_spec_string ("user-locale",
"User Locale",
"The locale for the currently selected user account, or %NULL if no user is selected.",
NULL,
G_PARAM_READWRITE |
G_PARAM_STATIC_STRINGS |
G_PARAM_EXPLICIT_NOTIFY);
/**
* MctUserControls:user-display-name: (nullable)
*
* The display name for the currently selected user account, or %NULL if no
* user is selected. This will typically be the users full name (if known)
* or their username.
*
* If set, it must be valid UTF-8 and non-empty.
*
* Since: 0.5.0
*/
properties[PROP_USER_DISPLAY_NAME] =
g_param_spec_string ("user-display-name",
"User Display Name",
"The display name for the currently selected user account, or %NULL if no user is selected.",
NULL,
G_PARAM_READWRITE |
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/MalcontentUi/ui/user-controls.ui"); gtk_widget_class_set_template_from_resource (widget_class, "/org/freedesktop/MalcontentUi/ui/user-controls.ui");
@ -808,6 +959,16 @@ mct_user_controls_init (MctUserControls *self)
G_BINDING_DEFAULT); G_BINDING_DEFAULT);
} }
/**
* mct_user_controls_get_user:
* @self: an #MctUserControls
*
* Get the value of #MctUserControls:user.
*
* Returns: (transfer none) (nullable): the user the controls are configured for,
* or %NULL if unknown
* Since: 0.5.0
*/
ActUser * ActUser *
mct_user_controls_get_user (MctUserControls *self) mct_user_controls_get_user (MctUserControls *self)
{ {
@ -816,6 +977,16 @@ mct_user_controls_get_user (MctUserControls *self)
return self->user; return self->user;
} }
/**
* mct_user_controls_set_user:
* @self: an #MctUserControls
* @user: (nullable) (transfer none): the user to configure the controls for,
* or %NULL if unknown
*
* Set the value of #MctUserControls:user.
*
* Since: 0.5.0
*/
void void
mct_user_controls_set_user (MctUserControls *self, mct_user_controls_set_user (MctUserControls *self,
ActUser *user) ActUser *user)
@ -829,10 +1000,21 @@ mct_user_controls_set_user (MctUserControls *self,
if (g_set_object (&self->user, user)) if (g_set_object (&self->user, user))
{ {
update_app_filter (self); g_object_freeze_notify (G_OBJECT (self));
/* Update the starting widget state from the user. */
if (user != NULL)
{
mct_user_controls_set_user_account_type (self, act_user_get_account_type (user));
mct_user_controls_set_user_locale (self, get_user_locale (user));
mct_user_controls_set_user_display_name (self, get_user_display_name (user));
}
update_app_filter_from_user (self);
setup_parental_control_settings (self); setup_parental_control_settings (self);
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_USER]); g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_USER]);
g_object_thaw_notify (G_OBJECT (self));
} }
} }
@ -843,11 +1025,22 @@ on_permission_allowed_cb (GObject *obj,
{ {
MctUserControls *self = MCT_USER_CONTROLS (user_data); MctUserControls *self = MCT_USER_CONTROLS (user_data);
update_app_filter (self); update_app_filter_from_user (self);
setup_parental_control_settings (self); setup_parental_control_settings (self);
} }
GPermission * /* (nullable) */ /**
* mct_user_controls_get_permission:
* @self: an #MctUserControls
*
* Get the value of #MctUserControls:permission.
*
* Returns: (transfer none) (nullable): a #GPermission indicating whether the
* current user has permission to view or change parental controls, or %NULL
* if permission is not allowed or is unknown
* Since: 0.5.0
*/
GPermission *
mct_user_controls_get_permission (MctUserControls *self) mct_user_controls_get_permission (MctUserControls *self)
{ {
g_return_val_if_fail (MCT_IS_USER_CONTROLS (self), NULL); g_return_val_if_fail (MCT_IS_USER_CONTROLS (self), NULL);
@ -855,9 +1048,20 @@ mct_user_controls_get_permission (MctUserControls *self)
return self->permission; return self->permission;
} }
/**
* mct_user_controls_set_permission:
* @self: an #MctUserControls
* @permission: (nullable) (transfer none): the #GPermission indicating whether
* the current user has permission to view or change parental controls, or
* %NULL if permission is not allowed or is unknown
*
* Set the value of #MctUserControls:permission.
*
* Since: 0.5.0
*/
void void
mct_user_controls_set_permission (MctUserControls *self, mct_user_controls_set_permission (MctUserControls *self,
GPermission *permission /* (nullable) */) GPermission *permission)
{ {
g_return_if_fail (MCT_IS_USER_CONTROLS (self)); g_return_if_fail (MCT_IS_USER_CONTROLS (self));
g_return_if_fail (permission == NULL || G_IS_PERMISSION (permission)); g_return_if_fail (permission == NULL || G_IS_PERMISSION (permission));
@ -883,8 +1087,287 @@ mct_user_controls_set_permission (MctUserControls *self,
} }
/* Handle changes. */ /* Handle changes. */
update_app_filter (self); update_app_filter_from_user (self);
setup_parental_control_settings (self); setup_parental_control_settings (self);
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_PERMISSION]); g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_PERMISSION]);
} }
/**
* mct_user_controls_get_app_filter:
* @self: an #MctUserControls
*
* Get the value of #MctUserControls:app-filter. If the app filter is unknown
* or could not be retrieved from #MctUserControls:user, this will be %NULL.
*
* Returns: (transfer none) (nullable): the initial app filter used to
* populate the user controls, or %NULL if unknown
* Since: 0.5.0
*/
MctAppFilter *
mct_user_controls_get_app_filter (MctUserControls *self)
{
g_return_val_if_fail (MCT_IS_USER_CONTROLS (self), NULL);
return self->filter;
}
/**
* mct_user_controls_set_app_filter:
* @self: an #MctUserControls
* @app_filter: (nullable) (transfer none): the app filter to configure the user
* controls from, or %NULL if unknown
*
* Set the value of #MctUserControls:app-filter.
*
* This will overwrite any user changes to the controls, so they should be saved
* first using mct_user_controls_build_app_filter() if desired. They will be
* saved automatically if #MctUserControls:user is set.
*
* Since: 0.5.0
*/
void
mct_user_controls_set_app_filter (MctUserControls *self,
MctAppFilter *app_filter)
{
g_return_if_fail (MCT_IS_USER_CONTROLS (self));
/* If we have pending unsaved changes from the previous configuration, force
* them to be saved first. */
flush_update_blacklisted_apps (self);
if (self->filter == app_filter)
return;
g_clear_pointer (&self->filter, mct_app_filter_unref);
if (app_filter != NULL)
self->filter = mct_app_filter_ref (app_filter);
g_debug ("Set new app filter from caller");
setup_parental_control_settings (self);
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_APP_FILTER]);
}
/**
* mct_user_controls_get_user_account_type:
* @self: an #MctUserControls
*
* Get the value of #MctUserControls:user-account-type.
*
* Returns: the account type of the user the controls are configured for
* Since: 0.5.0
*/
ActUserAccountType
mct_user_controls_get_user_account_type (MctUserControls *self)
{
g_return_val_if_fail (MCT_IS_USER_CONTROLS (self), ACT_USER_ACCOUNT_TYPE_STANDARD);
return self->user_account_type;
}
/**
* mct_user_controls_set_user_account_type:
* @self: an #MctUserControls
* @user_account_type: the account type of the user to configure the controls for
*
* Set the value of #MctUserControls:user-account-type.
*
* Since: 0.5.0
*/
void
mct_user_controls_set_user_account_type (MctUserControls *self,
ActUserAccountType user_account_type)
{
g_return_if_fail (MCT_IS_USER_CONTROLS (self));
/* If we have pending unsaved changes from the previous user, force them to be
* saved first. */
flush_update_blacklisted_apps (self);
if (self->user_account_type == user_account_type)
return;
self->user_account_type = user_account_type;
setup_parental_control_settings (self);
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_USER_ACCOUNT_TYPE]);
}
/**
* mct_user_controls_get_user_locale:
* @self: an #MctUserControls
*
* Get the value of #MctUserControls:user-locale.
*
* Returns: (transfer none) (nullable): the locale of the user the controls
* are configured for, or %NULL if unknown
* Since: 0.5.0
*/
const gchar *
mct_user_controls_get_user_locale (MctUserControls *self)
{
g_return_val_if_fail (MCT_IS_USER_CONTROLS (self), NULL);
return self->user_locale;
}
/**
* mct_user_controls_set_user_locale:
* @self: an #MctUserControls
* @user_locale: (nullable) (transfer none): the locale of the user
* to configure the controls for, or %NULL if unknown
*
* Set the value of #MctUserControls:user-locale.
*
* Since: 0.5.0
*/
void
mct_user_controls_set_user_locale (MctUserControls *self,
const gchar *user_locale)
{
g_return_if_fail (MCT_IS_USER_CONTROLS (self));
g_return_if_fail (user_locale == NULL ||
(*user_locale != '\0' &&
g_utf8_validate (user_locale, -1, NULL)));
/* If we have pending unsaved changes from the previous user, force them to be
* saved first. */
flush_update_blacklisted_apps (self);
if (g_strcmp0 (self->user_locale, user_locale) == 0)
return;
g_clear_pointer (&self->user_locale, g_free);
self->user_locale = g_strdup (user_locale);
setup_parental_control_settings (self);
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_USER_LOCALE]);
}
/**
* mct_user_controls_get_user_display_name:
* @self: an #MctUserControls
*
* Get the value of #MctUserControls:user-display-name.
*
* Returns: (transfer none) (nullable): the display name of the user the controls
* are configured for, or %NULL if unknown
* Since: 0.5.0
*/
const gchar *
mct_user_controls_get_user_display_name (MctUserControls *self)
{
g_return_val_if_fail (MCT_IS_USER_CONTROLS (self), NULL);
return self->user_display_name;
}
/**
* mct_user_controls_set_user_display_name:
* @self: an #MctUserControls
* @user_display_name: (nullable) (transfer none): the display name of the user
* to configure the controls for, or %NULL if unknown
*
* Set the value of #MctUserControls:user-display-name.
*
* Since: 0.5.0
*/
void
mct_user_controls_set_user_display_name (MctUserControls *self,
const gchar *user_display_name)
{
g_return_if_fail (MCT_IS_USER_CONTROLS (self));
g_return_if_fail (user_display_name == NULL ||
(*user_display_name != '\0' &&
g_utf8_validate (user_display_name, -1, NULL)));
/* If we have pending unsaved changes from the previous user, force them to be
* saved first. */
flush_update_blacklisted_apps (self);
if (g_strcmp0 (self->user_display_name, user_display_name) == 0)
return;
g_clear_pointer (&self->user_display_name, g_free);
self->user_display_name = g_strdup (user_display_name);
setup_parental_control_settings (self);
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_USER_DISPLAY_NAME]);
}
/**
* mct_user_controls_build_app_filter:
* @self: an #MctUserControls
* @builder: an existing #MctAppFilterBuilder to modify
*
* Get the app filter settings currently configured in the user controls, by
* modifying the given @builder. This can be used to save the settings manually.
*
* Since: 0.5.0
*/
void
mct_user_controls_build_app_filter (MctUserControls *self,
MctAppFilterBuilder *builder)
{
gboolean allow_web_browsers;
gsize i;
g_return_if_fail (MCT_IS_USER_CONTROLS (self));
g_return_if_fail (builder != NULL);
g_debug ("Building parental controls settings…");
/* Blacklist */
g_debug ("\t → Blacklisting apps");
mct_restrict_applications_dialog_build_app_filter (self->restrict_applications_dialog, builder);
/* Maturity level */
g_debug ("\t → Maturity level");
if (self->selected_age == oars_disabled_age)
g_debug ("\t\t → Disabled");
for (i = 0; self->selected_age != oars_disabled_age && oars_categories[i] != NULL; i++)
{
MctAppFilterOarsValue oars_value;
const gchar *oars_category;
oars_category = oars_categories[i];
oars_value = as_content_rating_id_csm_age_to_value (oars_category, self->selected_age);
g_debug ("\t\t → %s: %s", oars_category, oars_value_to_string (oars_value));
mct_app_filter_builder_set_oars_value (builder, oars_category, oars_value);
}
/* Web browsers */
allow_web_browsers = gtk_switch_get_active (self->allow_web_browsers_switch);
g_debug ("\t → %s web browsers", allow_web_browsers ? "Enabling" : "Disabling");
if (!allow_web_browsers)
mct_app_filter_builder_blacklist_content_type (builder, WEB_BROWSERS_CONTENT_TYPE);
/* App installation */
if (self->user_account_type != ACT_USER_ACCOUNT_TYPE_ADMINISTRATOR)
{
gboolean allow_system_installation;
gboolean allow_user_installation;
allow_system_installation = gtk_switch_get_active (self->allow_system_installation_switch);
allow_user_installation = gtk_switch_get_active (self->allow_user_installation_switch);
g_debug ("\t → %s system installation", allow_system_installation ? "Enabling" : "Disabling");
g_debug ("\t → %s user installation", allow_user_installation ? "Enabling" : "Disabling");
mct_app_filter_builder_set_allow_user_installation (builder, allow_user_installation);
mct_app_filter_builder_set_allow_system_installation (builder, allow_system_installation);
}
}

View File

@ -23,7 +23,11 @@
#pragma once #pragma once
#include <act/act.h> #include <act/act.h>
#include <gio/gio.h>
#include <glib.h>
#include <glib-object.h>
#include <gtk/gtk.h> #include <gtk/gtk.h>
#include <libmalcontent/malcontent.h>
G_BEGIN_DECLS G_BEGIN_DECLS
@ -39,4 +43,23 @@ GPermission *mct_user_controls_get_permission (MctUserControls *self);
void mct_user_controls_set_permission (MctUserControls *self, void mct_user_controls_set_permission (MctUserControls *self,
GPermission *permission); GPermission *permission);
MctAppFilter *mct_user_controls_get_app_filter (MctUserControls *self);
void mct_user_controls_set_app_filter (MctUserControls *self,
MctAppFilter *app_filter);
ActUserAccountType mct_user_controls_get_user_account_type (MctUserControls *self);
void mct_user_controls_set_user_account_type (MctUserControls *self,
ActUserAccountType user_account_type);
const gchar *mct_user_controls_get_user_locale (MctUserControls *self);
void mct_user_controls_set_user_locale (MctUserControls *self,
const gchar *user_locale);
const gchar *mct_user_controls_get_user_display_name (MctUserControls *self);
void mct_user_controls_set_user_display_name (MctUserControls *self,
const gchar *user_display_name);
void mct_user_controls_build_app_filter (MctUserControls *self,
MctAppFilterBuilder *builder);
G_END_DECLS G_END_DECLS

View File

@ -56,7 +56,7 @@ pkgconfig.generate(libmalcontent,
libraries_private: libmalcontent_private_deps, libraries_private: libmalcontent_private_deps,
) )
gnome.generate_gir(libmalcontent, libmalcontent_gir = gnome.generate_gir(libmalcontent,
sources: libmalcontent_sources + libmalcontent_headers + libmalcontent_private_headers, sources: libmalcontent_sources + libmalcontent_headers + libmalcontent_private_headers,
nsversion: libmalcontent_api_version, nsversion: libmalcontent_api_version,
namespace: 'Malcontent', namespace: 'Malcontent',