/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
 *
 * Copyright 2024, 2025 GNOME Foundation, Inc.
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * Authors:
 *  - Philip Withnall <pwithnall@gnome.org>
 */

#include "config.h"

#include <errno.h>
#include <glib.h>
#include <glib-unix.h>
#include <glib/gi18n-lib.h>
#include <gio/gio.h>
#include <libmalcontent/manager.h>
#include <libmalcontent-web/filtering-dbus-service.h>
#include <libmalcontent-web/filter-updater.h>
#include <pwd.h>
#include <string.h>
#include <sys/types.h>

#include "filtering-iface.h"
#include "enums.h"


static void mct_filtering_dbus_service_constructed  (GObject      *object);
static void mct_filtering_dbus_service_dispose      (GObject      *object);
static void mct_filtering_dbus_service_get_property (GObject    *object,
                                                  guint       property_id,
                                                  GValue     *value,
                                                  GParamSpec *pspec);
static void mct_filtering_dbus_service_set_property (GObject      *object,
                                                  guint         property_id,
                                                  const GValue *value,
                                                  GParamSpec   *pspec);

static void mct_filtering_dbus_service_method_call (GDBusConnection       *connection,
                                                 const char            *sender,
                                                 const char            *object_path,
                                                 const char            *interface_name,
                                                 const char            *method_name,
                                                 GVariant              *parameters,
                                                 GDBusMethodInvocation *invocation,
                                                 void                  *user_data);
static void mct_filtering_dbus_service_properties_get (MctFilteringDBusService  *self,
                                                    GDBusConnection       *connection,
                                                    const char            *sender,
                                                    GVariant              *parameters,
                                                    GDBusMethodInvocation *invocation);
static void mct_filtering_dbus_service_properties_set (MctFilteringDBusService  *self,
                                                    GDBusConnection       *connection,
                                                    const char            *sender,
                                                    GVariant              *parameters,
                                                    GDBusMethodInvocation *invocation);
static void mct_filtering_dbus_service_properties_get_all (MctFilteringDBusService  *self,
                                                        GDBusConnection       *connection,
                                                        const char            *sender,
                                                        GVariant              *parameters,
                                                        GDBusMethodInvocation *invocation);

static void mct_filtering_dbus_service_update_filters (MctFilteringDBusService *self,
                                                       GDBusConnection         *connection,
                                                       const char              *sender,
                                                       GVariant                *parameters,
                                                       GDBusMethodInvocation   *invocation);

/* These errors do go over the bus, and are registered in mct_filtering_dbus_service_class_init(). */
static const gchar *filtering_dbus_service_errors[] =
{
  "org.freedesktop.MalcontentWeb1.Filtering.Error.Busy",
  "org.freedesktop.MalcontentWeb1.Filtering.Error.Disabled",
  "org.freedesktop.MalcontentWeb1.Filtering.Error.QueryingPolicy",
  "org.freedesktop.MalcontentWeb1.Filtering.Error.InvalidFilterFormat",
  "org.freedesktop.MalcontentWeb1.Filtering.Error.FileSystem",
  "org.freedesktop.MalcontentWeb1.Filtering.Error.Downloading",
};
static const GDBusErrorEntry filtering_dbus_service_error_map[] =
  {
    { MCT_FILTER_UPDATER_ERROR_BUSY, "org.freedesktop.MalcontentWeb1.Filtering.Error.Busy" },
    { MCT_FILTER_UPDATER_ERROR_DISABLED, "org.freedesktop.MalcontentWeb1.Filtering.Error.Disabled" },
    { MCT_FILTER_UPDATER_ERROR_QUERYING_POLICY, "org.freedesktop.MalcontentWeb1.Filtering.Error.QueryingPolicy" },
    { MCT_FILTER_UPDATER_ERROR_INVALID_FILTER_FORMAT, "org.freedesktop.MalcontentWeb1.Filtering.Error.InvalidFilterFormat" },
    { MCT_FILTER_UPDATER_ERROR_FILE_SYSTEM, "org.freedesktop.MalcontentWeb1.Filtering.Error.FileSystem" },
    { MCT_FILTER_UPDATER_ERROR_DOWNLOADING, "org.freedesktop.MalcontentWeb1.Filtering.Error.Downloading" },

  };
