/*
 * Copyright © 2022 Matthew Leeds
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
 *
 * Authors:
 *       Matthew Leeds <mwleeds@protonmail.com>
 */

#include "config.h"

#include <locale.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdint.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#include <gio/gio.h>
#include <gio/gunixfdlist.h>
#include <gio/gunixinputstream.h>
#include <gio/gdesktopappinfo.h>
#include <glib/gi18n.h>

#include "xdp-app-launch-context.h"
#include "xdp-context.h"
#include "xdp-dbus.h"
#include "xdp-impl-dbus.h"
#include "xdp-portal-config.h"
#include "xdp-request.h"
#include "xdp-utils.h"

#include "dynamic-launcher.h"

#define MAX_DESKTOP_SIZE_BYTES 1048576

typedef struct _DynamicLauncher DynamicLauncher;
typedef struct _DynamicLauncherClass DynamicLauncherClass;

struct _DynamicLauncher
{
  XdpDbusDynamicLauncherSkeleton parent_instance;

  XdpDbusImplDynamicLauncher *impl;
  GMutex transient_permissions_lock;
  GHashTable *transient_permissions;
};

struct _DynamicLauncherClass
{
  XdpDbusDynamicLauncherSkeletonClass parent_class;
};

GType dynamic_launcher_get_type (void) G_GNUC_CONST;
static void dynamic_launcher_iface_init (XdpDbusDynamicLauncherIface *iface);

G_DEFINE_TYPE_WITH_CODE (DynamicLauncher, dynamic_launcher,
                         XDP_DBUS_TYPE_DYNAMIC_LAUNCHER_SKELETON,
                         G_IMPLEMENT_INTERFACE (XDP_DBUS_TYPE_DYNAMIC_LAUNCHER,
                                                dynamic_launcher_iface_init));

G_DEFINE_AUTOPTR_CLEANUP_FUNC (DynamicLauncher, g_object_unref)

typedef enum {
  DYNAMIC_LAUNCHER_TYPE_APPLICATION = 1,
  DYNAMIC_LAUNCHER_TYPE_WEBAPP = 2,
} DynamicLauncherType;

static GVariant *
get_launcher_data_and_revoke_token (DynamicLauncher *dynamic_launcher,
                                    const char      *token)
{
  g_autoptr(GMutexLocker) locker =
    g_mutex_locker_new (&dynamic_launcher->transient_permissions_lock);
  GVariant *launcher_data_wrapped;

  if (!dynamic_launcher->transient_permissions)
    return NULL;

  if (!g_uuid_string_is_valid (token))
    return NULL;

  launcher_data_wrapped =
    g_hash_table_lookup (dynamic_launcher->transient_permissions, token);
  if (launcher_data_wrapped)
    {
      g_autoptr(GVariant) launcher_data = NULL;
      guint timeout_id;

      g_variant_get (launcher_data_wrapped, "(vu)", &launcher_data, &timeout_id);

      g_source_remove (timeout_id);
      g_hash_table_remove (dynamic_launcher->transient_permissions, token);

      return g_steal_pointer (&launcher_data);
    }

  return NULL;
}

static gboolean
validate_desktop_file_id (XdpAppInfo  *app_info,
                          const char  *desktop_file_id,
                          GError     **error)
{
  g_autofree char *no_dot_desktop = NULL;

  if (!g_str_has_suffix (desktop_file_id, ".desktop"))
    {
      g_set_error (error,
                   XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT,
                   _("Desktop file id missing .desktop suffix: %s"), desktop_file_id);
      return FALSE;
    }

  no_dot_desktop = g_strndup (desktop_file_id,
                              strlen(desktop_file_id) - strlen (".desktop"));

  if (!xdp_app_info_is_valid_sub_app_id (app_info, no_dot_desktop))
    {
      g_set_error (error,
                   XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT,
                   _("Desktop file id is not valid"));
      return FALSE;
    }

  return TRUE;
}

static gboolean
validate_serialized_icon (GVariant  *arg_icon_v,
                          char     **icon_format,
                          char     **icon_size)
{
  GBytes *bytes;
  g_autoptr(GIcon) icon = NULL;
  g_autoptr(GVariant) icon_v = NULL;
  g_autoptr(XdpSealedFd) sealed_icon = NULL;

  if (!arg_icon_v)
    return FALSE;

  if (!(icon_v = g_variant_get_variant (arg_icon_v)))
    return FALSE;

  icon = g_icon_deserialize (icon_v);

  if (!G_IS_BYTES_ICON (icon))
    return FALSE;

  bytes = g_bytes_icon_get_bytes (G_BYTES_ICON (icon));
  sealed_icon = xdp_sealed_fd_new_from_bytes (bytes, NULL);

  return sealed_icon &&
         xdp_validate_icon (sealed_icon,
                            XDP_ICON_TYPE_DESKTOP,
                            icon_format,
                            icon_size);
}

