/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
 *
 * Copyright (C) 2020-2023 Marco Trevisan <marco.trevisan@canonical.com>
 *
 * 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, 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301, USA.
 */

#include "config.h"

#include <ctype.h>
#include <grp.h>
#include <locale.h>
#include <pwd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sysexits.h>

#include <glib.h>
#include <glib/gi18n.h>
#include <gio/gio.h>

#include "gdm-common.h"

#define DCONF_SYSCONFIG_PROFILES_PATH DCONF_SYSCONFIG "dconf/profile"
#define DCONF_SYSCONFIG_DB_PATH DCONF_SYSCONFIG "dconf/db"
#define DCONF_SYSTEM_DB_PREFIX "system-db:"
#define DCONF_SYSTEM_DB_DEFAULT_NAME "gdm_auth_config"

#define GDM_DEFAULT_DCONF_PROFILE \
        DCONF_PROFILES_PATH "/" GDM_DCONF_PROFILE
#define GDM_CONFIG_DCONF_PROFILE \
        DCONF_SYSCONFIG_PROFILES_PATH "/" GDM_DCONF_PROFILE

#define GDM_CONFIG_DCONF_DB_NAME GDM_DCONF_PROFILE
#define GDM_CONFIG_DCONF_DB DCONF_SYSTEM_DB_PREFIX GDM_CONFIG_DCONF_DB_NAME

#define GDM_CONFIG_DCONF_OVERRIDE_NAME "01_gdm-config"
#define GDM_CONFIG_DCONF_LOCKS_NAME GDM_CONFIG_DCONF_OVERRIDE_NAME "-locks"

#define LOGIN_SCHEMA "org.gnome.login-screen"

#define GSD_SC_SCHEMA "org.gnome.settings-daemon.peripherals.smartcard"
#define GSD_SC_REMOVAL_ACTION_KEY "removal-action"

#define PASSWORD_KEY "enable-password-authentication"
#define FINGERPRINT_KEY "enable-fingerprint-authentication"
#define SMARTCARD_KEY "enable-smartcard-authentication"

static int opt_enable = -1;
static int opt_disable = -1;
static int opt_required = -1;
static gboolean opt_cmd_help = FALSE;
static gboolean opt_debug = FALSE;
static gboolean opt_verbose = FALSE;
static const char *opt_removal_action = NULL;

typedef enum {
        COMMAND_HELP,
        COMMAND_SHOW,
        COMMAND_PASSWORD,
        COMMAND_FINGERPRINT,
        COMMAND_SMARTCARD,
        COMMAND_RESET,
        COMMAND_UNKNOWN,
} GdmConfigCommand;

typedef enum {
        AUTH_PASSWORD = COMMAND_PASSWORD,
        AUTH_FINGERPRINT = COMMAND_FINGERPRINT,
        AUTH_SMARTCARD = COMMAND_SMARTCARD,
        AUTH_NONE = COMMAND_UNKNOWN,
} GdmAuthType;

typedef enum {
        ACTION_UNSET,
        ACTION_INVALID,
        ACTION_ENABLED,
        ACTION_DISABLED,
        ACTION_REQUIRED,
} GdmAuthAction;

typedef struct
{
        GdmConfigCommand  config_command;
        GPtrArray        *args;
} OptionData;

typedef struct _GdmConfigCommandHandler GdmConfigCommandHandler;
typedef gboolean (*CommandHandlerFunc) (GdmConfigCommand, GError **);

typedef struct _GdmConfigCommandHandler {
        GPtrArray          *entries;
        GOptionParseFunc    post_parse_func;
        CommandHandlerFunc  handler_func;
        CommandHandlerFunc  options_handler_func;
        gpointer            data;
        GDestroyNotify      data_destroy;
} GdmConfigCommandHandler;

static const GOptionEntry generic_entries[] =
{
        {
                "help", 'h', 0, G_OPTION_ARG_NONE, &opt_cmd_help,
                N_("Show command help"), NULL
        },
        {
                "verbose", 'v', G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_NONE, &opt_verbose,
                N_("Show verbose output"), NULL
        },
        {
                "debug", 'u', G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_NONE, &opt_debug,
                N_("Show debug output"), NULL
        },
        { NULL }
};

static const GOptionEntry toggle_entries[] =
{
        {
                "enable", 'e', 0, G_OPTION_ARG_NONE, &opt_enable,
                N_("Enable the authentication method"), NULL
        },
        {
                "disable", 'd', 0, G_OPTION_ARG_NONE, &opt_disable,
                N_("Disable the authentication method"), NULL
        },
        { NULL }
};

static GOptionEntry smartcard_entries[] =
{
        {
                "required", 'r', 0, G_OPTION_ARG_NONE, &opt_required,
                N_("Require the authentication method"), NULL
        },
        {
                "removal-action", 'a', 0, G_OPTION_ARG_STRING, &opt_removal_action,
                N_("Action to perform on smartcard removal"), NULL,
        },
        { NULL }
};

static GOptionEntry reset_entries[] =
{
        {
                "yes", 'y', 0, G_OPTION_ARG_NONE, &opt_required,
                N_("Assume yes to any answer"), NULL
        },
        { NULL }
};

static const char* SC_REMOVAL_ACTIONS[] = {
        "none",         /* GSD_SMARTCARD_REMOVAL_ACTION_NONE */
        "lock-screen",  /* GSD_SMARTCARD_REMOVAL_ACTION_LOCK_SCREEN */
        "force-logout", /* GSD_SMARTCARD_REMOVAL_ACTION_FORCE_LOGOUT */
        "user-defined",
        NULL,
};

static gboolean handle_password (GdmConfigCommand, GError **);
static gboolean handle_fingerprint (GdmConfigCommand, GError **);
static gboolean handle_smartcard (GdmConfigCommand, GError **);
static gboolean handle_smartcard_options (GdmConfigCommand, GError **);
static gboolean handle_show (GdmConfigCommand, GError **);
static gboolean handle_reset (GdmConfigCommand, GError **);

static GdmAuthAction
get_requested_action (void)
{
        if (opt_enable == TRUE && opt_disable == TRUE) {
                return ACTION_INVALID;
        } else if (opt_required == TRUE && opt_disable == TRUE) {
                return ACTION_INVALID;
        }

        if (opt_required == TRUE)
                return ACTION_REQUIRED;

        if (opt_enable == TRUE)
                return ACTION_ENABLED;

        if (opt_disable == TRUE)
                return ACTION_DISABLED;

        return ACTION_UNSET;
}

static gboolean
smartcard_option_is_valid (const char *option_key,
                           const char *option_value)
{
        if (!option_key || !option_value)
                return FALSE;

        if (g_str_equal (GSD_SC_REMOVAL_ACTION_KEY, option_key)) {
                return g_strv_contains (SC_REMOVAL_ACTIONS, option_value);
        }

        return FALSE;
}

static GdmConfigCommand
parse_config_command (const char *command) {
        if (g_str_equal (command, "help")) {
                return COMMAND_HELP;
        } else if (g_str_equal (command, "password")) {
                return COMMAND_PASSWORD;
        } else if (g_str_equal (command, "fingerprint")) {
                return COMMAND_FINGERPRINT;
        } else if (g_str_equal (command, "smartcard")) {
                return COMMAND_SMARTCARD;
        } else if (g_str_equal (command, "show")) {
                return COMMAND_SHOW;
        } else if (g_str_equal (command, "reset")) {
                return COMMAND_RESET;
        }

        return COMMAND_UNKNOWN;
}

const char *
config_command_to_string (GdmConfigCommand config_command)
{
        switch (config_command) {
                case COMMAND_HELP:
                        return "help";
                case COMMAND_PASSWORD:
                        return "password";
                case COMMAND_FINGERPRINT:
                        return "fingerprint";
                case COMMAND_SMARTCARD:
                        return "smartcard";
                case COMMAND_RESET:
                        return "reset";
                case COMMAND_SHOW:
                        return "show";
                case COMMAND_UNKNOWN:
                        return "unknown";
        }

        g_assert_not_reached ();
}

static const char *
get_command_title (GdmConfigCommand config_command)
{
        switch (config_command) {
                case COMMAND_PASSWORD:
                        return _("Configure Password Authentication.");
                case COMMAND_FINGERPRINT:
                        return _("Configure Fingerprint Authentication.");
                case COMMAND_SMARTCARD:
                        return _("Configure Smart Card Authentication.");
                case COMMAND_RESET:
                        return _("Reset the GDM Authentication configuration.");
                case COMMAND_SHOW:
                        return _("Show GDM Authentication configuration.");
                default:
                        g_assert_not_reached ();
        }
}

