/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
 *
 * Copyright (C) 2007 William Jon McCann <mccann@jhu.edu>
 * Copyright (C) 2010,2011 Red Hat, Inc.
 * Copyright (C) 2020 Canonical Ltd.
 *
 * 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 <glib.h>
#include <gio/gio.h>
#include <p11-kit/p11-kit.h>

#include "gnome-settings-profile.h"
#include "gnome-settings-bus.h"
#include "gsd-smartcard-manager.h"
#include "gsd-smartcard-service.h"
#include "gsd-smartcard-enum-types.h"
#include "gsd-smartcard-utils.h"

#define GSD_SESSION_MANAGER_LOGOUT_MODE_FORCE 2

struct _GsdSmartcardManager
{
        GObject parent;

        guint start_idle_id;
        GsdSmartcardService *service;
        GList *smartcard_modules;
        GList *smartcards_watch_tasks;
        GCancellable *cancellable;

        GsdSessionManager *session_manager;
        GsdScreenSaver *screen_saver;

        GSettings *settings;
};

#define CONF_SCHEMA "org.gnome.settings-daemon.peripherals.smartcard"
#define KEY_REMOVE_ACTION "removal-action"

static void     gsd_smartcard_manager_class_init  (GsdSmartcardManagerClass *klass);
static void     gsd_smartcard_manager_init        (GsdSmartcardManager      *self);
static void     gsd_smartcard_manager_finalize    (GObject                  *object);
static void     lock_screen                       (GsdSmartcardManager *self);
static void     log_out                           (GsdSmartcardManager *self);
static void     on_smartcards_from_module_watched (GsdSmartcardManager *self,
                                                   GAsyncResult        *result,
                                                   gpointer             user_data);
G_DEFINE_TYPE (GsdSmartcardManager, gsd_smartcard_manager, G_TYPE_OBJECT)
G_DEFINE_QUARK (gsd-smartcard-manager-error, gsd_smartcard_manager_error)
G_LOCK_DEFINE_STATIC (gsd_smartcards_watch_tasks);

static gpointer manager_object = NULL;

static void
gsd_smartcard_manager_class_init (GsdSmartcardManagerClass *klass)
{
        GObjectClass   *object_class = G_OBJECT_CLASS (klass);

        object_class->finalize = gsd_smartcard_manager_finalize;

        gsd_smartcard_utils_register_error_domain (GSD_SMARTCARD_MANAGER_ERROR,
                                                   GSD_TYPE_SMARTCARD_MANAGER_ERROR);
}

static void
gsd_smartcard_manager_init (GsdSmartcardManager *self)
{
}

typedef struct
{
        GckModule *module;
        GHashTable *smartcards;
        int number_of_consecutive_errors;
} WatchSmartcardsOperation;

static void
on_watch_cancelled (GCancellable             *cancellable,
                    WatchSmartcardsOperation *operation)
{
        CK_FUNCTION_LIST_PTR p11_module;

        p11_module = gck_module_get_functions (operation->module);

        /* This will make C_WaitForSlotEvent return CKR_CRYPTOKI_NOT_INITIALIZED */
        p11_module->C_Finalize (NULL);

        /* And we initialize it again, even though it could not be really needed */
        p11_module->C_Initialize (NULL);
}

static GckSlot *
get_module_slot_by_handle (GckModule *module,
                           gulong     handle,
                           gboolean   with_token)
{
        g_autolist(GckSlot) slots = gck_module_get_slots (module, with_token);
        GList *l;

        for (l = slots; l; l = l->next) {
                GckSlot *slot = l->data;

                if (gck_slot_get_handle (slot) == handle)
                        return g_object_ref (slot);
        }

        return NULL;
}