static gboolean
write_icon_to_disk (GVariant    *icon_v,
                    const char  *icon_subdir,
                    const char  *icon_path,
                    GError     **error)
{
  g_autoptr(GIcon) icon = NULL;
  g_autoptr(GFile) icon_file = NULL;
  g_autoptr(GFileOutputStream) icon_stream = NULL;
  GBytes *icon_bytes;
  gconstpointer bytes_data;
  gsize bytes_len;

  icon = g_icon_deserialize (icon_v);
  g_assert (G_IS_BYTES_ICON (icon));
  icon_bytes = g_bytes_icon_get_bytes (G_BYTES_ICON (icon));

  g_mkdir_with_parents (icon_subdir, 0700);
  icon_file = g_file_new_for_path (icon_path);
  icon_stream = g_file_replace (icon_file, NULL, FALSE, G_FILE_CREATE_REPLACE_DESTINATION, NULL, error);
  if (icon_stream == NULL)
    return FALSE;

  /* Use write_all() instead of write_bytes() so we don't have to worry about
   * partial writes (https://gitlab.gnome.org/GNOME/glib/-/issues/570).
   */
  bytes_data = g_bytes_get_data (icon_bytes, &bytes_len);
  if (!g_output_stream_write_all (G_OUTPUT_STREAM (icon_stream),
                                  bytes_data, bytes_len,
                                  NULL, NULL, error) ||
      !g_output_stream_close (G_OUTPUT_STREAM (icon_stream), NULL, error))
    return FALSE;

  return TRUE;
}

static void
get_icon_path (const char  *desktop_file_id,
               const char *icon_extension,
               const char *icon_size,
               char       **icon_dir_path_out,
               char       **icon_path_out)
{
  g_autofree char *no_dot_desktop = NULL;
  g_autofree char *icon_name = NULL;
  g_autofree char *subdir = NULL;

  no_dot_desktop = g_strndup (desktop_file_id,
                              strlen(desktop_file_id) - strlen (".desktop"));
  icon_name = g_strconcat (no_dot_desktop, ".", icon_extension, NULL);

  /* Put the icon in a per-size subdirectory so the size is discernible
   * without reading the file
   */
  if (g_strcmp0 (icon_extension, "svg") == 0)
    subdir = g_strdup ("scalable");
  else
    subdir = g_strdup_printf ("%sx%s", icon_size, icon_size);

  *icon_dir_path_out = g_build_filename (g_get_user_data_dir (),
                                         XDG_PORTAL_ICONS_DIR,
                                         subdir,
                                         NULL);
  *icon_path_out = g_build_filename (*icon_dir_path_out,
                                     icon_name,
                                     NULL);
}

static gboolean
write_keyfile_to_disk (const char  *desktop_file_id,
                       GKeyFile    *desktop_keyfile,
                       GError     **error)
{
  g_autofree char *desktop_dir = NULL;
  g_autofree char *desktop_path = NULL;
  g_autofree char *link_dir = NULL;
  g_autofree char *link_path = NULL;
  g_autofree char *relative_path = NULL;
  g_autoptr(GFile) link_file = NULL;

  /* Put the desktop file in ~/.local/share/xdg-desktop-portal/applications/ so
   * there's no ambiguity about which launchers were created by this portal.
   */
  desktop_dir = g_build_filename (g_get_user_data_dir (),
                                  XDG_PORTAL_APPLICATIONS_DIR,
                                  NULL);
  g_mkdir_with_parents (desktop_dir, 0700);
  desktop_path = g_build_filename (desktop_dir, desktop_file_id, NULL);

  if (!g_key_file_save_to_file (desktop_keyfile, desktop_path, error))
    return FALSE;

  /* Make a sym link in ~/.local/share/applications so the launcher shows up in
   * the desktop environment's menu.
   */
  link_dir = g_build_filename (g_get_user_data_dir (),
                               "applications",
                               NULL);
  g_mkdir_with_parents (link_dir, 0700);
  link_path = g_build_filename (link_dir, desktop_file_id, NULL);

  relative_path = g_build_filename ("..",
                                    XDG_PORTAL_APPLICATIONS_DIR,
                                    desktop_file_id,
                                    NULL);

  link_file = g_file_new_for_path (link_path);
  g_file_delete (link_file, NULL, NULL);

  if (!g_file_make_symbolic_link (link_file, relative_path, NULL, error))
    {
      remove (desktop_path);
      return FALSE;
    }

  return TRUE;
}

static GKeyFile *
get_desktop_entry (const char  *desktop_file_id,
                   const char  *desktop_entry,
                   const char  *name,
                   char        *icon_path,
                   GError     **error)
{
  g_autoptr(GKeyFile) key_file = g_key_file_new ();
  g_auto(GStrv) groups = NULL;

  if (!g_key_file_load_from_data (key_file, desktop_entry, -1,
                                  G_KEY_FILE_KEEP_COMMENTS | G_KEY_FILE_KEEP_TRANSLATIONS,
                                  error))
    {
      g_set_error (error,
                   XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT,
                   _("Desktop entry given to Install() not a valid key file"));
      return NULL;
    }

  /* The desktop entry spec supports more than one group but we don't in case
   * there's a security risk.
   */
  groups = g_key_file_get_groups (key_file, NULL);
  if (g_strv_length (groups) != 1 ||
      !g_strv_contains ((const char * const *)groups, G_KEY_FILE_DESKTOP_GROUP))
    {
      g_set_error (error,
                   XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT,
                   _("Desktop entry given to Install() must have exactly one group"));
      return NULL;
    }

  /* Overwrite Name= and Icon= if they are present */
  g_key_file_set_string (key_file, G_KEY_FILE_DESKTOP_GROUP, "Name", name);
  g_key_file_set_string (key_file, G_KEY_FILE_DESKTOP_GROUP, "Icon", icon_path);

  return g_steal_pointer (&key_file);
}