static const char *
get_command_group_title (GdmConfigCommand config_command)
{
        switch (config_command) {
                case COMMAND_PASSWORD:
                        return _("Password options");
                case COMMAND_FINGERPRINT:
                        return _("Fingerprint options");
                case COMMAND_SMARTCARD:
                        return _("Smart Card options");
                case COMMAND_RESET:
                        return _("Reset options");
                case COMMAND_SHOW:
                        return _("Show options");
                default:
                        g_assert_not_reached ();
        }
}

static GdmAuthType
config_command_to_auth_type (GdmConfigCommand config_command)
{
        switch (config_command) {
                case COMMAND_PASSWORD:
                case COMMAND_SMARTCARD:
                case COMMAND_FINGERPRINT:
                        return (GdmAuthType) config_command;
                default:
                        return AUTH_NONE;
        }
}

const char *
auth_type_to_string (GdmAuthType auth_type)
{
        return config_command_to_string ((GdmConfigCommand) auth_type);
}

const char *
get_pam_module_missing_error (GdmAuthType auth_type)
{
        switch (auth_type) {
                case AUTH_PASSWORD:
                        return _("No PAM module available for Password authentication");
                case AUTH_SMARTCARD:
                        return _("No PAM module available for Smart Card authentication");
                case AUTH_FINGERPRINT:
                        return _("No PAM module available for Fingerprint authentication");
                default:
                        g_assert_not_reached ();
        }
}

const char *
auth_type_to_option_key (GdmAuthType auth_type)
{
        switch (auth_type) {
                case AUTH_PASSWORD:
                        return PASSWORD_KEY;
                case AUTH_FINGERPRINT:
                        return FINGERPRINT_KEY;
                case AUTH_SMARTCARD:
                        return SMARTCARD_KEY;
                default:
                        g_assert_not_reached ();
        }
}

static gboolean
toggle_option_check (GOptionContext *context,
                     GOptionGroup *group,
                     gpointer user_data,
                     GError **error)
{
        OptionData *data = user_data;

        if (data->args->len < 2) {
                g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE,
                             _("“%s” needs at least one parameter"),
                             config_command_to_string (data->config_command));
                return FALSE;
        }

        if (get_requested_action () == ACTION_INVALID) {

                g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE,
                             /* TRANSLATORS: “command” can't be enabled... */
                             _("“%s” can't be enabled and disabled at the same time"),
                             config_command_to_string (data->config_command));
                return FALSE;
        }

        return TRUE;
}

static gboolean
smartcard_option_check (GOptionContext *context,
                        GOptionGroup *group,
                        gpointer user_data,
                        GError **error)
{
        OptionData *data = user_data;

        if (!toggle_option_check (context, group, user_data, error))
                return FALSE;

        if (opt_removal_action &&
            !smartcard_option_is_valid (GSD_SC_REMOVAL_ACTION_KEY, opt_removal_action)) {
                g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE,
                             /* TRANSLATORS: option is not a valid command “option-key” value */
                             _("“%s” is not a valid %s “%s” value"),
                             opt_removal_action,
                             config_command_to_string (data->config_command),
                             GSD_SC_REMOVAL_ACTION_KEY);
                return FALSE;
        }

        return TRUE;
}

static void
gdm_config_command_handler_free (GdmConfigCommandHandler *command_handler)
{
        if (command_handler->data_destroy)
                command_handler->data_destroy (command_handler->data);

        g_clear_pointer (&command_handler->entries, g_ptr_array_unref);
        g_free (command_handler);
}
G_DEFINE_AUTOPTR_CLEANUP_FUNC(GdmConfigCommandHandler, gdm_config_command_handler_free);

static GdmConfigCommandHandler *
config_command_get_handler (GdmConfigCommand config_command)
{
        g_autoptr (GdmConfigCommandHandler) cmd_entries = NULL;

        cmd_entries = g_new0 (GdmConfigCommandHandler, 1);
        cmd_entries->entries = g_ptr_array_new ();

        switch (config_command) {
                case COMMAND_PASSWORD:
                case COMMAND_FINGERPRINT:
                case COMMAND_SMARTCARD:
                        g_ptr_array_add (cmd_entries->entries, (gpointer) toggle_entries);
                        cmd_entries->post_parse_func = toggle_option_check;
                        break;
                case COMMAND_SHOW:
                        cmd_entries->handler_func = handle_show;
                        return g_steal_pointer (&cmd_entries);
                case COMMAND_RESET:
                        g_ptr_array_add (cmd_entries->entries, (gpointer) reset_entries);
                        cmd_entries->handler_func = handle_reset;
                        return g_steal_pointer (&cmd_entries);
                case COMMAND_HELP:
                case COMMAND_UNKNOWN:
                        return NULL;
                default:
                        g_assert_not_reached ();
        }

        if (config_command == COMMAND_PASSWORD) {
                cmd_entries->handler_func = handle_password;
        } else if (config_command == COMMAND_FINGERPRINT) {
                cmd_entries->handler_func = handle_fingerprint;
        } else if (config_command == COMMAND_SMARTCARD) {
                g_autofree char *removal_args = NULL;
                int i;

                removal_args = g_strjoinv ("|", (GStrv) SC_REMOVAL_ACTIONS);
                for (i = 0; smartcard_entries[i].long_name; ++i) {
                        if (!g_str_equal (smartcard_entries[i].long_name, "removal-action"))
                                continue;

                        smartcard_entries[i].arg_description = removal_args;

                        cmd_entries->data = g_steal_pointer (&removal_args);
                        cmd_entries->data_destroy = g_free;
                }
                g_ptr_array_add (cmd_entries->entries, smartcard_entries);
                cmd_entries->post_parse_func = smartcard_option_check;
                cmd_entries->handler_func = handle_smartcard;
                cmd_entries->options_handler_func = handle_smartcard_options;
        } else {
                g_assert_not_reached ();
        }

        return g_steal_pointer (&cmd_entries);
}

static void
config_command_handler_add_to_group (GdmConfigCommandHandler *command_handler,
                                  GOptionGroup            *group)
{
        guint i;

        if (!command_handler || !command_handler->entries)
                return;

        for (i = 0; i < command_handler->entries->len; ++i) {
                const GOptionEntry *entries;

                entries = g_ptr_array_index (command_handler->entries, i);
                g_option_group_add_entries (group, entries);
        }

        g_option_group_set_parse_hooks (group, NULL,
                                        command_handler->post_parse_func);
}

static gboolean
switch_to_gdm_user (GError **error) {
        struct passwd *pwent;

        /* We don't care about forking here, as we need to do just one action
         * so, once we switch, there's no point of return */

        gdm_get_pwent_for_name (GDM_USERNAME, &pwent);

        if (pwent == NULL) {
                g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
                             _("Failed to switch to %s user"), GDM_USERNAME);
                return FALSE;
        }

        g_debug ("Switching to %s user (uid:gid) %d:%d",
                 GDM_USERNAME, pwent->pw_uid, pwent->pw_gid);

        if (setgid (pwent->pw_gid) < 0)  {
                g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                             _("Couldn’t set groupid to %d"), pwent->pw_gid);
                return FALSE;
        }

        if (initgroups (pwent->pw_name, pwent->pw_gid) < 0) {
                g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                             _("initgroups () failed for %s"), pwent->pw_name);
                return FALSE;
        }

        if (setuid (pwent->pw_uid) < 0)  {
                g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                             _("Couldn’t set userid to %u"), pwent->pw_uid);
                return FALSE;
        }

        g_unsetenv ("XDG_RUNTIME_DIR");
        g_unsetenv ("DISPLAY");

        if (pwent->pw_dir != NULL && pwent->pw_dir[0] != '\0') {
                g_setenv ("HOME", pwent->pw_dir, TRUE);
                g_setenv ("PWD", GDM_WORKING_DIR, TRUE);
        }

        return TRUE;
}

