/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
 *
 * Copyright (C) 2018 Ludovico de Nittis <denittis@gnome.org>
 *
 * 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 <http://www.gnu.org/licenses/>.
 *
 */

#include "config.h"

#include <gio/gio.h>
#include <glib-object.h>
#include <glib/gi18n.h>
#include <libnotify/notify.h>
#include <locale.h>
#include <string.h>

#include <gdesktop-enums.h>

#include "gnome-settings-bus.h"
#include "gnome-settings-profile.h"
#include "gsd-enums.h"
#include "gsd-usb-protection-manager.h"

#define PRIVACY_SETTINGS "org.gnome.desktop.privacy"
#define USB_PROTECTION "usb-protection"
#define USB_PROTECTION_LEVEL "usb-protection-level"

#define DBUS_VERSION "1"

#define USBGUARD_DBUS_NAME "org.usbguard" DBUS_VERSION
#define USBGUARD_DBUS_PATH "/org/usbguard" DBUS_VERSION
#define USBGUARD_DBUS_INTERFACE "org.usbguard"
#define USBGUARD_DBUS_INTERFACE_VERSIONED USBGUARD_DBUS_INTERFACE DBUS_VERSION

#define USBGUARD_DBUS_PATH_POLICY USBGUARD_DBUS_PATH "/Policy"
#define USBGUARD_DBUS_INTERFACE_POLICY USBGUARD_DBUS_INTERFACE ".Policy" DBUS_VERSION

#define USBGUARD_DBUS_PATH_DEVICES USBGUARD_DBUS_PATH "/Devices"
#define USBGUARD_DBUS_INTERFACE_DEVICES USBGUARD_DBUS_INTERFACE ".Devices" DBUS_VERSION

#define APPLY_POLICY "apply-policy"
#define BLOCK "block"
#define REJECT "reject"

#define APPLY_DEVICE_POLICY "applyDevicePolicy"
#define LIST_DEVICES "listDevices"
#define LIST_RULES "listRules"
#define ALLOW "allow"
#define DEVICE_POLICY_CHANGED "DevicePolicyChanged"
#define DEVICE_PRESENCE_CHANGED "DevicePresenceChanged"
#define INSERTED_DEVICE_POLICY "InsertedDevicePolicy"
#define APPEND_RULE "appendRule"
#define ALLOW_ALL "allow id *:* label \"GNOME_SETTINGS_DAEMON_RULE\""
#define WITH_CONNECT_TYPE "with-connect-type"
#define WITH_INTERFACE "with-interface"
#define NAME "name"

struct _GsdUsbProtectionManager
{
        GObject             parent;
        guint               start_idle_id;
        GDBusNodeInfo      *introspection_data;
        GSettings          *settings;
        guint               name_id;
        GDBusConnection    *connection;
        gboolean            available;
        GDBusProxy         *usb_protection;
        GDBusProxy         *usb_protection_devices;
        GDBusProxy         *usb_protection_policy;
        GCancellable       *cancellable;
        GsdScreenSaver     *screensaver_proxy;
        gboolean            screensaver_active;
        guint               last_device_id;
        NotifyNotification *notification;
};

typedef enum {
        EVENT_PRESENT,
        EVENT_INSERT,
        EVENT_UPDATE,
        EVENT_REMOVE
} UsbGuardEvent;


typedef enum {
        TARGET_ALLOW,
        TARGET_BLOCK,
        TARGET_REJECT
} UsbGuardTarget;

typedef enum {
        POLICY_DEVICE_ID,
        POLICY_TARGET_OLD,
        /* This is the rule that has been applied */
        POLICY_TARGET_NEW,
        POLICY_DEV_RULE,
        /* The ID of the rule that has been applied.
         * uint32 - 1 is one of the implicit rules,
         * e.g. ImplicitPolicyTarget or InsertedDevicePolicy.
         */
        POLICY_RULE_ID,
        POLICY_ATTRIBUTES
} UsbGuardPolicyChanged;

typedef enum {
        PRESENCE_DEVICE_ID,
        PRESENCE_EVENT,
        /* That does not reflect what USBGuard intends to do with the device :( */
        PRESENCE_TARGET,
        PRESENCE_DEV_RULE,
        PRESENCE_ATTRIBUTES
} UsbGuardPresenceChanged;

static void gsd_usb_protection_manager_finalize (GObject *object);

G_DEFINE_TYPE (GsdUsbProtectionManager, gsd_usb_protection_manager, G_TYPE_OBJECT)

static gpointer manager_object = NULL;

#define GSD_DBUS_NAME "org.gnome.SettingsDaemon"
#define GSD_DBUS_PATH "/org/gnome/SettingsDaemon"
#define GSD_DBUS_BASE_INTERFACE "org.gnome.SettingsDaemon"

#define GSD_USB_PROTECTION_DBUS_NAME GSD_DBUS_NAME ".UsbProtection"
#define GSD_USB_PROTECTION_DBUS_PATH GSD_DBUS_PATH "/UsbProtection"

static const gchar introspection_xml[] =
"<node>"
"  <interface name='org.gnome.SettingsDaemon.UsbProtection'>"
"    <property name='Available' type='b' access='read'/>"
"  </interface>"
"</node>";