G_STATIC_ASSERT (G_N_ELEMENTS (filtering_dbus_service_error_map) == MCT_FILTER_UPDATER_N_ERRORS);
G_STATIC_ASSERT (G_N_ELEMENTS (filtering_dbus_service_error_map) == G_N_ELEMENTS (filtering_dbus_service_errors));

/**
 * MctFilteringDBusService:
 *
 * An implementation of the `org.freedesktop.MalcontentWeb1.Filtering` D-Bus
 * interface, providing a way for unprivileged processes to request that the
 * compiled web filters for one or more child users are updated.
 *
 * This will expose all the necessary objects on the bus for peers to interact
 * with them, and hooks them up to internal state management using
 * [property@Malcontent.FilteringDBusService:filter-updater].
 *
 * Since: 0.14.0
 */
struct _MctFilteringDBusService
{
  GObject parent;

  GDBusConnection *connection;  /* (owned) */
  char *object_path;  /* (owned) */
  unsigned int object_id;

  /* Used to cancel any pending operations when the object is unregistered. */
  GCancellable *cancellable;  /* (owned) */

  MctFilterUpdater *filter_updater;  /* (owned) */
  unsigned int n_pending_operations;
};

typedef enum
{
  PROP_CONNECTION = 1,
  PROP_OBJECT_PATH,
  PROP_FILTER_UPDATER,
  PROP_BUSY,
} MctFilteringDBusServiceProperty;

static GParamSpec *props[PROP_BUSY + 1] = { NULL, };

G_DEFINE_TYPE (MctFilteringDBusService, mct_filtering_dbus_service, G_TYPE_OBJECT)

static void
mct_filtering_dbus_service_class_init (MctFilteringDBusServiceClass *klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;

  object_class->constructed = mct_filtering_dbus_service_constructed;
  object_class->dispose = mct_filtering_dbus_service_dispose;
  object_class->get_property = mct_filtering_dbus_service_get_property;
  object_class->set_property = mct_filtering_dbus_service_set_property;

  /**
   * MctFilteringDBusService:connection:
   *
   * D-Bus connection to export objects on.
   *
   * Since: 0.14.0
   */
  props[PROP_CONNECTION] =
      g_param_spec_object ("connection", NULL, NULL,
                           G_TYPE_DBUS_CONNECTION,
                           G_PARAM_READWRITE |
                           G_PARAM_CONSTRUCT_ONLY |
                           G_PARAM_STATIC_STRINGS);

  /**
   * MctFilteringDBusService:object-path:
   *
   * Object path to root all exported objects at. If this does not end in a
   * slash, one will be added.
   *
   * Since: 0.14.0
   */
  props[PROP_OBJECT_PATH] =
      g_param_spec_string ("object-path", NULL, NULL,
                           "/",
                           G_PARAM_READWRITE |
                           G_PARAM_CONSTRUCT_ONLY |
                           G_PARAM_STATIC_STRINGS);

  /**
   * MctFilteringDBusService:filter-updater:
   *
   * Helper object to update cached and compiled filters.
   *
   * Since: 0.14.0
   */
  props[PROP_FILTER_UPDATER] =
      g_param_spec_object ("filter-updater", NULL, NULL,
                           MCT_TYPE_FILTER_UPDATER,
                           G_PARAM_READWRITE |
                           G_PARAM_CONSTRUCT_ONLY |
                           G_PARAM_STATIC_STRINGS);

  /**
   * MctFilteringDBusService:busy:
   *
   * True if the D-Bus API is busy.
   *
   * For example, if there are any outstanding method calls which haven’t been
   * replied to yet.
   *
   * Since: 0.14.0
   */
  props[PROP_BUSY] =
      g_param_spec_boolean ("busy", NULL, NULL,
                            FALSE,
                            G_PARAM_READABLE |
                            G_PARAM_STATIC_STRINGS);

  g_object_class_install_properties (object_class, G_N_ELEMENTS (props), props);

  /* Error domain registration for D-Bus. We do this here, rather than in a
   * #GOnce section in mct_filtering_dbus_service_error_quark(), to avoid spreading the
   * D-Bus code outside this file.
   *
   * For the moment, all the errors are mapped directly from
   * `MctFilterUpdaterError`.*/
  for (size_t i = 0; i < G_N_ELEMENTS (filtering_dbus_service_error_map); i++)
    g_dbus_error_register_error (MCT_FILTER_UPDATER_ERROR,
                                 filtering_dbus_service_error_map[i].error_code,
                                 filtering_dbus_service_error_map[i].dbus_error_name);
}

