/* pinentry-gnome3.c
 * Copyright (C) 2015 g10 Code GmbH
 *
 * pinentry-gnome-3 is a pinentry application for GNOME 3.  It tries
 * to follow the Gnome Human Interface Guide as close as possible.
 *
 * 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 <https://www.gnu.org/licenses/>.
 * SPDX-License-Identifier: GPL-2.0+
 */

#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#ifdef GCR_API_4
#include <gcr/gcr.h>
#else
#include <gcr/gcr-base.h>
#endif

#include <string.h>
#include <stdlib.h>

#include <assuan.h>

#include "pinentry.h"

#ifdef FALLBACK_CURSES
#include "pinentry-curses.h"
#endif


#define PGMNAME "pinentry-gnome3"

#ifndef VERSION
#  define VERSION
#endif


struct pe_gnome3_run_s {
  pinentry_t pinentry;
  GcrPrompt *prompt;
  GMainLoop *main_loop;
  int ret;
  guint timeout_id;
  int timed_out;
};

static void pe_gcr_prompt_password_done (GObject *source_object,
                                         GAsyncResult *res, gpointer user_data);

static void pe_gcr_prompt_confirm_done (GObject *source_object,
                                        GAsyncResult *res, gpointer user_data);
static gboolean pe_gcr_timeout_done (gpointer user_data);



static gchar *
pinentry_utf8_validate (gchar *text)
{
  gchar *result;

  if (!text)
    return NULL;

  if (g_utf8_validate (text, -1, NULL))
    return g_strdup (text);

  /* Failure: Assume that it was encoded in the current locale and
     convert it to utf-8.  */
  result = g_locale_to_utf8 (text, -1, NULL, NULL, NULL);
  if (!result)
    {
      gchar *p;

      result = p = g_strdup (text);
      while (!g_utf8_validate (p, -1, (const gchar **) &p))
	*p = '?';
    }
  return result;
}


static void
_propagate_g_error_to_pinentry (pinentry_t pe, GError *error,
                                gpg_err_code_t code, const char *loc)
{
  char *t;

  /* We can't return the result of g_strdup_printf directly, because
   * this needs to be g_free'd, but the users of PE (e.g.,
   * pinentry_reset in pinentry/pinentry.c) use free.  */
  t = g_strdup_printf ("%d: %s", error->code, error->message);
  if (t)
    {
      /* If strdup fails, then PE->SPECIFIC_ERR_INFO will be NULL,
       * which is exactly what we want if strdup fails.  So, there is
       * no need to check for failure.  */
      pe->specific_err_info = strdup (t);
      g_free (t);
    }
  else
    {
      pe->specific_err_info = NULL;
    }

  pe->specific_err = gpg_error (code);
  pe->specific_err_loc = loc;
}

