/*
 * Copyright (C) 2019 Purism SPC
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 *
 * Author: Guido Günther <agx@sigxcpu.org>
 */

#define G_LOG_DOMAIN "phosh-polkit-auth-agent"

#include "polkit-auth-agent.h"
#include "polkit-auth-prompt.h"
#include "shell.h"

#include <sys/types.h>
#include <pwd.h>

#ifdef PHOSH_POLKIT_AUTH_DEBUG
#define auth_debug(...) g_debug(__VA_ARGS__)
#else
static void auth_debug(const char *str, ...) {}
#endif

/**
 * SECTION:polkit-auth-agent
 * @short_description: PolicyKit Authentication Agent
 * @Title: PhoshPolkitAuthAgent
 *
 * The #PhoshPolkitAuthAgent is responsible for handling policy kit
 * interaction so the shell can work as a authentication agent.
 */

typedef struct {
  /* not holding ref */
  PhoshPolkitAuthAgent *agent;
  GCancellable *cancellable;
  gulong handler_id;

  /* copies */
  char           *action_id;
  char           *message;
  char           *icon_name;
  PolkitDetails  *details;
  char           *cookie;
  GList          *identities;

  GTask *simple;
} AuthRequest;

struct _PhoshPolkitAuthAgent
{
  PolkitAgentListener parent;

  GList *scheduled_requests;
  AuthRequest *current_request;
  PhoshPolkitAuthPrompt *current_prompt;

  gpointer handle;
};
G_DEFINE_TYPE (PhoshPolkitAuthAgent, phosh_polkit_auth_agent, POLKIT_AGENT_TYPE_LISTENER);

static void auth_request_complete (AuthRequest *request, gboolean dismissed);

static gboolean
agent_register (PhoshPolkitAuthAgent *self)
{
  g_autoptr (GError) err = NULL;
  g_autoptr (PolkitSubject) subject;

  subject = polkit_unix_session_new_for_process_sync (getpid (),
                                                      NULL, /* GCancellable */
                                                      &err);
  if (subject == NULL) {
    if (g_str_has_prefix (err->message, "No session for pid"))
      g_message ("PolKit failed to properly get our session: %s", err->message);
    else
      g_warning ("PolKit failed to properly get our session: %s", err->message);
    return FALSE;
  }

  /* FIXME: this blocks so we should do it async */
  self->handle = polkit_agent_listener_register (POLKIT_AGENT_LISTENER (self),
                                                 POLKIT_AGENT_REGISTER_FLAGS_NONE,
                                                 subject,
                                                 NULL, /* use default object path */
                                                 NULL, /* GCancellable */
                                                 &err);

  if (!self->handle) {
    g_warning ("Auth agent failed to register: %s", err->message);
    return FALSE;
  }

  g_debug ("Polkit auth agent registered");
  return TRUE;
}


static void
auth_request_free (AuthRequest *request)
{
  g_free (request->action_id);
  g_free (request->message);
  g_free (request->icon_name);
  g_object_unref (request->details);
  g_free (request->cookie);
  g_list_foreach (request->identities, (GFunc) g_object_unref, NULL);
  g_list_free (request->identities);
  g_object_unref (request->simple);
  g_free (request);
}

static void
close_prompt (PhoshPolkitAuthAgent *self)
{
  if (self->current_prompt) {
    gtk_widget_destroy (GTK_WIDGET (self->current_prompt));
    self->current_prompt = NULL;
  }
}


static void
on_prompt_done (PhoshPolkitAuthPrompt *prompt, gboolean cancelled, AuthRequest *request)
{
  g_return_if_fail (PHOSH_IS_POLKIT_AUTH_PROMPT (prompt));

  close_prompt (request->agent);
  auth_request_complete (request, cancelled);
}