static GckSlot *
wait_for_any_slot_event (GckModule  *module,
                         gboolean   *wait_blocks,
                         GError    **error)
{
        GckSlot *slot;
        CK_FUNCTION_LIST_PTR p11_module;
        CK_SLOT_ID slot_id;
        CK_RV ret;

        p11_module = gck_module_get_functions (module);

        /* We first trt to use the blocking version of the call, in case it
         * is not supported, we fallback in the non-blocking version as
         * historically not all the p11-kit modules used supported it.
         */
        *wait_blocks = TRUE;
        ret = p11_module->C_WaitForSlotEvent (0, &slot_id, NULL);
        if (ret == CKR_FUNCTION_NOT_SUPPORTED) {
                *wait_blocks = FALSE;
                ret = p11_module->C_WaitForSlotEvent (CKF_DONT_BLOCK, &slot_id, NULL);
        }

        switch (ret) {
        case CKR_NO_EVENT:
                g_set_error (error, G_IO_ERROR, G_IO_ERROR_AGAIN,
                             "Got no event, ignoring...");
                return NULL;
        case CKR_FUNCTION_NOT_SUPPORTED:
                g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
                             "Device does not support waiting for slot events");
                return NULL;
        case CKR_CRYPTOKI_NOT_INITIALIZED:
                g_set_error (error, G_IO_ERROR, G_IO_ERROR_CANCELLED,
                             "Slot wait event cancelled");
                return NULL;
        default:
                if (ret != CKR_OK) {
                        g_set_error (error, GSD_SMARTCARD_MANAGER_ERROR,
                                GSD_SMARTCARD_MANAGER_ERROR_WITH_P11KIT,
                                "Failed to wait for slot event, error: %lx.",
                                ret);
                        return NULL;
                }
                break;
        }

        slot = get_module_slot_by_handle (module, slot_id, FALSE);

        if (!slot) {
                g_autofree char *module_name = NULL;

                module_name = p11_kit_module_get_name (gck_module_get_functions (module));
                g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
                             "Slot with ID %lu not found in module %s",
                             slot_id, module_name);
        }

        return slot;
}

static gboolean
token_info_equals (GckTokenInfo *a, GckTokenInfo *b)
{
        if ((a && !b) || (!a && b))
                return FALSE;
        if (a->total_private_memory != b->total_private_memory)
                return FALSE;
        if (a->total_public_memory != b->total_public_memory)
                return FALSE;
        if (a->hardware_version_major != b->hardware_version_major)
                return FALSE;
        if (a->hardware_version_minor != b->hardware_version_minor)
                return FALSE;
        if (a->firmware_version_major != b->firmware_version_major)
                return FALSE;
        if (a->firmware_version_minor != b->firmware_version_minor)
                return FALSE;
        if (g_strcmp0 (a->serial_number, b->serial_number) != 0)
                return FALSE;
        if (g_strcmp0 (a->manufacturer_id, b->manufacturer_id) != 0)
                return FALSE;
        if (g_strcmp0 (a->model, b->model) != 0)
                return FALSE;
        if (g_strcmp0 (a->label, b->label) != 0)
                return FALSE;

        return TRUE;
}

static GckSlot *
get_changed_slot (WatchSmartcardsOperation *operation)
{
        g_autolist(GckSlot) slots_with_token = NULL;
        GHashTableIter iter;
        gpointer key, value;
        GList *l;

        slots_with_token = gck_module_get_slots (operation->module, TRUE);

        g_hash_table_iter_init (&iter, operation->smartcards);
        while (g_hash_table_iter_next (&iter, &key, &value)) {
                GckSlot *slot = key;
                GckTokenInfo *old_token = value;
                g_autoptr(GckTokenInfo) current_token = NULL;

                if (!g_list_find_custom (slots_with_token, slot, (GCompareFunc) gck_slot_equal)) {
                        /* Saved slot has not a token anymore */
                        return g_object_ref (slot);
                }

                current_token = gck_slot_get_token_info (slot);
                if (!token_info_equals (current_token, old_token)) {
                        return g_object_ref (slot);
                }
        }

        /* At this point all the saved tokens match the ones in device.
         * Now we need to check if there's a token that is not saved */
        for (l = slots_with_token; l; l = l->next) {
                GckSlot *slot = l->data;

                if (!g_hash_table_contains (operation->smartcards, slot))
                        return g_object_ref (slot);
        }

        return NULL;
}