static void
dbus_call_log_error (GObject      *source_object,
                     GAsyncResult *res,
                     gpointer      user_data)
{
        g_autoptr(GVariant) result = NULL;
        g_autoptr(GError) error = NULL;
        const gchar *msg = user_data;

        result = g_dbus_proxy_call_finish (G_DBUS_PROXY (source_object),
                                           res,
                                           &error);
        if (result == NULL &&
            !g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_SERVICE_UNKNOWN))
                g_warning ("%s: %s", msg, error->message);
}

static void
add_usbguard_allow_rule (GsdUsbProtectionManager *manager)
{
        /* This appends a "allow all" rule.
         * It has the purpose of ensuring the authorization of new devices when
         * the lockscreen is off while respecting existing rules.
         * We make it temporary, so that we are stateless and don't alter the
         * existing (persistent) configuration.
         */

        GVariant *params;
        GDBusProxy *policy_proxy = manager->usb_protection_policy;

        if (policy_proxy == NULL) {
            g_warning ("Cannot add allow rule, because dbus proxy is missing");
        } else {
                gboolean temporary = TRUE;
                /* This is USBGuard's Rule::LastID */
                const guint32 last_rule_id = G_MAXUINT32 - 2;
                g_debug ("Adding rule %u", last_rule_id);
                params = g_variant_new ("(sub)", ALLOW_ALL, last_rule_id, temporary);
                g_dbus_proxy_call (policy_proxy,
                                   APPEND_RULE,
                                   params,
                                   G_DBUS_CALL_FLAGS_NONE,
                                   -1,
                                   manager->cancellable,
                                   dbus_call_log_error,
                                   "Error appending USBGuard rule");
        }
}

static gboolean
is_usbguard_allow_rule_present (GVariant *rules)
{
        g_autoptr(GVariantIter) iter = NULL;
        g_autofree gchar *value = NULL;
        guint number = 0;

        g_debug ("Detecting rule...");

        g_variant_get (rules, "a(us)", &iter);
        g_return_val_if_fail (iter != NULL, FALSE);
        while (g_variant_iter_loop (iter, "(us)", &number, &value)) {
                if (g_strcmp0 (value, ALLOW_ALL) == 0) {
                        g_debug ("Detected rule!");
                        return TRUE;
                    }
        }
        g_debug ("Rule not present");
        return FALSE;
}

static void
usbguard_listrules_cb (GObject      *source_object,
                       GAsyncResult *res,
                       gpointer      user_data)
{
        GVariant *result, *rules;
        g_autoptr(GError) error = NULL;

        result = g_dbus_proxy_call_finish (G_DBUS_PROXY (source_object),
                                           res,
                                           &error);

        if (!result) {
                if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
                        g_warning ("Failed to fetch USBGuard rules list: %s", error->message);
                }
                return;
        }

        rules = g_variant_get_child_value (result, 0);
        g_variant_unref (result);
        g_return_if_fail (rules != NULL);
        if (!is_usbguard_allow_rule_present (rules))
                add_usbguard_allow_rule (user_data);

}

static void
usbguard_ensure_allow_rule (GsdUsbProtectionManager *manager)
{
        GVariant *params;
        GDBusProxy *policy_proxy = manager->usb_protection_policy;

        if (policy_proxy == NULL) {
            g_warning ("Cannot list rules, because dbus proxy is missing");
        } else {
                /* listRules parameter is a label for matching rules.
                 * Currently we are using an empty label to get all the
                 * rules instead of just using "GNOME_SETTINGS_DAEMON_RULE"
                 * until this bug gets solved:
                 * https://github.com/USBGuard/usbguard/issues/328 */
                params = g_variant_new ("(s)", "");
                g_dbus_proxy_call (policy_proxy,
                                   LIST_RULES,
                                   params,
                                   G_DBUS_CALL_FLAGS_NONE,
                                   -1,
                                   manager->cancellable,
                                   usbguard_listrules_cb,
                                   manager);
        }
}

static void
settings_changed_callback (GSettings               *settings,
                           const char              *key,
                           GsdUsbProtectionManager *manager)
{
        gchar *value_usbguard;
        gboolean usbguard_controlled;
        GVariant *params;
        GDesktopUsbProtection protection_level;

        /* We react only if one of the two USB related properties has been changed */
        if (g_strcmp0 (key, USB_PROTECTION) != 0 && g_strcmp0 (key, USB_PROTECTION_LEVEL) != 0)
                return;

        usbguard_controlled = g_settings_get_boolean (settings, USB_PROTECTION);
        protection_level = g_settings_get_enum (settings, USB_PROTECTION_LEVEL);
        g_debug ("USBGuard control is currently %i with a protection level of %i",
                 usbguard_controlled, protection_level);

        /* If previously we were controlling USBGuard and now we are not,
         * we leave the USBGuard configuration in a clean state. I.e. we set
         * "InsertedDevicePolicy" to "apply-policy" and we ensure that
         * there is an always allow rule. In this way even if USBGuard daemon
         * is running every USB devices will be automatically authorized. */
        if (g_strcmp0 (key, USB_PROTECTION) == 0 && !usbguard_controlled) {
                g_debug ("let's clean usbguard config state");
                params = g_variant_new ("(ss)",
                                        INSERTED_DEVICE_POLICY,
                                        APPLY_POLICY);

                if (manager->usb_protection != NULL) {
                        g_dbus_proxy_call (manager->usb_protection,
                                           "setParameter",
                                           params,
                                           G_DBUS_CALL_FLAGS_NONE,
                                           -1,
                                           manager->cancellable,
                                           dbus_call_log_error,
                                           "Error calling USBGuard DBus to set a clean configuration state");
                }

                usbguard_ensure_allow_rule (manager);
        }

        /* Only if we are entitled to handle USBGuard */
        if (usbguard_controlled && manager->usb_protection != NULL) {
                value_usbguard = (protection_level == G_DESKTOP_USB_PROTECTION_ALWAYS) ? BLOCK : APPLY_POLICY;
                params = g_variant_new ("(ss)",
                                        INSERTED_DEVICE_POLICY,
                                        value_usbguard);

                g_dbus_proxy_call (manager->usb_protection,
                                   "setParameter",
                                   params,
                                   G_DBUS_CALL_FLAGS_NONE,
                                   -1,
                                   manager->cancellable,
                                   dbus_call_log_error,
                                   "Error calling USBGuard DBus to set the desidered protection level");

                /* If we are in "When lockscreen is active" we also check if the
                 * always allow rule is present. */
                if (protection_level == G_DESKTOP_USB_PROTECTION_LOCKSCREEN)
                        usbguard_ensure_allow_rule (manager);
        }
}