static void
mct_filtering_dbus_service_init (MctFilteringDBusService *self)
{
  self->cancellable = g_cancellable_new ();
}

static void
mct_filtering_dbus_service_constructed (GObject *object)
{
  MctFilteringDBusService *self = MCT_FILTERING_DBUS_SERVICE (object);

  /* Chain up. */
  G_OBJECT_CLASS (mct_filtering_dbus_service_parent_class)->constructed (object);

  /* Check our construct properties. */
  g_assert (G_IS_DBUS_CONNECTION (self->connection));
  g_assert (g_variant_is_object_path (self->object_path));
  g_assert (MCT_IS_FILTER_UPDATER (self->filter_updater));
}

static void
mct_filtering_dbus_service_dispose (GObject *object)
{
  MctFilteringDBusService *self = MCT_FILTERING_DBUS_SERVICE (object);

  g_assert (self->object_id == 0);
  g_assert (self->n_pending_operations == 0);

  g_clear_object (&self->filter_updater);

  g_clear_object (&self->connection);
  g_clear_pointer (&self->object_path, g_free);
  g_clear_object (&self->cancellable);

  /* Chain up to the parent class */
  G_OBJECT_CLASS (mct_filtering_dbus_service_parent_class)->dispose (object);
}

static void
mct_filtering_dbus_service_get_property (GObject    *object,
                                         guint       property_id,
                                         GValue     *value,
                                         GParamSpec *pspec)
{
  MctFilteringDBusService *self = MCT_FILTERING_DBUS_SERVICE (object);

  switch ((MctFilteringDBusServiceProperty) property_id)
    {
    case PROP_CONNECTION:
      g_value_set_object (value, self->connection);
      break;
    case PROP_OBJECT_PATH:
      g_value_set_string (value, self->object_path);
      break;
    case PROP_FILTER_UPDATER:
      g_value_set_object (value, self->filter_updater);
      break;
    case PROP_BUSY:
      g_value_set_boolean (value, mct_filtering_dbus_service_get_busy (self));
      break;
    default:
      g_assert_not_reached ();
    }
}

static void
mct_filtering_dbus_service_set_property (GObject      *object,
                                         guint         property_id,
                                         const GValue *value,
                                         GParamSpec   *pspec)
{
  MctFilteringDBusService *self = MCT_FILTERING_DBUS_SERVICE (object);

  switch ((MctFilteringDBusServiceProperty) property_id)
    {
    case PROP_CONNECTION:
      /* Construct only. */
      g_assert (self->connection == NULL);
      self->connection = g_value_dup_object (value);
      break;
    case PROP_OBJECT_PATH:
      /* Construct only. */
      g_assert (self->object_path == NULL);
      g_assert (g_variant_is_object_path (g_value_get_string (value)));
      self->object_path = g_value_dup_string (value);
      break;
    case PROP_FILTER_UPDATER:
      /* Construct only. */
      g_assert (self->filter_updater == NULL);
      self->filter_updater = g_value_dup_object (value);
      break;
    case PROP_BUSY:
      /* Read only. Fall through. */
      G_GNUC_FALLTHROUGH;
    default:
      g_assert_not_reached ();
    }
}