static gboolean
watch_one_event_from_module (GsdSmartcardManager       *self,
                             WatchSmartcardsOperation  *operation,
                             GCancellable              *cancellable,
                             GError                   **error)
{
        g_autoptr(GError) wait_error = NULL;
        g_autoptr(GckSlot) slot = NULL;
        GckTokenInfo *old_token;
        gulong return_sleep = 0;
        gulong handler_id;
        gboolean token_is_present;
        gboolean token_changed;
        gboolean wait_blocks;

        wait_blocks = FALSE;
        handler_id = g_cancellable_connect (cancellable,
                                            G_CALLBACK (on_watch_cancelled),
                                            operation,
                                            NULL);

        if (handler_id != 0) {
                slot = wait_for_any_slot_event (operation->module, &wait_blocks,
                                                &wait_error);
        }

        g_cancellable_disconnect (cancellable, handler_id);

        if (g_cancellable_set_error_if_cancelled (cancellable, error)) {
                g_warning ("smartcard event function cancelled");
                return FALSE;
        }

        if (g_error_matches (wait_error, G_IO_ERROR, G_IO_ERROR_AGAIN)) {
                if (!wait_blocks) {
                        g_usleep (1 * G_USEC_PER_SEC);
                }
                return TRUE;
        } else if (g_error_matches (wait_error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED)) {
                slot = get_changed_slot (operation);
                if (slot) {
                        return_sleep = wait_blocks ? 0 : 1 * G_USEC_PER_SEC;
                        g_clear_error (&wait_error);
                } else {
                        if (!wait_blocks) {
                                g_usleep (1 * G_USEC_PER_SEC);
                        }
                        return TRUE;
                }
        }

        if (wait_error) {
                operation->number_of_consecutive_errors++;
                if (operation->number_of_consecutive_errors > 10) {
                     g_warning ("Got %d consecutive smartcard errors, so giving up.",
                                operation->number_of_consecutive_errors);

                     g_set_error (error, wait_error->domain, wait_error->code,
                                  "encountered unexpected error while "
                                  "waiting for smartcard event: %s",
                                  wait_error->message);
                     return FALSE;
                }

                g_warning ("Got potentially spurious smartcard event error: %s",
                           wait_error->message);

                if (!wait_blocks) {
                        g_usleep (1 * G_USEC_PER_SEC);
                }
                return TRUE;
        }
        operation->number_of_consecutive_errors = 0;

        g_assert (slot);
        token_is_present = gck_slot_has_flags (slot, CKF_TOKEN_PRESENT);
        old_token = g_hash_table_lookup (operation->smartcards, slot);
        token_changed = TRUE;


        /* If there is a different card in the slot now than
         * there was before, then we need to emit a removed signal
         * for the old card
         */
        if (old_token != NULL) {
                if (token_is_present) {
                        g_autoptr(GckTokenInfo) token = NULL;

                        token = gck_slot_get_token_info (slot);
                        token_changed = !token_info_equals (token, old_token);
                }

                if (token_changed) {
                        /* Card registered with slot previously is
                         * different than this card, so update its
                         * exported state to track the implicit missed
                         * removal
                         */
                        gsd_smartcard_service_sync_token (self->service, slot, cancellable);

                        g_hash_table_remove (operation->smartcards, slot);
                }
        }

        if (token_is_present) {
                if (token_changed) {
                        g_debug ("Detected smartcard insertion event in slot %lu",
                                 gck_slot_get_handle (slot));

                        g_hash_table_replace (operation->smartcards,
                                        g_object_ref (slot),
                                        gck_slot_get_token_info (slot));

                        gsd_smartcard_service_sync_token (self->service, slot,
                                                          cancellable);
                }
        } else if (old_token == NULL) {
                /* If the just removed smartcard is not known to us then
                 * ignore the removal event. NSS sends a synthentic removal
                 * event for slots that are empty at startup
                 */
                g_debug ("Detected slot %lu is empty in reader",
                          gck_slot_get_handle (slot));
        } else {
                g_debug ("Detected smartcard removal event in slot %lu",
                         gck_slot_get_handle (slot));

                /* If the just removed smartcard is known to us then
                 * we need to update its exported state to reflect the
                 * removal
                 */
                if (!token_changed)
                        gsd_smartcard_service_sync_token (self->service, slot, cancellable);
        }

        g_usleep (return_sleep);

        return TRUE;
}