static void update_usb_protection_store (GsdUsbProtectionManager *manager,
                                         GVariant                *parameter)
{
        const gchar *key;
        gboolean usbguard_controlled;
        GDesktopUsbProtection protection_level;
        GSettings *settings = manager->settings;

        usbguard_controlled = g_settings_get_boolean (settings, USB_PROTECTION);
        /* If we are not handling USBGuard configuration (e.g. the user is using
         * a third party program) we do nothing when the config changes. */
        if (usbguard_controlled) {
                key = g_variant_get_string (parameter, NULL);
                protection_level = g_settings_get_enum (settings, USB_PROTECTION_LEVEL);
                /* If the USBGuard configuration has been changed and doesn't match
                 * our internal state, most likely means that the user externally
                 * changed it. When this happens we set to false the control value. */
                if ((g_strcmp0 (key, APPLY_POLICY) == 0 && protection_level == G_DESKTOP_USB_PROTECTION_ALWAYS)) {
                        g_settings_set (settings, USB_PROTECTION, "b", FALSE);
                        g_warning ("We don't control anymore USBGuard because the configuration changed externally.");
                }
        }
}

static gboolean
is_protection_active (GsdUsbProtectionManager *manager)
{
        GSettings *settings = manager->settings;

        return g_settings_get_boolean (settings, USB_PROTECTION);
}

static void
on_notification_closed (NotifyNotification      *n,
                        GsdUsbProtectionManager *manager)
{
        g_debug ("Clearing notification");
        g_clear_object (&manager->notification);
}

static void
show_notification (GsdUsbProtectionManager *manager,
                   const char              *summary,
                   const char              *body)
{
        /* Don't show a notice if one is already displayed */
        if (manager->notification != NULL) {
                g_debug ("A notification already exists, we do not show a new one");
                return;
        }

        manager->notification = notify_notification_new (summary, body, "drive-removable-media-symbolic");
        notify_notification_set_app_name (manager->notification, _("USB Protection"));
        notify_notification_set_hint (manager->notification, "transient", g_variant_new_boolean (TRUE));
        notify_notification_set_hint_string (manager->notification, "x-gnome-privacy-scope", "system");
        notify_notification_set_timeout (manager->notification, NOTIFY_EXPIRES_DEFAULT);
        notify_notification_set_urgency (manager->notification, NOTIFY_URGENCY_CRITICAL);
        g_signal_connect_object (manager->notification,
                                 "closed",
                                 G_CALLBACK (on_notification_closed),
                                 manager,
                                 0);
        g_debug ("Showing notification for %s: %s", summary, body);
        if (!notify_notification_show (manager->notification, NULL)) {
                g_warning ("Failed to send USB protection notification");
                g_clear_object (&manager->notification);
        }
}

static void call_usbguard_dbus (GDBusProxy              *proxy,
                                GsdUsbProtectionManager *manager,
                                guint                    device_id,
                                guint                    target,
                                gboolean                 permanent)
{
        if (manager->usb_protection_devices == NULL) {
            g_warning("Could not call USBGuard, because DBus is missing");
        } else {
            g_debug ("Calling applyDevicePolicy with device_id %u, target %u and permanent: %i", device_id, target, permanent);
            GVariant *params = g_variant_new ("(uub)", device_id, target, permanent);
            g_dbus_proxy_call (manager->usb_protection_devices,
                               APPLY_DEVICE_POLICY,
                               params,
                               G_DBUS_CALL_FLAGS_NONE,
                               -1,
                               manager->cancellable,
                               dbus_call_log_error,
                               "Error calling USBGuard DBus to authorize a device");
        }
}

