/*
 * dbus-update-activation-environment - update D-Bus, and optionally
 * systemd, activation environment
 *
 * Copyright © 2014-2015 Collabora Ltd.
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation files
 * (the "Software"), to deal in the Software without restriction,
 * including without limitation the rights to use, copy, modify, merge,
 * publish, distribute, sublicense, and/or sell copies of the Software,
 * and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
 * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
 * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

#include <config.h>

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef HAVE_SYSEXITS_H
# include <sysexits.h>
#endif

#include <dbus/dbus.h>

#ifdef DBUS_UNIX
# include <unistd.h>
# include <sys/stat.h>
# include <sys/types.h>
#endif

#include "tool-common.h"

#define PROGNAME "dbus-update-activation-environment"

#ifndef EX_USAGE
# define EX_USAGE 64
#endif

#ifndef EX_UNAVAILABLE
# define EX_UNAVAILABLE 69
#endif

#ifndef EX_OSERR
# define EX_OSERR 71
#endif

#ifdef DBUS_WIN
/* The Windows C runtime uses a different name */
#define environ _environ
#else
/* apparently this is the portable way to get the entire environment...
 * GNU platforms also put it in unistd.h but that's not portable */
extern char **environ;
#endif

/* we don't really have anything useful to say about the stage at which we
 * failed */
#define oom() tool_oom ("updating environment")

static dbus_bool_t verbose = FALSE;

static void say (const char *format, ...) _DBUS_GNUC_PRINTF (1, 2);

static void
say (const char *format,
    ...)
{
  va_list ap;

  if (!verbose)
    return;

  fprintf (stderr, "%s: ", PROGNAME);
  va_start (ap, format);
  vfprintf (stderr, format, ap);
  fputc ('\n', stderr);
  va_end (ap);
}

#ifdef __linux__
static dbus_bool_t
systemd_user_running (void)
{
  char *xdg_runtime_dir = getenv ("XDG_RUNTIME_DIR");
  char *path;
  struct stat buf;
  dbus_bool_t ret = FALSE;

  if (xdg_runtime_dir == NULL)
    return FALSE;

  /* Assume that XDG_RUNTIME_DIR/systemd exists if and only if
   * "systemd --user" is running. It's OK to use asprintf() here
   * because we know we're on Linux. */
  if (asprintf (&path, "%s/systemd", xdg_runtime_dir) < 0)
    oom ();

  if (stat (path, &buf) == 0)
    ret = TRUE;

  free (path);
  return ret;
}
#endif