static GPtrArray *
read_file_contents_to_array (const char  *file_path,
                             GError     **error)
{
        g_autoptr(GPtrArray) array = NULL;
        g_autofree char *content = NULL;

        array = g_ptr_array_new_with_free_func (g_free);

        if (!g_file_get_contents (file_path, &content, NULL, error)) {
                return NULL;
        }

        if (content) {
                char **lines = g_strsplit (content, "\n", -1);
                int i;

                if (lines && *lines) {
                        array->len = g_strv_length (lines) + 1;
                        array->pdata = (gpointer *) g_steal_pointer (&lines);
                }

                /* Strip the trailing empty and NULL lines */
                for (i = array->len - 1; i >= 0; i--) {
                        char *line = g_ptr_array_index (array, i);

                        if (line && (g_ascii_isgraph (line[0]) || strlen (line) > 1))
                                break;

                        g_ptr_array_remove_index (array, i);
                }
        }

        return g_steal_pointer (&array);
}

static gboolean
write_array_to_file (GPtrArray  *array,
                     char       *file_path,
                     GError    **error)
{
        g_autoptr (GPtrArray) array_copy = NULL;
        g_autofree char *contents = NULL;

        array_copy = g_ptr_array_copy (array, NULL, NULL);
        g_ptr_array_set_free_func (array_copy, NULL);

        if (array->len) {
                /* Ensure final new line */
                if (!g_str_equal (g_ptr_array_index (array_copy, array->len - 1), ""))
                        g_ptr_array_add (array_copy, "");
        }

        g_ptr_array_add (array_copy, NULL);

        contents = g_strjoinv ("\n", (char **) array_copy->pdata);

        return g_file_set_contents (file_path, contents, -1, error);
}

GPtrArray *
build_distro_hook_arguments (const char       *distro_hook,
                             GdmConfigCommand  config_command,
                             GPtrArray        *command_args)
{
        g_autoptr(GPtrArray) call_args = NULL;
        GdmAuthAction action;

        action = get_requested_action ();
        call_args = g_ptr_array_new ();

        g_ptr_array_add (call_args, (char *) distro_hook);
        g_ptr_array_add (call_args, (char *) config_command_to_string (config_command));

        if (command_args)
                g_ptr_array_extend (call_args, command_args, NULL, NULL);
        else if (action == ACTION_REQUIRED)
                g_ptr_array_add (call_args, "require");
        else if (action == ACTION_ENABLED)
                g_ptr_array_add (call_args, "enable");
        else if (action == ACTION_DISABLED)
                g_ptr_array_add (call_args, "disable");
        else if (config_command != COMMAND_RESET)
                return NULL;

        g_ptr_array_add (call_args, NULL);

        if (opt_verbose) {
                g_autofree char *cmdline = NULL;

                cmdline = g_strjoinv (" ", (GStrv) call_args->pdata);
                g_debug ("Calling hook command “%s”", cmdline);
        }

        return g_steal_pointer (&call_args);
}

static gboolean
try_run_distro_hook (GdmConfigCommand   config_command,
                     GPtrArray         *command_args,
                     char             **stdout_out,
                     GError           **error)
{
        g_autoptr(GPtrArray) call_args = NULL;
        g_autoptr(GError) local_error = NULL;
        g_autofree char *distro_hook = NULL;
        g_autofree char *local_stdout = NULL;
        g_autofree char *stderr = NULL;
        gint exit_status;

        /* Distro hooks need to follow these rules:
         *  - The name of the hook should be "gdm-auth-config-$DISTRO_NAME",
         *  - Must be executable and being placed in the GDM's libexec dir.
         *
         * And it will be called in this way:
         *  gdm-auth-config-foo [command] [enable|disable|require|$option] \
         *                                [option-specific-params]
         * or
         *  gdm-auth-config-foo show [command] [$option]
         * or
         *  gdm-auth-config-foo reset [require]
         *
         * In set mode, if the exit code is 19 (as SIGSTOP), we won't proceed
         * doing further actions, as we consider that the script already handled
         * all the required changes. Otherwise if it the exit status is 0,
         * then we will continue performing the default actions.
         *
         * When in 'show' mode it should print in stdout one of these values:
         *  - required
         *  - enabled
         *  - disabled
         *  - command's option specific output
         *
         * However, we'll always double-check that the returned value matches
         * the system settings.
         *
         * For example:
         *  gdm-auth-config-foo show smartcard
         *  > expected: enabled|disabled|required
         *  gdm-auth-config-foo show smartcard removal-action
         *  > expected: 'none|log-out|lock-screen'
         *  gdm-auth-config-foo password disable
         *  gdm-auth-config-foo smartcard require
         *  > expected: exit status of 0 if done or 19 if we need to continue
         */

        if (DISTRO[0] == '\0' || g_str_equal (DISTRO, "none")) {
                g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
                             _("No distro detected, no hook to run"));
                return FALSE;
        }

        distro_hook = g_build_filename (LIBEXECDIR, "gdm-auth-config-" DISTRO, NULL);
        g_debug ("Looking for distro hook “%s”", distro_hook);

        if (!g_file_test (distro_hook, G_FILE_TEST_IS_REGULAR |
                                       G_FILE_TEST_IS_EXECUTABLE)) {
                g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
                             "%s is not an executable", distro_hook);
                return FALSE;
        }

        call_args = build_distro_hook_arguments (distro_hook, config_command, command_args);

        if (!call_args) {
                g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
                             _("No valid args found to run hook “%s”"), distro_hook);
                return FALSE;
        }

        if (opt_verbose)
                g_print (_("Running distro hook “%s”\n"), distro_hook);

        g_spawn_sync (NULL, (GStrv) call_args->pdata, NULL, G_SPAWN_DEFAULT,
                      NULL, NULL, &local_stdout, &stderr, &exit_status, &local_error);

        if (local_stdout)
                local_stdout = g_strstrip (local_stdout);
        if (stderr)
                stderr = g_strstrip (stderr);

        if (!local_error) {
                if (WEXITSTATUS (exit_status) == SIGSTOP) {
                        g_print ("%s\n", local_stdout);
                        g_set_error (error, G_IO_ERROR, G_IO_ERROR_CANCELLED,
                                     _("Distro hook “%s” requested stopping"),
                                     distro_hook);
                        return FALSE;
                } else {
#if GLIB_CHECK_VERSION (2, 70, 0)
                        g_spawn_check_wait_status (exit_status, &local_error);
#else
                        g_spawn_check_exit_status (exit_status, &local_error);
#endif
                }
        }

        if (local_error) {
                g_warning (_("Distro hook failed with exit status %d and error %s:\n"
                             "Standard output:\n%s\n"
                             "Error output:\n%s"),
                           WEXITSTATUS (exit_status), local_error->message,
                           stderr, local_stdout);
                g_propagate_error (error, g_steal_pointer (&local_error));
                return FALSE;
        }

        g_debug ("Distro hook ran correctly\n"
                 "Standard output:\n%s\n"
                 "Error output:\n%s",
                local_stdout, stderr);

        if (stdout_out)
                *stdout_out = g_steal_pointer (&local_stdout);

        return TRUE;
}

static gboolean
set_distro_hook_config (GdmConfigCommand   config_command,
                        const char        *option_key,
                        const char        *option_value,
                        GError           **error)
{
        g_autoptr(GPtrArray) command_args = NULL;
        g_autoptr(GError) local_error = NULL;
        g_autofree char *stdout = NULL;

        command_args = g_ptr_array_sized_new (2);
        g_ptr_array_add (command_args, (char *) option_key);
        g_ptr_array_add (command_args, (char *) option_value);

        if (!try_run_distro_hook (config_command, command_args, &stdout, &local_error)) {
                if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
                        g_debug ("Distro hook for %s %s requested us to stop",
                                 config_command_to_string (config_command), option_key);
                        g_propagate_error (error, g_steal_pointer (&local_error));
                        return FALSE;
                }

                if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) {
                        g_debug ("Distro hook for command %s option-key %s not found: %s",
                        config_command_to_string (config_command), option_key,
                        local_error->message);
                }

                g_propagate_prefixed_error (error, g_steal_pointer (&local_error),
                                            /* TRANSLATORS: Failed to set command “command” option key “option-key” via distro hook */
                                            _("Failed to set command “%s” option key “%s” via distro hook: "),
                                            config_command_to_string (config_command),
                                            option_key);
                return FALSE;
        }

        g_debug ("Distro hook for %s %s completed, "
                 "continuing with default action...",
                 config_command_to_string (config_command), option_key);
        g_print ("%s\n", stdout);

        return TRUE;
}