static gboolean
is_hid_or_hub (GVariant *device,
               gboolean *has_other_classes)
{
        g_autoptr(GVariantIter) iter = NULL;
        g_autofree gchar *name = NULL;
        g_autofree gchar *value = NULL;
        guint i;
        gboolean is_hid_or_hub = FALSE;

        if (has_other_classes != NULL) {
                *has_other_classes = FALSE;
        }

        g_variant_get_child (device, PRESENCE_ATTRIBUTES, "a{ss}", &iter);
        g_return_val_if_fail (iter != NULL, FALSE);
        while (g_variant_iter_loop (iter, "{ss}", &name, &value)) {
                if (g_strcmp0 (name, WITH_INTERFACE) == 0) {
                        g_auto(GStrv) interfaces_splitted = NULL;
                        interfaces_splitted = g_strsplit (value, " ", -1);
                        for (i = 0; i < g_strv_length (interfaces_splitted); i++) {
                                if (g_str_has_prefix (interfaces_splitted[i], "03:")
                                    || g_str_has_prefix (interfaces_splitted[i], "09:")) {
                                        is_hid_or_hub = TRUE;
                                    }
                                else if (has_other_classes != NULL) {
                                        *has_other_classes = TRUE;
                                }
                        }
                }
        }
        return is_hid_or_hub;
}

static gboolean
is_hardwired (GVariant *device)
{
        g_autoptr(GVariantIter) iter = NULL;
        g_autofree gchar *name = NULL;
        g_autofree gchar *value = NULL;

        g_variant_get_child (device, PRESENCE_ATTRIBUTES, "a{ss}", &iter);
        g_return_val_if_fail (iter != NULL, FALSE);
        while (g_variant_iter_loop (iter, "{ss}", &name, &value)) {
                if (g_strcmp0 (name, WITH_CONNECT_TYPE) == 0) {
                        return g_strcmp0 (value, "hardwired") == 0;
                }
        }
        return FALSE;
}

static void
authorize_device (GsdUsbProtectionManager *manager,
                  guint device_id)
{
        g_return_if_fail (manager->usb_protection_devices != NULL);

        g_debug ("Authorizing device %u", device_id);
        call_usbguard_dbus(manager->usb_protection_devices,
                         manager,
                         device_id,
                         TARGET_ALLOW,
                         FALSE);
}

static void
on_screen_locked (GsdScreenSaver          *screen_saver,
                  GAsyncResult            *result,
                  GsdUsbProtectionManager *manager)
{
        guint device_id;
        g_autoptr(GError) error = NULL;

        gsd_screen_saver_call_lock_finish (screen_saver, result, &error);

        if (error) {
                if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
                        return;
                g_warning ("Couldn't lock screen: %s", error->message);
        }

        device_id = manager->last_device_id;
        authorize_device (manager, device_id);
        manager->last_device_id = G_MAXUINT;
        show_notification (manager,
                           _("New USB device"),
                           _("New device has been detected while the session was not locked. "
                             "If you did not plug anything, check your system for any suspicious device."));

}

