// SPDX-License-Identifier: GPL-2.0-or-later

#include "config.h"

#include <pwd.h>
#include <getopt.h>
#include <locale.h>
#include <shadow.h>
#include <stdbool.h>
#include <systemd/sd-daemon.h>
#include <systemd/sd-varlink.h>
#include <security/pam_misc.h>
#include <libeconf.h>

#include "basics.h"
#include "pwaccess.h"
#include "varlink-client-common.h"
#include "get_value.h"
#include "drop_privs.h"

#define USEC_INFINITY ((uint64_t) UINT64_MAX)

static int
get_shell_list(void)
{
  _cleanup_(econf_freeFilep) econf_file *key_file = NULL;
  _cleanup_(econf_freeArrayp) char **keys = NULL;
  size_t size = 0;
  econf_err error;

  error = econf_readConfig(&key_file,
			   NULL /* project */,
			   _PATH_VENDORDIR /* usr_conf_dir */,
			   "shells" /* config_name */,
			   NULL /* config_suffix */,
			   "" /* delim, key only */,
			   "#" /* comment */);
  if (error != ECONF_SUCCESS)
    {
      fprintf(stderr, "Cannot parse shell files: %s",
	      econf_errString(error));
      return 1;
    }

  error = econf_getKeys(key_file, NULL, &size, &keys);
  if (error)
    {
      fprintf(stderr, "Cannot evaluate entries in shell files: %s",
	      econf_errString(error));
      return 1;
    }

  for (size_t i = 0; i < size; i++)
    printf("%s\n", keys[i]);

  return 0;
}

static void
print_usage(FILE *stream)
{
  fprintf(stream, "Usage: chsh [-s shell] [-l] [--help] [--version] [user]\n");
}

static void
print_help(void)
{
  fprintf(stdout, "chsh - change login shell\n\n");
  print_usage(stdout);

  fputs("  -l, --list-shells   List allowed shells from /etc/shells\n", stdout);
  fputs("  -s, --shell <shell> Use 'shell' as new login shell\n", stdout);
  fputs("  -h, --help          Give this help list\n", stdout);
  fputs("  -v, --version       Print program version\n", stdout);
}

static void
print_error(void)
{
  fprintf (stderr, "Try `chsh --help' for more information.\n");
}