static void
auth_request_initiate (AuthRequest *request)
{
  g_auto(GStrv) user_names;
  GPtrArray *p;
  GList *l;

  p = g_ptr_array_new ();
  for (l = request->identities; l != NULL; l = l->next) {
      if (POLKIT_IS_UNIX_USER (l->data)) {
          PolkitUnixUser *user = POLKIT_UNIX_USER (l->data);
          int uid;
          char buf[4096];
          struct passwd pwd;
          struct passwd *ppwd;
          int ret;

          uid = polkit_unix_user_get_uid (user);
          ret = getpwuid_r (uid, &pwd, buf, sizeof (buf), &ppwd);
          if (!ret) {
            if (!g_utf8_validate (pwd.pw_name, -1, NULL))
              g_warning ("Invalid UTF-8 in username for uid %d. Skipping", uid);
            else
              g_ptr_array_add (p, g_strdup (pwd.pw_name));
          } else {
            g_warning ("Error looking up user name for uid %d: %d", uid, ret);
          }
      } else {
        g_warning ("Unsupporting identity of GType %s", g_type_name (G_TYPE_FROM_INSTANCE (l->data)));
      }
  }

  g_ptr_array_add (p, NULL);
  user_names = (char **) g_ptr_array_free (p, FALSE);

  g_debug("New prompt for %s", request->message);
  /* We must not issue a new prompt when there's one already */
  g_return_if_fail (!request->agent->current_prompt);
  request->agent->current_prompt = PHOSH_POLKIT_AUTH_PROMPT (
    phosh_polkit_auth_prompt_new (
      request->action_id,
      request->message,
      request->icon_name,
      request->cookie,
      user_names));

  g_signal_connect (request->agent->current_prompt,
                    "done",
                    G_CALLBACK (on_prompt_done),
                    request);

  /* Show widget when not locked and keep that in sync */
  g_object_bind_property (phosh_shell_get_default (), "locked",
                          request->agent->current_prompt, "visible",
                          G_BINDING_SYNC_CREATE | G_BINDING_INVERT_BOOLEAN);
}

static void
maybe_process_next_request (PhoshPolkitAuthAgent *self)
{
  auth_debug ("cur=%p len(scheduled)=%d",
              self->current_request,
              g_list_length (self->scheduled_requests));

  if (self->current_request == NULL && self->scheduled_requests != NULL) {
    AuthRequest *request;

    request = self->scheduled_requests->data;

    self->current_request = request;
    self->scheduled_requests = g_list_remove (self->scheduled_requests, request);

    auth_debug ("initiating %s cookie %s", request->action_id, request->cookie);
    auth_request_initiate (request);
  }
}


static void
auth_request_complete (AuthRequest *request, gboolean dismissed)
{
  PhoshPolkitAuthAgent *self = request->agent;
  gboolean is_current = self->current_request == request;

  auth_debug ("completing %s %s cookie %s", is_current ? "current" : "scheduled",
              request->action_id, request->cookie);

  if (!is_current)
    self->scheduled_requests = g_list_remove (self->scheduled_requests, request);
  g_cancellable_disconnect (request->cancellable, request->handler_id);

  if (dismissed) {
    g_task_return_new_error (request->simple,
                             POLKIT_ERROR,
                             POLKIT_ERROR_CANCELLED,
                             _("Authentication dialog was dismissed by the user"));
  } else {
    g_task_return_boolean (request->simple, TRUE);
  }

  auth_request_free (request);

  if (is_current) {
    self->current_request = NULL;
    maybe_process_next_request (self);
  }
}

static gboolean
handle_cancelled_in_idle (gpointer user_data)
{
  AuthRequest *request = user_data;

  auth_debug ("cancelled %s cookie %s", request->action_id, request->cookie);
  if (request == request->agent->current_request)
    close_prompt (request->agent);

  auth_request_complete (request, FALSE);
  return FALSE;
}

/*
 * on_request_cancelled:
 *
 * This happens when the application requesting authentication
 * is closed.
 */