/**
 * mct_filtering_dbus_service_register:
 * @self: a filtering service
 * @error: return location for a [type@GLib.Error]
 *
 * Register the filtering service objects on D-Bus using the connection details
 * given in [property@Malcontent.FilteringDBusService.connection] and
 * [property@Malcontent.FilteringDBusService.object-path].
 *
 * Use [method@Malcontent.FilteringDBusService.unregister] to unregister them.
 * Calls to these two functions must be well paired.
 *
 * Returns: true on success, false otherwise
 * Since: 0.14.0
 */
gboolean
mct_filtering_dbus_service_register (MctFilteringDBusService  *self,
                                     GError                  **error)
{
  g_return_val_if_fail (MCT_IS_FILTERING_DBUS_SERVICE (self), FALSE);
  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

  const GDBusInterfaceVTable interface_vtable =
    {
      mct_filtering_dbus_service_method_call,
      NULL,  /* handled in mct_filtering_dbus_service_method_call() */
      NULL,  /* handled in mct_filtering_dbus_service_method_call() */
      { NULL, },  /* padding */
    };

  guint id = g_dbus_connection_register_object (self->connection,
                                                self->object_path,
                                                (GDBusInterfaceInfo *) &org_freedesktop_malcontent_web1_filtering_interface,
                                                &interface_vtable,
                                                g_object_ref (self),
                                                g_object_unref,
                                                error);

  if (id == 0)
    return FALSE;

  self->object_id = id;

  /* This has potentially changed. */
  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_BUSY]);

  return TRUE;
}

/**
 * mct_filtering_dbus_service_unregister:
 * @self: a filtering service
 *
 * Unregister objects from D-Bus which were previously registered using
 * [method@Malcontent.FilteringDBusService.register].
 *
 * Calls to these two functions must be well paired.
 *
 * Since: 0.14.0
 */
void
mct_filtering_dbus_service_unregister (MctFilteringDBusService *self)
{
  g_return_if_fail (MCT_IS_FILTERING_DBUS_SERVICE (self));

  g_dbus_connection_unregister_object (self->connection, self->object_id);
  self->object_id = 0;

  /* This has potentially changed. */
  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_BUSY]);
}

static gboolean
validate_dbus_interface_name (GDBusMethodInvocation *invocation,
                              const gchar           *interface_name)
{
  if (!g_dbus_is_interface_name (interface_name))
    {
      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR,
                                             G_DBUS_ERROR_UNKNOWN_INTERFACE,
                                             _("Invalid interface name ‘%s’."),
                                             interface_name);
      return FALSE;
    }

  return TRUE;
}

typedef void (*ChildMethodCallFunc) (MctFilteringDBusService *self,
                                     GDBusConnection         *connection,
                                     const char              *sender,
                                     GVariant                *parameters,
                                     GDBusMethodInvocation   *invocation);

static const struct
  {
    const char *interface_name;
    const char *method_name;
    ChildMethodCallFunc func;
  }
filtering_methods[] =
  {
    /* Handle properties. */
    { "org.freedesktop.DBus.Properties", "Get",
      mct_filtering_dbus_service_properties_get },
    { "org.freedesktop.DBus.Properties", "Set",
      mct_filtering_dbus_service_properties_set },
    { "org.freedesktop.DBus.Properties", "GetAll",
      mct_filtering_dbus_service_properties_get_all },

    /* Filtering methods. */
    { "org.freedesktop.MalcontentWeb1.Filtering", "UpdateFilters",
      mct_filtering_dbus_service_update_filters },
  };