static void
on_usbguard_signal (GDBusProxy *proxy,
                           gchar      *sender_name,
                           gchar      *signal_name,
                           GVariant   *parameters,
                           gpointer    user_data)
{
        UsbGuardTarget target = TARGET_BLOCK;
        UsbGuardEvent device_event;
        GDesktopUsbProtection protection_level;
        GsdUsbProtectionManager *manager = user_data;
        g_autoptr(GVariantIter) iter = NULL;
        g_autofree gchar *name = NULL;
        g_autofree gchar *device_name = NULL;
        gboolean hid_or_hub = FALSE;
        gboolean has_other_classes = FALSE;

        g_debug ("USBGuard signal: %s", signal_name);

        /* We act only if we receive a signal indicating that a device has been inserted */
        if (g_strcmp0 (signal_name, DEVICE_PRESENCE_CHANGED) != 0) {
                return;
        }

        g_return_if_fail (g_variant_n_children (parameters) >= PRESENCE_EVENT);
        g_variant_get_child (parameters, PRESENCE_EVENT, "u", &device_event);
        if (device_event != EVENT_INSERT) {
            g_debug ("Device hat not been inserted (%d); ignoring", device_event);
            return;
        }

        /* We would like to show a notification for an inserted device that
         * *has not been blocked*.  But USBGuard is not providing that information.
         * So we have to work around that limitation and assume that any device plugged in
         * during screensaver shall be blocked.
         * https://github.com/USBGuard/usbguard/issues/353
         
           g_variant_get_child (parameters, POLICY_TARGET_NEW, "u", &target);
        */

        /* If the device is already authorized we do nothing */
        if (target == TARGET_ALLOW) {
                g_debug ("Device will be allowed, we return");
                return;
        }

        /* If the USB protection is disabled we do nothing */
        if (!is_protection_active (manager)) {
                g_debug ("Protection is not active. Not acting on the device");
                return;
        }

        g_variant_get_child (parameters, PRESENCE_ATTRIBUTES, "a{ss}", &iter);
        g_return_if_fail (iter != NULL);
        while (g_variant_iter_loop (iter, "{ss}", &name, &device_name)) {
                if (g_strcmp0 (name, NAME) == 0)
                        g_debug ("A new USB device has been connected: %s", device_name);
        }

        if (is_hardwired (parameters)) {
            guint device_id;
            g_debug ("Device is hardwired, allowing it to be connected");
            g_variant_get_child (parameters, POLICY_DEVICE_ID, "u", &device_id);
            authorize_device (manager, device_id);
            return;
        }

        protection_level = g_settings_get_enum (manager->settings, USB_PROTECTION_LEVEL);

        g_debug ("Screensaver active: %d", manager->screensaver_active);
        hid_or_hub = is_hid_or_hub (parameters, &has_other_classes);
        if (manager->screensaver_active) {
                /* If the session is locked we check if the inserted device is a HID,
                 * e.g. a keyboard or a mouse, or an HUB.
                 * If that is the case we authorize the newly inserted device as an
                 * antilockout policy.
                 *
                 * If this device advertises also interfaces outside the HID class, or the
                 * HUB class, it is suspect. It could be a false positive because this could
                 * be a "smart" keyboard for example, but at this stage is better be safe. */
                if (hid_or_hub && !has_other_classes) {
                        guint device_id;
                        show_notification (manager,
                                           _("New device detected"),
                                           _("Either one of your existing devices has been reconnected or a new one has been plugged in. "
                                             "If you did not do it, check your system for any suspicious device."));
                        g_variant_get_child (parameters, POLICY_DEVICE_ID, "u", &device_id);
                        authorize_device (manager, device_id);
                } else {
                    if (protection_level == G_DESKTOP_USB_PROTECTION_LOCKSCREEN) {
                            show_notification (manager,
                                               _("Reconnect USB device"),
                                               _("New device has been detected while you were away. "
                                                 "Please disconnect and reconnect the device to start using it."));
                    } else {
                            const char* name_for_notification = device_name ? device_name : "unknown name";
                            g_debug ("Showing notification for %s", name_for_notification);
                            show_notification (manager,
                                               _("USB device blocked"),
                                               _("New device has been detected while you were away. "
                                                 "It has been blocked because the USB protection is active."));
                    }
                }
        } else {
                /* If the protection level is "lockscreen" the device will be automatically
                 * authorized by usbguard. */
                if (protection_level == G_DESKTOP_USB_PROTECTION_ALWAYS) {
                        /* We authorize the device if this is a HID,
                         * e.g. a keyboard or a mouse, or an HUB.
                         * We also lock the screen to prevent an attacker to plug malicious
                         * devices if the legitimate user forgot to lock his session.
                         *
                         * If this device advertises also interfaces outside the HID class, or the
                         * HUB class, it is suspect. It could be a false positive because this could
                         * be a "smart" keyboard for example, but at this stage is better be safe. */
                        if (hid_or_hub && !has_other_classes) {
                                g_variant_get_child (parameters, POLICY_DEVICE_ID, "u", &(manager->last_device_id));
                                gsd_screen_saver_call_lock (manager->screensaver_proxy,
                                                            manager->cancellable,
                                                            (GAsyncReadyCallback) on_screen_locked,
                                                            manager);
                        } else {
                                show_notification (manager,
                                                   _("USB device blocked"),
                                                   _("The new inserted device has been blocked because the USB protection is active."));
                        }
                }
            }
}

static void
on_usb_protection_signal (GDBusProxy *proxy,
                          gchar      *sender_name,
                          gchar      *signal_name,
                          GVariant   *parameters,
                          gpointer    user_data)
{
        g_autoptr(GVariant) parameter = NULL;
        g_autofree gchar *policy_name = NULL;

        if (g_strcmp0 (signal_name, "PropertyParameterChanged") != 0)
                return;

        g_variant_get_child (parameters, 0, "s", &policy_name);

        /* Right now we just care about the InsertedDevicePolicy value */
        if (g_strcmp0 (policy_name, INSERTED_DEVICE_POLICY) != 0)
                return;

        parameter = g_variant_get_child_value (parameters, 2);
        g_return_if_fail (parameter != NULL);
        update_usb_protection_store (user_data, parameter);

}

static void
get_parameter_cb (GObject      *source_object,
                  GAsyncResult *res,
                  gpointer      user_data)
{
        GVariant *result;
        GVariant *params = NULL;
        g_autofree gchar *key = NULL;
        GDesktopUsbProtection protection_level;
        GsdUsbProtectionManager *manager;
        GSettings *settings;
        g_autoptr(GError) error = NULL;

        result = g_dbus_proxy_call_finish (G_DBUS_PROXY (source_object),
                                           res,
                                           &error);
        if (result == NULL) {
                if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
                        g_warning ("Failed to fetch USBGuard parameters: %s", error->message);
                }
                return;
        }

        manager = GSD_USB_PROTECTION_MANAGER (user_data);
        settings = manager->settings;

        g_variant_get_child (result, 0, "s", &key);
        g_variant_unref (result);
        protection_level = g_settings_get_enum (settings, USB_PROTECTION_LEVEL);

        g_debug ("InsertedDevicePolicy is: %s", key);

        if (protection_level == G_DESKTOP_USB_PROTECTION_LOCKSCREEN) {
                if (g_strcmp0 (key, APPLY_POLICY) != 0) {
                        /* We are out of sync. */
                        params = g_variant_new ("(ss)",
                                                INSERTED_DEVICE_POLICY,
                                                APPLY_POLICY);
                }
        } else if (protection_level == G_DESKTOP_USB_PROTECTION_ALWAYS) {
                if (g_strcmp0 (key, BLOCK) != 0) {
                        /* We are out of sync. */
                        params = g_variant_new ("(ss)",
                                                INSERTED_DEVICE_POLICY,
                                                BLOCK);
                }
        }

        if (params != NULL) {
                /* We are out of sync. We need to call setParameter to update USBGuard state */
                if (manager->usb_protection != NULL) {
                        g_debug ("Setting InsertedDevicePolicy");
                        g_dbus_proxy_call (manager->usb_protection,
                                           "setParameter",
                                           params,
                                           G_DBUS_CALL_FLAGS_NONE,
                                           -1,
                                           manager->cancellable,
                                           dbus_call_log_error,
                                           "Error calling USBGuard DBus while we were out of sync");
                }

        }

        /* If we are in "When lockscreen is active" we also check
         * if the "always allow" rule is present. */
        if (protection_level == G_DESKTOP_USB_PROTECTION_LOCKSCREEN) {
                g_debug ("Ensuring allow all");
                usbguard_ensure_allow_rule (manager);
        }
}