int
main(int argc, char **argv)
{
  struct callback_data cb_data = { .resp = NULL, .error_code = 0 };
  char *new_shell = NULL;
  int l_flag = 0;
  int r;

  setlocale(LC_ALL, "");

  while (1)
    {
      int c;
      int option_index = 0;
      static struct option long_options[] =
        {
          {"shell",       required_argument, NULL, 's' },
          {"list-shells", no_argument,       NULL, 'l' },
          {"version",     no_argument,       NULL, 'v' },
          {"help",        no_argument,       NULL, 'h' },
          {NULL,          0,                 NULL, '\0'}
        };

      c = getopt_long (argc, argv, "s:lvh",
                       long_options, &option_index);
      if (c == (-1))
        break;
      switch (c)
        {
        case 'l':
          l_flag = 1;
          break;
        case 's':
          if (!optarg)
            {
              print_usage(stderr);
              return 1;
            }
          new_shell = optarg;
          break;
        case 'h':
          print_help();
          return 0;
        case 'v':
	  printf("chsh (%s) %s\n", PACKAGE, VERSION);
          return 0;
        default:
          print_error();
          return 1;
        }
    }

  argc -= optind;
  argv += optind;

  if (argc > 1 || (l_flag && argc > 0))
    {
      fprintf(stderr, "chsh: Too many arguments.\n");
      print_error();
      return 1;
    }

  if (l_flag && new_shell)
    {
      fprintf(stderr, "chsh: Too many arguments.\n");
      print_error();
      return 1;
    }

  r = check_and_drop_privs();
  if (r < 0)
    return -r;

  if (l_flag)
    return get_shell_list();
  else
    {
      _cleanup_(sd_varlink_unrefp) sd_varlink *link = NULL;
      _cleanup_(sd_json_variant_unrefp) sd_json_variant *params = NULL;
      _cleanup_(struct_passwd_freep) struct passwd *pw = NULL;
      _cleanup_free_ char *error = NULL;
      const char *user = NULL;
      const char *old_shell = NULL;

      if (argc == 1)
	r = pwaccess_get_user_record(-1, argv[0], &pw, NULL, NULL, &error);
      else
	r = pwaccess_get_user_record(getuid(), NULL, &pw, NULL, NULL, &error);
      if (r < 0)
	{
	  if (argc == 1 && streq(error, "org.openSUSE.pwaccess.NoEntryFound"))
	    fprintf(stderr, "chsh: user '%s' does not exist\n", argv[0]);
	  else
	    fprintf (stderr, "get_user_record failed: %s\n", error?error:strerror(-r));
	  return -r;
	}

      user = pw->pw_name;
      old_shell = pw->pw_shell;

      if (new_shell == NULL)
	{
	  printf("Enter the new value, or press return for the default.\n");
	  r = get_value(old_shell, "Login Shell", &new_shell);
	  if (r < 0)
	    return -r;
	}

      /* we don't need to change the shell if here is no change */
      if (new_shell == NULL || streq(old_shell, new_shell))
	{
	  printf("Shell not changed.\n");
	  return 0;
	}

      r = connect_to_pwupdd(&link, _VARLINK_PWUPD_SOCKET, &error);
      if (r < 0)
	{
	  if (error)
	    fprintf(stderr, "%s\n", error);
	  else
	    fprintf(stderr, "Cannot connect to pwupd! (%s)\n", strerror(-r));

	  return -r;
	}
      sd_varlink_set_userdata(link, &cb_data);

      r = sd_json_buildo(&params,
			 SD_JSON_BUILD_PAIR("userName", SD_JSON_BUILD_STRING(user)),
			 SD_JSON_BUILD_PAIR("shell", SD_JSON_BUILD_STRING(strempty(new_shell))));
      if (r < 0)
	{
	  fprintf(stderr, "Failed to build param list: %s\n", strerror(-r));
	  return -r;
	}

      r = sd_varlink_bind_reply(link, reply_callback);
      if (r < 0)
	{
	  fprintf(stderr, "Failed to bind reply callback: %s\n", strerror(-r));
	  return -r;
	}

      r = sd_varlink_observe(link, "org.openSUSE.pwupd.Chsh", params);
      if (r < 0)
	{
	  fprintf(stderr, "Failed to call chsh method: %s\n", strerror(-r));
	  return -r;
	}

    loop:
      for (;;)
	{
	  r = sd_varlink_is_idle(link);
	  if (r < 0)
	    {
	      fprintf(stderr, "Failed to check if varlink connection is idle: %s\n", strerror(-r));
	      return -r;
	    }
	  if (r > 0)
	    break;

	  r = sd_varlink_process(link);
	  if (r < 0)
	    {
	      fprintf(stderr, "Failed to process varlink connection: %s\n", strerror(-r));
	      return -r;
	    }
	  if (r != 0)
	    continue;

	  r = sd_varlink_wait(link, USEC_INFINITY);
	  if (r < 0)
	    {
	      fprintf(stderr, "Failed to wait for varlink connection events: %s\n", strerror(-r));
	      return -r;
	    }
	}

      /* Check if an error occurred in the callback */
      if (cb_data.error_code != 0)
	{
	  if (cb_data.error_code == ENODATA)
	    fprintf(stderr, "chsh: user '%s' does not exist\n", user);
	  else
	    fprintf(stderr, "chsh: %s\n", strerror(cb_data.error_code));
	  return cb_data.error_code;
	}

      if (cb_data.resp)
	{
	  _cleanup_(sd_json_variant_unrefp) sd_json_variant *answer = NULL;

	  r = sd_json_buildo(&answer,
			     SD_JSON_BUILD_PAIR("response", SD_JSON_BUILD_STRING(strempty(cb_data.resp->resp))));
	  if (r < 0)
	    {
	      fprintf(stderr, "Failed to build response list: %s\n", strerror(-r));
	      return -r;
	    }

	  free(cb_data.resp->resp);
	  cb_data.resp = mfree(cb_data.resp);

	  sd_json_variant_sensitive(answer); /* password is sensitive */

	  r = sd_varlink_observe(link, "org.openSUSE.pwupd.Conv", answer);
	  if (r < 0)
	    {
	      fprintf(stderr, "Failed to call conv method: %s\n", strerror(-r));
	      return -r;
	    }
	  goto loop;
	}
    }

  return cb_data.error_code;
}