static void
mct_filtering_dbus_service_method_call (GDBusConnection       *connection,
                                        const char            *sender,
                                        const char            *object_path,
                                        const char            *interface_name,
                                        const char            *method_name,
                                        GVariant              *parameters,
                                        GDBusMethodInvocation *invocation,
                                        void                  *user_data)
{
  MctFilteringDBusService *self = MCT_FILTERING_DBUS_SERVICE (user_data);

  /* Check we’ve implemented all the methods. Unfortunately this can’t be a
   * compile time check because the method array is declared in a separate
   * compilation unit. */
  size_t n_filtering_interface_methods = 0;
  for (size_t i = 0; org_freedesktop_malcontent_web1_filtering_interface.methods[i] != NULL; i++)
    n_filtering_interface_methods++;

  g_assert (G_N_ELEMENTS (filtering_methods) ==
            n_filtering_interface_methods +
            3  /* o.fdo.DBus.Properties */);

  /* Remove the service prefix from the path. */
  g_assert (g_str_equal (object_path, self->object_path));

  /* Work out which method to call. */
  for (gsize i = 0; i < G_N_ELEMENTS (filtering_methods); i++)
    {
      if (g_str_equal (filtering_methods[i].interface_name, interface_name) &&
          g_str_equal (filtering_methods[i].method_name, method_name))
        {
          filtering_methods[i].func (self, connection, sender, parameters, invocation);
          return;
        }
    }

  /* Make sure we actually called a method implementation. GIO guarantees that
   * this function is only called with methods we’ve declared in the interface
   * info, so this should never fail. */
  g_assert_not_reached ();
}

static void
mct_filtering_dbus_service_properties_get (MctFilteringDBusService *self,
                                           GDBusConnection         *connection,
                                           const char              *sender,
                                           GVariant                *parameters,
                                           GDBusMethodInvocation   *invocation)
{
  const char *interface_name, *property_name;
  g_variant_get (parameters, "(&s&s)", &interface_name, &property_name);

  /* D-Bus property names can be anything. */
  if (!validate_dbus_interface_name (invocation, interface_name))
    return;

  /* No properties exposed. */
  g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR,
                                         G_DBUS_ERROR_UNKNOWN_PROPERTY,
                                         _("Unknown property ‘%s.%s’."),
                                         interface_name, property_name);
  }

static void
mct_filtering_dbus_service_properties_set (MctFilteringDBusService *self,
                                           GDBusConnection         *connection,
                                           const char              *sender,
                                           GVariant                *parameters,
                                           GDBusMethodInvocation   *invocation)
{
  const char *interface_name, *property_name;
  g_variant_get (parameters, "(&s&sv)", &interface_name, &property_name, NULL);

  /* D-Bus property names can be anything. */
  if (!validate_dbus_interface_name (invocation, interface_name))
    return;

  /* No properties exposed. */
  g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR,
                                         G_DBUS_ERROR_UNKNOWN_PROPERTY,
                                         _("Unknown property ‘%s.%s’."),
                                         interface_name, property_name);
}

static void
mct_filtering_dbus_service_properties_get_all (MctFilteringDBusService *self,
                                               GDBusConnection         *connection,
                                               const char              *sender,
                                               GVariant                *parameters,
                                               GDBusMethodInvocation   *invocation)
{
  const char *interface_name;
  g_variant_get (parameters, "(&s)", &interface_name);

  if (!validate_dbus_interface_name (invocation, interface_name))
    return;

  /* Try the interface. */
  if (g_str_equal (interface_name, "org.freedesktop.MalcontentWeb1.Filtering"))
    g_dbus_method_invocation_return_value (invocation,
                                           g_variant_new_parsed ("(@a{sv} {},)"));
  else
    g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR,
                                           G_DBUS_ERROR_UNKNOWN_INTERFACE,
                                           _("Unknown interface ‘%s’."),
                                           interface_name);
}

typedef struct
{
  MctFilteringDBusService *service;  /* (not owned) (not nullable) */
  GDBusMethodInvocation *invocation;  /* (owned) (not nullable) */
} UpdateFiltersData;

static void
update_filters_data_free (UpdateFiltersData *data)
{
  g_clear_object (&data->invocation);
  g_free (data);
}

G_DEFINE_AUTOPTR_CLEANUP_FUNC (UpdateFiltersData, update_filters_data_free)

static void update_filters_cb (GObject      *object,
                               GAsyncResult *result,
                               void         *user_data);