static char *
get_distro_hook_config (GdmAuthType   auth_type,
                        const char   *option_key,
                        GError      **error)
{
        g_autofree char *output = NULL;
        g_autoptr(GPtrArray) command_args = NULL;

        command_args = g_ptr_array_sized_new (2);
        g_ptr_array_add (command_args, (char *) auth_type_to_string (auth_type));
        g_ptr_array_add (command_args, (char *) option_key);

        if (!try_run_distro_hook (COMMAND_SHOW, command_args, &output, error)) {
                return NULL;
        }

        return g_steal_pointer (&output);
}

static gboolean
make_directory_with_parents (const char *path,
                             GError     **error)
{
        if (!g_file_test (path, G_FILE_TEST_IS_DIR) &&
            g_mkdir_with_parents (path, 0755) != 0) {
                g_set_error (error, G_IO_ERROR, g_io_error_from_errno (errno),
                             _("Failed to create directory %s"), path);
                return FALSE;
        }

        return TRUE;
}

static gboolean
check_distro_hook_is_required (GdmConfigCommand config_command)
{
        switch (config_command) {
                case COMMAND_SMARTCARD:
                        /* Enabling smartcard or requiring authentication might
                         * involve further actions that depend on distribution,
                         * (such as switching PAM profiles), as per this we
                         * always require an hook to be present.
                         * For the generic cases for which GDM already ships PAM
                         * configuration files, we may use a generic hook that
                         * does nothing, but leaves us to change the parameters
                         * that we can handle.
                         */
                        return (opt_enable == TRUE || opt_required == TRUE);
                default:
                        return FALSE;
        }
}

static const char *
get_dconf_system_profile (void)
{
        const char *profile;

        profile = g_getenv ("DCONF_PROFILE");
        if (profile)
                return profile;

        return "user";
}

static char *
get_dconf_db_path (const char *db_name)
{
        g_autofree char *db_dir = NULL;

        db_dir = g_strdup_printf ("%s.d", db_name);

        return g_build_filename (DCONF_SYSCONFIG_DB_PATH, db_dir, NULL);
}

static char *
get_dconf_system_profile_file (GError **error)
{
        const char *profile;
        const char * const *xdg_data_dirs;
        const char *prefix;

        /* This is based on what happens on dconf_engine_open_profile_file */
        profile = get_dconf_system_profile ();
        prefix = DCONF_SYSCONFIG;

        xdg_data_dirs = g_get_system_data_dirs ();
        do {
                g_autofree char *filename = NULL;

                filename = g_build_filename (prefix, "dconf", "profile", profile, NULL);
                if (g_file_test (filename, G_FILE_TEST_IS_REGULAR)) {
                            return g_steal_pointer (&filename);
                }
        } while ((prefix = *xdg_data_dirs++));

        g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
                     _("dconf profile not found"));

        return NULL;
}

static char *
ensure_dconf_configurable_system_profile (GError **error)
{
        g_autofree char *profile_file = NULL;
        g_autofree char *db_path = NULL;
        g_autoptr(GPtrArray) profile_lines = NULL;
        const char *gdm_sys_db;

        gdm_sys_db = DCONF_SYSTEM_DB_PREFIX DCONF_SYSTEM_DB_DEFAULT_NAME;
        profile_file = g_build_filename (DCONF_SYSCONFIG_PROFILES_PATH,
                                         get_dconf_system_profile (), NULL);

        if (!g_file_test (profile_file, G_FILE_TEST_IS_REGULAR)) {
                if (!make_directory_with_parents (DCONF_SYSCONFIG_PROFILES_PATH, error))
                        return FALSE;

                profile_lines = g_ptr_array_new_with_free_func (g_free);
                g_ptr_array_add (profile_lines, g_strdup ("user-db:user"));
                g_ptr_array_add (profile_lines, g_strdup (gdm_sys_db));
        } else {
                profile_lines = read_file_contents_to_array (profile_file, error);
                if (!profile_lines)
                        return FALSE;

                if (!g_ptr_array_find_with_equal_func (profile_lines, gdm_sys_db,
                                                       g_str_equal, NULL)) {
                        g_ptr_array_add (profile_lines, g_strdup (gdm_sys_db));
                } else {
                        g_clear_pointer (&profile_lines, g_ptr_array_unref);
                }
        }

        if (profile_lines) {
                if (!write_array_to_file (profile_lines, profile_file, error))
                        return FALSE;
        }

        db_path = get_dconf_db_path (DCONF_SYSTEM_DB_DEFAULT_NAME);
        if (!make_directory_with_parents (db_path, error)) {
                return FALSE;
        }

        return g_steal_pointer (&profile_file);
}

GPtrArray *
get_system_dconf_profile_contents (GError **error)
{
        g_autofree char *profile_file = NULL;

        profile_file = get_dconf_system_profile_file (error);
        if (!profile_file)
                return NULL;

        return read_file_contents_to_array (profile_file, error);
}

char *
get_system_dconf_profile_db_name (const char  *preferred,
                                  GError     **error)
{
        g_autoptr (GPtrArray) profile_contents = NULL;
        const char *db_name = NULL;
        int i;

        profile_contents = get_system_dconf_profile_contents (error);
        if (!profile_contents)
                return NULL;

        for (i = 0; i < profile_contents->len; i++) {
                const char *profile_line = g_ptr_array_index (profile_contents, i);

                if (g_str_has_prefix (profile_line, DCONF_SYSTEM_DB_PREFIX) &&
                    strlen (profile_line) > sizeof (DCONF_SYSTEM_DB_PREFIX) - 1) {
                        db_name = profile_line + sizeof (DCONF_SYSTEM_DB_PREFIX) - 1;

                        if (!preferred || g_str_equal (preferred, db_name))
                                return g_strdup (db_name);
                }
        }

        if (db_name)
                return g_strdup (db_name);

        g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
                     _("dconf has no system-db configured"));

        return NULL;
}

static char *
ensure_system_user_dconf_profile (GError **error)
{
        g_autofree char *db_name = NULL;
        g_autoptr(GError) local_error = NULL;

        db_name = get_system_dconf_profile_db_name (DCONF_SYSTEM_DB_DEFAULT_NAME,
                                                    &local_error);
        if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) {
                g_autofree char *profile_file = NULL;

                profile_file = ensure_dconf_configurable_system_profile (error);
                if (!profile_file)
                        return FALSE;

                g_debug ("Initialized dconf profile at %s", profile_file);
                db_name = get_system_dconf_profile_db_name (DCONF_SYSTEM_DB_DEFAULT_NAME,
                                                            error);
                if (!db_name)
                        return FALSE;
        } else if (local_error) {
                g_propagate_error (error, g_steal_pointer (&local_error));
                return FALSE;
        }

        g_assert (db_name);
        g_debug ("Found system database with name “%s”", db_name);

        return g_steal_pointer (&db_name);
}

static gboolean
ensure_gdm_config_dconf_profile (GError **error)
{
        g_autofree char* current_content = NULL;
        gboolean needs_profile_update = TRUE;

        g_debug ("Ensuring dconf profile in " GDM_CONFIG_DCONF_PROFILE);

        g_file_get_contents (GDM_CONFIG_DCONF_PROFILE, &current_content,
                             NULL, NULL);

        if (current_content) {
                g_auto(GStrv) content_lines = NULL;

                content_lines = g_strsplit (current_content, "\n", -1);

                if (g_strv_contains ((const char **) content_lines,
                                     GDM_CONFIG_DCONF_DB)) {
                        g_debug ("Profile doesn't need to be updated");
                        needs_profile_update = FALSE;
                }
        }

        if (needs_profile_update) {
                g_autofree char* default_content = NULL;
                g_autofree char* profile_content = NULL;

                if (!g_file_get_contents (GDM_DEFAULT_DCONF_PROFILE, &default_content,
                                          NULL, error))
                        return FALSE;

                profile_content = g_strdup_printf ("# This profile is managed by %s\n"
                                                   "%s\n"
                                                   "%s\n",
                                                   g_get_prgname(),
                                                   default_content,
                                                   GDM_CONFIG_DCONF_DB);

                g_debug ("Creating profile file "
                         GDM_CONFIG_DCONF_PROFILE
                         " using default content from "
                         GDM_DEFAULT_DCONF_PROFILE " and "
                         GDM_CONFIG_DCONF_DB);

                if (!make_directory_with_parents (DCONF_SYSCONFIG_PROFILES_PATH, error))
                        return FALSE;

                if (!g_file_set_contents (GDM_CONFIG_DCONF_PROFILE, profile_content,
                                          -1, error))
                        return FALSE;
        }

        return TRUE;
}

