*
* Copyright 2026 Alexander Vanhee
*
* 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 3 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 <https://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#include "config.h"
#include <appstream.h>
#include <libmalcontent/malcontent.h>
#include <unistd.h>
#include "bz-malcontent-service.h"
struct _BzMalcontentService
{
GObject parent_instance;
MctManager *manager;
BzStateInfo *state;
GDBusConnection *bus;
MctAppFilter *filter;
gulong changed_id;
};
G_DEFINE_FINAL_TYPE (BzMalcontentService, bz_malcontent_service, G_TYPE_OBJECT)
static void fetch_blocked_ids (BzMalcontentService *self);
static void apply_filter (BzMalcontentService *self,
MctAppFilter *filter);
static void on_filter_loaded (GObject *source,
GAsyncResult *result,
gpointer user_data);
static void on_filter_changed (MctManager *manager,
guint64 user_id,
gpointer user_data);
static void
bz_malcontent_service_dispose (GObject *object)
{
BzMalcontentService *self = NULL;
self = BZ_MALCONTENT_SERVICE (object);
g_signal_handler_disconnect (self->manager, self->changed_id);
g_clear_object (&self->manager);
g_clear_object (&self->state);
g_clear_object (&self->bus);
g_clear_pointer (&self->filter, mct_app_filter_unref);
G_OBJECT_CLASS (bz_malcontent_service_parent_class)->dispose (object);
}
static void
bz_malcontent_service_class_init (BzMalcontentServiceClass *klass)
{
G_OBJECT_CLASS (klass)->dispose = bz_malcontent_service_dispose;
}
static void
bz_malcontent_service_init (BzMalcontentService *self)
{
}
BzMalcontentService *
bz_malcontent_service_new (GDBusConnection *bus,
BzStateInfo *state)
{
BzMalcontentService *self = NULL;
self = g_object_new (BZ_TYPE_MALCONTENT_SERVICE, NULL);
self->state = g_object_ref (state);
self->bus = g_object_ref (bus);
self->manager = mct_manager_new (bus);
self->changed_id = g_signal_connect (self->manager,
"app-filter-changed",
G_CALLBACK (on_filter_changed),
self);
return self;
}
void
bz_malcontent_service_start (BzMalcontentService *self)
{
mct_manager_get_app_filter_async (self->manager,
getuid (),
MCT_MANAGER_GET_VALUE_FLAGS_INTERACTIVE,
NULL,
on_filter_loaded,
self);
}
static void
fetch_blocked_ids (BzMalcontentService *self)
{
g_autoptr (GError) local_error = NULL;
g_autoptr (GVariant) result = NULL;
g_autoptr (GVariant) prop = NULL;
g_autoptr (GVariant) filter_tuple = NULL;
g_autoptr (GtkStringList) blocked = NULL;
g_autofree char *object_path = NULL;
gboolean is_allowlist = FALSE;
GVariantIter *iter = NULL;
const char *ref = NULL;
object_path = g_strdup_printf ("/org/freedesktop/Accounts/User%d", (int) getuid ());
result = g_dbus_connection_call_sync (
self->bus,
"org.freedesktop.Accounts",
object_path,
"org.freedesktop.DBus.Properties",
"Get",
g_variant_new ("(ss)",
"com.endlessm.ParentalControls.AppFilter",
"AppFilter"),
G_VARIANT_TYPE ("(v)"),
G_DBUS_CALL_FLAGS_NONE,
-1,
NULL,
&local_error);
if (result == NULL)
{
g_warning ("failed to fetch malcontent AppFilter from D-Bus: %s", local_error->message);
return;
}
prop = g_variant_get_child_value (result, 0);
filter_tuple = g_variant_get_variant (prop);
g_variant_get (filter_tuple, "(bas)", &is_allowlist, &iter);
blocked = gtk_string_list_new (NULL);
while (g_variant_iter_next (iter, "s", &ref))
gtk_string_list_append (blocked, ref);
g_variant_iter_free (iter);
bz_state_info_set_parental_blocked_ids (self->state, G_LIST_MODEL (blocked));
}
static void
apply_filter (BzMalcontentService *self,
MctAppFilter *filter)
{
const char *const *oars_sections = NULL;
int max_age = -1;
g_clear_pointer (&self->filter, mct_app_filter_unref);
self->filter = mct_app_filter_ref (filter);
oars_sections = mct_app_filter_get_oars_sections (self->filter);
for (gsize i = 0; oars_sections[i] != NULL; i++)
{
MctAppFilterOarsValue filter_value;
int section_age;
filter_value = mct_app_filter_get_oars_value (self->filter, oars_sections[i]);
if (filter_value == MCT_APP_FILTER_OARS_VALUE_UNKNOWN)
continue;
section_age = as_content_rating_attribute_to_csm_age (oars_sections[i],
(AsContentRatingValue) filter_value);
if (section_age > max_age)
max_age = section_age;
}
bz_state_info_set_parental_age_rating (self->state, max_age);
fetch_blocked_ids (self);
}
static void
on_filter_loaded (GObject *source,
GAsyncResult *result,
gpointer user_data)
{
BzMalcontentService *self = NULL;
g_autoptr (MctAppFilter) filter = NULL;
g_autoptr (GError) local_error = NULL;
self = user_data;
filter = mct_manager_get_app_filter_finish (self->manager, result, &local_error);
if (filter != NULL)
apply_filter (self, filter);
else
g_warning ("Failed to load malcontent app filter: %s", local_error->message);
}
static void
on_filter_changed (MctManager *manager,
guint64 user_id,
gpointer user_data)
{
BzMalcontentService *self = NULL;
self = user_data;
if (user_id != (guint64) getuid ())
return;
mct_manager_get_app_filter_async (self->manager,
getuid (),
MCT_MANAGER_GET_VALUE_FLAGS_NONE,
NULL,
on_filter_loaded,
self);
}