static GcrPrompt *
create_prompt (pinentry_t pe, int confirm)
{
  GcrPrompt *prompt;
  GError *error = NULL;
  char *msg, *p;
  char window_id[32];

  /* Create the prompt.  */
  prompt = GCR_PROMPT (gcr_system_prompt_open (pe->timeout ? pe->timeout : -1, NULL, &error));
  if (! prompt)
    {
      /* this means the timeout elapsed, but no prompt was ever shown. */
      if (error->code == GCR_SYSTEM_PROMPT_IN_PROGRESS)
        {
          fprintf (stderr, "Timeout: the Gcr system prompter was already in use.\n");
          pe->specific_err_info = strdup ("Timeout: the Gcr system prompter was already in use.");
          /* not using GPG_ERR_TIMEOUT here because the user never saw
             a prompt: */
          pe->specific_err = gpg_error (GPG_ERR_PIN_ENTRY);
        }
      else
        {
          fprintf (stderr, "couldn't create prompt for gnupg passphrase: %s\n",
                   error->message);
          _propagate_g_error_to_pinentry (pe, error, GPG_ERR_CONFIGURATION,
                                          "gcr_system_prompt_open");
        }
      g_error_free (error);
      return NULL;
    }

  /* Set the messages for the various buttons, etc.  */
  p = pinentry_get_title (pe);
  if (p)
    {
      msg = pinentry_utf8_validate (p);
      if (msg)
        {
          gcr_prompt_set_title (prompt, msg);
          g_free (msg);
        }
      free (p);
    }

  if (pe->description)
    {
      msg = pinentry_utf8_validate (pe->description);
      gcr_prompt_set_description (prompt, msg);
      g_free (msg);
    }

  /* An error occurred during the last prompt.  */
  if (pe->error)
    {
      msg = pinentry_utf8_validate (pe->error);
      gcr_prompt_set_warning (prompt, msg);
      g_free (msg);
    }

  if (! pe->prompt && confirm)
    gcr_prompt_set_message (prompt, "Message");
  else if (! pe->prompt && ! confirm)
    gcr_prompt_set_message (prompt, "Enter Passphrase");
  else
    {
      msg = pinentry_utf8_validate (pe->prompt);
      gcr_prompt_set_message (prompt, msg);
      g_free (msg);
    }

  if (! confirm)
    gcr_prompt_set_password_new (prompt, !!pe->repeat_passphrase);

  if (pe->ok || pe->default_ok)
    {
      msg = pinentry_utf8_validate (pe->ok ?: pe->default_ok);
      gcr_prompt_set_continue_label (prompt, msg);
      g_free (msg);
    }
  /* XXX: Disable this button if pe->one_button is set.  */
  if (pe->cancel || pe->default_cancel)
    {
      msg = pinentry_utf8_validate (pe->cancel ?: pe->default_cancel);
      gcr_prompt_set_cancel_label (prompt, msg);
      g_free (msg);
    }

  if (confirm && pe->notok)
    {
      /* XXX: Add support for the third option.  */
    }

  /* gcr expects a string; we have a int.  see gcr's
     ui/frob-system-prompt.c for example conversion using %lu */
  snprintf (window_id, sizeof (window_id), "%lu",
            (long unsigned int)pe->parent_wid);
  gcr_prompt_set_caller_window (prompt, window_id);

#ifdef HAVE_LIBSECRET
  if (! confirm && pe->allow_external_password_cache && pe->keyinfo)
    {
      if (pe->default_pwmngr)
	{
	  msg = pinentry_utf8_validate (pe->default_pwmngr);
	  gcr_prompt_set_choice_label (prompt, msg);
	  g_free (msg);
	}
      else
	gcr_prompt_set_choice_label
	  (prompt, "Automatically unlock this key, whenever I'm logged in");
    }
#endif

  return prompt;
}

static int
gnome3_cmd_handler (pinentry_t pe)
{
  struct pe_gnome3_run_s state;

  state.main_loop = g_main_loop_new (NULL, FALSE);
  if (!state.main_loop)
    {
      pe->specific_err_info = strdup ("Failed to create GMainLoop");
      pe->specific_err = gpg_error (GPG_ERR_PIN_ENTRY);
      pe->specific_err_loc = "g_main_loop_new";
      pe->canceled = 1;
      return -1;
    }
  state.pinentry = pe;
  state.ret = 0;
  state.timeout_id = 0;
  state.timed_out = 0;
  state.prompt = create_prompt (pe, !(pe->pin));
  if (!state.prompt)
    {
      pe->canceled = 1;
      return -1;
    }
  if (pe->pin)
    gcr_prompt_password_async (state.prompt, NULL, pe_gcr_prompt_password_done,
                               &state);
  else
    gcr_prompt_confirm_async (state.prompt, NULL, pe_gcr_prompt_confirm_done,
                              &state);

  if (pe->timeout)
    state.timeout_id = g_timeout_add_seconds (pe->timeout,
                                              pe_gcr_timeout_done, &state);
  g_main_loop_run (state.main_loop);

  /* clean up state: */
  if (state.timeout_id && !state.timed_out)
    g_source_destroy
      (g_main_context_find_source_by_id (NULL, state.timeout_id));

  g_clear_object (&state.prompt);
  g_main_loop_unref (state.main_loop);
  return state.ret;
};