static void
sync_usb_protection (GDBusProxy              *proxy,
                     GsdUsbProtectionManager *manager)
{
        GVariant *params;
        gboolean usbguard_controlled;
        GSettings *settings = manager->settings;

        usbguard_controlled = g_settings_get_boolean (settings, USB_PROTECTION);

        g_debug ("Attempting to sync USB parameters: %d %p %p",
            usbguard_controlled, proxy, manager->usb_protection);

        if (!usbguard_controlled || manager->usb_protection == NULL)
                return;

        params = g_variant_new ("(s)", INSERTED_DEVICE_POLICY);
        g_dbus_proxy_call (manager->usb_protection,
                           "getParameter",
                           params,
                           G_DBUS_CALL_FLAGS_NONE,
                           -1,
                           manager->cancellable,
                           get_parameter_cb,
                           manager);
}

static void
usb_protection_properties_changed (GsdUsbProtectionManager *manager)
{
        GVariantBuilder props_builder;
        GVariant *props_changed = NULL;

        /* not yet connected to the session bus */
        if (manager->connection == NULL)
                return;

        g_variant_builder_init (&props_builder, G_VARIANT_TYPE ("a{sv}"));

        g_variant_builder_add (&props_builder, "{sv}", "Available",
                               g_variant_new_boolean (manager->available));

        props_changed = g_variant_new ("(s@a{sv}@as)", GSD_USB_PROTECTION_DBUS_NAME,
                                       g_variant_builder_end (&props_builder),
                                       g_variant_new_strv (NULL, 0));

        g_dbus_connection_emit_signal (manager->connection,
                                       NULL,
                                       GSD_USB_PROTECTION_DBUS_PATH,
                                       "org.freedesktop.DBus.Properties",
                                       "PropertiesChanged",
                                       props_changed, NULL);
}

static void
on_usb_protection_owner_changed_cb (GObject    *object,
                                    GParamSpec *pspec,
                                    gpointer    user_data)
{
        GsdUsbProtectionManager *manager = user_data;
        GDBusProxy *proxy = G_DBUS_PROXY(object);
        g_autofree gchar *name_owner = NULL;

        name_owner = g_dbus_proxy_get_name_owner (proxy);
        g_debug ("Got owner change: %s", name_owner);

        if (name_owner) {
                manager->available = TRUE;
        } else {
                manager->available = FALSE;
        }

        usb_protection_properties_changed (manager);
}

static void
handle_screensaver_active (GsdUsbProtectionManager *manager,
                           GVariant                *parameters)
{
        gboolean active;
        gchar *value_usbguard;
        gboolean usbguard_controlled;
        GVariant *params;
        GDesktopUsbProtection protection_level;
        GSettings *settings = manager->settings;

        usbguard_controlled = g_settings_get_boolean (settings, USB_PROTECTION);
        protection_level = g_settings_get_enum (settings, USB_PROTECTION_LEVEL);

        g_variant_get (parameters, "(b)", &active);
        g_debug ("Received screensaver ActiveChanged signal: %d (old: %d)", active, manager->screensaver_active);
        if (manager->screensaver_active != active) {
                manager->screensaver_active = active;
                if (usbguard_controlled && protection_level == G_DESKTOP_USB_PROTECTION_LOCKSCREEN) {
                        /* If we are in the "lockscreen protection" level we change
                         * the usbguard config with apply-policy or block if the session
                         * is unlocked or locked, respectively. */
                        value_usbguard = active ? BLOCK : APPLY_POLICY;
                        params = g_variant_new ("(ss)",
                                                INSERTED_DEVICE_POLICY,
                                                value_usbguard);
                        if (manager->usb_protection != NULL) {
                                g_dbus_proxy_call (manager->usb_protection,
                                                   "setParameter",
                                                   params,
                                                   G_DBUS_CALL_FLAGS_NONE,
                                                   -1,
                                                   manager->cancellable,
                                                   dbus_call_log_error,
                                                   "Error calling USBGuard DBus to change the protection after a screensaver event");
                        }
                }
        }
}

static void
screensaver_signal_cb (GDBusProxy  *proxy,
                       const gchar *sender_name,
                       const gchar *signal_name,
                       GVariant    *parameters,
                       gpointer     user_data)
{
        g_debug ("ScreenSaver Signal: %s", signal_name);
        if (g_strcmp0 (signal_name, "ActiveChanged") == 0)
                handle_screensaver_active (GSD_USB_PROTECTION_MANAGER (user_data), parameters);
}