static void
watch_smartcards_from_module (GTask                    *task,
                              GsdSmartcardManager      *self,
                              WatchSmartcardsOperation *operation,
                              GCancellable             *cancellable)
{
        g_debug ("watching for smartcard events");
        while (!g_cancellable_is_cancelled (cancellable)) {
                gboolean watch_succeeded;
                g_autoptr(GError) error = NULL;

                watch_succeeded = watch_one_event_from_module (self, operation, cancellable, &error);

                if (g_task_return_error_if_cancelled (task)) {
                        break;
                }

                if (!watch_succeeded) {
                        g_task_return_error (task, g_steal_pointer (&error));
                        break;
                }
        }
}

static void
destroy_watch_smartcards_operation (WatchSmartcardsOperation *operation)
{
        g_clear_object (&operation->module);
        g_hash_table_unref (operation->smartcards);
        g_free (operation);
}

static void
on_smartcards_watch_task_destroyed (GsdSmartcardManager *self,
                                    GTask               *freed_task)
{
        g_autoptr(GMutexLocker) locked = NULL;

        locked = g_mutex_locker_new (&G_LOCK_NAME (gsd_smartcards_watch_tasks));
        self->smartcards_watch_tasks = g_list_remove (self->smartcards_watch_tasks,
                                                      freed_task);
}

static void
sync_initial_tokens_from_driver (GsdSmartcardManager *self,
                                 GckModule           *module,
                                 GHashTable          *smartcards,
                                 GCancellable        *cancellable)
{
        GList *l;
        g_autolist(GckSlot) full_slots = NULL;

        full_slots = gck_module_get_slots (module, TRUE);

        for (l = full_slots; l; l = l->next) {
                GckSlot *slot = l->data;
                GckTokenInfo *token_info;

                if (!gck_slot_has_flags (slot, CKF_TOKEN_PRESENT)) {
                        CK_FUNCTION_LIST_PTR p11k_module = gck_module_get_functions (module);

                        g_warning ("Module %s returned slot with no tokens",
                                   p11_kit_module_get_name (p11k_module));
                        continue;
                }

                token_info = gck_slot_get_token_info (slot);

                g_debug ("Detected smartcard '%s' in slot %lu at start up",
                         token_info->label, gck_slot_get_handle (slot));

                g_hash_table_replace (smartcards, g_object_ref (slot), token_info);

                gsd_smartcard_service_sync_token (self->service, slot, cancellable);
        }
}

static void
watch_smartcards_from_module_async (GsdSmartcardManager *self,
                                    GckModule           *module,
                                    GCancellable        *cancellable,
                                    GAsyncReadyCallback  callback,
                                    gpointer             user_data)
{
        GTask *task;
        WatchSmartcardsOperation *operation;

        operation = g_new0 (WatchSmartcardsOperation, 1);
        operation->module = g_object_ref (module);
        operation->smartcards = g_hash_table_new_full ((GHashFunc) gck_slot_hash,
                                                       (GEqualFunc) gck_slot_equal,
                                                       g_object_unref,
                                                       (GDestroyNotify) gck_token_info_free);

        task = g_task_new (self, cancellable, callback, user_data);

        g_task_set_task_data (task,
                              operation,
                              (GDestroyNotify) destroy_watch_smartcards_operation);

        G_LOCK (gsd_smartcards_watch_tasks);
        self->smartcards_watch_tasks = g_list_prepend (self->smartcards_watch_tasks,
                                                       task);
        g_object_weak_ref (G_OBJECT (task),
                           (GWeakNotify) on_smartcards_watch_task_destroyed,
                           self);
        G_UNLOCK (gsd_smartcards_watch_tasks);

        sync_initial_tokens_from_driver (self, module, operation->smartcards, cancellable);

        g_task_run_in_thread (task, (GTaskThreadFunc) watch_smartcards_from_module);
}