static gboolean
handle_install (XdpDbusDynamicLauncher *object,
                GDBusMethodInvocation  *invocation,
                const gchar            *arg_token,
                const gchar            *arg_desktop_file_id,
                const gchar            *arg_desktop_entry,
                GVariant               *arg_options)
{
  DynamicLauncher *dynamic_launcher = (DynamicLauncher *) object;
  XdpAppInfo *app_info = xdp_invocation_get_app_info (invocation);
  g_autoptr(GVariant) launcher_data = NULL;
  g_autoptr(GError) error = NULL;
  g_autoptr(GKeyFile) desktop_keyfile = NULL;
  gsize desktop_entry_length = G_MAXSIZE;
  g_autoptr(GVariant) icon_v = NULL;
  const char *name, *icon_extension, *icon_size;
  g_autofree char *icon_dir_path = NULL;
  g_autofree char *icon_path = NULL;
  g_autoptr(GDesktopAppInfo) desktop_app_info = NULL;

  launcher_data = get_launcher_data_and_revoke_token (dynamic_launcher,
                                                      arg_token);
  if (launcher_data == NULL)
    {
      g_dbus_method_invocation_return_error (invocation,
                                             XDG_DESKTOP_PORTAL_ERROR,
                                             XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT,
                                             _("Token given is invalid: %s"), arg_token);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  g_variant_get (launcher_data, "(&sv&s&s)", &name, &icon_v, &icon_extension, &icon_size);
  g_assert (name != NULL && name[0] != '\0');
  g_assert (icon_v);
  g_assert (icon_extension != NULL && icon_extension[0] != '\0');
  g_assert (icon_size != NULL && icon_size[0] != '\0');

  if (!validate_desktop_file_id (app_info, arg_desktop_file_id, &error))
    {
      g_dbus_method_invocation_return_gerror (invocation, error);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  get_icon_path (arg_desktop_file_id, icon_extension, icon_size,
                 &icon_dir_path, &icon_path);

  desktop_keyfile = get_desktop_entry (arg_desktop_file_id,
                                       arg_desktop_entry,
                                       name,
                                       icon_path,
                                       &error);
  if (desktop_keyfile == NULL)
    {
      g_dbus_method_invocation_return_gerror (invocation, error);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  if (!xdp_app_info_validate_dynamic_launcher (app_info,
                                               desktop_keyfile,
                                               &error))
    {
      g_dbus_method_invocation_return_gerror (invocation, error);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  desktop_app_info = g_desktop_app_info_new_from_keyfile (desktop_keyfile);
  if (desktop_app_info == NULL)
    {
      g_set_error (&error,
                   XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT,
                   _("Desktop entry given to Install() not valid"));
      goto error;
    }

  g_free (g_key_file_to_data (desktop_keyfile, &desktop_entry_length, NULL));
  if (desktop_entry_length > MAX_DESKTOP_SIZE_BYTES)
    {
      g_set_error (&error,
                   XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_FAILED,
                   _("Desktop file exceeds max size (%d): %s"),
                   MAX_DESKTOP_SIZE_BYTES, arg_desktop_file_id);
      goto error;
    }

  /* Write the files last so it's only on-disk if other checks passed */
  if (!write_icon_to_disk (icon_v, icon_dir_path, icon_path, &error))
    goto error;

  if (!write_keyfile_to_disk (arg_desktop_file_id, desktop_keyfile, &error))
    {
      remove (icon_path);
      goto error;
    }

  xdp_dbus_dynamic_launcher_complete_install (object, invocation);
  return G_DBUS_METHOD_INVOCATION_HANDLED;

error:
  g_dbus_method_invocation_return_gerror (invocation, error);
  return G_DBUS_METHOD_INVOCATION_HANDLED;
}

static XdpOptionKey response_options[] = {
  { "name", G_VARIANT_TYPE_STRING, NULL },
  { "icon", G_VARIANT_TYPE_VARIANT, NULL },
  { "token", G_VARIANT_TYPE_UINT32, NULL }
};

typedef struct _InstallTokenTimeoutData
{
  DynamicLauncher *dynamic_launcher;
  char *token;
} InstallTokenTimeoutData;

static InstallTokenTimeoutData *
install_token_timeout_data_new (DynamicLauncher *dynamic_launcher,
                                const char      *token)
{
  InstallTokenTimeoutData *data = g_new0 (InstallTokenTimeoutData, 1);

  data->dynamic_launcher = g_object_ref (dynamic_launcher);
  data->token = g_strdup (token);

  return data;
}

static void
install_token_timeout_data_free (InstallTokenTimeoutData *data)
{
  g_object_unref (data->dynamic_launcher);
  g_free (data->token);
  g_free (data);
}

static gboolean
install_token_timeout (gpointer user_data)
{
  InstallTokenTimeoutData *data = user_data;
  g_autoptr(GVariant) launcher_data = NULL;

  g_debug ("Revoking install token %s", data->token);
  launcher_data = get_launcher_data_and_revoke_token (data->dynamic_launcher,
                                                      data->token);

  return G_SOURCE_REMOVE;
}

static void
set_launcher_data_for_token (DynamicLauncher *dynamic_launcher,
                             const char      *token,
                             GVariant        *launcher_data)
{
  g_autoptr(GMutexLocker) locker =
    g_mutex_locker_new (&dynamic_launcher->transient_permissions_lock);
  guint timeout_id;
  g_autoptr(GVariant) launcher_data_wrapped = NULL;

  /* Revoke the token if it hasn't been used after 5 minutes, in case of
   * client bugs. This is what the GNOME print portal implementation does.
   */
  timeout_id =
    g_timeout_add_seconds_full (G_PRIORITY_DEFAULT, 300,
                                install_token_timeout,
                                install_token_timeout_data_new (dynamic_launcher,
                                                                token),
                                (GDestroyNotify)install_token_timeout_data_free);
  launcher_data_wrapped =
    g_variant_ref_sink (g_variant_new ("(vu)", launcher_data, timeout_id));

  g_hash_table_insert (dynamic_launcher->transient_permissions,
                       g_strdup (token),
                       g_steal_pointer (&launcher_data_wrapped));
}

static void
prepare_install_done (GObject      *source,
                      GAsyncResult *result,
                      gpointer      data)
{
  g_autoptr(XdpRequest) request = data;
  guint response = 2;
  g_autoptr(GVariant) results = NULL;
  g_autoptr(GError) error = NULL;
  g_auto(GVariantBuilder) results_builder =
    G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_VARDICT);

  REQUEST_AUTOLOCK (request);

  if (!xdp_dbus_impl_dynamic_launcher_call_prepare_install_finish (XDP_DBUS_IMPL_DYNAMIC_LAUNCHER (source),
                                                                   &response,
                                                                   &results,
                                                                   result,
                                                                   &error))
    {
      g_dbus_error_strip_remote_error (error);
      g_warning ("Backend call failed: %s", error->message);
      goto out;
    }

  if (request->exported && response == 0)
    {
      g_autofree char *token = g_uuid_string_random ();
      const char *chosen_name = NULL;
      const char *icon_format = NULL;
      const char *icon_size = NULL;
      g_autoptr(GVariant) chosen_icon = NULL;

      icon_format = g_object_get_data (G_OBJECT (request), "icon-format");
      g_assert (icon_format != NULL && icon_format[0] != '\0');
      icon_size = g_object_get_data (G_OBJECT (request), "icon-size");
      g_assert (icon_size != NULL && icon_size[0] != '\0');

      if (!xdp_filter_options (results, &results_builder,
                               response_options, G_N_ELEMENTS (response_options),
                               NULL, &error) ||
          !g_variant_lookup (results, "name", "&s", &chosen_name) ||
          chosen_name[0] == '\0' ||
          !g_variant_lookup (results, "icon", "v", &chosen_icon))
        {
          g_warning ("Results from backend failed validation: %s",
                     error ? error->message : "missing entries");
          response = 2;
        }
      else
        {
          DynamicLauncher *dynamic_launcher =
            g_object_get_data (G_OBJECT (request), "dynamic-launcher");
          GVariant *launcher_data;

          /* Save the token in memory and return it to the caller */
          launcher_data = g_variant_new ("(svss)", chosen_name, chosen_icon, icon_format, icon_size);
          set_launcher_data_for_token (dynamic_launcher, token, launcher_data);
          g_variant_builder_add (&results_builder, "{sv}", "token", g_variant_new_string (token));
        }
    }

out:
  if (request->exported)
    {
      xdp_dbus_request_emit_response (XDP_DBUS_REQUEST (request),
                                      response,
                                      g_variant_builder_end (&results_builder));

      xdp_request_unexport (request);
    }
}

static gboolean
validate_uri (const char  *key,
              GVariant    *value,
              GVariant    *options,
              gpointer     user_data,
              GError     **error)
{
  const char *uri = g_variant_get_string (value, NULL);
  g_autoptr(GError) local_error = NULL;
  guint32 launcher_type;

  g_variant_lookup (options, "launcher_type", "u", &launcher_type);
  if (launcher_type == DYNAMIC_LAUNCHER_TYPE_WEBAPP &&
      !g_uri_is_valid (uri, G_URI_FLAGS_NONE, &local_error))
    {
      g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT,
                   _("Given URI is invalid: %s"), local_error->message);
      return FALSE;
    }

  return TRUE;
}

static gboolean
validate_launcher_type (const char  *key,
                        GVariant    *value,
                        GVariant    *options,
                        gpointer     user_data,
                        GError     **error)
{
  DynamicLauncher *dynamic_launcher = user_data;
  uint32_t supported_launcher_types =
    xdp_dbus_dynamic_launcher_get_supported_launcher_types (XDP_DBUS_DYNAMIC_LAUNCHER (dynamic_launcher));
  uint32_t launcher_type = g_variant_get_uint32 (value);

  if (__builtin_popcount (launcher_type) != 1)
    {
      g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT,
                   _("Invalid launcher type: %x"), launcher_type);
      return FALSE;
    }

  if (!(supported_launcher_types & launcher_type))
    {
      g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT,
                   _("Unsupported launcher type: %x"), launcher_type);
      return FALSE;
    }

  return TRUE;
}