static void
usb_protection_policy_proxy_ready (GObject      *source_object,
                                   GAsyncResult *res,
                                   gpointer      user_data)
{
        GsdUsbProtectionManager *manager;
        GDBusProxy *proxy;
        g_autoptr(GError) error = NULL;
        g_debug ("usb_protection_policy_proxy_ready");

        proxy = g_dbus_proxy_new_for_bus_finish (res, &error);
        if (!proxy) {
                if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
                        g_warning ("Failed to contact USBGuard: %s", error->message);
                return;
        } else {
            manager = GSD_USB_PROTECTION_MANAGER (user_data);
            manager->usb_protection_policy = proxy;
            g_debug ("Set protection policy proxy to %p", proxy);
            sync_usb_protection (proxy, manager);
        }
}

static void
usb_protection_devices_proxy_ready (GObject      *source_object,
                                    GAsyncResult *res,
                                    gpointer      user_data)
{
        GsdUsbProtectionManager *manager;
        GDBusProxy *proxy;
        g_autoptr(GError) error = NULL;

        proxy = g_dbus_proxy_new_for_bus_finish (res, &error);
        if (!proxy) {
                if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
                        g_warning ("Failed to contact USBGuard: %s", error->message);
                return;
        }
        manager = GSD_USB_PROTECTION_MANAGER (user_data);
        manager->usb_protection_devices = proxy;

        /* We don't care about already plugged in devices because they'll be
         * already autorized by the "allow all" rule in USBGuard. */
        g_debug ("Listening to signals");
        g_signal_connect_object (source_object,
                                 "g-signal",
                                 G_CALLBACK (on_usbguard_signal),
                                 user_data,
                                 0);
}

static void
get_current_screen_saver_status (GsdUsbProtectionManager *manager)
{
        g_autoptr(GVariant) ret = NULL;
        g_autoptr(GError) error = NULL;

        ret = g_dbus_proxy_call_sync (G_DBUS_PROXY (manager->screensaver_proxy),
                                      "GetActive",
                                      NULL,
                                      G_DBUS_CALL_FLAGS_NONE,
                                      -1,
                                      manager->cancellable,
                                      &error);
        if (ret == NULL) {
                if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
                        g_warning ("Failed to get screen saver status: %s", error->message);
                return;
        }
        handle_screensaver_active (manager, ret);
}

static void
usb_protection_proxy_ready (GObject      *source_object,
                            GAsyncResult *res,
                            gpointer      user_data)
{
        GsdUsbProtectionManager *manager;
        GDBusProxy *proxy;
        g_autofree gchar *name_owner = NULL;
        g_autoptr(GError) error = NULL;

        proxy = g_dbus_proxy_new_for_bus_finish (res, &error);
        if (!proxy) {
                if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
                        g_warning ("Failed to contact USBGuard: %s", error->message);
                return;
        }
        manager = GSD_USB_PROTECTION_MANAGER (user_data);
        manager->usb_protection = proxy;

        g_signal_connect (G_OBJECT (manager->settings), "changed",
                          G_CALLBACK (settings_changed_callback), manager);

        manager->screensaver_proxy = gnome_settings_bus_get_screen_saver_proxy ();
        if (!manager->screensaver_proxy) {
                if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
                        g_warning ("Failed to connect to screensaver service: %s", error->message);
                g_clear_object (&manager->usb_protection);
                return;
        }

        get_current_screen_saver_status (manager);

        g_signal_connect (manager->screensaver_proxy, "g-signal",
                          G_CALLBACK (screensaver_signal_cb), manager);

        name_owner = g_dbus_proxy_get_name_owner (G_DBUS_PROXY (proxy));

        if (name_owner == NULL) {
                g_debug("Probably USBGuard >= 0.7.5 is not currently installed.");
                manager->available = FALSE;
        } else {
                manager->available = TRUE;
        }

        g_signal_connect_object (source_object,
                                 "notify::g-name-owner",
                                 G_CALLBACK (on_usb_protection_owner_changed_cb),
                                 user_data,
                                 0);

        g_signal_connect_object (source_object,
                                 "g-signal",
                                 G_CALLBACK (on_usb_protection_signal),
                                 user_data,
                                 0);

        g_dbus_proxy_new_for_bus (G_BUS_TYPE_SYSTEM,
                                  G_DBUS_PROXY_FLAGS_NONE,
                                  NULL,
                                  USBGUARD_DBUS_NAME,
                                  USBGUARD_DBUS_PATH_DEVICES,
                                  USBGUARD_DBUS_INTERFACE_DEVICES,
                                  manager->cancellable,
                                  usb_protection_devices_proxy_ready,
                                  manager);

        g_dbus_proxy_new_for_bus (G_BUS_TYPE_SYSTEM,
                                  G_DBUS_PROXY_FLAGS_NONE,
                                  NULL,
                                  USBGUARD_DBUS_NAME,
                                  USBGUARD_DBUS_PATH_POLICY,
                                  USBGUARD_DBUS_INTERFACE_POLICY,
                                  manager->cancellable,
                                  usb_protection_policy_proxy_ready,
                                  manager);
}