static void
on_smartcards_from_module_watched (GsdSmartcardManager *self,
                                   GAsyncResult        *result,
                                   gpointer             user_data)
{
        g_autoptr(GError) error = NULL;

        if (!g_task_propagate_boolean (G_TASK (result), &error)) {
                g_debug ("Done watching smartcards from module: %s", error->message);
                return;
        }
        g_debug ("Done watching smartcards from module");
}

static gboolean
module_has_removable_slot (GckModule *module)
{
        g_autolist(GckSlot) slots = NULL;
        GList *l;

        slots = gck_module_get_slots (module, FALSE);

        for (l = slots; l; l = l->next) {
                GckSlot *slot = l->data;

                if (gck_slot_has_flags (slot, CKF_REMOVABLE_DEVICE))
                        return TRUE;
        }

        return FALSE;
}

static void
on_modules_initialized (GObject      *source_object,
                        GAsyncResult *result,
                        gpointer      user_data)
{
        g_autoptr(GTask) task = NULL;
        g_autoptr(GError) error = NULL;
        g_autolist(GckModule) modules = NULL;
        g_autoptr(GckSlot) login_token = NULL;
        GsdSmartcardManager *self;
        GList *l;

        task = g_steal_pointer (&user_data);
        self = g_task_get_source_object (task);
        modules = gck_modules_initialize_registered_finish (result, &error);

        if (error) {
                g_task_return_error (task, g_steal_pointer (&error));
                return;
        }

        for (l = modules; l; l = l->next) {
                GckModule *module = l->data;
                CK_FUNCTION_LIST_PTR p11_module;
                g_autofree char *module_name = NULL;

                p11_module = gck_module_get_functions (module);
                module_name = p11_kit_module_get_name (p11_module);

                g_debug ("Found p11-kit module %s", module_name);

                if (!module_has_removable_slot (module))
                        continue;

                self->smartcard_modules = g_list_prepend (self->smartcard_modules,
                                                          g_object_ref (module));

                gsd_smartcard_service_register_driver (self->service, module);
                watch_smartcards_from_module_async (self,
                                                    module,
                                                    self->cancellable,
                                                    (GAsyncReadyCallback) on_smartcards_from_module_watched,
                                                    NULL);
        }

        if (!self->smartcard_modules) {
                g_task_return_new_error (task, GSD_SMARTCARD_MANAGER_ERROR,
                                         GSD_SMARTCARD_MANAGER_ERROR_NO_DRIVERS,
                                         "No smartcard exist to be activated.");
                return;
        }

        login_token = gsd_smartcard_manager_get_login_token (self);

        if (login_token || gsd_smartcard_utils_get_login_token_name () != NULL) {
                if (!login_token ||
                    !gck_slot_has_flags (login_token, CKF_TOKEN_PRESENT))
                        gsd_smartcard_manager_do_remove_action (self);
        }

        g_task_return_boolean (task, TRUE);
}

static void
watch_smartcards_async (GsdSmartcardManager *self,
                        GCancellable        *cancellable,
                        GAsyncReadyCallback  callback,
                        gpointer             user_data)
{
        GTask *task;

        task = g_task_new (self, cancellable, callback, user_data);

        gck_modules_initialize_registered_async (self->cancellable,
                                                 (GAsyncReadyCallback) on_modules_initialized,
                                                 task);
}

static gboolean
watch_smartcards_async_finish (GsdSmartcardManager  *self,
                               GAsyncResult         *result,
                               GError              **error)
{
        return g_task_propagate_boolean (G_TASK (result), error);
}