static char *schema_to_key (const char *schema)
{
        g_auto(GStrv) split = NULL;

        split = g_strsplit (schema, ".", -1);
        return g_strjoinv ("/", split);
}

static gboolean
write_dconf_setting_to_key_file (const char  *db_name,
                                 const char  *file_name,
                                 const char  *schema,
                                 const char  *key,
                                 GVariant    *value,
                                 GError     **error)
{
        g_autoptr(GKeyFile) key_file = NULL;
        g_autoptr(GError) local_error = NULL;
        g_autoptr(GVariant) value_ref = NULL;
        g_autofree char *value_str = NULL;
        g_autofree char *header_comment = NULL;
        g_autofree char *db_dir = NULL;
        g_autofree char *file_path = NULL;
        g_autofree char *setting_path = NULL;
        gboolean create_new = FALSE;

        db_dir = get_dconf_db_path (db_name);
        file_path = g_build_filename (db_dir, file_name, NULL);
        key_file = g_key_file_new ();

        if (!g_key_file_load_from_file (key_file, file_path,
                                        G_KEY_FILE_KEEP_COMMENTS, &local_error)) {
                create_new = TRUE;
                g_debug ("Impossible to load setting file “%s” %s", file_path,
                         local_error->message);

                if (!value) {
                        /* We had no value to set anyways... */
                        return TRUE;
                }
        }

        header_comment = g_strdup_printf ("This file has been generated by %s\n"
                                          "Do no edit!", g_get_prgname ());

        if (create_new) {
                if (!g_key_file_set_comment (key_file, NULL, NULL,
                                             header_comment, error)) {
                        return FALSE;
                }
        } else {
                g_autofree char *file_comment = NULL;

                file_comment = g_key_file_get_comment (key_file, NULL, NULL, &local_error);
                file_comment = g_strstrip (file_comment);

                if (local_error) {
                        /* TRANSLATORS: First value is a file path, second is an error message */
                        g_warning (_("Failed to get the “%s” header comment: %s, was it modified?"),
                                   file_path, local_error->message);
                } else if (g_strcmp0 (file_comment, header_comment) != 0) {
                        g_warning (_("File “%s” header comment does not match, was it modified?"),
                                   file_path);
                }
        }

        setting_path = schema_to_key (schema);

        if (value) {
                value_ref = g_variant_ref_sink (value);
                value_str = g_variant_print (value_ref, FALSE);

                if (g_variant_is_of_type (value, G_VARIANT_TYPE_BOOLEAN))
                        g_key_file_set_boolean (key_file, setting_path, key,
                                                g_variant_get_boolean (value));
                else
                        g_key_file_set_string (key_file, setting_path, key, value_str);
        } else {
                g_clear_error (&local_error);
                g_key_file_remove_key (key_file, setting_path, key, &local_error);

                if (local_error &&
                    !g_error_matches (local_error, G_KEY_FILE_ERROR,
                                      G_KEY_FILE_ERROR_KEY_NOT_FOUND) &&
                    !g_error_matches (local_error, G_KEY_FILE_ERROR,
                                      G_KEY_FILE_ERROR_GROUP_NOT_FOUND)) {
                        g_propagate_error (error, g_steal_pointer (&local_error));
                        return FALSE;
                }
        }

        g_debug ("Writing %s configuration “%s = %s” to file %s",
                 setting_path, key, value_str,
                 file_path);

        if (create_new && !make_directory_with_parents (db_dir, error))
                return FALSE;

        return g_key_file_save_to_file (key_file, file_path, error);
}

static gboolean
remove_dconf_setting_key_file (const char *db_name,
                               const char *key_file,
                               GError     **error)
{
        g_autofree char *db_dir = NULL;
        g_autofree char *key_file_path = NULL;
        g_autoptr (GFile) file = NULL;

        db_dir = get_dconf_db_path (db_name);
        key_file_path = g_build_filename (db_dir, key_file, NULL);
        file = g_file_new_for_path (key_file_path);

        g_debug ("Removing setting key file %s", key_file_path);

        if (g_file_delete (file, NULL, error)) {
                /* Try to remove the parent dir, if empty */
                g_autoptr (GFile) parent_dir = g_file_get_parent (file);
                g_file_delete (parent_dir, NULL, NULL);
                return TRUE;
        }

        return FALSE;
}

static char *
get_dconf_db_lock_path (const char *db_name,
                        const char *lock_name)
{
        g_autofree char *db_dir = NULL;
        g_autofree char *db_locks_dir = NULL;

        db_dir = get_dconf_db_path (db_name);
        db_locks_dir = g_build_filename (db_dir, "locks", NULL);

        return g_build_filename (db_locks_dir, lock_name, NULL);
}

static gboolean
set_dconf_setting_lock_full (const char *db_name,
                             const char *lock_name,
                             const char *schema,
                             const char *key,
                             gboolean    add,
                             GError    **error)
{
        g_autoptr(GPtrArray) locks_list = NULL;
        g_autoptr(GError) local_error = NULL;
        g_autofree char *lock_file = NULL;
        g_autofree char *file_header = NULL;
        g_autofree char *locked_schema = NULL;
        g_autofree char *full_key = NULL;
        gboolean found;
        unsigned int idx;

        lock_file = get_dconf_db_lock_path (db_name, lock_name);
        locks_list = read_file_contents_to_array (lock_file, &local_error);

        if (local_error) {
                g_debug ("Impossible to read current locks: %s", local_error->message);

                if (!add) {
                        if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND) ||
                            g_error_matches (local_error, G_FILE_ERROR, G_FILE_ERROR_NOENT))
                                return TRUE;

                        g_propagate_error (error, g_steal_pointer (&local_error));
                        return FALSE;
                }
        }

        file_header = g_strdup_printf ("# This file is managed by %s",
                                       g_get_prgname ());

        if (!locks_list) {
                g_autofree char *db_locks_dir = NULL;

                g_assert (add);
                locks_list = g_ptr_array_new_with_free_func (g_free);
                g_ptr_array_add (locks_list, g_steal_pointer (&file_header));
                g_ptr_array_add (locks_list, g_strdup ("# Do no edit!"));

                db_locks_dir = g_path_get_dirname (lock_file);
                if (!make_directory_with_parents (db_locks_dir, error))
                        return FALSE;
        } else {
                if (locks_list->len && !g_str_equal (g_ptr_array_index (locks_list, 0), file_header)) {
                        /* XXX: Fail with an error instead? */
                        g_warning (_("No expected header found on lock file “%s”, was it modified?"),
                                   lock_file);
                }
        }

        locked_schema = schema_to_key (schema);
        full_key = g_strdup_printf ("/%s/%s", locked_schema, key);
        found = g_ptr_array_find_with_equal_func (locks_list, full_key, g_str_equal, &idx);

        if (found && add) {
                g_debug ("No need to add lock %s to %s, already present",
                         full_key, lock_file);
                return TRUE;
        } else if (!found && !add) {
                g_debug ("No need to remove lock %s to %s, not present",
                         full_key, lock_file);
                return TRUE;
        }

        if (add) {
                g_debug ("Adding key %s lock to %s", full_key, lock_file);
                g_ptr_array_add (locks_list, g_strdup (full_key));
        } else {
                g_debug ("Removing key %s lock from %s", full_key, lock_file);
                g_ptr_array_remove_index (locks_list, idx);
        }

        return write_array_to_file (locks_list, lock_file, error);
}

static gboolean
set_dconf_setting_lock (const char *db_name,
                        const char *lock_name,
                        const char *schema,
                        const char *key,
                        GError    **error)
{
        return set_dconf_setting_lock_full (db_name, lock_name, schema, key, TRUE, error);
}

static gboolean
unset_dconf_setting_lock (const char *db_name,
                          const char *lock_name,
                          const char *schema,
                          const char *key,
                          GError    **error)
{
        return set_dconf_setting_lock_full (db_name, lock_name, schema, key, FALSE, error);
}