static XdpOptionKey prepare_install_options[] = {
  { "modal", G_VARIANT_TYPE_BOOLEAN },
  { "launcher_type", G_VARIANT_TYPE_UINT32, validate_launcher_type },
  { "target", G_VARIANT_TYPE_STRING, validate_uri },
  { "editable_name", G_VARIANT_TYPE_BOOLEAN },
  { "editable_icon", G_VARIANT_TYPE_BOOLEAN }
};

static gboolean
handle_prepare_install (XdpDbusDynamicLauncher *object,
                        GDBusMethodInvocation  *invocation,
                        const gchar            *arg_parent_window,
                        const gchar            *arg_name,
                        GVariant               *arg_icon_v,
                        GVariant               *arg_options)
{
  DynamicLauncher *dynamic_launcher = (DynamicLauncher *) object;
  XdpRequest *request = xdp_request_from_invocation (invocation);
  const char *app_id = xdp_app_info_get_id (request->app_info);
  g_autoptr(GError) error = NULL;
  g_autoptr(XdpDbusImplRequest) impl_request = NULL;
  g_auto(GVariantBuilder) opt_builder =
    G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_VARDICT);
  g_autofree char *icon_format = NULL;
  g_autofree char *icon_size = NULL;
  g_autoptr(GVariant) icon_v = NULL;

  REQUEST_AUTOLOCK (request);

  impl_request = xdp_dbus_impl_request_proxy_new_sync (
    g_dbus_proxy_get_connection (G_DBUS_PROXY (dynamic_launcher->impl)),
    G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES,
    g_dbus_proxy_get_name (G_DBUS_PROXY (dynamic_launcher->impl)),
    request->id,
    NULL, &error);

  if (!impl_request)
    {
      g_dbus_method_invocation_return_gerror (invocation, error);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  xdp_request_set_impl_request (request, impl_request);
  xdp_request_export (request, g_dbus_method_invocation_get_connection (invocation));

  if (!xdp_filter_options (arg_options, &opt_builder,
                           prepare_install_options, G_N_ELEMENTS (prepare_install_options),
                           dynamic_launcher, &error))
    {
      g_dbus_method_invocation_return_gerror (invocation, error);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  /* Do some validation on the icon before passing it along */
  if (!validate_serialized_icon (arg_icon_v, &icon_format, &icon_size))
    {
      g_dbus_method_invocation_return_error (invocation,
                                             XDG_DESKTOP_PORTAL_ERROR,
                                             XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT,
                                             _("Dynamic launcher icon failed validation"));
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  g_object_set_data_full (G_OBJECT (request), "dynamic-launcher",
                          g_object_ref (dynamic_launcher), g_object_unref);
  g_object_set_data_full (G_OBJECT (request), "icon-format",
                          g_steal_pointer (&icon_format), g_free);
  g_object_set_data_full (G_OBJECT (request), "icon-size",
                          g_steal_pointer (&icon_size), g_free);

  xdp_dbus_impl_dynamic_launcher_call_prepare_install (dynamic_launcher->impl,
                                                       request->id,
                                                       app_id,
                                                       arg_parent_window,
                                                       arg_name,
                                                       arg_icon_v,
                                                       g_variant_builder_end (&opt_builder),
                                                       NULL, /* cancellable */
                                                       prepare_install_done,
                                                       g_object_ref (request));

  xdp_dbus_dynamic_launcher_complete_prepare_install (object, invocation, request->id);

  return G_DBUS_METHOD_INVOCATION_HANDLED;
}

static gboolean
handle_request_install_token (XdpDbusDynamicLauncher *object,
                              GDBusMethodInvocation  *invocation,
                              const gchar            *arg_name,
                              GVariant               *arg_icon_v,
                              GVariant               *arg_options)
{
  DynamicLauncher *dynamic_launcher = (DynamicLauncher *) object;
  XdpAppInfo *app_info = xdp_invocation_get_app_info (invocation);
  const char *app_id = xdp_app_info_get_id (app_info);
  g_autoptr(GError) error = NULL;
  g_autofree char *token = NULL;
  g_autofree char *icon_format = NULL;
  g_autofree char *icon_size = NULL;
  g_autoptr(GVariant) icon_v = NULL;
  guint response = 2;

  /* Don't enforce app ID requirements on unsandboxed apps if the app ID
   * couldn't be determined. Otherwise the check would fail if for example the
   * app was launched from the CLI:
   * https://github.com/flatpak/xdg-desktop-portal/pull/719#issuecomment-1057412221
   */
  if (xdp_app_info_is_host (app_info) && g_str_equal (app_id, ""))
    {
      response = 0;
    }
  else if (!xdp_dbus_impl_dynamic_launcher_call_request_install_token_sync (dynamic_launcher->impl,
                                                                            app_id,
                                                                            arg_options,
                                                                            &response,
                                                                            NULL, /* cancellable */
                                                                            &error))
    {
      g_dbus_error_strip_remote_error (error);
      g_warning ("Backend call failed: %s", error->message);
      response = 2;
    }

  if (response == 0)
    {
      GVariant *launcher_data;

      /* Do some validation on the icon before saving it */
      if (!validate_serialized_icon (arg_icon_v, &icon_format, &icon_size))
        {
          g_dbus_method_invocation_return_error (invocation,
                                                 XDG_DESKTOP_PORTAL_ERROR,
                                                 XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT,
                                                 _("Dynamic launcher icon failed validation"));
          return G_DBUS_METHOD_INVOCATION_HANDLED;
        }

      launcher_data = g_variant_new ("(svss)", arg_name, icon_v, icon_format, icon_size);
      token = g_uuid_string_random ();

      /* Save the token in memory and return it to the caller */
      set_launcher_data_for_token (dynamic_launcher, token, launcher_data);

      xdp_dbus_dynamic_launcher_complete_request_install_token (object, invocation, token);
    }
  else
    {
      g_dbus_method_invocation_return_error (invocation,
                                             XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED,
                                             _("RequestInstallToken() not allowed for app id %s"),
                                             xdp_app_info_get_id (app_info));
    }

  return G_DBUS_METHOD_INVOCATION_HANDLED;
}

static gboolean
handle_uninstall (XdpDbusDynamicLauncher *object,
                  GDBusMethodInvocation  *invocation,
                  const gchar            *arg_desktop_file_id,
                  GVariant               *arg_options)
{
  XdpAppInfo *app_info = xdp_invocation_get_app_info (invocation);
  g_autoptr(GError) error = NULL;
  g_autoptr(GError) desktop_file_error = NULL;
  g_autofree char *icon_dir = NULL;
  g_autofree char *icon_path = NULL;
  g_autofree char *desktop_dir = NULL;
  g_autoptr(GFile) icon_file = NULL;
  g_autoptr(GFile) desktop_file = NULL;
  g_autoptr(GFile) link_file = NULL;
  g_autoptr(GKeyFile) desktop_keyfile = NULL;

  if (!validate_desktop_file_id (app_info, arg_desktop_file_id, &error))
    {
      g_dbus_method_invocation_return_gerror (invocation, error);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  icon_dir = g_build_filename (g_get_user_data_dir (), XDG_PORTAL_ICONS_DIR, NULL);
  desktop_dir = g_build_filename (g_get_user_data_dir (), XDG_PORTAL_APPLICATIONS_DIR, NULL);

  link_file = g_file_new_build_filename (g_get_user_data_dir (), "applications", arg_desktop_file_id, NULL);
  if (!g_file_delete (link_file, NULL, &error))
    {
      if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
        g_info ("Uninstall() method failed because launcher '%s' does not exist", arg_desktop_file_id);
      goto error;
    }

  desktop_file = g_file_new_build_filename (desktop_dir, arg_desktop_file_id, NULL);
  desktop_keyfile = g_key_file_new ();
  if (g_key_file_load_from_file (desktop_keyfile, g_file_peek_path (desktop_file), G_KEY_FILE_NONE, NULL))
    icon_path = g_key_file_get_string (desktop_keyfile, G_KEY_FILE_DESKTOP_GROUP, "Icon", NULL);

  g_file_delete (desktop_file, NULL, &desktop_file_error);

  if (icon_path && g_str_has_prefix (icon_path, icon_dir))
    {
      icon_file = g_file_new_for_path (icon_path);
      if (!g_file_delete (icon_file, NULL, &error))
        goto error;
    }

  if (desktop_file_error)
    goto error;

  xdp_dbus_dynamic_launcher_complete_uninstall (object, invocation);
  return G_DBUS_METHOD_INVOCATION_HANDLED;

error:
  g_dbus_method_invocation_return_gerror (invocation, error);
  return G_DBUS_METHOD_INVOCATION_HANDLED;
}

static gboolean
handle_get_desktop_entry (XdpDbusDynamicLauncher *object,
                          GDBusMethodInvocation  *invocation,
                          const gchar            *arg_desktop_file_id)
{
  XdpAppInfo *app_info = xdp_invocation_get_app_info (invocation);
  g_autoptr(GError) error = NULL;
  g_autofree char *desktop_dir = NULL;
  g_autofree char *contents = NULL;
  g_autofree char *desktop_path = NULL;
  gsize length;

  if (!validate_desktop_file_id (app_info, arg_desktop_file_id, &error))
    goto error;

  desktop_dir = g_build_filename (g_get_user_data_dir (), XDG_PORTAL_APPLICATIONS_DIR, NULL);

  desktop_path = g_build_filename (desktop_dir, arg_desktop_file_id, NULL);
  if (!g_file_get_contents (desktop_path, &contents, &length, &error))
    goto error;
  if (length > MAX_DESKTOP_SIZE_BYTES)
    {
      g_set_error (&error,
                   XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_FAILED,
                   _("Desktop file exceeds max size (%d): %s"),
                   MAX_DESKTOP_SIZE_BYTES, arg_desktop_file_id);
      goto error;
    }

  xdp_dbus_dynamic_launcher_complete_get_desktop_entry (object, invocation, contents);
  return G_DBUS_METHOD_INVOCATION_HANDLED;

error:
  g_dbus_method_invocation_return_gerror (invocation, error);
  return G_DBUS_METHOD_INVOCATION_HANDLED;
}

static gboolean
handle_get_icon (XdpDbusDynamicLauncher *object,
                 GDBusMethodInvocation  *invocation,
                 const gchar            *arg_desktop_file_id)
{
  XdpAppInfo *app_info = xdp_invocation_get_app_info (invocation);
  g_autoptr(GError) error = NULL;
  g_autofree char *desktop_dir = NULL;
  g_autofree char *contents = NULL;
  g_autofree char *desktop_path = NULL;
  g_autofree char *icon_dir = NULL;
  g_autofree char *icon_path = NULL;
  gsize length;
  g_autoptr(GKeyFile) key_file = NULL;
  g_autoptr(GFile) icon_file = NULL;
  g_autoptr(GIcon) icon = NULL;
  g_autoptr(GInputStream) stream = NULL;
  g_autoptr(GBytes) bytes = NULL;
  g_autoptr(GIcon) bytes_icon = NULL;
  g_autoptr(GVariant) icon_v = NULL;
  const gchar *icon_format = NULL;
  int icon_size = 0;

  if (!validate_desktop_file_id (app_info, arg_desktop_file_id, &error))
    goto error;

  desktop_dir = g_build_filename (g_get_user_data_dir (), XDG_PORTAL_APPLICATIONS_DIR, NULL);
  icon_dir = g_build_filename (g_get_user_data_dir (), XDG_PORTAL_ICONS_DIR, NULL);

  desktop_path = g_build_filename (desktop_dir, arg_desktop_file_id, NULL);
  if (!g_file_get_contents (desktop_path, &contents, &length, &error))
    goto error;
  if (length > MAX_DESKTOP_SIZE_BYTES)
    {
      g_set_error (&error,
                   XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_FAILED,
                   _("Desktop file exceeds max size (%d): %s"),
                   MAX_DESKTOP_SIZE_BYTES, arg_desktop_file_id);
      goto error;
    }

  key_file = g_key_file_new ();
  if (!g_key_file_load_from_data (key_file, contents, -1, G_KEY_FILE_NONE, &error))
    goto error;

  icon_path = g_key_file_get_string (key_file, G_KEY_FILE_DESKTOP_GROUP, "Icon", NULL);
  if (icon_path && g_str_has_prefix (icon_path, icon_dir))
    {
      g_autofree char *icon_dir = NULL;
      g_autofree char *icon_dir_basename = NULL;
      const char *x;

      if (g_str_has_suffix (icon_path, ".png"))
          icon_format = "png";
      else if (g_str_has_suffix (icon_path, ".svg"))
          icon_format = "svg";
      else if (g_str_has_suffix (icon_path, ".jpeg") || g_str_has_suffix (icon_path, ".jpg"))
          icon_format = "jpeg";

      /* dir should be either scalable or e.g. 512x512 */
      icon_dir = g_path_get_dirname (icon_path);
      icon_dir_basename = g_path_get_basename (icon_dir);
      if (g_strcmp0 (icon_dir_basename, "scalable") == 0) {
          /* An svg can have a width and height set, but it is probably not
           * needed since it can be scaled to any size.
           */
          icon_size = 4096;
      } else if ((x = strchr (icon_dir_basename, 'x')) != NULL) {
          icon_size = atoi (x + 1);
      }
    }

  if (!icon_format || icon_size <= 0 || icon_size > 4096)
    {
      g_set_error (&error,
                   XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_FAILED,
                   _("Desktop file '%s' icon at unrecognized path"), arg_desktop_file_id);
      goto error;
    }

  icon_file = g_file_new_for_path (icon_path);
  icon = g_file_icon_new (icon_file);
  stream = g_loadable_icon_load (G_LOADABLE_ICON (icon), 0, NULL, NULL, NULL);

  /* Icons are usually smaller than 1 MiB. Set a 10 MiB
   * limit so we can't use a huge amount of memory or hit
   * the D-Bus message size limit
   */
  if (stream)
      bytes = g_input_stream_read_bytes (stream, 10485760 /* 10 MiB */, NULL, NULL);
  if (bytes)
      bytes_icon = g_bytes_icon_new (bytes);
  if (bytes_icon)
      icon_v = g_icon_serialize (bytes_icon);

  if (icon_v == NULL)
    {
      g_set_error (&error,
                   XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_FAILED,
                   _("Desktop file '%s' icon failed to serialize"), arg_desktop_file_id);
      goto error;
    }

  xdp_dbus_dynamic_launcher_complete_get_icon (object, invocation,
                                               g_variant_new_variant (icon_v),
                                               icon_format, icon_size);
  return G_DBUS_METHOD_INVOCATION_HANDLED;

error:
  g_dbus_method_invocation_return_gerror (invocation, error);
  return G_DBUS_METHOD_INVOCATION_HANDLED;
}

static gboolean
handle_launch (XdpDbusDynamicLauncher *object,
               GDBusMethodInvocation  *invocation,
               const gchar            *arg_desktop_file_id,
               GVariant               *arg_options)
{
  XdpAppInfo *app_info = xdp_invocation_get_app_info (invocation);
  g_autoptr(GError) error = NULL;
  g_autofree char *desktop_dir = NULL;
  g_autofree char *desktop_path = NULL;
  const char *activation_token = NULL;
  g_autoptr(XdpAppLaunchContext) xdp_launch_context = NULL;
  GAppLaunchContext *launch_context = NULL;
  g_autoptr(GDesktopAppInfo) gappinfo = NULL;

  if (!validate_desktop_file_id (app_info, arg_desktop_file_id, &error))
    goto error;

  desktop_dir = g_build_filename (g_get_user_data_dir (), XDG_PORTAL_APPLICATIONS_DIR, NULL);

  desktop_path = g_build_filename (desktop_dir, arg_desktop_file_id, NULL);
  if (!g_file_test (desktop_path, G_FILE_TEST_EXISTS))
    {
      g_set_error (&error,
                   XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT,
                   _("No dynamic launcher exists with id '%s'"), arg_desktop_file_id);
      goto error;
    }

  g_variant_lookup (arg_options, "activation_token", "&s", &activation_token);

  xdp_launch_context = xdp_app_launch_context_new ();
  launch_context = G_APP_LAUNCH_CONTEXT (xdp_launch_context);
  /* Unset env var set in main() */
  g_app_launch_context_unsetenv (launch_context, "GIO_USE_VFS");
  /* Set activation token for focus stealing prevention */
  xdp_app_launch_context_set_activation_token (xdp_launch_context,
                                               activation_token);

  gappinfo = g_desktop_app_info_new_from_filename (desktop_path);
  if (gappinfo == NULL)
    {
      g_set_error (&error,
                   XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_FAILED,
                   _("Failed to create GDesktopAppInfo for launcher with id '%s'"),
                   arg_desktop_file_id);
      goto error;
    }

  g_debug ("Launching %s", arg_desktop_file_id);
  if (!g_app_info_launch (G_APP_INFO (gappinfo), NULL, launch_context, &error))
    goto error;

  xdp_dbus_dynamic_launcher_complete_launch (object, invocation);
  return G_DBUS_METHOD_INVOCATION_HANDLED;

error:
  g_dbus_method_invocation_return_gerror (invocation, error);
  return G_DBUS_METHOD_INVOCATION_HANDLED;
}

static void
dynamic_launcher_iface_init (XdpDbusDynamicLauncherIface *iface)
{
  iface->handle_install = handle_install;
  iface->handle_prepare_install = handle_prepare_install;
  iface->handle_request_install_token = handle_request_install_token;
  iface->handle_uninstall = handle_uninstall;
  iface->handle_get_desktop_entry = handle_get_desktop_entry;
  iface->handle_get_icon = handle_get_icon;
  iface->handle_launch = handle_launch;
}

static void
dynamic_launcher_dispose (GObject *object)
{
  DynamicLauncher *dl = (DynamicLauncher *) object;

  g_clear_object (&dl->impl);

  if (dl->transient_permissions)
    {
      g_mutex_clear (&dl->transient_permissions_lock);
      g_clear_pointer (&dl->transient_permissions, g_hash_table_unref);
    }

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

static void
dynamic_launcher_init (DynamicLauncher *dl)
{
}

static void
dynamic_launcher_class_init (DynamicLauncherClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->dispose = dynamic_launcher_dispose;
}

static DynamicLauncher*
dynamic_launcher_new (XdpDbusImplDynamicLauncher *impl)
{
  DynamicLauncher *dynamic_launcher;

  dynamic_launcher = g_object_new (dynamic_launcher_get_type (), NULL);
  dynamic_launcher->impl = g_object_ref (impl);

  g_dbus_proxy_set_default_timeout (G_DBUS_PROXY (dynamic_launcher->impl),
                                    G_MAXINT);

  xdp_dbus_dynamic_launcher_set_version (XDP_DBUS_DYNAMIC_LAUNCHER (dynamic_launcher), 1);

  g_object_bind_property (G_OBJECT (dynamic_launcher->impl), "supported-launcher-types",
                          G_OBJECT (dynamic_launcher), "supported-launcher-types",
                          G_BINDING_SYNC_CREATE);

  g_mutex_init (&dynamic_launcher->transient_permissions_lock);
  dynamic_launcher->transient_permissions =
    g_hash_table_new_full (g_str_hash, g_str_equal,
                           g_free, (GDestroyNotify) g_variant_unref);

  return dynamic_launcher;
}

void
init_dynamic_launcher (XdpContext *context)
{
  g_autoptr(DynamicLauncher) dynamic_launcher = NULL;
  GDBusConnection *connection = xdp_context_get_connection (context);
  XdpPortalConfig *config = xdp_context_get_config (context);
  XdpImplConfig *impl_config;
  g_autoptr(XdpDbusImplDynamicLauncher) impl = NULL;
  g_autoptr(GError) error = NULL;

  impl_config = xdp_portal_config_find (config, DYNAMIC_LAUNCHER_DBUS_IMPL_IFACE);
  if (impl_config == NULL)
    return;

  impl = xdp_dbus_impl_dynamic_launcher_proxy_new_sync (connection,
                                                        G_DBUS_PROXY_FLAGS_NONE,
                                                        impl_config->dbus_name,
                                                        DESKTOP_DBUS_PATH,
                                                        NULL,
                                                        &error);
  if (impl == NULL)
    {
      g_warning ("Failed to create dynamic_launcher proxy: %s", error->message);
      return;
    }

  dynamic_launcher = dynamic_launcher_new (impl);

  xdp_context_take_and_export_portal (context,
                                      G_DBUS_INTERFACE_SKELETON (g_steal_pointer (&dynamic_launcher)),
                                      XDP_CONTEXT_EXPORT_FLAGS_NONE);
}