static void
on_smartcards_watched (GsdSmartcardManager *self,
                       GAsyncResult        *result)
{
        g_autoptr(GError) error = NULL;

        if (!watch_smartcards_async_finish (self, result, &error)) {
                g_debug ("Error watching smartcards: %s", error->message);
        }
}

static void
on_service_created (GObject             *source_object,
                    GAsyncResult        *result,
                    GsdSmartcardManager *self)
{
        GsdSmartcardService *service;
        g_autoptr(GError) error = NULL;

        service = gsd_smartcard_service_new_finish (result, &error);

        if (service == NULL) {
                g_warning("Couldn't create session bus service: %s", error->message);
                return;
        }

        self->service = service;

        g_debug("Service created, getting modules...");

        watch_smartcards_async (self,
                                self->cancellable,
                                (GAsyncReadyCallback) on_smartcards_watched,
                                NULL);
}

static gboolean
gsd_smartcard_manager_idle_cb (GsdSmartcardManager *self)
{
        gnome_settings_profile_start (NULL);

        self->cancellable = g_cancellable_new();
        self->settings = g_settings_new (CONF_SCHEMA);

        gsd_smartcard_service_new_async (self,
                                         self->cancellable,
                                         (GAsyncReadyCallback) on_service_created,
                                         self);

        gnome_settings_profile_end (NULL);

        self->start_idle_id = 0;

        return G_SOURCE_REMOVE;
}

gboolean
gsd_smartcard_manager_start (GsdSmartcardManager  *self,
                             GError              **error)
{
        gnome_settings_profile_start (NULL);

        self->start_idle_id = g_idle_add ((GSourceFunc) gsd_smartcard_manager_idle_cb, self);
        g_source_set_name_by_id (self->start_idle_id, "[gnome-settings-daemon] gsd_smartcard_manager_idle_cb");

        gnome_settings_profile_end (NULL);

        return TRUE;
}

void
gsd_smartcard_manager_stop (GsdSmartcardManager *self)
{
        g_debug ("Stopping smartcard manager");

        g_cancellable_cancel (self->cancellable);

        g_list_free_full (g_steal_pointer (&self->smartcard_modules), g_object_unref);
        g_clear_object (&self->settings);
        g_clear_object (&self->cancellable);
        g_clear_object (&self->session_manager);
        g_clear_object (&self->screen_saver);
}