static GVariant *
handle_get_property (GDBusConnection *connection,
                     const gchar     *sender,
                     const gchar     *object_path,
                     const gchar     *interface_name,
                     const gchar     *property_name,
                     GError         **error,
                     gpointer         user_data)
{
        GsdUsbProtectionManager *manager = GSD_USB_PROTECTION_MANAGER (user_data);

        /* Check session pointer as a proxy for whether the manager is in the
           start or stop state */
        if (manager->connection == NULL)
                return NULL;

        if (g_strcmp0 (property_name, "Available") == 0)
                return g_variant_new_boolean (manager->available);

        return NULL;
}

static const GDBusInterfaceVTable interface_vtable =
{
        NULL,
        handle_get_property,
        NULL
};

static void
on_bus_gotten (GObject                 *source_object,
               GAsyncResult            *res,
               GsdUsbProtectionManager *manager)
{
        GDBusConnection *connection;
        GError *error = NULL;

        connection = g_bus_get_finish (res, &error);
        if (connection == NULL) {
                if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
                        g_warning ("Could not get session bus: %s", error->message);
                g_error_free (error);
                return;
        }
        manager->connection = connection;

        g_dbus_connection_register_object (connection,
                                           GSD_USB_PROTECTION_DBUS_PATH,
                                           manager->introspection_data->interfaces[0],
                                           &interface_vtable,
                                           manager,
                                           NULL,
                                           NULL);

        manager->name_id = g_bus_own_name_on_connection (connection,
                                                         GSD_USB_PROTECTION_DBUS_NAME,
                                                         G_BUS_NAME_OWNER_FLAGS_NONE,
                                                         NULL,
                                                         NULL,
                                                         NULL,
                                                         NULL);
}

static gboolean
start_usb_protection_idle_cb (GsdUsbProtectionManager *manager)
{
        g_debug ("Starting USB protection manager");

        manager->settings = g_settings_new (PRIVACY_SETTINGS);
        manager->cancellable = g_cancellable_new ();

        g_dbus_proxy_new_for_bus (G_BUS_TYPE_SYSTEM,
                                  G_DBUS_PROXY_FLAGS_NONE,
                                  NULL,
                                  USBGUARD_DBUS_NAME,
                                  USBGUARD_DBUS_PATH,
                                  USBGUARD_DBUS_INTERFACE_VERSIONED,
                                  manager->cancellable,
                                  usb_protection_proxy_ready,
                                  manager);

        notify_init ("gnome-settings-daemon");

        manager->start_idle_id = 0;

        return FALSE;
}

gboolean
gsd_usb_protection_manager_start (GsdUsbProtectionManager *manager,
                                  GError                 **error)
{
        gnome_settings_profile_start (NULL);

        manager->start_idle_id = g_idle_add ((GSourceFunc) start_usb_protection_idle_cb, manager);
        g_source_set_name_by_id (manager->start_idle_id, "[gnome-settings-daemon] start_usbguard_idle_cb");

        manager->introspection_data = g_dbus_node_info_new_for_xml (introspection_xml, NULL);
        g_assert (manager->introspection_data != NULL);

        /* Start process of owning a D-Bus name */
        g_bus_get (G_BUS_TYPE_SESSION,
                   manager->cancellable,
                   (GAsyncReadyCallback) on_bus_gotten,
                   manager);

        gnome_settings_profile_end (NULL);
        return TRUE;
}

void
gsd_usb_protection_manager_stop (GsdUsbProtectionManager *manager)
{
        g_debug ("Stopping USB protection manager");

        if (manager->cancellable != NULL) {
                g_cancellable_cancel (manager->cancellable);
                g_clear_object (&manager->cancellable);
        }

        g_clear_object (&manager->notification);

        if (manager->start_idle_id != 0) {
                g_source_remove (manager->start_idle_id);
                manager->start_idle_id = 0;
        }

        if (manager->name_id != 0) {
                g_bus_unown_name (manager->name_id);
                manager->name_id = 0;
        }

        g_clear_pointer (&manager->introspection_data, g_dbus_node_info_unref);
        g_clear_object (&manager->connection);
        g_clear_object (&manager->settings);
        g_clear_object (&manager->usb_protection);
        g_clear_object (&manager->usb_protection_devices);
        g_clear_object (&manager->usb_protection_policy);
        g_clear_object (&manager->screensaver_proxy);
}

static void
gsd_usb_protection_manager_class_init (GsdUsbProtectionManagerClass *klass)
{
        GObjectClass *object_class = G_OBJECT_CLASS (klass);

        object_class->finalize = gsd_usb_protection_manager_finalize;
}

static void
gsd_usb_protection_manager_init (GsdUsbProtectionManager *manager)
{
}

static void
gsd_usb_protection_manager_finalize (GObject *object)
{
        GsdUsbProtectionManager *usb_protection_manager;

        usb_protection_manager = GSD_USB_PROTECTION_MANAGER (object);
        gsd_usb_protection_manager_stop (usb_protection_manager);

        G_OBJECT_CLASS (gsd_usb_protection_manager_parent_class)->finalize (object);
}

GsdUsbProtectionManager *
gsd_usb_protection_manager_new (void)
{
        g_debug ("Starting USB Protection");
        if (manager_object != NULL) {
                g_object_ref (manager_object);
        } else {
                manager_object = g_object_new (GSD_TYPE_USB_PROTECTION_MANAGER, NULL);
                g_object_add_weak_pointer (manager_object,
                                           (gpointer *) &manager_object);
        }

        return GSD_USB_PROTECTION_MANAGER (manager_object);
}