static gboolean
remove_dconf_setting_lock (const char  *db_name,
                           const char  *lock_name,
                           GError     **error)
{
        g_autofree char *lock_path = NULL;
        g_autoptr (GFile) file = NULL;

        lock_path = get_dconf_db_lock_path (db_name, lock_name);
        file = g_file_new_for_path (lock_path);

        g_debug ("Removing setting lock file %s", lock_path);

        if (g_file_delete (file, NULL, error)) {
                /* Try to remove the parent dir, if empty */
                g_autoptr (GFile) parent_dir = g_file_get_parent (file);
                g_file_delete (parent_dir, NULL, NULL);
                return TRUE;
        }

        return FALSE;
}

static gboolean
update_dconf (GError **error)
{
        g_auto(GStrv) environ = NULL;
        const char* command[] = { "dconf", "update", NULL };
        gint exit_code;

        g_debug ("Updating dconf...");

        environ = g_get_environ ();
        environ = g_environ_setenv (environ, "G_DEBUG", "fatal-warnings", FALSE);

        if (!g_spawn_sync (NULL, (GStrv) command, environ, G_SPAWN_SEARCH_PATH,
                           NULL, NULL, NULL, NULL, &exit_code, error))
                return FALSE;

#if GLIB_CHECK_VERSION (2, 70, 0)
        return g_spawn_check_wait_status (exit_code, error);
#else
        return g_spawn_check_exit_status (exit_code, error);
#endif
}

static gboolean
write_system_setting_to_db (const char  *db_name,
                            const char  *schema,
                            const char  *key,
                            GVariant    *value,
                            GError     **error)
{
        return write_dconf_setting_to_key_file (db_name,
                                                GDM_CONFIG_DCONF_OVERRIDE_NAME,
                                                schema,
                                                key,
                                                value,
                                                error);
}

static gboolean
write_locked_setting_to_db (const char   *db_name,
                            const char   *schema,
                            const char   *key,
                            GVariant     *value,
                            GError      **error)
{
        if (!write_system_setting_to_db (db_name, schema, key, value, error)) {
                return FALSE;
        }

        if (!set_dconf_setting_lock_full (db_name, GDM_CONFIG_DCONF_LOCKS_NAME,
                                          schema, key, value != NULL, error)) {
                return FALSE;
        }

        if (!update_dconf (error))
                return FALSE;

        return TRUE;
}

static gboolean
write_locked_system_settings_to_db (const char   *schema,
                                    const char   *key,
                                    GVariant     *value,
                                    GError      **error)
{
        g_autofree char *db_name = NULL;

        if (value) {
                db_name = ensure_system_user_dconf_profile (error);
                if (!db_name)
                        return FALSE;
        } else {
                g_autoptr (GError) local_error = NULL;

                db_name = get_system_dconf_profile_db_name (DCONF_SYSTEM_DB_DEFAULT_NAME,
                                                            &local_error);

                if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) {
                        return TRUE;
                } else if (local_error) {
                        g_propagate_error (error, g_steal_pointer (&local_error));
                        return FALSE;
                }
        }

        return write_locked_setting_to_db (db_name, schema, key, value, error);
}

static gboolean
write_gdm_auth_setting_to_db (GdmAuthType   auth_type,
                              gboolean      value,
                              GError      **error)
{
        if (!ensure_gdm_config_dconf_profile (error))
                return FALSE;

        return write_system_setting_to_db (GDM_CONFIG_DCONF_DB_NAME,
                                           LOGIN_SCHEMA,
                                           auth_type_to_option_key (auth_type),
                                           g_variant_new_boolean (value),
                                           error);
}

static gboolean
set_gdm_auth_setting_lock (GdmAuthType   config_command,
                           GError      **error)
{
        return set_dconf_setting_lock (GDM_CONFIG_DCONF_DB_NAME,
                                       GDM_CONFIG_DCONF_LOCKS_NAME,
                                       LOGIN_SCHEMA,
                                       auth_type_to_option_key (config_command),
                                       error);
}

static gboolean
write_locked_gdm_settings_to_db (GdmAuthType   auth_type,
                                 gboolean      value,
                                 GError      **error)
{
        if (!write_gdm_auth_setting_to_db (auth_type, value, error))
                return FALSE;

        if (!set_gdm_auth_setting_lock (auth_type, error))
                return FALSE;

        if (!update_dconf (error))
                return FALSE;

        return TRUE;
}

static GPtrArray *
create_cmd_args (int argc, gchar **argv, GdmConfigCommand command)
{
        GPtrArray *args;
        const char *command_str;
        int i;

        args = g_ptr_array_new_full (argc, g_free);
        command_str = config_command_to_string (command);
        g_ptr_array_add (args, g_strdup_printf ("%s %s", g_get_prgname (), command_str));

        for (i = 2; i < argc; i++) {
                g_ptr_array_add (args, g_strdup (argv[i]));
        }

        return g_steal_pointer (&args);
}

static gboolean
have_pam_module (GdmAuthType auth_type)
{
        g_autofree char *pam_profile = NULL;

        pam_profile = g_strdup_printf(PAM_PROFILES_DIR "/gdm-%s",
                auth_type_to_string(auth_type));

        g_debug ("Checking for PAM profile “%s” existance", pam_profile);

        return g_file_test (pam_profile, G_FILE_TEST_IS_REGULAR);
}

static gboolean
handle_config_command_options (GdmConfigCommand          config_command,
                               GdmConfigCommandHandler  *command_handler,
                               GError                  **error)
{
        if (!command_handler->options_handler_func)
                return TRUE;

        return command_handler->options_handler_func (config_command, error);
}

static gboolean
handle_config_command (GdmConfigCommand          config_command,
                       GdmConfigCommandHandler  *command_handler,
                       GError                  **error)
{
        g_autoptr(GError) local_error = NULL;
        g_autofree char *output = NULL;
        GdmAuthType auth_type;

        auth_type = config_command_to_auth_type (config_command);
        g_assert (auth_type != AUTH_NONE);

        if (!have_pam_module (auth_type)) {
                g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED_HANDLED,
                                     get_pam_module_missing_error (auth_type));
                return FALSE;
        }

        if (try_run_distro_hook (config_command, NULL, &output, &local_error)) {
                g_print ("%s", output);
                return handle_config_command_options (config_command, command_handler, error);
        }

        if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
                g_debug ("Hook completed, stopping execution: %s",
                         local_error->message);

                if (!handle_config_command_options (config_command, command_handler, error))
                        return FALSE;

                /* If the option handle didn't fail, we still need to propagate
                 * the cancellation, or we will continue with default handler */
                g_propagate_error (error, g_steal_pointer (&local_error));
                return FALSE;
        }

        if (check_distro_hook_is_required (config_command)) {
                g_propagate_prefixed_error (error, g_steal_pointer (&local_error),
                        _("Failed to run a required distro hook: "));
                return FALSE;
        }

        g_debug ("Failed to run optional distro hook: %s", local_error->message);
        return TRUE;
}

static gboolean
print_usage (int                   argc,
             char                 *argv[],
             GdmConfigCommand      config_command)
{
        g_autoptr(GOptionContext) ctx = NULL;
        g_autofree char *usage = NULL;

        ctx = g_option_context_new (_("COMMAND"));
        g_option_context_set_help_enabled (ctx, FALSE);

        g_option_context_parse (ctx, &argc, &argv, NULL);
        usage = g_strdup_printf (_("Commands:\n"
                "  help         Shows this information\n"
                "  password     Configure the password authentication\n"
                "  fingerprint  Configure the fingerprint authentication\n"
                "  smartcard    Configure the smartcard authentication\n"
                "  reset        Resets the default configuration\n"
                "  show         Shows the current configuration\n"
                "\n"
                "Use “%s COMMAND --help” to get help on each command.\n"),
                        g_get_prgname ());

        g_option_context_set_description (ctx, usage);

        usage = g_option_context_get_help (ctx, FALSE, NULL);
        if (config_command == COMMAND_HELP) {
                g_print ("%s", usage);
                return TRUE;
        }

        g_printerr ("%s", usage);
        return FALSE;
}

