From a176204ad9260e5887bc4919b54b790630c46965 Mon Sep 17 00:00:00 2001 From: Matthew Barnes Date: Thu, 29 Sep 2011 16:08:20 -0400 Subject: [PATCH] Add a new "evolution-source-registry" D-Bus service. This new service manages data source key files and serves them to clients, through the ESource and ESourceRegistry client-side APIs. --- configure.ac | 34 +- docs/reference/libebackend/libebackend-docs.xml | 10 + .../reference/libebackend/libebackend-sections.txt | 174 + docs/reference/libebackend/libebackend.types | 12 + libebackend/Makefile.am | 21 +- libebackend/e-authentication-mediator.c | 1032 ++++++ libebackend/e-authentication-mediator.h | 100 + libebackend/e-authentication-session.c | 1745 ++++++++++ libebackend/e-authentication-session.h | 212 ++ libebackend/e-backend-enums.h | 39 +- libebackend/e-collection-backend-factory.c | 186 ++ libebackend/e-collection-backend-factory.h | 88 + libebackend/e-collection-backend.c | 835 +++++ libebackend/e-collection-backend.h | 94 + libebackend/e-dbus-server.c | 6 +- libebackend/e-server-side-source.c | 1404 ++++++++ libebackend/e-server-side-source.h | 104 + libebackend/e-source-registry-server.c | 1801 ++++++++++ libebackend/e-source-registry-server.h | 132 + libebackend/libebackend.pc.in | 1 + services/Makefile.am | 1 + services/evolution-addressbook-factory/Makefile.am | 5 +- ...evolution-addressbook-factory-migrate-basedir.c | 319 -- .../evolution-addressbook-factory.c | 6 - services/evolution-calendar-factory/Makefile.am | 5 +- .../evolution-calendar-factory.c | 6 - services/evolution-source-registry/Makefile.am | 41 + .../evolution-source-registry-migrate-basedir.c} | 79 +- .../evolution-source-registry-migrate-sources.c | 3493 ++++++++++++++++++++ .../evolution-source-registry.c | 85 + ...g.gnome.evolution.dataserver.Sources.service.in | 3 + 31 files changed, 11722 insertions(+), 351 deletions(-) create mode 100644 libebackend/e-authentication-mediator.c create mode 100644 libebackend/e-authentication-mediator.h create mode 100644 libebackend/e-authentication-session.c create mode 100644 libebackend/e-authentication-session.h create mode 100644 libebackend/e-collection-backend-factory.c create mode 100644 libebackend/e-collection-backend-factory.h create mode 100644 libebackend/e-collection-backend.c create mode 100644 libebackend/e-collection-backend.h create mode 100644 libebackend/e-server-side-source.c create mode 100644 libebackend/e-server-side-source.h create mode 100644 libebackend/e-source-registry-server.c create mode 100644 libebackend/e-source-registry-server.h delete mode 100644 services/evolution-addressbook-factory/evolution-addressbook-factory-migrate-basedir.c create mode 100644 services/evolution-source-registry/Makefile.am rename services/{evolution-calendar-factory/evolution-calendar-factory-migrate-basedir.c => evolution-source-registry/evolution-source-registry-migrate-basedir.c} (82%) create mode 100644 services/evolution-source-registry/evolution-source-registry-migrate-sources.c create mode 100644 services/evolution-source-registry/evolution-source-registry.c create mode 100644 services/evolution-source-registry/org.gnome.evolution.dataserver.Sources.service.in diff --git a/configure.ac b/configure.ac index f1acae1..acff506 100644 --- a/configure.ac +++ b/configure.ac @@ -63,6 +63,7 @@ dnl D-Bus versioning dnl ****************************** ADDRESS_BOOK_DBUS_SERVICE_NAME="org.gnome.evolution.dataserver.AddressBook3" CALENDAR_DBUS_SERVICE_NAME="org.gnome.evolution.dataserver.Calendar2" +SOURCES_DBUS_SERVICE_NAME="org.gnome.evolution.dataserver.Sources0" AC_DEFINE_UNQUOTED( ADDRESS_BOOK_DBUS_SERVICE_NAME, @@ -74,8 +75,14 @@ AC_DEFINE_UNQUOTED( ["$CALENDAR_DBUS_SERVICE_NAME"], [D-Bus service name for the calendar factory]) +AC_DEFINE_UNQUOTED( + SOURCES_DBUS_SERVICE_NAME, + ["$SOURCES_DBUS_SERVICE_NAME"], + [D-Bus service name for the source registry]) + AC_SUBST(ADDRESS_BOOK_DBUS_SERVICE_NAME) AC_SUBST(CALENDAR_DBUS_SERVICE_NAME) +AC_SUBST(SOURCES_DBUS_SERVICE_NAME) dnl ****************************** dnl Libtool versioning @@ -397,14 +404,15 @@ if test "x$enable_goa" = xyes; then fi AM_CONDITIONAL(HAVE_GOA, [test x$enable_goa = xyes]) +dnl *********************************** +dnl Check for GNOME Keyring. +dnl *********************************** if test x$os_win32 = xno; then - dnl *********************************** - dnl Check for GNOME Keyring. - dnl *********************************** - PKG_CHECK_MODULES(GNOME_KEYRING, - [gnome-keyring-1 >= gnome_keyring_minimum_version]) + PKG_CHECK_MODULES(GNOME_KEYRING, + [gnome-keyring-1 >= gnome_keyring_minimum_version]) fi - +AC_SUBST(GNOME_KEYRING_CFLAGS) +AC_SUBST(GNOME_KEYRING_LIBS) dnl ********************************************************** dnl gcr-base is needed for secure password exchange over D-Bus @@ -1327,7 +1335,7 @@ if test "x$enable_maintainer_mode" = "xyes" ; then AC_SUBST(FACTORY_GTK_LIBS) fi -E_BACKEND_DEPS="gio-2.0 gmodule-2.0 libxml-2.0 gconf-2.0" +E_BACKEND_DEPS="gio-2.0 gmodule-2.0 gnome-keyring-1 libxml-2.0 gconf-2.0" dnl ****************************** dnl libebackend flags @@ -1491,12 +1499,21 @@ AC_SUBST(privlibdir) imagesdir='${datadir}'/pixmaps/evolution-data-server AC_SUBST(imagesdir) +moduledir='${libdir}'/evolution-data-server/registry-modules +AC_SUBST(moduledir) + ebook_backenddir='${libdir}'/evolution-data-server/addressbook-backends AC_SUBST(ebook_backenddir) ecal_backenddir='${libdir}'/evolution-data-server/calendar-backends AC_SUBST(ecal_backenddir) +ro_sourcesdir='${datadir}'/evolution-data-server-$BASE_VERSION/ro-sources +AC_SUBST(ro_sourcesdir) + +rw_sourcesdir='${datadir}'/evolution-data-server-$BASE_VERSION/rw-sources +AC_SUBST(rw_sourcesdir) + if test "x$use_gweather" = "xyes"; then weatherdatadir="$privdatadir/weather" AC_SUBST(weatherdatadir) @@ -1510,7 +1527,7 @@ dnl ******************* dnl D-BUS service stuff dnl ******************* m4_pattern_allow([AM_V_GEN]) -EVO_SUBST_SERVICE_RULE='%.service: %.service.in Makefile ; $(AM_V_GEN) sed -e "s|\@libexecdir\@|$(libexecdir)|" -e s"|\@ADDRESS_BOOK_DBUS_SERVICE_NAME\@|$(ADDRESS_BOOK_DBUS_SERVICE_NAME)|" -e "s|\@CALENDAR_DBUS_SERVICE_NAME\@|$(CALENDAR_DBUS_SERVICE_NAME)|" $< > $@' +EVO_SUBST_SERVICE_RULE='%.service: %.service.in Makefile ; $(AM_V_GEN) sed -e "s|\@libexecdir\@|$(libexecdir)|" -e s"|\@ADDRESS_BOOK_DBUS_SERVICE_NAME\@|$(ADDRESS_BOOK_DBUS_SERVICE_NAME)|" -e "s|\@CALENDAR_DBUS_SERVICE_NAME\@|$(CALENDAR_DBUS_SERVICE_NAME)|" -e "s|\@SOURCES_DBUS_SERVICE_NAME\@|$(SOURCES_DBUS_SERVICE_NAME)|" $< > $@' AC_SUBST(EVO_SUBST_SERVICE_RULE) dnl ****************************** @@ -1626,6 +1643,7 @@ private/Makefile services/Makefile services/evolution-addressbook-factory/Makefile services/evolution-calendar-factory/Makefile +services/evolution-source-registry/Makefile tests/Makefile tests/libebook/Makefile tests/libebook/client/Makefile diff --git a/docs/reference/libebackend/libebackend-docs.xml b/docs/reference/libebackend/libebackend-docs.xml index 5abb0ca..7d94fc2 100644 --- a/docs/reference/libebackend/libebackend-docs.xml +++ b/docs/reference/libebackend/libebackend-docs.xml @@ -20,6 +20,16 @@ + Registry Service Classes + + + + + + + + + Miscellaneous Utilities diff --git a/docs/reference/libebackend/libebackend-sections.txt b/docs/reference/libebackend/libebackend-sections.txt index f1872c3..89d1fa6 100644 --- a/docs/reference/libebackend/libebackend-sections.txt +++ b/docs/reference/libebackend/libebackend-sections.txt @@ -1,4 +1,75 @@
+e-authentication-mediator +EAuthenticationMediator +EAuthenticationMediator +e_authentication_mediator_new +e_authentication_mediator_get_connection +e_authentication_mediator_get_object_path +e_authentication_mediator_get_sender +e_authentication_mediator_wait_for_client_sync +e_authentication_mediator_wait_for_client +e_authentication_mediator_wait_for_client_finish +e_authentication_mediator_dismiss + +E_AUTHENTICATION_MEDIATOR +E_IS_AUTHENTICATION_MEDIATOR +E_TYPE_AUTHENTICATION_MEDIATOR +E_AUTHENTICATION_MEDIATOR_CLASS +E_IS_AUTHENTICATION_MEDIATOR_CLASS +E_AUTHENTICATION_MEDIATOR_GET_CLASS + +EAuthenticationMediatorPrivate +e_authentication_mediator_get_type +
+ +
+e-authentication-session +EAuthenticationSession +E_AUTHENTICATION_SESSION_KEYRING_ERROR +EAuthenticationSession +EAuthenticationSessionResult +e_authentication_session_new +e_authentication_session_get_server +e_authentication_session_get_authenticator +e_authentication_session_get_source_uid +e_authentication_session_get_prompt_title +e_authentication_session_dup_prompt_title +e_authentication_session_set_prompt_title +e_authentication_session_get_prompt_message +e_authentication_session_dup_prompt_message +e_authentication_session_set_prompt_message +e_authentication_session_get_prompt_description +e_authentication_session_dup_prompt_description +e_authentication_session_set_prompt_description +e_authentication_session_execute_sync +e_authentication_session_execute +e_authentication_session_execute_finish +e_authentication_session_store_password_sync +e_authentication_session_store_password +e_authentication_session_store_password_finish +e_authentication_session_lookup_password_sync +e_authentication_session_lookup_password +e_authentication_session_lookup_password_finish +e_authentication_session_delete_password_sync +e_authentication_session_delete_password +e_authentication_session_delete_password_finish + +E_AUTHENTICATION_SESSION +E_IS_AUTHENTICATION_SESSION +E_TYPE_AUTHENTICATION_SESSION +E_AUTHENTICATION_SESSION_CLASS +E_IS_AUTHENTICATION_SESSION_CLASS +E_AUTHENTICATION_SESSION_GET_CLASS +E_TYPE_AUTHENTICATION_SESSION_RESULT +EAuthenticationSessionClass + +EAuthenticationSessionPrivate +e_authentication_session_error_quark +e_authentication_session_get_type +e_authentication_session_result_get_type +
+ +
e-backend EBackend EBackend @@ -38,6 +109,46 @@ e_backend_factory_get_type
+e-collection-backend +ECollectionBackend +ECollectionBackend +e_collection_backend_new_child +e_collection_backend_ref_server +e_collection_backend_list_calendar_sources +e_collection_backend_list_contacts_sources +e_collection_backend_list_mail_sources + +E_COLLECTION_BACKEND +E_IS_COLLECTION_BACKEND +E_TYPE_COLLECTION_BACKEND +E_COLLECTION_BACKEND_CLASS +E_IS_COLLECTION_BACKEND_CLASS +E_COLLECTION_BACKEND_GET_CLASS +ECollectionBackendClass + +ECollectionBackendPrivate +e_collection_backend_get_type +
+ +
+e-collection-backend-factory +ECollectionBackendFactory +ECollectionBackendFactory +e_collection_backend_factory_prepare_mail + +E_COLLECTION_BACKEND_FACTORY +E_IS_COLLECTION_BACKEND_FACTORY +E_TYPE_COLLECTION_BACKEND_FACTORY +E_COLLECTION_BACKEND_FACTORY_CLASS +E_IS_COLLECTION_BACKEND_FACTORY_CLASS +E_COLLECTION_BACKEND_FACTORY_GET_CLASS +ECollectionBackendFactoryClass + +ECollectionBackendFactoryPrivate +e_collection_backend_factory_get_type +
+ +
e-data-factory EDataFactory EDataFactory @@ -209,6 +320,69 @@ e_offline_listener_get_type
+e-server-side-source +EServerSideSource +EServerSideSource +e_server_side_source_get_user_dir +e_server_side_source_new_user_file +e_server_side_source_uid_from_file +e_server_side_source_new +e_server_side_source_new_memory_only +e_server_side_source_load +e_server_side_source_get_file +e_server_side_source_get_node +e_server_side_source_get_server +e_server_side_source_get_allow_auth_prompt +e_server_side_source_set_allow_auth_prompt +e_server_side_source_set_removable +e_server_side_source_set_writable + +E_SERVER_SIDE_SOURCE +E_IS_SERVER_SIDE_SOURCE +E_TYPE_SERVER_SIDE_SOURCE +E_SERVER_SIDE_SOURCE_CLASS +E_IS_SERVER_SIDE_SOURCE_CLASS +E_SERVER_SIDE_SOURCE_GET_CLASS +EServerSideSourceClass + +EServerSideSourcePrivate +e_server_side_source_get_type +
+ +
+e-source-registry-server +ESourceRegistryServer +E_SOURCE_REGISTRY_SERVER_OBJECT_PATH +ESourceRegistryServer +e_source_registry_server_new +e_source_registry_server_add_source +e_source_registry_server_remove_source +e_source_registry_server_queue_auth_session +e_source_registry_server_load_all +ESourcePermissionFlags +e_source_registry_server_load_directory +e_source_registry_server_load_file +e_source_registry_server_load_error +e_source_registry_server_ref_source +e_source_registry_server_list_sources +e_source_registry_server_ref_backend_factory + +E_SOURCE_REGISTRY_SERVER +E_IS_SOURCE_REGISTRY_SERVER +E_TYPE_SOURCE_REGISTRY_SERVER +E_SOURCE_REGISTRY_SERVER_CLASS +E_IS_SOURCE_REGISTRY_SERVER_CLASS +E_SOURCE_REGISTRY_SERVER_GET_CLASS +E_TYPE_SOURCE_PERMISSION_FLAGS +E_TYPE_PASSWORD_REMEMBER_TYPE +ESourceRegistryServerClass + +ESourceRegistryServerPrivate +e_source_registry_server_get_type +e_source_permission_flags_get_type +
+ +
e-sqlite3-vfs e_sqlite3_vfs_init
diff --git a/docs/reference/libebackend/libebackend.types b/docs/reference/libebackend/libebackend.types index e23f54b..7ecf584 100644 --- a/docs/reference/libebackend/libebackend.types +++ b/docs/reference/libebackend/libebackend.types @@ -1,5 +1,9 @@ +#include +#include #include #include +#include +#include #include #include #include @@ -7,9 +11,15 @@ #include #include #include +#include +#include +e_authentication_mediator_get_type +e_authentication_session_get_type e_backend_get_type e_backend_factory_get_type +e_collection_backend_get_type +e_collection_backend_factory_get_type e_data_factory_get_type e_dbus_server_get_type e_extensible_get_type @@ -17,3 +27,5 @@ e_extension_get_type e_file_cache_get_type e_module_get_type e_offline_listener_get_type +e_server_side_source_get_type +e_source_registry_server_get_type diff --git a/libebackend/Makefile.am b/libebackend/Makefile.am index 0b6c2a6..378b205 100644 --- a/libebackend/Makefile.am +++ b/libebackend/Makefile.am @@ -14,18 +14,27 @@ libebackend_1_2_la_CPPFLAGS = \ $(AM_CPPFLAGS) \ -I$(top_srcdir) \ -I$(top_srcdir)/private \ - -DG_LOG_DOMAIN=\"e-data-server\" \ + -DG_LOG_DOMAIN=\"libebackend\" \ + -DMODULE_DIRECTORY=\"$(moduledir)\" \ + -DE_DATA_SERVER_PRIVDATADIR=\"$(privdatadir)\" \ + -DSYSTEM_WIDE_RO_SOURCES_DIRECTORY=\"$(ro_sourcesdir)\" \ + -DSYSTEM_WIDE_RW_SOURCES_DIRECTORY=\"$(rw_sourcesdir)\" \ $(DB_CFLAGS) \ $(SQLITE3_CFLAGS) \ $(E_BACKEND_CFLAGS) \ + $(GCR_BASE_CFLAGS) \ $(GIO_UNIX_CFLAGS) \ $(CODE_COVERAGE_CFLAGS) \ $(NULL) libebackend_1_2_la_SOURCES = \ $(BUILT_SOURCES) \ + e-authentication-mediator.c \ + e-authentication-session.c \ e-backend.c \ e-backend-factory.c \ + e-collection-backend.c \ + e-collection-backend-factory.c \ e-data-factory.c \ e-dbus-server.c \ e-extensible.c \ @@ -34,13 +43,17 @@ libebackend_1_2_la_SOURCES = \ e-dbhash.c \ e-db3-utils.c \ e-module.c \ + e-server-side-source.c \ + e-source-registry-server.c \ e-sqlite3-vfs.c \ e-file-cache.c libebackend_1_2_la_LIBADD = \ $(top_builddir)/libedataserver/libedataserver-1.2.la \ + $(top_builddir)/private/libedbus-private.la \ $(E_BACKEND_LIBS) \ $(SQLITE3_LIBS) \ + $(GCR_BASE_LIBS) \ $(GIO_UNIX_LIBS) \ $(DB_LIBS) @@ -52,9 +65,13 @@ libebackend_1_2_la_LDFLAGS = \ libebackendincludedir = $(privincludedir)/libebackend libebackendinclude_HEADERS = \ + e-authentication-mediator.h \ + e-authentication-session.h \ e-backend.h \ e-backend-enums.h \ e-backend-factory.h \ + e-collection-backend.h \ + e-collection-backend-factory.h \ e-data-factory.h \ e-dbus-server.h \ e-extensible.h \ @@ -63,6 +80,8 @@ libebackendinclude_HEADERS = \ e-db3-utils.h \ e-dbhash.h \ e-module.h \ + e-server-side-source.h \ + e-source-registry-server.h \ e-sqlite3-vfs.h \ e-file-cache.h diff --git a/libebackend/e-authentication-mediator.c b/libebackend/e-authentication-mediator.c new file mode 100644 index 0000000..0af6805 --- /dev/null +++ b/libebackend/e-authentication-mediator.c @@ -0,0 +1,1032 @@ +/* + * e-authentication-mediator.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-authentication-mediator + * @include: libebackend/e-authentication-mediator.h + * @short_description: Authenticator proxy for remote clients + * + * #EAuthenticationMediator runs on the registry D-Bus service. It mediates + * authentication attempts between the client requesting authentication and + * the server-side #EAuthenticationSession interacting with the user and/or + * secret service. It implements the #ESourceAuthenticator interface and + * securely transmits passwords to a remote #ESourceRegistry over D-Bus. + **/ + +#include "e-authentication-mediator.h" + +/* XXX Yeah, yeah... */ +#define GCR_API_SUBJECT_TO_CHANGE + +#include +#include +#include + +#include +#include + +/* Private D-Bus classes. */ +#include + +#define E_AUTHENTICATION_MEDIATOR_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_AUTHENTICATION_MEDIATOR, EAuthenticationMediatorPrivate)) + +/* How long should clients have to respond before timing out the + * authentication session? Need to balance allowing adequate time + * without blocking other requests too long if a client gets stuck. */ +#define INACTIVITY_TIMEOUT (2 * 60) /* in seconds */ + +typedef struct _AsyncContext AsyncContext; + +struct _EAuthenticationMediatorPrivate { + GDBusConnection *connection; + EDBusAuthenticator *interface; + GcrSecretExchange *secret_exchange; + gchar *object_path; + gchar *sender; + + GMutex *shared_data_lock; + + GQueue try_password_queue; + GQueue wait_for_client_queue; + + gboolean client_is_ready; + gboolean client_cancelled; + gboolean client_vanished; + + guint watcher_id; +}; + +struct _AsyncContext { + /* These point into the EAuthenticationMediatorPrivate + * struct. Do not free them in async_context_free(). */ + GMutex *shared_data_lock; + GQueue *operation_queue; + + GCancellable *cancellable; + gulong cancel_id; + guint timeout_id; +}; + +enum { + PROP_0, + PROP_CONNECTION, + PROP_OBJECT_PATH, + PROP_SENDER +}; + +/* Forward Declarations */ +static void e_authentication_mediator_initable_init + (GInitableIface *interface); +static void e_authentication_mediator_interface_init + (ESourceAuthenticatorInterface *interface); + +G_DEFINE_TYPE_WITH_CODE ( + EAuthenticationMediator, + e_authentication_mediator, + G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE ( + G_TYPE_INITABLE, + e_authentication_mediator_initable_init) + G_IMPLEMENT_INTERFACE ( + E_TYPE_SOURCE_AUTHENTICATOR, + e_authentication_mediator_interface_init)) + +static void +async_context_free (AsyncContext *async_context) +{ + if (async_context->cancellable != NULL) { + g_cancellable_disconnect ( + async_context->cancellable, + async_context->cancel_id); + g_object_unref (async_context->cancellable); + } + + if (async_context->timeout_id > 0) + g_source_remove (async_context->timeout_id); + + g_slice_free (AsyncContext, async_context); +} + +static void +authentication_mediator_name_vanished_cb (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + EAuthenticationMediator *mediator; + GSimpleAsyncResult *simple; + GQueue *queue; + + mediator = E_AUTHENTICATION_MEDIATOR (user_data); + + g_mutex_lock (mediator->priv->shared_data_lock); + + mediator->priv->client_vanished = TRUE; + + queue = &mediator->priv->try_password_queue; + + /* Notify any unfinished try_password() operations. */ + while ((simple = g_queue_pop_head (queue)) != NULL) { + g_simple_async_result_set_error ( + simple, G_IO_ERROR, G_IO_ERROR_CANCELLED, + "%s", _("Bus name vanished (client terminated?)")); + g_simple_async_result_complete_in_idle (simple); + g_object_unref (simple); + } + + queue = &mediator->priv->wait_for_client_queue; + + /* Notify any unfinished wait_for_client() operations. */ + while ((simple = g_queue_pop_head (queue)) != NULL) { + g_simple_async_result_set_error ( + simple, G_IO_ERROR, G_IO_ERROR_CANCELLED, + "%s", _("Bus name vanished (client terminated?)")); + g_simple_async_result_complete_in_idle (simple); + g_object_unref (simple); + } + + g_bus_unwatch_name (mediator->priv->watcher_id); + mediator->priv->watcher_id = 0; + + g_mutex_unlock (mediator->priv->shared_data_lock); +} + +static void +authentication_mediator_cancelled_cb (GCancellable *cancellable, + gpointer user_data) +{ + GSimpleAsyncResult *simple; + AsyncContext *async_context; + + simple = G_SIMPLE_ASYNC_RESULT (user_data); + async_context = g_simple_async_result_get_op_res_gpointer (simple); + + g_mutex_lock (async_context->shared_data_lock); + + /* Because we called g_simple_async_result_set_check_cancellable(), + * g_simple_async_result_propagate_error() will automatically set a + * cancelled error so we don't need to explicitly set one here. */ + if (g_queue_remove (async_context->operation_queue, simple)) { + g_simple_async_result_complete_in_idle (simple); + g_object_unref (simple); + } + + g_mutex_unlock (async_context->shared_data_lock); +} + +static gboolean +authentication_mediator_timeout_cb (gpointer user_data) +{ + GSimpleAsyncResult *simple; + AsyncContext *async_context; + + simple = G_SIMPLE_ASYNC_RESULT (user_data); + async_context = g_simple_async_result_get_op_res_gpointer (simple); + + g_mutex_lock (async_context->shared_data_lock); + + if (g_queue_remove (async_context->operation_queue, simple)) { + g_simple_async_result_set_error ( + simple, G_IO_ERROR, G_IO_ERROR_TIMED_OUT, + "%s", _("No response from client")); + g_simple_async_result_complete_in_idle (simple); + g_object_unref (simple); + } + + g_mutex_unlock (async_context->shared_data_lock); + + return FALSE; +} + +static gboolean +authentication_mediator_handle_ready (EDBusAuthenticator *interface, + GDBusMethodInvocation *invocation, + const gchar *encrypted_key, + EAuthenticationMediator *mediator) +{ + GcrSecretExchange *secret_exchange; + GSimpleAsyncResult *simple; + GQueue *queue; + + g_mutex_lock (mediator->priv->shared_data_lock); + + mediator->priv->client_is_ready = TRUE; + + secret_exchange = mediator->priv->secret_exchange; + gcr_secret_exchange_receive (secret_exchange, encrypted_key); + + queue = &mediator->priv->wait_for_client_queue; + + /* Notify any unfinished wait_for_client() operations. */ + while ((simple = g_queue_pop_head (queue)) != NULL) { + g_simple_async_result_complete_in_idle (simple); + g_object_unref (simple); + } + + g_mutex_unlock (mediator->priv->shared_data_lock); + + e_dbus_authenticator_complete_ready (interface, invocation); + + return TRUE; +} + +static gboolean +authentication_mediator_handle_cancel (EDBusAuthenticator *interface, + GDBusMethodInvocation *invocation, + EAuthenticationMediator *mediator) +{ + GSimpleAsyncResult *simple; + GQueue *queue; + + g_mutex_lock (mediator->priv->shared_data_lock); + + mediator->priv->client_cancelled = TRUE; + + queue = &mediator->priv->try_password_queue; + + /* Notify any unfinished try_password() operations. */ + while ((simple = g_queue_pop_head (queue)) != NULL) { + g_simple_async_result_set_error ( + simple, G_IO_ERROR, G_IO_ERROR_CANCELLED, + "%s", _("Client cancelled the operation")); + g_simple_async_result_complete_in_idle (simple); + g_object_unref (simple); + } + + queue = &mediator->priv->wait_for_client_queue; + + /* Notify any unfinished wait_for_client() operations. */ + while ((simple = g_queue_pop_head (queue)) != NULL) { + g_simple_async_result_set_error ( + simple, G_IO_ERROR, G_IO_ERROR_CANCELLED, + "%s", _("Client cancelled the operation")); + g_simple_async_result_complete_in_idle (simple); + g_object_unref (simple); + } + + g_mutex_unlock (mediator->priv->shared_data_lock); + + e_dbus_authenticator_complete_cancel (interface, invocation); + + return TRUE; +} + +static gboolean +authentication_mediator_handle_accepted (EDBusAuthenticator *interface, + GDBusMethodInvocation *invocation, + EAuthenticationMediator *mediator) +{ + GSimpleAsyncResult *simple; + GQueue *queue; + + g_mutex_lock (mediator->priv->shared_data_lock); + + queue = &mediator->priv->try_password_queue; + + if (g_queue_is_empty (queue)) + g_warning ("%s: Unexpected 'accepted' signal", G_STRFUNC); + + /* Notify any unfinished try_password() operations. */ + while ((simple = g_queue_pop_head (queue)) != NULL) { + g_simple_async_result_complete_in_idle (simple); + g_object_unref (simple); + } + + g_mutex_unlock (mediator->priv->shared_data_lock); + + e_dbus_authenticator_complete_accepted (interface, invocation); + + return TRUE; +} + +static gboolean +authentication_mediator_handle_rejected (EDBusAuthenticator *interface, + GDBusMethodInvocation *invocation, + EAuthenticationMediator *mediator) +{ + GSimpleAsyncResult *simple; + GQueue *queue; + + g_mutex_lock (mediator->priv->shared_data_lock); + + queue = &mediator->priv->try_password_queue; + + if (g_queue_is_empty (queue)) + g_warning ("%s: Unexpected 'rejected' signal", G_STRFUNC); + + /* Notify any unfinished try_password() operations. */ + while ((simple = g_queue_pop_head (queue)) != NULL) { + g_simple_async_result_set_error ( + simple, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED, + "%s", _("Client reports password was rejected")); + g_simple_async_result_complete_in_idle (simple); + g_object_unref (simple); + } + + g_mutex_unlock (mediator->priv->shared_data_lock); + + e_dbus_authenticator_complete_rejected (interface, invocation); + + return TRUE; +} + +static void +authentication_mediator_set_connection (EAuthenticationMediator *mediator, + GDBusConnection *connection) +{ + g_return_if_fail (G_IS_DBUS_CONNECTION (connection)); + g_return_if_fail (mediator->priv->connection == NULL); + + mediator->priv->connection = g_object_ref (connection); +} + +static void +authentication_mediator_set_object_path (EAuthenticationMediator *mediator, + const gchar *object_path) +{ + g_return_if_fail (object_path != NULL); + g_return_if_fail (mediator->priv->object_path == NULL); + + mediator->priv->object_path = g_strdup (object_path); +} + +static void +authentication_mediator_set_sender (EAuthenticationMediator *mediator, + const gchar *sender) +{ + g_return_if_fail (sender != NULL); + g_return_if_fail (mediator->priv->sender == NULL); + + mediator->priv->sender = g_strdup (sender); +} + +static void +authentication_mediator_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_CONNECTION: + authentication_mediator_set_connection ( + E_AUTHENTICATION_MEDIATOR (object), + g_value_get_object (value)); + return; + + case PROP_OBJECT_PATH: + authentication_mediator_set_object_path ( + E_AUTHENTICATION_MEDIATOR (object), + g_value_get_string (value)); + return; + + case PROP_SENDER: + authentication_mediator_set_sender ( + E_AUTHENTICATION_MEDIATOR (object), + g_value_get_string (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +authentication_mediator_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_CONNECTION: + g_value_set_object ( + value, + e_authentication_mediator_get_connection ( + E_AUTHENTICATION_MEDIATOR (object))); + return; + + case PROP_OBJECT_PATH: + g_value_set_string ( + value, + e_authentication_mediator_get_object_path ( + E_AUTHENTICATION_MEDIATOR (object))); + return; + + case PROP_SENDER: + g_value_set_string ( + value, + e_authentication_mediator_get_sender ( + E_AUTHENTICATION_MEDIATOR (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +authentication_mediator_dispose (GObject *object) +{ + EAuthenticationMediatorPrivate *priv; + GQueue *queue; + + priv = E_AUTHENTICATION_MEDIATOR_GET_PRIVATE (object); + + if (priv->connection != NULL) { + g_object_unref (priv->connection); + priv->connection = NULL; + } + + if (priv->interface != NULL) { + g_object_unref (priv->interface); + priv->interface = NULL; + } + + if (priv->secret_exchange != NULL) { + g_object_unref (priv->secret_exchange); + priv->secret_exchange = NULL; + } + + queue = &priv->wait_for_client_queue; + + while (!g_queue_is_empty (queue)) + g_object_unref (g_queue_pop_head (queue)); + + if (priv->watcher_id > 0) { + g_bus_unwatch_name (priv->watcher_id); + priv->watcher_id = 0; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_authentication_mediator_parent_class)-> + dispose (object); +} + +static void +authentication_mediator_finalize (GObject *object) +{ + EAuthenticationMediatorPrivate *priv; + + priv = E_AUTHENTICATION_MEDIATOR_GET_PRIVATE (object); + + g_mutex_free (priv->shared_data_lock); + + g_free (priv->object_path); + g_free (priv->sender); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_authentication_mediator_parent_class)-> + finalize (object); +} + +static void +authentication_mediator_constructed (GObject *object) +{ + EAuthenticationMediator *mediator; + GDBusConnection *connection; + const gchar *sender; + + mediator = E_AUTHENTICATION_MEDIATOR (object); + connection = e_authentication_mediator_get_connection (mediator); + sender = e_authentication_mediator_get_sender (mediator); + + /* This should notify us if the client process terminates. */ + mediator->priv->watcher_id = + g_bus_watch_name_on_connection ( + connection, sender, + G_BUS_NAME_WATCHER_FLAGS_NONE, + NULL, + authentication_mediator_name_vanished_cb, + mediator, (GDestroyNotify) NULL); + + /* Chain up to parent's constructed() method. */ + G_OBJECT_CLASS (e_authentication_mediator_parent_class)-> + constructed (object); +} + +static gboolean +authentication_mediator_initable_init (GInitable *initable, + GCancellable *cancellable, + GError **error) +{ + EAuthenticationMediator *mediator; + GDBusInterfaceSkeleton *interface; + GDBusConnection *connection; + const gchar *object_path; + + mediator = E_AUTHENTICATION_MEDIATOR (initable); + + interface = G_DBUS_INTERFACE_SKELETON (mediator->priv->interface); + connection = e_authentication_mediator_get_connection (mediator); + object_path = e_authentication_mediator_get_object_path (mediator); + + return g_dbus_interface_skeleton_export ( + interface, connection, object_path, error); +} + +static ESourceAuthenticationResult +authentication_mediator_try_password_sync (ESourceAuthenticator *auth, + const GString *password, + GCancellable *cancellable, + GError **error) +{ + ESourceAuthenticationResult auth_result; + GAsyncResult *async_result; + EAsyncClosure *closure; + + closure = e_async_closure_new (); + + e_source_authenticator_try_password ( + auth, password, cancellable, + e_async_closure_callback, closure); + + async_result = e_async_closure_wait (closure); + + auth_result = e_source_authenticator_try_password_finish ( + auth, async_result, error); + + e_async_closure_free (closure); + + return auth_result; +} + +static void +authentication_mediator_try_password (ESourceAuthenticator *auth, + const GString *password, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + EAuthenticationMediator *mediator; + GSimpleAsyncResult *simple; + AsyncContext *async_context; + + mediator = E_AUTHENTICATION_MEDIATOR (auth); + + async_context = g_slice_new0 (AsyncContext); + async_context->shared_data_lock = mediator->priv->shared_data_lock; + async_context->operation_queue = &mediator->priv->try_password_queue; + + simple = g_simple_async_result_new ( + G_OBJECT (mediator), callback, user_data, + authentication_mediator_try_password); + + g_simple_async_result_set_check_cancellable (simple, cancellable); + + g_simple_async_result_set_op_res_gpointer ( + simple, async_context, (GDestroyNotify) async_context_free); + + if (G_IS_CANCELLABLE (cancellable)) { + async_context->cancellable = g_object_ref (cancellable); + async_context->cancel_id = g_cancellable_connect ( + async_context->cancellable, + G_CALLBACK (authentication_mediator_cancelled_cb), + simple, (GDestroyNotify) NULL); + } + + async_context->timeout_id = g_timeout_add_seconds ( + INACTIVITY_TIMEOUT, + authentication_mediator_timeout_cb, simple); + + g_mutex_lock (mediator->priv->shared_data_lock); + + if (mediator->priv->client_cancelled) { + g_simple_async_result_set_error ( + simple, G_IO_ERROR, G_IO_ERROR_CANCELLED, + "%s", _("Client cancelled the operation")); + g_simple_async_result_complete_in_idle (simple); + + } else if (mediator->priv->client_vanished) { + g_simple_async_result_set_error ( + simple, G_IO_ERROR, G_IO_ERROR_CANCELLED, + "%s", _("Bus name vanished (client terminated?)")); + g_simple_async_result_complete_in_idle (simple); + + } else { + gchar *encrypted_secret; + + g_queue_push_tail ( + async_context->operation_queue, + g_object_ref (simple)); + + encrypted_secret = gcr_secret_exchange_send ( + mediator->priv->secret_exchange, password->str, -1); + + e_dbus_authenticator_emit_authenticate ( + mediator->priv->interface, encrypted_secret); + + g_free (encrypted_secret); + } + + g_mutex_unlock (mediator->priv->shared_data_lock); + + g_object_unref (simple); +} + +static ESourceAuthenticationResult +authentication_mediator_try_password_finish (ESourceAuthenticator *auth, + GAsyncResult *result, + GError **error) +{ + GSimpleAsyncResult *simple; + GError *local_error = NULL; + + simple = G_SIMPLE_ASYNC_RESULT (result); + + if (!g_simple_async_result_propagate_error (simple, &local_error)) + return E_SOURCE_AUTHENTICATION_ACCEPTED; + + if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED)) { + g_clear_error (&local_error); + return E_SOURCE_AUTHENTICATION_REJECTED; + } + + g_propagate_error (error, local_error); + + return E_SOURCE_AUTHENTICATION_ERROR; +} + +static void +e_authentication_mediator_class_init (EAuthenticationMediatorClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private (class, sizeof (EAuthenticationMediatorPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = authentication_mediator_set_property; + object_class->get_property = authentication_mediator_get_property; + object_class->dispose = authentication_mediator_dispose; + object_class->finalize = authentication_mediator_finalize; + object_class->constructed = authentication_mediator_constructed; + + g_object_class_install_property ( + object_class, + PROP_CONNECTION, + g_param_spec_object ( + "connection", + "Connection", + "The GDBusConnection on which to " + "export the authenticator interface", + G_TYPE_DBUS_CONNECTION, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property ( + object_class, + PROP_OBJECT_PATH, + g_param_spec_string ( + "object-path", + "Object Path", + "The object path at which to " + "export the authenticator interface", + NULL, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property ( + object_class, + PROP_SENDER, + g_param_spec_string ( + "sender", + "Sender", + "Unique bus name of the process that " + "initiated the authentication session", + NULL, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); +} + +static void +e_authentication_mediator_initable_init (GInitableIface *interface) +{ + interface->init = authentication_mediator_initable_init; +} + +static void +e_authentication_mediator_interface_init (ESourceAuthenticatorInterface *interface) +{ + interface->try_password_sync = authentication_mediator_try_password_sync; + interface->try_password = authentication_mediator_try_password; + interface->try_password_finish = authentication_mediator_try_password_finish; +} + +static void +e_authentication_mediator_init (EAuthenticationMediator *mediator) +{ + mediator->priv = E_AUTHENTICATION_MEDIATOR_GET_PRIVATE (mediator); + + mediator->priv->interface = e_dbus_authenticator_skeleton_new (); + mediator->priv->secret_exchange = gcr_secret_exchange_new (NULL); + + mediator->priv->shared_data_lock = g_mutex_new (); + + g_dbus_interface_skeleton_set_flags ( + G_DBUS_INTERFACE_SKELETON (mediator->priv->interface), + G_DBUS_INTERFACE_SKELETON_FLAGS_HANDLE_METHOD_INVOCATIONS_IN_THREAD); + + g_signal_connect ( + mediator->priv->interface, "handle-ready", + G_CALLBACK (authentication_mediator_handle_ready), mediator); + + g_signal_connect ( + mediator->priv->interface, "handle-cancel", + G_CALLBACK (authentication_mediator_handle_cancel), mediator); + + g_signal_connect ( + mediator->priv->interface, "handle-accepted", + G_CALLBACK (authentication_mediator_handle_accepted), mediator); + + g_signal_connect ( + mediator->priv->interface, "handle-rejected", + G_CALLBACK (authentication_mediator_handle_rejected), mediator); +} + +/** + * e_authentication_mediator_new: + * @connection: a #GDBusConnection + * @object_path: object path of the authentication session + * @sender: bus name of the client requesting authentication + * @error: return location for a #GError, or %NULL + * + * Creates a new #EAuthenticationMediator and exports the Authenticator + * D-Bus interface on @connection at @object_path. If the Authenticator + * interface fails to export, the function sets @error and returns %NULL. + * + * #EAuthenticationMediator watches the bus name of the client requesting + * authentication, given by @sender. If it sees the bus name vanish, it + * cancels the authentication session so the next authentication session + * can begin without delay. + * + * Returns: an #EAuthenticationMediator, or %NULL on error + * + * Since: 3.6 + **/ +ESourceAuthenticator * +e_authentication_mediator_new (GDBusConnection *connection, + const gchar *object_path, + const gchar *sender, + GError **error) +{ + g_return_val_if_fail (G_IS_DBUS_CONNECTION (connection), NULL); + g_return_val_if_fail (object_path != NULL, NULL); + g_return_val_if_fail (sender != NULL, NULL); + + return g_initable_new ( + E_TYPE_AUTHENTICATION_MEDIATOR, NULL, error, + "connection", connection, + "object-path", object_path, + "sender", sender, NULL); +} + +/** + * e_authentication_mediator_get_connection: + * @mediator: an #EAuthenticationMediator + * + * Returns the #GDBusConnection on which the Authenticator D-Bus interface + * is exported. + * + * Returns: the #GDBusConnection + * + * Since: 3.6 + **/ +GDBusConnection * +e_authentication_mediator_get_connection (EAuthenticationMediator *mediator) +{ + g_return_val_if_fail (E_IS_AUTHENTICATION_MEDIATOR (mediator), NULL); + + return mediator->priv->connection; +} + +/** + * e_authentication_mediator_get_object_path: + * @mediator: an #EAuthenticationMediator + * + * Returns the object path at which the Authenticator D-Bus interface is + * exported. + * + * Returns: the object path + * + * Since: 3.6 + **/ +const gchar * +e_authentication_mediator_get_object_path (EAuthenticationMediator *mediator) +{ + g_return_val_if_fail (E_IS_AUTHENTICATION_MEDIATOR (mediator), NULL); + + return mediator->priv->object_path; +} + +/** + * e_authentication_mediator_get_sender: + * @mediator: an #EAuthenticationMediator + * + * Returns the authentication client's unique bus name. + * + * Returns: the client's bus name + * + * Since: 3.6 + **/ +const gchar * +e_authentication_mediator_get_sender (EAuthenticationMediator *mediator) +{ + g_return_val_if_fail (E_IS_AUTHENTICATION_MEDIATOR (mediator), NULL); + + return mediator->priv->sender; +} + +/** + * e_authentication_mediator_wait_for_client_sync: + * @mediator: an #EAuthenticationMediator + * @cancellable: optional #GCancellable object, or %NULL + * @error: return location for a #GError, or %NULL + * + * Waits for the authentication client to indicate it is ready to begin + * authentication attempts. Call this function to synchronize with the + * client before initiating any authentication attempts through @mediator. + * + * If the authentication client's bus name vanishes or the client fails + * to signal it is ready before a timer expires, the function sets @error + * and returns %FALSE. + * + * Returns: %TRUE if the client is ready, %FALSE if an error occurred + * + * Since: 3.6 + **/ +gboolean +e_authentication_mediator_wait_for_client_sync (EAuthenticationMediator *mediator, + GCancellable *cancellable, + GError **error) +{ + EAsyncClosure *closure; + GAsyncResult *result; + gboolean success; + + g_return_val_if_fail (E_IS_AUTHENTICATION_MEDIATOR (mediator), FALSE); + + closure = e_async_closure_new (); + + e_authentication_mediator_wait_for_client ( + mediator, cancellable, e_async_closure_callback, closure); + + result = e_async_closure_wait (closure); + + success = e_authentication_mediator_wait_for_client_finish ( + mediator, result, error); + + e_async_closure_free (closure); + + return success; +} + +/** + * e_authentication_mediator_wait_for_client: + * @mediator: an #EAuthenticationMediator + * @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 + * + * Asynchronously waits for the authentication client to indicate it + * is ready to being authentication attempts. Call this function to + * synchronize with the client before initiating any authentication + * attempts through @mediator. + * + * When the operation is finished, @callback will be called. You can then + * call e_authentication_mediator_wait_for_client_finished() to get the + * result of the operation. + * + * Since: 3.6 + **/ +void +e_authentication_mediator_wait_for_client (EAuthenticationMediator *mediator, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GSimpleAsyncResult *simple; + AsyncContext *async_context; + + g_return_if_fail (E_IS_AUTHENTICATION_MEDIATOR (mediator)); + + async_context = g_slice_new0 (AsyncContext); + async_context->shared_data_lock = mediator->priv->shared_data_lock; + async_context->operation_queue = &mediator->priv->wait_for_client_queue; + + simple = g_simple_async_result_new ( + G_OBJECT (mediator), callback, user_data, + e_authentication_mediator_wait_for_client); + + g_simple_async_result_set_check_cancellable (simple, cancellable); + + g_simple_async_result_set_op_res_gpointer ( + simple, async_context, (GDestroyNotify) async_context_free); + + if (G_IS_CANCELLABLE (cancellable)) { + async_context->cancellable = g_object_ref (cancellable); + async_context->cancel_id = g_cancellable_connect ( + cancellable, + G_CALLBACK (authentication_mediator_cancelled_cb), + simple, (GDestroyNotify) NULL); + } + + async_context->timeout_id = g_timeout_add_seconds ( + INACTIVITY_TIMEOUT, authentication_mediator_timeout_cb, simple); + + g_mutex_lock (mediator->priv->shared_data_lock); + + if (mediator->priv->client_is_ready) { + g_simple_async_result_complete_in_idle (simple); + + } else if (mediator->priv->client_cancelled) { + g_simple_async_result_set_error ( + simple, G_IO_ERROR, G_IO_ERROR_CANCELLED, + "%s", _("Client cancelled the operation")); + g_simple_async_result_complete_in_idle (simple); + + } else if (mediator->priv->client_vanished) { + g_simple_async_result_set_error ( + simple, G_IO_ERROR, G_IO_ERROR_CANCELLED, + "%s", _("Bus name vanished (client terminated?)")); + g_simple_async_result_complete_in_idle (simple); + + } else { + g_queue_push_tail ( + async_context->operation_queue, + g_object_ref (simple)); + } + + g_mutex_unlock (mediator->priv->shared_data_lock); + + g_object_unref (simple); +} + +/** + * e_authentication_mediator_wait_for_client_finish: + * @mediator: an #EAuthenticationMediator + * @result: a #GAsyncResult + * @error: return location for a #GError, or %NULL + * + * Finishes the operation started with + * e_authentication_mediator_wait_for_client(). + * + * If the authentication client's bus name vanishes or the client fails + * to signal it is ready before a timer expires, the function sets @error + * and returns %FALSE. + * + * Returns: %TRUE if the client is ready, %FALSE if an error occurred + * + * Since: 3.6 + **/ +gboolean +e_authentication_mediator_wait_for_client_finish (EAuthenticationMediator *mediator, + GAsyncResult *result, + GError **error) +{ + GSimpleAsyncResult *simple; + + g_return_val_if_fail ( + g_simple_async_result_is_valid ( + result, G_OBJECT (mediator), + e_authentication_mediator_wait_for_client), FALSE); + + simple = G_SIMPLE_ASYNC_RESULT (result); + + /* Assume success unless a GError is set. */ + return !g_simple_async_result_propagate_error (simple, error); +} + +/** + * e_authentication_mediator_dismiss: + * @mediator: an #EAuthenticationMediator + * + * Signals to the authentication client that the user declined to provide a + * password when prompted and that the authentication session has terminated. + * This is also called when a server-side error has occurred, but the client + * doesn't need to know the difference. + * + * Since: 3.6 + **/ +void +e_authentication_mediator_dismiss (EAuthenticationMediator *mediator) +{ + g_return_if_fail (E_IS_AUTHENTICATION_MEDIATOR (mediator)); + + e_dbus_authenticator_emit_dismissed (mediator->priv->interface); +} + diff --git a/libebackend/e-authentication-mediator.h b/libebackend/e-authentication-mediator.h new file mode 100644 index 0000000..8161ffc --- /dev/null +++ b/libebackend/e-authentication-mediator.h @@ -0,0 +1,100 @@ +/* + * e-authentication-mediator.h + * + * 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 + * + */ + +#ifndef E_AUTHENTICATION_MEDIATOR_H +#define E_AUTHENTICATION_MEDIATOR_H + +#include + +/* Standard GObject macros */ +#define E_TYPE_AUTHENTICATION_MEDIATOR \ + (e_authentication_mediator_get_type ()) +#define E_AUTHENTICATION_MEDIATOR(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_AUTHENTICATION_MEDIATOR, EAuthenticationMediator)) +#define E_AUTHENTICATION_MEDIATOR_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_AUTHENTICATION_MEDIATOR, EAuthenticationMediatorClass)) +#define E_IS_AUTHENTICATION_MEDIATOR(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_AUTHENTICATION_MEDIATOR)) +#define E_IS_AUTHENTICATION_MEDIATOR_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_AUTHENTICATION_MEDIATOR)) +#define E_AUTHENTICATION_MEDIATOR_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_AUTHENTICATION_MEDIATOR, EAuthenticationMediatorClass)) + +G_BEGIN_DECLS + +typedef struct _EAuthenticationMediator EAuthenticationMediator; +typedef struct _EAuthenticationMediatorClass EAuthenticationMediatorClass; +typedef struct _EAuthenticationMediatorPrivate EAuthenticationMediatorPrivate; + +/** + * EAuthenticationMediator: + * + * Contains only private data that should be read and manipulated using the + * functions below. + * + * Since: 3.6 + **/ +struct _EAuthenticationMediator { + GObject parent; + EAuthenticationMediatorPrivate *priv; +}; + +struct _EAuthenticationMediatorClass { + GObjectClass parent_class; +}; + +GType e_authentication_mediator_get_type + (void) G_GNUC_CONST; +ESourceAuthenticator * + e_authentication_mediator_new + (GDBusConnection *connection, + const gchar *object_path, + const gchar *sender, + GError **error); +GDBusConnection * + e_authentication_mediator_get_connection + (EAuthenticationMediator *mediator); +const gchar * e_authentication_mediator_get_object_path + (EAuthenticationMediator *mediator); +const gchar * e_authentication_mediator_get_sender + (EAuthenticationMediator *mediator); +gboolean e_authentication_mediator_wait_for_client_sync + (EAuthenticationMediator *mediator, + GCancellable *cancellable, + GError **error); +void e_authentication_mediator_wait_for_client + (EAuthenticationMediator *mediator, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean e_authentication_mediator_wait_for_client_finish + (EAuthenticationMediator *mediator, + GAsyncResult *result, + GError **error); +void e_authentication_mediator_dismiss + (EAuthenticationMediator *mediator); + +G_END_DECLS + +#endif /* E_AUTHENTICATION_MEDIATOR_H */ + diff --git a/libebackend/e-authentication-session.c b/libebackend/e-authentication-session.c new file mode 100644 index 0000000..0e6ebd1 --- /dev/null +++ b/libebackend/e-authentication-session.c @@ -0,0 +1,1745 @@ +/* + * e-authentication-session.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-authentication-session + * @include: libedataserver/e-authentication-session.h + * @short_description: Centralized authentication management + * + * #EAuthenticationSession provides centralized password management and + * password prompting for all clients of the registry D-Bus service. + * + * An #EAuthenticationSession is created within the registry D-Bus service + * when the service receives a request to authenticate some data source. + * Clients can issue requests by calling e_source_registry_authenticate(). + * Requests can also come from any #ECollectionBackend running within the + * service itself. + * + * An #EAuthenticationSession requires some object implementing the + * #ESourceAuthenticator interface to verify stored or user-provided + * passwords. #EAuthenticationMediator is used for client-issued + * authentication requests. Custom collection backends derived from + * #ECollectionBackend usually implement the #ESourceAuthenticator + * interface themselves. + * + * The #EAuthenticationSession is then handed to #ESourceRegistryServer + * through e_source_registry_server_queue_auth_session() where it waits + * in line behind other previously added authentication sessions. When + * its turn comes, the server calls e_authentication_session_execute() + * to begin the interactive authentication session. + **/ + +#include "e-authentication-session.h" + +/* XXX Yeah, yeah... */ +#define GCR_API_SUBJECT_TO_CHANGE + +#include +#include +#include +#include + +/* Private D-Bus classes. */ +#include + +#include +#include +#include +#include + +#define E_AUTHENTICATION_SESSION_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_AUTHENTICATION_SESSION, EAuthenticationSessionPrivate)) + +/* Wait forever for a system prompt. */ +#define SYSTEM_PROMPT_TIMEOUT (-1) + +#define KEYRING_ITEM_ATTRIBUTE_NAME "e-source-uid" +#define KEYRING_ITEM_DISPLAY_FORMAT "Evolution Data Source %s" + +typedef struct _AsyncContext AsyncContext; + +struct _EAuthenticationSessionPrivate { + ESourceRegistryServer *server; + ESourceAuthenticator *authenticator; + gchar *source_uid; + + /* These are for configuring system prompts. */ + GMutex *property_lock; + gchar *prompt_title; + gchar *prompt_message; + gchar *prompt_description; +}; + +struct _AsyncContext { + EAuthenticationSessionResult auth_result; + gchar *password; + gboolean permanently; +}; + +enum { + PROP_0, + PROP_AUTHENTICATOR, + PROP_PROMPT_DESCRIPTION, + PROP_PROMPT_MESSAGE, + PROP_PROMPT_TITLE, + PROP_SERVER, + PROP_SOURCE_UID +}; + +static GnomeKeyringPasswordSchema schema = { + GNOME_KEYRING_ITEM_GENERIC_SECRET, + { + { KEYRING_ITEM_ATTRIBUTE_NAME, + GNOME_KEYRING_ATTRIBUTE_TYPE_STRING }, + { NULL, 0 } + } +}; + +/* Forward Declarations */ +static void authentication_session_msg + (EAuthenticationSession *session, + const gchar *format, + ...) G_GNUC_PRINTF (2, 3); + +G_DEFINE_TYPE ( + EAuthenticationSession, + e_authentication_session, + G_TYPE_OBJECT) + +static void +async_context_free (AsyncContext *async_context) +{ + g_free (async_context->password); + g_slice_free (AsyncContext, async_context); +} + +static void +authentication_session_msg (EAuthenticationSession *session, + const gchar *format, + ...) +{ + GString *buffer; + va_list args; + + buffer = g_string_sized_new (256); + + g_string_append_printf ( + buffer, "AUTH (%s): ", + session->priv->source_uid); + + va_start (args, format); + g_string_append_vprintf (buffer, format, args); + va_end (args); + + g_print ("%s\n", buffer->str); + + g_string_free (buffer, TRUE); +} + +static void +authentication_session_set_authenticator (EAuthenticationSession *session, + ESourceAuthenticator *authenticator) +{ + g_return_if_fail (E_IS_SOURCE_AUTHENTICATOR (authenticator)); + g_return_if_fail (session->priv->authenticator == NULL); + + session->priv->authenticator = g_object_ref (authenticator); +} + +static void +authentication_session_set_server (EAuthenticationSession *session, + ESourceRegistryServer *server) +{ + g_return_if_fail (E_IS_SOURCE_REGISTRY_SERVER (server)); + g_return_if_fail (session->priv->server == NULL); + + session->priv->server = g_object_ref (server); +} + +static void +authentication_session_set_source_uid (EAuthenticationSession *session, + const gchar *source_uid) +{ + g_return_if_fail (source_uid != NULL); + g_return_if_fail (session->priv->source_uid == NULL); + + session->priv->source_uid = g_strdup (source_uid); +} + +static void +authentication_session_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_AUTHENTICATOR: + authentication_session_set_authenticator ( + E_AUTHENTICATION_SESSION (object), + g_value_get_object (value)); + return; + + case PROP_PROMPT_DESCRIPTION: + e_authentication_session_set_prompt_description ( + E_AUTHENTICATION_SESSION (object), + g_value_get_string (value)); + return; + + case PROP_PROMPT_MESSAGE: + e_authentication_session_set_prompt_message ( + E_AUTHENTICATION_SESSION (object), + g_value_get_string (value)); + return; + + case PROP_PROMPT_TITLE: + e_authentication_session_set_prompt_title ( + E_AUTHENTICATION_SESSION (object), + g_value_get_string (value)); + return; + + case PROP_SERVER: + authentication_session_set_server ( + E_AUTHENTICATION_SESSION (object), + g_value_get_object (value)); + return; + + case PROP_SOURCE_UID: + authentication_session_set_source_uid ( + E_AUTHENTICATION_SESSION (object), + g_value_get_string (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +authentication_session_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_AUTHENTICATOR: + g_value_set_object ( + value, + e_authentication_session_get_authenticator ( + E_AUTHENTICATION_SESSION (object))); + return; + + case PROP_PROMPT_DESCRIPTION: + g_value_take_string ( + value, + e_authentication_session_dup_prompt_description ( + E_AUTHENTICATION_SESSION (object))); + return; + + case PROP_PROMPT_MESSAGE: + g_value_take_string ( + value, + e_authentication_session_dup_prompt_message ( + E_AUTHENTICATION_SESSION (object))); + return; + + case PROP_PROMPT_TITLE: + g_value_take_string ( + value, + e_authentication_session_dup_prompt_title ( + E_AUTHENTICATION_SESSION (object))); + return; + + case PROP_SERVER: + g_value_set_object ( + value, + e_authentication_session_get_server ( + E_AUTHENTICATION_SESSION (object))); + return; + + case PROP_SOURCE_UID: + g_value_set_string ( + value, + e_authentication_session_get_source_uid ( + E_AUTHENTICATION_SESSION (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +authentication_session_dispose (GObject *object) +{ + EAuthenticationSessionPrivate *priv; + + priv = E_AUTHENTICATION_SESSION_GET_PRIVATE (object); + + if (priv->server != NULL) { + g_object_unref (priv->server); + priv->server = NULL; + } + + if (priv->authenticator != NULL) { + g_object_unref (priv->authenticator); + priv->authenticator = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_authentication_session_parent_class)-> + dispose (object); +} + +static void +authentication_session_finalize (GObject *object) +{ + EAuthenticationSessionPrivate *priv; + + priv = E_AUTHENTICATION_SESSION_GET_PRIVATE (object); + + g_mutex_free (priv->property_lock); + + g_free (priv->source_uid); + g_free (priv->prompt_title); + g_free (priv->prompt_message); + g_free (priv->prompt_description); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_authentication_session_parent_class)-> + finalize (object); +} + +static void +authentication_session_constructed (GObject *object) +{ + EAuthenticationSession *session; + ESourceAuthenticator *authenticator; + ESourceRegistryServer *server; + ESource *source; + const gchar *source_uid; + + session = E_AUTHENTICATION_SESSION (object); + + /* Chain up to parent's constructed() method. */ + G_OBJECT_CLASS (e_authentication_session_parent_class)-> + constructed (object); + + /* If the server knows about the data source UID we've been + * given, then we can auto-configure our own prompt strings. */ + + server = e_authentication_session_get_server (session); + source_uid = e_authentication_session_get_source_uid (session); + authenticator = e_authentication_session_get_authenticator (session); + + source = e_source_registry_server_ref_source (server, source_uid); + if (source != NULL) { + gchar *prompt_title = NULL; + gchar *prompt_message = NULL; + gchar *prompt_description = NULL; + + e_source_authenticator_get_prompt_strings ( + authenticator, source, + &prompt_title, + &prompt_message, + &prompt_description); + + g_object_set ( + session, + "prompt-title", prompt_title, + "prompt-message", prompt_message, + "prompt-description", prompt_description, + NULL); + + g_free (prompt_title); + g_free (prompt_message); + g_free (prompt_description); + + g_object_unref (source); + } +} + +/* Helper for authentication_session_execute() */ +static void +authentication_session_execute_thread (GSimpleAsyncResult *simple, + GObject *object, + GCancellable *cancellable) +{ + AsyncContext *async_context; + GError *error = NULL; + + async_context = g_simple_async_result_get_op_res_gpointer (simple); + + async_context->auth_result = + e_authentication_session_execute_sync ( + E_AUTHENTICATION_SESSION (object), + cancellable, &error); + + if (error != NULL) + g_simple_async_result_take_error (simple, error); +} + +static EAuthenticationSessionResult +authentication_session_execute_sync (EAuthenticationSession *session, + GCancellable *cancellable, + GError **error) +{ + ESourceAuthenticator *authenticator; + EAuthenticationSessionResult session_result; + ESourceAuthenticationResult auth_result; + ESourceRegistryServer *server; + ESource *source; + GcrPrompt *prompt; + GString *password_string; + gboolean allow_auth_prompt; + const gchar *label; + const gchar *source_uid; + const gchar *prompt_password; + gchar *stored_password = NULL; + gboolean success; + GError *local_error = NULL; + + /* XXX I moved the execute() operation into a class method thinking + * we might someday want to subclass EAuthenticationSession and + * override this method to make it behave differently. + * + * It would be a little premature to do this now, but we might + * also want to use the basic algorithm here as a template and + * turn the password lookup/delete/store operations into class + * methods that could also be overridden. I reserved adequate + * space in the class struct for this should the need arise. + * + * For now though we'll keep it simple. I don't want to over- + * engineer this too much in trying to make it future-proof. + */ + + authentication_session_msg (session, "Initiated"); + + server = e_authentication_session_get_server (session); + source_uid = e_authentication_session_get_source_uid (session); + authenticator = e_authentication_session_get_authenticator (session); + + success = e_authentication_session_lookup_password_sync ( + session, cancellable, &stored_password, error); + + if (!success) { + session_result = E_AUTHENTICATION_SESSION_ERROR; + goto exit; + } + + auth_result = E_SOURCE_AUTHENTICATION_REJECTED; + + /* If we found a stored password, signal the client without + * interrupting the user. Note, if the client responds with + * REJECTED, we'll have to interrupt the user after all. */ + if (stored_password != NULL) { + password_string = g_string_new (stored_password); + + auth_result = e_source_authenticator_try_password_sync ( + authenticator, password_string, cancellable, error); + + g_string_free (password_string, TRUE); + password_string = NULL; + + g_free (stored_password); + stored_password = NULL; + } + + if (auth_result == E_SOURCE_AUTHENTICATION_ERROR) { + session_result = E_AUTHENTICATION_SESSION_ERROR; + goto exit; + } + + if (auth_result == E_SOURCE_AUTHENTICATION_ACCEPTED) { + session_result = E_AUTHENTICATION_SESSION_SUCCESS; + goto exit; + } + + g_warn_if_fail (auth_result == E_SOURCE_AUTHENTICATION_REJECTED); + + /* The stored password is bad so delete it from the keyring. + * Failure here does not affect the outcome of this operation, + * but leave a breadcrumb as evidence that something went wrong. */ + + e_authentication_session_delete_password_sync ( + session, cancellable, &local_error); + + if (local_error != NULL) { + g_warning ("%s: %s", G_STRFUNC, local_error->message); + g_clear_error (&local_error); + } + + /* This will return NULL if the authenticating data source + * has not yet been submitted to the D-Bus registry service. */ + source = e_source_registry_server_ref_source (server, source_uid); + if (source != NULL) { + allow_auth_prompt = + e_server_side_source_get_allow_auth_prompt ( + E_SERVER_SIDE_SOURCE (source)); + g_object_unref (source); + } else { + allow_auth_prompt = TRUE; + } + + /* Check if we're allowed to interrupt the user for a password. + * If not, we have no choice but to dismiss the authentication + * request. */ + if (!allow_auth_prompt) { + session_result = E_AUTHENTICATION_SESSION_DISMISSED; + goto exit; + } + + /* Configure a system prompt. */ + + prompt = gcr_system_prompt_open ( + SYSTEM_PROMPT_TIMEOUT, cancellable, error); + + if (prompt == NULL) { + session_result = E_AUTHENTICATION_SESSION_ERROR; + goto exit; + } + + g_object_bind_property ( + session, "prompt-title", + prompt, "title", + G_BINDING_SYNC_CREATE); + + g_object_bind_property ( + session, "prompt-message", + prompt, "message", + G_BINDING_SYNC_CREATE); + + g_object_bind_property ( + session, "prompt-description", + prompt, "description", + G_BINDING_SYNC_CREATE); + + label = _("Add this password to your keyring"); + gcr_prompt_set_choice_label (prompt, label); + gcr_prompt_set_choice_chosen (prompt, TRUE); + +try_again: + + /* Prompt the user for a password. */ + + prompt_password = gcr_prompt_password ( + prompt, cancellable, &local_error); + + if (local_error != NULL) { + session_result = E_AUTHENTICATION_SESSION_ERROR; + g_propagate_error (error, local_error); + local_error = NULL; + goto close_prompt; + } + + /* No password and no error indicates a dismissal. */ + if (prompt_password == NULL) { + session_result = E_AUTHENTICATION_SESSION_DISMISSED; + goto close_prompt; + } + + /* Attempt authentication with the provided password. */ + + password_string = g_string_new (prompt_password); + + auth_result = e_source_authenticator_try_password_sync ( + authenticator, password_string, cancellable, error); + + g_string_free (password_string, TRUE); + password_string = NULL; + + if (auth_result == E_SOURCE_AUTHENTICATION_ERROR) { + session_result = E_AUTHENTICATION_SESSION_ERROR; + goto close_prompt; + } + + if (auth_result == E_SOURCE_AUTHENTICATION_ACCEPTED) { + gboolean permanently; + + permanently = gcr_prompt_get_choice_chosen (prompt); + session_result = E_AUTHENTICATION_SESSION_SUCCESS; + + /* Failure here does not affect the outcome of this + * operation, but leave a breadcrumb as evidence that + * something went wrong. */ + + e_authentication_session_store_password_sync ( + session, prompt_password, permanently, + cancellable, &local_error); + + if (local_error != NULL) { + g_warning ("%s: %s", G_STRFUNC, local_error->message); + g_clear_error (&local_error); + } + + goto close_prompt; + } + + g_warn_if_fail (auth_result == E_SOURCE_AUTHENTICATION_REJECTED); + + gcr_prompt_set_warning (prompt, _("Password was incorrect")); + + goto try_again; + +close_prompt: + + /* Failure here does not affect the outcome of this operation, + * but leave a breadcrumb as evidence that something went wrong. */ + + gcr_system_prompt_close ( + GCR_SYSTEM_PROMPT (prompt), + cancellable, &local_error); + + if (local_error != NULL) { + g_warning ("%s: %s", G_STRFUNC, local_error->message); + g_clear_error (&local_error); + } + + g_object_unref (prompt); + +exit: + + switch (session_result) { + case E_AUTHENTICATION_SESSION_ERROR: + authentication_session_msg ( + session, "Complete (ERROR)"); + break; + case E_AUTHENTICATION_SESSION_SUCCESS: + authentication_session_msg ( + session, "Complete (SUCCESS)"); + break; + case E_AUTHENTICATION_SESSION_DISMISSED: + authentication_session_msg ( + session, "Complete (DISMISSED)"); + break; + default: + g_warn_if_reached (); + } + + return session_result; +} + +static void +authentication_session_execute (EAuthenticationSession *session, + gint io_priority, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GSimpleAsyncResult *simple; + AsyncContext *async_context; + + g_return_if_fail (E_IS_AUTHENTICATION_SESSION (session)); + + async_context = g_slice_new0 (AsyncContext); + + simple = g_simple_async_result_new ( + G_OBJECT (session), callback, user_data, + e_authentication_session_execute); + + g_simple_async_result_set_check_cancellable (simple, cancellable); + + g_simple_async_result_set_op_res_gpointer ( + simple, async_context, (GDestroyNotify) async_context_free); + + g_simple_async_result_run_in_thread ( + simple, authentication_session_execute_thread, + io_priority, cancellable); + + g_object_unref (simple); +} + +static EAuthenticationSessionResult +authentication_session_execute_finish (EAuthenticationSession *session, + GAsyncResult *result, + GError **error) +{ + GSimpleAsyncResult *simple; + AsyncContext *async_context; + + g_return_val_if_fail ( + g_simple_async_result_is_valid ( + result, G_OBJECT (session), + e_authentication_session_execute), + E_AUTHENTICATION_SESSION_DISMISSED); + + simple = G_SIMPLE_ASYNC_RESULT (result); + async_context = g_simple_async_result_get_op_res_gpointer (simple); + + if (g_simple_async_result_propagate_error (simple, error)) + return E_AUTHENTICATION_SESSION_ERROR; + + return async_context->auth_result; +} + +static void +e_authentication_session_class_init (EAuthenticationSessionClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private ( + class, sizeof (EAuthenticationSessionPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = authentication_session_set_property; + object_class->get_property = authentication_session_get_property; + object_class->dispose = authentication_session_dispose; + object_class->finalize = authentication_session_finalize; + object_class->constructed = authentication_session_constructed; + + class->execute_sync = authentication_session_execute_sync; + class->execute = authentication_session_execute; + class->execute_finish = authentication_session_execute_finish; + + g_object_class_install_property ( + object_class, + PROP_AUTHENTICATOR, + g_param_spec_object ( + "authenticator", + "Authenticator", + "Handles authentication attempts", + E_TYPE_SOURCE_AUTHENTICATOR, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property ( + object_class, + PROP_PROMPT_DESCRIPTION, + g_param_spec_string ( + "prompt-description", + "Prompt Description", + "The detailed description of the prompt", + NULL, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property ( + object_class, + PROP_PROMPT_MESSAGE, + g_param_spec_string ( + "prompt-message", + "Prompt Message", + "The prompt message for the user", + NULL, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property ( + object_class, + PROP_PROMPT_TITLE, + g_param_spec_string ( + "prompt-title", + "Prompt Title", + "The title of the prompt", + NULL, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property ( + object_class, + PROP_SERVER, + g_param_spec_object ( + "server", + "Server", + "The server to which the session belongs", + E_TYPE_SOURCE_REGISTRY_SERVER, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property ( + object_class, + PROP_SOURCE_UID, + g_param_spec_string ( + "source-uid", + "Source UID", + "Unique ID of the data source being authenticated", + NULL, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); +} + +static void +e_authentication_session_init (EAuthenticationSession *session) +{ + session->priv = E_AUTHENTICATION_SESSION_GET_PRIVATE (session); + session->priv->property_lock = g_mutex_new (); +} + +GQuark +e_authentication_session_error_quark (void) +{ + static GQuark quark = 0; + + if (G_UNLIKELY (quark == 0)) { + const gchar *string; + string = "e-authentication-session-error-quark"; + quark = g_quark_from_static_string (string); + } + + return quark; +} + +/** + * e_authentication_session_new: + * @server: an #ESourceRegistryServer + * @authenticator: an #ESourceAuthenticator + * @source_uid: a data source identifier + * + * Creates a new #EAuthenticationSession instance for @server using + * @authenticator to handle authentication attempts. + * + * Note that @source_uid does not necessarily have to be known to the + * @server, as in the case when configuring a new data source, but it + * still has to be unique. + * + * Returns: a newly-created #EAuthenticationSession + * + * Since: 3.6 + **/ +EAuthenticationSession * +e_authentication_session_new (ESourceRegistryServer *server, + ESourceAuthenticator *authenticator, + const gchar *source_uid) +{ + 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); + + return g_object_new ( + E_TYPE_AUTHENTICATION_SESSION, + "server", server, + "authenticator", authenticator, + "source-uid", source_uid, NULL); +} + +/** + * e_authentication_session_get_server: + * @session: an #EAuthenticationSession + * + * Returns the #ESourceRegistryServer to which @session belongs. + * + * Returns: the #ESourceRegistryServer for @session + * + * Since: 3.6 + **/ +ESourceRegistryServer * +e_authentication_session_get_server (EAuthenticationSession *session) +{ + g_return_val_if_fail (E_IS_AUTHENTICATION_SESSION (session), NULL); + + return session->priv->server; +} + +/** + * e_authentication_session_get_authenticator: + * @session: an #EAuthenticationSession + * + * Returns the #ESourceAuthenticator handling authentication attempts for + * @session. This is usually an #EAuthenticationMediator but can also be + * a custom collection backend derived from #ECollectionBackend. + * + * Returns: the #ESourceAuthenticator for @session + * + * Since: 3.6 + **/ +ESourceAuthenticator * +e_authentication_session_get_authenticator (EAuthenticationSession *session) +{ + g_return_val_if_fail (E_IS_AUTHENTICATION_SESSION (session), NULL); + + return session->priv->authenticator; +} + +/** + * e_authentication_session_get_source_uid: + * @session: an #EAuthenticationSession + * + * Returns the #ESource:uid of the authenticating data source. The data + * source may or may not be known to the #EAuthenticationSession:server. + * + * Returns: the UID of the authenticating data source + * + * Since: 3.6 + **/ +const gchar * +e_authentication_session_get_source_uid (EAuthenticationSession *session) +{ + g_return_val_if_fail (E_IS_AUTHENTICATION_SESSION (session), NULL); + + return session->priv->source_uid; +} + +/** + * e_authentication_session_get_prompt_title: + * @session: an #EAuthenticationSession + * + * Returns the text used for the password prompt title should prompting + * be necessary. See #GcrPrompt for more details about password prompts. + * + * Returns: the password prompt title + * + * Since: 3.6 + **/ +const gchar * +e_authentication_session_get_prompt_title (EAuthenticationSession *session) +{ + g_return_val_if_fail (E_IS_AUTHENTICATION_SESSION (session), NULL); + + return session->priv->prompt_title; +} + +/** + * e_authentication_session_dup_prompt_title: + * @session: an #EAuthenticationSession + * + * Thread-safe variation of e_authentication_session_get_prompt_title(). + * Use this function when accessing @session from multiple threads. + * + * The returned string should be freed with g_free() when no longer needed. + * + * Returns: a newly-allocated copy of #EAuthenticationSession:prompt-title + * + * Since: 3.6 + **/ +gchar * +e_authentication_session_dup_prompt_title (EAuthenticationSession *session) +{ + const gchar *protected; + gchar *duplicate; + + g_return_val_if_fail (E_IS_AUTHENTICATION_SESSION (session), NULL); + + g_mutex_lock (session->priv->property_lock); + + protected = e_authentication_session_get_prompt_title (session); + duplicate = g_strdup (protected); + + g_mutex_unlock (session->priv->property_lock); + + return duplicate; +} + +/** + * e_authentication_session_set_prompt_title: + * @session: an #EAuthenticationSession + * @prompt_title: the password prompt title, or %NULL + * + * Sets the text used for the password prompt title should prompting be + * necessary. See #GcrPrompt for more details about password prompts. + * + * Since: 3.6 + **/ +void +e_authentication_session_set_prompt_title (EAuthenticationSession *session, + const gchar *prompt_title) +{ + g_return_if_fail (E_IS_AUTHENTICATION_SESSION (session)); + + g_mutex_lock (session->priv->property_lock); + + g_free (session->priv->prompt_title); + session->priv->prompt_title = g_strdup (prompt_title); + + g_mutex_unlock (session->priv->property_lock); + + g_object_notify (G_OBJECT (session), "prompt-title"); +} + +/** + * e_authentication_session_get_prompt_message: + * @session: an #EAuthenticationSession + * + * Returns the text used for the password prompt message should prompting + * be necessary. See #GcrPrompt for more details about password prompts. + * + * Returns: the password prompt message + * + * Since: 3.6 + **/ +const gchar * +e_authentication_session_get_prompt_message (EAuthenticationSession *session) +{ + g_return_val_if_fail (E_IS_AUTHENTICATION_SESSION (session), NULL); + + return session->priv->prompt_message; +} + +/** + * e_authentication_session_dup_prompt_message: + * @session: an #EAuthenticationSession + * + * Thread-safe variation of e_authentication_session_get_prompt_message(). + * Use this function when accessing @session from multiple threads. + * + * The returned string should be freed with g_free() when no longer needed. + * + * Returns: a newly-allocated copy of #EAuthenticationSession:prompt-message + * + * Since: 3.6 + **/ +gchar * +e_authentication_session_dup_prompt_message (EAuthenticationSession *session) +{ + const gchar *protected; + gchar *duplicate; + + g_return_val_if_fail (E_IS_AUTHENTICATION_SESSION (session), NULL); + + g_mutex_lock (session->priv->property_lock); + + protected = e_authentication_session_get_prompt_message (session); + duplicate = g_strdup (protected); + + g_mutex_unlock (session->priv->property_lock); + + return duplicate; +} + +/** + * e_authentication_session_set_prompt_message: + * @session: an #EAuthenticationSession + * @prompt_message: the password prompt message, or %NULL + * + * Sets the text used for the password prompt message should prompting be + * necessary. See #GcrPrompt for more details about password prompts. + * + * Since: 3.6 + **/ +void +e_authentication_session_set_prompt_message (EAuthenticationSession *session, + const gchar *prompt_message) +{ + g_return_if_fail (E_IS_AUTHENTICATION_SESSION (session)); + + g_mutex_lock (session->priv->property_lock); + + g_free (session->priv->prompt_message); + session->priv->prompt_message = g_strdup (prompt_message); + + g_mutex_unlock (session->priv->property_lock); + + g_object_notify (G_OBJECT (session), "prompt-message"); +} + +/** + * e_authentication_session_get_prompt_description: + * @session: an #EAuthenticationSession + * + * Returns the text used for the password prompt description should prompting + * be necessary. See #GcrPrompt for more details about password prompts. + * + * Returns: the password prompt description + * + * Since: 3.6 + **/ +const gchar * +e_authentication_session_get_prompt_description (EAuthenticationSession *session) +{ + g_return_val_if_fail (E_IS_AUTHENTICATION_SESSION (session), NULL); + + return session->priv->prompt_description; +} + +/** + * e_authentication_session_dup_prompt_description: + * @session: an #EAuthenticationSession + * + * Thread-safe variation of e_authentication_session_get_prompt_description(). + * Use this function when accessing @session from multiple threads. + * + * The returned string should be freed with g_free() when no longer needed. + * + * Returns: a newly-allocated copy of + * #EAuthenticationSession:prompt-description + * + * Since: 3.6 + **/ +gchar * +e_authentication_session_dup_prompt_description (EAuthenticationSession *session) +{ + const gchar *protected; + gchar *duplicate; + + g_return_val_if_fail (E_IS_AUTHENTICATION_SESSION (session), NULL); + + g_mutex_lock (session->priv->property_lock); + + protected = e_authentication_session_get_prompt_description (session); + duplicate = g_strdup (protected); + + g_mutex_unlock (session->priv->property_lock); + + return duplicate; +} + +/** + * e_authentication_session_set_prompt_description: + * @session: an #EAuthenticationSession + * @prompt_description: the password prompt description + * + * Sets the text used for the password prompt description should prompting + * be necessary. See #GcrPrompt for more details about password prompts. + * + * Since: 3.6 + **/ +void +e_authentication_session_set_prompt_description (EAuthenticationSession *session, + const gchar *prompt_description) +{ + g_return_if_fail (E_IS_AUTHENTICATION_SESSION (session)); + + g_mutex_lock (session->priv->property_lock); + + g_free (session->priv->prompt_description); + session->priv->prompt_description = g_strdup (prompt_description); + + g_mutex_unlock (session->priv->property_lock); + + g_object_notify (G_OBJECT (session), "prompt-description"); +} + +/** + * e_authentication_session_execute_sync: + * @session: an #EAuthenticationSession + * @cancellable: optional #GCancellable object, or %NULL + * @error: return location for a #GError, or %NULL + * + * Executes an authentication session. + * + * First the secret service is queried for a stored password. If found, + * an authentication attempt is made without disturbing the user. If no + * stored password is found, or if the stored password is rejected, the + * user is shown a system-modal dialog requesting a password. Further + * authentication attempts are repeated with user-provided passwords + * until authentication is verified or the user dismisses the prompt. + * The returned #EAuthenticationSessionResult indicates the outcome. + * + * If an error occurs while interacting with the secret service, or while + * prompting the user for a password, or while attempting authentication, + * the function sets @error and returns #E_AUTHENTICATION_SESSION_ERROR. + * + * Returns: the result of the authentication session + * + * Since: 3.6 + **/ +EAuthenticationSessionResult +e_authentication_session_execute_sync (EAuthenticationSession *session, + GCancellable *cancellable, + GError **error) +{ + EAuthenticationSessionClass *class; + + g_return_val_if_fail ( + E_IS_AUTHENTICATION_SESSION (session), + E_AUTHENTICATION_SESSION_DISMISSED); + + class = E_AUTHENTICATION_SESSION_GET_CLASS (session); + g_return_val_if_fail ( + class->execute_sync != NULL, + E_AUTHENTICATION_SESSION_DISMISSED); + + return class->execute_sync (session, cancellable, error); +} + +/** + * e_authentication_session_execute: + * @session: an #EAuthenticationSession + * @io_priority: the I/O priority of the request + * @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 + * + * See e_authentication_session_execute_sync() for details. + * + * When the operation is finished, @callback will be called. You can then + * call e_authentication_session_execute_finish() to get the result of the + * operation. + * + * Since: 3.6 + **/ +void +e_authentication_session_execute (EAuthenticationSession *session, + gint io_priority, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + EAuthenticationSessionClass *class; + + g_return_if_fail (E_IS_AUTHENTICATION_SESSION (session)); + + class = E_AUTHENTICATION_SESSION_GET_CLASS (session); + g_return_if_fail (class->execute != NULL); + + return class->execute ( + session, io_priority, cancellable, callback, user_data); +} + +/** + * e_authentication_session_execute_finish: + * @session: an #EAuthenticationSession + * @result: a #GAsyncResult + * @error: return location for a #GError, or %NULL + * + * Finishes the operation started with e_authentication_session_execute(). + * + * If an error occurs while interacting with the secret service, or while + * prompting the user for a password, or while attempting authentication, + * the function sets @error and returns #E_AUTHENTICATION_SESSION_ERROR. + * + * Returns: the result of the authentication session + * + * Since: 3.6 + **/ +EAuthenticationSessionResult +e_authentication_session_execute_finish (EAuthenticationSession *session, + GAsyncResult *result, + GError **error) +{ + EAuthenticationSessionClass *class; + + g_return_val_if_fail ( + E_IS_AUTHENTICATION_SESSION (session), + E_AUTHENTICATION_SESSION_DISMISSED); + g_return_val_if_fail ( + G_IS_ASYNC_RESULT (result), + E_AUTHENTICATION_SESSION_DISMISSED); + + class = E_AUTHENTICATION_SESSION_GET_CLASS (session); + g_return_val_if_fail ( + class->execute_finish != NULL, + E_AUTHENTICATION_SESSION_DISMISSED); + + return class->execute_finish (session, result, error); +} + +/* Helper for e_authentication_session_store_password() */ +static void +authentication_session_store_password_thread (GSimpleAsyncResult *simple, + GObject *object, + GCancellable *cancellable) +{ + AsyncContext *async_context; + GError *error = NULL; + + async_context = g_simple_async_result_get_op_res_gpointer (simple); + + e_authentication_session_store_password_sync ( + E_AUTHENTICATION_SESSION (object), + async_context->password, + async_context->permanently, + cancellable, &error); + + if (error != NULL) + g_simple_async_result_take_error (simple, error); +} + +/** + * e_authentication_session_store_password_sync: + * @session: an #EAuthenticationSession + * @password: the password to store + * @permanently: store permanently or just for the session + * @cancellable: optional #GCancellable object, or %NULL + * @error: return location for a #GError, or %NULL + * + * Store a password for the data source that @session is representing. + * If @permanently is %TRUE, the password is stored in the default keyring. + * Otherwise the password is stored in the memory-only session keyring. If + * an error occurs, the function sets @error and returns %FALSE. + * + * Returns: %TRUE on success, %FALSE on error + * + * Since: 3.6 + **/ +gboolean +e_authentication_session_store_password_sync (EAuthenticationSession *session, + const gchar *password, + gboolean permanently, + GCancellable *cancellable, + GError **error) +{ + GnomeKeyringResult result; + const gchar *keyring; + const gchar *uid; + gchar *display_name; + + g_return_val_if_fail (E_IS_AUTHENTICATION_SESSION (session), FALSE); + g_return_val_if_fail (password != NULL, FALSE); + + /* XXX Synchronous gnome-keyring functions are not cancellable. + * Maybe they will be someday, but in the meantime check for + * cancellation ourselves before doing this. */ + if (g_cancellable_set_error_if_cancelled (cancellable, error)) + return FALSE; + + if (permanently) + keyring = GNOME_KEYRING_DEFAULT; + else + keyring = GNOME_KEYRING_SESSION; + + uid = e_authentication_session_get_source_uid (session); + display_name = g_strdup_printf (KEYRING_ITEM_DISPLAY_FORMAT, uid); + + result = gnome_keyring_store_password_sync ( + &schema, keyring, display_name, password, + KEYRING_ITEM_ATTRIBUTE_NAME, uid, NULL); + + if (result == GNOME_KEYRING_RESULT_CANCELLED) { + g_cancellable_cancel (cancellable); + g_set_error_literal ( + error, G_IO_ERROR, G_IO_ERROR_CANCELLED, + _("Keyring operation was cancelled")); + + } else if (result != GNOME_KEYRING_RESULT_OK) { + g_set_error_literal ( + error, E_AUTHENTICATION_SESSION_KEYRING_ERROR, + result, gnome_keyring_result_to_message (result)); + } + + g_free (display_name); + + return (result == GNOME_KEYRING_RESULT_OK); +} + +/** + * e_authentication_session_store_password: + * @session: an #EAuthenticationSession + * @password: the password to store + * @permanently: store permanently or just for the session + * @io_priority: the I/O priority of the request + * @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 + * + * Asynchronously stores a password for the data source that @session + * is representing. If @permanently is %TRUE, the password is stored in the + * default keyring. Otherwise the password is stored in the memory-only + * session keyring. + * + * When the operation is finished, @callback will be called. You can then + * call e_authentication_session_store_password_finish() to get the result + * of the operation. + * + * Since: 3.6 + **/ +void +e_authentication_session_store_password (EAuthenticationSession *session, + const gchar *password, + gboolean permanently, + gint io_priority, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GSimpleAsyncResult *simple; + AsyncContext *async_context; + + g_return_if_fail (E_IS_AUTHENTICATION_SESSION (session)); + g_return_if_fail (password != NULL); + + async_context = g_slice_new0 (AsyncContext); + async_context->password = g_strdup (password); + async_context->permanently = permanently; + + simple = g_simple_async_result_new ( + G_OBJECT (session), callback, user_data, + e_authentication_session_store_password); + + g_simple_async_result_set_check_cancellable (simple, cancellable); + + g_simple_async_result_set_op_res_gpointer ( + simple, async_context, (GDestroyNotify) async_context_free); + + g_simple_async_result_run_in_thread ( + simple, authentication_session_store_password_thread, + io_priority, cancellable); + + g_object_unref (simple); +} + +/** + * e_authentication_session_store_password_finish: + * @session: an #EAuthenticationSession + * @result: a #GAsyncResult + * @error: return location for a #GError, or %NULL + * + * Finished the operation started with + * e_authentication_session_store_password(). + * + * Returns: %TRUE on success, %FALSE on error + * + * Since: 3.6 + **/ +gboolean +e_authentication_session_store_password_finish (EAuthenticationSession *session, + GAsyncResult *result, + GError **error) +{ + GSimpleAsyncResult *simple; + + g_return_val_if_fail ( + g_simple_async_result_is_valid ( + result, G_OBJECT (session), + e_authentication_session_store_password), FALSE); + + simple = G_SIMPLE_ASYNC_RESULT (result); + + /* Assume success unless a GError is set. */ + return !g_simple_async_result_propagate_error (simple, error); +} + +/* Helper for e_authentication_session_store_password() */ +static void +authentication_session_lookup_password_thread (GSimpleAsyncResult *simple, + GObject *object, + GCancellable *cancellable) +{ + AsyncContext *async_context; + GError *error = NULL; + + async_context = g_simple_async_result_get_op_res_gpointer (simple); + + e_authentication_session_lookup_password_sync ( + E_AUTHENTICATION_SESSION (object), cancellable, + &async_context->password, &error); + + if (error != NULL) + g_simple_async_result_take_error (simple, error); +} + +/** + * e_authentication_session_lookup_password_sync: + * @session: an #EAuthenticationSession + * @cancellable: optional #GCancellable object, or %NULL + * @password: return location for the password, or %NULL + * @error: return location for a #GError, or %NULL + * + * Looks up a password for the data source that @session is + * representing. Both the default and session keyrings are queried. + * + * Note the boolean return value indicates whether the lookup operation + * itself completed successfully, not whether a password was found. If + * no password was found, the function will set @password to %NULL but + * still return %TRUE. If an error occurs, the function sets @error + * and returns %FALSE. + * + * Returns: %TRUE on success, %FALSE on error + * + * Since: 3.6 + **/ +gboolean +e_authentication_session_lookup_password_sync (EAuthenticationSession *session, + GCancellable *cancellable, + gchar **password, + GError **error) +{ + GnomeKeyringResult result; + const gchar *uid; + gchar *temp = NULL; + + g_return_val_if_fail (E_IS_AUTHENTICATION_SESSION (session), FALSE); + + /* XXX Synchronous gnome-keyring functions are not cancellable. + * Maybe they will be someday, but in the meantime check for + * cancellation ourselves before doing this. */ + if (g_cancellable_set_error_if_cancelled (cancellable, error)) + return FALSE; + + uid = e_authentication_session_get_source_uid (session); + + result = gnome_keyring_find_password_sync ( + &schema, &temp, KEYRING_ITEM_ATTRIBUTE_NAME, uid, NULL); + + /* Not finding a data source password is not an error. */ + if (result == GNOME_KEYRING_RESULT_NO_MATCH) { + result = GNOME_KEYRING_RESULT_OK; + gnome_keyring_free_password (temp); + temp = NULL; + + } else if (result == GNOME_KEYRING_RESULT_CANCELLED) { + g_cancellable_cancel (cancellable); + g_set_error_literal ( + error, G_IO_ERROR, G_IO_ERROR_CANCELLED, + _("Keyring operation was cancelled")); + g_warn_if_fail (temp == NULL); + + } else if (result != GNOME_KEYRING_RESULT_OK) { + g_set_error_literal ( + error, E_AUTHENTICATION_SESSION_KEYRING_ERROR, + result, gnome_keyring_result_to_message (result)); + g_warn_if_fail (temp == NULL); + } + + /* Do not impose gnome-keyring's non-pageable memory API on the + * caller, it's not worth the hassle. Return a newly-allocated + * string so the caller can free it with g_free(). */ + if (password != NULL) + *password = g_strdup (temp); + + if (temp != NULL) + gnome_keyring_free_password (temp); + + return (result == GNOME_KEYRING_RESULT_OK); +} + +/** + * e_authentication_session_lookup_password: + * @session: an #EAuthenticationSession + * @io_priority: the I/O priority of the request + * @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 + * + * Asynchronously looks up a password for the data source that @session + * is representing. Both the default and session keyrings are queried. + * + * When the operation is finished, @callback will be called. You can then + * call e_authentication_session_lookup_password_finish() to get the + * result of the operation. + * + * Since: 3.6 + **/ +void +e_authentication_session_lookup_password (EAuthenticationSession *session, + gint io_priority, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GSimpleAsyncResult *simple; + AsyncContext *async_context; + + g_return_if_fail (E_IS_AUTHENTICATION_SESSION (session)); + + async_context = g_slice_new0 (AsyncContext); + + simple = g_simple_async_result_new ( + G_OBJECT (session), callback, user_data, + e_authentication_session_lookup_password); + + g_simple_async_result_set_check_cancellable (simple, cancellable); + + g_simple_async_result_set_op_res_gpointer ( + simple, async_context, (GDestroyNotify) async_context_free); + + g_simple_async_result_run_in_thread ( + simple, authentication_session_lookup_password_thread, + io_priority, cancellable); + + g_object_unref (simple); +} + +/** + * e_authentication_session_lookup_password_finish: + * @session: an #EAuthenticationSession + * @result: a #GAsyncResult + * @password: return location for the password, or %NULL + * @error: return location for a #GError, or %NULL + * + * Finishes the operation started with + * e_authentication_session_lookup_password(). + * + * Note the boolean return value indicates whether the lookup operation + * itself completed successfully, not whether a password was found. If + * no password was found, the function will set @password to %NULL but + * still return %TRUE. If an error occurs, the function sets @error + * and returns %FALSE. + * + * Returns: %TRUE on success, %FALSE on error + * + * Since: 3.6 + **/ +gboolean +e_authentication_session_lookup_password_finish (EAuthenticationSession *session, + GAsyncResult *result, + gchar **password, + GError **error) +{ + GSimpleAsyncResult *simple; + AsyncContext *async_context; + + g_return_val_if_fail ( + g_simple_async_result_is_valid ( + result, G_OBJECT (session), + e_authentication_session_lookup_password), FALSE); + + simple = G_SIMPLE_ASYNC_RESULT (result); + async_context = g_simple_async_result_get_op_res_gpointer (simple); + + if (g_simple_async_result_propagate_error (simple, error)) + return FALSE; + + if (password != NULL) { + *password = async_context->password; + async_context->password = NULL; + } + + return TRUE; +} + +/* Helper for e_authentication_session_delete_password() */ +static void +authentication_session_delete_password_thread (GSimpleAsyncResult *simple, + GObject *object, + GCancellable *cancellable) +{ + GError *error = NULL; + + e_authentication_session_delete_password_sync ( + E_AUTHENTICATION_SESSION (object), cancellable, &error); + + if (error != NULL) + g_simple_async_result_take_error (simple, error); +} + +/** + * e_authentication_session_delete_password_sync: + * @session: an #EAuthenticationSession + * @cancellable: optional #GCancellable object, or %NULL + * @error: return location for a #GError, or %NULL + * + * Deletes the password for the data source that @session is + * representing from either the default keyring or session keyring. + * + * Note the boolean return value indicates whether the delete operation + * itself completed successfully, not whether a password was found and + * deleted. If no password was found, the function will still return + * %TRUE. If an error occurs, the function sets @error and returns %FALSE. + * + * Returns: %TRUE on success, %FALSE on error + * + * Since: 3.6 + **/ +gboolean +e_authentication_session_delete_password_sync (EAuthenticationSession *session, + GCancellable *cancellable, + GError **error) +{ + GnomeKeyringResult result; + const gchar *uid; + + g_return_val_if_fail (E_IS_AUTHENTICATION_SESSION (session), FALSE); + + /* XXX Synchronous gnome-keyring functions are not cancellable. + * Maybe they will be someday, but in the meantime check for + * cancellation ourselves before doing this. */ + if (g_cancellable_set_error_if_cancelled (cancellable, error)) + return FALSE; + + uid = e_authentication_session_get_source_uid (session); + + result = gnome_keyring_delete_password_sync ( + &schema, KEYRING_ITEM_ATTRIBUTE_NAME, uid, NULL); + + /* Not finding a data source password is not an error. */ + if (result == GNOME_KEYRING_RESULT_NO_MATCH) { + result = GNOME_KEYRING_RESULT_OK; + + } else if (result == GNOME_KEYRING_RESULT_CANCELLED) { + g_cancellable_cancel (cancellable); + g_set_error_literal ( + error, G_IO_ERROR, G_IO_ERROR_CANCELLED, + _("Keyring operation was cancelled")); + + } else if (result != GNOME_KEYRING_RESULT_OK) { + g_set_error_literal ( + error, E_AUTHENTICATION_SESSION_KEYRING_ERROR, + result, gnome_keyring_result_to_message (result)); + } + + return (result == GNOME_KEYRING_RESULT_OK); +} + +/** + * e_authentication_session_delete_password: + * @session: an #EAuthenticationSession + * @io_priority: the I/O priority of the request + * @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 + * + * Asyncronously deletes the password for the data source that @session + * is representing from either the default keyring or session keyring. + * + * When the operation is finished, @callback will be called. You can then + * call e_authentication_session_delete_password_finish() to get the result + * of the operation. + * + * Since: 3.6 + **/ +void +e_authentication_session_delete_password (EAuthenticationSession *session, + gint io_priority, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GSimpleAsyncResult *simple; + + g_return_if_fail (E_IS_AUTHENTICATION_SESSION (session)); + + simple = g_simple_async_result_new ( + G_OBJECT (session), callback, user_data, + e_authentication_session_delete_password); + + g_simple_async_result_set_check_cancellable (simple, cancellable); + + g_simple_async_result_run_in_thread ( + simple, authentication_session_delete_password_thread, + io_priority, cancellable); + + g_object_unref (simple); +} + +/** + * e_authentication_session_delete_password_finish: + * @session: an #EAuthenticationSession + * @result: a #GAsyncResult + * @error: return location for a #GError, or %NULL + * + * Finishes the operation started with + * e_authentication_session_delete_password(). + * + * Note the boolean return value indicates whether the delete operation + * itself completed successfully, not whether a password was found and + * deleted. If no password was found, the function will still return + * %TRUE. If an error occurs, the function sets @error and returns %FALSE. + * + * Returns: %TRUE on success, %FALSE on error + * + * Since: 3.6 + **/ +gboolean +e_authentication_session_delete_password_finish (EAuthenticationSession *session, + GAsyncResult *result, + GError **error) +{ + GSimpleAsyncResult *simple; + + g_return_val_if_fail ( + g_simple_async_result_is_valid ( + result, G_OBJECT (session), + e_authentication_session_delete_password), FALSE); + + simple = G_SIMPLE_ASYNC_RESULT (result); + + /* Assume success unless a GError is set. */ + return !g_simple_async_result_propagate_error (simple, error); +} + diff --git a/libebackend/e-authentication-session.h b/libebackend/e-authentication-session.h new file mode 100644 index 0000000..4ef4e78 --- /dev/null +++ b/libebackend/e-authentication-session.h @@ -0,0 +1,212 @@ +/* + * e-authentication-session.h + * + * 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 + * + */ + +#ifndef E_AUTHENTICATION_SESSION_H +#define E_AUTHENTICATION_SESSION_H + +#include + +/* This needs to be in the public header since we're + * reusing the GnomeKeyringResult enum for error codes. */ +#include + +#include +#include + +/* Standard GObject macros */ +#define E_TYPE_AUTHENTICATION_SESSION \ + (e_authentication_session_get_type ()) +#define E_AUTHENTICATION_SESSION(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_AUTHENTICATION_SESSION, EAuthenticationSession)) +#define E_AUTHENTICATION_SESSION_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_AUTHENTICATION_SESSION, EAuthenticationSessionClass)) +#define E_IS_AUTHENTICATION_SESSION(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_AUTHENTICATION_SESSION)) +#define E_IS_AUTHENTICATION_SESSION_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_AUTHENTICATION_SESSION)) +#define E_AUTHENTICATION_SESSION_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_AUTHENTICATION_SESSION, EAuthenticationSessionClass)) + +/** + * E_AUTHENTICATION_SESSION_KEYRING_ERROR: + * + * Error domain for password storage and retrieval. Error codes in this + * domain are defined by the #GnomeKeyringResult enumeration. See #GError + * for information on error domains. + * + * Since: 3.6 + **/ +#define E_AUTHENTICATION_SESSION_KEYRING_ERROR \ + (e_authentication_session_error_quark ()) + +G_BEGIN_DECLS + +struct _ESourceRegistryServer; + +typedef struct _EAuthenticationSession EAuthenticationSession; +typedef struct _EAuthenticationSessionClass EAuthenticationSessionClass; +typedef struct _EAuthenticationSessionPrivate EAuthenticationSessionPrivate; + +/** + * EAuthenticationSession: + * + * Contains only private data that should be read and manipulated using the + * functions below. + * + * Since: 3.6 + **/ +struct _EAuthenticationSession { + GObject parent; + EAuthenticationSessionPrivate *priv; +}; + +struct _EAuthenticationSessionClass { + GObjectClass parent_class; + + /* Methods */ + EAuthenticationSessionResult + (*execute_sync) (EAuthenticationSession *session, + GCancellable *cancellable, + GError **error); + void (*execute) (EAuthenticationSession *session, + gint io_priority, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + EAuthenticationSessionResult + (*execute_finish) + (EAuthenticationSession *session, + GAsyncResult *result, + GError **error); + + /* Reserved slots. */ + gpointer reserved[16]; +}; + +GQuark e_authentication_session_error_quark + (void) G_GNUC_CONST; +GType e_authentication_session_get_type + (void) G_GNUC_CONST; +EAuthenticationSession * + e_authentication_session_new + (struct _ESourceRegistryServer *server, + ESourceAuthenticator *authenticator, + const gchar *source_uid); +struct _ESourceRegistryServer * + e_authentication_session_get_server + (EAuthenticationSession *session); +ESourceAuthenticator * + e_authentication_session_get_authenticator + (EAuthenticationSession *session); +const gchar * e_authentication_session_get_source_uid + (EAuthenticationSession *session); +const gchar * e_authentication_session_get_prompt_title + (EAuthenticationSession *session); +gchar * e_authentication_session_dup_prompt_title + (EAuthenticationSession *session); +void e_authentication_session_set_prompt_title + (EAuthenticationSession *session, + const gchar *prompt_title); +const gchar * e_authentication_session_get_prompt_message + (EAuthenticationSession *session); +gchar * e_authentication_session_dup_prompt_message + (EAuthenticationSession *session); +void e_authentication_session_set_prompt_message + (EAuthenticationSession *session, + const gchar *prompt_message); +const gchar * e_authentication_session_get_prompt_description + (EAuthenticationSession *session); +gchar * e_authentication_session_dup_prompt_description + (EAuthenticationSession *session); +void e_authentication_session_set_prompt_description + (EAuthenticationSession *session, + const gchar *prompt_description); +EAuthenticationSessionResult + e_authentication_session_execute_sync + (EAuthenticationSession *session, + GCancellable *cancellable, + GError **error); +void e_authentication_session_execute + (EAuthenticationSession *session, + gint io_priority, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +EAuthenticationSessionResult + e_authentication_session_execute_finish + (EAuthenticationSession *session, + GAsyncResult *result, + GError **error); +gboolean e_authentication_session_store_password_sync + (EAuthenticationSession *session, + const gchar *password, + gboolean permanently, + GCancellable *cancellable, + GError **error); +void e_authentication_session_store_password + (EAuthenticationSession *session, + const gchar *password, + gboolean permanently, + gint io_priority, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean e_authentication_session_store_password_finish + (EAuthenticationSession *session, + GAsyncResult *result, + GError **error); +gboolean e_authentication_session_lookup_password_sync + (EAuthenticationSession *session, + GCancellable *cancellable, + gchar **password, + GError **error); +void e_authentication_session_lookup_password + (EAuthenticationSession *session, + gint io_priority, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean e_authentication_session_lookup_password_finish + (EAuthenticationSession *session, + GAsyncResult *result, + gchar **password, + GError **error); +gboolean e_authentication_session_delete_password_sync + (EAuthenticationSession *session, + GCancellable *cancellable, + GError **error); +void e_authentication_session_delete_password + (EAuthenticationSession *session, + gint io_priority, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean e_authentication_session_delete_password_finish + (EAuthenticationSession *session, + GAsyncResult *result, + GError **error); + +G_END_DECLS + +#endif /* E_AUTHENTICATION_SESSION_H */ + diff --git a/libebackend/e-backend-enums.h b/libebackend/e-backend-enums.h index 4d06518..f011aab 100644 --- a/libebackend/e-backend-enums.h +++ b/libebackend/e-backend-enums.h @@ -20,6 +20,25 @@ #define E_BACKEND_ENUMS_H /** + * EAuthenticationSessionResult: + * @E_AUTHENTICATION_SESSION_ERROR: + * An error occurred while authenticating. + * @E_AUTHENTICATION_SESSION_SUCCESS: + * Client reported successful authentication. + * @E_AUTHENTICATION_SESSION_DISMISSED: + * User dismissed the authentication prompt. + * + * Completion codes used by #EAuthenticationSession. + * + * Since: 3.6 + **/ +typedef enum { + E_AUTHENTICATION_SESSION_ERROR, + E_AUTHENTICATION_SESSION_SUCCESS, + E_AUTHENTICATION_SESSION_DISMISSED +} EAuthenticationSessionResult; + +/** * EDBusServerExitCode: * @E_DBUS_SERVER_EXIT_NONE: * The server's run state is unchanged. @@ -41,5 +60,23 @@ typedef enum { E_DBUS_SERVER_EXIT_RELOAD } EDBusServerExitCode; -#endif /* E_BACKEND_ENUMS_H */ +/** + * ESourcePermissionFlags: + * @E_SOURCE_PERMISSION_NONE: + * The data source gets no initial permissions. + * @E_SOURCE_PERMISSION_WRITABLE: + * The data source is initially writable. + * @E_SOURCE_PERMISSION_REMOVABLE: + * The data source is initially removable. + * + * Initial permissions for a newly-loaded data source key file. + * + * Since: 3.6 + **/ +typedef enum { /*< flags >*/ + E_SOURCE_PERMISSION_NONE = 0, + E_SOURCE_PERMISSION_WRITABLE = 1 << 0, + E_SOURCE_PERMISSION_REMOVABLE = 1 << 1 +} ESourcePermissionFlags; +#endif /* E_BACKEND_ENUMS_H */ diff --git a/libebackend/e-collection-backend-factory.c b/libebackend/e-collection-backend-factory.c new file mode 100644 index 0000000..f7daf77 --- /dev/null +++ b/libebackend/e-collection-backend-factory.c @@ -0,0 +1,186 @@ +/* + * e-collection-backend-factory.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-collection-backend-factory + * @include: libebackend/e-collection-backend-factory.h + * @short_description: An abstract base class for a data source collection + * backend factory + * + * #ECollectionBackendFactory is a type of #EBackendFactory for creating + * #ECollectionBackend instances. + **/ + +#include "e-collection-backend-factory.h" + +#include +#include +#include +#include + +#include +#include + +G_DEFINE_ABSTRACT_TYPE ( + ECollectionBackendFactory, + e_collection_backend_factory, + E_TYPE_BACKEND_FACTORY) + +static ESourceRegistryServer * +collection_backend_factory_get_server (EBackendFactory *factory) +{ + EExtensible *extensible; + + extensible = e_extension_get_extensible (E_EXTENSION (factory)); + + return E_SOURCE_REGISTRY_SERVER (extensible); +} + +static const gchar * +collection_backend_factory_get_hash_key (EBackendFactory *factory) +{ + ECollectionBackendFactoryClass *class; + + class = E_COLLECTION_BACKEND_FACTORY_GET_CLASS (factory); + g_return_val_if_fail (class->factory_name != NULL, NULL); + + return class->factory_name; +} + +static EBackend * +collection_backend_factory_new_backend (EBackendFactory *factory, + ESource *source) +{ + ECollectionBackendFactoryClass *class; + ESourceRegistryServer *server; + + class = E_COLLECTION_BACKEND_FACTORY_GET_CLASS (factory); + g_return_val_if_fail (g_type_is_a ( + class->backend_type, E_TYPE_COLLECTION_BACKEND), NULL); + + server = collection_backend_factory_get_server (factory); + + return g_object_new ( + class->backend_type, + "server", server, + "source", source, NULL); +} + +static void +collection_backend_factory_prepare_mail (ECollectionBackendFactory *factory, + ESource *mail_account_source, + ESource *mail_identity_source, + ESource *mail_transport_source) +{ + ESource *source; + ESourceExtension *extension; + const gchar *extension_name; + + /* This does only very basic configuration. + * The rest is for subclasses to implement. */ + + source = mail_account_source; + + extension_name = E_SOURCE_EXTENSION_MAIL_ACCOUNT; + extension = e_source_get_extension (source, extension_name); + + e_source_mail_account_set_identity_uid ( + E_SOURCE_MAIL_ACCOUNT (extension), + e_source_get_uid (mail_identity_source)); + + source = mail_identity_source; + + /* This just makes sure the extension is present + * so the source is recognized as a mail identity. */ + extension_name = E_SOURCE_EXTENSION_MAIL_IDENTITY; + e_source_get_extension (source, extension_name); + + extension_name = E_SOURCE_EXTENSION_MAIL_SUBMISSION; + extension = e_source_get_extension (source, extension_name); + + e_source_mail_submission_set_transport_uid ( + E_SOURCE_MAIL_SUBMISSION (extension), + e_source_get_uid (mail_transport_source)); + + source = mail_transport_source; + + /* This just makes sure the extension is present + * so the source is recognized as a mail transport. */ + extension_name = E_SOURCE_EXTENSION_MAIL_TRANSPORT; + e_source_get_extension (source, extension_name); +} + +static void +e_collection_backend_factory_class_init (ECollectionBackendFactoryClass *class) +{ + EExtensionClass *extension_class; + EBackendFactoryClass *factory_class; + + extension_class = E_EXTENSION_CLASS (class); + extension_class->extensible_type = E_TYPE_SOURCE_REGISTRY_SERVER; + + factory_class = E_BACKEND_FACTORY_CLASS (class); + factory_class->get_hash_key = collection_backend_factory_get_hash_key; + factory_class->new_backend = collection_backend_factory_new_backend; + + class->prepare_mail = collection_backend_factory_prepare_mail; +} + +static void +e_collection_backend_factory_init (ECollectionBackendFactory *factory) +{ +} + +/** + * e_collection_backend_factory_prepare_mail: + * @factory: an #ECollectionBackendFactory + * @mail_account_source: an #ESource to hold mail account information + * @mail_identity_source: an #ESource to hold mail identity information + * @mail_transport_source: an #ESource to hold mail transport information + * + * Convenience function to populate a set of #ESource instances with mail + * account information to be added to an #ECollectionBackend. This is mainly + * used for vendor-specific collection backends like Google or Yahoo! where + * the host, port, and security details are known ahead of time and only + * user-specific information needs to be filled in. + * + * Since: 3.6 + **/ +void +e_collection_backend_factory_prepare_mail (ECollectionBackendFactory *factory, + ESource *mail_account_source, + ESource *mail_identity_source, + ESource *mail_transport_source) +{ + ECollectionBackendFactoryClass *class; + + g_return_if_fail (E_IS_COLLECTION_BACKEND_FACTORY (factory)); + g_return_if_fail (E_IS_SOURCE (mail_account_source)); + g_return_if_fail (E_IS_SOURCE (mail_identity_source)); + g_return_if_fail (E_IS_SOURCE (mail_transport_source)); + + class = E_COLLECTION_BACKEND_FACTORY_GET_CLASS (factory); + g_return_if_fail (class->prepare_mail != NULL); + + class->prepare_mail ( + factory, + mail_account_source, + mail_identity_source, + mail_transport_source); +} + diff --git a/libebackend/e-collection-backend-factory.h b/libebackend/e-collection-backend-factory.h new file mode 100644 index 0000000..c2a436f --- /dev/null +++ b/libebackend/e-collection-backend-factory.h @@ -0,0 +1,88 @@ +/* + * e-collection-backend-factory.h + * + * 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 + * + */ + +#ifndef E_COLLECTION_BACKEND_FACTORY_H +#define E_COLLECTION_BACKEND_FACTORY_H + +#include + +/* Standard GObject macros */ +#define E_TYPE_COLLECTION_BACKEND_FACTORY \ + (e_collection_backend_factory_get_type ()) +#define E_COLLECTION_BACKEND_FACTORY(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_COLLECTION_BACKEND_FACTORY, ECollectionBackendFactory)) +#define E_COLLECTION_BACKEND_FACTORY_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_COLLECTION_BACKEND_FACTORY, ECollectionBackendFactoryClass)) +#define E_IS_COLLECTION_BACKEND_FACTORY(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_COLLECTION_BACKEND_FACTORY)) +#define E_IS_COLLECTION_BACKEND_FACTORY_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_COLLECTION_BACKEND_FACTORY)) +#define E_COLLECTION_BACKEND_FACTORY_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_COLLECTION_BACKEND_FACTORY, ECollectionBackendFactoryClass)) + +G_BEGIN_DECLS + +typedef struct _ECollectionBackendFactory ECollectionBackendFactory; +typedef struct _ECollectionBackendFactoryClass ECollectionBackendFactoryClass; +typedef struct _ECollectionBackendFactoryPrivate ECollectionBackendFactoryPrivate; + +/** + * ECollectionBackendFactory: + * + * Contains only private data that should be read and manipulated using the + * functions below. + * + * Since: 3.6 + **/ +struct _ECollectionBackendFactory { + EBackendFactory parent; + ECollectionBackendFactoryPrivate *priv; +}; + +struct _ECollectionBackendFactoryClass { + EBackendFactoryClass parent_class; + + const gchar *factory_name; + GType backend_type; + + /* Methods */ + void (*prepare_mail) (ECollectionBackendFactory *factory, + ESource *mail_account_source, + ESource *mail_identity_source, + ESource *mail_transport_source); + + gpointer reserved[16]; +}; + +GType e_collection_backend_factory_get_type + (void) G_GNUC_CONST; +void e_collection_backend_factory_prepare_mail + (ECollectionBackendFactory *factory, + ESource *mail_account_source, + ESource *mail_identity_source, + ESource *mail_transport_source); + +G_END_DECLS + +#endif /* E_COLLECTION_BACKEND_FACTORY_H */ + diff --git a/libebackend/e-collection-backend.c b/libebackend/e-collection-backend.c new file mode 100644 index 0000000..7af1192 --- /dev/null +++ b/libebackend/e-collection-backend.c @@ -0,0 +1,835 @@ +/* + * e-collection-backend.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-collection-backend + * @include: libebackend/e-collection-backend.h + * @short_description: An abstract base class for a data source + * collection backend + * + * #ECollectionBackend is an abstract base class for backends which + * manage a collection of data sources which collectively represent the + * resources on a remote server. The resources can include any number + * of private and shared email stores, calendars and address books. + * + * The backend's job is to synchronize local representations of remote + * resources by adding and removing #EServerSideSource instances in an + * #ESourceRegistryServer. If possible the backend should also listen + * for notifications of newly-added or deleted resources on the remote + * server or else poll the remote server at regular intervals and then + * update the data source collection accordingly. + * + * As most remote servers require authentication, the backend may also + * wish to implement the #ESourceAuthenticator interface so it can submit + * its own #EAuthenticationSession instances to the #ESourceRegistryServer. + **/ + +#include "e-collection-backend.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#define E_COLLECTION_BACKEND_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_COLLECTION_BACKEND, ECollectionBackendPrivate)) + +struct _ECollectionBackendPrivate { + GWeakRef server; + GQueue children; + + /* Remembers memory-only data source UIDs + * based on a server-assigned resource ID. */ + gchar *collection_filename; + GKeyFile *collection_key_file; + guint save_collection_idle_id; + + gulong source_added_handler_id; + gulong source_removed_handler_id; +}; + +enum { + PROP_0, + PROP_SERVER +}; + +enum { + CHILD_ADDED, + CHILD_REMOVED, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL]; + +G_DEFINE_ABSTRACT_TYPE ( + ECollectionBackend, + e_collection_backend, + E_TYPE_BACKEND) + +static void +collection_backend_load_collection_file (ECollectionBackend *backend) +{ + ESource *source; + const gchar *uid; + const gchar *cache_dir; + gchar *dirname; + gchar *basename; + gchar *filename; + GError *error = NULL; + + cache_dir = e_get_user_cache_dir (); + source = e_backend_get_source (E_BACKEND (backend)); + + uid = e_source_get_uid (source); + dirname = g_build_filename (cache_dir, "sources", NULL); + basename = g_strconcat (uid, ".collection", NULL); + filename = g_build_filename (dirname, basename, NULL); + backend->priv->collection_filename = filename; /* takes ownership */ + g_mkdir_with_parents (dirname, 0700); + g_free (basename); + g_free (dirname); + + g_key_file_load_from_file ( + backend->priv->collection_key_file, + backend->priv->collection_filename, + G_KEY_FILE_KEEP_COMMENTS, &error); + + /* Disregard "file not found" errors. */ + if (g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_NOENT)) { + g_error_free (error); + } else if (error != NULL) { + g_warning ("%s: %s", G_STRFUNC, error->message); + g_error_free (error); + } +} + +static void +collection_backend_save_collection_file (ECollectionBackend *backend) +{ + gchar *contents; + gsize length; + GError *error = NULL; + + contents = g_key_file_to_data ( + backend->priv->collection_key_file, &length, NULL); + + g_file_set_contents ( + backend->priv->collection_filename, + contents, (gssize) length, &error); + + if (error != NULL) { + g_warning ("%s: %s", G_STRFUNC, error->message); + g_error_free (error); + } + + g_free (contents); +} + +static gboolean +collection_backend_save_idle_cb (gpointer user_data) +{ + ECollectionBackend *backend; + + backend = E_COLLECTION_BACKEND (user_data); + collection_backend_save_collection_file (backend); + backend->priv->save_collection_idle_id = 0; + + return FALSE; +} + +static ESource * +collection_backend_register_resource (ECollectionBackend *backend, + const gchar *resource_id, + GError **error) +{ + ESourceRegistryServer *server; + ESource *source; + gchar *group_name; + gchar *uid; + + server = e_collection_backend_ref_server (backend); + group_name = g_strdup_printf ("Resource %s", resource_id); + + uid = g_key_file_get_string ( + backend->priv->collection_key_file, + group_name, "SourceUid", NULL); + + /* Verify the UID is not already in use. + * If it is, we'll have to pick a new one. */ + if (uid != NULL) { + source = e_source_registry_server_ref_source (server, uid); + if (source != NULL) { + g_object_unref (source); + g_free (uid); + uid = NULL; + } + } + + if (uid == NULL) + uid = e_uid_new (); + + g_key_file_set_string ( + backend->priv->collection_key_file, + group_name, "SourceUid", uid); + + if (backend->priv->save_collection_idle_id == 0) { + guint idle_id; + + idle_id = g_idle_add_full ( + G_PRIORITY_DEFAULT_IDLE, + collection_backend_save_idle_cb, + g_object_ref (backend), + (GDestroyNotify) g_object_unref); + backend->priv->save_collection_idle_id = idle_id; + } + + source = e_server_side_source_new_memory_only (server, uid, error); + + g_free (uid); + g_free (group_name); + g_object_unref (server); + + return source; +} + +static gboolean +collection_backend_child_is_calendar (ESource *child_source) +{ + const gchar *extension_name; + + extension_name = E_SOURCE_EXTENSION_CALENDAR; + if (e_source_has_extension (child_source, extension_name)) + return TRUE; + + extension_name = E_SOURCE_EXTENSION_MEMO_LIST; + if (e_source_has_extension (child_source, extension_name)) + return TRUE; + + extension_name = E_SOURCE_EXTENSION_TASK_LIST; + if (e_source_has_extension (child_source, extension_name)) + return TRUE; + + return FALSE; +} + +static gboolean +collection_backend_child_is_contacts (ESource *child_source) +{ + const gchar *extension_name; + + extension_name = E_SOURCE_EXTENSION_ADDRESS_BOOK; + if (e_source_has_extension (child_source, extension_name)) + return TRUE; + + return FALSE; +} + +static gboolean +collection_backend_child_is_mail (ESource *child_source) +{ + const gchar *extension_name; + + extension_name = E_SOURCE_EXTENSION_MAIL_ACCOUNT; + if (e_source_has_extension (child_source, extension_name)) + return TRUE; + + extension_name = E_SOURCE_EXTENSION_MAIL_IDENTITY; + if (e_source_has_extension (child_source, extension_name)) + return TRUE; + + extension_name = E_SOURCE_EXTENSION_MAIL_TRANSPORT; + if (e_source_has_extension (child_source, extension_name)) + return TRUE; + + return FALSE; +} + +static void +collection_backend_bind_child_enabled (ECollectionBackend *backend, + ESource *child_source) +{ + ESource *collection_source; + ESourceCollection *extension; + const gchar *extension_name; + + /* See if the child source's "enabled" property can be + * bound to any ESourceCollection "enabled" properties. */ + + extension_name = E_SOURCE_EXTENSION_COLLECTION; + collection_source = e_backend_get_source (E_BACKEND (backend)); + extension = e_source_get_extension (collection_source, extension_name); + + if (collection_backend_child_is_calendar (child_source)) { + g_object_bind_property ( + extension, "calendar-enabled", + child_source, "enabled", + G_BINDING_SYNC_CREATE); + return; + } + + if (collection_backend_child_is_contacts (child_source)) { + g_object_bind_property ( + extension, "contacts-enabled", + child_source, "enabled", + G_BINDING_SYNC_CREATE); + return; + } + + if (collection_backend_child_is_mail (child_source)) { + g_object_bind_property ( + extension, "mail-enabled", + child_source, "enabled", + G_BINDING_SYNC_CREATE); + return; + } +} + +static void +collection_backend_source_added_cb (ESourceRegistryServer *server, + ESource *source, + ECollectionBackend *backend) +{ + ESource *collection_source; + ESource *parent_source; + const gchar *uid; + + /* If the newly-added source is our own child, emit "child-added". */ + + collection_source = e_backend_get_source (E_BACKEND (backend)); + + uid = e_source_get_parent (source); + if (uid == NULL) + return; + + parent_source = e_source_registry_server_ref_source (server, uid); + g_return_if_fail (parent_source != NULL); + + if (e_source_equal (collection_source, parent_source)) + g_signal_emit (backend, signals[CHILD_ADDED], 0, source); + + g_object_unref (parent_source); +} + +static void +collection_backend_source_removed_cb (ESourceRegistryServer *server, + ESource *source, + ECollectionBackend *backend) +{ + ESource *collection_source; + ESource *parent_source; + const gchar *uid; + + /* If the removed source was our own child, emit "child-removed". + * Note that the source is already unlinked from the GNode tree. */ + + collection_source = e_backend_get_source (E_BACKEND (backend)); + + uid = e_source_get_parent (source); + if (uid == NULL) + return; + + parent_source = e_source_registry_server_ref_source (server, uid); + g_return_if_fail (parent_source != NULL); + + if (e_source_equal (collection_source, parent_source)) + g_signal_emit (backend, signals[CHILD_REMOVED], 0, source); + + g_object_unref (parent_source); +} + +static gboolean +collection_backend_populate_idle_cb (gpointer user_data) +{ + ECollectionBackend *backend; + ECollectionBackendClass *class; + + backend = E_COLLECTION_BACKEND (user_data); + + class = E_COLLECTION_BACKEND_GET_CLASS (backend); + g_return_val_if_fail (class->populate != NULL, FALSE); + + class->populate (backend); + + return FALSE; +} + +static void +collection_backend_set_server (ECollectionBackend *backend, + ESourceRegistryServer *server) +{ + g_return_if_fail (E_IS_SOURCE_REGISTRY_SERVER (server)); + + g_weak_ref_set (&backend->priv->server, server); +} + +static void +collection_backend_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_SERVER: + collection_backend_set_server ( + E_COLLECTION_BACKEND (object), + g_value_get_object (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +collection_backend_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_SERVER: + g_value_take_object ( + value, + e_collection_backend_ref_server ( + E_COLLECTION_BACKEND (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +collection_backend_dispose (GObject *object) +{ + ECollectionBackendPrivate *priv; + ESourceRegistryServer *server; + + priv = E_COLLECTION_BACKEND_GET_PRIVATE (object); + + server = g_weak_ref_get (&priv->server); + if (server != NULL) { + g_signal_handler_disconnect ( + server, priv->source_added_handler_id); + g_signal_handler_disconnect ( + server, priv->source_removed_handler_id); + g_weak_ref_set (&priv->server, NULL); + g_object_unref (server); + } + + while (!g_queue_is_empty (&priv->children)) + g_object_unref (g_queue_pop_head (&priv->children)); + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_collection_backend_parent_class)->dispose (object); +} + +static void +collection_backend_finalize (GObject *object) +{ + ECollectionBackendPrivate *priv; + + priv = E_COLLECTION_BACKEND_GET_PRIVATE (object); + + g_free (priv->collection_filename); + g_key_file_free (priv->collection_key_file); + + /* The idle source ID should be zero since the idle + * source itself holds a reference on the backend. */ + g_warn_if_fail (priv->save_collection_idle_id == 0); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_collection_backend_parent_class)->finalize (object); +} + +static void +collection_backend_constructed (GObject *object) +{ + ECollectionBackend *backend; + ESourceRegistryServer *server; + ESource *source; + GNode *node; + gulong handler_id; + + backend = E_COLLECTION_BACKEND (object); + + /* Chain up to parent's constructed() method. */ + G_OBJECT_CLASS (e_collection_backend_parent_class)-> + constructed (object); + + collection_backend_load_collection_file (backend); + + /* Emit "child-added" signals for the children we already have. */ + + source = e_backend_get_source (E_BACKEND (backend)); + node = e_server_side_source_get_node (E_SERVER_SIDE_SOURCE (source)); + node = g_node_first_child (node); + + while (node != NULL) { + ESource *child = E_SOURCE (node->data); + g_signal_emit (backend, signals[CHILD_ADDED], 0, child); + node = g_node_next_sibling (node); + } + + /* Listen for "source-added" and "source-removed" signals + * from the server, which may trigger our own "child-added" + * and "child-removed" signals. */ + + server = e_collection_backend_ref_server (backend); + + handler_id = g_signal_connect ( + server, "source-added", + G_CALLBACK (collection_backend_source_added_cb), backend); + + backend->priv->source_added_handler_id = handler_id; + + handler_id = g_signal_connect ( + server, "source-removed", + G_CALLBACK (collection_backend_source_removed_cb), backend); + + backend->priv->source_removed_handler_id = handler_id; + + g_object_unref (server); + + /* Populate the newly-added collection from an idle callback + * so persistent child sources have a chance to be added first. */ + + g_idle_add_full ( + G_PRIORITY_LOW, + collection_backend_populate_idle_cb, + g_object_ref (backend), + (GDestroyNotify) g_object_unref); +} + +static void +collection_backend_populate (ECollectionBackend *backend) +{ + /* Placeholder so subclasses can safely chain up. */ +} + +static void +collection_backend_child_added (ECollectionBackend *backend, + ESource *child_source) +{ + collection_backend_bind_child_enabled (backend, child_source); + + g_queue_push_tail ( + &backend->priv->children, + g_object_ref (child_source)); + + /* Collection children are not removable. */ + e_server_side_source_set_removable ( + E_SERVER_SIDE_SOURCE (child_source), FALSE); +} + +static void +collection_backend_child_removed (ECollectionBackend *backend, + ESource *child_source) +{ + if (g_queue_remove (&backend->priv->children, child_source)) + g_object_unref (child_source); +} + +static void +e_collection_backend_class_init (ECollectionBackendClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private (class, sizeof (ECollectionBackendPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = collection_backend_set_property; + object_class->get_property = collection_backend_get_property; + object_class->dispose = collection_backend_dispose; + object_class->finalize = collection_backend_finalize; + object_class->constructed = collection_backend_constructed; + + class->populate = collection_backend_populate; + class->child_added = collection_backend_child_added; + class->child_removed = collection_backend_child_removed; + + g_object_class_install_property ( + object_class, + PROP_SERVER, + g_param_spec_object ( + "server", + "Server", + "The server to which the backend belongs", + E_TYPE_SOURCE_REGISTRY_SERVER, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + /** + * ECollectionBackend::child-added: + * @backend: the #ECollectionBackend which emitted the signal + * @child_source: the newly-added child #EServerSideSource + * + * Emitted when an #EServerSideSource is added to @backend's + * #ECollectionBackend:server as a child of @backend's collection + * #EBackend:source. + * + * You can think of this as a filtered version of + * #ESourceRegistryServer's #ESourceRegistryServer::source-added + * signal which only lets through sources relevant to @backend. + **/ + signals[CHILD_ADDED] = g_signal_new ( + "child-added", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ECollectionBackendClass, child_added), + NULL, NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, 1, + E_TYPE_SERVER_SIDE_SOURCE); + + /** + * ECollectionBackend::child-removed: + * @backend: the #ECollectionBackend which emitted the signal + * @child_source: the child #EServerSideSource that got removed + * + * Emitted when an #EServerSideSource that is a child of + * @backend's collection #EBackend:source is removed from + * @backend's #ECollectionBackend:server. + * + * You can think of this as a filtered version of + * #ESourceRegistryServer's #ESourceRegistryServer::source-removed + * signal which only lets through sources relevant to @backend. + **/ + signals[CHILD_REMOVED] = g_signal_new ( + "child-removed", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ECollectionBackendClass, child_removed), + NULL, NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, 1, + E_TYPE_SERVER_SIDE_SOURCE); +} + +static void +e_collection_backend_init (ECollectionBackend *backend) +{ + backend->priv = E_COLLECTION_BACKEND_GET_PRIVATE (backend); + backend->priv->collection_key_file = g_key_file_new (); +} + +/** + * e_collection_backend_new_child: + * @backend: an #ECollectionBackend + * @resource_id: a stable and unique resource ID + * + * Creates a new memory-only #EServerSideSource as a child of the collection + * #EBackend:source owned by @backend. If possible, the #EServerSideSource + * is assigned a previously used #ESource:uid based on @resource_id so that + * locally cached data can be reused. + * + * The returned data source should be passed to + * e_source_registry_server_add_source() to export it over D-Bus. + * + * Return: a newly-created data source + * + * Since: 3.6 + **/ +ESource * +e_collection_backend_new_child (ECollectionBackend *backend, + const gchar *resource_id) +{ + ESource *collection_source; + ESource *child_source; + const gchar *collection_uid; + GError *error = NULL; + + g_return_val_if_fail (E_IS_COLLECTION_BACKEND (backend), NULL); + g_return_val_if_fail (resource_id != NULL, NULL); + + /* This being a memory-only data source, creating the instance + * should never fail but we'll check for errors just the same. + * It's unlikely enough that we don't need a GError parameter. */ + child_source = collection_backend_register_resource ( + backend, resource_id, &error); + + if (error != NULL) { + g_warn_if_fail (child_source == NULL); + g_warning ("%s: %s", G_STRFUNC, error->message); + g_error_free (error); + return NULL; + } + + collection_source = e_backend_get_source (E_BACKEND (backend)); + collection_uid = e_source_get_uid (collection_source); + e_source_set_parent (child_source, collection_uid); + + g_print ("%s: Pairing %s with resource %s\n", + e_source_get_display_name (collection_source), + e_source_get_uid (child_source), resource_id); + + return child_source; +} + +/** + * e_collection_backend_ref_server: + * @backend: an #ECollectionBackend + * + * Returns the #ESourceRegistryServer to which @backend belongs. + * + * The returned #ESourceRegistryServer is referenced for thread-safety. + * Unreference the #ESourceRegistryServer with g_object_unref() when + * finished with it. + * + * Returns: the #ESourceRegisterServer for @backend + * + * Since: 3.6 + **/ +ESourceRegistryServer * +e_collection_backend_ref_server (ECollectionBackend *backend) +{ + g_return_val_if_fail (E_IS_COLLECTION_BACKEND (backend), NULL); + + return g_weak_ref_get (&backend->priv->server); +} + +/** + * e_collection_backend_list_calendar_sources: + * @backend: an #ECollectionBackend + * + * Returns a list of calendar sources belonging to the data source + * collection managed by @backend. + * + * 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 list of calendar sources + * + * Since: 3.6 + **/ +GList * +e_collection_backend_list_calendar_sources (ECollectionBackend *backend) +{ + GList *result_list = NULL; + GList *list, *link; + + g_return_val_if_fail (E_IS_COLLECTION_BACKEND (backend), NULL); + + list = g_queue_peek_head_link (&backend->priv->children); + + for (link = list; link != NULL; link = g_list_next (link)) { + ESource *child_source = E_SOURCE (link->data); + if (collection_backend_child_is_calendar (child_source)) + result_list = g_list_prepend ( + result_list, g_object_ref (child_source)); + } + + return g_list_reverse (result_list); +} + +/** + * e_collection_backend_list_contacts_sources: + * @backend: an #ECollectionBackend + * + * Returns a list of address book sources belonging to the data source + * collection managed by @backend. + * + * 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 list of address book sources + * + * Since: 3.6 + **/ +GList * +e_collection_backend_list_contacts_sources (ECollectionBackend *backend) +{ + GList *result_list = NULL; + GList *list, *link; + + g_return_val_if_fail (E_IS_COLLECTION_BACKEND (backend), NULL); + + list = g_queue_peek_head_link (&backend->priv->children); + + for (link = list; link != NULL; link = g_list_next (link)) { + ESource *child_source = E_SOURCE (link->data); + if (collection_backend_child_is_contacts (child_source)) + result_list = g_list_prepend ( + result_list, g_object_ref (child_source)); + } + + return g_list_reverse (result_list); +} + +/** + * e_collection_backend_list_mail_sources: + * @backend: an #ECollectionBackend + * + * Returns a list of mail sources belonging to the data source collection + * managed by @backend. + * + * 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 list of mail sources + * + * Since: 3.6 + **/ +GList * +e_collection_backend_list_mail_sources (ECollectionBackend *backend) +{ + GList *result_list = NULL; + GList *list, *link; + + g_return_val_if_fail (E_IS_COLLECTION_BACKEND (backend), NULL); + + list = g_queue_peek_head_link (&backend->priv->children); + + for (link = list; link != NULL; link = g_list_next (link)) { + ESource *child_source = E_SOURCE (link->data); + if (collection_backend_child_is_mail (child_source)) + result_list = g_list_prepend ( + result_list, g_object_ref (child_source)); + } + + return g_list_reverse (result_list); +} + diff --git a/libebackend/e-collection-backend.h b/libebackend/e-collection-backend.h new file mode 100644 index 0000000..8f2b4b7 --- /dev/null +++ b/libebackend/e-collection-backend.h @@ -0,0 +1,94 @@ +/* + * e-collection-backend.h + * + * 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 + * + */ + +#ifndef E_COLLECTION_BACKEND_H +#define E_COLLECTION_BACKEND_H + +#include + +/* Standard GObject macros */ +#define E_TYPE_COLLECTION_BACKEND \ + (e_collection_backend_get_type ()) +#define E_COLLECTION_BACKEND(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_COLLECTION_BACKEND, ECollectionBackend)) +#define E_COLLECTION_BACKEND_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_COLLECTION_BACKEND, ECollectionBackendClass)) +#define E_IS_COLLECTION_BACKEND(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_COLLECTION_BACKEND)) +#define E_IS_COLLECTION_BACKEND_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_COLLECTION_BACKEND)) +#define E_COLLECTION_BACKEND_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_COLLECTION_BACKEND, ECollectionBackendClass)) + +G_BEGIN_DECLS + +struct _ESourceRegistryServer; + +typedef struct _ECollectionBackend ECollectionBackend; +typedef struct _ECollectionBackendClass ECollectionBackendClass; +typedef struct _ECollectionBackendPrivate ECollectionBackendPrivate; + +/** + * ECollectionBackend: + * + * Contains only private data that should be read and manipulated using the + * functions below. + * + * Since: 3.6 + **/ +struct _ECollectionBackend { + EBackend parent; + ECollectionBackendPrivate *priv; +}; + +struct _ECollectionBackendClass { + EBackendClass parent_class; + + /* Methods */ + void (*populate) (ECollectionBackend *backend); + + /* Signals */ + void (*child_added) (ECollectionBackend *backend, + ESource *child_source); + void (*child_removed) (ECollectionBackend *backend, + ESource *child_source); + + gpointer reserved[16]; +}; + +GType e_collection_backend_get_type (void) G_GNUC_CONST; +ESource * e_collection_backend_new_child (ECollectionBackend *backend, + const gchar *resource_id); +struct _ESourceRegistryServer * + e_collection_backend_ref_server (ECollectionBackend *backend); +GList * e_collection_backend_list_calendar_sources + (ECollectionBackend *backend); +GList * e_collection_backend_list_contacts_sources + (ECollectionBackend *backend); +GList * e_collection_backend_list_mail_sources + (ECollectionBackend *backend); + +G_END_DECLS + +#endif /* E_COLLECTION_BACKEND_H */ + diff --git a/libebackend/e-dbus-server.c b/libebackend/e-dbus-server.c index 0d72a36..056d975 100644 --- a/libebackend/e-dbus-server.c +++ b/libebackend/e-dbus-server.c @@ -31,7 +31,6 @@ #endif #include -#include #include #define E_DBUS_SERVER_GET_PRIVATE(obj) \ @@ -66,9 +65,8 @@ static guint signals[LAST_SIGNAL]; static GHashTable *directories_loaded; G_LOCK_DEFINE_STATIC (directories_loaded); -G_DEFINE_ABSTRACT_TYPE_WITH_CODE ( - EDBusServer, e_dbus_server, G_TYPE_OBJECT, - G_IMPLEMENT_INTERFACE (E_TYPE_EXTENSIBLE, NULL)) +G_DEFINE_ABSTRACT_TYPE ( + EDBusServer, e_dbus_server, G_TYPE_OBJECT) static void dbus_server_bus_acquired_cb (GDBusConnection *connection, diff --git a/libebackend/e-server-side-source.c b/libebackend/e-server-side-source.c new file mode 100644 index 0000000..ec55cdd --- /dev/null +++ b/libebackend/e-server-side-source.c @@ -0,0 +1,1404 @@ +/* + * e-server-side-source.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-server-side-source + * @short_description: A server-side data source + * @include: libebackend/e-server-side-source.h + * + * An #EServerSideSource is an #ESource with some additional capabilities + * exclusive to the registry D-Bus service. + **/ + +#include "e-server-side-source.h" + +#include +#include + +/* Private D-Bus classes. */ +#include + +#include +#include + +#define E_SERVER_SIDE_SOURCE_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_SERVER_SIDE_SOURCE, EServerSideSourcePrivate)) + +#define DBUS_OBJECT_PATH E_SOURCE_REGISTRY_SERVER_OBJECT_PATH "/Source" + +#define PRIMARY_GROUP_NAME "Data Source" + +typedef struct _AsyncClosure AsyncClosure; + +struct _EServerSideSourcePrivate { + gpointer server; /* weak pointer */ + + GNode node; + GFile *file; + gchar *uid; + + /* For comparison. */ + gchar *file_contents; + + gboolean allow_auth_prompt; +}; + +struct _AsyncClosure { + GMainLoop *loop; + GMainContext *context; + GAsyncResult *result; +}; + +enum { + PROP_0, + PROP_ALLOW_AUTH_PROMPT, + PROP_FILE, + PROP_REMOVABLE, + PROP_SERVER, + PROP_UID, + PROP_WRITABLE +}; + +static GInitableIface *initable_parent_interface; + +/* Forward Declarations */ +static void e_server_side_source_initable_init + (GInitableIface *interface); + +G_DEFINE_TYPE_WITH_CODE ( + EServerSideSource, + e_server_side_source, + E_TYPE_SOURCE, + G_IMPLEMENT_INTERFACE ( + G_TYPE_INITABLE, + e_server_side_source_initable_init)) + +static AsyncClosure * +async_closure_new (void) +{ + AsyncClosure *closure; + + closure = g_slice_new0 (AsyncClosure); + closure->context = g_main_context_new (); + closure->loop = g_main_loop_new (closure->context, FALSE); + + g_main_context_push_thread_default (closure->context); + + return closure; +} + +static GAsyncResult * +async_closure_wait (AsyncClosure *closure) +{ + g_main_loop_run (closure->loop); + + return closure->result; +} + +static void +async_closure_free (AsyncClosure *closure) +{ + g_main_context_pop_thread_default (closure->context); + + g_main_loop_unref (closure->loop); + g_main_context_unref (closure->context); + + if (closure->result != NULL) + g_object_unref (closure->result); + + g_slice_free (AsyncClosure, closure); +} + +static void +async_closure_callback (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + AsyncClosure *closure = user_data; + + /* Replace any previous result. */ + if (closure->result != NULL) + g_object_unref (closure->result); + closure->result = g_object_ref (result); + + g_main_loop_quit (closure->loop); +} + +static gboolean +server_side_source_parse_data (GKeyFile *key_file, + const gchar *data, + gsize length, + GError **error) +{ + gboolean success; + + success = g_key_file_load_from_data ( + key_file, data, length, G_KEY_FILE_NONE, error); + + if (!success) + return FALSE; + + /* Make sure the key file has a [Data Source] group. */ + if (!g_key_file_has_group (key_file, PRIMARY_GROUP_NAME)) { + g_set_error ( + error, G_KEY_FILE_ERROR, + G_KEY_FILE_ERROR_GROUP_NOT_FOUND, + _("Data source is missing a [%s] group"), + PRIMARY_GROUP_NAME); + return FALSE; + } + + return TRUE; +} + +static void +server_side_source_print_diff (ESource *source, + const gchar *old_data, + const gchar *new_data) +{ + gchar **old_strv = NULL; + gchar **new_strv = NULL; + guint old_length = 0; + guint new_length = 0; + guint ii; + + g_print ("Saving %s\n", e_source_get_uid (source)); + + if (old_data != NULL) { + old_strv = g_strsplit (old_data, "\n", 0); + old_length = g_strv_length (old_strv); + } + + if (new_data != NULL) { + new_strv = g_strsplit (new_data, "\n", 0); + new_length = g_strv_length (new_strv); + } + + for (ii = 0; ii < MIN (old_length, new_length); ii++) { + if (g_strcmp0 (old_strv[ii], new_strv[ii]) != 0) { + g_print (" - : %s\n", old_strv[ii]); + g_print (" + : %s\n", new_strv[ii]); + } else { + g_print (" : %s\n", old_strv[ii]); + } + } + + for (; ii < old_length; ii++) + g_print (" - : %s\n", old_strv[ii]); + + for (; ii < new_length; ii++) + g_print (" + : %s\n", new_strv[ii]); + + g_strfreev (old_strv); + g_strfreev (new_strv); +} + +static gboolean +server_side_source_traverse_cb (GNode *node, + GQueue *queue) +{ + g_queue_push_tail (queue, g_object_ref (node->data)); + + return FALSE; +} + +static gboolean +server_side_source_allow_auth_prompt_cb (EDBusSource *interface, + GDBusMethodInvocation *invocation, + EServerSideSource *source) +{ + e_server_side_source_set_allow_auth_prompt (source, TRUE); + + e_dbus_source_complete_allow_auth_prompt (interface, invocation); + + return TRUE; +} + +static gboolean +server_side_source_remove_cb (EDBusSourceRemovable *interface, + GDBusMethodInvocation *invocation, + EServerSideSource *source) +{ + GError *error = NULL; + + /* Note we don't need to verify the source is removable here + * since if it isn't, the remove() method won't be available. + * Descendants of the source are removed whether they export + * a remove() method or not. */ + + e_source_remove_sync (E_SOURCE (source), NULL, &error); + + if (error != NULL) + g_dbus_method_invocation_take_error (invocation, error); + else + e_dbus_source_removable_complete_remove ( + interface, invocation); + + return TRUE; +} + +static gboolean +server_side_source_write_cb (EDBusSourceWritable *interface, + GDBusMethodInvocation *invocation, + const gchar *data, + ESource *source) +{ + GKeyFile *key_file; + GDBusObject *dbus_object; + EDBusSource *dbus_source; + GError *error = NULL; + + /* Note we don't need to verify the source is writable here + * since if it isn't, the write() method won't be available. */ + + dbus_object = e_source_ref_dbus_object (source); + dbus_source = e_dbus_object_get_source (E_DBUS_OBJECT (dbus_object)); + + /* Validate the raw data before making the changes live. */ + key_file = g_key_file_new (); + server_side_source_parse_data (key_file, data, strlen (data), &error); + g_key_file_free (key_file); + + /* Q: How does this trigger data being written to disk? + * + * A: Here's the sequence of events: + * + * 1) We set the EDBusSource:data property. + * 2) ESource picks up the "notify::data" signal and parses + * the raw data, which triggers an ESource:changed signal. + * 3) Our changed() method schedules an idle callback. + * 4) The idle callback calls e_source_write_sync(). + * 5) e_source_write_sync() calls e_dbus_source_dup_data() + * and synchronously writes the resulting string to disk. + */ + + if (error == NULL) + e_dbus_source_set_data (dbus_source, data); + + if (error != NULL) + g_dbus_method_invocation_take_error (invocation, error); + else + e_dbus_source_writable_complete_write ( + interface, invocation); + + g_object_unref (dbus_source); + g_object_unref (dbus_object); + + return TRUE; +} + +static void +server_side_source_set_file (EServerSideSource *source, + GFile *file) +{ + g_return_if_fail (file == NULL || G_IS_FILE (file)); + g_return_if_fail (source->priv->file == NULL); + + if (file != NULL) + source->priv->file = g_object_ref (file); +} + +static void +server_side_source_set_server (EServerSideSource *source, + ESourceRegistryServer *server) +{ + g_return_if_fail (E_IS_SOURCE_REGISTRY_SERVER (server)); + g_return_if_fail (source->priv->server == NULL); + + source->priv->server = server; + + g_object_add_weak_pointer ( + G_OBJECT (server), &source->priv->server); +} + +static void +server_side_source_set_uid (EServerSideSource *source, + const gchar *uid) +{ + g_return_if_fail (source->priv->uid == NULL); + + /* It's okay for this to be NULL, in fact if we're given a + * GFile the UID is derived from its basename anyway. This + * is just for memory-only sources in a collection backend, + * which don't have a GFile. */ + source->priv->uid = g_strdup (uid); +} + +static void +server_side_source_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_ALLOW_AUTH_PROMPT: + e_server_side_source_set_allow_auth_prompt ( + E_SERVER_SIDE_SOURCE (object), + g_value_get_boolean (value)); + return; + + case PROP_FILE: + server_side_source_set_file ( + E_SERVER_SIDE_SOURCE (object), + g_value_get_object (value)); + return; + + case PROP_REMOVABLE: + e_server_side_source_set_removable ( + E_SERVER_SIDE_SOURCE (object), + g_value_get_boolean (value)); + return; + + case PROP_SERVER: + server_side_source_set_server ( + E_SERVER_SIDE_SOURCE (object), + g_value_get_object (value)); + return; + + case PROP_UID: + server_side_source_set_uid ( + E_SERVER_SIDE_SOURCE (object), + g_value_get_string (value)); + return; + + case PROP_WRITABLE: + e_server_side_source_set_writable ( + E_SERVER_SIDE_SOURCE (object), + g_value_get_boolean (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +server_side_source_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_ALLOW_AUTH_PROMPT: + g_value_set_boolean ( + value, + e_server_side_source_get_allow_auth_prompt ( + E_SERVER_SIDE_SOURCE (object))); + return; + + case PROP_FILE: + g_value_set_object ( + value, + e_server_side_source_get_file ( + E_SERVER_SIDE_SOURCE (object))); + return; + + case PROP_REMOVABLE: + g_value_set_boolean ( + value, + e_source_get_removable ( + E_SOURCE (object))); + return; + + case PROP_SERVER: + g_value_set_object ( + value, + e_server_side_source_get_server ( + E_SERVER_SIDE_SOURCE (object))); + return; + + case PROP_UID: + g_value_take_string ( + value, + e_source_dup_uid ( + E_SOURCE (object))); + return; + + case PROP_WRITABLE: + g_value_set_boolean ( + value, + e_source_get_writable ( + E_SOURCE (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +server_side_source_dispose (GObject *object) +{ + EServerSideSourcePrivate *priv; + + priv = E_SERVER_SIDE_SOURCE_GET_PRIVATE (object); + + if (priv->server != NULL) { + g_object_remove_weak_pointer ( + G_OBJECT (priv->server), &priv->server); + priv->server = NULL; + } + + if (priv->file != NULL) { + g_object_unref (priv->file); + priv->file = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_server_side_source_parent_class)->dispose (object); +} + +static void +server_side_source_finalize (GObject *object) +{ + EServerSideSourcePrivate *priv; + + priv = E_SERVER_SIDE_SOURCE_GET_PRIVATE (object); + + g_node_unlink (&priv->node); + + g_free (priv->uid); + g_free (priv->file_contents); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_server_side_source_parent_class)->finalize (object); +} + +static void +server_side_source_changed (ESource *source) +{ + GDBusObject *dbus_object; + EDBusSource *dbus_source; + gchar *old_data; + gchar *new_data; + GError *error = NULL; + + dbus_object = e_source_ref_dbus_object (source); + dbus_source = e_dbus_object_get_source (E_DBUS_OBJECT (dbus_object)); + + old_data = e_dbus_source_dup_data (dbus_source); + new_data = e_source_to_string (source, NULL); + + /* Setting the "data" property triggers the ESource::changed, + * signal, which invokes this callback, which sets the "data" + * property, etc. This breaks an otherwise infinite loop. */ + if (g_strcmp0 (old_data, new_data) != 0) + e_dbus_source_set_data (dbus_source, new_data); + + g_free (old_data); + g_free (new_data); + + g_object_unref (dbus_source); + g_object_unref (dbus_object); + + /* This writes the "data" property to disk. */ + e_source_write_sync (source, NULL, &error); + + if (error != NULL) { + g_warning ("%s: %s", G_STRFUNC, error->message); + g_error_free (error); + } +} + +static gboolean +server_side_source_remove_sync (ESource *source, + GCancellable *cancellable, + GError **error) +{ + AsyncClosure *closure; + GAsyncResult *result; + gboolean success; + + closure = async_closure_new (); + + e_source_remove ( + source, cancellable, async_closure_callback, closure); + + result = async_closure_wait (closure); + + success = e_source_remove_finish (source, result, error); + + async_closure_free (closure); + + return success; +} + +static void +server_side_source_remove (ESource *source, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + EServerSideSourcePrivate *priv; + GSimpleAsyncResult *simple; + ESourceRegistryServer *server; + GQueue queue = G_QUEUE_INIT; + GList *list, *link; + gboolean success = TRUE; + GError *error = NULL; + + /* XXX Yes we block here. We do this operation + * synchronously to keep the server code simple. */ + + priv = E_SERVER_SIDE_SOURCE_GET_PRIVATE (source); + + simple = g_simple_async_result_new ( + G_OBJECT (source), callback, user_data, + server_side_source_remove); + + g_simple_async_result_set_check_cancellable (simple, cancellable); + + /* Collect the source and its descendants into a queue. + * Do this before unexporting so we hold references to + * all the removed sources. */ + g_node_traverse ( + &priv->node, G_POST_ORDER, G_TRAVERSE_ALL, -1, + (GNodeTraverseFunc) server_side_source_traverse_cb, &queue); + + /* Unexport the object and its descendants. */ + server = E_SOURCE_REGISTRY_SERVER (priv->server); + e_source_registry_server_remove_source (server, source); + + list = g_queue_peek_head_link (&queue); + + /* Delete the key file for each source in the queue. */ + for (link = list; link != NULL; link = g_list_next (link)) { + EServerSideSource *child; + GFile *file; + + child = E_SERVER_SIDE_SOURCE (link->data); + file = e_server_side_source_get_file (child); + + if (file != NULL) + success = g_file_delete (file, cancellable, &error); + + if (!success) + goto exit; + } + +exit: + while (!g_queue_is_empty (&queue)) + g_object_unref (g_queue_pop_head (&queue)); + + if (error != NULL) + g_simple_async_result_take_error (simple, error); + + g_simple_async_result_complete_in_idle (simple); + g_object_unref (simple); +} + +static gboolean +server_side_source_remove_finish (ESource *source, + GAsyncResult *result, + GError **error) +{ + GSimpleAsyncResult *simple; + + g_return_val_if_fail ( + g_simple_async_result_is_valid ( + result, G_OBJECT (source), + server_side_source_remove), FALSE); + + simple = G_SIMPLE_ASYNC_RESULT (result); + + /* Assume success unless a GError is set. */ + return !g_simple_async_result_propagate_error (simple, error); +} + +static gboolean +server_side_source_write_sync (ESource *source, + GCancellable *cancellable, + GError **error) +{ + AsyncClosure *closure; + GAsyncResult *result; + gboolean success; + + closure = async_closure_new (); + + e_source_write ( + source, cancellable, async_closure_callback, closure); + + result = async_closure_wait (closure); + + success = e_source_write_finish (source, result, error); + + async_closure_free (closure); + + return success; +} + +static void +server_side_source_write (ESource *source, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + EServerSideSourcePrivate *priv; + GSimpleAsyncResult *simple; + GDBusObject *dbus_object; + EDBusSource *dbus_source; + gboolean replace_file; + const gchar *old_data; + gchar *new_data; + GError *error = NULL; + + /* XXX Yes we block here. We do this operation + * synchronously to keep the server code simple. */ + + priv = E_SERVER_SIDE_SOURCE_GET_PRIVATE (source); + + simple = g_simple_async_result_new ( + G_OBJECT (source), callback, user_data, + server_side_source_write); + + g_simple_async_result_set_check_cancellable (simple, cancellable); + + dbus_object = e_source_ref_dbus_object (source); + dbus_source = e_dbus_object_get_source (E_DBUS_OBJECT (dbus_object)); + + old_data = priv->file_contents; + new_data = e_source_to_string (source, NULL); + + /* When writing source data to disk, we always write to the + * user directory even if the key file was originally in the + * system-wide directory. For that reason, we want to avoid + * writing unmodified data from the system-wide directory. */ + + replace_file = + G_IS_FILE (priv->file) && + (g_strcmp0 (old_data, new_data) != 0); + + if (replace_file) { + GFile *file; + gchar *uid; + + uid = e_server_side_source_uid_from_file (priv->file, NULL); + g_warn_if_fail (uid != NULL); /* this should never fail */ + file = e_server_side_source_new_user_file (uid); + g_free (uid); + + if (!g_file_equal (file, priv->file)) { + g_object_unref (priv->file); + priv->file = g_object_ref (file); + } + + server_side_source_print_diff (source, old_data, new_data); + + g_file_replace_contents ( + file, new_data, strlen (new_data), NULL, FALSE, + G_FILE_CREATE_NONE, NULL, cancellable, &error); + + if (error == NULL) { + g_free (priv->file_contents); + priv->file_contents = new_data; + new_data = NULL; + } + + g_object_unref (file); + } + + g_free (new_data); + + g_object_unref (dbus_source); + g_object_unref (dbus_object); + + if (error != NULL) + g_simple_async_result_take_error (simple, error); + + g_simple_async_result_complete_in_idle (simple); + g_object_unref (simple); +} + +static gboolean +server_side_source_write_finish (ESource *source, + GAsyncResult *result, + GError **error) +{ + GSimpleAsyncResult *simple; + + g_return_val_if_fail ( + g_simple_async_result_is_valid ( + result, G_OBJECT (source), + server_side_source_write), FALSE); + + simple = G_SIMPLE_ASYNC_RESULT (result); + + /* Assume success unless a GError is set. */ + return !g_simple_async_result_propagate_error (simple, error); +} + +static gboolean +server_side_source_initable_init (GInitable *initable, + GCancellable *cancellable, + GError **error) +{ + EServerSideSource *source; + GDBusObject *dbus_object; + EDBusSource *dbus_source; + GFile *file; + + source = E_SERVER_SIDE_SOURCE (initable); + file = e_server_side_source_get_file (source); + + if (file != NULL) { + g_warn_if_fail (source->priv->uid == NULL); + source->priv->uid = + e_server_side_source_uid_from_file (file, error); + if (source->priv->uid == NULL) + return FALSE; + } + + if (source->priv->uid == NULL) + source->priv->uid = e_uid_new (); + + dbus_source = e_dbus_source_skeleton_new (); + + e_dbus_source_set_uid (dbus_source, source->priv->uid); + + dbus_object = e_source_ref_dbus_object (E_SOURCE (source)); + e_dbus_object_skeleton_set_source ( + E_DBUS_OBJECT_SKELETON (dbus_object), dbus_source); + g_object_unref (dbus_object); + + g_signal_connect ( + dbus_source, "handle-allow-auth-prompt", + G_CALLBACK (server_side_source_allow_auth_prompt_cb), source); + + g_object_unref (dbus_source); + + if (!e_server_side_source_load (source, cancellable, error)) + return FALSE; + + /* Chain up to parent interface's init() method. */ + return initable_parent_interface->init (initable, cancellable, error); +} + +static void +e_server_side_source_class_init (EServerSideSourceClass *class) +{ + GObjectClass *object_class; + ESourceClass *source_class; + + g_type_class_add_private (class, sizeof (EServerSideSourcePrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = server_side_source_set_property; + object_class->get_property = server_side_source_get_property; + object_class->dispose = server_side_source_dispose; + object_class->finalize = server_side_source_finalize; + + source_class = E_SOURCE_CLASS (class); + source_class->changed = server_side_source_changed; + source_class->remove_sync = server_side_source_remove_sync; + source_class->remove = server_side_source_remove; + source_class->remove_finish = server_side_source_remove_finish; + source_class->write_sync = server_side_source_write_sync; + source_class->write = server_side_source_write; + source_class->write_finish = server_side_source_write_finish; + + g_object_class_install_property ( + object_class, + PROP_ALLOW_AUTH_PROMPT, + g_param_spec_boolean ( + "allow-auth-prompt", + "Allow Auth Prompt", + "Whether authentication sessions may " + "interrupt the user for a password", + TRUE, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property ( + object_class, + PROP_FILE, + g_param_spec_object ( + "file", + "File", + "The key file for the data source", + G_TYPE_FILE, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + /* This overrides the "removable" property + * in ESourceClass with a writable version. */ + g_object_class_install_property ( + object_class, + PROP_REMOVABLE, + g_param_spec_boolean ( + "removable", + "Removable", + "Whether the data source is removable", + FALSE, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property ( + object_class, + PROP_SERVER, + g_param_spec_object ( + "server", + "Server", + "The server to which the data source belongs", + E_TYPE_SOURCE_REGISTRY_SERVER, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + /* This overrides the "uid" property in + * ESourceClass with a construct-only version. */ + g_object_class_install_property ( + object_class, + PROP_UID, + g_param_spec_string ( + "uid", + "UID", + "The unique identity of the data source", + NULL, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + /* This overrides the "writable" property + * in ESourceClass with a writable version. */ + g_object_class_install_property ( + object_class, + PROP_WRITABLE, + g_param_spec_boolean ( + "writable", + "Writable", + "Whether the data source is writable", + FALSE, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); +} + +static void +e_server_side_source_initable_init (GInitableIface *interface) +{ + initable_parent_interface = g_type_interface_peek_parent (interface); + + interface->init = server_side_source_initable_init; +} + +static void +e_server_side_source_init (EServerSideSource *source) +{ + source->priv = E_SERVER_SIDE_SOURCE_GET_PRIVATE (source); + + source->priv->node.data = source; +} + +/** + * e_server_side_source_get_user_dir: + * + * Returns the directory where user-specific data source files are stored. + * + * Returns: the user-specific data source directory + * + * Since: 3.6 + **/ +const gchar * +e_server_side_source_get_user_dir (void) +{ + static gchar *dirname = NULL; + + if (G_UNLIKELY (dirname == NULL)) { + const gchar *config_dir = e_get_user_config_dir (); + dirname = g_build_filename (config_dir, "sources", NULL); + g_mkdir_with_parents (dirname, 0700); + } + + return dirname; +} + +/** + * e_server_side_source_new_user_file: + * @uid: unique identifier for a data source, or %NULL + * + * Generates a unique file name for a new user-specific data source. + * If @uid is non-%NULL it will be used in the basename of the file, + * otherwise a unique basename will be generated using e_uid_new(). + * + * The returned #GFile can then be passed to e_server_side_source_new(). + * Unreference the #GFile with g_object_unref() when finished with it. + * + * Note the data source file itself is not created here, only its name. + * + * Returns: the #GFile for a new data source + * + * Since: 3.6 + **/ +GFile * +e_server_side_source_new_user_file (const gchar *uid) +{ + GFile *file; + gchar *safe_uid; + gchar *basename; + gchar *filename; + const gchar *user_dir; + + if (uid == NULL) + safe_uid = e_uid_new (); + else + safe_uid = g_strdup (uid); + e_filename_make_safe (safe_uid); + + user_dir = e_server_side_source_get_user_dir (); + basename = g_strconcat (safe_uid, ".source", NULL); + filename = g_build_filename (user_dir, basename, NULL); + + file = g_file_new_for_path (filename); + + g_free (basename); + g_free (filename); + g_free (safe_uid); + + return file; +} + +/** + * e_server_side_source_uid_from_file: + * @file: a #GFile for a data source + * @error: return location for a #GError, or %NULL + * + * Extracts a unique identity string from the base name of @file. + * If the base name of @file is missing a '.source' extension, the + * function sets @error and returns %NULL. + * + * Returns: the unique identity string for @file, or %NULL + * + * Since: 3.6 + **/ +gchar * +e_server_side_source_uid_from_file (GFile *file, + GError **error) +{ + gchar *basename; + gchar *uid = NULL; + + g_return_val_if_fail (G_IS_FILE (file), FALSE); + + basename = g_file_get_basename (file); + + if (g_str_has_suffix (basename, ".source")) { + /* strlen(".source") --> 7 */ + uid = g_strndup (basename, strlen (basename) - 7); + } else { + g_set_error ( + error, G_IO_ERROR, + G_IO_ERROR_INVALID_FILENAME, + _("File must have a '.source' extension")); + } + + g_free (basename); + + return uid; +} + +/** + * e_server_side_source_new: + * @server: an #ESourceRegistryServer + * @file: a #GFile, or %NULL + * @error: return location for a #GError, or %NULL + * + * Creates a new #EServerSideSource which belongs to @server. If @file + * is non-%NULL and points to an existing file, the #EServerSideSource is + * initialized from the file content. If a read error occurs or the file + * contains syntax errors, the function sets @error and returns %NULL. + * + * Returns: a new #EServerSideSource, or %NULL + * + * Since: 3.6 + **/ +ESource * +e_server_side_source_new (ESourceRegistryServer *server, + GFile *file, + GError **error) +{ + EDBusObjectSkeleton *dbus_object; + ESource *source; + + g_return_val_if_fail (E_IS_SOURCE_REGISTRY_SERVER (server), NULL); + g_return_val_if_fail (file == NULL || G_IS_FILE (file), NULL); + + /* XXX This is an awkward way of initializing the "dbus-object" + * property, but e_source_ref_dbus_object() needs to work. */ + dbus_object = e_dbus_object_skeleton_new (DBUS_OBJECT_PATH); + + source = g_initable_new ( + E_TYPE_SERVER_SIDE_SOURCE, NULL, error, + "dbus-object", dbus_object, + "file", file, "server", server, NULL); + + g_object_unref (dbus_object); + + return source; +} + +/** + * e_server_side_source_new_memory_only: + * @server: an #ESourceRegistryServer + * @uid: a unique identifier, or %NULL + * @error: return location for a #GError, or %NULL + * + * Creates a memory-only #EServerSideSource which belongs to @server. + * No on-disk key file is created for this data source, so it will not + * be remembered across sessions. + * + * Data source collections are often populated with memory-only data + * sources to serve as proxies for resources discovered on a remote server. + * These data sources are usually neither #EServerSideSource:writable nor + * #EServerSideSource:removable by clients, at least not directly. + * + * If an error occurs while instantiating the #EServerSideSource, the + * function sets @error and returns %NULL. Although at this time there + * are no known error conditions for memory-only data sources. + * + * Returns: a new memory-only #EServerSideSource, or %NULL + * + * Since: 3.6 + **/ +ESource * +e_server_side_source_new_memory_only (ESourceRegistryServer *server, + const gchar *uid, + GError **error) +{ + EDBusObjectSkeleton *dbus_object; + ESource *source; + + g_return_val_if_fail (E_IS_SOURCE_REGISTRY_SERVER (server), NULL); + + /* XXX This is an awkward way of initializing the "dbus-object" + * property, but e_source_ref_dbus_object() needs to work. */ + dbus_object = e_dbus_object_skeleton_new (DBUS_OBJECT_PATH); + + source = g_initable_new ( + E_TYPE_SERVER_SIDE_SOURCE, NULL, error, + "dbus-object", dbus_object, + "server", server, "uid", uid, NULL); + + g_object_unref (dbus_object); + + return source; +} + +/** + * e_server_side_source_load: + * @source: an #EServerSideSource + * @cancellable: optional #GCancellable object, or %NULL + * @error: return location for a #GError, or %NULL + * + * Reloads data source content from the file pointed to by the + * #EServerSideSource:file property. + * + * If the #EServerSideSource:file property is %NULL or the file it points + * to does not exist, the function does nothing and returns %TRUE. + * + * If a read error occurs or the file contains syntax errors, the function + * sets @error and returns %FALSE. + * + * Returns: %TRUE on success, %FALSE on failure + * + * Since: 3.6 + **/ +gboolean +e_server_side_source_load (EServerSideSource *source, + GCancellable *cancellable, + GError **error) +{ + GDBusObject *dbus_object; + EDBusSource *dbus_source; + GKeyFile *key_file; + GFile *file; + gboolean success; + gchar *data = NULL; + gsize length; + GError *local_error = NULL; + + g_return_val_if_fail (E_IS_SERVER_SIDE_SOURCE (source), FALSE); + + file = e_server_side_source_get_file (source); + + if (file != NULL) + g_file_load_contents ( + file, cancellable, &data, + &length, NULL, &local_error); + + /* Disregard G_IO_ERROR_NOT_FOUND and treat it as a successful load. */ + if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) { + g_error_free (local_error); + + } else if (local_error != NULL) { + g_propagate_error (error, local_error); + return FALSE; + + } else { + source->priv->file_contents = g_strdup (data); + } + + if (data == NULL) { + /* Create the bare minimum to pass parse_data(). */ + data = g_strdup_printf ("[%s]", PRIMARY_GROUP_NAME); + length = strlen (data); + } + + key_file = g_key_file_new (); + + success = server_side_source_parse_data ( + key_file, data, length, error); + + g_key_file_free (key_file); + + if (!success) { + g_free (data); + return FALSE; + } + + /* Update the D-Bus interface properties. */ + + dbus_object = e_source_ref_dbus_object (E_SOURCE (source)); + dbus_source = e_dbus_object_get_source (E_DBUS_OBJECT (dbus_object)); + + e_dbus_source_set_data (dbus_source, data); + + g_object_unref (dbus_source); + g_object_unref (dbus_object); + + g_free (data); + + return TRUE; +} + +/** + * e_server_side_source_get_file: + * @source: an #EServerSideSource + * + * Returns the #GFile from which data source content is loaded and to + * which changes are saved. Note the @source may not have a #GFile. + * + * Returns: the #GFile for @source, or %NULL + * + * Since: 3.6 + **/ +GFile * +e_server_side_source_get_file (EServerSideSource *source) +{ + g_return_val_if_fail (E_IS_SERVER_SIDE_SOURCE (source), NULL); + + return source->priv->file; +} + +/** + * e_server_side_source_get_node: + * @source: an #EServerSideSource + * + * Returns the #GNode representing the @source's hierarchical placement, + * or %NULL if @source has not been placed in the data source hierarchy. + * The data member of the #GNode points back to @source. This is an easy + * way to traverse ancestor and descendant data sources. + * + * Note that accessing other data sources this way is not thread-safe, + * and this therefore function may be replaced at some later date. + * + * Returns: a #GNode, or %NULL + * + * Since: 3.6 + **/ +GNode * +e_server_side_source_get_node (EServerSideSource *source) +{ + g_return_val_if_fail (E_IS_SERVER_SIDE_SOURCE (source), NULL); + + return &source->priv->node; +} + +/** + * e_server_side_source_get_server: + * @source: an #EServerSideSource + * + * Returns the #ESourceRegistryServer to which @source belongs. + * + * Returns: the #ESourceRegistryServer for @source + * + * Since: 3.6 + **/ +ESourceRegistryServer * +e_server_side_source_get_server (EServerSideSource *source) +{ + g_return_val_if_fail (E_IS_SERVER_SIDE_SOURCE (source), NULL); + + return source->priv->server; +} + +/** + * e_server_side_source_get_allow_auth_prompt: + * @source: an #EServerSideSource + * + * Returns whether an authentication prompt is allowed to be shown + * for @source. #EAuthenticationSession will honor this setting by + * dismissing the session if it can't find a valid stored password. + * + * See e_server_side_source_set_allow_auth_prompt() for further + * discussion. + * + * Returns: whether auth prompts are allowed for @source + * + * Since: 3.6 + **/ +gboolean +e_server_side_source_get_allow_auth_prompt (EServerSideSource *source) +{ + g_return_val_if_fail (E_IS_SERVER_SIDE_SOURCE (source), FALSE); + + return source->priv->allow_auth_prompt; +} + +/** + * e_server_side_source_set_allow_auth_prompt: + * @source: an #EServerSideSource + * @allow_auth_prompt: whether auth prompts are allowed for @source + * + * Sets whether an authentication prompt is allowed to be shown for @source. + * #EAuthenticationSession will honor this setting by dismissing the session + * if it can't find a valid stored password. + * + * If the user declines to provide a password for @source when prompted + * by an #EAuthenticationSession, the #ESourceRegistryServer will set this + * property to %FALSE to suppress any further prompting, which would likely + * annoy the user. However when an #ESourceRegistry instance is created by + * a client application, the first thing it does is reset this property to + * %TRUE for all registered data sources. So suppressing authentication + * prompts is only ever temporary. + * + * Since: 3.6 + **/ +void +e_server_side_source_set_allow_auth_prompt (EServerSideSource *source, + gboolean allow_auth_prompt) +{ + g_return_if_fail (E_IS_SERVER_SIDE_SOURCE (source)); + + source->priv->allow_auth_prompt = allow_auth_prompt; + + g_object_notify (G_OBJECT (source), "allow-auth-prompt"); +} + +/** + * e_server_side_source_set_removable: + * @source: an #EServerSideSource + * @removable: whether to export the Removable interface + * + * Sets whether to allow registry clients to remove @source and its + * descendants. If %TRUE, the Removable D-Bus interface is exported at + * the object path for @source. If %FALSE, the Removable D-Bus interface + * is unexported at the object path for @source, and any attempt by clients + * to call e_source_remove() will fail. + * + * Note this is only enforced for clients of the registry D-Bus service. + * The service itself may remove any data source at any time. + * + * Since: 3.6 + **/ +void +e_server_side_source_set_removable (EServerSideSource *source, + gboolean removable) +{ + EDBusSourceRemovable *dbus_source_removable = NULL; + GDBusObject *dbus_object; + gboolean currently_removable; + + g_return_if_fail (E_IS_SERVER_SIDE_SOURCE (source)); + + currently_removable = e_source_get_removable (E_SOURCE (source)); + + if (removable == currently_removable) + return; + + if (removable) { + dbus_source_removable = + e_dbus_source_removable_skeleton_new (); + + g_signal_connect ( + dbus_source_removable, "handle-remove", + G_CALLBACK (server_side_source_remove_cb), source); + } + + dbus_object = e_source_ref_dbus_object (E_SOURCE (source)); + e_dbus_object_skeleton_set_source_removable ( + E_DBUS_OBJECT_SKELETON (dbus_object), dbus_source_removable); + g_object_unref (dbus_object); + + if (dbus_source_removable != NULL) + g_object_unref (dbus_source_removable); + + g_object_notify (G_OBJECT (source), "removable"); +} + +/** + * e_server_side_source_set_writable: + * @source: an #EServerSideSource + * @writable: whether to export the Writable interface + * + * Sets whether to allow registry clients to alter the content of @source. + * If %TRUE, the Writable D-Bus interface is exported at the object path + * for @source. If %FALSE, the Writable D-Bus interface is unexported at + * the object path for @source, and any attempt by clients to call + * e_source_write() will fail. + * + * Note this is only enforced for clients of the registry D-Bus service. + * The service itself can write to any data source at any time. + * + * Since: 3.6 + **/ +void +e_server_side_source_set_writable (EServerSideSource *source, + gboolean writable) +{ + EDBusSourceWritable *dbus_source_writable = NULL; + GDBusObject *dbus_object; + gboolean currently_writable; + + g_return_if_fail (E_IS_SERVER_SIDE_SOURCE (source)); + + currently_writable = e_source_get_writable (E_SOURCE (source)); + + if (writable == currently_writable) + return; + + if (writable) { + dbus_source_writable = + e_dbus_source_writable_skeleton_new (); + + g_signal_connect ( + dbus_source_writable, "handle-write", + G_CALLBACK (server_side_source_write_cb), source); + } + + dbus_object = e_source_ref_dbus_object (E_SOURCE (source)); + e_dbus_object_skeleton_set_source_writable ( + E_DBUS_OBJECT_SKELETON (dbus_object), dbus_source_writable); + g_object_unref (dbus_object); + + if (dbus_source_writable != NULL) + g_object_unref (dbus_source_writable); + + g_object_notify (G_OBJECT (source), "writable"); +} + diff --git a/libebackend/e-server-side-source.h b/libebackend/e-server-side-source.h new file mode 100644 index 0000000..74ca374 --- /dev/null +++ b/libebackend/e-server-side-source.h @@ -0,0 +1,104 @@ +/* + * e-server-side-source.h + * + * 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 + * + */ + +#ifndef E_SERVER_SIDE_SOURCE_H +#define E_SERVER_SIDE_SOURCE_H + +#include +#include + +/* Standard GObject macros */ +#define E_TYPE_SERVER_SIDE_SOURCE \ + (e_server_side_source_get_type ()) +#define E_SERVER_SIDE_SOURCE(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_SERVER_SIDE_SOURCE, EServerSideSource)) +#define E_SERVER_SIDE_SOURCE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_SERVER_SIDE_SOURCE, EServerSideSourceClass)) +#define E_IS_SERVER_SIDE_SOURCE(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_SERVER_SIDE_SOURCE)) +#define E_IS_SERVER_SIDE_SOURCE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_SERVER_SIDE_SOURCE)) +#define E_SERVER_SIDE_SOURCE_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_SERVER_SIDE_SOURCE, EServerSideSourceClass)) + +G_BEGIN_DECLS + +typedef struct _EServerSideSource EServerSideSource; +typedef struct _EServerSideSourceClass EServerSideSourceClass; +typedef struct _EServerSideSourcePrivate EServerSideSourcePrivate; + +/** + * EServerSideSource: + * + * Contains only private data that should be read and manipulated using the + * functions below. + * + * Since: 3.6 + **/ +struct _EServerSideSource { + ESource parent; + EServerSideSourcePrivate *priv; +}; + +struct _EServerSideSourceClass { + ESourceClass parent_class; +}; + +GType e_server_side_source_get_type (void) G_GNUC_CONST; +const gchar * e_server_side_source_get_user_dir + (void) G_GNUC_CONST; +GFile * e_server_side_source_new_user_file + (const gchar *uid); +gchar * e_server_side_source_uid_from_file + (GFile *file, + GError **error); +ESource * e_server_side_source_new (ESourceRegistryServer *server, + GFile *file, + GError **error); +ESource * e_server_side_source_new_memory_only + (ESourceRegistryServer *server, + const gchar *uid, + GError **error); +gboolean e_server_side_source_load (EServerSideSource *source, + GCancellable *cancellable, + GError **error); +GFile * e_server_side_source_get_file (EServerSideSource *source); +GNode * e_server_side_source_get_node (EServerSideSource *source); +ESourceRegistryServer * + e_server_side_source_get_server (EServerSideSource *source); +gboolean e_server_side_source_get_allow_auth_prompt + (EServerSideSource *source); +void e_server_side_source_set_allow_auth_prompt + (EServerSideSource *source, + gboolean allow_auth_prompt); +void e_server_side_source_set_removable + (EServerSideSource *source, + gboolean removable); +void e_server_side_source_set_writable + (EServerSideSource *source, + gboolean writable); + +G_END_DECLS + +#endif /* E_SERVER_SIDE_SOURCE_H */ + diff --git a/libebackend/e-source-registry-server.c b/libebackend/e-source-registry-server.c new file mode 100644 index 0000000..8db22e2 --- /dev/null +++ b/libebackend/e-source-registry-server.c @@ -0,0 +1,1801 @@ +/* + * 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/e-source-registry-server.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 +#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__" + +struct _ESourceRegistryServerPrivate { + 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: + * + * auth_queue = [ UID, ... ] + * auth_table = { UID : [ authenticator, ... ] } + * active_auth_table_queue = auth_table[auth_queue[0]] + * active_auth = active_auth_table_queue[0] + * + * 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. + */ + GQueue *auth_queue; + GHashTable *auth_table; + GQueue *active_auth_table_queue; + EAuthenticationSession *active_auth; + GCancellable *auth_cancellable; + + guint authentication_count; +}; + +enum { + LOAD_ERROR, + FILES_LOADED, + SOURCE_ADDED, + SOURCE_REMOVED, + LAST_SIGNAL +}; + +/* Forward Declarations */ +static void source_registry_server_maybe_start_auth_session + (ESourceRegistryServer *server); + +static guint signals[LAST_SIGNAL]; + +G_DEFINE_TYPE ( + ESourceRegistryServer, + e_source_registry_server, + E_TYPE_DATA_FACTORY) + +/* 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 'auth_table' 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 GQueue * +source_registry_server_auth_table_lookup (ESourceRegistryServer *server, + const gchar *uid) +{ + GHashTable *hash_table; + GQueue *queue; + + hash_table = server->priv->auth_table; + queue = g_hash_table_lookup (hash_table, uid); + + if (queue == NULL) { + queue = g_queue_new (); + g_hash_table_insert (hash_table, g_strdup (uid), queue); + } + + return queue; +} + +static void +source_registry_server_auth_session_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + EAuthenticationSession *session; + ESourceRegistryServer *server; + EAuthenticationSessionResult auth_result; + GQueue *queue; + const gchar *uid; + GError *error = NULL; + + session = E_AUTHENTICATION_SESSION (source_object); + server = E_SOURCE_REGISTRY_SERVER (user_data); + + auth_result = e_authentication_session_execute_finish ( + session, result, &error); + + if (error != NULL) { + g_warning ("%s: %s", G_STRFUNC, error->message); + g_error_free (error); + } + + uid = e_authentication_session_get_source_uid (session); + g_return_if_fail (uid != NULL); + + /* Authentication dismissals may require additional handling. */ + if (auth_result == E_AUTHENTICATION_SESSION_DISMISSED) { + ESourceAuthenticator *authenticator; + ESource *source; + + /* 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)); + + /* This will return NULL if the authenticating data source + * has not yet been submitted to the D-Bus registry service. */ + source = e_source_registry_server_ref_source (server, uid); + if (source != NULL) { + /* Prevent further user interruptions. */ + e_server_side_source_set_allow_auth_prompt ( + E_SERVER_SIDE_SOURCE (source), FALSE); + g_object_unref (source); + } + } + + queue = source_registry_server_auth_table_lookup (server, uid); + + /* Remove the completed auth session from its queue. */ + if (g_queue_remove (queue, session)) + g_object_unref (session); + + /* If the completed auth session was the active one, + * clear the active pointer for the next auth session. */ + if (session == server->priv->active_auth) + server->priv->active_auth = NULL; + + source_registry_server_maybe_start_auth_session (server); + + g_object_unref (server); +} + +static void +source_registry_server_maybe_start_auth_session (ESourceRegistryServer *server) +{ + GQueue *queue; + + if (server->priv->active_auth != NULL) + return; + + if (g_cancellable_is_cancelled (server->priv->auth_cancellable)) + return; + + /* Check if there's any authenticators left in the active + * auth table queue. If the user provided a valid secret + * and elected to save it in the keyring, or if the user + * dismissed the authentication prompt, then we should be + * able to process the remaining authenticators quickly. */ + if (server->priv->active_auth_table_queue != NULL && + !g_queue_is_empty (server->priv->active_auth_table_queue)) { + queue = server->priv->active_auth_table_queue; + server->priv->active_auth = g_queue_peek_head (queue); + + /* Otherwise find the next non-empty auth table queue to + * be processed, according to the UIDs in the auth queue. */ + } else while (!g_queue_is_empty (server->priv->auth_queue)) { + gchar *uid; + + uid = g_queue_pop_head (server->priv->auth_queue); + queue = source_registry_server_auth_table_lookup (server, uid); + g_free (uid); + + if (!g_queue_is_empty (queue)) { + server->priv->active_auth_table_queue = queue; + server->priv->active_auth = g_queue_peek_head (queue); + break; + } + } + + /* Initiate the new active authenicator. 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. */ + if (server->priv->active_auth != NULL) + e_authentication_session_execute ( + server->priv->active_auth, + G_PRIORITY_DEFAULT, + server->priv->auth_cancellable, + source_registry_server_auth_session_cb, + g_object_ref (server)); + else + server->priv->active_auth_table_queue = NULL; +} + +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; + + /* This references the session and adds it to a queue. */ + server = e_authentication_session_get_server (session); + e_source_registry_server_queue_auth_session (server, session); + + } 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_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_authentication_session_new ( + 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; + GKeyFile *key_file; + gboolean success; + gsize length; + + 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); + + /* 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. */ + + 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); + + 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; + + source = e_server_side_source_new (server, file, &error); + + /* Sanity check. */ + g_return_if_fail ( + ((source != NULL) && (error == NULL)) || + ((source == NULL) && (error != NULL))); + + if (error == NULL) { + e_source_registry_server_add_source (server, source); + g_object_unref (source); + } else { + 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->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->auth_table); + + if (priv->auth_cancellable != NULL) { + g_object_unref (priv->auth_cancellable); + priv->auth_cancellable = NULL; + } + + /* 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_free (priv->sources_lock); + g_mutex_free (priv->orphans_lock); + + g_queue_free_full (priv->auth_queue, (GDestroyNotify) g_free); + g_hash_table_destroy (priv->auth_table); + + /* 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); + + /* Cancel any active authentication session. */ + g_cancellable_cancel (priv->auth_cancellable); + + /* 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)); + + 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_print ("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_unref (dbus_object); +} + +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; + + 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 = MODULE_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, + e_marshal_VOID__OBJECT_BOXED, + 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, + g_cclosure_marshal_VOID__VOID, + 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, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, 1, + E_TYPE_SERVER_SIDE_SOURCE); + + /** + * ESourceRegistryServer::source-removed: + * @server: the #ESourceRegistryServer when 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, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, 1, + E_TYPE_SERVER_SIDE_SOURCE); +} + +static void +e_source_registry_server_init (ESourceRegistryServer *server) +{ + GDBusObjectManagerServer *object_manager; + EDBusSourceManager *source_manager; + GHashTable *sources; + GHashTable *orphans; + GHashTable *monitors; + GHashTable *auth_table; + 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); + + auth_table = 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->object_manager = object_manager; + server->priv->source_manager = source_manager; + server->priv->sources = sources; + server->priv->orphans = orphans; + server->priv->monitors = monitors; + server->priv->sources_lock = g_mutex_new (); + server->priv->orphans_lock = g_mutex_new (); + server->priv->auth_queue = g_queue_new (); + server->priv->auth_table = auth_table; + server->priv->auth_cancellable = g_cancellable_new (); + + 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); + + /* 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_queue_auth_session: + * @server: an #ESourceRegistryServer + * @session: an #EDBusSourceAuthenticator + * + * Queues an authentication session. When its turn comes, and if necessary, + * the user will be prompted for a secret. Sessions are queued this way to + * prevent user prompts from piling up on the screen. + * + * Since: 3.6 + **/ +void +e_source_registry_server_queue_auth_session (ESourceRegistryServer *server, + EAuthenticationSession *session) +{ + const gchar *uid; + GQueue *queue; + + 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); + + /* Add the session to the appropriate queue. */ + queue = source_registry_server_auth_table_lookup (server, uid); + g_queue_push_tail (queue, g_object_ref (session)); + + /* Blindly push the UID onto the processing queue. */ + g_queue_push_tail (server->priv->auth_queue, g_strdup (uid)); + + source_registry_server_maybe_start_auth_session (server); +} + +/** + * 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 + **/ +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; + + monitor = g_file_monitor_directory ( + file, G_FILE_MONITOR_NONE, NULL, error); + if (monitor == NULL) + return FALSE; + + 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), monitor); + } + + g_object_unref (file); + + return TRUE; +} + +/** + * 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; + 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); + + g_free (uid); + + if (source == NULL) + source = e_server_side_source_new (server, file, error); + + 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_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); +} + diff --git a/libebackend/e-source-registry-server.h b/libebackend/e-source-registry-server.h new file mode 100644 index 0000000..9568e5e --- /dev/null +++ b/libebackend/e-source-registry-server.h @@ -0,0 +1,132 @@ +/* + * e-source-registry-server.h + * + * 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 + * + */ + +#ifndef E_SOURCE_REGISTRY_SERVER_H +#define E_SOURCE_REGISTRY_SERVER_H + +#include +#include +#include +#include +#include + +/* Standard GObject macros */ +#define E_TYPE_SOURCE_REGISTRY_SERVER \ + (e_source_registry_server_get_type ()) +#define E_SOURCE_REGISTRY_SERVER(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_SOURCE_REGISTRY_SERVER, ESourceRegistryServer)) +#define E_SOURCE_REGISTRY_SERVER_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_SOURCE_REGISTRY_SERVER, ESourceRegistryServerClass)) +#define E_IS_SOURCE_REGISTRY_SERVER(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_SOURCE_REGISTRY_SERVER)) +#define E_IS_SOURCE_REGISTRY_SERVER_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_SOURCE_REGISTRY_SERVER)) +#define E_SOURCE_REGISTRY_SERVER_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_SOURCE_REGISTRY_SERVER, ESourceRegistryServerClass)) + +/** + * E_SOURCE_REGISTRY_SERVER_OBJECT_PATH: + * + * D-Bus object path of the data source server. + * + * Since: 3.6 + **/ +#define E_SOURCE_REGISTRY_SERVER_OBJECT_PATH \ + "/org/gnome/evolution/dataserver/SourceManager" + +G_BEGIN_DECLS + +typedef struct _ESourceRegistryServer ESourceRegistryServer; +typedef struct _ESourceRegistryServerClass ESourceRegistryServerClass; +typedef struct _ESourceRegistryServerPrivate ESourceRegistryServerPrivate; + +/** + * ESourceRegistryServer: + * + * Contains only private data that should be read and manipulated using the + * functions below. + * + * Since: 3.6 + **/ +struct _ESourceRegistryServer { + EDataFactory parent; + ESourceRegistryServerPrivate *priv; +}; + +struct _ESourceRegistryServerClass { + EDataFactoryClass parent_class; + + /* Signals */ + void (*load_error) (ESourceRegistryServer *server, + GFile *file, + const GError *error); + void (*files_loaded) (ESourceRegistryServer *server); + void (*source_added) (ESourceRegistryServer *server, + ESource *source); + void (*source_removed) (ESourceRegistryServer *server, + ESource *source); +}; + +GType e_source_registry_server_get_type + (void) G_GNUC_CONST; +EDBusServer * e_source_registry_server_new (void); +void e_source_registry_server_add_source + (ESourceRegistryServer *server, + ESource *source); +void e_source_registry_server_remove_source + (ESourceRegistryServer *server, + ESource *source); +void e_source_registry_server_queue_auth_session + (ESourceRegistryServer *server, + EAuthenticationSession *session); +gboolean e_source_registry_server_load_all + (ESourceRegistryServer *server, + GError **error); +gboolean e_source_registry_server_load_directory + (ESourceRegistryServer *server, + const gchar *path, + ESourcePermissionFlags flags, + GError **error); +ESource * e_source_registry_server_load_file + (ESourceRegistryServer *server, + GFile *file, + ESourcePermissionFlags flags, + GError **error); +void e_source_registry_server_load_error + (ESourceRegistryServer *server, + GFile *file, + const GError *error); +ESource * e_source_registry_server_ref_source + (ESourceRegistryServer *server, + const gchar *uid); +GList * e_source_registry_server_list_sources + (ESourceRegistryServer *server, + const gchar *extension_name); +ECollectionBackendFactory * + e_source_registry_server_ref_backend_factory + (ESourceRegistryServer *server, + ESource *source); + +G_END_DECLS + +#endif /* E_SOURCE_REGISTRY_SERVER_H */ diff --git a/libebackend/libebackend.pc.in b/libebackend/libebackend.pc.in index bb661ea..931b931 100644 --- a/libebackend/libebackend.pc.in +++ b/libebackend/libebackend.pc.in @@ -6,6 +6,7 @@ datarootdir=@datarootdir@ datadir=@datadir@ privincludedir=@privincludedir@ +moduledir=@moduledir@ Name: libebackend Description: Utility library for Evolution Data Server Backends diff --git a/services/Makefile.am b/services/Makefile.am index 1750bfa..551a440 100644 --- a/services/Makefile.am +++ b/services/Makefile.am @@ -3,6 +3,7 @@ NULL = SUBDIRS = \ evolution-addressbook-factory \ evolution-calendar-factory \ + evolution-source-registry \ $(NULL) -include $(top_srcdir)/git.mk diff --git a/services/evolution-addressbook-factory/Makefile.am b/services/evolution-addressbook-factory/Makefile.am index 0d99ff7..beafb50 100644 --- a/services/evolution-addressbook-factory/Makefile.am +++ b/services/evolution-addressbook-factory/Makefile.am @@ -19,15 +19,16 @@ evolution_addressbook_factory_CPPFLAGS = \ -I$(top_builddir) \ -I$(top_builddir)/addressbook \ $(EVOLUTION_ADDRESSBOOK_CFLAGS) \ + $(GNOME_KEYRING_CFLAGS) \ $(FACTORY_GTK_CFLAGS) \ $(DBUS_GLIB_CFLAGS) \ + $(SOUP_CFLAGS) \ $(GOA_CFLAGS) \ $(CODE_COVERAGE_CFLAGS) \ $(NULL) evolution_addressbook_factory_SOURCES = \ evolution-addressbook-factory.c \ - evolution-addressbook-factory-migrate-basedir.c \ $(NULL) evolution_addressbook_factory_LDADD = \ @@ -36,8 +37,10 @@ evolution_addressbook_factory_LDADD = \ $(top_builddir)/libebackend/libebackend-1.2.la \ $(top_builddir)/libedataserver/libedataserver-1.2.la \ $(EVOLUTION_ADDRESSBOOK_LIBS) \ + $(GNOME_KEYRING_LIBS) \ $(FACTORY_GTK_LIBS) \ $(DBUS_GLIB_LIBS) \ + $(SOUP_LIBS) \ $(GOA_LIBS) \ $(NULL) diff --git a/services/evolution-addressbook-factory/evolution-addressbook-factory-migrate-basedir.c b/services/evolution-addressbook-factory/evolution-addressbook-factory-migrate-basedir.c deleted file mode 100644 index e8ed74c..0000000 --- a/services/evolution-addressbook-factory/evolution-addressbook-factory-migrate-basedir.c +++ /dev/null @@ -1,319 +0,0 @@ -/* - * evolution-addressbook-factory-migrate-basedir.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 - * - */ - -#include -#include -#include - -/* Forward Declarations */ -void evolution_addressbook_factory_migrate_basedir (void); - -static gboolean -migrate_rename (const gchar *old_filename, - const gchar *new_filename) -{ - gboolean old_filename_is_dir; - gboolean old_filename_exists; - gboolean new_filename_exists; - gboolean success = TRUE; - - old_filename_is_dir = g_file_test (old_filename, G_FILE_TEST_IS_DIR); - old_filename_exists = g_file_test (old_filename, G_FILE_TEST_EXISTS); - new_filename_exists = g_file_test (new_filename, G_FILE_TEST_EXISTS); - - if (!old_filename_exists) - return TRUE; - - g_print (" mv %s %s\n", old_filename, new_filename); - - /* It's safe to go ahead and move directories because rename() - * will fail if the new directory already exists with content. - * With regular files we have to be careful not to overwrite - * new files with old files. */ - if (old_filename_is_dir || !new_filename_exists) { - if (g_rename (old_filename, new_filename) < 0) { - g_printerr (" FAILED: %s\n", g_strerror (errno)); - success = FALSE; - } - } else { - g_printerr (" FAILED: Destination file already exists\n"); - success = FALSE; - } - - return success; -} - -static gboolean -migrate_rmdir (const gchar *dirname) -{ - GDir *dir = NULL; - gboolean success = TRUE; - - if (g_file_test (dirname, G_FILE_TEST_IS_DIR)) { - g_print (" rmdir %s\n", dirname); - if (g_rmdir (dirname) < 0) { - g_printerr (" FAILED: %s", g_strerror (errno)); - if (errno == ENOTEMPTY) { - dir = g_dir_open (dirname, 0, NULL); - g_printerr (" (contents follows)"); - } - g_printerr ("\n"); - success = FALSE; - } - } - - /* List the directory's contents to aid debugging. */ - if (dir != NULL) { - const gchar *basename; - - /* Align the filenames beneath the error message. */ - while ((basename = g_dir_read_name (dir)) != NULL) - g_print (" %s\n", basename); - - g_dir_close (dir); - } - - return success; -} - -static void -migrate_process_corrections (GHashTable *corrections) -{ - GHashTableIter iter; - gpointer old_filename; - gpointer new_filename; - - g_hash_table_iter_init (&iter, corrections); - - while (g_hash_table_iter_next (&iter, &old_filename, &new_filename)) { - migrate_rename (old_filename, new_filename); - g_hash_table_iter_remove (&iter); - } -} - -static gboolean -migrate_move_contents (const gchar *src_directory, - const gchar *dst_directory) -{ - GDir *dir; - GHashTable *corrections; - const gchar *basename; - - dir = g_dir_open (src_directory, 0, NULL); - if (dir == NULL) - return FALSE; - - /* This is to avoid renaming files while we're iterating over the - * directory. POSIX says the outcome of that is unspecified. */ - corrections = g_hash_table_new_full ( - g_str_hash, g_str_equal, - (GDestroyNotify) g_free, - (GDestroyNotify) g_free); - - g_mkdir_with_parents (dst_directory, 0700); - - while ((basename = g_dir_read_name (dir)) != NULL) { - gchar *old_filename; - gchar *new_filename; - - old_filename = g_build_filename (src_directory, basename, NULL); - new_filename = g_build_filename (dst_directory, basename, NULL); - - g_hash_table_insert (corrections, old_filename, new_filename); - } - - g_dir_close (dir); - - migrate_process_corrections (corrections); - g_hash_table_destroy (corrections); - - /* It's tempting to want to remove the source directory here. - * Don't. We might be iterating over the source directory's - * parent directory, and removing the source directory would - * screw up the iteration. */ - - return TRUE; -} - -static void -migrate_fix_groupwise_bug (const gchar *old_base_dir) -{ - GDir *dir; - GHashTable *corrections; - const gchar *basename; - gchar *old_data_dir; - gchar *old_cache_dir; - - /* The groupwise backend mistakenly put its addressbook - * cache files in ~/.evolution/addressbook instead of - * ~/.evolution/cache/addressbook. Fix that before - * we migrate the cache directory. */ - - old_data_dir = g_build_filename (old_base_dir, "addressbook", NULL); - old_cache_dir = g_build_filename (old_base_dir, "cache", "addressbook", NULL); - - dir = g_dir_open (old_data_dir, 0, NULL); - if (dir == NULL) - goto exit; - - /* This is to avoid renaming files while we're iterating over the - * directory. POSIX says the outcome of that is unspecified. */ - corrections = g_hash_table_new_full ( - g_str_hash, g_str_equal, - (GDestroyNotify) g_free, - (GDestroyNotify) g_free); - - while ((basename = g_dir_read_name (dir)) != NULL) { - gchar *old_filename; - gchar *new_filename; - - if (!g_str_has_prefix (basename, "groupwise___")) - continue; - - old_filename = g_build_filename (old_data_dir, basename, NULL); - new_filename = g_build_filename (old_cache_dir, basename, NULL); - - g_hash_table_insert (corrections, old_filename, new_filename); - } - - g_dir_close (dir); - - migrate_process_corrections (corrections); - g_hash_table_destroy (corrections); - -exit: - g_free (old_data_dir); - g_free (old_cache_dir); -} - -static void -migrate_to_user_cache_dir (const gchar *old_base_dir) -{ - const gchar *new_cache_dir; - gchar *old_cache_dir; - gchar *src_directory; - gchar *dst_directory; - - old_cache_dir = g_build_filename (old_base_dir, "cache", NULL); - new_cache_dir = e_get_user_cache_dir (); - - g_print ("Migrating cached backend data\n"); - - /* We don't want to move the source directory directly because the - * destination directory may already exist with content. Instead - * we want to merge the content of the source directory into the - * destination directory. - * - * For example, given: - * - * $(src_directory)/A and $(dst_directory)/B - * $(src_directory)/C - * - * we want to end up with: - * - * $(dst_directory)/A - * $(dst_directory)/B - * $(dst_directory)/C - * - * Any name collisions will be left in the source directory. - */ - - src_directory = g_build_filename (old_cache_dir, "addressbook", NULL); - dst_directory = g_build_filename (new_cache_dir, "addressbook", NULL); - - migrate_move_contents (src_directory, dst_directory); - migrate_rmdir (src_directory); - - g_free (src_directory); - g_free (dst_directory); - - /* Try to remove the old cache directory. Good chance this will - * fail on the first try, since Evolution puts stuff here too. */ - migrate_rmdir (old_cache_dir); - - g_free (old_cache_dir); -} - -static void -migrate_to_user_data_dir (const gchar *old_base_dir) -{ - const gchar *new_data_dir; - gchar *src_directory; - gchar *dst_directory; - - new_data_dir = e_get_user_data_dir (); - - g_print ("Migrating local backend data\n"); - - /* We don't want to move the source directory directly because the - * destination directory may already exist with content. Instead - * we want to merge the content of the source directory into the - * destination directory. - * - * For example, given: - * - * $(src_directory)/A and $(dst_directory)/B - * $(src_directory)/C - * - * we want to end up with: - * - * $(dst_directory)/A - * $(dst_directory)/B - * $(dst_directory)/C - * - * Any name collisions will be left in the source directory. - */ - - src_directory = g_build_filename (old_base_dir, "addressbook", "local", NULL); - dst_directory = g_build_filename (new_data_dir, "addressbook", NULL); - - migrate_move_contents (src_directory, dst_directory); - migrate_rmdir (src_directory); - - g_free (src_directory); - g_free (dst_directory); -} - -void -evolution_addressbook_factory_migrate_basedir (void) -{ - const gchar *home_dir; - gchar *old_base_dir; - - /* XXX This blocks, but it's all just local directory - * renames so it should be nearly instantaneous. */ - - home_dir = g_get_home_dir (); - old_base_dir = g_build_filename (home_dir, ".evolution", NULL); - - /* Is there even anything to migrate? */ - if (!g_file_test (old_base_dir, G_FILE_TEST_IS_DIR)) - goto exit; - - migrate_fix_groupwise_bug (old_base_dir); - - migrate_to_user_cache_dir (old_base_dir); - migrate_to_user_data_dir (old_base_dir); - - /* Try to remove the old base directory. Good chance this will - * fail on the first try, since Evolution puts stuff here too. */ - migrate_rmdir (old_base_dir); - -exit: - g_free (old_base_dir); -} diff --git a/services/evolution-addressbook-factory/evolution-addressbook-factory.c b/services/evolution-addressbook-factory/evolution-addressbook-factory.c index aa1080d..9450c81 100644 --- a/services/evolution-addressbook-factory/evolution-addressbook-factory.c +++ b/services/evolution-addressbook-factory/evolution-addressbook-factory.c @@ -53,9 +53,6 @@ static GOptionEntry entries[] = { { NULL } }; -/* Forward Declarations */ -void evolution_addressbook_factory_migrate_basedir (void); - gint main (gint argc, gchar **argv) @@ -119,9 +116,6 @@ main (gint argc, exit (EXIT_FAILURE); } - /* Migrate user data from ~/.evolution to XDG base directories. */ - evolution_addressbook_factory_migrate_basedir (); - e_gdbus_templates_init_main_thread (); server = e_data_book_factory_new (NULL, &error); diff --git a/services/evolution-calendar-factory/Makefile.am b/services/evolution-calendar-factory/Makefile.am index ba4d3b8..6c205d5 100644 --- a/services/evolution-calendar-factory/Makefile.am +++ b/services/evolution-calendar-factory/Makefile.am @@ -19,14 +19,15 @@ evolution_calendar_factory_CPPFLAGS = \ -I$(top_builddir) \ -I$(top_builddir)/calendar \ $(EVOLUTION_CALENDAR_CFLAGS) \ + $(GNOME_KEYRING_CFLAGS) \ $(FACTORY_GTK_CFLAGS) \ $(DBUS_GLIB_CFLAGS) \ + $(SOUP_CFLAGS) \ $(CODE_COVERAGE_CFLAGS) \ $(NULL) evolution_calendar_factory_SOURCES = \ evolution-calendar-factory.c \ - evolution-calendar-factory-migrate-basedir.c \ $(NULL) evolution_calendar_factory_LDADD = \ @@ -35,8 +36,10 @@ evolution_calendar_factory_LDADD = \ $(top_builddir)/libebackend/libebackend-1.2.la \ $(top_builddir)/libedataserver/libedataserver-1.2.la \ $(EVOLUTION_CALENDAR_LIBS) \ + $(GNOME_KEYRING_LIBS) \ $(FACTORY_GTK_LIBS) \ $(DBUS_GLIB_LIBS) \ + $(SOUP_LIBS) \ $(NULL) evolution_calendar_factory_LDFLAGS = \ diff --git a/services/evolution-calendar-factory/evolution-calendar-factory.c b/services/evolution-calendar-factory/evolution-calendar-factory.c index a044e62..aa76072 100644 --- a/services/evolution-calendar-factory/evolution-calendar-factory.c +++ b/services/evolution-calendar-factory/evolution-calendar-factory.c @@ -57,9 +57,6 @@ static GOptionEntry entries[] = { { NULL } }; -/* Forward Declarations */ -void evolution_calendar_factory_migrate_basedir (void); - gint main (gint argc, gchar **argv) @@ -127,9 +124,6 @@ main (gint argc, ical_set_unknown_token_handling_setting (ICAL_DISCARD_TOKEN); #endif - /* Migrate user data from ~/.evolution to XDG base directories. */ - evolution_calendar_factory_migrate_basedir (); - e_gdbus_templates_init_main_thread (); server = e_data_cal_factory_new (NULL, &error); diff --git a/services/evolution-source-registry/Makefile.am b/services/evolution-source-registry/Makefile.am new file mode 100644 index 0000000..4e65ea2 --- /dev/null +++ b/services/evolution-source-registry/Makefile.am @@ -0,0 +1,41 @@ +NULL = + +service_in_files = org.gnome.evolution.dataserver.Sources.service.in +servicedir = $(datadir)/dbus-1/services +service_DATA = $(service_in_files:.service.in=.service) +@EVO_SUBST_SERVICE_RULE@ + +CLEANFILES = $(service_DATA) +EXTRA_DIST = $(service_in_files) + +libexec_PROGRAMS = evolution-source-registry + +evolution_source_registry_CPPFLAGS = \ + $(AM_CPPFLAGS) \ + -I$(top_srcdir) \ + -I$(top_builddir) \ + -DG_LOG_DOMAIN=\"evolution-source-registry\" \ + -DLOCALEDIR=\"$(localedir)\" \ + $(E_DATA_SERVER_CFLAGS) \ + $(GNOME_KEYRING_CFLAGS) \ + $(CAMEL_CFLAGS) \ + $(SOUP_CFLAGS) \ + $(NULL) + +evolution_source_registry_SOURCES = \ + evolution-source-registry.c \ + evolution-source-registry-migrate-basedir.c \ + evolution-source-registry-migrate-sources.c \ + $(NULL) + +evolution_source_registry_LDADD = \ + $(top_builddir)/libebackend/libebackend-1.2.la \ + $(top_builddir)/libedataserver/libedataserver-1.2.la \ + $(top_builddir)/camel/libcamel-1.2.la \ + $(E_DATA_SERVER_LIBS) \ + $(GNOME_KEYRING_LIBS) \ + $(CAMEL_LIBS) \ + $(SOUP_LIBS) \ + $(NULL) + +-include $(top_srcdir)/git.mk diff --git a/services/evolution-calendar-factory/evolution-calendar-factory-migrate-basedir.c b/services/evolution-source-registry/evolution-source-registry-migrate-basedir.c similarity index 82% rename from services/evolution-calendar-factory/evolution-calendar-factory-migrate-basedir.c rename to services/evolution-source-registry/evolution-source-registry-migrate-basedir.c index d44119c..0c320a0 100644 --- a/services/evolution-calendar-factory/evolution-calendar-factory-migrate-basedir.c +++ b/services/evolution-source-registry/evolution-source-registry-migrate-basedir.c @@ -1,5 +1,5 @@ /* - * evolution-calendar-factory-migrate-basedir.c + * evolution-addressbook-factory-migrate-basedir.c * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -21,7 +21,7 @@ #include /* Forward Declarations */ -void evolution_calendar_factory_migrate_basedir (void); +void evolution_source_registry_migrate_basedir (void); static gboolean migrate_rename (const gchar *old_filename, @@ -221,6 +221,57 @@ migrate_fix_memos_cache_bug (const gchar *old_base_dir) } static void +migrate_fix_groupwise_bug (const gchar *old_base_dir) +{ + GDir *dir; + GHashTable *corrections; + const gchar *basename; + gchar *old_data_dir; + gchar *old_cache_dir; + + /* The groupwise backend mistakenly put its addressbook + * cache files in ~/.evolution/addressbook instead of + * ~/.evolution/cache/addressbook. Fix that before + * we migrate the cache directory. */ + + old_data_dir = g_build_filename (old_base_dir, "addressbook", NULL); + old_cache_dir = g_build_filename (old_base_dir, "cache", "addressbook", NULL); + + dir = g_dir_open (old_data_dir, 0, NULL); + if (dir == NULL) + goto exit; + + /* This is to avoid renaming files while we're iterating over the + * directory. POSIX says the outcome of that is unspecified. */ + corrections = g_hash_table_new_full ( + g_str_hash, g_str_equal, + (GDestroyNotify) g_free, + (GDestroyNotify) g_free); + + while ((basename = g_dir_read_name (dir)) != NULL) { + gchar *old_filename; + gchar *new_filename; + + if (!g_str_has_prefix (basename, "groupwise___")) + continue; + + old_filename = g_build_filename (old_data_dir, basename, NULL); + new_filename = g_build_filename (old_cache_dir, basename, NULL); + + g_hash_table_insert (corrections, old_filename, new_filename); + } + + g_dir_close (dir); + + migrate_process_corrections (corrections); + g_hash_table_destroy (corrections); + +exit: + g_free (old_data_dir); + g_free (old_cache_dir); +} + +static void migrate_to_user_cache_dir (const gchar *old_base_dir) { const gchar *new_cache_dir; @@ -252,6 +303,15 @@ migrate_to_user_cache_dir (const gchar *old_base_dir) * Any name collisions will be left in the source directory. */ + src_directory = g_build_filename (old_cache_dir, "addressbook", NULL); + dst_directory = g_build_filename (new_cache_dir, "addressbook", NULL); + + migrate_move_contents (src_directory, dst_directory); + migrate_rmdir (src_directory); + + g_free (src_directory); + g_free (dst_directory); + src_directory = g_build_filename (old_cache_dir, "calendar", NULL); dst_directory = g_build_filename (new_cache_dir, "calendar", NULL); @@ -316,6 +376,15 @@ migrate_to_user_data_dir (const gchar *old_base_dir) * Any name collisions will be left in the source directory. */ + src_directory = g_build_filename (old_base_dir, "addressbook", "local", NULL); + dst_directory = g_build_filename (new_data_dir, "addressbook", NULL); + + migrate_move_contents (src_directory, dst_directory); + migrate_rmdir (src_directory); + + g_free (src_directory); + g_free (dst_directory); + src_directory = g_build_filename (old_base_dir, "calendar", "local", NULL); dst_directory = g_build_filename (new_data_dir, "calendar", NULL); @@ -359,12 +428,12 @@ migrate_to_user_data_dir (const gchar *old_base_dir) } void -evolution_calendar_factory_migrate_basedir (void) +evolution_source_registry_migrate_basedir (void) { const gchar *home_dir; gchar *old_base_dir; - /* XXX This blocks, but it's all just local file + /* XXX This blocks, but it's all just local directory * renames so it should be nearly instantaneous. */ home_dir = g_get_home_dir (); @@ -374,8 +443,10 @@ evolution_calendar_factory_migrate_basedir (void) if (!g_file_test (old_base_dir, G_FILE_TEST_IS_DIR)) goto exit; + /* Miscellaneous tweaks before we start. */ migrate_fix_exchange_bug (old_base_dir); migrate_fix_memos_cache_bug (old_base_dir); + migrate_fix_groupwise_bug (old_base_dir); migrate_to_user_cache_dir (old_base_dir); migrate_to_user_data_dir (old_base_dir); diff --git a/services/evolution-source-registry/evolution-source-registry-migrate-sources.c b/services/evolution-source-registry/evolution-source-registry-migrate-sources.c new file mode 100644 index 0000000..3b304b7 --- /dev/null +++ b/services/evolution-source-registry/evolution-source-registry-migrate-sources.c @@ -0,0 +1,3493 @@ +/* + * migrate-from-gconf.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 + * + */ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +/* These constants are collected from various e-source-*.h files + * throughout evolution-data-server and known extension packages. */ +#define E_SOURCE_GROUP_NAME "Data Source" +#define E_SOURCE_EXTENSION_CONTACTS_BACKEND "Contacts Backend" +#define E_SOURCE_EXTENSION_LDAP_BACKEND "LDAP Backend" +#define E_SOURCE_EXTENSION_LOCAL_BACKEND "Local Backend" +#define E_SOURCE_EXTENSION_VCF_BACKEND "VCF Backend" +#define E_SOURCE_EXTENSION_WEATHER_BACKEND "Weather Backend" +#define E_SOURCE_EXTENSION_WEBDAV_BACKEND "WebDAV Backend" + +/* These constants are copied from e-source-password.c. */ +#define KEYRING_ITEM_ATTRIBUTE_NAME "e-source-uid" +#define KEYRING_ITEM_DISPLAY_FORMAT "Evolution Data Source %s" + +typedef struct _ParseData ParseData; + +typedef void (*PropertyFunc) (ParseData *parse_data, + const gchar *property_name, + const gchar *property_value); + +typedef enum { + PARSE_TYPE_MAIL, + PARSE_TYPE_ADDRESSBOOK, + PARSE_TYPE_CALENDAR, + PARSE_TYPE_TASKS, + PARSE_TYPE_MEMOS +} ParseType; + +typedef enum { + PARSE_STATE_INITIAL, + + PARSE_STATE_IN_GCONF, /* GConf XML */ + PARSE_STATE_IN_ACCOUNTS_ENTRY, /* GConf XML */ + PARSE_STATE_IN_ACCOUNTS_VALUE, /* GConf XML */ + PARSE_STATE_IN_SIGNATURES_ENTRY, /* GConf XML */ + PARSE_STATE_IN_SIGNATURES_VALUE, /* GConf XML */ + PARSE_STATE_IN_SOURCES_ENTRY, /* GConf XML */ + PARSE_STATE_IN_SOURCES_VALUE, /* GConf XML */ + + PARSE_STATE_IN_ACCOUNT, /* EAccount XML */ + PARSE_STATE_IN_IDENTITY, /* EAccount XML */ + PARSE_STATE_IN_IDENTITY_NAME, /* EAccount XML */ + PARSE_STATE_IN_IDENTITY_ADDR_SPEC, /* EAccount XML */ + PARSE_STATE_IN_IDENTITY_REPLY_TO, /* EAccount XML */ + PARSE_STATE_IN_IDENTITY_ORGANIZATION, /* EAccount XML */ + PARSE_STATE_IN_IDENTITY_SIGNATURE, /* EAccount XML */ + PARSE_STATE_IN_MAIL_SOURCE, /* EAccount XML */ + PARSE_STATE_IN_MAIL_SOURCE_URL, /* EAccount XML */ + PARSE_STATE_IN_MAIL_TRANSPORT, /* EAccount XML */ + PARSE_STATE_IN_MAIL_TRANSPORT_URL, /* EAccount XML */ + PARSE_STATE_IN_AUTO_CC, /* EAccount XML */ + PARSE_STATE_IN_AUTO_CC_RECIPIENTS, /* EAccount XML */ + PARSE_STATE_IN_AUTO_BCC, /* EAccount XML */ + PARSE_STATE_IN_AUTO_BCC_RECIPIENTS, /* EAccount XML */ + PARSE_STATE_IN_DRAFTS_FOLDER, /* EAccount XML */ + PARSE_STATE_IN_SENT_FOLDER, /* EAccount XML */ + PARSE_STATE_IN_RECEIPT_POLICY, /* EAccount XML */ + PARSE_STATE_IN_PGP, /* EAccount XML */ + PARSE_STATE_IN_PGP_KEY_ID, /* EAccount XML */ + PARSE_STATE_IN_SMIME, /* EAccount XML */ + PARSE_STATE_IN_SMIME_SIGN_KEY_ID, /* EAccount XML */ + PARSE_STATE_IN_SMIME_ENCRYPT_KEY_ID, /* EAccount XML */ + + PARSE_STATE_IN_SIGNATURE, /* ESignature XML */ + PARSE_STATE_IN_FILENAME, /* ESignature XML */ + + PARSE_STATE_IN_GROUP, /* ESource XML */ + PARSE_STATE_IN_SOURCE, /* ESource XML */ + PARSE_STATE_IN_PROPERTIES /* ESource XML */ +} ParseState; + +struct _ParseData { + ParseType type; + ParseState state; + + /* Whether to skip writing a file + * for this account information. */ + gboolean skip; + + /* Set by , and tags. */ + GFile *file; + GKeyFile *key_file; + + /* Set by / tags. */ + gboolean auto_bcc; + gboolean auto_cc; + + /* Set by tags. */ + GFile *identity_file; + GKeyFile *identity_key_file; + + /* Set by tags. */ + GFile *transport_file; + GKeyFile *transport_key_file; + + /* Set by tags. */ + GFile *collection_file; + GKeyFile *collection_key_file; + + /* Set by tags. */ + GFile *signature_file; + gboolean is_script; + + /* Set by tags. */ + gchar *base_uri; + + /* Set by tags. */ + gchar *mangled_uri; + SoupURI *soup_uri; + PropertyFunc property_func; +}; + +static GnomeKeyringPasswordSchema schema = { + GNOME_KEYRING_ITEM_GENERIC_SECRET, + { + { KEYRING_ITEM_ATTRIBUTE_NAME, + GNOME_KEYRING_ATTRIBUTE_TYPE_STRING }, + { NULL, 0 } + } +}; + +/* Forward Declarations */ +void evolution_source_registry_migrate_sources (void); + +static ParseData * +parse_data_new (ParseType parse_type) +{ + ParseData *parse_data; + + parse_data = g_slice_new0 (ParseData); + parse_data->type = parse_type; + parse_data->state = PARSE_STATE_INITIAL; + + return parse_data; +} + +static void +parse_data_free (ParseData *parse_data) +{ + /* Normally the allocated data in ParseData is freed and the + * pointers are cleared before we get here. But if an error + * occurred we may leave data behind. This cleans it up. */ + + if (parse_data->file != NULL) + g_object_unref (parse_data->file); + + if (parse_data->key_file != NULL) + g_key_file_free (parse_data->key_file); + + if (parse_data->identity_file != NULL) + g_object_unref (parse_data->identity_file); + + if (parse_data->identity_key_file != NULL) + g_key_file_free (parse_data->identity_key_file); + + if (parse_data->transport_file != NULL) + g_object_unref (parse_data->transport_file); + + if (parse_data->transport_key_file != NULL) + g_key_file_free (parse_data->transport_key_file); + + if (parse_data->collection_file != NULL) + g_object_unref (parse_data->collection_file); + + if (parse_data->collection_key_file != NULL) + g_key_file_free (parse_data->collection_key_file); + + if (parse_data->signature_file != NULL) + g_object_unref (parse_data->signature_file); + + g_free (parse_data->base_uri); + g_free (parse_data->mangled_uri); + + if (parse_data->soup_uri != NULL) + soup_uri_free (parse_data->soup_uri); + + g_slice_free (ParseData, parse_data); +} + +static gboolean +is_true (const gchar *string) +{ + return (g_ascii_strcasecmp (string, "1") == 0) || + (g_ascii_strcasecmp (string, "true") == 0); +} + +static gboolean +is_false (const gchar *string) +{ + return (g_ascii_strcasecmp (string, "0") == 0) || + (g_ascii_strcasecmp (string, "false") == 0); +} + +static gboolean +base_uri_is_groupware (const gchar *base_uri) +{ + /* Well-known scheme names from various groupware packages. */ + + /* We use a limited string comparsion here because the + * base_uri string may be 'scheme://' or just 'scheme'. */ + + g_return_val_if_fail (base_uri != NULL, FALSE); + + if (g_ascii_strncasecmp (base_uri, "ews", 3) == 0) + return TRUE; + + if (g_ascii_strncasecmp (base_uri, "exchange", 8) == 0) + return TRUE; + + if (g_ascii_strncasecmp (base_uri, "groupwise", 9) == 0) + return TRUE; + + if (g_ascii_strncasecmp (base_uri, "kolab", 5) == 0) + return TRUE; + + if (g_ascii_strncasecmp (base_uri, "mapi", 4) == 0) + return TRUE; + + return FALSE; +} + +static void +migrate_keyring_entry (const gchar *uid, + const gchar *user, + const gchar *server, + const gchar *protocol) +{ + GnomeKeyringAttributeList *attributes; + GList *found_list = NULL; + gchar *display_name; + + /* This is a best-effort routine, so we don't really care about + * errors. We leave the old keyring entry in place since it may + * be reused for address book or calendar migration. */ + + display_name = g_strdup_printf (KEYRING_ITEM_DISPLAY_FORMAT, uid); + + attributes = gnome_keyring_attribute_list_new (); + + gnome_keyring_attribute_list_append_string ( + attributes, "application", "Evolution"); + if (user != NULL) + gnome_keyring_attribute_list_append_string ( + attributes, "user", user); + if (server != NULL) + gnome_keyring_attribute_list_append_string ( + attributes, "server", server); + if (protocol != NULL) + gnome_keyring_attribute_list_append_string ( + attributes, "protocol", protocol); + + gnome_keyring_find_items_sync ( + GNOME_KEYRING_ITEM_NETWORK_PASSWORD, attributes, &found_list); + + /* Pick the first match we find. */ + if (found_list != NULL) { + GnomeKeyringFound *found = found_list->data; + + /* Sanity check. */ + g_return_if_fail (found->secret != NULL); + + gnome_keyring_store_password_sync ( + &schema, GNOME_KEYRING_DEFAULT, display_name, + found->secret, KEYRING_ITEM_ATTRIBUTE_NAME, uid, NULL); + } + + gnome_keyring_attribute_list_free (attributes); + gnome_keyring_found_list_free (found_list); + + g_free (display_name); +} + +static gboolean +migrate_parse_commit_changes (ParseType parse_type, + GFile *file, + GKeyFile *key_file, + const gchar *mangled_uri, + GError **error) +{ + const gchar *data_dir; + const gchar *cache_dir; + const gchar *component; + gchar *old_directory; + gchar *new_directory; + gchar *contents; + gchar *uid; + gsize length; + gboolean success; + gboolean old_directory_exists; + gboolean new_directory_exists; + + data_dir = e_get_user_data_dir (); + cache_dir = e_get_user_cache_dir (); + + uid = e_server_side_source_uid_from_file (file, error); + + if (uid == NULL) + return FALSE; + + g_print (" * Source: %s\n", uid); + + g_print (" Writing key file...\n"); + + /* Save the key file contents to disk. */ + contents = g_key_file_to_data (key_file, &length, NULL); + success = g_file_replace_contents ( + file, contents, length, NULL, FALSE, + G_FILE_CREATE_PRIVATE, NULL, NULL, error); + g_free (contents); + + if (!success) + goto exit; + + /* Rename the source's local cache directory from its mangled + * URI to its UID. The key file's basename contains the UID. + * All source types but "local" should have cache directories. */ + + /* Mail cache directories already use UIDs. */ + switch (parse_type) { + case PARSE_TYPE_ADDRESSBOOK: + component = "addressbook"; + break; + case PARSE_TYPE_CALENDAR: + component = "calendar"; + break; + case PARSE_TYPE_TASKS: + component = "tasks"; + break; + case PARSE_TYPE_MEMOS: + component = "memos"; + break; + default: + goto exit; + } + + g_assert (mangled_uri != NULL); + + old_directory = g_build_filename ( + cache_dir, component, mangled_uri, NULL); + + new_directory = g_build_filename ( + cache_dir, component, uid, NULL); + + old_directory_exists = g_file_test (old_directory, G_FILE_TEST_EXISTS); + new_directory_exists = g_file_test (new_directory, G_FILE_TEST_EXISTS); + + g_print ( + " Checking for old cache dir '%s'... %s\n", + old_directory, + old_directory_exists ? "found" : "not found"); + + if (old_directory_exists) { + g_print ( + " Checking for new cache dir '%s'... %s\n", + new_directory, + new_directory_exists ? "found" : "not found"); + + if (new_directory_exists) + g_print (" Skipping cache directory rename.\n"); + else { + g_print (" Renaming old cache directory...\n"); + if (g_rename (old_directory, new_directory) < 0) { + g_set_error ( + error, G_FILE_ERROR, + g_file_error_from_errno (errno), + "%s", g_strerror (errno)); + success = FALSE; + } + } + } + + g_free (old_directory); + g_free (new_directory); + + if (!success) + goto exit; + + /* Rename the source's local data directory from its mangled + * URI to its UID. The key file's basename contains the UID. + * Only "local" sources have local data directores. */ + + old_directory = g_build_filename ( + data_dir, component, mangled_uri, NULL); + + new_directory = g_build_filename ( + data_dir, component, uid, NULL); + + old_directory_exists = g_file_test (old_directory, G_FILE_TEST_EXISTS); + new_directory_exists = g_file_test (new_directory, G_FILE_TEST_EXISTS); + + g_print ( + " Checking for old data dir '%s'... %s\n", + old_directory, + old_directory_exists ? "found" : "not found"); + + if (old_directory_exists) { + g_print ( + " Checking for new data dir '%s'... %s\n", + new_directory, + new_directory_exists ? "found" : "not found"); + + if (new_directory_exists) + g_print (" Skipping data directory rename.\n"); + else { + g_print (" Renaming old data directory...\n"); + if (g_rename (old_directory, new_directory) < 0) { + g_set_error ( + error, G_FILE_ERROR, + g_file_error_from_errno (errno), + "%s", g_strerror (errno)); + success = FALSE; + } + } + } + + g_free (old_directory); + g_free (new_directory); + +exit: + g_free (uid); + + return success; +} + +static void +migrate_setup_collection (ParseData *parse_data, + CamelURL *url) +{ + gchar *collection_uid; + gchar *display_name; + gboolean enabled; + + g_return_if_fail (parse_data->key_file != NULL); + g_return_if_fail (parse_data->identity_key_file != NULL); + g_return_if_fail (parse_data->transport_key_file != NULL); + + parse_data->collection_file = e_server_side_source_new_user_file (NULL); + parse_data->collection_key_file = g_key_file_new (); + + collection_uid = e_server_side_source_uid_from_file ( + parse_data->collection_file, NULL); + + /* Copy the display name from the mail account source. */ + + display_name = g_key_file_get_string ( + parse_data->key_file, + E_SOURCE_GROUP_NAME, "DisplayName", NULL); + + g_key_file_set_string ( + parse_data->collection_key_file, + E_SOURCE_GROUP_NAME, "DisplayName", display_name); + + /* Copy the enabled state from the mail account source. */ + + enabled = g_key_file_get_boolean ( + parse_data->key_file, + E_SOURCE_GROUP_NAME, "Enabled", NULL); + + g_key_file_set_boolean ( + parse_data->collection_key_file, + E_SOURCE_GROUP_NAME, "Enabled", enabled); + + /* Collection sources are always top-level sources. */ + + g_key_file_set_string ( + parse_data->collection_key_file, + E_SOURCE_GROUP_NAME, "Parent", ""); + + /* Collection backend name should match the CamelURL protocol. */ + + g_key_file_set_string ( + parse_data->collection_key_file, + E_SOURCE_EXTENSION_COLLECTION, + "BackendName", url->protocol); + + g_key_file_set_boolean ( + parse_data->collection_key_file, + E_SOURCE_EXTENSION_COLLECTION, + "CalendarEnabled", TRUE); + + g_key_file_set_boolean ( + parse_data->collection_key_file, + E_SOURCE_EXTENSION_COLLECTION, + "ContactsEnabled", TRUE); + + g_key_file_set_boolean ( + parse_data->collection_key_file, + E_SOURCE_EXTENSION_COLLECTION, + "MailEnabled", TRUE); + + /* Enable all mail sources since we set "MailEnabled=true" above. */ + + g_key_file_set_boolean ( + parse_data->key_file, + E_SOURCE_GROUP_NAME, "Enabled", TRUE); + + g_key_file_set_boolean ( + parse_data->identity_key_file, + E_SOURCE_GROUP_NAME, "Enabled", TRUE); + + g_key_file_set_boolean ( + parse_data->transport_key_file, + E_SOURCE_GROUP_NAME, "Enabled", TRUE); + + /* The other mail sources are children of the collection source. */ + + g_key_file_set_string ( + parse_data->key_file, + E_SOURCE_GROUP_NAME, + "Parent", collection_uid); + + g_key_file_set_string ( + parse_data->identity_key_file, + E_SOURCE_GROUP_NAME, + "Parent", collection_uid); + + g_key_file_set_string ( + parse_data->transport_key_file, + E_SOURCE_GROUP_NAME, + "Parent", collection_uid); + + /* The collection identity has to be determined case-by-case. + * Some are based on user name, some are based on email address. */ + + if (g_strcmp0 (url->protocol, "ews") == 0) + g_key_file_set_string ( + parse_data->collection_key_file, + E_SOURCE_EXTENSION_COLLECTION, + "Identity", url->user); + + g_free (collection_uid); + g_free (display_name); +} + +static void +migrate_parse_account (ParseData *parse_data, + const gchar *element_name, + const gchar **attribute_names, + const gchar **attribute_values, + GError **error) +{ + const gchar *uid; + const gchar *name; + gchar *identity_uid; + gchar *transport_uid; + gboolean enabled; + gboolean success; + + success = g_markup_collect_attributes ( + element_name, + attribute_names, + attribute_values, + error, + G_MARKUP_COLLECT_STRING, + "uid", &uid, + G_MARKUP_COLLECT_STRING, + "name", &name, + G_MARKUP_COLLECT_BOOLEAN, + "enabled", &enabled, + G_MARKUP_COLLECT_INVALID); + + if (!success) + return; + + parse_data->file = e_server_side_source_new_user_file (uid); + + /* If the file already exists, skip this source. It may be that we + * already migrated it, in which case we don't want to overwrite it. */ + if (g_file_query_exists (parse_data->file, NULL)) + return; + + parse_data->key_file = g_key_file_new (); + + parse_data->identity_file = e_server_side_source_new_user_file (NULL); + parse_data->identity_key_file = g_key_file_new (); + + parse_data->transport_file = e_server_side_source_new_user_file (NULL); + parse_data->transport_key_file = g_key_file_new (); + + identity_uid = e_server_side_source_uid_from_file ( + parse_data->identity_file, NULL); + + transport_uid = e_server_side_source_uid_from_file ( + parse_data->transport_file, NULL); + + g_key_file_set_string ( + parse_data->key_file, + E_SOURCE_GROUP_NAME, + "DisplayName", name); + + g_key_file_set_boolean ( + parse_data->key_file, + E_SOURCE_GROUP_NAME, + "Enabled", enabled); + + /* Mail account source references the identity source. */ + g_key_file_set_string ( + parse_data->key_file, + E_SOURCE_EXTENSION_MAIL_ACCOUNT, + "IdentityUid", identity_uid); + + /* Mail account source references the transport source. */ + g_key_file_set_string ( + parse_data->identity_key_file, + E_SOURCE_EXTENSION_MAIL_SUBMISSION, + "TransportUid", transport_uid); + + /* Identity source gets the same display name. */ + g_key_file_set_string ( + parse_data->identity_key_file, + E_SOURCE_GROUP_NAME, + "DisplayName", name); + + /* Identity source is a child of the mail account. */ + g_key_file_set_string ( + parse_data->identity_key_file, + E_SOURCE_GROUP_NAME, + "Parent", uid); + + /* Transport source gets the same display name. */ + g_key_file_set_string ( + parse_data->transport_key_file, + E_SOURCE_GROUP_NAME, + "DisplayName", name); + + /* Transport source is a child of the mail account. */ + g_key_file_set_string ( + parse_data->transport_key_file, + E_SOURCE_GROUP_NAME, + "Parent", uid); + + g_free (identity_uid); + g_free (transport_uid); +} + +static void +migrate_parse_pgp (ParseData *parse_data, + const gchar *element_name, + const gchar **attribute_names, + const gchar **attribute_values, + GError **error) +{ + const gchar *hash_algo; + gboolean always_sign; + gboolean always_trust; + gboolean encrypt_to_self; + gboolean no_imip_sign; + gboolean success; + + success = g_markup_collect_attributes ( + element_name, + attribute_names, + attribute_values, + error, + G_MARKUP_COLLECT_BOOLEAN, + "always-sign", &always_sign, + G_MARKUP_COLLECT_BOOLEAN, + "always-trust", &always_trust, + G_MARKUP_COLLECT_BOOLEAN, + "encrypt-to-self", &encrypt_to_self, + G_MARKUP_COLLECT_BOOLEAN, + "no-imip-sign", &no_imip_sign, + G_MARKUP_COLLECT_STRING | + G_MARKUP_COLLECT_OPTIONAL, + "hash-algo", &hash_algo, + G_MARKUP_COLLECT_INVALID); + + if (!success) + return; + + g_key_file_set_boolean ( + parse_data->identity_key_file, + E_SOURCE_EXTENSION_OPENPGP, + "AlwaysSign", always_sign); + + g_key_file_set_boolean ( + parse_data->identity_key_file, + E_SOURCE_EXTENSION_OPENPGP, + "AlwaysTrust", always_trust); + + g_key_file_set_boolean ( + parse_data->identity_key_file, + E_SOURCE_EXTENSION_OPENPGP, + "EncryptToSelf", encrypt_to_self); + + if (hash_algo != NULL && *hash_algo != '\0') + g_key_file_set_string ( + parse_data->identity_key_file, + E_SOURCE_EXTENSION_OPENPGP, + "SigningAlgorithm", hash_algo); + + /* XXX Don't know why this is under the + * element, it applies to S/MIME as well. + * Also note we're inverting the setting. */ + g_key_file_set_boolean ( + parse_data->identity_key_file, + E_SOURCE_EXTENSION_MAIL_COMPOSITION, + "SignImip", !no_imip_sign); +} + +static void +migrate_parse_recipients (ParseData *parse_data, + const gchar *key, + const gchar *recipients) +{ + CamelAddress *address; + CamelInternetAddress *inet_address; + gchar **string_list; + gint ii, length; + gsize index = 0; + + if (recipients == NULL || *recipients == '\0') + return; + + inet_address = camel_internet_address_new (); + address = CAMEL_ADDRESS (inet_address); + + if (camel_address_decode (address, recipients) == -1) + goto exit; + + length = camel_address_length (address); + string_list = g_new0 (gchar *, length + 1); + + for (ii = 0; ii < length; ii++) { + const gchar *name, *addr; + + if (!camel_internet_address_get ( + inet_address, ii, &name, &addr)) + continue; + + string_list[index++] = + camel_internet_address_format_address (name, addr); + } + + g_key_file_set_string_list ( + parse_data->identity_key_file, + E_SOURCE_EXTENSION_MAIL_COMPOSITION, key, + (const gchar *const *) string_list, index); + + g_strfreev (string_list); + +exit: + g_object_unref (inet_address); +} + +static void +migrate_parse_smime (ParseData *parse_data, + const gchar *element_name, + const gchar **attribute_names, + const gchar **attribute_values, + GError **error) +{ + const gchar *hash_algo; + gboolean encrypt_default; + gboolean encrypt_to_self; + gboolean sign_default; + gboolean success; + + success = g_markup_collect_attributes ( + element_name, + attribute_names, + attribute_values, + error, + G_MARKUP_COLLECT_BOOLEAN, + "encrypt-default", &encrypt_default, + G_MARKUP_COLLECT_BOOLEAN, + "encrypt-to-self", &encrypt_to_self, + G_MARKUP_COLLECT_STRING | + G_MARKUP_COLLECT_OPTIONAL, + "hash-algo", &hash_algo, + G_MARKUP_COLLECT_BOOLEAN, + "sign-default", &sign_default, + G_MARKUP_COLLECT_INVALID); + + if (!success) + return; + + g_key_file_set_boolean ( + parse_data->identity_key_file, + E_SOURCE_EXTENSION_SMIME, + "EncryptByDefault", encrypt_default); + + g_key_file_set_boolean ( + parse_data->identity_key_file, + E_SOURCE_EXTENSION_SMIME, + "EncryptToSelf", encrypt_to_self); + + if (hash_algo != NULL && *hash_algo != '\0') + g_key_file_set_string ( + parse_data->identity_key_file, + E_SOURCE_EXTENSION_SMIME, + "SigningAlgorithm", hash_algo); + + g_key_file_set_boolean ( + parse_data->identity_key_file, + E_SOURCE_EXTENSION_SMIME, + "SignByDefault", sign_default); +} + +static void +migrate_parse_mail_source (ParseData *parse_data, + const gchar *element_name, + const gchar **attribute_names, + const gchar **attribute_values, + GError **error) +{ + const gchar *auto_check_timeout; + glong interval_minutes = 0; + gboolean auto_check; + gboolean success; + + /* Disregard "keep-on-server" and "save-passwd" attributes. */ + success = g_markup_collect_attributes ( + element_name, + attribute_names, + attribute_values, + error, + G_MARKUP_COLLECT_BOOLEAN, + "auto-check", &auto_check, + G_MARKUP_COLLECT_STRING, + "auto-check-timeout", &auto_check_timeout, + G_MARKUP_COLLECT_BOOLEAN | + G_MARKUP_COLLECT_OPTIONAL, + "keep-on-server", NULL, + G_MARKUP_COLLECT_BOOLEAN | + G_MARKUP_COLLECT_OPTIONAL, + "save-passwd", NULL, + G_MARKUP_COLLECT_INVALID); + + if (!success) + return; + + if (auto_check_timeout != NULL) + interval_minutes = strtol (auto_check_timeout, NULL, 10); + + g_key_file_set_boolean ( + parse_data->key_file, + E_SOURCE_EXTENSION_REFRESH, + "Enabled", auto_check); + + if (interval_minutes > 0) + g_key_file_set_integer ( + parse_data->key_file, + E_SOURCE_EXTENSION_REFRESH, + "IntervalMinutes", interval_minutes); +} + +static void +migrate_parse_url_rename_params (CamelURL *url) +{ + /* This list includes known URL parameters from built-in providers + * in Camel, as well as from evolution-exchange/groupwise/mapi/ews. + * Add more as needed. */ + static struct { + const gchar *url_parameter; + const gchar *property_name; + } camel_url_conversion[] = { + { "account_uid", "account-uid" }, + { "ad_auth", "gc-auth-method" }, + { "ad_browse", "gc-allow-browse" }, + { "ad_expand_groups", "gc-expand-groups" }, + { "ad_limit", "gc-results-limit" }, + { "ad_server", "gc-server-name" }, + { "all_headers", "fetch-headers" }, + { "basic_headers", "fetch-headers" }, + { "cachedconn" "concurrent-connections" }, + { "check_all", "check-all" }, + { "check_lsub", "check-subscribed" }, + { "command", "shell-command" }, + { "delete_after", "delete-after-days" }, + { "delete_expunged", "delete-expunged" }, + { "disable_extensions", "disable-extensions" }, + { "dotfolders", "use-dot-folders" }, + { "filter", "filter-inbox" }, + { "filter_junk", "filter-junk" }, + { "filter_junk_inbox", "filter-junk-inbox" }, + { "folder_hierarchy_relative", "folder-hierarchy-relative" }, + { "imap_custom_headers", "fetch-headers-extra" }, + { "keep_on_server", "keep-on-server" }, + { "oab_offline", "oab-offline" }, + { "oal_selected", "oal-selected" }, + { "offline_sync", "stay-synchronized" }, + { "override_namespace", "use-namespace" }, + { "owa_path", "owa-path" }, + { "owa_url", "owa-url" }, + { "password_exp_warn_period", "password-exp-warn-period" }, + { "real_junk_path", "real-junk-path" }, + { "real_trash_path", "real-trash-path" }, + { "show_short_notation", "short-folder-names" }, + { "soap_port", "soap-port" }, + { "ssl", "security-method" }, + { "sync_offline", "stay-synchronized" }, + { "use_command", "use-shell-command" }, + { "use_idle", "use-idle" }, + { "use_lsub", "use-subscriptions" }, + { "use_qresync", "use-qresync" }, + { "use_ssl", "security-method" }, + { "xstatus", "use-xstatus-headers" } + }; + + const gchar *param; + const gchar *use_param; + gint ii; + + for (ii = 0; ii < G_N_ELEMENTS (camel_url_conversion); ii++) { + const gchar *key; + gpointer value; + + key = camel_url_conversion[ii].url_parameter; + value = g_datalist_get_data (&url->params, key); + + if (value == NULL) + continue; + + g_datalist_remove_no_notify (&url->params, key); + + key = camel_url_conversion[ii].property_name; + + /* Deal with a few special enum cases where + * the parameter value also needs renamed. */ + + if (strcmp (key, "all_headers") == 0) { + GEnumClass *enum_class; + GEnumValue *enum_value; + + enum_class = g_type_class_ref ( + CAMEL_TYPE_FETCH_HEADERS_TYPE); + enum_value = g_enum_get_value ( + enum_class, CAMEL_FETCH_HEADERS_ALL); + if (enum_value != NULL) { + g_free (value); + value = g_strdup (enum_value->value_nick); + } else + g_warn_if_reached (); + g_type_class_unref (enum_class); + } + + if (strcmp (key, "basic_headers") == 0) { + GEnumClass *enum_class; + GEnumValue *enum_value; + + enum_class = g_type_class_ref ( + CAMEL_TYPE_FETCH_HEADERS_TYPE); + enum_value = g_enum_get_value ( + enum_class, CAMEL_FETCH_HEADERS_BASIC); + if (enum_value != NULL) { + g_free (value); + value = g_strdup (enum_value->value_nick); + } else + g_warn_if_reached (); + g_type_class_unref (enum_class); + } + + if (strcmp (key, "imap_custom_headers") == 0) + g_strdelimit (value, " ", ','); + + if (strcmp (key, "security-method") == 0) { + CamelNetworkSecurityMethod method; + GEnumClass *enum_class; + GEnumValue *enum_value; + + if (strcmp (value, "always") == 0) + method = CAMEL_NETWORK_SECURITY_METHOD_SSL_ON_ALTERNATE_PORT; + else if (strcmp (value, "1") == 0) + method = CAMEL_NETWORK_SECURITY_METHOD_SSL_ON_ALTERNATE_PORT; + else if (strcmp (value, "when-possible") == 0) + method = CAMEL_NETWORK_SECURITY_METHOD_STARTTLS_ON_STANDARD_PORT; + else + method = CAMEL_NETWORK_SECURITY_METHOD_NONE; + + enum_class = g_type_class_ref ( + CAMEL_TYPE_NETWORK_SECURITY_METHOD); + enum_value = g_enum_get_value (enum_class, method); + if (enum_value != NULL) { + g_free (value); + value = g_strdup (enum_value->value_nick); + } else + g_warn_if_reached (); + g_type_class_unref (enum_class); + } + + g_datalist_set_data_full (&url->params, key, value, g_free); + } + + /* A few more adjustments... + * + * These are all CAMEL_PROVIDER_CONF_CHECKSPIN settings. The spin + * button value is bound to "param" and the checkbox state is bound + * to "use-param". The "use-param" settings are new. If "param" + * exists but no "use-param", then set "use-param" to "true". */ + + param = g_datalist_get_data (&url->params, "gc-results-limit"); + use_param = g_datalist_get_data (&url->params, "use-gc-results-limit"); + if (param != NULL && *param != '\0' && use_param == NULL) { + g_datalist_set_data_full ( + &url->params, "use-gc-results-limit", + g_strdup ("true"), (GDestroyNotify) g_free); + } + + param = g_datalist_get_data (&url->params, "kerberos"); + if (g_strcmp0 (param, "required") == 0) { + g_datalist_set_data_full ( + &url->params, "kerberos", + g_strdup ("true"), (GDestroyNotify) g_free); + } + + param = g_datalist_get_data ( + &url->params, "password-exp-warn-period"); + use_param = g_datalist_get_data ( + &url->params, "use-password-exp-warn-period"); + if (param != NULL && *param != '\0' && use_param == NULL) { + g_datalist_set_data_full ( + &url->params, "use-password-exp-warn-period", + g_strdup ("true"), (GDestroyNotify) g_free); + } + + param = g_datalist_get_data (&url->params, "real-junk-path"); + use_param = g_datalist_get_data (&url->params, "use-real-junk-path"); + if (param != NULL && *param != '\0' && use_param == NULL) { + g_datalist_set_data_full ( + &url->params, "use-real-junk-path", + g_strdup ("true"), (GDestroyNotify) g_free); + } + + param = g_datalist_get_data (&url->params, "real-trash-path"); + use_param = g_datalist_get_data (&url->params, "use-real-trash-path"); + if (param != NULL && *param != '\0' && use_param == NULL) { + g_datalist_set_data_full ( + &url->params, "use-real-trash-path", + g_strdup ("true"), (GDestroyNotify) g_free); + } +} + +static void +migrate_parse_url_foreach (GQuark key_id, + const gchar *value, + gpointer user_data) +{ + const gchar *param_name; + const gchar *key; + + struct { + GKeyFile *key_file; + const gchar *group_name; + } *foreach_data = user_data; + + g_return_if_fail (value != NULL); + + param_name = g_quark_to_string (key_id); + key = e_source_parameter_to_key (param_name); + + /* If the value is empty, then the mere + * presence of the parameter implies TRUE. */ + if (*value == '\0') + value = "true"; + + g_key_file_set_string ( + foreach_data->key_file, + foreach_data->group_name, + key, value); +} + +static void +migrate_parse_url (ParseData *parse_data, + GKeyFile *key_file, + GFile *file, + const gchar *group_name, + const gchar *url_string, + GError **error) +{ + CamelURL *url; + GKeyFile *backend_key_file; + GFile *backend_file; + const gchar *value; + gboolean setup_collection; + gchar *uid; + + struct { + GKeyFile *key_file; + const gchar *group_name; + } foreach_data; + + url = camel_url_new (url_string, error); + if (url == NULL || url->protocol == NULL) + return; + + /* Rename URL params as necessary to match + * their ESourceExtension property names. */ + migrate_parse_url_rename_params (url); + + setup_collection = + (key_file == parse_data->key_file) && + base_uri_is_groupware (url->protocol); + + if (setup_collection) + migrate_setup_collection (parse_data, url); + + /* Store backend settings in the collection GKeyFile, if one is + * defined. Otherwise store them in the GKeyFile we were passed. + * Same goes for the keyring entry, which uses the GFile. */ + if (parse_data->collection_key_file != NULL) { + backend_key_file = parse_data->collection_key_file; + backend_file = parse_data->collection_file; + } else { + backend_key_file = key_file; + backend_file = file; + } + + /* This is not a backend setting. */ + g_key_file_set_string ( + key_file, group_name, + "BackendName", url->protocol); + + /* Set authentication details. */ + + if (url->host != NULL) + g_key_file_set_string ( + backend_key_file, + E_SOURCE_EXTENSION_AUTHENTICATION, + "Host", url->host); + + if (url->authmech != NULL) + g_key_file_set_string ( + backend_key_file, + E_SOURCE_EXTENSION_AUTHENTICATION, + "Method", url->authmech); + + if (url->port > 0) + g_key_file_set_integer ( + backend_key_file, + E_SOURCE_EXTENSION_AUTHENTICATION, + "Port", url->port); + + if (url->user != NULL) + g_key_file_set_string ( + backend_key_file, + E_SOURCE_EXTENSION_AUTHENTICATION, + "User", url->user); + + /* Pick out particular URL parameters we know about. */ + + /* If set, this should be "true" or "false", + * but we'll just write it like it's a string. */ + value = g_datalist_get_data (&url->params, "stay-synchronized"); + if (value != NULL) + g_key_file_set_string ( + backend_key_file, + E_SOURCE_EXTENSION_OFFLINE, + "StaySynchronized", value); + g_datalist_set_data (&url->params, "stay-synchronized", NULL); + + value = g_datalist_get_data (&url->params, "security-method"); + if (value != NULL) + g_key_file_set_string ( + backend_key_file, + E_SOURCE_EXTENSION_SECURITY, + "Method", value); + g_datalist_set_data (&url->params, "security-method", NULL); + + /* If we see a "goa-account-id" parameter, skip the entire + * account and let the online-accounts module recreate it. */ + value = g_datalist_get_data (&url->params, "goa-account-id"); + if (value != NULL && *value != '\0') + parse_data->skip = TRUE; + + /* The rest of the URL parameters go in the backend group. */ + + group_name = e_source_camel_get_extension_name (url->protocol); + + foreach_data.key_file = backend_key_file; + foreach_data.group_name = group_name; + + g_datalist_foreach ( + &url->params, (GDataForeachFunc) + migrate_parse_url_foreach, &foreach_data); + + uid = e_server_side_source_uid_from_file (backend_file, error); + + if (uid != NULL) { + migrate_keyring_entry ( + uid, url->user, url->host, url->protocol); + g_free (uid); + } + + camel_url_free (url); +} + +static void +migrate_parse_account_xml_start_element (GMarkupParseContext *context, + const gchar *element_name, + const gchar **attribute_names, + const gchar **attribute_values, + gpointer user_data, + GError **error) +{ + ParseData *parse_data = user_data; + + if (g_strcmp0 (element_name, "account") == 0) { + if (parse_data->state != PARSE_STATE_IN_ACCOUNTS_VALUE) + goto invalid_content; + + parse_data->state = PARSE_STATE_IN_ACCOUNT; + + migrate_parse_account ( + parse_data, + element_name, + attribute_names, + attribute_values, + error); + + return; + } + + if (g_strcmp0 (element_name, "addr-spec") == 0) { + if (parse_data->state != PARSE_STATE_IN_IDENTITY) + goto invalid_content; + + parse_data->state = PARSE_STATE_IN_IDENTITY_ADDR_SPEC; + + return; + } + + if (g_strcmp0 (element_name, "auto-bcc") == 0) { + if (parse_data->state != PARSE_STATE_IN_ACCOUNT) + goto invalid_content; + + parse_data->state = PARSE_STATE_IN_AUTO_BCC; + + g_markup_collect_attributes ( + element_name, + attribute_names, + attribute_values, + error, + G_MARKUP_COLLECT_BOOLEAN, + "always", &parse_data->auto_bcc, + G_MARKUP_COLLECT_INVALID); + + return; + } + + if (g_strcmp0 (element_name, "auto-cc") == 0) { + if (parse_data->state != PARSE_STATE_IN_ACCOUNT) + goto invalid_content; + + parse_data->state = PARSE_STATE_IN_AUTO_CC; + + g_markup_collect_attributes ( + element_name, + attribute_names, + attribute_values, + error, + G_MARKUP_COLLECT_BOOLEAN, + "always", &parse_data->auto_cc, + G_MARKUP_COLLECT_INVALID); + + return; + } + + if (g_strcmp0 (element_name, "drafts-folder") == 0) { + if (parse_data->state != PARSE_STATE_IN_ACCOUNT) + goto invalid_content; + + parse_data->state = PARSE_STATE_IN_DRAFTS_FOLDER; + + return; + } + + if (g_strcmp0 (element_name, "encrypt-key-id") == 0) { + if (parse_data->state != PARSE_STATE_IN_SMIME) + goto invalid_content; + + parse_data->state = PARSE_STATE_IN_SMIME_ENCRYPT_KEY_ID; + + return; + } + + if (g_strcmp0 (element_name, "identity") == 0) { + if (parse_data->state != PARSE_STATE_IN_ACCOUNT) + goto invalid_content; + + parse_data->state = PARSE_STATE_IN_IDENTITY; + + return; + } + + if (g_strcmp0 (element_name, "key-id") == 0) { + if (parse_data->state != PARSE_STATE_IN_PGP) + goto invalid_content; + + parse_data->state = PARSE_STATE_IN_PGP_KEY_ID; + + return; + } + + if (g_strcmp0 (element_name, "name") == 0) { + if (parse_data->state != PARSE_STATE_IN_IDENTITY) + goto invalid_content; + + parse_data->state = PARSE_STATE_IN_IDENTITY_NAME; + + return; + } + + if (g_strcmp0 (element_name, "reply-to") == 0) { + if (parse_data->state != PARSE_STATE_IN_IDENTITY) + goto invalid_content; + + parse_data->state = PARSE_STATE_IN_IDENTITY_REPLY_TO; + + return; + } + + if (g_strcmp0 (element_name, "organization") == 0) { + if (parse_data->state != PARSE_STATE_IN_IDENTITY) + goto invalid_content; + + parse_data->state = PARSE_STATE_IN_IDENTITY_ORGANIZATION; + + return; + } + + if (g_strcmp0 (element_name, "pgp") == 0) { + if (parse_data->state != PARSE_STATE_IN_ACCOUNT) + goto invalid_content; + + parse_data->state = PARSE_STATE_IN_PGP; + + migrate_parse_pgp ( + parse_data, + element_name, + attribute_names, + attribute_values, + error); + + return; + } + + if (g_strcmp0 (element_name, "receipt-policy") == 0) { + const gchar *policy; + gboolean success; + + if (parse_data->state != PARSE_STATE_IN_ACCOUNT) + goto invalid_content; + + parse_data->state = PARSE_STATE_IN_RECEIPT_POLICY; + + success = g_markup_collect_attributes ( + element_name, + attribute_names, + attribute_values, + error, + G_MARKUP_COLLECT_STRING, + "policy", &policy, + G_MARKUP_COLLECT_INVALID); + + /* The new enum strings match the old ones. */ + if (success && policy != NULL) + g_key_file_set_string ( + parse_data->key_file, + E_SOURCE_EXTENSION_MDN, + "ResponsePolicy", policy); + + return; + } + + if (g_strcmp0 (element_name, "recipients") == 0) { + if (parse_data->state == PARSE_STATE_IN_AUTO_BCC) { + parse_data->state = PARSE_STATE_IN_AUTO_BCC_RECIPIENTS; + return; + } + + if (parse_data->state == PARSE_STATE_IN_AUTO_CC) { + parse_data->state = PARSE_STATE_IN_AUTO_CC_RECIPIENTS; + return; + } + + goto invalid_content; + } + + if (g_strcmp0 (element_name, "sent-folder") == 0) { + if (parse_data->state != PARSE_STATE_IN_ACCOUNT) + goto invalid_content; + + parse_data->state = PARSE_STATE_IN_SENT_FOLDER; + + return; + } + + if (g_strcmp0 (element_name, "signature") == 0) { + const gchar *uid; + gboolean success; + + if (parse_data->state != PARSE_STATE_IN_IDENTITY) + goto invalid_content; + + parse_data->state = PARSE_STATE_IN_IDENTITY_SIGNATURE; + + success = g_markup_collect_attributes ( + element_name, + attribute_names, + attribute_values, + error, + G_MARKUP_COLLECT_STRING, + "uid", &uid, + G_MARKUP_COLLECT_INVALID); + + if (success && uid != NULL) + g_key_file_set_string ( + parse_data->identity_key_file, + E_SOURCE_EXTENSION_MAIL_IDENTITY, + "SignatureUid", uid); + + return; + } + + if (g_strcmp0 (element_name, "sign-key-id") == 0) { + if (parse_data->state != PARSE_STATE_IN_SMIME) + goto invalid_content; + + parse_data->state = PARSE_STATE_IN_SMIME_SIGN_KEY_ID; + + return; + } + + if (g_strcmp0 (element_name, "smime") == 0) { + if (parse_data->state != PARSE_STATE_IN_ACCOUNT) + goto invalid_content; + + parse_data->state = PARSE_STATE_IN_SMIME; + + migrate_parse_smime ( + parse_data, + element_name, + attribute_names, + attribute_values, + error); + + return; + } + + if (g_strcmp0 (element_name, "source") == 0) { + if (parse_data->state != PARSE_STATE_IN_ACCOUNT) + goto invalid_content; + + parse_data->state = PARSE_STATE_IN_MAIL_SOURCE; + + migrate_parse_mail_source ( + parse_data, + element_name, + attribute_names, + attribute_values, + error); + + return; + } + + if (g_strcmp0 (element_name, "transport") == 0) { + if (parse_data->state != PARSE_STATE_IN_ACCOUNT) + goto invalid_content; + + parse_data->state = PARSE_STATE_IN_MAIL_TRANSPORT; + + return; + } + + if (g_strcmp0 (element_name, "url") == 0) { + if (parse_data->state == PARSE_STATE_IN_MAIL_SOURCE) { + parse_data->state = PARSE_STATE_IN_MAIL_SOURCE_URL; + return; + } + + if (parse_data->state == PARSE_STATE_IN_MAIL_TRANSPORT) { + parse_data->state = PARSE_STATE_IN_MAIL_TRANSPORT_URL; + return; + } + + goto invalid_content; + } + + g_set_error ( + error, G_MARKUP_ERROR, G_MARKUP_ERROR_UNKNOWN_ELEMENT, + "Unknown element <%s>", element_name); + + return; + +invalid_content: + + g_set_error ( + error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, + "Element <%s> at unexpected location", element_name); +} + +static void +migrate_parse_account_xml_end_element (GMarkupParseContext *context, + const gchar *element_name, + gpointer user_data, + GError **error) +{ + ParseData *parse_data = user_data; + + if (g_strcmp0 (element_name, "account") == 0) { + if (parse_data->state == PARSE_STATE_IN_ACCOUNT) { + parse_data->state = PARSE_STATE_IN_ACCOUNTS_VALUE; + + /* Clean up tag data. */ + + /* The key file will be NULL if we decided to skip it. + * e.g. A file with the same UID may already exist. */ + if (parse_data->key_file != NULL) { + GError *local_error = NULL; + + if (!parse_data->skip) + migrate_parse_commit_changes ( + parse_data->type, + parse_data->file, + parse_data->key_file, + NULL, &local_error); + + if (local_error != NULL) { + g_printerr ( + " FAILED: %s\n", + local_error->message); + g_error_free (local_error); + } + + g_key_file_free (parse_data->key_file); + parse_data->key_file = NULL; + } + + /* Same deal for the identity key file. */ + if (parse_data->identity_key_file != NULL) { + GError *local_error = NULL; + + if (!parse_data->skip) + migrate_parse_commit_changes ( + parse_data->type, + parse_data->identity_file, + parse_data->identity_key_file, + NULL, &local_error); + + if (local_error != NULL) { + g_printerr ( + " FAILED: %s\n", + local_error->message); + g_error_free (local_error); + } + + g_key_file_free (parse_data->identity_key_file); + parse_data->identity_key_file = NULL; + } + + /* Same deal for the transport key file. */ + if (parse_data->transport_key_file != NULL) { + GError *local_error = NULL; + + if (!parse_data->skip) + migrate_parse_commit_changes ( + parse_data->type, + parse_data->transport_file, + parse_data->transport_key_file, + NULL, &local_error); + + if (local_error != NULL) { + g_printerr ( + " FAILED: %s\n", + local_error->message); + g_error_free (local_error); + } + + g_key_file_free (parse_data->transport_key_file); + parse_data->transport_key_file = NULL; + } + + /* The collection key file is optional anyway. */ + if (parse_data->collection_key_file != NULL) { + GError *local_error = NULL; + + if (!parse_data->skip) + migrate_parse_commit_changes ( + parse_data->type, + parse_data->collection_file, + parse_data->collection_key_file, + NULL, &local_error); + + if (local_error != NULL) { + g_printerr ( + " FAILED: %s\n", + local_error->message); + g_error_free (local_error); + } + + g_key_file_free (parse_data->collection_key_file); + parse_data->collection_key_file = NULL; + } + + if (parse_data->file != NULL) { + g_object_unref (parse_data->file); + parse_data->file = NULL; + } + + if (parse_data->identity_file != NULL) { + g_object_unref (parse_data->identity_file); + parse_data->identity_file = NULL; + } + + if (parse_data->transport_file != NULL) { + g_object_unref (parse_data->transport_file); + parse_data->transport_file = NULL; + } + + if (parse_data->collection_file != NULL) { + g_object_unref (parse_data->collection_file); + parse_data->collection_file = NULL; + } + + parse_data->skip = FALSE; + } + return; + } + + if (g_strcmp0 (element_name, "addr-spec") == 0) { + if (parse_data->state == PARSE_STATE_IN_IDENTITY_ADDR_SPEC) + parse_data->state = PARSE_STATE_IN_IDENTITY; + return; + } + + if (g_strcmp0 (element_name, "auto-bcc") == 0) { + if (parse_data->state == PARSE_STATE_IN_AUTO_BCC) + parse_data->state = PARSE_STATE_IN_ACCOUNT; + return; + } + + if (g_strcmp0 (element_name, "auto-cc") == 0) { + if (parse_data->state == PARSE_STATE_IN_AUTO_CC) + parse_data->state = PARSE_STATE_IN_ACCOUNT; + return; + } + + if (g_strcmp0 (element_name, "drafts-folder") == 0) { + if (parse_data->state == PARSE_STATE_IN_DRAFTS_FOLDER) + parse_data->state = PARSE_STATE_IN_ACCOUNT; + return; + } + + if (g_strcmp0 (element_name, "encrypt-key-id") == 0) { + if (parse_data->state == PARSE_STATE_IN_SMIME_ENCRYPT_KEY_ID) + parse_data->state = PARSE_STATE_IN_SMIME; + return; + } + + if (g_strcmp0 (element_name, "identity") == 0) { + if (parse_data->state == PARSE_STATE_IN_IDENTITY) + parse_data->state = PARSE_STATE_IN_ACCOUNT; + return; + } + + if (g_strcmp0 (element_name, "key-id") == 0) { + if (parse_data->state == PARSE_STATE_IN_PGP_KEY_ID) + parse_data->state = PARSE_STATE_IN_PGP; + return; + } + + if (g_strcmp0 (element_name, "name") == 0) { + if (parse_data->state == PARSE_STATE_IN_IDENTITY_NAME) + parse_data->state = PARSE_STATE_IN_IDENTITY; + return; + } + + if (g_strcmp0 (element_name, "organization") == 0) { + if (parse_data->state == PARSE_STATE_IN_IDENTITY_ORGANIZATION) + parse_data->state = PARSE_STATE_IN_IDENTITY; + return; + } + + if (g_strcmp0 (element_name, "pgp") == 0) { + if (parse_data->state == PARSE_STATE_IN_PGP) + parse_data->state = PARSE_STATE_IN_ACCOUNT; + return; + } + + if (g_strcmp0 (element_name, "receipt-policy") == 0) { + if (parse_data->state == PARSE_STATE_IN_RECEIPT_POLICY) + parse_data->state = PARSE_STATE_IN_ACCOUNT; + return; + } + + if (g_strcmp0 (element_name, "recipients") == 0) { + if (parse_data->state == PARSE_STATE_IN_AUTO_BCC_RECIPIENTS) + parse_data->state = PARSE_STATE_IN_AUTO_BCC; + if (parse_data->state == PARSE_STATE_IN_AUTO_CC_RECIPIENTS) + parse_data->state = PARSE_STATE_IN_AUTO_CC; + return; + } + + if (g_strcmp0 (element_name, "reply-to") == 0) { + if (parse_data->state == PARSE_STATE_IN_IDENTITY_REPLY_TO) + parse_data->state = PARSE_STATE_IN_IDENTITY; + return; + } + + if (g_strcmp0 (element_name, "sent-folder") == 0) { + if (parse_data->state == PARSE_STATE_IN_SENT_FOLDER) + parse_data->state = PARSE_STATE_IN_ACCOUNT; + return; + } + + if (g_strcmp0 (element_name, "signature") == 0) { + if (parse_data->state == PARSE_STATE_IN_IDENTITY_SIGNATURE) + parse_data->state = PARSE_STATE_IN_IDENTITY; + return; + } + + if (g_strcmp0 (element_name, "sign-key-id") == 0) { + if (parse_data->state == PARSE_STATE_IN_SMIME_SIGN_KEY_ID) + parse_data->state = PARSE_STATE_IN_SMIME; + return; + } + + if (g_strcmp0 (element_name, "smime") == 0) { + if (parse_data->state == PARSE_STATE_IN_SMIME) + parse_data->state = PARSE_STATE_IN_ACCOUNT; + return; + } + + if (g_strcmp0 (element_name, "source") == 0) { + if (parse_data->state == PARSE_STATE_IN_MAIL_SOURCE) + parse_data->state = PARSE_STATE_IN_ACCOUNT; + return; + } + + if (g_strcmp0 (element_name, "transport") == 0) { + if (parse_data->state == PARSE_STATE_IN_MAIL_TRANSPORT) + parse_data->state = PARSE_STATE_IN_ACCOUNT; + return; + } + + if (g_strcmp0 (element_name, "url") == 0) { + if (parse_data->state == PARSE_STATE_IN_MAIL_SOURCE_URL) + parse_data->state = PARSE_STATE_IN_MAIL_SOURCE; + if (parse_data->state == PARSE_STATE_IN_MAIL_TRANSPORT_URL) + parse_data->state = PARSE_STATE_IN_MAIL_TRANSPORT; + return; + } +} + +static void +migrate_parse_account_xml_text (GMarkupParseContext *context, + const gchar *text, + gsize text_len, + gpointer user_data, + GError **error) +{ + ParseData *parse_data = user_data; + + switch (parse_data->state) { + case PARSE_STATE_IN_AUTO_BCC_RECIPIENTS: + /* Disregard the recipient list if + * we're not going to auto-BCC them. */ + if (parse_data->auto_bcc) + migrate_parse_recipients ( + parse_data, "Bcc", text); + break; + + case PARSE_STATE_IN_AUTO_CC_RECIPIENTS: + /* Disregard the recipient list if + * we're not going to auto-CC them. */ + if (parse_data->auto_cc) + migrate_parse_recipients ( + parse_data, "Cc", text); + break; + + case PARSE_STATE_IN_DRAFTS_FOLDER: + g_key_file_set_string ( + parse_data->identity_key_file, + E_SOURCE_EXTENSION_MAIL_COMPOSITION, + "DraftsFolder", text); + break; + + case PARSE_STATE_IN_SMIME_ENCRYPT_KEY_ID: + g_key_file_set_string ( + parse_data->identity_key_file, + E_SOURCE_EXTENSION_SMIME, + "EncryptionCertificate", text); + break; + + case PARSE_STATE_IN_IDENTITY_ADDR_SPEC: + g_key_file_set_string ( + parse_data->identity_key_file, + E_SOURCE_EXTENSION_MAIL_IDENTITY, + "Address", text); + break; + + case PARSE_STATE_IN_IDENTITY_NAME: + g_key_file_set_string ( + parse_data->identity_key_file, + E_SOURCE_EXTENSION_MAIL_IDENTITY, + "Name", text); + break; + + case PARSE_STATE_IN_IDENTITY_ORGANIZATION: + g_key_file_set_string ( + parse_data->identity_key_file, + E_SOURCE_EXTENSION_MAIL_IDENTITY, + "Organization", text); + break; + + case PARSE_STATE_IN_IDENTITY_REPLY_TO: + g_key_file_set_string ( + parse_data->identity_key_file, + E_SOURCE_EXTENSION_MAIL_IDENTITY, + "ReplyTo", text); + break; + + case PARSE_STATE_IN_MAIL_SOURCE_URL: + migrate_parse_url ( + parse_data, + parse_data->key_file, + parse_data->file, + E_SOURCE_EXTENSION_MAIL_ACCOUNT, + text, error); + break; + + case PARSE_STATE_IN_MAIL_TRANSPORT_URL: + migrate_parse_url ( + parse_data, + parse_data->transport_key_file, + parse_data->transport_file, + E_SOURCE_EXTENSION_MAIL_TRANSPORT, + text, error); + break; + + case PARSE_STATE_IN_PGP_KEY_ID: + g_key_file_set_string ( + parse_data->identity_key_file, + E_SOURCE_EXTENSION_OPENPGP, + "KeyId", text); + break; + + case PARSE_STATE_IN_SENT_FOLDER: + g_key_file_set_string ( + parse_data->identity_key_file, + E_SOURCE_EXTENSION_MAIL_SUBMISSION, + "SentFolder", text); + break; + + case PARSE_STATE_IN_SMIME_SIGN_KEY_ID: + g_key_file_set_string ( + parse_data->identity_key_file, + E_SOURCE_EXTENSION_SMIME, + "SigningCertificate", text); + break; + + default: + break; + } +} + +static GMarkupParser account_xml_parser = { + migrate_parse_account_xml_start_element, + migrate_parse_account_xml_end_element, + migrate_parse_account_xml_text, + NULL, /* passthrough */ + NULL /* error */ +}; + +static void +migrate_parse_signature (ParseData *parse_data, + const gchar *element_name, + const gchar **attribute_names, + const gchar **attribute_values, + GError **error) +{ + const gchar *uid; + const gchar *name; + const gchar *format; + const gchar *config_dir; + gchar *directory; + gchar *absolute_path; + gboolean autogenerated; + gboolean success; + + success = g_markup_collect_attributes ( + element_name, + attribute_names, + attribute_values, + error, + G_MARKUP_COLLECT_STRING, + "uid", &uid, + G_MARKUP_COLLECT_STRING, + "name", &name, + G_MARKUP_COLLECT_BOOLEAN, + "auto", &autogenerated, + G_MARKUP_COLLECT_STRING | + G_MARKUP_COLLECT_OPTIONAL, + "format", &format, + G_MARKUP_COLLECT_INVALID); + + if (!success) + return; + + /* Skip the "autogenerated" signature. */ + if (autogenerated) + return; + + parse_data->file = e_server_side_source_new_user_file (uid); + + config_dir = e_get_user_config_dir (); + directory = g_build_filename (config_dir, "signatures", NULL); + absolute_path = g_build_filename (directory, uid, NULL); + parse_data->signature_file = g_file_new_for_path (absolute_path); + g_mkdir_with_parents (directory, 0700); + g_free (absolute_path); + g_free (directory); + + /* If the file already exists, skip this source. It may be that we + * already migrated it, in which case we don't want to overwrite it. */ + if (g_file_query_exists (parse_data->file, NULL)) + return; + + parse_data->key_file = g_key_file_new (); + + g_key_file_set_string ( + parse_data->key_file, + E_SOURCE_GROUP_NAME, + "DisplayName", name); + + g_key_file_set_string ( + parse_data->key_file, + E_SOURCE_EXTENSION_MAIL_SIGNATURE, + "MimeType", format); +} + +static void +migrate_parse_signature_xml_start_element (GMarkupParseContext *context, + const gchar *element_name, + const gchar **attribute_names, + const gchar **attribute_values, + gpointer user_data, + GError **error) +{ + ParseData *parse_data = user_data; + + if (g_strcmp0 (element_name, "filename") == 0) { + if (parse_data->state != PARSE_STATE_IN_SIGNATURE) + goto invalid_content; + + parse_data->state = PARSE_STATE_IN_FILENAME; + + g_markup_collect_attributes ( + element_name, + attribute_names, + attribute_values, + error, + G_MARKUP_COLLECT_BOOLEAN | + G_MARKUP_COLLECT_OPTIONAL, + "script", &parse_data->is_script, + G_MARKUP_COLLECT_INVALID); + + return; + } + + if (g_strcmp0 (element_name, "signature") == 0) { + if (parse_data->state != PARSE_STATE_IN_SIGNATURES_VALUE) + goto invalid_content; + + parse_data->state = PARSE_STATE_IN_SIGNATURE; + + migrate_parse_signature ( + parse_data, + element_name, + attribute_names, + attribute_values, + error); + + return; + } + + g_set_error ( + error, G_MARKUP_ERROR, G_MARKUP_ERROR_UNKNOWN_ELEMENT, + "Unknown element <%s>", element_name); + + return; + +invalid_content: + + g_set_error ( + error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, + "Element <%s> at unexpected location", element_name); +} + +static void +migrate_parse_signature_xml_end_element (GMarkupParseContext *context, + const gchar *element_name, + gpointer user_data, + GError **error) +{ + ParseData *parse_data = user_data; + + if (g_strcmp0 (element_name, "filename") == 0) { + if (parse_data->state == PARSE_STATE_IN_FILENAME) { + parse_data->state = PARSE_STATE_IN_SIGNATURE; + + return; + } + } + + if (g_strcmp0 (element_name, "signature") == 0) { + if (parse_data->state == PARSE_STATE_IN_SIGNATURE) { + parse_data->state = PARSE_STATE_IN_SIGNATURES_VALUE; + + /* Clean up tag data. */ + + /* The key file will be NULL if we decided to skip it. + * e.g. A file with the same UID may already exist. */ + if (parse_data->key_file != NULL) { + GError *local_error = NULL; + + if (!parse_data->skip) + migrate_parse_commit_changes ( + parse_data->type, + parse_data->file, + parse_data->key_file, + NULL, &local_error); + + if (local_error != NULL) { + g_printerr ( + " FAILED: %s\n", + local_error->message); + g_error_free (local_error); + } + + g_key_file_free (parse_data->key_file); + parse_data->key_file = NULL; + } + + if (parse_data->file != NULL) { + g_object_unref (parse_data->file); + parse_data->file = NULL; + } + + parse_data->skip = FALSE; + + return; + } + } +} + +static void +migrate_parse_signature_xml_text (GMarkupParseContext *context, + const gchar *text, + gsize text_len, + gpointer user_data, + GError **error) +{ + ParseData *parse_data = user_data; + + if (parse_data->state == PARSE_STATE_IN_FILENAME) { + GFile *old_signature_file; + GFile *new_signature_file; + const gchar *data_dir; + gchar *absolute_path; + + /* Note we're moving the signature files + * from $XDG_DATA_HOME to $XDG_CONFIG_HOME. */ + data_dir = e_get_user_data_dir (); + + /* Text should be either an absolute file name + * or a base file name with no path components. */ + if (g_path_is_absolute (text)) + absolute_path = g_strdup (text); + else + absolute_path = g_build_filename ( + data_dir, "signatures", text, NULL); + + old_signature_file = g_file_new_for_path (absolute_path); + new_signature_file = parse_data->signature_file; + parse_data->signature_file = NULL; + + /* If the signature is a script, we symlink to it. + * Otherwise we move and rename the regular file. */ + if (parse_data->is_script) + g_file_make_symbolic_link ( + new_signature_file, + absolute_path, NULL, error); + else + g_file_move ( + old_signature_file, + new_signature_file, + G_FILE_COPY_NONE, + NULL, NULL, NULL, error); + + g_object_unref (old_signature_file); + g_object_unref (new_signature_file); + g_free (absolute_path); + } +} + +static GMarkupParser signature_xml_parser = { + migrate_parse_signature_xml_start_element, + migrate_parse_signature_xml_end_element, + migrate_parse_signature_xml_text, + NULL, /* passthrough */ + NULL /* error */ +}; + +static void +migrate_parse_local_calendar_property (ParseData *parse_data, + const gchar *property_name, + const gchar *property_value) +{ + if (g_strcmp0 (property_name, "custom-file") == 0) { + gchar *uri; + + /* Property value is a local filename. Convert it to a + * "file://" URI. + * + * Note: The key is named "CustomFile" instead of, say, + * "CustomURI" because the corresponding ESourceExtension + * property is a GFile. The fact that ESource saves GFile + * properties as URI strings is an implementation detail. */ + uri = g_filename_to_uri (property_value, NULL, NULL); + if (uri != NULL) { + g_key_file_set_string ( + parse_data->key_file, + E_SOURCE_EXTENSION_LOCAL_BACKEND, + "CustomFile", uri); + g_free (uri); + } + } +} + +static void +migrate_parse_local_source (ParseData *parse_data) +{ + if (parse_data->type != PARSE_TYPE_ADDRESSBOOK) + parse_data->property_func = + migrate_parse_local_calendar_property; + + /* Local ADDRESS BOOK Backend has no special properties to parse. */ +} + +static void +migrate_parse_caldav_property (ParseData *parse_data, + const gchar *property_name, + const gchar *property_value) +{ + if (g_strcmp0 (property_name, "autoschedule") == 0) { + g_key_file_set_boolean ( + parse_data->key_file, + E_SOURCE_EXTENSION_WEBDAV_BACKEND, + "CalendarAutoSchedule", + is_true (property_value)); + + } else if (g_strcmp0 (property_name, "usermail") == 0) { + g_key_file_set_string ( + parse_data->key_file, + E_SOURCE_EXTENSION_WEBDAV_BACKEND, + "EmailAddress", property_value); + } +} + +static void +migrate_parse_caldav_source (ParseData *parse_data) +{ + if (parse_data->soup_uri->host != NULL) + g_key_file_set_string ( + parse_data->key_file, + E_SOURCE_EXTENSION_AUTHENTICATION, + "Host", parse_data->soup_uri->host); + + /* We may override this later if we see an "ssl" property. */ + if (parse_data->soup_uri->port == 0) + parse_data->soup_uri->port = 80; + + g_key_file_set_integer ( + parse_data->key_file, + E_SOURCE_EXTENSION_AUTHENTICATION, + "Port", parse_data->soup_uri->port); + + if (parse_data->soup_uri->user != NULL) + g_key_file_set_string ( + parse_data->key_file, + E_SOURCE_EXTENSION_AUTHENTICATION, + "User", parse_data->soup_uri->user); + + if (parse_data->soup_uri->path != NULL) + g_key_file_set_string ( + parse_data->key_file, + E_SOURCE_EXTENSION_WEBDAV_BACKEND, + "ResourcePath", parse_data->soup_uri->path); + + parse_data->property_func = migrate_parse_caldav_property; +} + +static void +migrate_parse_google_calendar_property (ParseData *parse_data, + const gchar *property_name, + const gchar *property_value) +{ + if (g_strcmp0 (property_name, "username") == 0) { + gchar *path; + + g_key_file_set_string ( + parse_data->key_file, + E_SOURCE_EXTENSION_AUTHENTICATION, + "User", property_value); + + path = g_strdup_printf ( + "/calendar/dav/%s/events", property_value); + + g_key_file_set_string ( + parse_data->key_file, + E_SOURCE_EXTENSION_WEBDAV_BACKEND, + "ResourcePath", path); + + g_free (path); + } +} + +static void +migrate_parse_google_contacts_property (ParseData *parse_data, + const gchar *property_name, + const gchar *property_value) +{ + if (g_strcmp0 (property_name, "refresh-interval") == 0) { + guint64 interval_seconds; + + interval_seconds = + g_ascii_strtoull (property_value, NULL, 10); + + if (interval_seconds >= 60) { + g_key_file_set_boolean ( + parse_data->key_file, + E_SOURCE_EXTENSION_REFRESH, + "Enabled", TRUE); + g_key_file_set_uint64 ( + parse_data->key_file, + E_SOURCE_EXTENSION_REFRESH, + "IntervalMinutes", + interval_seconds / 60); + } + + } else if (g_strcmp0 (property_name, "username") == 0) { + g_key_file_set_string ( + parse_data->key_file, + E_SOURCE_EXTENSION_AUTHENTICATION, + "User", property_value); + } +} + +static void +migrate_parse_google_source (ParseData *parse_data) +{ + if (parse_data->type == PARSE_TYPE_ADDRESSBOOK) + parse_data->property_func = + migrate_parse_google_contacts_property; + + else { + g_key_file_set_string ( + parse_data->key_file, + E_SOURCE_EXTENSION_AUTHENTICATION, + "Host", "www.google.com"); + + g_key_file_set_integer ( + parse_data->key_file, + E_SOURCE_EXTENSION_AUTHENTICATION, + "Port", 443); + + g_key_file_set_string ( + parse_data->key_file, + E_SOURCE_EXTENSION_SECURITY, + "Method", "tls"); + + parse_data->property_func = + migrate_parse_google_calendar_property; + } +} + +static void +migrate_parse_ldap_property (ParseData *parse_data, + const gchar *property_name, + const gchar *property_value) +{ + if (g_strcmp0 (property_name, "can-browse") == 0) { + g_key_file_set_boolean ( + parse_data->key_file, + E_SOURCE_EXTENSION_LDAP_BACKEND, + "CanBrowse", + is_true (property_value)); + + /* This is an integer value, but we can use the string as is. */ + } else if (g_strcmp0 (property_name, "limit") == 0) { + g_key_file_set_string ( + parse_data->key_file, + E_SOURCE_EXTENSION_LDAP_BACKEND, + "Limit", property_value); + } +} + +static void +migrate_parse_ldap_source (ParseData *parse_data) +{ + if (parse_data->soup_uri->host != NULL) + g_key_file_set_string ( + parse_data->key_file, + E_SOURCE_EXTENSION_AUTHENTICATION, + "Host", parse_data->soup_uri->host); + + if (parse_data->soup_uri->port != 0) + g_key_file_set_integer ( + parse_data->key_file, + E_SOURCE_EXTENSION_AUTHENTICATION, + "Port", parse_data->soup_uri->port); + + if (parse_data->soup_uri->user != NULL) + g_key_file_set_string ( + parse_data->key_file, + E_SOURCE_EXTENSION_AUTHENTICATION, + "User", parse_data->soup_uri->user); + + /* Skip the leading slash on the URI path to get the RootDn. */ + if (parse_data->soup_uri->path != NULL) + g_key_file_set_string ( + parse_data->key_file, + E_SOURCE_EXTENSION_LDAP_BACKEND, + "RootDn", parse_data->soup_uri->path + 1); + + if (g_strcmp0 (parse_data->soup_uri->query, "?sub?") == 0) + g_key_file_set_string ( + parse_data->key_file, + E_SOURCE_EXTENSION_LDAP_BACKEND, + "Scope", "subtree"); + + if (g_strcmp0 (parse_data->soup_uri->query, "?one?") == 0) + g_key_file_set_string ( + parse_data->key_file, + E_SOURCE_EXTENSION_LDAP_BACKEND, + "Scope", "onelevel"); + + parse_data->property_func = migrate_parse_ldap_property; +} + +static void +migrate_parse_vcf_source (ParseData *parse_data) +{ + if (parse_data->soup_uri->path != NULL) + g_key_file_set_string ( + parse_data->key_file, + E_SOURCE_EXTENSION_VCF_BACKEND, + "Path", parse_data->soup_uri->path); + + /* VCF Backend has no special properties to parse. */ +} + +static void +migrate_parse_weather_property (ParseData *parse_data, + const gchar *property_name, + const gchar *property_value) +{ + /* XXX Temperature property was replaced by units... I think. */ + if (g_strcmp0 (property_name, "temperature") == 0) { + gboolean metric; + + metric = (g_strcmp0 (property_value, "fahrenheit") != 0); + + g_key_file_set_string ( + parse_data->key_file, + E_SOURCE_EXTENSION_WEATHER_BACKEND, + "Units", metric ? "metric" : "imperial"); + + } else if (g_strcmp0 (property_name, "units") == 0) { + gboolean metric; + + metric = (g_strcmp0 (property_value, "metric") == 0); + + g_key_file_set_string ( + parse_data->key_file, + E_SOURCE_EXTENSION_WEATHER_BACKEND, + "Units", metric ? "metric" : "imperial"); + } +} + +static void +migrate_parse_weather_source (ParseData *parse_data) +{ + /* Oh man, we actually try to shove a weather location into + * a URI! The station code winds up as the host component, + * and the location name winds up as the path component. */ + if (parse_data->soup_uri->host != NULL) { + gchar *location; + + if (parse_data->soup_uri->path != NULL) + location = g_strconcat ( + parse_data->soup_uri->host, + parse_data->soup_uri->path, NULL); + else + location = g_strdup (parse_data->soup_uri->host); + + g_key_file_set_string ( + parse_data->key_file, + E_SOURCE_EXTENSION_WEATHER_BACKEND, + "Location", location); + + g_free (location); + } + + + parse_data->property_func = migrate_parse_weather_property; +} + +static void +migrate_parse_webcal_source (ParseData *parse_data) +{ + if (parse_data->soup_uri->host != NULL) + g_key_file_set_string ( + parse_data->key_file, + E_SOURCE_EXTENSION_AUTHENTICATION, + "Host", parse_data->soup_uri->host); + + /* We may override this later if we see an "ssl" property. */ + if (parse_data->soup_uri->port == 0) + parse_data->soup_uri->port = 80; + + g_key_file_set_integer ( + parse_data->key_file, + E_SOURCE_EXTENSION_AUTHENTICATION, + "Port", parse_data->soup_uri->port); + + if (parse_data->soup_uri->user != NULL) + g_key_file_set_string ( + parse_data->key_file, + E_SOURCE_EXTENSION_AUTHENTICATION, + "User", parse_data->soup_uri->user); + + if (parse_data->soup_uri->path != NULL) + g_key_file_set_string ( + parse_data->key_file, + E_SOURCE_EXTENSION_WEBDAV_BACKEND, + "ResourcePath", parse_data->soup_uri->path); + + /* Webcal Backend has no special properties to parse. */ +} + +static void +migrate_parse_webdav_property (ParseData *parse_data, + const gchar *property_name, + const gchar *property_value) +{ + if (g_strcmp0 (property_name, "avoid_ifmatch") == 0) { + g_key_file_set_boolean ( + parse_data->key_file, + E_SOURCE_EXTENSION_WEBDAV_BACKEND, + "AvoidIfmatch", + is_true (property_value)); + } +} + +static void +migrate_parse_webdav_source (ParseData *parse_data) +{ + if (parse_data->soup_uri->host != NULL) + g_key_file_set_string ( + parse_data->key_file, + E_SOURCE_EXTENSION_AUTHENTICATION, + "Host", parse_data->soup_uri->host); + + if (parse_data->soup_uri->port != 0) + g_key_file_set_integer ( + parse_data->key_file, + E_SOURCE_EXTENSION_AUTHENTICATION, + "Port", parse_data->soup_uri->port); + + if (parse_data->soup_uri->user != NULL) + g_key_file_set_string ( + parse_data->key_file, + E_SOURCE_EXTENSION_AUTHENTICATION, + "User", parse_data->soup_uri->user); + + if (parse_data->soup_uri->path != NULL) + g_key_file_set_string ( + parse_data->key_file, + E_SOURCE_EXTENSION_WEBDAV_BACKEND, + "ResourcePath", parse_data->soup_uri->path); + + parse_data->property_func = migrate_parse_webdav_property; +} + +static void +migrate_parse_group (ParseData *parse_data, + const gchar *element_name, + const gchar **attribute_names, + const gchar **attribute_values, + GError **error) +{ + const gchar *base_uri; + gboolean success; + + success = g_markup_collect_attributes ( + element_name, + attribute_names, + attribute_values, + error, + G_MARKUP_COLLECT_STRING, + "uid", NULL, + G_MARKUP_COLLECT_STRING, + "name", NULL, + G_MARKUP_COLLECT_STRING, + "base_uri", &base_uri, + G_MARKUP_COLLECT_BOOLEAN | + G_MARKUP_COLLECT_OPTIONAL, + "readonly", NULL, + G_MARKUP_COLLECT_INVALID); + + if (!success) + return; + + /* Convert "file://" schemes to "local:". */ + if (g_strcmp0 (base_uri, "file://") == 0) + base_uri = "local:"; + + parse_data->base_uri = g_strdup (base_uri); +} + +static void +migrate_parse_source (ParseData *parse_data, + const gchar *element_name, + const gchar **attribute_names, + const gchar **attribute_values, + GError **error) +{ + const gchar *uid; + const gchar *name; + const gchar *color_spec; + const gchar *group_name; + const gchar *absolute_uri; + const gchar *relative_uri; + gchar *backend_name; + gchar *parent_name; + gchar *uri_string; + gchar *cp; + gboolean success; + gboolean is_google_calendar; + + success = g_markup_collect_attributes ( + element_name, + attribute_names, + attribute_values, + error, + G_MARKUP_COLLECT_STRING, + "uid", &uid, + G_MARKUP_COLLECT_STRING, + "name", &name, + G_MARKUP_COLLECT_STRING | + G_MARKUP_COLLECT_OPTIONAL, + "color_spec", &color_spec, + G_MARKUP_COLLECT_STRING, + "relative_uri", &relative_uri, + G_MARKUP_COLLECT_STRING | + G_MARKUP_COLLECT_OPTIONAL, + "uri", &absolute_uri, + G_MARKUP_COLLECT_INVALID); + + if (!success) + return; + + /* Don't try and migrate the "system" sources, as + * we'll defer to the built-in "system-*" key files. */ + if (g_strcmp0 (relative_uri, "system") == 0) + return; + + /* Also skip any sources with a "contacts://" base URI, which + * should just be "Birthdays & Anniversaries". We'll reset to + * the built-in key file. */ + if (g_strcmp0 (parse_data->base_uri, "contacts://") == 0) + return; + + /* Also skip any sources for groupware extensions, as these are + * no longer saved to disk. We do have a mechanism in place for + * remembering the UIDs of memory-only sources so that cached + * data can be reused, but let's not bother with it here. Let + * the sources be set up fresh. */ + if (base_uri_is_groupware (parse_data->base_uri)) + return; + + switch (parse_data->type) { + case PARSE_TYPE_ADDRESSBOOK: + group_name = E_SOURCE_EXTENSION_ADDRESS_BOOK; + break; + case PARSE_TYPE_CALENDAR: + group_name = E_SOURCE_EXTENSION_CALENDAR; + break; + case PARSE_TYPE_TASKS: + group_name = E_SOURCE_EXTENSION_TASK_LIST; + break; + case PARSE_TYPE_MEMOS: + group_name = E_SOURCE_EXTENSION_MEMO_LIST; + break; + default: + g_return_if_reached (); + } + + parse_data->file = e_server_side_source_new_user_file (uid); + + /* If the file already exists, skip this source. It may be that we + * already migrated it, in which case we don't want to overwrite it. */ + if (g_file_query_exists (parse_data->file, NULL)) + return; + + parse_data->key_file = g_key_file_new (); + + /* Trim ':' or '://' off the base_uri to get the backend name. */ + backend_name = g_strdup (parse_data->base_uri); + if ((cp = strchr (backend_name, ':')) != NULL) + *cp = '\0'; + + /* The parent name is generally the backend name + "-stub". */ + parent_name = g_strdup_printf ("%s-stub", backend_name); + + g_key_file_set_string ( + parse_data->key_file, + E_SOURCE_GROUP_NAME, + "DisplayName", name); + + g_key_file_set_string ( + parse_data->key_file, + E_SOURCE_GROUP_NAME, + "Parent", parent_name); + + if (color_spec != NULL) + g_key_file_set_string ( + parse_data->key_file, group_name, + "Color", color_spec); + + is_google_calendar = + (parse_data->type == PARSE_TYPE_CALENDAR) && + (g_strcmp0 (parse_data->base_uri, "google://") == 0); + + /* For Google Calendar sources we override the backend name. */ + if (is_google_calendar) + g_key_file_set_string ( + parse_data->key_file, group_name, + "BackendName", "caldav"); + else + g_key_file_set_string ( + parse_data->key_file, group_name, + "BackendName", backend_name); + + g_free (backend_name); + g_free (parent_name); + + /* Prefer absolute URIs over relative URIs. All these + * other strange rules are for backward-compatibility. */ + if (absolute_uri != NULL) + uri_string = g_strdup (absolute_uri); + else if (g_str_has_suffix (parse_data->base_uri, "/")) + uri_string = g_strconcat ( + parse_data->base_uri, relative_uri, NULL); + else if (g_strcmp0 (parse_data->base_uri, "local:") == 0) + uri_string = g_strconcat ( + parse_data->base_uri, relative_uri, NULL); + else + uri_string = g_strconcat ( + parse_data->base_uri, "/", relative_uri, NULL); + + parse_data->soup_uri = soup_uri_new (uri_string); + + /* Mangle the URI to not contain invalid characters. We'll need + * this later to rename the source's cache and data directories. */ + parse_data->mangled_uri = g_strdelimit (uri_string, ":/", '_'); + + /* g_strdelimit() modifies the input string in place, so ParseData + * now owns 'uri_string'. Clear the pointer to emphasize that. */ + uri_string = NULL; + + if (parse_data->soup_uri == NULL) { + g_warning ( + " Failed to parse source URI: %s", + (absolute_uri != NULL) ? absolute_uri : relative_uri); + g_key_file_free (parse_data->key_file); + parse_data->key_file = NULL; + return; + } + + if (g_strcmp0 (parse_data->base_uri, "local:") == 0) + migrate_parse_local_source (parse_data); + + else if (g_strcmp0 (parse_data->base_uri, "caldav://") == 0) + migrate_parse_caldav_source (parse_data); + + else if (g_strcmp0 (parse_data->base_uri, "google://") == 0) + migrate_parse_google_source (parse_data); + + else if (g_strcmp0 (parse_data->base_uri, "ldap://") == 0) + migrate_parse_ldap_source (parse_data); + + else if (g_strcmp0 (parse_data->base_uri, "vcf://") == 0) + migrate_parse_vcf_source (parse_data); + + else if (g_strcmp0 (parse_data->base_uri, "weather://") == 0) + migrate_parse_weather_source (parse_data); + + else if (g_strcmp0 (parse_data->base_uri, "webcal://") == 0) + migrate_parse_webcal_source (parse_data); + + else if (g_strcmp0 (parse_data->base_uri, "webdav://") == 0) + migrate_parse_webdav_source (parse_data); + + migrate_keyring_entry ( + uid, + parse_data->soup_uri->user, + parse_data->soup_uri->host, + parse_data->soup_uri->scheme); +} + +static void +migrate_parse_property (ParseData *parse_data, + const gchar *element_name, + const gchar **attribute_names, + const gchar **attribute_values, + GError **error) +{ + const gchar *property_name; + const gchar *property_value; + gboolean success; + + success = g_markup_collect_attributes ( + element_name, + attribute_names, + attribute_values, + error, + G_MARKUP_COLLECT_STRING, + "name", &property_name, + G_MARKUP_COLLECT_STRING, + "value", &property_value, + G_MARKUP_COLLECT_INVALID); + + if (!success) + return; + + if (g_strcmp0 (property_name, "alarm") == 0) { + g_key_file_set_boolean ( + parse_data->key_file, + E_SOURCE_EXTENSION_ALARMS, + "IncludeMe", + is_true (property_value)); + + } else if (g_strcmp0 (property_name, "auth") == 0) { + if (is_true (property_value)) + g_key_file_set_string ( + parse_data->key_file, + E_SOURCE_EXTENSION_AUTHENTICATION, + "Method", "plain/password"); + else if (is_false (property_value)) + g_key_file_set_string ( + parse_data->key_file, + E_SOURCE_EXTENSION_AUTHENTICATION, + "Method", "none"); + else + g_key_file_set_string ( + parse_data->key_file, + E_SOURCE_EXTENSION_AUTHENTICATION, + "Method", property_value); + + } else if (g_strcmp0 (property_name, "completion") == 0) { + g_key_file_set_boolean ( + parse_data->key_file, + E_SOURCE_EXTENSION_AUTOCOMPLETE, + "IncludeMe", + is_true (property_value)); + + /* If we see a "goa-account-id" property, skip the entire + * source and let the online-accounts module recreate it. */ + } else if (g_strcmp0 (property_name, "goa-account-id") == 0) { + parse_data->skip = TRUE; + + } else if (g_strcmp0 (property_name, "last-notified") == 0) { + g_key_file_set_string ( + parse_data->key_file, + E_SOURCE_EXTENSION_ALARMS, + "LastNotified", property_value); + + } else if (g_strcmp0 (property_name, "offline_sync") == 0) { + g_key_file_set_boolean ( + parse_data->key_file, + E_SOURCE_EXTENSION_OFFLINE, + "StaySynchronized", + is_true (property_value)); + + } else if (g_strcmp0 (property_name, "refresh") == 0) { + g_key_file_set_boolean ( + parse_data->key_file, + E_SOURCE_EXTENSION_REFRESH, + "Enabled", TRUE); + g_key_file_set_string ( + parse_data->key_file, + E_SOURCE_EXTENSION_REFRESH, + "IntervalMinutes", property_value); + + } else if (g_strcmp0 (property_name, "remember_password") == 0) { + g_key_file_set_boolean ( + parse_data->key_file, + E_SOURCE_EXTENSION_AUTHENTICATION, + "RememberPassword", + is_true (property_value)); + + } else if (g_strcmp0 (property_name, "ssl") == 0) { + if (is_true (property_value)) + g_key_file_set_string ( + parse_data->key_file, + E_SOURCE_EXTENSION_SECURITY, + "Method", "tls"); + else if (is_false (property_value)) + g_key_file_set_string ( + parse_data->key_file, + E_SOURCE_EXTENSION_SECURITY, + "Method", "none"); + + /* These next two are LDAP-specific. */ + else if (g_strcmp0 (property_value, "always") == 0) + g_key_file_set_string ( + parse_data->key_file, + E_SOURCE_EXTENSION_SECURITY, + "Method", "starttls"); + else if (g_strcmp0 (property_value, "whenever_possible") == 0) + g_key_file_set_string ( + parse_data->key_file, + E_SOURCE_EXTENSION_SECURITY, + "Method", "ldaps"); + + /* For WebDAV-based backends we set the port to 80 + * (http://) by default. If we see that and we're + * using a secure connection, bump the port to 443 + * (https://). */ + if (parse_data->soup_uri->port == 80) + if (is_true (property_value)) + g_key_file_set_integer ( + parse_data->key_file, + E_SOURCE_EXTENSION_AUTHENTICATION, + "Port", 443); + + } else if (g_strcmp0 (property_name, "use_ssl") == 0) { + g_key_file_set_string ( + parse_data->key_file, + E_SOURCE_EXTENSION_SECURITY, + "Method", + is_true (property_value) ? + "tls" : "none"); + + /* For WebDAV-based backends we set the port to 80 + * (http://) by default. If we see that and we're + * using a secure connection, bump the port to 443 + * (https://). */ + if (parse_data->soup_uri->port == 80) + if (is_true (property_value)) + g_key_file_set_integer ( + parse_data->key_file, + E_SOURCE_EXTENSION_AUTHENTICATION, + "Port", 443); + + } else if (g_strcmp0 (property_name, "use-in-contacts-calendar") == 0) { + g_key_file_set_boolean ( + parse_data->key_file, + E_SOURCE_EXTENSION_CONTACTS_BACKEND, + "IncludeMe", + is_true (property_value)); + + } else if (parse_data->property_func != NULL) { + parse_data->property_func ( + parse_data, property_name, property_value); + } +} + +static void +migrate_parse_source_xml_start_element (GMarkupParseContext *context, + const gchar *element_name, + const gchar **attribute_names, + const gchar **attribute_values, + gpointer user_data, + GError **error) +{ + ParseData *parse_data = user_data; + + if (g_strcmp0 (element_name, "group") == 0) { + if (parse_data->state != PARSE_STATE_IN_SOURCES_VALUE) + goto invalid_content; + + parse_data->state = PARSE_STATE_IN_GROUP; + + migrate_parse_group ( + parse_data, + element_name, + attribute_names, + attribute_values, + error); + + return; + } + + if (g_strcmp0 (element_name, "source") == 0) { + if (parse_data->state != PARSE_STATE_IN_GROUP) + goto invalid_content; + + parse_data->state = PARSE_STATE_IN_SOURCE; + + migrate_parse_source ( + parse_data, + element_name, + attribute_names, + attribute_values, + error); + + return; + } + + if (g_strcmp0 (element_name, "properties") == 0) { + /* Disregard group properties, we're only + * interested in source properties. */ + if (parse_data->state == PARSE_STATE_IN_GROUP) + return; + + if (parse_data->state != PARSE_STATE_IN_SOURCE) + goto invalid_content; + + parse_data->state = PARSE_STATE_IN_PROPERTIES; + + return; + } + + if (g_strcmp0 (element_name, "property") == 0) { + /* Disregard group properties, we're only + * interested in source properties. */ + if (parse_data->state == PARSE_STATE_IN_GROUP) + return; + + if (parse_data->state != PARSE_STATE_IN_PROPERTIES) + goto invalid_content; + + /* The key file will be NULL if we decided to skip it. + * e.g. A file with the same UID may already exist. */ + if (parse_data->key_file != NULL) + migrate_parse_property ( + parse_data, + element_name, + attribute_names, + attribute_values, + error); + + return; + } + + g_set_error ( + error, G_MARKUP_ERROR, G_MARKUP_ERROR_UNKNOWN_ELEMENT, + "Unknown element <%s>", element_name); + + return; + +invalid_content: + + g_set_error ( + error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, + "Element <%s> at unexpected location", element_name); +} + +static void +migrate_parse_source_xml_end_element (GMarkupParseContext *context, + const gchar *element_name, + gpointer user_data, + GError **error) +{ + ParseData *parse_data = user_data; + + if (g_strcmp0 (element_name, "group") == 0) { + if (parse_data->state == PARSE_STATE_IN_GROUP) { + parse_data->state = PARSE_STATE_IN_SOURCES_VALUE; + + /* Clean up tag data. */ + + g_free (parse_data->base_uri); + parse_data->base_uri = NULL; + } + return; + } + + if (g_strcmp0 (element_name, "source") == 0) { + if (parse_data->state == PARSE_STATE_IN_SOURCE) { + parse_data->state = PARSE_STATE_IN_GROUP; + + /* Clean up tag data. */ + + /* The key file will be NULL if we decided to skip it. + * e.g. A file with the same UID may already exist. */ + if (parse_data->key_file != NULL) { + GError *local_error = NULL; + + if (!parse_data->skip) + migrate_parse_commit_changes ( + parse_data->type, + parse_data->file, + parse_data->key_file, + parse_data->mangled_uri, + &local_error); + + if (local_error != NULL) { + g_printerr ( + " FAILED: %s\n", + local_error->message); + g_error_free (local_error); + } + + g_key_file_free (parse_data->key_file); + parse_data->key_file = NULL; + } + + if (parse_data->file != NULL) { + g_object_unref (parse_data->file); + parse_data->file = NULL; + } + + g_free (parse_data->mangled_uri); + parse_data->mangled_uri = NULL; + + if (parse_data->soup_uri != NULL) { + soup_uri_free (parse_data->soup_uri); + parse_data->soup_uri = NULL; + } + + parse_data->property_func = NULL; + + parse_data->skip = FALSE; + } + return; + } + + if (g_strcmp0 (element_name, "properties") == 0) { + if (parse_data->state == PARSE_STATE_IN_PROPERTIES) + parse_data->state = PARSE_STATE_IN_SOURCE; + return; + } +} + +static GMarkupParser source_xml_parser = { + migrate_parse_source_xml_start_element, + migrate_parse_source_xml_end_element, + NULL, /* text */ + NULL, /* passthrough */ + NULL /* error */ +}; + +static void +migrate_parse_gconf_xml_start_element (GMarkupParseContext *context, + const gchar *element_name, + const gchar **attribute_names, + const gchar **attribute_values, + gpointer user_data, + GError **error) +{ + ParseData *parse_data = user_data; + + if (g_strcmp0 (element_name, "gconf") == 0) { + if (parse_data->state != PARSE_STATE_INITIAL) + goto invalid_content; + + parse_data->state = PARSE_STATE_IN_GCONF; + + return; + } + + if (g_strcmp0 (element_name, "entry") == 0) { + const gchar *name; + gboolean success; + + if (parse_data->state != PARSE_STATE_IN_GCONF) + goto invalid_content; + + success = g_markup_collect_attributes ( + element_name, + attribute_names, + attribute_values, + error, + G_MARKUP_COLLECT_STRING, + "name", &name, + G_MARKUP_COLLECT_STRING, + "mtime", NULL, + G_MARKUP_COLLECT_STRING | + G_MARKUP_COLLECT_OPTIONAL, + "type", NULL, + G_MARKUP_COLLECT_STRING | + G_MARKUP_COLLECT_OPTIONAL, + "ltype", NULL, + G_MARKUP_COLLECT_STRING | + G_MARKUP_COLLECT_OPTIONAL, + "schema", NULL, + G_MARKUP_COLLECT_STRING | + G_MARKUP_COLLECT_OPTIONAL, + "value", NULL, + G_MARKUP_COLLECT_INVALID); + + if (success && g_strcmp0 (name, "accounts") == 0) + parse_data->state = PARSE_STATE_IN_ACCOUNTS_ENTRY; + + if (success && g_strcmp0 (name, "signatues") == 0) + parse_data->state = PARSE_STATE_IN_SIGNATURES_ENTRY; + + if (success && g_strcmp0 (name, "sources") == 0) + parse_data->state = PARSE_STATE_IN_SOURCES_ENTRY; + + return; + } + + if (g_strcmp0 (element_name, "li") == 0) + return; + + if (g_strcmp0 (element_name, "stringvalue") == 0) { + if (parse_data->state == PARSE_STATE_IN_ACCOUNTS_ENTRY) + parse_data->state = PARSE_STATE_IN_ACCOUNTS_VALUE; + + if (parse_data->state == PARSE_STATE_IN_SIGNATURES_ENTRY) + parse_data->state = PARSE_STATE_IN_SIGNATURES_VALUE; + + if (parse_data->state == PARSE_STATE_IN_SOURCES_ENTRY) + parse_data->state = PARSE_STATE_IN_SOURCES_VALUE; + + return; + } + + g_set_error ( + error, G_MARKUP_ERROR, G_MARKUP_ERROR_UNKNOWN_ELEMENT, + "Unknown element <%s>", element_name); + + return; + +invalid_content: + + g_set_error ( + error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, + "Element <%s> at unexpected location", element_name); +} + +static void +migrate_parse_gconf_xml_end_element (GMarkupParseContext *context, + const gchar *element_name, + gpointer user_data, + GError **error) +{ + ParseData *parse_data = user_data; + + if (g_strcmp0 (element_name, "gconf") == 0) { + if (parse_data->state == PARSE_STATE_IN_GCONF) + parse_data->state = PARSE_STATE_INITIAL; + + return; + } + + if (g_strcmp0 (element_name, "entry") == 0) { + if (parse_data->state == PARSE_STATE_IN_ACCOUNTS_ENTRY) + parse_data->state = PARSE_STATE_IN_GCONF; + + if (parse_data->state == PARSE_STATE_IN_SIGNATURES_ENTRY) + parse_data->state = PARSE_STATE_IN_GCONF; + + if (parse_data->state == PARSE_STATE_IN_SOURCES_ENTRY) + parse_data->state = PARSE_STATE_IN_GCONF; + + return; + } + + if (g_strcmp0 (element_name, "stringvalue") == 0) { + if (parse_data->state == PARSE_STATE_IN_ACCOUNTS_VALUE) + parse_data->state = PARSE_STATE_IN_ACCOUNTS_ENTRY; + + if (parse_data->state == PARSE_STATE_IN_SIGNATURES_VALUE) + parse_data->state = PARSE_STATE_IN_SIGNATURES_ENTRY; + + if (parse_data->state == PARSE_STATE_IN_SOURCES_VALUE) + parse_data->state = PARSE_STATE_IN_SOURCES_ENTRY; + + return; + } +} + +static void +migrate_parse_gconf_xml_text (GMarkupParseContext *context, + const gchar *text, + gsize length, + gpointer user_data, + GError **error) +{ + ParseData *parse_data = user_data; + + /* The account and signature data is encoded XML stuffed into + * GConf XML (yuck!). Fortunately GMarkupParseContext decodes + * the XML for us, so we just have to feed it to a nested + * GMarkupParseContext. */ + + switch (parse_data->state) { + case PARSE_STATE_IN_ACCOUNTS_VALUE: + context = g_markup_parse_context_new ( + &account_xml_parser, 0, parse_data, NULL); + break; + + case PARSE_STATE_IN_SIGNATURES_VALUE: + context = g_markup_parse_context_new ( + &signature_xml_parser, 0, parse_data, NULL); + break; + + case PARSE_STATE_IN_SOURCES_VALUE: + context = g_markup_parse_context_new ( + &source_xml_parser, 0, parse_data, NULL); + break; + + default: + return; + } + + if (g_markup_parse_context_parse (context, text, length, error)) + g_markup_parse_context_end_parse (context, error); + + g_markup_parse_context_free (context); +} + +static GMarkupParser gconf_xml_parser = { + migrate_parse_gconf_xml_start_element, + migrate_parse_gconf_xml_end_element, + migrate_parse_gconf_xml_text, + NULL, /* passthrough */ + NULL /* error */ +}; + +static gboolean +migrate_parse_gconf_xml (ParseType parse_type, + const gchar *contents, + gsize length, + GError **error) +{ + GMarkupParseContext *context; + ParseData *parse_data; + gboolean success = FALSE; + + parse_data = parse_data_new (parse_type); + + context = g_markup_parse_context_new ( + &gconf_xml_parser, 0, parse_data, + (GDestroyNotify) parse_data_free); + + if (g_markup_parse_context_parse (context, contents, length, error)) + if (g_markup_parse_context_end_parse (context, error)) + success = TRUE; + + g_markup_parse_context_free (context); + + return success; +} + +static void +migrate_remove_gconf_xml (const gchar *gconf_key, + const gchar *gconf_xml) +{ + /* Remove the GConf string list so the user is not haunted by + * old data sources being resurrected from leftover GConf data. + * Also delete the %gconf.xml file itself. If gconfd is running + * then it will just recreate the file from memory when it exits + * (which is why we invoke gconftool-2), otherwise the file will + * stay deleted. */ + + gchar *path_to_program; + + path_to_program = g_find_program_in_path ("gconftool-2"); + + if (path_to_program != NULL) { + gchar *command_line; + GError *error = NULL; + + command_line = g_strjoin ( + " ", + path_to_program, + "--set", + "--type=list", + "--list-type=string", + gconf_key, "[]", NULL); + + /* We don't really care if the command worked or not, + * just check that the program got spawned successfully. */ + if (!g_spawn_command_line_async (command_line, &error)) { + g_printerr ( + "Failed to spawn '%s': %s\n", + path_to_program, error->message); + g_error_free (error); + } + + g_free (path_to_program); + g_free (command_line); + } + + if (g_file_test (gconf_xml, G_FILE_TEST_IS_REGULAR)) { + if (g_remove (gconf_xml) == -1) { + g_printerr ( + "Failed to remove '%s': %s\n", + gconf_xml, g_strerror (errno)); + } + } +} + +void +evolution_source_registry_migrate_sources (void) +{ + gchar *base_dir; + gchar *contents; + gchar *gconf_xml; + gsize length; + const gchar *gconf_key; + GError *error = NULL; + + base_dir = g_build_filename ( + g_get_home_dir (), ".gconf", "apps", "evolution", NULL); + + /* ------------------------------------------------------------------*/ + + g_print ("Migrating mail accounts from GConf...\n"); + + gconf_xml = g_build_filename ( + base_dir, "mail", "%gconf.xml", NULL); + g_file_get_contents (gconf_xml, &contents, &length, &error); + + if (error == NULL) { + migrate_parse_gconf_xml ( + PARSE_TYPE_MAIL, + contents, length, &error); + g_free (contents); + } + + if (error == NULL) { + gconf_key = "/apps/evolution/mail/accounts"; + migrate_remove_gconf_xml (gconf_key, gconf_xml); + } else { + g_printerr (" FAILED: %s\n", error->message); + g_clear_error (&error); + } + + g_free (gconf_xml); + + /* ------------------------------------------------------------------*/ + + g_print ("Migrating addressbook sources from GConf...\n"); + + gconf_xml = g_build_filename ( + base_dir, "addressbook", "%gconf.xml", NULL); + g_file_get_contents (gconf_xml, &contents, &length, &error); + + if (error == NULL) { + migrate_parse_gconf_xml ( + PARSE_TYPE_ADDRESSBOOK, + contents, length, &error); + g_free (contents); + } + + if (error == NULL) { + gconf_key = "/apps/evolution/addressbook/sources"; + migrate_remove_gconf_xml (gconf_key, gconf_xml); + } else { + g_printerr (" FAILED: %s\n", error->message); + g_clear_error (&error); + } + + g_free (gconf_xml); + + /* ------------------------------------------------------------------*/ + + g_print ("Migrating calendar sources from GConf...\n"); + + gconf_xml = g_build_filename ( + base_dir, "calendar", "%gconf.xml", NULL); + g_file_get_contents (gconf_xml, &contents, &length, &error); + + if (error == NULL) { + migrate_parse_gconf_xml ( + PARSE_TYPE_CALENDAR, + contents, length, &error); + g_free (contents); + } + + if (error == NULL) { + gconf_key = "/apps/evolution/calendar/sources"; + migrate_remove_gconf_xml (gconf_key, gconf_xml); + } else { + g_printerr (" FAILED: %s\n", error->message); + g_clear_error (&error); + } + + g_free (gconf_xml); + + /* ------------------------------------------------------------------*/ + + g_print ("Migrating task list sources from GConf...\n"); + + gconf_xml = g_build_filename ( + base_dir, "tasks", "%gconf.xml", NULL); + g_file_get_contents (gconf_xml, &contents, &length, &error); + + if (error == NULL) { + migrate_parse_gconf_xml ( + PARSE_TYPE_TASKS, + contents, length, &error); + g_free (contents); + } + + if (error == NULL) { + gconf_key = "/apps/evolution/tasks/sources"; + migrate_remove_gconf_xml (gconf_key, gconf_xml); + } else { + g_printerr (" FAILED: %s\n", error->message); + g_clear_error (&error); + } + + g_free (gconf_xml); + + /* ------------------------------------------------------------------*/ + + g_print ("Migrating memo list sources from GConf...\n"); + + gconf_xml = g_build_filename ( + base_dir, "memos", "%gconf.xml", NULL); + g_file_get_contents (gconf_xml, &contents, &length, &error); + + if (error == NULL) { + migrate_parse_gconf_xml ( + PARSE_TYPE_MEMOS, + contents, length, &error); + g_free (contents); + } + + if (error == NULL) { + gconf_key = "/apps/evolution/memos/sources"; + migrate_remove_gconf_xml (gconf_key, gconf_xml); + } else { + g_printerr (" FAILED: %s\n", error->message); + g_clear_error (&error); + } + + g_free (gconf_xml); + + /* ------------------------------------------------------------------*/ + + g_free (base_dir); +} diff --git a/services/evolution-source-registry/evolution-source-registry.c b/services/evolution-source-registry/evolution-source-registry.c new file mode 100644 index 0000000..56b9a21 --- /dev/null +++ b/services/evolution-source-registry/evolution-source-registry.c @@ -0,0 +1,85 @@ +/* + * e-source-registry.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 + * + */ + +#include +#include +#include +#include + +#include + +/* Forward Declarations */ +void evolution_source_registry_migrate_basedir (void); +void evolution_source_registry_migrate_sources (void); + +gint +main (gint argc, + gchar **argv) +{ + EDBusServer *server; + EDBusServerExitCode exit_code; + GError *error = NULL; + + setlocale (LC_ALL, ""); + bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR); + bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); + + g_type_init (); + +reload: + /* Migrate user data from ~/.evolution to XDG base directories. */ + evolution_source_registry_migrate_basedir (); + + /* Migrate ESource data from GConf XML blobs to key files. + * Do this AFTER XDG base directory migration since the key + * files are saved according to XDG base directory settings. */ + evolution_source_registry_migrate_sources (); + + server = e_source_registry_server_new (); + + /* Failure here is fatal. Don't even try to keep going. */ + e_source_registry_server_load_all ( + E_SOURCE_REGISTRY_SERVER (server), &error); + + if (error != NULL) { + g_printerr ("%s\n", error->message); + g_object_unref (server); + exit (EXIT_FAILURE); + } + + g_print ("Server is up and running...\n"); + + /* Keep the server from quitting on its own. + * We don't have a way of tracking number of + * active clients, so once the server is up, + * it's up until the session bus closes. */ + e_dbus_server_hold (server); + + exit_code = e_dbus_server_run (server, FALSE); + + g_object_unref (server); + + if (exit_code == E_DBUS_SERVER_EXIT_RELOAD) { + g_print ("Reloading...\n"); + goto reload; + } + + g_print ("Bye.\n"); + + return 0; +} diff --git a/services/evolution-source-registry/org.gnome.evolution.dataserver.Sources.service.in b/services/evolution-source-registry/org.gnome.evolution.dataserver.Sources.service.in new file mode 100644 index 0000000..bc89d45 --- /dev/null +++ b/services/evolution-source-registry/org.gnome.evolution.dataserver.Sources.service.in @@ -0,0 +1,3 @@ +[D-BUS Service] +Name=@SOURCES_DBUS_SERVICE_NAME@ +Exec=@libexecdir@/evolution-source-registry -- 2.7.4