static void
pe_gcr_prompt_password_done (GObject *source_object,
                             GAsyncResult *res, gpointer user_data)
{
  struct pe_gnome3_run_s *state = user_data;
  GcrPrompt *prompt = GCR_PROMPT (source_object);

  if (state && prompt && state->prompt == prompt)
    {
      const char *password;
      GError *error = NULL;
      pinentry_t pe = state->pinentry;
      int ret = -1;

      /* "The returned password is valid until the next time a method
	 is called to display another prompt."  */
      password = gcr_prompt_password_finish (prompt, res, &error);
      if ((! password && ! error)
          || (error && error->code == G_IO_ERROR_CANCELLED))
        {
          /* operation was cancelled or timed out.  */
          ret = -1;
          if (state->timed_out)
            state->pinentry->specific_err = gpg_error (GPG_ERR_TIMEOUT);
          if (error)
            g_error_free (error);
        }
      else if (error)
	{
          _propagate_g_error_to_pinentry (pe, error,
                                          GPG_ERR_PIN_ENTRY,
                                          "gcr_system_password_finish");
	  g_error_free (error);
	  ret = -1;
	}
      else
	{
	  pinentry_setbufferlen (pe, strlen (password) + 1);
	  if (pe->pin)
	    strcpy (pe->pin, password);

	  if (pe->repeat_passphrase)
	    pe->repeat_okay = 1;

#ifdef HAVE_LIBSECRET
	  if (pe->allow_external_password_cache && pe->keyinfo)
	    pe->may_cache_password = gcr_prompt_get_choice_chosen (prompt);
#endif

	  ret = 1;
	}
      state->ret = ret;
    }

  if (state)
    g_main_loop_quit (state->main_loop);
}

static void
pe_gcr_prompt_confirm_done (GObject *source_object, GAsyncResult *res,
                            gpointer user_data)
{
  struct pe_gnome3_run_s *state = user_data;
  GcrPrompt *prompt = GCR_PROMPT (source_object);

  if (state && prompt && state->prompt == prompt)
    {
      GcrPromptReply reply;
      GError *error = NULL;
      pinentry_t pe = state->pinentry;
      int ret = -1;

      /* XXX: We don't support a third button!  */

      reply = gcr_prompt_confirm_finish (prompt, res, &error);
      if (error)
	{
          if (error->code == G_IO_ERROR_CANCELLED)
            {
              pe->canceled = 1;
              if (state->timed_out)
                state->pinentry->specific_err = gpg_error (GPG_ERR_TIMEOUT);
            }
          else
            _propagate_g_error_to_pinentry (state->pinentry, error, GPG_ERR_PIN_ENTRY,
                                            "gcr_system_confirm_finish");
	  g_error_free (error);
	  ret = 0;
	}
      else if (reply == GCR_PROMPT_REPLY_CONTINUE
	       /* XXX: Hack since gcr doesn't yet support one button
		  message boxes treat cancel the same as okay.  */
	       || pe->one_button)
        {
          /* Confirmation.  */
          ret = 1;
        }
      else /* GCR_PROMPT_REPLY_CANCEL */
	{
	  pe->canceled = 1;
          if (state->timed_out)
            state->pinentry->specific_err = gpg_error (GPG_ERR_TIMEOUT);
	  ret = 0;
	}
      state->ret = ret;
    }

  if (state)
    g_main_loop_quit (state->main_loop);
}

static gboolean
pe_gcr_timeout_done (gpointer user_data)
{
  struct pe_gnome3_run_s *state = user_data;

  if (!state)
    return FALSE;

  state->timed_out = 1;
  gcr_prompt_close (state->prompt);

  return FALSE;
}

pinentry_cmd_handler_t pinentry_cmd_handler = gnome3_cmd_handler;

/* Test whether there is a GNOME screensaver running that happens to
 * be locked.  Note that if there is no GNOME screensaver running at
 * all the answer is still FALSE.  */
