/* * e-source-registry-server.c * * 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) version 3. * * This program 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 the program; 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 authentication * and 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 "e-source-registry-server.h" #include #include #include /* Private D-Bus classes. */ #include #include #include #include #include #define E_SOURCE_REGISTRY_SERVER_GET_PRIVATE(obj) \ (G_TYPE_INSTANCE_GET_PRIVATE \ ((obj), E_TYPE_SOURCE_REGISTRY_SERVER, ESourceRegistryServerPrivate)) /* Collection backends get tacked on to * sources with a [Collection] extension. */ #define BACKEND_DATA_KEY "__e_collection_backend__" typedef struct _AuthRequest AuthRequest; 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; /* In pseudo-Python notation: * * running_auths = { UID : AuthRequest } * waiting_auths = { UID : [ AuthRequest, ... ] } * * We process all authenticators for a given source UID at once. * The thought being after the first authenticator for a given UID * completes (the first being most likely to trigger a user prompt), * then any other authenticators for that same UID should complete * quickly, hopefully without having to reprompt. That is unless * the user decides not to cache the secret at all, in which case * he gets what he asked for: lots of annoying prompts. */ GMutex auth_lock; GHashTable *running_auths; GHashTable *waiting_auths; guint authentication_count; }; struct _AuthRequest { volatile gint ref_count; EAuthenticationSession *session; GSimpleAsyncResult *simple; ESource *source; /* may be NULL */ GCancellable *cancellable; }; enum { LOAD_ERROR, FILES_LOADED, SOURCE_ADDED, SOURCE_REMOVED, TWEAK_KEY_FILE, LAST_SIGNAL }; /* Forward Declarations */ static void source_registry_server_maybe_start_auth_session (ESourceRegistryServer *server, const gchar *uid); static guint signals[LAST_SIGNAL]; G_DEFINE_TYPE ( ESourceRegistryServer, e_source_registry_server, E_TYPE_DATA_FACTORY) static AuthRequest * auth_request_new (EAuthenticationSession *session, GSimpleAsyncResult *simple, GCancellable *cancellable) { ESourceRegistryServer *server; AuthRequest *request; const gchar *uid; server = e_authentication_session_get_server (session); uid = e_authentication_session_get_source_uid (session); request = g_slice_new0 (AuthRequest); request->ref_count = 1; request->session = g_object_ref (session); request->simple = g_object_ref (simple); /* This will return NULL if the authenticating data source * has not yet been submitted to the D-Bus registry server. */ request->source = e_source_registry_server_ref_source (server, uid); if (G_IS_CANCELLABLE (cancellable)) request->cancellable = g_object_ref (cancellable); return request; } static AuthRequest * auth_request_ref (AuthRequest *request) { g_return_val_if_fail (request != NULL, NULL); g_return_val_if_fail (request->ref_count > 0, NULL); g_atomic_int_inc (&request->ref_count); return request; } static void auth_request_unref (AuthRequest *request) { g_return_if_fail (request != NULL); g_return_if_fail (request->ref_count > 0); if (g_atomic_int_dec_and_test (&request->ref_count)) { g_object_unref (request->session); g_object_unref (request->simple); if (request->source != NULL) g_object_unref (request->source); if (request->cancellable != NULL) g_object_unref (request->cancellable); g_slice_free (AuthRequest, request); } } /* 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); } /* GDestroyNotify callback for 'waiting_auths' values */ static void free_auth_queue (GQueue *queue) { /* XXX g_queue_clear_full() would be nice here. */ while (!g_queue_is_empty (queue)) g_object_unref (g_queue_pop_head (queue)); g_queue_free (queue); } 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 void source_request_server_auth_request_cancel_all (ESourceRegistryServer *server) { GHashTableIter iter; gpointer value; g_mutex_lock (&server->priv->auth_lock); g_hash_table_iter_init (&iter, server->priv->waiting_auths); while (g_hash_table_iter_next (&iter, NULL, &value)) { GQueue *queue = value; GList *list, *link; list = g_queue_peek_head_link (queue); for (link = list; link != NULL; link = g_list_next (link)) { AuthRequest *request = link->data; g_cancellable_cancel (request->cancellable); } } g_hash_table_iter_init (&iter, server->priv->running_auths); while (g_hash_table_iter_next (&iter, NULL, &value)) { AuthRequest *request = value; g_cancellable_cancel (request->cancellable); } g_mutex_unlock (&server->priv->auth_lock); } static void source_registry_server_auth_request_push (ESourceRegistryServer *server, const gchar *uid, AuthRequest *request) { GQueue *queue; g_return_if_fail (uid != NULL); g_mutex_lock (&server->priv->auth_lock); queue = g_hash_table_lookup (server->priv->waiting_auths, uid); if (queue == NULL) { queue = g_queue_new (); g_hash_table_insert ( server->priv->waiting_auths, g_strdup (uid), queue); } g_queue_push_tail (queue, auth_request_ref (request)); g_mutex_unlock (&server->priv->auth_lock); } static AuthRequest * source_registry_server_auth_request_next (ESourceRegistryServer *server, const gchar *uid) { AuthRequest *request = NULL; g_return_val_if_fail (uid != NULL, NULL); g_mutex_lock (&server->priv->auth_lock); /* If we're already busy processing an authentication request * for this UID, the next request will have to wait in line. */ if (!g_hash_table_contains (server->priv->running_auths, uid)) { GQueue *queue; queue = g_hash_table_lookup ( server->priv->waiting_auths, uid); if (queue != NULL) request = g_queue_pop_head (queue); if (request != NULL) g_hash_table_insert ( server->priv->running_auths, g_strdup (uid), auth_request_ref (request)); } g_mutex_unlock (&server->priv->auth_lock); return request; } static void source_registry_server_auth_request_done (ESourceRegistryServer *server, const gchar *uid) { g_return_if_fail (uid != NULL); g_mutex_lock (&server->priv->auth_lock); g_hash_table_remove (server->priv->running_auths, uid); g_mutex_unlock (&server->priv->auth_lock); } static void source_registry_server_auth_session_cb (GObject *source_object, GAsyncResult *result, gpointer user_data) { ESourceRegistryServer *server; EAuthenticationSession *session; EAuthenticationSessionResult auth_result; AuthRequest *request; const gchar *uid; GError *error = NULL; session = E_AUTHENTICATION_SESSION (source_object); request = (AuthRequest *) user_data; auth_result = e_authentication_session_execute_finish ( session, result, &error); if (error != NULL) { ESourceAuthenticator *authenticator; g_warn_if_fail (auth_result == E_AUTHENTICATION_SESSION_ERROR); g_simple_async_result_take_error (request->simple, error); /* If the authenticator is an EAuthenticationMediator, * have it emit a "server-error" signal to the client. */ authenticator = e_authentication_session_get_authenticator (session); if (E_IS_AUTHENTICATION_MEDIATOR (authenticator)) e_authentication_mediator_server_error ( E_AUTHENTICATION_MEDIATOR (authenticator), error); } /* Authentication dismissals require additional handling. */ if (auth_result == E_AUTHENTICATION_SESSION_DISMISSED) { ESourceAuthenticator *authenticator; /* If the authenticator is an EAuthenticationMediator, * have it emit a "dismissed" signal to the client. */ authenticator = e_authentication_session_get_authenticator (session); if (E_IS_AUTHENTICATION_MEDIATOR (authenticator)) e_authentication_mediator_dismiss ( E_AUTHENTICATION_MEDIATOR (authenticator)); /* Prevent further user interruptions. */ if (request->source != NULL) e_server_side_source_set_allow_auth_prompt ( E_SERVER_SIDE_SOURCE (request->source), FALSE); /* e_source_registry_server_authenticate_finish() should * return an error since authentication did not complete. */ g_simple_async_result_set_error ( request->simple, G_IO_ERROR, G_IO_ERROR_CANCELLED, _("The user declined to authenticate")); } g_simple_async_result_complete_in_idle (request->simple); server = e_authentication_session_get_server (session); uid = e_authentication_session_get_source_uid (session); source_registry_server_auth_request_done (server, uid); source_registry_server_maybe_start_auth_session (server, uid); auth_request_unref (request); } static gboolean source_registry_server_start_auth_session_idle_cb (gpointer user_data) { AuthRequest *request = user_data; /* Execute the new active auth session. This signals it to * respond with a cached secret in the keyring if it can, or * else show an authentication prompt and wait for input. */ e_authentication_session_execute ( request->session, G_PRIORITY_DEFAULT, request->cancellable, source_registry_server_auth_session_cb, auth_request_ref (request)); return FALSE; } static void source_registry_server_maybe_start_auth_session (ESourceRegistryServer *server, const gchar *uid) { AuthRequest *request; /* We own the returned reference, unless we get NULL. */ request = source_registry_server_auth_request_next (server, uid); if (request != NULL) { GSource *idle_source; /* Process the next AuthRequest from an idle callback * on our own GMainContext, since the current thread- * default GMainContext may only be temporary. */ idle_source = g_idle_source_new (); g_source_set_callback ( idle_source, source_registry_server_start_auth_session_idle_cb, auth_request_ref (request), (GDestroyNotify) auth_request_unref); g_source_attach (idle_source, server->priv->main_context); g_source_unref (idle_source); auth_request_unref (request); } } static void source_registry_server_authenticate_done_cb (GObject *source_object, GAsyncResult *result, gpointer user_data) { GError *error = NULL; e_source_registry_server_authenticate_finish ( E_SOURCE_REGISTRY_SERVER (source_object), result, &error); /* Ignore cancellations. */ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { g_error_free (error); } else if (error != NULL) { g_warning ("%s: %s", G_STRFUNC, error->message); g_error_free (error); } } static void source_registry_server_wait_for_client_cb (GObject *source_object, GAsyncResult *result, gpointer user_data) { EAuthenticationMediator *mediator; EAuthenticationSession *session; GError *error = NULL; mediator = E_AUTHENTICATION_MEDIATOR (source_object); session = E_AUTHENTICATION_SESSION (user_data); e_authentication_mediator_wait_for_client_finish ( mediator, result, &error); if (error == NULL) { ESourceRegistryServer *server; server = e_authentication_session_get_server (session); /* Client is ready and waiting to test passwords, so * execute the authentication session as soon as all * other authentication sessions for this same data * source are finished. * * XXX Note this asynchronous operation is not cancellable * but it does time out on its own after a few minutes. */ e_source_registry_server_authenticate ( server, session, NULL, source_registry_server_authenticate_done_cb, NULL); } else { /* Most likely the client went dark and the operation * timed out. Emit a dismissed signal anyway just in * case the client is still alive and listening. */ e_authentication_mediator_dismiss (mediator); g_warning ("%s: %s", G_STRFUNC, error->message); g_error_free (error); } g_object_unref (session); } static gboolean source_registry_server_allow_auth_prompt_all_cb (EDBusSourceManager *interface, GDBusMethodInvocation *invocation, ESourceRegistryServer *server) { GList *list, *link; list = e_source_registry_server_list_sources (server, NULL); for (link = list; link != NULL; link = g_list_next (link)) e_server_side_source_set_allow_auth_prompt ( E_SERVER_SIDE_SOURCE (link->data), TRUE); g_list_free_full (list, (GDestroyNotify) g_object_unref); e_dbus_source_manager_complete_allow_auth_prompt_all ( interface, invocation); return TRUE; } static gboolean source_registry_server_authenticate_cb (EDBusSourceManager *interface, GDBusMethodInvocation *invocation, const gchar *source_uid, const gchar *prompt_title, const gchar *prompt_message, const gchar *prompt_description, ESourceRegistryServer *server) { GDBusConnection *connection; EAuthenticationSession *session; ESourceAuthenticator *authenticator; const gchar *base_object_path; const gchar *sender; gchar *auth_object_path; GError *error = NULL; /* Export the D-Bus interface to a unique object path. This * effectively starts a new authentication session with the * method caller. */ base_object_path = E_SOURCE_REGISTRY_SERVER_OBJECT_PATH; connection = g_dbus_method_invocation_get_connection (invocation); sender = g_dbus_method_invocation_get_sender (invocation); auth_object_path = g_strdup_printf ( "%s/auth_%u", base_object_path, server->priv->authentication_count++); authenticator = e_authentication_mediator_new ( connection, auth_object_path, sender, &error); if (error != NULL) { g_warn_if_fail (authenticator == NULL); g_dbus_method_invocation_take_error (invocation, error); g_free (auth_object_path); return TRUE; } g_return_val_if_fail ( E_IS_SOURCE_AUTHENTICATOR (authenticator), FALSE); /* Create the authentication session. */ session = e_source_registry_server_new_auth_session ( server, authenticator, source_uid); /* Configure the authentication session. */ g_object_set ( session, "prompt-title", prompt_title, "prompt-message", prompt_message, "prompt-description", prompt_description, NULL); /* Before adding the authentication session to the server we * must handshake with the client requesting authentication. * We do this by returning the object path of the exported * Authenticator interface and then waiting for the client to * acknowledge by calling the Ready() method on the interface. * This indicates the client is ready to receive signals. * * XXX Note this asynchronous operation is not cancellable * but it does time out on its own after a few minutes. */ e_authentication_mediator_wait_for_client ( E_AUTHENTICATION_MEDIATOR (authenticator), NULL, source_registry_server_wait_for_client_cb, g_object_ref (session)); e_dbus_source_manager_complete_authenticate ( interface, invocation, auth_object_path); g_object_unref (authenticator); g_object_unref (session); g_free (auth_object_path); return TRUE; } 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 *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 ( interface, invocation); return TRUE; } static gboolean source_registry_server_reload_cb (EDBusSourceManager *interface, GDBusMethodInvocation *invocation, ESourceRegistryServer *server) { e_dbus_server_quit ( E_DBUS_SERVER (server), E_DBUS_SERVER_EXIT_RELOAD); e_dbus_source_manager_complete_reload (interface, invocation); return TRUE; } static void source_registry_server_monitor_changed_cb (GFileMonitor *monitor, GFile *file, GFile *other_file, GFileMonitorEvent event_type, ESourceRegistryServer *server) { if (event_type == G_FILE_MONITOR_EVENT_CREATED) { ESource *source; GError *error = NULL; /* it can return NULL source for hidden files */ source = e_server_side_source_new (server, 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); g_object_unref (source); } else if (error) { e_source_registry_server_load_error ( server, file, error); g_error_free (error); } } if (event_type == G_FILE_MONITOR_EVENT_DELETED) { ESource *source; gchar *uid; uid = e_server_side_source_uid_from_file (file, NULL); if (uid == NULL) return; source = e_source_registry_server_ref_source (server, uid); g_free (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_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 void source_registry_server_dispose (GObject *object) { ESourceRegistryServerPrivate *priv; priv = E_SOURCE_REGISTRY_SERVER_GET_PRIVATE (object); if (priv->main_context != NULL) { g_main_context_unref (priv->main_context); priv->main_context = NULL; } if (priv->object_manager != NULL) { g_object_unref (priv->object_manager); priv->object_manager = NULL; } if (priv->source_manager != NULL) { g_object_unref (priv->source_manager); priv->source_manager = NULL; } 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->running_auths); g_hash_table_remove_all (priv->waiting_auths); /* 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_GET_PRIVATE (object); g_hash_table_destroy (priv->sources); g_hash_table_destroy (priv->orphans); g_hash_table_destroy (priv->monitors); g_mutex_clear (&priv->sources_lock); g_mutex_clear (&priv->orphans_lock); g_mutex_clear (&priv->auth_lock); g_hash_table_destroy (priv->running_auths); g_hash_table_destroy (priv->waiting_auths); /* 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; GError *error = NULL; priv = E_SOURCE_REGISTRY_SERVER_GET_PRIVATE (server); g_dbus_object_manager_server_set_connection ( priv->object_manager, connection); g_dbus_interface_skeleton_export ( G_DBUS_INTERFACE_SKELETON (priv->source_manager), connection, E_SOURCE_REGISTRY_SERVER_OBJECT_PATH, &error); /* Terminate the server if we can't export the interface. */ if (error != NULL) { g_warning ("%s: %s", G_STRFUNC, error->message); e_dbus_server_quit (server, E_DBUS_SERVER_EXIT_NORMAL); g_error_free (error); } /* 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_GET_PRIVATE (server); source_request_server_auth_request_cancel_all ( E_SOURCE_REGISTRY_SERVER (server)); /* This makes the object manager unexport all objects. */ g_dbus_object_manager_server_set_connection ( priv->object_manager, NULL); g_dbus_interface_skeleton_unexport ( G_DBUS_INTERFACE_SKELETON (priv->source_manager)); /* 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; ESourceBackend *extension; const gchar *backend_name; 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 = e_data_factory_ref_backend ( E_DATA_FACTORY (server), backend_name, source); 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", backend_name, e_source_get_uid (source)); } } /* 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')\n", 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; g_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 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); g_type_class_add_private (class, sizeof (ESourceRegistryServerPrivate)); object_class = G_OBJECT_CLASS (class); 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; 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_init (ESourceRegistryServer *server) { GDBusObjectManagerServer *object_manager; EDBusSourceManager *source_manager; GHashTable *sources; GHashTable *orphans; GHashTable *monitors; GHashTable *running_auths; GHashTable *waiting_auths; 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); running_auths = g_hash_table_new_full ( (GHashFunc) g_str_hash, (GEqualFunc) g_str_equal, (GDestroyNotify) g_free, (GDestroyNotify) auth_request_unref); waiting_auths = g_hash_table_new_full ( (GHashFunc) g_str_hash, (GEqualFunc) g_str_equal, (GDestroyNotify) g_free, (GDestroyNotify) free_auth_queue); server->priv = E_SOURCE_REGISTRY_SERVER_GET_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->auth_lock); server->priv->waiting_auths = waiting_auths; server->priv->running_auths = running_auths; g_signal_connect ( source_manager, "handle-allow-auth-prompt-all", G_CALLBACK (source_registry_server_allow_auth_prompt_all_cb), server); g_signal_connect ( source_manager, "handle-authenticate", G_CALLBACK (source_registry_server_authenticate_cb), server); 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); } /** * 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, NULL); } /** * 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_DBUS_LOAD_DIRECTORY_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 (); g_file_load_contents (file, NULL, &contents, &length, NULL, error); 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: 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 #EBusSourceServer * @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: 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: 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: 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: 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: 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 a #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: 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); 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_new_auth_session: * @server: an #ESourceRegistryServer * @authenticator: an #ESourceAuthenticator * @source_uid: a data source identifier * * Convenience function instantiates an appropriate authentication * session type for @source_uid. * * If @server has an #EServerSideSource instance for @source_uid, then * its #EServerSideSource:auth-session-type is used to instantiate a new * authentication session. Otherwise a plain #EAuthenticationSession is * instantiated. * * Unreference the returned #EAuthenticationSession with g_object_unref() * when finished with it. * * Returns: a new #EAuthenticationSession for @source_uid * * Since: 3.8 **/ EAuthenticationSession * e_source_registry_server_new_auth_session (ESourceRegistryServer *server, ESourceAuthenticator *authenticator, const gchar *source_uid) { GType auth_session_type = E_TYPE_AUTHENTICATION_SESSION; ESource *source; g_return_val_if_fail (E_IS_SOURCE_REGISTRY_SERVER (server), NULL); g_return_val_if_fail (E_IS_SOURCE_AUTHENTICATOR (authenticator), NULL); g_return_val_if_fail (source_uid != NULL, NULL); source = e_source_registry_server_ref_source (server, source_uid); if (source != NULL) { auth_session_type = e_server_side_source_get_auth_session_type ( E_SERVER_SIDE_SOURCE (source)); g_object_unref (source); } g_return_val_if_fail ( g_type_is_a ( auth_session_type, E_TYPE_AUTHENTICATION_SESSION), NULL); return g_object_new ( auth_session_type, "server", server, "authenticator", authenticator, "source-uid", source_uid, NULL); } /** * e_source_registry_server_authenticate_sync: * @server: an #ESourceRegistryServer * @session: an #EAuthenticationSession * @cancellable: optional #GCancellable object, or %NULL * @error: return location for a #GError, or %NULL * * Queues the @session behind any ongoing or pending authentication * sessions for the same data source, and eventually executes @session * (see e_authentication_session_execute_sync() for more details). * * This function blocks until @session is finished executing. For a * non-blocking variation see e_source_registry_server_authenticate(). * * If an error occurs, the function sets @error and returns %FALSE. * * Returns: %TRUE on success, %FALSE on failure * * Since: 3.6 **/ gboolean e_source_registry_server_authenticate_sync (ESourceRegistryServer *server, EAuthenticationSession *session, GCancellable *cancellable, GError **error) { EAsyncClosure *closure; GAsyncResult *result; gboolean success; g_return_val_if_fail (E_IS_SOURCE_REGISTRY_SERVER (server), FALSE); g_return_val_if_fail (E_IS_AUTHENTICATION_SESSION (session), FALSE); closure = e_async_closure_new (); e_source_registry_server_authenticate ( server, session, cancellable, e_async_closure_callback, closure); result = e_async_closure_wait (closure); success = e_source_registry_server_authenticate_finish ( server, result, error); e_async_closure_free (closure); return success; } /** * e_source_registry_server_authenticate: * @server: an #ESourceRegistryServer * @session: an #EAuthenticationSession * @cancellable: optional #GCancellable object, or %NULL * @callback: a #GAsyncReadyCallback to call when the request is satisfied * @user_data: data to pass to the callback function * * Queues the @session behind any ongoing or pending authentication * sessions for the same data source, and eventually executes @session * (see e_authentication_session_execute_sync() for more details). * * This function returns immediately after enqueuing @session. When * @session is finished executing, @callback will be called. You can * then call e_source_registry_server_authenticate_finish() to get the * result of the operation. * * Since: 3.6 **/ void e_source_registry_server_authenticate (ESourceRegistryServer *server, EAuthenticationSession *session, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { GSimpleAsyncResult *simple; AuthRequest *request; const gchar *uid; g_return_if_fail (E_IS_SOURCE_REGISTRY_SERVER (server)); g_return_if_fail (E_IS_AUTHENTICATION_SESSION (session)); uid = e_authentication_session_get_source_uid (session); g_return_if_fail (uid != NULL); simple = g_simple_async_result_new ( G_OBJECT (server), callback, user_data, e_source_registry_server_authenticate); g_simple_async_result_set_check_cancellable (simple, cancellable); request = auth_request_new (session, simple, cancellable); source_registry_server_auth_request_push (server, uid, request); auth_request_unref (request); source_registry_server_maybe_start_auth_session (server, uid); g_object_unref (simple); } /** * e_source_registry_server_authenticate_finish: * @server: an #ESourceRegistryServer * @result: a #GAsyncResult * @error: return location for a #GError, or %NULL * * Finishes the operation started with e_source_registry_server_authenticate(). * If an error occurred, the function will set @error and return %FALSE. * * Returns: %TRUE on success, %FALSE on failure * * Since: 3.6 **/ gboolean e_source_registry_server_authenticate_finish (ESourceRegistryServer *server, GAsyncResult *result, GError **error) { GSimpleAsyncResult *simple; g_return_val_if_fail ( g_simple_async_result_is_valid ( result, G_OBJECT (server), e_source_registry_server_authenticate), FALSE); simple = G_SIMPLE_ASYNC_RESULT (result); /* Assume success unless a GError is set. */ return !g_simple_async_result_propagate_error (simple, error); }