/*
* e-source-registry-server.c
*
* This library 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.
*
* 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 .
*
*/
/**
* SECTION: e-source-registry-server
* @include: libebackend/libebackend.h
* @short_description: Server-side repository for data sources
*
* The #ESourceRegistryServer is the heart of the registry D-Bus service.
* Acting as a global singleton store for all #EServerSideSource instances,
* its responsibilities include loading data source content from key files,
* exporting data sources to clients over D-Bus, handling content change
* requests from clients, and saving content changes back to key files.
*
* It also hosts any number of built-in or 3rd party data source collection
* backends, which coordinate with #ESourceRegistryServer to automatically
* advertise available data sources on a remote server.
**/
#include "evolution-data-server-config.h"
#include
#include
/* Private D-Bus classes. */
#include "e-dbus-source.h"
#include "e-dbus-source-manager.h"
#include "e-server-side-source.h"
#include "e-server-side-source-credentials-provider.h"
#include "e-source-registry-server.h"
/* Collection backends get tacked on to
* sources with a [Collection] extension. */
#define BACKEND_DATA_KEY "__e_collection_backend__"
struct _ESourceRegistryServerPrivate {
GMainContext *main_context;
GDBusObjectManagerServer *object_manager;
EDBusSourceManager *source_manager;
GHashTable *sources; /* sources added to hierarchy */
GHashTable *orphans; /* sources waiting for parent */
GHashTable *monitors;
GMutex sources_lock;
GMutex orphans_lock;
ESourceCredentialsProvider *credentials_provider;
GMutex file_monitor_lock;
GHashTable *file_monitor_events; /* gchar *uid ~> FileEventData * */
GSource *file_monitor_source;
EOAuth2Services *oauth2_services;
};
enum {
LOAD_ERROR,
FILES_LOADED,
SOURCE_ADDED,
SOURCE_REMOVED,
TWEAK_KEY_FILE,
LAST_SIGNAL
};
static guint signals[LAST_SIGNAL];
static void e_source_registry_server_oauth2_support_init (EOAuth2SupportInterface *iface);
G_DEFINE_TYPE_WITH_CODE (ESourceRegistryServer, e_source_registry_server, E_TYPE_DATA_FACTORY,
G_ADD_PRIVATE (ESourceRegistryServer)
G_IMPLEMENT_INTERFACE (E_TYPE_OAUTH2_SUPPORT, e_source_registry_server_oauth2_support_init))
/* GDestroyNotify callback for 'sources' values */
static void
unref_data_source (ESource *source)
{
/* The breaks the reference cycle with ECollectionBackend. */
g_object_set_data (G_OBJECT (source), BACKEND_DATA_KEY, NULL);
g_object_unref (source);
}
static void
source_registry_server_sources_insert (ESourceRegistryServer *server,
ESource *source)
{
const gchar *uid;
uid = e_source_get_uid (source);
g_return_if_fail (uid != NULL);
g_mutex_lock (&server->priv->sources_lock);
g_hash_table_insert (
server->priv->sources,
g_strdup (uid), g_object_ref (source));
g_mutex_unlock (&server->priv->sources_lock);
}
static gboolean
source_registry_server_sources_remove (ESourceRegistryServer *server,
ESource *source)
{
const gchar *uid;
gboolean removed;
uid = e_source_get_uid (source);
g_return_val_if_fail (uid != NULL, FALSE);
g_mutex_lock (&server->priv->sources_lock);
removed = g_hash_table_remove (server->priv->sources, uid);
g_mutex_unlock (&server->priv->sources_lock);
return removed;
}
static ESource *
source_registry_server_sources_lookup (ESourceRegistryServer *server,
const gchar *uid)
{
ESource *source;
g_return_val_if_fail (uid != NULL, NULL);
g_mutex_lock (&server->priv->sources_lock);
source = g_hash_table_lookup (server->priv->sources, uid);
if (source != NULL)
g_object_ref (source);
g_mutex_unlock (&server->priv->sources_lock);
return source;
}
static GList *
source_registry_server_sources_get_values (ESourceRegistryServer *server)
{
GList *values;
g_mutex_lock (&server->priv->sources_lock);
values = g_hash_table_get_values (server->priv->sources);
g_list_foreach (values, (GFunc) g_object_ref, NULL);
g_mutex_unlock (&server->priv->sources_lock);
return values;
}
static void
source_registry_server_orphans_insert (ESourceRegistryServer *server,
ESource *orphan_source)
{
GHashTable *orphans;
GPtrArray *array;
gchar *parent_uid;
g_mutex_lock (&server->priv->orphans_lock);
orphans = server->priv->orphans;
parent_uid = e_source_dup_parent (orphan_source);
/* A top-level object has no parent UID, so we
* use a special "empty" key in the hash table. */
if (parent_uid == NULL)
parent_uid = g_strdup ("");
array = g_hash_table_lookup (orphans, parent_uid);
if (array == NULL) {
array = g_ptr_array_new_with_free_func (g_object_unref);
/* Takes ownership of the 'parent_uid' string. */
g_hash_table_insert (orphans, parent_uid, array);
parent_uid = NULL;
}
g_ptr_array_add (array, g_object_ref (orphan_source));
g_free (parent_uid);
g_mutex_unlock (&server->priv->orphans_lock);
}
static gboolean
source_registry_server_orphans_remove (ESourceRegistryServer *server,
ESource *orphan_source)
{
GHashTable *orphans;
GPtrArray *array;
gchar *parent_uid;
gboolean removed = FALSE;
g_mutex_lock (&server->priv->orphans_lock);
orphans = server->priv->orphans;
parent_uid = e_source_dup_parent (orphan_source);
/* A top-level object has no parent UID, so we
* use a special "empty" key in the hash table. */
if (parent_uid == NULL)
parent_uid = g_strdup ("");
array = g_hash_table_lookup (orphans, parent_uid);
if (array != NULL) {
/* Array is not ordered, so use "remove_fast". */
removed = g_ptr_array_remove_fast (array, orphan_source);
}
g_free (parent_uid);
g_mutex_unlock (&server->priv->orphans_lock);
return removed;
}
static GPtrArray *
source_registry_server_orphans_steal (ESourceRegistryServer *server,
ESource *parent_source)
{
GHashTable *orphans;
GPtrArray *array;
const gchar *parent_uid;
parent_uid = e_source_get_uid (parent_source);
g_return_val_if_fail (parent_uid != NULL, NULL);
g_mutex_lock (&server->priv->orphans_lock);
orphans = server->priv->orphans;
array = g_hash_table_lookup (orphans, parent_uid);
/* g_hash_table_remove() will unreference the array,
* so we need to reference it first to keep it alive. */
if (array != NULL) {
g_ptr_array_ref (array);
g_hash_table_remove (orphans, parent_uid);
}
g_mutex_unlock (&server->priv->orphans_lock);
return array;
}
static gboolean
source_registry_server_create_source (ESourceRegistryServer *server,
const gchar *uid,
const gchar *data,
GError **error)
{
ESource *source = NULL;
GFile *file;
GFile *parent;
GKeyFile *key_file;
gboolean success;
gsize length;
GError *local_error = NULL;
g_return_val_if_fail (uid != NULL, FALSE);
g_return_val_if_fail (data != NULL, FALSE);
length = strlen (data);
/* Make sure the data is syntactically valid. */
key_file = g_key_file_new ();
success = g_key_file_load_from_data (
key_file, data, length, G_KEY_FILE_NONE, error);
g_key_file_free (key_file);
if (!success)
return FALSE;
/* Check that the given unique identifier really is unique.
*
* XXX There's a valid case to be made that the server should be
* assigning unique identifiers to new sources to avoid this
* error. That's fine for standalone sources but makes life
* more difficult for clients creating a set or hierarchy of
* sources that cross reference one another, such for a mail
* account. Having CLIENTS generate new UIDs means they can
* prepare any cross references in advance, then submit each
* source as is without having to make further modifications
* as would be necessary if using server-assigned UIDs.
*
* Anyway, if used properly the odds of a UID collision here
* are slim enough that I think it's a reasonable trade-off.
*/
source = e_source_registry_server_ref_source (server, uid);
if (source != NULL) {
g_set_error (
error, G_IO_ERROR, G_IO_ERROR_EXISTS,
_("UID “%s” is already in use"), uid);
g_object_unref (source);
return FALSE;
}
file = e_server_side_source_new_user_file (uid);
/* Create the directory where we'll be writing. */
parent = g_file_get_parent (file);
g_file_make_directory_with_parents (parent, NULL, &local_error);
g_object_unref (parent);
if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_EXISTS))
g_clear_error (&local_error);
if (local_error != NULL) {
g_propagate_error (error, local_error);
success = FALSE;
}
/* Write the data to disk. The file monitor should eventually
* notice the new file and call e_source_registry_server_load_file()
* per design, but we're going to beat it to the punch since we
* need to return the new D-Bus object path back to the caller.
* By the time the file monitor gets around to loading the file,
* it will simply get back the EDBusSourceObject we've already
* created and exported. */
if (success)
success = g_file_replace_contents (
file, data, length, NULL, FALSE,
G_FILE_CREATE_PRIVATE, NULL, NULL, error);
if (success) {
ESourcePermissionFlags flags;
/* New sources are always writable + removable. */
flags = E_SOURCE_PERMISSION_WRITABLE |
E_SOURCE_PERMISSION_REMOVABLE;
source = e_source_registry_server_load_file (
server, file, flags, error);
/* We don't need the returned reference. */
if (source != NULL)
g_object_unref (source);
else
success = FALSE;
}
g_object_unref (file);
return success;
}
static gboolean
source_registry_server_create_sources_cb (EDBusSourceManager *dbus_interface,
GDBusMethodInvocation *invocation,
GVariant *array,
ESourceRegistryServer *server)
{
GVariantIter iter;
gchar *uid, *data;
GError *error = NULL;
g_variant_iter_init (&iter, array);
while (g_variant_iter_next (&iter, "{ss}", &uid, &data)) {
source_registry_server_create_source (
server, uid, data, &error);
g_free (uid);
g_free (data);
if (error != NULL)
break;
}
if (error != NULL)
g_dbus_method_invocation_take_error (invocation, error);
else
e_dbus_source_manager_complete_create_sources (
dbus_interface, invocation);
return TRUE;
}
static gboolean
source_registry_server_reload_cb (EDBusSourceManager *dbus_interface,
GDBusMethodInvocation *invocation,
ESourceRegistryServer *server)
{
e_dbus_server_quit (
E_DBUS_SERVER (server),
E_DBUS_SERVER_EXIT_RELOAD);
e_dbus_source_manager_complete_reload (dbus_interface, invocation);
return TRUE;
}
static gboolean
source_registry_server_refresh_backend_cb (EDBusSourceManager *dbus_interface,
GDBusMethodInvocation *invocation,
const gchar *source_uid,
ESourceRegistryServer *server)
{
ESource *source;
GError *error = NULL;
g_return_val_if_fail (E_IS_SOURCE_REGISTRY_SERVER (server), FALSE);
g_return_val_if_fail (source_uid != NULL, FALSE);
source = e_source_registry_server_ref_source (server, source_uid);
if (source) {
if (e_source_has_extension (source, E_SOURCE_EXTENSION_COLLECTION)) {
ECollectionBackend *backend;
backend = e_source_registry_server_ref_backend (server, source);
if (backend) {
e_collection_backend_schedule_populate (backend);
g_object_unref (backend);
} else {
error = g_error_new (G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
_("Cannot find corresponding collection backend for source “%s”"), source_uid);
}
} else {
error = g_error_new (G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
_("Source “%s” is not a collection source"), source_uid);
}
g_object_unref (source);
} else {
error = g_error_new (G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
_("Cannot find source “%s”"), source_uid);
}
if (error)
g_dbus_method_invocation_take_error (invocation, error);
else
e_dbus_source_manager_complete_refresh_backend (dbus_interface, invocation);
return TRUE;
}
typedef struct _FileEventData {
GFile *file;
GFileMonitorEvent event_type;
} FileEventData;
static FileEventData *
file_event_data_new (GFile *file,
GFileMonitorEvent event_type)
{
FileEventData *fed;
fed = g_slice_new0 (FileEventData);
fed->file = g_object_ref (file);
fed->event_type = event_type;
return fed;
}
static void
file_event_data_free (gpointer ptr)
{
FileEventData *fed = ptr;
if (fed) {
g_clear_object (&fed->file);
g_slice_free (FileEventData, fed);
}
}
static void
source_registry_server_process_file_monitor_event (gpointer key,
gpointer value,
gpointer user_data)
{
const gchar *uid = key;
const FileEventData *fed = value;
ESourceRegistryServer *server = user_data;
GFileMonitorEvent event_type;
g_return_if_fail (uid != NULL);
g_return_if_fail (fed != NULL);
event_type = fed->event_type;
if (e_source_registry_debug_enabled ()) {
e_source_registry_debug_print ("Processing file monitor event %s (%u) for UID: %s\n",
event_type == G_FILE_MONITOR_EVENT_CHANGED ? "CHANGED" :
event_type == G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT ? "CHANGES_DONE_HINT" :
event_type == G_FILE_MONITOR_EVENT_DELETED ? "DELETED" :
event_type == G_FILE_MONITOR_EVENT_CREATED ? "CREATED" :
event_type == G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED ? "ATTRIBUTE_CHANGED" :
event_type == G_FILE_MONITOR_EVENT_PRE_UNMOUNT ? "PRE_UNMOUNT" :
event_type == G_FILE_MONITOR_EVENT_UNMOUNTED ? "UNMOUNTED" :
event_type == G_FILE_MONITOR_EVENT_MOVED ? "MOVED" : "???",
event_type,
uid);
}
if (event_type == G_FILE_MONITOR_EVENT_CHANGED ||
event_type == G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT) {
ESource *source;
GError *error = NULL;
source = e_source_registry_server_ref_source (server, uid);
/* If the source does not exist, create it; parsing may have
* failed when the file was originally created. This can happen
* if the file is created (empty), then e-source-registry-server
* detects it, then it’s populated and made valid.
*
* Otherwise, reload the file since it has changed. */
if (source == NULL) {
event_type = G_FILE_MONITOR_EVENT_CREATED;
} else if (!e_server_side_source_load (E_SERVER_SIDE_SOURCE (source), NULL, &error)) {
g_warning ("Error reloading source ‘%s’: %s", uid, error->message);
g_error_free (error);
g_object_unref (source);
return;
}
g_clear_object (&source);
}
if (event_type == G_FILE_MONITOR_EVENT_CREATED) {
ESource *source;
GError *error = NULL;
source = e_source_registry_server_ref_source (server, uid);
if (!source) {
/* it can return NULL source for hidden files */
source = e_server_side_source_new (server, fed->file, &error);
}
if (!error && source) {
/* File monitors are only placed on directories
* where data sources are writable and removable,
* so it should be safe to assume these flags. */
e_server_side_source_set_writable (E_SERVER_SIDE_SOURCE (source), TRUE);
e_server_side_source_set_removable (E_SERVER_SIDE_SOURCE (source), TRUE);
e_source_registry_server_add_source (server, source);
} else if (error) {
e_source_registry_server_load_error (server, fed->file, error);
g_error_free (error);
}
g_clear_object (&source);
}
if (event_type == G_FILE_MONITOR_EVENT_DELETED) {
ESource *source;
source = e_source_registry_server_ref_source (server, uid);
if (source == NULL)
return;
/* If the key file for a non-removable source was
* somehow deleted, disregard the event and leave
* the source object in memory. */
if (e_source_get_removable (source))
e_source_registry_server_remove_source (server, source);
g_object_unref (source);
}
}
static gboolean
source_registry_server_process_file_monitor_events_cb (gpointer user_data)
{
ESourceRegistryServer *server = user_data;
GHashTable *events;
if (g_source_is_destroyed (g_main_current_source ()))
return FALSE;
g_return_val_if_fail (E_IS_SOURCE_REGISTRY_SERVER (server), FALSE);
g_mutex_lock (&server->priv->file_monitor_lock);
events = server->priv->file_monitor_events;
server->priv->file_monitor_events = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, file_event_data_free);
g_mutex_unlock (&server->priv->file_monitor_lock);
g_hash_table_foreach (events, source_registry_server_process_file_monitor_event, server);
g_hash_table_destroy (events);
return FALSE;
}
static void
source_registry_server_monitor_changed_cb (GFileMonitor *monitor,
GFile *file,
GFile *other_file,
GFileMonitorEvent event_type,
ESourceRegistryServer *server)
{
if (e_source_registry_debug_enabled ()) {
gchar *uri;
uri = g_file_get_uri (file);
e_source_registry_debug_print ("Handling file monitor event %s (%u) for URI: %s\n",
event_type == G_FILE_MONITOR_EVENT_CHANGED ? "CHANGED" :
event_type == G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT ? "CHANGES_DONE_HINT" :
event_type == G_FILE_MONITOR_EVENT_DELETED ? "DELETED" :
event_type == G_FILE_MONITOR_EVENT_CREATED ? "CREATED" :
event_type == G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED ? "ATTRIBUTE_CHANGED" :
event_type == G_FILE_MONITOR_EVENT_PRE_UNMOUNT ? "PRE_UNMOUNT" :
event_type == G_FILE_MONITOR_EVENT_UNMOUNTED ? "UNMOUNTED" :
event_type == G_FILE_MONITOR_EVENT_MOVED ? "MOVED" : "???",
event_type,
uri);
g_free (uri);
}
if (event_type == G_FILE_MONITOR_EVENT_CHANGED ||
event_type == G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT ||
event_type == G_FILE_MONITOR_EVENT_CREATED ||
event_type == G_FILE_MONITOR_EVENT_DELETED) {
gchar *uid;
uid = e_server_side_source_uid_from_file (file, NULL);
if (uid == NULL)
return;
g_mutex_lock (&server->priv->file_monitor_lock);
/* This overwrites any previous events, aka the last wins
(overwrite can be DELETE + CREATE, which handles it correctly). */
g_hash_table_insert (server->priv->file_monitor_events, uid, file_event_data_new (file, event_type));
if (server->priv->file_monitor_source) {
g_source_destroy (server->priv->file_monitor_source);
g_source_unref (server->priv->file_monitor_source);
}
server->priv->file_monitor_source = g_timeout_source_new_seconds (3);
g_source_set_callback (
server->priv->file_monitor_source,
source_registry_server_process_file_monitor_events_cb,
server, NULL);
g_source_attach (
server->priv->file_monitor_source,
server->priv->main_context);
g_mutex_unlock (&server->priv->file_monitor_lock);
}
}
static gboolean
source_registry_server_traverse_cb (GNode *node,
GQueue *queue)
{
g_queue_push_tail (queue, g_object_ref (node->data));
return FALSE;
}
static void
source_registry_server_queue_subtree (ESource *source,
GQueue *queue)
{
GNode *node;
node = e_server_side_source_get_node (E_SERVER_SIDE_SOURCE (source));
g_node_traverse (
node, G_POST_ORDER, G_TRAVERSE_ALL, -1,
(GNodeTraverseFunc) source_registry_server_traverse_cb, queue);
}
static gboolean
source_registry_server_find_parent (ESourceRegistryServer *server,
ESource *source)
{
ESource *parent;
const gchar *parent_uid;
/* If the given source references a parent source and the
* parent source is not present in the hierarchy, the given
* source is added to an orphan table until the referenced
* parent is added to the hierarchy. */
parent_uid = e_source_get_parent (source);
if (parent_uid == NULL || *parent_uid == '\0')
return TRUE;
parent = g_hash_table_lookup (server->priv->sources, parent_uid);
if (parent != NULL) {
GNode *parent_node;
GNode *object_node;
parent_node = e_server_side_source_get_node (
E_SERVER_SIDE_SOURCE (parent));
object_node = e_server_side_source_get_node (
E_SERVER_SIDE_SOURCE (source));
g_node_append (parent_node, object_node);
return TRUE;
}
source_registry_server_orphans_insert (server, source);
return FALSE;
}
static void
source_registry_server_adopt_orphans (ESourceRegistryServer *server,
ESource *source)
{
GPtrArray *array;
/* Check if a newly-added source has any orphan sources
* that are waiting for it. The orphans can now be added
* to the hierarchy as children of the newly-added source. */
array = source_registry_server_orphans_steal (server, source);
if (array != NULL) {
guint ii;
for (ii = 0; ii < array->len; ii++) {
ESource *orphan = array->pdata[ii];
e_source_registry_server_add_source (server, orphan);
}
g_ptr_array_unref (array);
}
}
static GObject *server_singleton = NULL;
G_LOCK_DEFINE_STATIC (server_singleton);
static void
server_singleton_weak_ref_cb (gpointer user_data,
GObject *object)
{
G_LOCK (server_singleton);
g_warn_if_fail (object == server_singleton);
server_singleton = NULL;
G_UNLOCK (server_singleton);
}
static GObject *
source_registry_server_constructor (GType type,
guint n_construct_params,
GObjectConstructParam *construct_params)
{
GObject *object;
G_LOCK (server_singleton);
if (server_singleton) {
object = g_object_ref (server_singleton);
} else {
object = G_OBJECT_CLASS (e_source_registry_server_parent_class)->constructor (type, n_construct_params, construct_params);
if (object)
g_object_weak_ref (object, server_singleton_weak_ref_cb, NULL);
server_singleton = object;
}
G_UNLOCK (server_singleton);
return object;
}
static void
source_registry_server_constructed (GObject *object)
{
ESourceRegistryServer *server;
server = E_SOURCE_REGISTRY_SERVER (object);
/* Chain up to parent's method. */
G_OBJECT_CLASS (e_source_registry_server_parent_class)->constructed (object);
server->priv->credentials_provider = e_server_side_source_credentials_provider_new (server);
server->priv->oauth2_services = e_oauth2_services_new ();
}
static void
source_registry_server_dispose (GObject *object)
{
ESourceRegistryServerPrivate *priv;
priv = E_SOURCE_REGISTRY_SERVER (object)->priv;
g_mutex_lock (&priv->file_monitor_lock);
if (priv->file_monitor_source) {
g_source_destroy (priv->file_monitor_source);
g_source_unref (priv->file_monitor_source);
priv->file_monitor_source = NULL;
}
g_mutex_unlock (&priv->file_monitor_lock);
g_clear_pointer (&priv->main_context, g_main_context_unref);
g_clear_object (&priv->object_manager);
g_clear_object (&priv->source_manager);
g_clear_object (&priv->credentials_provider);
g_hash_table_remove_all (priv->sources);
g_hash_table_remove_all (priv->orphans);
g_hash_table_remove_all (priv->monitors);
g_hash_table_remove_all (priv->file_monitor_events);
/* Chain up to parent's dispose() method. */
G_OBJECT_CLASS (e_source_registry_server_parent_class)->
dispose (object);
}
static void
source_registry_server_finalize (GObject *object)
{
ESourceRegistryServerPrivate *priv;
priv = E_SOURCE_REGISTRY_SERVER (object)->priv;
g_hash_table_destroy (priv->sources);
g_hash_table_destroy (priv->orphans);
g_hash_table_destroy (priv->monitors);
g_hash_table_destroy (priv->file_monitor_events);
g_mutex_clear (&priv->sources_lock);
g_mutex_clear (&priv->orphans_lock);
g_mutex_clear (&priv->file_monitor_lock);
g_clear_object (&priv->oauth2_services);
/* Chain up to parent's finalize() method. */
G_OBJECT_CLASS (e_source_registry_server_parent_class)->
finalize (object);
}
static void
source_registry_server_bus_acquired (EDBusServer *server,
GDBusConnection *connection)
{
ESourceRegistryServerPrivate *priv;
priv = E_SOURCE_REGISTRY_SERVER (server)->priv;
g_dbus_object_manager_server_set_connection (
priv->object_manager, connection);
/* Chain up to parent's bus_acquired() method. */
E_DBUS_SERVER_CLASS (e_source_registry_server_parent_class)->
bus_acquired (server, connection);
}
static void
source_registry_server_quit_server (EDBusServer *server,
EDBusServerExitCode code)
{
ESourceRegistryServerPrivate *priv;
priv = E_SOURCE_REGISTRY_SERVER (server)->priv;
/* This makes the object manager unexport all objects. */
g_dbus_object_manager_server_set_connection (
priv->object_manager, NULL);
/* Chain up to parent's quit_server() method. */
E_DBUS_SERVER_CLASS (e_source_registry_server_parent_class)->
quit_server (server, code);
}
static void
source_registry_server_source_added (ESourceRegistryServer *server,
ESource *source)
{
GDBusObject *dbus_object;
GDBusObject *g_dbus_object;
const gchar *uid;
const gchar *object_name;
const gchar *object_path;
const gchar *extension_name;
/* Instantiate an ECollectionBackend if appropriate.
*
* Do this BEFORE exporting so backends have a chance
* to make any last-minute tweaks to the data source. */
extension_name = E_SOURCE_EXTENSION_COLLECTION;
if (e_source_has_extension (source, extension_name)) {
EBackend *backend = NULL;
ECollectionBackendFactory *backend_factory;
ESourceBackend *extension;
const gchar *backend_name;
GError *error = NULL;
extension = e_source_get_extension (source, extension_name);
backend_name = e_source_backend_get_backend_name (extension);
/* For convenience, we attach the EBackend to the ESource
* itself, which creates a reference cycle. The cycle is
* explicitly broken when the ESource is removed from the
* 'sources' hash table (see unref_data_source() above). */
backend_factory = e_source_registry_server_ref_backend_factory (server, source);
backend = e_backend_factory_new_backend (E_BACKEND_FACTORY (backend_factory), source);
if (G_IS_INITABLE (backend)) {
GInitable *initable = G_INITABLE (backend);
if (!g_initable_init (initable, NULL, &error))
g_clear_object (&backend);
}
g_object_unref (backend_factory);
if (backend != NULL) {
g_object_set_data_full (
G_OBJECT (source),
BACKEND_DATA_KEY, backend,
(GDestroyNotify) g_object_unref);
} else {
g_warning (
"No collection backend '%s' for %s: %s",
backend_name, e_source_get_uid (source),
error ? error->message : "Unknown error");
g_clear_error (&error);
}
}
/* Export the data source to clients over D-Bus. */
dbus_object = e_source_ref_dbus_object (source);
g_dbus_object_manager_server_export_uniquely (
server->priv->object_manager,
G_DBUS_OBJECT_SKELETON (dbus_object));
g_object_notify (G_OBJECT (source), "exported");
uid = e_source_get_uid (source);
g_dbus_object = G_DBUS_OBJECT (dbus_object);
object_path = g_dbus_object_get_object_path (g_dbus_object);
object_name = strrchr (object_path, '/') + 1;
g_debug ("Adding %s ('%s')", uid, object_name);
g_object_unref (dbus_object);
}
static void
source_registry_server_source_removed (ESourceRegistryServer *server,
ESource *source)
{
GDBusObject *dbus_object;
const gchar *uid;
const gchar *object_name;
const gchar *object_path;
uid = e_source_get_uid (source);
dbus_object = e_source_ref_dbus_object (source);
object_path = g_dbus_object_get_object_path (dbus_object);
object_name = strrchr (object_path, '/') + 1;
e_source_registry_debug_print ("Removing %s ('%s')\n", uid, object_name);
g_dbus_object_manager_server_unexport (
server->priv->object_manager, object_path);
g_object_notify (G_OBJECT (source), "exported");
g_object_unref (dbus_object);
}
static gboolean
source_registry_server_any_true (GSignalInvocationHint *ihint,
GValue *return_accu,
const GValue *handler_return,
gpointer unused)
{
if (g_value_get_boolean (handler_return))
g_value_set_boolean (return_accu, TRUE);
return TRUE;
}
static gboolean
e_source_registry_server_get_access_token_sync (EOAuth2Support *support,
ESource *source,
GCancellable *cancellable,
gchar **out_access_token,
gint *out_expires_in,
GError **error)
{
EOAuth2ServiceRefSourceFunc ref_source;
ESourceRegistryServer *server;
EOAuth2Service *service;
gboolean success;
g_return_val_if_fail (E_IS_SOURCE_REGISTRY_SERVER (support), FALSE);
g_return_val_if_fail (E_IS_SOURCE (source), FALSE);
server = E_SOURCE_REGISTRY_SERVER (support);
service = e_oauth2_services_find (server->priv->oauth2_services, source);
if (!service) {
g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
_("Data source “%s” does not support OAuth 2.0 authentication"),
e_source_get_display_name (source));
return FALSE;
}
ref_source = (EOAuth2ServiceRefSourceFunc) e_source_registry_server_ref_source;
success = e_oauth2_service_get_access_token_sync (service, source, ref_source, server,
out_access_token, out_expires_in, cancellable, error);
g_clear_object (&service);
return success;
}
static GDBusInterfaceSkeleton *
source_registry_server_get_dbus_interface_skeleton (EDBusServer *server)
{
ESourceRegistryServerPrivate *priv;
priv = E_SOURCE_REGISTRY_SERVER (server)->priv;
return G_DBUS_INTERFACE_SKELETON (priv->source_manager);
}
static void
e_source_registry_server_class_init (ESourceRegistryServerClass *class)
{
GObjectClass *object_class;
EDBusServerClass *dbus_server_class;
EDataFactoryClass *data_factory_class;
GType backend_factory_type;
const gchar *modules_directory = MODULE_DIRECTORY;
const gchar *modules_directory_env;
modules_directory_env = g_getenv (EDS_REGISTRY_MODULES);
if (modules_directory_env &&
g_file_test (modules_directory_env, G_FILE_TEST_IS_DIR))
modules_directory = g_strdup (modules_directory_env);
object_class = G_OBJECT_CLASS (class);
object_class->constructor = source_registry_server_constructor;
object_class->constructed = source_registry_server_constructed;
object_class->dispose = source_registry_server_dispose;
object_class->finalize = source_registry_server_finalize;
dbus_server_class = E_DBUS_SERVER_CLASS (class);
dbus_server_class->bus_name = SOURCES_DBUS_SERVICE_NAME;
dbus_server_class->module_directory = modules_directory;
dbus_server_class->bus_acquired = source_registry_server_bus_acquired;
dbus_server_class->quit_server = source_registry_server_quit_server;
data_factory_class = E_DATA_FACTORY_CLASS (class);
backend_factory_type = E_TYPE_COLLECTION_BACKEND_FACTORY;
data_factory_class->backend_factory_type = backend_factory_type;
data_factory_class->factory_object_path = E_SOURCE_REGISTRY_SERVER_OBJECT_PATH;
data_factory_class->data_object_path_prefix = E_SOURCE_REGISTRY_SERVER_OBJECT_PATH;
data_factory_class->get_dbus_interface_skeleton = source_registry_server_get_dbus_interface_skeleton;
class->source_added = source_registry_server_source_added;
class->source_removed = source_registry_server_source_removed;
/**
* ESourceRegistryServer::load-error:
* @server: the #ESourceRegistryServer which emitted the signal
* @file: the #GFile being loaded
* @error: a #GError describing the error
*
* Emitted when an error occurs while loading or parsing a
* data source key file.
**/
signals[LOAD_ERROR] = g_signal_new (
"load-error",
G_OBJECT_CLASS_TYPE (object_class),
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (ESourceRegistryServerClass, load_error),
NULL, NULL, NULL,
G_TYPE_NONE, 2,
G_TYPE_FILE,
G_TYPE_ERROR | G_SIGNAL_TYPE_STATIC_SCOPE);
/**
* ESourceRegistryServer::files-loaded:
* @server: the #ESourceRegistryServer which emitted the signal
*
* Emitted after all data source key files are loaded on startup.
* Extensions can connect to this signal to perform any additional
* work prior to running the main loop.
**/
signals[FILES_LOADED] = g_signal_new (
"files-loaded",
G_OBJECT_CLASS_TYPE (object_class),
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (ESourceRegistryServerClass, files_loaded),
NULL, NULL, NULL,
G_TYPE_NONE, 0);
/**
* ESourceRegistryServer::source-added:
* @server: the #ESourceRegistryServer which emitted the signal
* @source: the newly-added #EServerSideSource
*
* Emitted when an #EServerSideSource is added to @server.
**/
signals[SOURCE_ADDED] = g_signal_new (
"source-added",
G_OBJECT_CLASS_TYPE (object_class),
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (ESourceRegistryServerClass, source_added),
NULL, NULL, NULL,
G_TYPE_NONE, 1,
E_TYPE_SERVER_SIDE_SOURCE);
/**
* ESourceRegistryServer::source-removed:
* @server: the #ESourceRegistryServer which emitted the signal
* @source: the #EServerSideSource that got removed
*
* Emitted when an #EServerSideSource is removed from @server.
**/
signals[SOURCE_REMOVED] = g_signal_new (
"source-removed",
G_OBJECT_CLASS_TYPE (object_class),
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (ESourceRegistryServerClass, source_removed),
NULL, NULL, NULL,
G_TYPE_NONE, 1,
E_TYPE_SERVER_SIDE_SOURCE);
/**
* ESourceRegistryServer::tweak-key-file:
* @server: the #ESourceRegistryServer which emitted the signal
* @key_file: a #GKeyFile
* @uid: a unique identifier string for @key_file
*
* Emitted from e_source_registry_server_load_file() just prior
* to instantiating an #EServerSideSource. Signal handlers can
* tweak the @key_file content as necessary and return %TRUE to
* write the modified content back to disk.
*
* For the purposes of tweaking, it's easier to deal with a plain
* #GKeyFile than an #ESource instance. An #ESource, for example,
* does not allow key file groups to be removed.
*
* The return value is cumulative. If any signal handler returns
* %TRUE, the @key_file content is written back to disk.
*
* Returns: %TRUE if @key_file was modified, %FALSE otherwise
*
* Since: 3.8
**/
signals[TWEAK_KEY_FILE] = g_signal_new (
"tweak-key-file",
G_OBJECT_CLASS_TYPE (object_class),
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (ESourceRegistryServerClass, tweak_key_file),
source_registry_server_any_true, NULL,
NULL,
G_TYPE_BOOLEAN, 2,
G_TYPE_KEY_FILE,
G_TYPE_STRING);
}
static void
e_source_registry_server_oauth2_support_init (EOAuth2SupportInterface *iface)
{
iface->get_access_token_sync = e_source_registry_server_get_access_token_sync;
}
static void
e_source_registry_server_init (ESourceRegistryServer *server)
{
GDBusObjectManagerServer *object_manager;
EDBusSourceManager *source_manager;
GHashTable *sources;
GHashTable *orphans;
GHashTable *monitors;
const gchar *object_path;
object_path = E_SOURCE_REGISTRY_SERVER_OBJECT_PATH;
object_manager = g_dbus_object_manager_server_new (object_path);
source_manager = e_dbus_source_manager_skeleton_new ();
/* UID string -> ESource */
sources = g_hash_table_new_full (
(GHashFunc) g_str_hash,
(GEqualFunc) g_str_equal,
(GDestroyNotify) g_free,
(GDestroyNotify) unref_data_source);
/* Parent UID string -> GPtrArray of ESources */
orphans = g_hash_table_new_full (
(GHashFunc) g_str_hash,
(GEqualFunc) g_str_equal,
(GDestroyNotify) g_free,
(GDestroyNotify) g_ptr_array_unref);
/* GFile -> GFileMonitor */
monitors = g_hash_table_new_full (
(GHashFunc) g_file_hash,
(GEqualFunc) g_file_equal,
(GDestroyNotify) g_object_unref,
(GDestroyNotify) g_object_unref);
server->priv = e_source_registry_server_get_instance_private (server);
server->priv->main_context = g_main_context_ref_thread_default ();
server->priv->object_manager = object_manager;
server->priv->source_manager = source_manager;
server->priv->sources = sources;
server->priv->orphans = orphans;
server->priv->monitors = monitors;
g_mutex_init (&server->priv->sources_lock);
g_mutex_init (&server->priv->orphans_lock);
g_mutex_init (&server->priv->file_monitor_lock);
server->priv->file_monitor_source = NULL;
server->priv->file_monitor_events = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, file_event_data_free);
g_signal_connect (
source_manager, "handle-create-sources",
G_CALLBACK (source_registry_server_create_sources_cb),
server);
g_signal_connect (
source_manager, "handle-reload",
G_CALLBACK (source_registry_server_reload_cb),
server);
g_signal_connect (
source_manager, "handle-refresh-backend",
G_CALLBACK (source_registry_server_refresh_backend_cb),
server);
}
/**
* e_source_registry_server_new:
*
* Creates a new instance of #ESourceRegistryServer.
*
* Returns: a new instance of #ESourceRegistryServer
*
* Since: 3.6
**/
EDBusServer *
e_source_registry_server_new (void)
{
return g_object_new (E_TYPE_SOURCE_REGISTRY_SERVER, "reload-supported", TRUE, NULL);
}
/**
* e_source_registry_server_ref_credentials_provider:
* @server: an #ESourceRegistryServer
*
* Returns a referenced #ESourceCredentialsProvider. Unref it with
* g_object_unref(), when no longer needed.
*
* Returns: (transfer full): A referenced #ESourceCredentialsProvider.
*
* Since: 3.16
**/
ESourceCredentialsProvider *
e_source_registry_server_ref_credentials_provider (ESourceRegistryServer *server)
{
g_return_val_if_fail (E_IS_SOURCE_REGISTRY_SERVER (server), NULL);
return g_object_ref (server->priv->credentials_provider);
}
/**
* e_source_registry_server_get_oauth2_services:
* @server: an #ESourceRegistryServer
*
* Returns: (transfer none): an #EOAuth2Services instance owned by @server
*
* Since: 3.28
**/
EOAuth2Services *
e_source_registry_server_get_oauth2_services (ESourceRegistryServer *server)
{
g_return_val_if_fail (E_IS_SOURCE_REGISTRY_SERVER (server), NULL);
return server->priv->oauth2_services;
}
/**
* e_source_registry_server_add_source:
* @server: an #ESourceRegistryServer
* @source: an #ESource
*
* Adds @source to @server.
*
* Since: 3.6
**/
void
e_source_registry_server_add_source (ESourceRegistryServer *server,
ESource *source)
{
GDBusObject *dbus_object;
EDBusSource *dbus_source;
const gchar *extension_name;
const gchar *uid;
gchar *data;
g_return_if_fail (E_IS_SOURCE_REGISTRY_SERVER (server));
g_return_if_fail (E_IS_SERVER_SIDE_SOURCE (source));
uid = e_source_get_uid (source);
g_return_if_fail (uid != NULL);
g_mutex_lock (&server->priv->sources_lock);
/* Check if we already have this object in the hierarchy. */
if (g_hash_table_lookup (server->priv->sources, uid) != NULL) {
g_mutex_unlock (&server->priv->sources_lock);
return;
}
/* Make sure the parent object (if any) is in the hierarchy. */
if (!source_registry_server_find_parent (server, source)) {
g_mutex_unlock (&server->priv->sources_lock);
return;
}
g_mutex_unlock (&server->priv->sources_lock);
/* Before we emit, make sure the EDBusSource's "data" property
* is up-to-date. ESource changes get propagated to the "data"
* property from an idle callback, which may still be pending. */
dbus_object = e_source_ref_dbus_object (source);
dbus_source = e_dbus_object_get_source (E_DBUS_OBJECT (dbus_object));
data = e_source_to_string (source, NULL);
e_dbus_source_set_data (dbus_source, data);
g_free (data);
g_object_unref (dbus_source);
g_object_unref (dbus_object);
/* If the added source has a [Collection] extension but the
* corresponding ECollectionBackendFactory is not available,
* the source gets permanently inserted in the orphans table
* to prevent it from being exported to client applications. */
extension_name = E_SOURCE_EXTENSION_COLLECTION;
if (e_source_has_extension (source, extension_name)) {
ECollectionBackendFactory *backend_factory;
backend_factory =
e_source_registry_server_ref_backend_factory (
server, source);
if (backend_factory == NULL) {
source_registry_server_orphans_insert (server, source);
return;
}
g_object_unref (backend_factory);
}
source_registry_server_sources_insert (server, source);
g_signal_emit (server, signals[SOURCE_ADDED], 0, source);
/* This is to ensure the source data gets written to disk, since
* the ESource is exported now. Could be racy otherwise if this
* function is called from a worker thread. */
e_source_changed (source);
/* Adopt any orphans that have been waiting for this object. */
source_registry_server_adopt_orphans (server, source);
}
/* Helper for e_source_registry_server_remove_object() */
static void
source_registry_server_remove_object (ESourceRegistryServer *server,
ESource *source)
{
g_object_ref (source);
if (source_registry_server_sources_remove (server, source)) {
EServerSideSource *ss_source;
ss_source = E_SERVER_SIDE_SOURCE (source);
source_registry_server_orphans_insert (server, source);
g_node_unlink (e_server_side_source_get_node (ss_source));
g_signal_emit (server, signals[SOURCE_REMOVED], 0, source);
}
g_object_unref (source);
}
/**
* e_source_registry_server_remove_source:
* @server: an #ESourceRegistryServer
* @source: an #ESource
*
* Removes @source and all of its descendants from @server.
*
* Since: 3.6
**/
void
e_source_registry_server_remove_source (ESourceRegistryServer *server,
ESource *source)
{
ESource *child;
ESource *exported;
GQueue queue = G_QUEUE_INIT;
const gchar *uid;
g_return_if_fail (E_IS_SOURCE_REGISTRY_SERVER (server));
g_return_if_fail (E_IS_SERVER_SIDE_SOURCE (source));
uid = e_source_get_uid (source);
/* If the removed source is in the server hierarchy, gather
* it and all of its descendants into a queue in "post-order"
* so we're always processing leaf nodes as we pop sources off
* the head of the queue. */
exported = e_source_registry_server_ref_source (server, uid);
if (exported != NULL) {
source_registry_server_queue_subtree (source, &queue);
g_object_unref (exported);
}
/* Move the queued descendants to the orphan table, and emit a
* "source-removed" signal for each source. This will include
* the removed source unless the source was already an orphan,
* in which case the queue will be empty. */
while ((child = g_queue_pop_head (&queue)) != NULL) {
source_registry_server_remove_object (server, child);
g_object_unref (child);
}
/* The removed source should be in the orphan table now. */
source_registry_server_orphans_remove (server, source);
}
/**
* e_source_registry_server_load_all:
* @server: an #ESourceRegistryServer
* @error: return location for a #GError, or %NULL
*
* Loads data source key files from standard system-wide and user-specific
* locations. Because multiple errors can occur when loading multiple files,
* @error is only set if a directory can not be opened. If a data source key
* file fails to load, the error is broadcast through the
* #ESourceRegistryServer::load-error signal.
*
* Returns: %TRUE if the standard directories were successfully opened,
* but this does not imply the key files were successfully loaded
*
* Since: 3.6
*
* Deprecated: 3.8: Instead, implement an equivalent function yourself.
* It was a mistake to encode this much file location
* policy directly into the library API.
**/
gboolean
e_source_registry_server_load_all (ESourceRegistryServer *server,
GError **error)
{
ESourcePermissionFlags flags;
const gchar *directory;
gboolean success;
g_return_val_if_fail (E_IS_SOURCE_REGISTRY_SERVER (server), FALSE);
/* Load the user's sources directory first so that user-specific
* data sources overshadow predefined data sources with identical
* UIDs. The 'local' data source is one such example. */
directory = e_server_side_source_get_user_dir ();
flags = E_SOURCE_PERMISSION_REMOVABLE |
E_SOURCE_PERMISSION_WRITABLE;
success = e_source_registry_server_load_directory (
server, directory, flags, error);
g_prefix_error (error, "%s: ", directory);
if (!success)
return FALSE;
directory = SYSTEM_WIDE_RO_SOURCES_DIRECTORY;
flags = E_SOURCE_PERMISSION_NONE;
success = e_source_registry_server_load_directory (
server, directory, flags, error);
g_prefix_error (error, "%s: ", directory);
if (!success)
return FALSE;
directory = SYSTEM_WIDE_RW_SOURCES_DIRECTORY;
flags = E_SOURCE_PERMISSION_WRITABLE;
success = e_source_registry_server_load_directory (
server, directory, flags, error);
g_prefix_error (error, "%s: ", directory);
if (!success)
return FALSE;
/* Signal that all files are now loaded. */
g_signal_emit (server, signals[FILES_LOADED], 0);
return TRUE;
}
/**
* e_source_registry_server_load_directory:
* @server: an #ESourceRegistryServer
* @path: the path to the directory to load
* @flags: permission flags for files loaded from @path
* @error: return location for a #GError, or %NULL
*
* Loads data source key files in @path. Because multiple errors can
* occur when loading multiple files, @error is only set if @path can
* not be opened. If a key file fails to load, the error is broadcast
* through the #ESourceRegistryServer::load-error signal.
*
* If the #E_SOURCE_PERMISSION_REMOVABLE flag is given, then the @server
* will emit signals on the D-Bus interface when key files are created or
* deleted in @path.
*
* Returns: %TRUE if @path was successfully opened, but this
* does not imply the key files were successfully loaded
*
* Since: 3.6
**/
gboolean
e_source_registry_server_load_directory (ESourceRegistryServer *server,
const gchar *path,
ESourcePermissionFlags flags,
GError **error)
{
GDir *dir;
GFile *file;
const gchar *name;
gboolean removable;
g_return_val_if_fail (E_IS_SOURCE_REGISTRY_SERVER (server), FALSE);
g_return_val_if_fail (path != NULL, FALSE);
removable = ((flags & E_SOURCE_PERMISSION_REMOVABLE) != 0);
/* If the directory doesn't exist then there's nothing to load.
* Note we do not use G_FILE_TEST_DIR here. If the given path
* exists but is not a directory then we let g_dir_open() fail. */
if (!g_file_test (path, G_FILE_TEST_EXISTS))
return TRUE;
dir = g_dir_open (path, 0, error);
if (dir == NULL)
return FALSE;
file = g_file_new_for_path (path);
while ((name = g_dir_read_name (dir)) != NULL) {
ESource *source;
GFile *child;
GError *local_error = NULL;
/* Ignore files with no ".source" suffix. */
if (!g_str_has_suffix (name, ".source"))
continue;
child = g_file_get_child (file, name);
source = e_source_registry_server_load_file (
server, child, flags, &local_error);
/* We don't need the returned reference. */
if (source != NULL)
g_object_unref (source);
if (local_error != NULL) {
e_source_registry_server_load_error (
server, child, local_error);
g_error_free (local_error);
}
g_object_unref (child);
}
g_dir_close (dir);
/* Only data source files in the user's
* sources directory should be removable. */
if (removable) {
GFileMonitor *monitor;
GError *local_error = NULL;
/* Directory monitoring is a nice-to-have feature.
* If this fails, leave a breadcrumb on the console
* to indicate something went wrong, but don't return
* an error status. */
monitor = g_file_monitor_directory (
file, G_FILE_MONITOR_NONE, NULL, &local_error);
/* Sanity check. */
g_warn_if_fail (
((monitor != NULL) && (local_error == NULL)) ||
((monitor == NULL) && (local_error != NULL)));
if (monitor != NULL) {
g_signal_connect (
monitor, "changed", G_CALLBACK (
source_registry_server_monitor_changed_cb),
server);
g_hash_table_insert (
server->priv->monitors,
g_object_ref (file),
g_object_ref (monitor));
g_object_unref (monitor);
}
if (local_error != NULL) {
g_warning ("%s: %s", G_STRFUNC, local_error->message);
g_error_free (local_error);
}
}
g_object_unref (file);
return TRUE;
}
/**
* e_source_registry_server_load_resource:
* @server: an #ESourceRegistryServer
* @resource: a #GResource containing data source key files
* @path: the path to the data source key files inside @resource
* @flags: permission flags for files loaded from @path
* @error: return location for a #GError, or %NULL
*
* Loads data source key files from @resource by enumerating the children
* at @path and calling e_source_registry_server_load_file() on each child.
* Because multiple errors can occur when loading multiple files, @error is
* only set if @path is invalid. If a key file fails to load, the error is
* broadcast through the #ESourceRegistryServer::load-error signal.
*
* Returns: %TRUE if @path was successfully located, but this does not
* imply the key files were successfully loaded
*
* Since: 3.8
**/
gboolean
e_source_registry_server_load_resource (ESourceRegistryServer *server,
GResource *resource,
const gchar *path,
ESourcePermissionFlags flags,
GError **error)
{
gchar **children;
gint ii;
g_return_val_if_fail (E_IS_SOURCE_REGISTRY_SERVER (server), FALSE);
g_return_val_if_fail (resource != NULL, FALSE);
g_return_val_if_fail (path != NULL, FALSE);
children = g_resource_enumerate_children (
resource, path, G_RESOURCE_LOOKUP_FLAGS_NONE, error);
if (children == NULL)
return FALSE;
for (ii = 0; children[ii] != NULL; ii++) {
ESource *source;
GFile *file;
gchar *child_path;
gchar *resource_uri;
GError *local_error = NULL;
child_path = g_build_path ("/", path, children[ii], NULL);
resource_uri = g_strconcat ("resource://", child_path, NULL);
file = g_file_new_for_uri (resource_uri);
g_free (resource_uri);
g_free (child_path);
source = e_source_registry_server_load_file (
server, file, flags, &local_error);
/* We don't need the returned reference. */
if (source != NULL)
g_object_unref (source);
if (local_error != NULL) {
e_source_registry_server_load_error (
server, file, local_error);
g_error_free (local_error);
}
g_object_unref (file);
}
g_strfreev (children);
return TRUE;
}
/* Helper for e_source_registry_server_load_file() */
static gboolean
source_registry_server_tweak_key_file (ESourceRegistryServer *server,
GFile *file,
const gchar *uid,
GError **error)
{
GKeyFile *key_file;
gchar *contents = NULL;
gsize length;
gboolean handler_pending;
gboolean success = FALSE;
gboolean tweaked = FALSE;
/* Skip this if no one's listening. */
handler_pending = g_signal_has_handler_pending (
server, signals[TWEAK_KEY_FILE], 0, FALSE);
if (!handler_pending)
return TRUE;
key_file = g_key_file_new ();
if (!g_file_load_contents (file, NULL, &contents, &length, NULL, error)) {
contents = NULL;
length = 0;
}
if (contents != NULL) {
success = g_key_file_load_from_data (
key_file, contents, length,
G_KEY_FILE_KEEP_COMMENTS |
G_KEY_FILE_KEEP_TRANSLATIONS,
error);
g_free (contents);
}
if (success)
g_signal_emit (
server, signals[TWEAK_KEY_FILE], 0,
key_file, uid, &tweaked);
if (tweaked) {
contents = g_key_file_to_data (key_file, &length, NULL);
success = g_file_replace_contents (
file, contents, length, NULL, FALSE,
G_FILE_CREATE_NONE, NULL, NULL, error);
g_free (contents);
}
g_key_file_free (key_file);
return success;
}
/**
* e_source_registry_server_load_file:
* @server: an #ESourceRegistryServer
* @file: the data source key file to load
* @flags: initial permission flags for the data source
* @error: return location for a #GError, or %NULL
*
* Creates an #ESource for a native key file and adds it to @server.
* If an error occurs, the function returns %NULL and sets @error.
*
* The returned #ESource is referenced for thread-safety. Unreference
* the #ESource with g_object_unref() when finished with it.
*
* Returns: (transfer full) (nullable): the newly-added #ESource, or %NULL on error
*
* Since: 3.6
**/
ESource *
e_source_registry_server_load_file (ESourceRegistryServer *server,
GFile *file,
ESourcePermissionFlags flags,
GError **error)
{
ESource *source;
gboolean writable;
gboolean removable;
gboolean success = TRUE;
gchar *uid;
g_return_val_if_fail (E_IS_SOURCE_REGISTRY_SERVER (server), NULL);
g_return_val_if_fail (G_IS_FILE (file), NULL);
writable = ((flags & E_SOURCE_PERMISSION_WRITABLE) != 0);
removable = ((flags & E_SOURCE_PERMISSION_REMOVABLE) != 0);
uid = e_server_side_source_uid_from_file (file, error);
if (uid == NULL)
return NULL;
/* Check if we already have this file loaded. */
source = e_source_registry_server_ref_source (server, uid);
/* If the source is to be removable then the key file can
* be written back to disk, and can therefore be tweaked. */
if (source == NULL && removable)
success = source_registry_server_tweak_key_file (
server, file, uid, error);
if (source == NULL && success)
source = e_server_side_source_new (server, file, error);
g_free (uid);
if (source == NULL)
return NULL;
/* Set the data source's initial permissions, which
* determines which D-Bus methods it exports: write()
* if writable, remove() if removable. We apply these
* before adding the source to the server because some
* "source-added" signal handlers may wish to override
* the initial permissions.
*
* Note that we apply the initial permission flags even
* if the data source has already been loaded. That is
* intentional. That is why the load_all() function loads
* the user directory before loading system-wide directories.
* If there's a UID collision between a data source in the
* user's directory and a data source in a system-wide
* directory, the permission flags for the system-wide
* directory should win.
*
* Consider an example:
*
* The built-in 'local' data source should always be
* writable but not removable.
*
* Suppose the user temporarily disables the 'local'
* data source. The altered 'local' data source file
* (with Enabled=false) is saved in the user's sources
* directory.
*
* On the next startup, the altered 'local' file is
* first loaded from the user's source directory and
* given removable + writable permissions.
*
* We then load data sources from the 'rw-sources'
* system directory containing the unaltered 'local'
* file (with Enabled=true), which is not removable.
*
* We keep the contents of the altered 'local' file
* (Enabled=false), but override its permissions to
* just be writable, not removable.
*/
e_server_side_source_set_writable (
E_SERVER_SIDE_SOURCE (source), writable);
e_server_side_source_set_removable (
E_SERVER_SIDE_SOURCE (source), removable);
/* This does nothing if the source is already added. */
e_source_registry_server_add_source (server, source);
return source;
}
/**
* e_source_registry_server_load_error:
* @server: an #ESourceRegistryServer
* @file: the #GFile that failed to load
* @error: a #GError describing the load error
*
* Emits the #ESourceRegistryServer::load-error signal.
*
* Since: 3.6
**/
void
e_source_registry_server_load_error (ESourceRegistryServer *server,
GFile *file,
const GError *error)
{
g_return_if_fail (E_IS_SOURCE_REGISTRY_SERVER (server));
g_return_if_fail (G_IS_FILE (file));
g_return_if_fail (error != NULL);
g_signal_emit (server, signals[LOAD_ERROR], 0, file, error);
}
/**
* e_source_registry_server_ref_source:
* @server: an #ESourceRegistryServer
* @uid: a unique identifier string
*
* Looks up an #ESource in @server by its unique identifier string.
*
* The returned #ESource is referenced for thread-safety and must be
* unreferenced with g_object_unref() when finished with it.
*
* Returns: (transfer full) (nullable): an #ESource, or %NULL if no match was found
*
* Since: 3.6
**/
ESource *
e_source_registry_server_ref_source (ESourceRegistryServer *server,
const gchar *uid)
{
g_return_val_if_fail (E_IS_SOURCE_REGISTRY_SERVER (server), NULL);
g_return_val_if_fail (uid != NULL, NULL);
return source_registry_server_sources_lookup (server, uid);
}
/**
* e_source_registry_server_list_sources:
* @server: an #ESourceRegistryServer
* @extension_name: (nullable): an extension name, or %NULL
*
* Returns a list of registered sources, sorted by display name. If
* @extension_name is given, restrict the list to sources having that
* extension name.
*
* The sources returned in the list are referenced for thread-safety.
* They must each be unreferenced with g_object_unref() when finished
* with them. Free the returned #GList itself with g_list_free().
*
* An easy way to free the list properly in one step is as follows:
*
* |[
* g_list_free_full (list, g_object_unref);
* ]|
*
* Returns: (element-type ESource) (transfer full): a sorted list of sources
*
* Since: 3.6
**/
GList *
e_source_registry_server_list_sources (ESourceRegistryServer *server,
const gchar *extension_name)
{
GList *list, *link;
GQueue trash = G_QUEUE_INIT;
g_return_val_if_fail (E_IS_SOURCE_REGISTRY_SERVER (server), NULL);
list = g_list_sort (
source_registry_server_sources_get_values (server),
(GCompareFunc) e_source_compare_by_display_name);
if (extension_name == NULL)
return list;
for (link = list; link != NULL; link = g_list_next (link)) {
ESource *source = E_SOURCE (link->data);
if (!e_source_has_extension (source, extension_name)) {
g_queue_push_tail (&trash, link);
g_object_unref (source);
}
}
/* We do want pop_head() here, not pop_head_link(). */
while ((link = g_queue_pop_head (&trash)) != NULL)
list = g_list_delete_link (list, link);
return list;
}
/**
* e_source_registry_server_find_extension:
* @server: an #ESourceRegistryServer
* @source: an #ESource
* @extension_name: the extension name to find
*
* Examines @source and its ancestors and returns the "deepest" #ESource
* having an #ESourceExtension with the given @extension_name. If neither
* @source nor any of its ancestors have such an extension, the function
* returns %NULL.
*
* This function is useful in cases when an #ESourceExtension is meant to
* apply to both the #ESource it belongs to and the #ESource's descendants.
*
* A common example is the #ESourceCollection extension, where descendants
* of an #ESource having an #ESourceCollection extension are implied to be
* members of that collection. In that example, this function can be used
* to test whether @source is a member of a collection.
*
* The returned #ESource is referenced for thread-safety and must be
* unreferenced with g_object_unref() when finished with it.
*
* Note the function returns the #ESource containing the #ESourceExtension
* instead of the #ESourceExtension itself because extension instances are
* not to be referenced directly (see e_source_get_extension()).
*
* Returns: (transfer full) (nullable): an #ESource, or %NULL if no match was found
*
* Since: 3.8
**/
ESource *
e_source_registry_server_find_extension (ESourceRegistryServer *server,
ESource *source,
const gchar *extension_name)
{
g_return_val_if_fail (E_IS_SOURCE_REGISTRY_SERVER (server), NULL);
g_return_val_if_fail (E_IS_SOURCE (source), NULL);
g_return_val_if_fail (extension_name != NULL, NULL);
g_object_ref (source);
while (!e_source_has_extension (source, extension_name)) {
gchar *uid;
uid = e_source_dup_parent (source);
g_object_unref (source);
source = NULL;
if (uid != NULL) {
source = e_source_registry_server_ref_source (
server, uid);
g_free (uid);
}
if (source == NULL)
break;
}
return source;
}
/**
* e_source_registry_server_ref_backend:
* @server: an #ESourceRegistryServer
* @source: an #ESource
*
* Returns the #ECollectionBackend associated with @source, or %NULL if
* there is no #ECollectionBackend associated with @source.
*
* An #ESource is associated with an #ECollectionBackend if the #ESource has
* an #ESourceCollection extension, or if it is a hierarchical descendant of
* another #ESource which has an #ESourceCollection extension.
*
* The returned #ECollectionBackend is referenced for thread-safety.
* Unreference the #ECollectionBackend with g_object_unref() when finished
* with it.
*
* Returns: (transfer full) (nullable): the #ECollectionBackend for @source, or %NULL
*
* Since: 3.6
**/
ECollectionBackend *
e_source_registry_server_ref_backend (ESourceRegistryServer *server,
ESource *source)
{
ECollectionBackend *backend = NULL;
g_return_val_if_fail (E_IS_SOURCE_REGISTRY_SERVER (server), NULL);
g_return_val_if_fail (E_IS_SOURCE (source), NULL);
source = e_source_registry_server_find_extension (
server, source, E_SOURCE_EXTENSION_COLLECTION);
if (source != NULL) {
backend = g_object_get_data (
G_OBJECT (source), BACKEND_DATA_KEY);
if (backend != NULL)
g_object_ref (backend);
g_object_unref (source);
}
return backend;
}
/**
* e_source_registry_server_ref_backend_factory:
* @server: an #ESourceRegistryServer
* @source: an #ESource
*
* Returns the #ECollectionBackendFactory for @source, if available.
* If @source does not have an #ESourceCollection extension, or if the
* #ESourceCollection extension names an #ESourceBackend:backend-name for
* which there is no corresponding #ECollectionBackendFactory, the function
* returns %NULL.
*
* The returned #ECollectionBackendFactory is referenced for thread-safety.
* Unreference the #ECollectionBackendFactory with g_object_unref() when
* finished with it.
*
* Returns: (transfer full) (nullable): the #ECollectionBackendFactory for @source,
* or %NULL
*
* Since: 3.6
**/
ECollectionBackendFactory *
e_source_registry_server_ref_backend_factory (ESourceRegistryServer *server,
ESource *source)
{
EBackendFactory *factory;
ESourceBackend *extension;
const gchar *backend_name;
const gchar *extension_name;
g_return_val_if_fail (E_IS_SOURCE_REGISTRY_SERVER (server), NULL);
g_return_val_if_fail (E_IS_SOURCE (source), NULL);
/* XXX Should we also check ancestor sources for a collection
* extension so this function works for ANY source in the
* collection? Gonna refrain til a real use case emerges
* but it's something to keep in mind. */
extension_name = E_SOURCE_EXTENSION_COLLECTION;
if (!e_source_has_extension (source, extension_name))
return NULL;
extension = e_source_get_extension (source, extension_name);
backend_name = e_source_backend_get_backend_name (extension);
factory = e_data_factory_ref_backend_factory (
E_DATA_FACTORY (server), backend_name, extension_name);
if (factory == NULL)
return NULL;
/* The factory *should* be an ECollectionBackendFactory.
* We specify this in source_registry_server_class_init(). */
return E_COLLECTION_BACKEND_FACTORY (factory);
}
/**
* e_source_registry_server_ref_oauth2_support:
* @server: an #ESourceRegistryServer
*
* Returns the default #EOAuth2Support implementation, which can be used when
* the source doesn't have it overwritten.
*
* Free the returned object with g_object_unref(), when no longer needed.
*
* Returns: (transfer full) (nullable): the default #EOAuth2Support,
* or %NULL, when none exists
*
* Since: 3.40
**/
EOAuth2Support *
e_source_registry_server_ref_oauth2_support (ESourceRegistryServer *server)
{
g_return_val_if_fail (E_IS_SOURCE_REGISTRY_SERVER (server), NULL);
if (!server->priv->oauth2_services)
return NULL;
return g_object_ref (E_OAUTH2_SUPPORT (server));
}