static void
mct_filtering_dbus_service_update_filters (MctFilteringDBusService *self,
                                           GDBusConnection         *connection,
                                           const char              *sender,
                                           GVariant                *parameters,
                                           GDBusMethodInvocation   *invocation)
{
  g_autoptr(UpdateFiltersData) data_owned = NULL;
  UpdateFiltersData *data;
  uid_t uid;

  /* Validate the parameters. */
  g_variant_get (parameters, "(u)", &uid);

  /* We don’t check the credentials of the caller because there’s no need — this
   * method is called on a systemd timer for all users anyway, so preventing any
   * user from calling it would just delay an update of the filters rather than
   * stop it. */

  self->n_pending_operations++;
  if (self->n_pending_operations == 1)
    g_object_notify_by_pspec (G_OBJECT (self), props[PROP_BUSY]);

  /* Update the filters. */
  data = data_owned = g_new0 (UpdateFiltersData, 1);
  data->service = self;
  data->invocation = g_object_ref (invocation);

  mct_filter_updater_update_filters_async (self->filter_updater, uid,
                                           self->cancellable, update_filters_cb,
                                           g_steal_pointer (&data_owned));
}

static void
update_filters_cb (GObject      *object,
                   GAsyncResult *result,
                   void         *user_data)
{
  g_autoptr(UpdateFiltersData) data = g_steal_pointer (&user_data);
  MctFilteringDBusService *self = data->service;
  MctFilterUpdater *filter_updater = MCT_FILTER_UPDATER (object);
  g_autoptr(GError) local_error = NULL;

  self->n_pending_operations--;
  if (self->n_pending_operations == 0)
    g_object_notify_by_pspec (G_OBJECT (self), props[PROP_BUSY]);

  if (!mct_filter_updater_update_filters_finish (filter_updater, result, &local_error))
    {
      /* This will always be a MctFilterUpdaterError, and as we’ve mapped those
       * to D-Bus error names, then we can just prefix the message. */
      g_assert (local_error->domain == MCT_FILTER_UPDATER_ERROR);
      g_dbus_method_invocation_return_error (data->invocation, local_error->domain,
                                             local_error->code,
                                             _("Error updating filters: %s"), local_error->message);
    }
  else
    {
      g_dbus_method_invocation_return_value (data->invocation, NULL);
    }
}

/**
 * mct_filtering_dbus_service_new:
 * @connection: (transfer none): D-Bus connection to export objects on
 * @object_path: root path to export objects below; must be a valid D-Bus object
 *    path
 * @filter_updater: (transfer none): filter updater object
 *
 * Create a new [class@Malcontent.FilteringDBusService] instance which is set up
 * to run as a service.
 *
 * Returns: (transfer full): a new [class@Malcontent.FilteringDBusService]
 * Since: 0.14.0
 */
MctFilteringDBusService *
mct_filtering_dbus_service_new (GDBusConnection  *connection,
                                const char       *object_path,
                                MctFilterUpdater *filter_updater)
{
  g_return_val_if_fail (G_IS_DBUS_CONNECTION (connection), NULL);
  g_return_val_if_fail (g_variant_is_object_path (object_path), NULL);
  g_return_val_if_fail (MCT_IS_FILTER_UPDATER (filter_updater), NULL);

  return g_object_new (MCT_TYPE_FILTERING_DBUS_SERVICE,
                       "connection", connection,
                       "object-path", object_path,
                       "filter-updater", filter_updater,
                       NULL);
}

/**
 * mct_filtering_dbus_service_get_busy:
 * @self: a filtering service
 *
 * Get the value of [property@Malcontent.FilteringDBusService.busy].
 *
 * Returns: true if the service is busy, false otherwise
 * Since: 0.14.0
 */
gboolean
mct_filtering_dbus_service_get_busy (MctFilteringDBusService *self)
{
  g_return_val_if_fail (MCT_IS_FILTERING_DBUS_SERVICE (self), FALSE);

  return (self->object_id != 0 && self->n_pending_operations > 0);
}