static void
on_screen_locked (GsdScreenSaver      *screen_saver,
                  GAsyncResult        *result,
                  GsdSmartcardManager *self)
{
        g_autoptr(GError) error = NULL;
        gboolean is_locked;

        is_locked = gsd_screen_saver_call_lock_finish (screen_saver, result, &error);

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

static void
lock_screen (GsdSmartcardManager *self)
{
        if (self->screen_saver == NULL)
                self->screen_saver = gnome_settings_bus_get_screen_saver_proxy ();

        gsd_screen_saver_call_lock (self->screen_saver,
                                    self->cancellable,
                                    (GAsyncReadyCallback) on_screen_locked,
                                    self);
}

static void
on_logged_out (GsdSessionManager   *session_manager,
               GAsyncResult        *result,
               GsdSmartcardManager *self)
{
        g_autoptr(GError) error = NULL;
        gboolean is_logged_out;

        is_logged_out = gsd_session_manager_call_logout_finish (session_manager, result, &error);

        if (!is_logged_out) {
                g_warning ("Couldn't log out: %s", error->message);
                return;
        }
}

static void
log_out (GsdSmartcardManager *self)
{
        if (self->session_manager == NULL)
                self->session_manager = gnome_settings_bus_get_session_proxy ();

        gsd_session_manager_call_logout (self->session_manager,
                                         GSD_SESSION_MANAGER_LOGOUT_MODE_FORCE,
                                         self->cancellable,
                                         (GAsyncReadyCallback) on_logged_out,
                                         self);
}

void
gsd_smartcard_manager_do_remove_action (GsdSmartcardManager *self)
{
        char *remove_action;

        remove_action = g_settings_get_string (self->settings, KEY_REMOVE_ACTION);
        g_debug("Do remove action %s", remove_action);

        if (strcmp (remove_action, "lock-screen") == 0)
                lock_screen (self);
        else if (strcmp (remove_action, "force-logout") == 0)
                log_out (self);
}

static GckSlot *
get_login_token_for_operation (GsdSmartcardManager      *self,
                               WatchSmartcardsOperation *operation)
{
        GHashTableIter iter;
        gpointer key, value;

        g_hash_table_iter_init (&iter, operation->smartcards);
        while (g_hash_table_iter_next (&iter, &key, &value)) {
                GckSlot *card_slot = key;
                GckTokenInfo *token_info = value;
                const char *token_name = token_info->label;

                if (g_strcmp0 (gsd_smartcard_utils_get_login_token_name (), token_name) == 0)
                        return g_object_ref (card_slot);
        }

        return NULL;
}

GckSlot *
gsd_smartcard_manager_get_login_token (GsdSmartcardManager *self)
{
        g_autoptr(GMutexLocker) locked = NULL;
        GckSlot *card_slot = NULL;
        GList *node;

        locked = g_mutex_locker_new (&G_LOCK_NAME (gsd_smartcards_watch_tasks));
        node = self->smartcards_watch_tasks;
        while (node != NULL) {
                GTask *task = node->data;
                WatchSmartcardsOperation *operation = g_task_get_task_data (task);

                card_slot = get_login_token_for_operation (self, operation);

                if (card_slot != NULL)
                        break;

                node = node->next;
        }

        return card_slot;
}

static GList *
get_inserted_tokens_for_operation (GsdSmartcardManager      *self,
                                   WatchSmartcardsOperation *operation)
{
        GList *inserted_tokens = NULL;
        GHashTableIter iter;
        gpointer key, value;

        g_hash_table_iter_init (&iter, operation->smartcards);
        while (g_hash_table_iter_next (&iter, &key, &value)) {
                GckSlot *card_slot = key;

                if (gck_slot_has_flags (card_slot, CKF_TOKEN_PRESENT))
                        inserted_tokens = g_list_prepend (inserted_tokens, g_object_ref (card_slot));
        }

        return inserted_tokens;
}

GList *
gsd_smartcard_manager_get_inserted_tokens (GsdSmartcardManager *self,
                                           gsize               *num_tokens)
{
        g_autoptr(GMutexLocker) locked = NULL;
        GList *inserted_tokens = NULL, *node;

        locked = g_mutex_locker_new (&G_LOCK_NAME (gsd_smartcards_watch_tasks));
        for (node = self->smartcards_watch_tasks; node != NULL; node = node->next) {
                GTask *task = node->data;
                WatchSmartcardsOperation *operation = g_task_get_task_data (task);
                GList *operation_inserted_tokens;

                operation_inserted_tokens = get_inserted_tokens_for_operation (self, operation);

                inserted_tokens = g_list_concat (inserted_tokens, operation_inserted_tokens);
        }

        if (num_tokens != NULL)
                *num_tokens = g_list_length (inserted_tokens);

        return inserted_tokens;
}

static void
gsd_smartcard_manager_finalize (GObject *object)
{
        GsdSmartcardManager *self;

        g_return_if_fail (object != NULL);
        g_return_if_fail (GSD_IS_SMARTCARD_MANAGER (object));

        self = GSD_SMARTCARD_MANAGER (object);

        g_return_if_fail (self != NULL);

        g_clear_handle_id (&self->start_idle_id, g_source_remove);

        gsd_smartcard_manager_stop (self);

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

GsdSmartcardManager *
gsd_smartcard_manager_new (void)
{
        if (manager_object != NULL) {
                g_object_ref (manager_object);
        } else {
                manager_object = g_object_new (GSD_TYPE_SMARTCARD_MANAGER, NULL);
                g_object_add_weak_pointer (manager_object,
                                           (gpointer *) &manager_object);
        }

        return GSD_SMARTCARD_MANAGER (manager_object);
}
