/*
* Copyright © 2016 Red Hat, Inc
*
* 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 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 .
*
* Authors:
* Alexander Larsson
* Matthias Clasen
*/
#include "config.h"
#include "portal-impl.h"
#include
#include
#include
#include
typedef struct _PortalInterface {
/* dbus_name is NULL if this is the default */
char *dbus_name;
char **portals;
} PortalInterface;
typedef struct _PortalConfig {
char *source;
PortalInterface **interfaces;
size_t n_ifaces;
PortalInterface *default_portal;
} PortalConfig;
static void
portal_interface_free (PortalInterface *iface)
{
g_clear_pointer (&iface->dbus_name, g_free);
g_clear_pointer (&iface->portals, g_strfreev);
g_free (iface);
}
static void
portal_config_free (PortalConfig *config)
{
g_clear_pointer (&config->source, g_free);
for (size_t i = 0; i < config->n_ifaces; i++)
portal_interface_free (config->interfaces[i]);
g_clear_pointer (&config->default_portal, portal_interface_free);
g_clear_pointer (&config->interfaces, g_free);
g_free (config);
}
static void
portal_implementation_free (PortalImplementation *impl)
{
g_clear_pointer (&impl->source, g_free);
g_clear_pointer (&impl->dbus_name, g_free);
g_clear_pointer (&impl->interfaces, g_strfreev);
g_clear_pointer (&impl->use_in, g_strfreev);
g_free (impl);
}
G_DEFINE_AUTOPTR_CLEANUP_FUNC(PortalImplementation, portal_implementation_free)
G_DEFINE_AUTOPTR_CLEANUP_FUNC(PortalInterface, portal_interface_free)
G_DEFINE_AUTOPTR_CLEANUP_FUNC(PortalConfig, portal_config_free)
/* Validation code taken from gdesktopappinfo.c {{{ */
/* See: https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html
*
* There's not much to go on: a desktop name must be composed of alphanumeric
* characters, including '-' and '_'. Since we use this value to construct file
* names, we are going to need avoid invalid characters
*/
static gboolean
validate_xdg_desktop (const char *desktop)
{
size_t i;
for (i = 0; desktop[i] != '\0'; i++)
{
if (desktop[i] != '-' &&
desktop[i] != '_' &&
!g_ascii_isalnum (desktop[i]))
return FALSE;
}
if (i == 0)
return FALSE;
return TRUE;
}
static char **
get_valid_current_desktops (const char *value)
{
GPtrArray *valid_desktops;
char **tmp;
if (value == NULL)
value = g_getenv ("XDG_CURRENT_DESKTOP");
if (value == NULL)
value = "";
tmp = g_strsplit (value, G_SEARCHPATH_SEPARATOR_S, 0);
valid_desktops = g_ptr_array_new_full (g_strv_length (tmp) + 1, g_free);
for (size_t i = 0; tmp[i] != NULL; i++)
{
if (validate_xdg_desktop (tmp[i]))
g_ptr_array_add (valid_desktops, tmp[i]);
else
g_free (tmp[i]);
}
g_ptr_array_add (valid_desktops, NULL);
g_free (tmp);
tmp = (char **) g_ptr_array_steal (valid_desktops, NULL);
g_ptr_array_unref (valid_desktops);
return tmp;
}
static const char **
get_current_lowercase_desktops (void)
{
static char **result;
if (g_once_init_enter (&result))
{
char **tmp = get_valid_current_desktops (NULL);
for (size_t i = 0; tmp[i] != NULL; i++)
{
/* Convert to lowercase */
for (size_t j = 0; tmp[i][j] != '\0'; j++)
tmp[i][j] = g_ascii_tolower (tmp[i][j]);
}
g_once_init_leave (&result, tmp);
}
return (const char **) result;
}
/* }}} */
static PortalConfig *config = NULL;
static GList *implementations = NULL;
static gboolean
register_portal (const char *path,
gboolean opt_verbose,
GError **error)
{
g_autoptr(PortalImplementation) impl = g_new0 (PortalImplementation, 1);
g_autoptr(GKeyFile) keyfile = g_key_file_new ();
g_autofree char *basename = NULL;
int i;
g_debug ("loading %s", path);
if (!g_key_file_load_from_file (keyfile, path, G_KEY_FILE_NONE, error))
return FALSE;
basename = g_path_get_basename (path);
impl->source = g_strndup (basename, strrchr (basename, '.') - basename);
impl->dbus_name = g_key_file_get_string (keyfile, "portal", "DBusName", error);
if (impl->dbus_name == NULL)
return FALSE;
if (!g_dbus_is_name (impl->dbus_name))
{
g_set_error (error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_INVALID_VALUE,
"Not a valid bus name: %s", impl->dbus_name);
return FALSE;
}
impl->interfaces = g_key_file_get_string_list (keyfile, "portal", "Interfaces", NULL, error);
if (impl->interfaces == NULL)
return FALSE;
for (i = 0; impl->interfaces[i]; i++)
{
if (!g_dbus_is_interface_name (impl->interfaces[i]))
{
g_set_error (error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_INVALID_VALUE,
"Not a valid interface name: %s", impl->interfaces[i]);
return FALSE;
}
if (!g_str_has_prefix (impl->interfaces[i], "org.freedesktop.impl.portal."))
{
g_set_error (error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_INVALID_VALUE,
"Not a portal backend interface: %s", impl->interfaces[i]);
return FALSE;
}
}
if (opt_verbose)
{
for (i = 0; impl->interfaces[i]; i++)
g_debug ("portal implementation supports %s", impl->interfaces[i]);
}
impl->use_in = g_key_file_get_string_list (keyfile, "portal", "UseIn", NULL, error);
implementations = g_list_prepend (implementations, impl);
impl = NULL;
return TRUE;
}
static gboolean
g_strv_case_contains (const gchar * const *strv,
const gchar *str)
{
for (; strv && *strv != NULL; strv++)
{
if (g_ascii_strcasecmp (str, *strv) == 0)
return TRUE;
}
return FALSE;
}
static gint
sort_impl_by_use_in_and_name (gconstpointer a,
gconstpointer b)
{
const PortalImplementation *pa = a;
const PortalImplementation *pb = b;
const char **desktops;
int i;
desktops = get_current_lowercase_desktops ();
for (i = 0; desktops[i] != NULL; i++)
{
gboolean use_a = pa->use_in != NULL
? g_strv_case_contains ((const char **)pa->use_in, desktops[i])
: FALSE;
gboolean use_b = pb->use_in != NULL
? g_strv_case_contains ((const char **)pb->use_in, desktops[i])
: FALSE;
if (use_a != use_b)
return use_b - use_a;
else if (use_a)
break;
else
continue;
}
return strcmp (pa->source, pb->source);
}
void
load_installed_portals (gboolean opt_verbose)
{
g_autoptr(GFileEnumerator) enumerator = NULL;
g_autoptr(GFile) dir = NULL;
const char *portal_dir;
/* We need to override this in the tests */
portal_dir = g_getenv ("XDG_DESKTOP_PORTAL_DIR");
if (portal_dir == NULL)
portal_dir = DATADIR "/xdg-desktop-portal/portals";
g_debug ("load portals from %s", portal_dir);
dir = g_file_new_for_path (portal_dir);
enumerator = g_file_enumerate_children (dir, "*", G_FILE_QUERY_INFO_NONE, NULL, NULL);
if (enumerator == NULL)
return;
while (TRUE)
{
g_autoptr(GFileInfo) info = NULL;
g_autoptr(GError) error = NULL;
g_autoptr(GFile) child = NULL;
g_autofree char *path = NULL;
const char *name;
info = g_file_enumerator_next_file (enumerator, NULL, NULL);
if (info == NULL)
break;
name = g_file_info_get_name (info);
if (!g_str_has_suffix (name, ".portal"))
continue;
child = g_file_enumerator_get_child (enumerator, info);
path = g_file_get_path (child);
if (!register_portal (path, opt_verbose, &error))
{
g_warning ("Error loading %s: %s", path, error->message);
continue;
}
}
implementations = g_list_sort (implementations, sort_impl_by_use_in_and_name);
}
static PortalConfig *
load_portal_configuration_for_dir (gboolean opt_verbose,
const char *base_directory,
const char *portal_file)
{
g_autoptr(GKeyFile) key_file = NULL;
g_autofree char *path = NULL;
g_auto(GStrv) ifaces = NULL;
key_file = g_key_file_new ();
path = g_build_filename (base_directory, portal_file, NULL);
g_debug ("Looking for portals configuration in '%s'", path);
if (!g_key_file_load_from_file (key_file, path, G_KEY_FILE_NONE, NULL))
return NULL;
ifaces = g_key_file_get_keys (key_file, "preferred", NULL, NULL);
if (ifaces != NULL)
{
g_autoptr(PortalInterface) default_portal = NULL;
g_autoptr(PortalConfig) portal_config = NULL;
g_autoptr(GPtrArray) interfaces = NULL;
portal_config = g_new0 (PortalConfig, 1);
interfaces = g_ptr_array_new_full (g_strv_length (ifaces) + 1, NULL);
for (size_t i = 0; ifaces[i] != NULL; i++)
{
g_autoptr(PortalInterface) interface = g_new0 (PortalInterface, 1);
interface->dbus_name = g_strdup (ifaces[i]);
interface->portals = g_key_file_get_string_list (key_file, "preferred", ifaces[i], NULL, NULL);
if (interface->portals == NULL)
{
g_critical ("Invalid portals for interface '%s' in %s", ifaces[i], portal_file);
return NULL;
}
if (opt_verbose)
{
g_autofree char *preferred = g_strjoinv (", ", interface->portals);
g_debug ("Preferred portals for interface '%s': %s", ifaces[i], preferred);
}
if (strcmp (ifaces[i], "default") == 0)
default_portal = g_steal_pointer (&interface);
else
g_ptr_array_add (interfaces, g_steal_pointer (&interface));
}
portal_config->n_ifaces = interfaces->len;
portal_config->interfaces = (PortalInterface **) g_ptr_array_steal (interfaces, NULL);
portal_config->default_portal = g_steal_pointer (&default_portal);
return g_steal_pointer (&portal_config);
}
return NULL;
}
/*
* Returns: %TRUE if configuration was found in @dir
*/
static gboolean
load_config_directory (const char *dir,
const char **desktops,
gboolean opt_verbose)
{
g_autoptr(PortalConfig) conf = NULL;
for (size_t i = 0; desktops[i] != NULL; i++)
{
g_autofree char *portals_conf = g_strdup_printf ("%s-portals.conf", desktops[i]);
conf = load_portal_configuration_for_dir (opt_verbose, dir, portals_conf);
if (conf != NULL)
{
if (opt_verbose)
g_debug ("Using portal configuration file '%s/%s' for desktop '%s'",
dir, portals_conf, desktops[i]);
config = g_steal_pointer (&conf);
return TRUE;
}
}
conf = load_portal_configuration_for_dir (opt_verbose, dir, "portals.conf");
if (conf != NULL)
{
if (opt_verbose)
g_debug ("Using portal configuration file '%s/%s' for non-specific desktop",
dir, "portals.conf");
config = g_steal_pointer (&conf);
return TRUE;
}
return FALSE;
}
#define XDP_SUBDIR "xdg-desktop-portal"
void
load_portal_configuration (gboolean opt_verbose)
{
g_autofree char *user_portal_dir = NULL;
const char * const *dirs;
const char * const *iter;
const char **desktops;
const char *portal_dir;
desktops = get_current_lowercase_desktops ();
/* We need to override this in the tests */
portal_dir = g_getenv ("XDG_DESKTOP_PORTAL_DIR");
if (portal_dir != NULL)
{
load_config_directory (portal_dir, desktops, opt_verbose);
/* All other config directories are ignored when this is set */
return;
}
/* $XDG_CONFIG_HOME/xdg-desktop-portal/(DESKTOP-)portals.conf */
user_portal_dir = g_build_filename (g_get_user_config_dir (), XDP_SUBDIR, NULL);
if (load_config_directory (user_portal_dir, desktops, opt_verbose))
return;
/* $XDG_CONFIG_DIRS/xdg-desktop-portal/(DESKTOP-)portals.conf */
dirs = g_get_system_config_dirs ();
for (iter = dirs; iter != NULL && *iter != NULL; iter++)
{
g_autofree char *dir = g_build_filename (*iter, XDP_SUBDIR, NULL);
if (load_config_directory (dir, desktops, opt_verbose))
return;
}
/* ${sysconfdir}/xdg-desktop-portal/(DESKTOP-)portals.conf */
if (load_config_directory (SYSCONFDIR "/" XDP_SUBDIR, desktops, opt_verbose))
return;
/* $XDG_DATA_HOME/xdg-desktop-portal/(DESKTOP-)portals.conf
* (just for consistency with other XDG specifications) */
g_clear_pointer (&user_portal_dir, g_free);
user_portal_dir = g_build_filename (g_get_user_data_dir (), XDP_SUBDIR, NULL);
if (load_config_directory (user_portal_dir, desktops, opt_verbose))
return;
/* $XDG_DATA_DIRS/xdg-desktop-portal/(DESKTOP-)portals.conf */
dirs = g_get_system_data_dirs ();
for (iter = dirs; iter != NULL && *iter != NULL; iter++)
{
g_autofree char *dir = g_build_filename (*iter, XDP_SUBDIR, NULL);
if (load_config_directory (dir, desktops, opt_verbose))
return;
}
/* ${datadir}/xdg-desktop-portal/(DESKTOP-)portals.conf */
if (load_config_directory (DATADIR "/" XDP_SUBDIR, desktops, opt_verbose))
return;
}
PortalInterface *
find_matching_iface_config (const char *interface)
{
if (config == NULL)
return NULL;
for (size_t i = 0; i < config->n_ifaces; i++)
{
PortalInterface *iface = config->interfaces[i];
if (g_strcmp0 (iface->dbus_name, interface) == 0)
return iface;
}
return NULL;
}
static gboolean
portal_default_prefers_none (void)
{
if (config && config->default_portal &&
g_strv_contains ((const char * const *) config->default_portal->portals, "none"))
{
g_debug ("Found 'none' in configuration for default");
return TRUE;
}
return FALSE;
}
static gboolean
portal_interface_prefers_none (const char *interface)
{
const PortalInterface *iface = find_matching_iface_config (interface);
if (iface == NULL)
return portal_default_prefers_none ();
if (g_strv_contains ((const char * const *) iface->portals, "none"))
{
g_debug ("Found 'none' in configuration for %s", iface->dbus_name);
return TRUE;
}
return FALSE;
}
static gboolean
portal_impl_name_matches (const PortalImplementation *impl,
const PortalInterface *iface)
{
/* Exact match */
if (g_strv_contains ((const char * const *) iface->portals, impl->source))
{
g_debug ("Found '%s' in configuration for %s", impl->source, iface->dbus_name);
return TRUE;
}
/* The "*" alias means "any" */
if (g_strv_contains ((const char * const *) iface->portals, "*"))
{
g_debug ("Found '*' in configuration for %s", iface->dbus_name);
return TRUE;
}
/* No portal */
if (portal_interface_prefers_none (iface->dbus_name))
{
g_debug ("Found 'none' in configuration for %s", iface->dbus_name);
return FALSE;
}
return FALSE;
}
static gboolean
portal_impl_matches_config (const PortalImplementation *impl,
const char *interface)
{
if (config == NULL)
return FALSE;
/* Interfaces have precedence, followed by the "default" catch all,
* to allow for specific interfaces to override the default
*/
for (int i = 0; i < config->n_ifaces; i++)
{
const PortalInterface *iface = config->interfaces[i];
if (g_strcmp0 (iface->dbus_name, interface) == 0)
return portal_impl_name_matches (impl, iface);
}
if (config->default_portal)
return portal_impl_name_matches (impl, config->default_portal);
return FALSE;
}
static void
warn_please_use_portals_conf (void)
{
g_warning_once ("The preferred method to match portal implementations "
"to desktop environments is to use the portals.conf(5) "
"configuration file");
}
PortalImplementation *
find_portal_implementation (const char *interface)
{
const char **desktops;
GList *l;
int i;
if (portal_interface_prefers_none (interface))
return NULL;
for (l = implementations; l != NULL; l = l->next)
{
PortalImplementation *impl = l->data;
if (!g_strv_contains ((const char **)impl->interfaces, interface))
continue;
if (portal_impl_matches_config (impl, interface))
{
g_debug ("Using %s.portal for %s (config)", impl->source, interface);
return impl;
}
}
desktops = get_current_lowercase_desktops ();
/* Fallback to the old UseIn key */
for (i = 0; desktops[i] != NULL; i++)
{
for (l = implementations; l != NULL; l = l->next)
{
PortalImplementation *impl = l->data;
if (!g_strv_contains ((const char **)impl->interfaces, interface))
continue;
if (impl->use_in != NULL && g_strv_case_contains ((const char **)impl->use_in, desktops[i]))
{
g_warning ("Choosing %s.portal for %s via the deprecated UseIn key",
impl->source, interface);
warn_please_use_portals_conf ();
g_debug ("Using %s.portal for %s in %s (fallback)", impl->source, interface, desktops[i]);
return impl;
}
}
}
/* As a last resort, if nothing was selected for this desktop by
* ${desktop}-portals.conf or portals.conf, and no portal volunteered
* itself as suitable for this desktop via the legacy UseIn mechanism,
* try to fall back to x-d-p-gtk, which has historically been the portal
* UI backend used by desktop environments with no backend of their own.
* If it isn't installed, that is not an error: we just don't use it. */
for (l = implementations; l != NULL; l = l->next)
{
PortalImplementation *impl = l->data;
if (!g_str_equal (impl->dbus_name, "org.freedesktop.impl.portal.desktop.gtk"))
continue;
if (!g_strv_contains ((const char **)impl->interfaces, interface))
continue;
g_warning ("Choosing %s.portal for %s as a last-resort fallback",
impl->source, interface);
warn_please_use_portals_conf ();
return impl;
}
return NULL;
}
GPtrArray *
find_all_portal_implementations (const char *interface)
{
GPtrArray *impls;
GList *l;
impls = g_ptr_array_new ();
if (portal_interface_prefers_none (interface))
return impls;
for (l = implementations; l != NULL; l = l->next)
{
PortalImplementation *impl = l->data;
if (!g_strv_contains ((const char **)impl->interfaces, interface))
continue;
if (portal_impl_matches_config (impl, interface))
{
g_debug ("Using %s.portal for %s (config)", impl->source, interface);
g_ptr_array_add (impls, impl);
}
}
return impls;
}