static gboolean
handle_command (int   argc,
                char *argv[])
{
        GdmConfigCommand config_command;
        GdmAuthType auth_type;
        g_autoptr(GOptionContext) option_cx = NULL;
        g_autoptr(GdmConfigCommandHandler) command_handler = NULL;
        g_autoptr(GOptionGroup) group = NULL;
        g_autoptr(GPtrArray) cmd_args = NULL;
        g_autoptr(GPtrArray) args_copy = NULL;
        g_autoptr(GError) error = NULL;
        g_autofree OptionData *options_data = NULL;
        const char *title;
        const char *group_title;

        config_command = parse_config_command (argv[1]);
        command_handler = config_command_get_handler (config_command);

        if (!command_handler) {
                return print_usage (argc, argv, config_command);
        }

        g_assert (command_handler);
        title = get_command_title (config_command);
        group_title = get_command_group_title (config_command);

        cmd_args = create_cmd_args (argc, argv, config_command);

        options_data = g_new0 (OptionData, 1);
        options_data->config_command = config_command;
        options_data->args = cmd_args;

        option_cx = g_option_context_new (NULL);
        g_option_context_set_help_enabled (option_cx, FALSE);
        g_option_context_set_summary (option_cx, title);

        group = g_option_group_new (config_command_to_string (config_command),
                                    group_title,
                                    N_("Command options"),
                                    g_steal_pointer (&options_data), g_free);
        g_option_group_set_translation_domain (group, GETTEXT_PACKAGE);
        g_option_group_add_entries (group, generic_entries);
        config_command_handler_add_to_group (command_handler, group);
        g_option_context_add_group (option_cx, g_steal_pointer (&group));

        /* We need to make a further container copy here as the parsing
         * may remove some elements from the array, leading us to a leak
         */
        args_copy = g_ptr_array_copy (cmd_args, NULL, NULL);
        g_ptr_array_set_free_func (args_copy, NULL);
        argc = args_copy->len;
        argv = (char **) args_copy->pdata;

        if (!g_option_context_parse (option_cx, &argc, &argv, &error) ||
            opt_cmd_help || /* help requested */
            argc > 1 /* More than one parameter has not been parsed */) {
                g_autofree char *help = NULL;
                help = g_option_context_get_help (option_cx, FALSE, NULL);

                if (error && argc > 1) {
                        g_printerr ("%sError: %s\n", help, error->message);
                } else if (opt_cmd_help) {
                        g_print ("%s", help);
                        return TRUE;
                } else {
                        g_printerr ("%s", help);
                }

                return FALSE;
        }

        if (geteuid () != 0 && !g_getenv ("UNDER_JHBUILD")) {
                /* TRANSLATORS: You need to be root to use PROGRAM-NAME “command” command */
                g_critical (_("You need to be root to use %s “%s” command"),
                              g_get_prgname(), config_command_to_string (config_command));
                return FALSE;
        }

        if (opt_debug) {
                g_setenv ("G_MESSAGES_DEBUG", G_LOG_DOMAIN, FALSE);
        }

        auth_type = config_command_to_auth_type (config_command);
        if (auth_type != AUTH_NONE &&
            !handle_config_command (config_command, command_handler, &error)) {
                if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
                        return TRUE;

                g_critical ("Failed to handle “%s” command: %s",
                            config_command_to_string (config_command),
                            error->message);
                return FALSE;
        }

        if (!command_handler->handler_func (config_command, &error)) {
                if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
                        return FALSE;

                g_critical ("Failed to run “%s” command: %s",
                            config_command_to_string (config_command),
                            error->message);
                return FALSE;
        }

        if (opt_verbose && config_command != COMMAND_SHOW) {
                handle_show (COMMAND_SHOW, NULL);
        }

        return TRUE;
}

static gboolean
handle_toggle_command (GdmConfigCommand   config_command,
                       GError           **error)
{
        g_autoptr(GError) local_error = NULL;
        GdmAuthType auth_type = config_command_to_auth_type (config_command);
        gboolean enabled = (get_requested_action () == ACTION_ENABLED);

        g_assert (auth_type != AUTH_NONE);

        if (!write_locked_gdm_settings_to_db (auth_type, enabled, &local_error)) {
                g_propagate_prefixed_error (error, g_steal_pointer (&local_error),
                                            _("Failed to set %s setting: "),
                                            config_command_to_string (config_command));
                return FALSE;
        }

        return TRUE;
}

static gboolean
handle_password (GdmConfigCommand   config_command,
                 GError           **error)
{
        return handle_toggle_command (config_command, error);
}

static gboolean
handle_fingerprint (GdmConfigCommand   config_command,
                    GError           **error)
{
        return handle_toggle_command (config_command, error);
}

static gboolean
toggle_smartcard_settings (const char  *key,
                           GVariant    *value,
                           GError     **error)
{
        g_autoptr(GError) local_error = NULL;

        /* While this is not strictly needed we also do this for the gdm
         * database so that we keep the values in sync for both profiles */
        if (!write_locked_setting_to_db (GDM_CONFIG_DCONF_DB_NAME,
                                         GSD_SC_SCHEMA,
                                         key,
                                         value ? g_variant_ref_sink (value) : NULL,
                                         &local_error)) {
                g_debug ("Failed to write setting to %s database: %s",
                         GDM_CONFIG_DCONF_DB_NAME,
                         local_error->message);
        }

        return write_locked_system_settings_to_db (GSD_SC_SCHEMA,
                                                   key,
                                                   value,
                                                   error);
}

static gboolean
handle_smartcard_options (GdmConfigCommand   config_command,
                          GError           **error)
{
        g_autoptr(GError) local_error = NULL;
        GVariant *value;

        if (!opt_removal_action)
                return TRUE;

        if (!set_distro_hook_config (config_command, GSD_SC_REMOVAL_ACTION_KEY,
                                     opt_removal_action, &local_error)) {
                if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
                        return TRUE;

                if (!g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) {
                        g_propagate_error (error, g_steal_pointer (&local_error));
                        return FALSE;
                }

                g_clear_error (&local_error);
        }

        if (g_str_equal (opt_removal_action, "user-defined")) {
                value = NULL;
        } else {
                value = g_variant_new_string (opt_removal_action);
        }

        return toggle_smartcard_settings (GSD_SC_REMOVAL_ACTION_KEY, value, error);
}

static gboolean
handle_smartcard (GdmConfigCommand   config_command,
                  GError           **error)
{
        g_autoptr(GError) local_error = NULL;
        GdmAuthAction action;

        action = get_requested_action ();

        if (action != ACTION_UNSET) {
                gboolean enabled = (action == ACTION_REQUIRED || action == ACTION_ENABLED);
                if (!write_locked_gdm_settings_to_db (AUTH_SMARTCARD, enabled, &local_error)) {
                        g_propagate_prefixed_error (error, g_steal_pointer (&local_error),
                                                    _("Failed to set smartcard setting"));
                        return FALSE;
                }

                if (!write_locked_gdm_settings_to_db (AUTH_PASSWORD, action != ACTION_REQUIRED, &local_error)) {
                        g_propagate_prefixed_error (error, g_steal_pointer (&local_error),
                                                    _("Failed to set password setting"));
                        return FALSE;
                }
        }

        if (!handle_smartcard_options (config_command, error))
                return FALSE;

        return TRUE;
}

static const char *
get_boolean_string (gboolean val)
{
        return val ? N_("Enabled") : N_("Disabled");
}

static const char *
get_gdm_setting_state (GdmAuthType auth_type) {
        g_autoptr(GSettings) login_settings = NULL;
        gboolean enabled;

        if (!have_pam_module (auth_type))
                return N_("Not supported");

        login_settings = g_settings_new (LOGIN_SCHEMA);
        enabled = g_settings_get_boolean (login_settings,
                                          auth_type_to_option_key (auth_type));

        if (enabled) {
                g_autofree char *output = NULL;

                output = get_distro_hook_config (auth_type, NULL, NULL);

                if (g_strcmp0 (output, "enabled") == 0) {
                        return get_boolean_string (TRUE);
                } else if (g_strcmp0 (output, "disabled") == 0) {
                        return get_boolean_string (FALSE);
                } else if (g_strcmp0 (output, "required") == 0) {
                        return N_("Required");
                }
        }

        return get_boolean_string (enabled);
}

static char *
get_smartcard_option (const char *option_key) {
        g_autoptr(GSettings) smartcard_settings = NULL;
        g_autofree char *option_value = NULL;

        option_value = get_distro_hook_config (AUTH_SMARTCARD, option_key, NULL);
        if (option_value && smartcard_option_is_valid (option_key, option_value)) {
                return g_steal_pointer (&option_value);
        }

        smartcard_settings = g_settings_new (GSD_SC_SCHEMA);

        if (g_settings_is_writable (smartcard_settings, option_key)) {
                g_autoptr(GVariant) defalut_value = NULL;

                defalut_value = g_settings_get_default_value (smartcard_settings,
                                                              option_key);
                return g_strdup_printf ("user-defined (default is %s)",
                                        g_variant_get_string (defalut_value, NULL));
        }

        return g_settings_get_string (smartcard_settings, option_key);
}