static void
on_request_cancelled (GCancellable *cancellable,
                      gpointer      user_data)
{
  AuthRequest *request = user_data;
  guint id;

  /*
   * post-pone to idle to handle GCancellable deadlock in
   * https://bugzilla.gnome.org/show_bug.cgi?id=642968
   * https://gitlab.gnome.org/GNOME/glib/issues/740
   */
  id = g_idle_add (handle_cancelled_in_idle, request);
  g_source_set_name_by_id (id, "[phosh] handle_cancelled_in_idle");
}


static void
initiate_authentication (PolkitAgentListener  *listener,
                         const char           *action_id,
                         const char           *message,
                         const char           *icon_name,
                         PolkitDetails        *details,
                         const char           *cookie,
                         GList                *identities,
                         GCancellable         *cancellable,
                         GAsyncReadyCallback   callback,
                         gpointer              user_data)
{
  PhoshPolkitAuthAgent *self = PHOSH_POLKIT_AUTH_AGENT (listener);
  AuthRequest *request;

  request = g_new0 (AuthRequest, 1);
  request->agent = self;
  request->action_id = g_strdup (action_id);
  request->message = g_strdup (message);
  request->icon_name = g_strdup (icon_name);
  request->details = g_object_ref (details);
  request->cookie = g_strdup (cookie);
  request->identities = g_list_copy (identities);
  g_list_foreach (request->identities, (GFunc) g_object_ref, NULL);
  request->simple = g_task_new (listener, NULL, callback, user_data);
  request->cancellable = cancellable;
  request->handler_id = g_cancellable_connect (request->cancellable,
                                               G_CALLBACK (on_request_cancelled),
                                               request,
                                               NULL); /* GDestroyNotify for request */

  auth_debug ("scheduling %s cookie %s", request->action_id, request->cookie);
  self->scheduled_requests = g_list_append (self->scheduled_requests, request);

  maybe_process_next_request (self);
}

static gboolean
initiate_authentication_finish (PolkitAgentListener  *listener,
                                GAsyncResult         *res,
                                GError              **error)
{
  return g_task_propagate_boolean (G_TASK (res), error);
}


static void
phosh_polkit_auth_agent_constructed (GObject *object)
{
  PhoshPolkitAuthAgent *self = PHOSH_POLKIT_AUTH_AGENT (object);

  G_OBJECT_CLASS (phosh_polkit_auth_agent_parent_class)->constructed (object);

  if (agent_register (self))
    g_return_if_fail (self->handle);
}


static void
auth_request_dismiss (AuthRequest *request)
{
  auth_request_complete (request, TRUE);
}


static void
phosh_polkit_auth_agent_unregister (PhoshPolkitAuthAgent *self)
{
  if (self->scheduled_requests != NULL) {
    g_list_foreach (self->scheduled_requests, (GFunc)auth_request_dismiss, NULL);
    self->scheduled_requests = NULL;
  }

  if (self->current_request != NULL)
    auth_request_dismiss (self->current_request);

  polkit_agent_listener_unregister (self->handle);
  self->handle = NULL;
}


static void
phosh_polkit_auth_agent_dispose (GObject *object)
{
  PhoshPolkitAuthAgent *self = PHOSH_POLKIT_AUTH_AGENT (object);

  if (self->handle)
    phosh_polkit_auth_agent_unregister (self);

  G_OBJECT_CLASS (phosh_polkit_auth_agent_parent_class)->dispose (object);
}


static void
phosh_polkit_auth_agent_class_init (PhoshPolkitAuthAgentClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);
  PolkitAgentListenerClass *listener_class;

  object_class->constructed = phosh_polkit_auth_agent_constructed;
  object_class->dispose = phosh_polkit_auth_agent_dispose;

  listener_class = POLKIT_AGENT_LISTENER_CLASS (klass);
  listener_class->initiate_authentication = initiate_authentication;
  listener_class->initiate_authentication_finish = initiate_authentication_finish;
}


static void
phosh_polkit_auth_agent_init (PhoshPolkitAuthAgent *self)
{
}


PhoshPolkitAuthAgent *
phosh_polkit_auth_agent_new (void)
{
  return g_object_new (PHOSH_TYPE_POLKIT_AUTH_AGENT, NULL);
}