static gboolean
pe_gnome_screen_locked (void)
{
  GDBusConnection *dbus;
  GError *error = NULL;
  GVariant *reply, *reply_bool;
  gboolean ret;

  dbus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error);

  if (!dbus)
    {
      fprintf (stderr, "failed to connect to user session D-Bus (%d): %s",
               error ? error->code : -1,
               error ? error->message : "<no GError>");
      if (error)
        g_error_free (error);
      return FALSE;
    }

  /* this is intended to be the equivalent of:
   * dbus-send --print-reply=literal --session          \
   *           --dest=org.gnome.ScreenSaver             \
   *           /org/gnome/ScreenSaver                   \
   *           org.gnome.ScreenSaver.GetActive
   */
  reply = g_dbus_connection_call_sync (dbus,
                                       "org.gnome.ScreenSaver",
                                       "/org/gnome/ScreenSaver",
                                       "org.gnome.ScreenSaver",
                                       "GetActive",
                                       NULL,
                                       ((const GVariantType *) "(b)"),
                                       G_DBUS_CALL_FLAGS_NO_AUTO_START,
                                       -1,
                                       NULL,
                                       &error);
  g_object_unref(dbus);
  if (!reply)
    {
      /* G_IO_ERROR_IS_DIRECTORY is the expected response when there is
       * no gnome screensaver at all, don't be noisy in that case: */
      if (!(error && error->code == G_IO_ERROR_IS_DIRECTORY))
        fprintf (stderr, "Failed to get d-bus reply for org.gnome.ScreenSaver.GetActive (%d): %s\n",
                 error ? error->code : -1,
                 error ? error->message : "<no GError>");
      if (error)
        g_error_free (error);
      return FALSE;
    }
  reply_bool = g_variant_get_child_value (reply, 0);
  if (!reply_bool)
    {
      fprintf (stderr, "Failed to get d-bus boolean from org.gnome.ScreenSaver.GetActive; assuming screensaver is not locked\n");
      ret = FALSE;
    }
  else
    {
      ret = g_variant_get_boolean (reply_bool);
      g_variant_unref (reply_bool);
    }

  g_variant_unref (reply);
  return ret;
}

/* Test whether we can create a system prompt or not.  This briefly
 * does create a system prompt, which blocks other tools from making
 * the same request concurrently, so we just create it to test if it is
 * available, and quickly close it.  */
static int
pe_gcr_system_prompt_available (void)
{
  GcrSystemPrompt *prompt;
  GError *error = NULL;
  int ret = 0;

  prompt = GCR_SYSTEM_PROMPT (gcr_system_prompt_open (0, NULL, &error));
  if (prompt)
    {
      ret = 1;
      if (!gcr_system_prompt_close (prompt, NULL, &error))
          fprintf (stderr, "failed to close test Gcr System Prompt (%d): %s\n",
                   error ? error->code : -1,
                   error ? error->message : "<no GError>");
      g_clear_object (&prompt);
    }
  else if (error && error->code == GCR_SYSTEM_PROMPT_IN_PROGRESS)
    {
      /* This one particular failure is OK; we're clearly capable of
       * making a system prompt, even though someone else has the
       * system prompter right now: */
      ret = 1;
    }

  if (error)
    g_error_free (error);
  return ret;
}

int
main (int argc, char *argv[])
{
  pinentry_init (PGMNAME);

#ifdef FALLBACK_CURSES
  if (!getenv ("DBUS_SESSION_BUS_ADDRESS"))
    {
      fprintf (stderr, "No $DBUS_SESSION_BUS_ADDRESS found,"
               " falling back to curses\n");
      pinentry_cmd_handler = curses_cmd_handler;
      pinentry_set_flavor_flag ("curses");
    }
  else if (!pe_gcr_system_prompt_available ())
    {
      fprintf (stderr, "No Gcr System Prompter available,"
               " falling back to curses\n");
      pinentry_cmd_handler = curses_cmd_handler;
      pinentry_set_flavor_flag ("curses");
    }
  else if (pe_gnome_screen_locked ())
    {
      fprintf (stderr, "GNOME screensaver is locked,"
               " falling back to curses\n");
      pinentry_cmd_handler = curses_cmd_handler;
      pinentry_set_flavor_flag ("curses");
    }
#endif

  pinentry_parse_opts (argc, argv);

  if (pinentry_loop ())
    return 1;

  return 0;
}