static gboolean
handle_show (GdmConfigCommand   config_command,
             GError           **error)
{
        g_autofree char *removal_setting = NULL;
        g_autoptr(GError) local_error = NULL;
        g_autofree char *default_dconf_profile = NULL;

        /* This is not completely correct when getting the smartcard removal
         * option, as that's a per-user / system setting until we don't add the
         * system db to the gdm ones, but we need to set it now otherwise the
         * gsettings backend won't give us proper results for auth parameters,
         * as the backend isn't really ever released on unref, however the
         * alternative may be just using DConfClient API directly or just
         * calling gsettings tool without DCONF_PROFILE set or doing a fork
         * here, but that's probably too much for this tool */
        default_dconf_profile = g_strdup (g_getenv ("DCONF_PROFILE"));
        g_setenv ("DCONF_PROFILE", GDM_DCONF_PROFILE, TRUE);

        removal_setting = get_smartcard_option (GSD_SC_REMOVAL_ACTION_KEY);

        /* While this may be not super-needed it ensures that we act as if we
         * were gdm itself */
        if (!g_getenv ("UNDER_JHBUILD") && !switch_to_gdm_user (&local_error)) {
                g_critical ("Impossible to switch to GDM user %s: %s",
                            GDM_USERNAME, local_error->message);
                g_propagate_error (error, g_steal_pointer (&local_error));
                return FALSE;
        }

        g_print(_("GDM Authorization configuration\n"
                  "\n"
                  "  Password authentication: %s\n"
                  "  Fingerprint authentication: %s\n"
                  "  Smart Card authentication: %s\n"
                  "  Smart Card removal action: %s\n"),
                  get_gdm_setting_state (AUTH_PASSWORD),
                  get_gdm_setting_state (AUTH_FINGERPRINT),
                  get_gdm_setting_state (AUTH_SMARTCARD),
                  removal_setting
                );

        if (default_dconf_profile)
                g_setenv ("DCONF_PROFILE", default_dconf_profile, TRUE);

        return TRUE;
}

static gboolean
handle_reset (GdmConfigCommand   config_command,
              GError           **error)
{
        g_autoptr(GError) local_error = NULL;
        g_autoptr(GFile) profile_file = NULL;
        g_autofree char *system_db_name = NULL;
        int i;

        if (get_requested_action () != ACTION_REQUIRED) {
                const char *reply_Y = C_("Interactive question", "Y");
                const char *reply_y = C_("Interactive question", "y");
                const char *reply_N = C_("Interactive question", "N");
                const char *reply_n = C_("Interactive question", "n");
                char buffer[50];

                while (TRUE) {
                        g_print (C_("Interactive question", "Do you want to continue? [Y/n]? "));
                        if (!fgets(buffer, sizeof (buffer), stdin))
                                continue;

                        g_strstrip (buffer);

                        if (g_str_equal (buffer, reply_Y))
                                break;
                        if (g_str_equal (buffer, reply_y))
                                break;
                        if (g_str_equal (buffer, reply_N))
                                break;
                        if (g_str_equal (buffer, reply_n))
                                break;
                }

                if (!g_str_equal (buffer, reply_y) && !g_str_equal (buffer, reply_Y)) {
                        g_set_error (error, G_IO_ERROR, G_IO_ERROR_CANCELLED,
                                     _("User cancelled the request"));
                        return FALSE;
                }
        }

        if (!try_run_distro_hook (COMMAND_RESET, NULL, NULL, &local_error)) {
                if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
                        return TRUE;

                if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) {
                        g_debug ("Ignored reset distro hook: %s", local_error->message);
                        g_clear_error (&local_error);
                } else {
                        g_propagate_error (error, g_steal_pointer (&local_error));
                        return FALSE;
                }
        }

        for (i = 0; i < AUTH_NONE; i++) {
                const char *option_key;

                if (config_command_to_auth_type (i) == AUTH_NONE)
                        continue;

                option_key = auth_type_to_option_key (i);
                if (!write_system_setting_to_db (GDM_CONFIG_DCONF_DB_NAME,
                                                 LOGIN_SCHEMA,
                                                 option_key,
                                                 NULL,
                                                 &local_error)) {
                        g_propagate_prefixed_error (error, g_steal_pointer (&local_error),
                                                    _("Failed to reset %s setting: "),
                                                    config_command_to_string (config_command));
                        return FALSE;
                }

                if (!unset_dconf_setting_lock (GDM_CONFIG_DCONF_DB_NAME,
                                               GDM_CONFIG_DCONF_LOCKS_NAME,
                                               LOGIN_SCHEMA, option_key, error))
                        return FALSE;
        }

        if (!toggle_smartcard_settings (GSD_SC_REMOVAL_ACTION_KEY, NULL, error))
                return FALSE;

        system_db_name = get_system_dconf_profile_db_name (DCONF_SYSTEM_DB_DEFAULT_NAME, NULL);
        if (g_strcmp0 (system_db_name, DCONF_SYSTEM_DB_DEFAULT_NAME) == 0) {
                g_autofree char *profile_file = NULL;
                g_autoptr(GPtrArray) profile_contents = NULL;

                profile_file = get_dconf_system_profile_file (error);
                profile_contents = get_system_dconf_profile_contents (NULL);

                if (profile_contents) {
                        g_autofree char *full_line = NULL;
                        guint index;

                        full_line = g_strconcat (DCONF_SYSTEM_DB_PREFIX, system_db_name, NULL);
                        while (g_ptr_array_find_with_equal_func (profile_contents, full_line,
                                                                 g_str_equal, &index)) {
                                g_debug ("Removing %s db from profile %s",
                                         system_db_name, profile_file);
                                g_ptr_array_remove_index (profile_contents, index);
                                write_array_to_file (profile_contents, profile_file, NULL);
                        }
                }
        }

        if (system_db_name) {
                if (!remove_dconf_setting_lock (system_db_name,
                                                GDM_CONFIG_DCONF_LOCKS_NAME,
                                                &local_error)) {
                        g_warning ("Failed to remove lock file for DB %s: %s",
                                   system_db_name,
                                   local_error->message);
                        g_clear_error (&local_error);
                }

                if (!remove_dconf_setting_key_file (system_db_name,
                                                    GDM_CONFIG_DCONF_OVERRIDE_NAME,
                                                    &local_error)) {
                        g_warning ("Failed to remove setting key file for DB %s: %s",
                                   system_db_name,
                                   local_error->message);
                        g_clear_error (&local_error);
                }
        }

        if (!remove_dconf_setting_lock (GDM_CONFIG_DCONF_DB_NAME,
                                        GDM_CONFIG_DCONF_LOCKS_NAME,
                                        &local_error)) {
                g_warning ("Failed to remove lock file for DB %s: %s",
                           GDM_CONFIG_DCONF_DB_NAME,
                           local_error->message);
                g_clear_error (&local_error);
        }

        if (!remove_dconf_setting_key_file (GDM_CONFIG_DCONF_DB_NAME,
                                            GDM_CONFIG_DCONF_OVERRIDE_NAME,
                                            &local_error)) {
                g_warning ("Failed to remove setting key file for DB %s: %s",
                           GDM_CONFIG_DCONF_DB_NAME,
                           local_error->message);
                g_clear_error (&local_error);
        }

        /* finally remove the profile/gdm */
        profile_file = g_file_new_for_path (GDM_CONFIG_DCONF_PROFILE);
        if (!g_file_delete (profile_file, NULL, &local_error)) {
                g_warning ("Failed to remove gdm-auth-config profile override: %s",
                           local_error->message);
                g_clear_error (&local_error);
        }

        if (!update_dconf (error))
                return FALSE;

        return TRUE;
}

int
main (int argc, char *argv[])
{
        g_autofree char *program_name = NULL;

        setlocale (LC_ALL, "");
        textdomain (GETTEXT_PACKAGE);

        program_name = g_path_get_basename (argv[0]);
        g_set_prgname (program_name);

        if (argc < 2) {
                print_usage (argc, argv, COMMAND_UNKNOWN);
                return EXIT_FAILURE;
        }

        if (!handle_command (argc, argv))
                return EXIT_FAILURE;

        return EXIT_SUCCESS;
}