int
main (int argc, char **argv)
{
  DBusConnection *conn;
  DBusMessage *msg;
  DBusMessage *reply;
  DBusError error = DBUS_ERROR_INIT;
  DBusMessageIter msg_iter;
  DBusMessageIter array_iter;
  int i;
  int first_non_option = argc;
  dbus_bool_t all = FALSE;
#ifdef __linux__
  DBusMessage *sd_msg = NULL;
  DBusMessageIter sd_msg_iter;
  DBusMessageIter sd_array_iter;
  dbus_bool_t systemd = FALSE;
#endif

  for (i = 1; i < argc; i++)
    {
      if (argv[i][0] != '-')
        {
          first_non_option = i;
          break;
        }
      else if (strcmp (argv[i], "--") == 0)
        {
          first_non_option = i + 1;
          break;
        }
      else if (strcmp (argv[i], "--all") == 0)
        {
          all = TRUE;
        }
      else if (strcmp (argv[i], "--systemd") == 0)
        {
#ifdef __linux__
          systemd = TRUE;
#else
          say ("not on Linux, ignoring --systemd argument");
#endif
        }
      else if (strcmp (argv[i], "--verbose") == 0)
        {
          verbose = TRUE;
        }
      else
        {
          fprintf (stderr,
              "%1$s: update environment variables that will be set for D-Bus\n"
              "    session services\n"
              "\n"
              "%1$s [options] VAR[=VAL] [VAR2[=VAL2] ...]\n"
              "    Add specified variables to D-Bus activation environment.\n"
              "    If omitted, values are taken from current environment;\n"
              "    variables not found in the environment are ignored.\n"
              "%1$s --all\n"
              "    Add entire current environment to D-Bus activation\n"
              "    environment.\n"
              "\n"
              "Options:\n"
              "\n"
              "--all\n"
              "    Upload all environment variables.\n"
              "--systemd\n"
              "    Also update the 'systemd --user' environment\n"
              "    if possible.\n"
              "--verbose\n"
              "    Talk about it.\n"
              ,
              PROGNAME);
          exit (EX_USAGE);
        }
    }

  if (all && first_non_option < argc)
    {
      fprintf (stderr, "%s: error: --all cannot be used with VAR or "
               "VAR=VAL arguments\n", PROGNAME);
      exit (EX_USAGE);
    }

  conn = dbus_bus_get (DBUS_BUS_SESSION, &error);

  if (conn == NULL)
    {
      fprintf (stderr,
          "%s: error: unable to connect to D-Bus: %s\n", PROGNAME,
          error.message);
      exit (EX_OSERR);
    }

  msg = dbus_message_new_method_call (DBUS_SERVICE_DBUS, DBUS_PATH_DBUS,
      DBUS_INTERFACE_DBUS, "UpdateActivationEnvironment");

  if (msg == NULL)
    oom ();

  dbus_message_iter_init_append (msg, &msg_iter);

  if (!dbus_message_iter_open_container (&msg_iter, DBUS_TYPE_ARRAY,
      "{ss}", &array_iter))
    oom ();

#ifdef __linux__
  if (systemd)
    {
      if (!systemd_user_running ())
        {
          /* This is only best-effort. */
          say ("systemd --user not found, ignoring --systemd argument");
          systemd = FALSE;
        }
    }

  if (systemd)
    {
      sd_msg = dbus_message_new_method_call ("org.freedesktop.systemd1",
          "/org/freedesktop/systemd1", "org.freedesktop.systemd1.Manager",
          "SetEnvironment");

      if (sd_msg == NULL)
        oom ();

      dbus_message_iter_init_append (sd_msg, &sd_msg_iter);

      if (!dbus_message_iter_open_container (&sd_msg_iter, DBUS_TYPE_ARRAY,
          "s", &sd_array_iter))
        oom ();
    }
#endif

  for (i = all ? 0 : first_non_option;
      all ? environ[i] != NULL : i < argc;
      i++)
    {
      const char *var;
      char *copy;
      char *eq;
      const char *val;
      DBusMessageIter pair_iter;

      if (all)
        var = environ[i];
      else
        var = argv[i];

      copy = strdup (var);

      if (copy == NULL)
        oom ();

      if (!dbus_validate_utf8 (var, NULL))
        {
          /* var is either of the form VAR or VAR=VAL */
          fprintf (stderr,
              "%s: warning: environment variable not UTF-8: %s\n",
              PROGNAME, var);
          goto next;
        }

      eq = strchr (copy, '=');

      if (eq == NULL)
        {
          if (all)
            {
              /* items in the environment block should be of the form
               * VAR=VAL */
              fprintf (stderr,
                  "%s: warning: environment variable without '=': %s\n",
                  PROGNAME, var);
              goto next;
            }
          else
            {
              /* items on the command-line may be of the form VAR
               * in which case we infer the value from the environment */
              val = getenv (var);

              if (val == NULL)
                {
                  /* nothing to be done here */
                  goto next;
                }

              if (!dbus_validate_utf8 (val, NULL))
                {
                  fprintf (stderr,
                      "%s: warning: environment variable not UTF-8: %s=%s\n",
                      PROGNAME, var, val);
                  goto next;
                }
            }
        }
      else
        {
          /* split VAR=VAL into VAR and VAL */
          *eq = '\0';
          val = eq + 1;
        }

#ifdef __linux__
      if (systemd)
        {
          char *combined;

          /* recombine if necessary */
          if (asprintf (&combined, "%s=%s", copy, val) < 0)
            oom ();

          if (!dbus_message_iter_append_basic (&sd_array_iter,
                DBUS_TYPE_STRING, &combined))
            oom ();

          free (combined);
        }
#endif

      if (!dbus_message_iter_open_container (&array_iter,
              DBUS_TYPE_DICT_ENTRY, NULL, &pair_iter))
        oom ();

      say ("setting %s=%s", copy, val);

      if (!dbus_message_iter_append_basic (&pair_iter, DBUS_TYPE_STRING,
              &copy))
        oom ();

      if (!dbus_message_iter_append_basic (&pair_iter, DBUS_TYPE_STRING,
              &val))
        oom ();

      if (!dbus_message_iter_close_container (&array_iter, &pair_iter))
        oom ();

next:
      free (copy);
    }

  if (!dbus_message_iter_close_container (&msg_iter, &array_iter))
    oom ();

#ifdef __linux__
  if (systemd &&
      !dbus_message_iter_close_container (&sd_msg_iter, &sd_array_iter))
    oom ();
#endif

  reply = dbus_connection_send_with_reply_and_block (conn, msg, -1, &error);

  if (reply == NULL)
    {
      fprintf (stderr,
          "%s: error sending to dbus-daemon: %s: %s\n",
          PROGNAME, error.name, error.message);
      exit (EX_UNAVAILABLE);
    }

  if (dbus_set_error_from_message (&error, msg) ||
      !dbus_message_get_args (msg, &error, DBUS_TYPE_INVALID))
    {
      fprintf (stderr,
          "%s: error from dbus-daemon: %s: %s\n",
          PROGNAME, error.name, error.message);
      exit (EX_UNAVAILABLE);
    }

  dbus_message_unref (reply);

#ifdef __linux__
  if (systemd)
    {
      reply = dbus_connection_send_with_reply_and_block (conn, sd_msg, -1,
          &error);

      /* non-fatal, the main purpose of this thing is to communicate
       * with dbus-daemon */
      if (reply == NULL)
        {
          fprintf (stderr,
              "%s: warning: error sending to systemd: %s: %s\n",
              PROGNAME, error.name, error.message);
        }
      else if (dbus_set_error_from_message (&error, msg) ||
          !dbus_message_get_args (msg, &error, DBUS_TYPE_INVALID))
        {
          fprintf (stderr,
              "%s: warning: error from systemd: %s: %s\n",
              PROGNAME, error.name, error.message);
        }

      if (reply != NULL)
        dbus_message_unref (reply);

      dbus_message_unref (sd_msg);
      dbus_error_free (&error);
    }
#endif

  dbus_message_unref (msg);
  dbus_connection_unref (conn);
  return 0;
}
