/*
 * 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 <http://www.gnu.org/licenses/>
 *
 */

/**
 * SECTION: e-server-side-source
 * @include: libebackend/libebackend.h
 * @short_description: A server-side data source
 *
 * An #EServerSideSource is an #ESource with some additional capabilities
 * exclusive to the registry D-Bus service.
 **/

#include "e-server-side-source.h"

#include <config.h>
#include <glib/gi18n-lib.h>

/* Private D-Bus classes. */
#include <e-dbus-source.h>

#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 _AsyncContext AsyncContext;

struct _EServerSideSourcePrivate {
	gpointer server;  /* weak pointer */
	GWeakRef oauth2_support;

	GNode node;
	GFile *file;

	/* For comparison. */
	gchar *file_contents;

	gboolean allow_auth_prompt;
	GType auth_session_type;
	gchar *write_directory;
};

struct _AsyncContext {
	EDBusSourceRemoteCreatable *remote_creatable;
	EDBusSourceRemoteDeletable *remote_deletable;
	EDBusSourceOAuth2Support *oauth2_support;
	GDBusMethodInvocation *invocation;
};

enum {
	PROP_0,
	PROP_ALLOW_AUTH_PROMPT,
	PROP_AUTH_SESSION_TYPE,
	PROP_EXPORTED,
	PROP_FILE,
	PROP_OAUTH2_SUPPORT,
	PROP_REMOTE_CREATABLE,
	PROP_REMOTE_DELETABLE,
	PROP_REMOVABLE,
	PROP_SERVER,
	PROP_WRITABLE,
	PROP_WRITE_DIRECTORY
};

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 void
async_context_free (AsyncContext *async_context)
{
	if (async_context->remote_creatable != NULL)
		g_object_unref (async_context->remote_creatable);

	if (async_context->remote_deletable != NULL)
		g_object_unref (async_context->remote_deletable);

	if (async_context->oauth2_support != NULL)
		g_object_unref (async_context->oauth2_support);

	if (async_context->invocation != NULL)
		g_object_unref (async_context->invocation);

	g_slice_free (AsyncContext, async_context);
}

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;
}

/* Helper for server_side_source_remote_create_cb() */
static void
server_side_source_remote_create_done_cb (GObject *source_object,
                                          GAsyncResult *result,
                                          gpointer user_data)
{
	ESource *source;
	AsyncContext *async_context;
	GError *error = NULL;

	source = E_SOURCE (source_object);
	async_context = (AsyncContext *) user_data;

	e_source_remote_create_finish (source, result, &error);

	if (error != NULL)
		g_dbus_method_invocation_take_error (
			async_context->invocation, error);
	else
		e_dbus_source_remote_creatable_complete_create (
			async_context->remote_creatable,
			async_context->invocation);

	async_context_free (async_context);
}

static gboolean
server_side_source_remote_create_cb (EDBusSourceRemoteCreatable *interface,
                                     GDBusMethodInvocation *invocation,
                                     const gchar *uid,
                                     const gchar *data,
                                     ESource *source)
{
	EServerSideSource *server_side_source;
	ESourceRegistryServer *server;
	AsyncContext *async_context;
	ESource *scratch_source;
	GDBusObject *dbus_object;
	EDBusSource *dbus_source;
	GKeyFile *key_file;
	GFile *file;
	GError *error = NULL;

	/* Create a new EServerSideSource from 'uid' and 'data' but
	 * DO NOT add it to the ESourceRegistryServer yet.  It's up
	 * to the ECollectionBackend whether to use source as given
	 * or create its own equivalent EServerSideSource, possibly
	 * in response to a notification from a remote server. */

	/* Validate the raw data. */
	key_file = g_key_file_new ();
	server_side_source_parse_data (key_file, data, strlen (data), &error);
	g_key_file_free (key_file);

	if (error != NULL) {
		g_dbus_method_invocation_take_error (invocation, error);
		return TRUE;
	}

	server_side_source = E_SERVER_SIDE_SOURCE (source);
	server = e_server_side_source_get_server (server_side_source);

	file = e_server_side_source_new_user_file (uid);
	scratch_source = e_server_side_source_new (server, file, &error);
	g_object_unref (file);

	/* Sanity check. */
	g_warn_if_fail (
		((scratch_source != NULL) && (error == NULL)) ||
		((scratch_source == NULL) && (error != NULL)));

	if (error != NULL) {
		g_dbus_method_invocation_take_error (invocation, error);
		return TRUE;
	}

	dbus_object = e_source_ref_dbus_object (scratch_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_object);
	g_object_unref (dbus_source);

	async_context = g_slice_new0 (AsyncContext);
	async_context->remote_creatable = g_object_ref (interface);
	async_context->invocation = g_object_ref (invocation);

	e_source_remote_create (
		source, scratch_source, NULL,
		server_side_source_remote_create_done_cb,
		async_context);

	g_object_unref (scratch_source);

	return TRUE;
}

/* Helper for server_side_source_remote_delete_cb() */
static void
server_side_source_remote_delete_done_cb (GObject *source_object,
                                          GAsyncResult *result,
                                          gpointer user_data)
{
	ESource *source;
	AsyncContext *async_context;
	GError *error = NULL;

	source = E_SOURCE (source_object);
	async_context = (AsyncContext *) user_data;

	e_source_remote_delete_finish (source, result, &error);

	if (error != NULL)
		g_dbus_method_invocation_take_error (
			async_context->invocation, error);
	else
		e_dbus_source_remote_deletable_complete_delete (
			async_context->remote_deletable,
			async_context->invocation);

	async_context_free (async_context);
}

static gboolean
server_side_source_remote_delete_cb (EDBusSourceRemoteDeletable *interface,
                                     GDBusMethodInvocation *invocation,
                                     ESource *source)
{
	AsyncContext *async_context;

	async_context = g_slice_new0 (AsyncContext);
	async_context->remote_deletable = g_object_ref (interface);
	async_context->invocation = g_object_ref (invocation);

	e_source_remote_delete (
		source, NULL,
		server_side_source_remote_delete_done_cb,
		async_context);

	return TRUE;
}

/* Helper for server_side_source_get_access_token_cb() */
static void
server_side_source_get_access_token_done_cb (GObject *source_object,
                                             GAsyncResult *result,
                                             gpointer user_data)
{
	ESource *source;
	AsyncContext *async_context;
	gchar *access_token = NULL;
	gint expires_in = 0;
	GError *error = NULL;

	source = E_SOURCE (source_object);
	async_context = (AsyncContext *) user_data;

	e_source_get_oauth2_access_token_finish (
		source, result, &access_token, &expires_in, &error);

	/* Sanity check. */
	g_return_if_fail (
		((access_token != NULL) && (error == NULL)) ||
		((access_token == NULL) && (error != NULL)));

	if (error != NULL)
		g_dbus_method_invocation_take_error (
			async_context->invocation, error);
	else
		e_dbus_source_oauth2_support_complete_get_access_token (
			async_context->oauth2_support,
			async_context->invocation,
			access_token, expires_in);

	g_free (access_token);

	async_context_free (async_context);
}

static gboolean
server_side_source_get_access_token_cb (EDBusSourceOAuth2Support *interface,
                                        GDBusMethodInvocation *invocation,
                                        ESource *source)
{
	AsyncContext *async_context;

	async_context = g_slice_new0 (AsyncContext);
	async_context->oauth2_support = g_object_ref (interface);
	async_context->invocation = g_object_ref (invocation);

	e_source_get_oauth2_access_token (
		source, NULL,
		server_side_source_get_access_token_done_cb,
		async_context);

	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_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_AUTH_SESSION_TYPE:
			e_server_side_source_set_auth_session_type (
				E_SERVER_SIDE_SOURCE (object),
				g_value_get_gtype (value));
			return;

		case PROP_FILE:
			server_side_source_set_file (
				E_SERVER_SIDE_SOURCE (object),
				g_value_get_object (value));
			return;

		case PROP_OAUTH2_SUPPORT:
			e_server_side_source_set_oauth2_support (
				E_SERVER_SIDE_SOURCE (object),
				g_value_get_object (value));
			return;

		case PROP_REMOTE_CREATABLE:
			e_server_side_source_set_remote_creatable (
				E_SERVER_SIDE_SOURCE (object),
				g_value_get_boolean (value));
			return;

		case PROP_REMOTE_DELETABLE:
			e_server_side_source_set_remote_deletable (
				E_SERVER_SIDE_SOURCE (object),
				g_value_get_boolean (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_WRITABLE:
			e_server_side_source_set_writable (
				E_SERVER_SIDE_SOURCE (object),
				g_value_get_boolean (value));
			return;

		case PROP_WRITE_DIRECTORY:
			e_server_side_source_set_write_directory (
				E_SERVER_SIDE_SOURCE (object),
				g_value_get_string (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_AUTH_SESSION_TYPE:
			g_value_set_gtype (
				value,
				e_server_side_source_get_auth_session_type (
				E_SERVER_SIDE_SOURCE (object)));
			return;

		case PROP_EXPORTED:
			g_value_set_boolean (
				value,
				e_server_side_source_get_exported (
				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_OAUTH2_SUPPORT:
			g_value_take_object (
				value,
				e_server_side_source_ref_oauth2_support (
				E_SERVER_SIDE_SOURCE (object)));
			return;

		case PROP_REMOTE_CREATABLE:
			g_value_set_boolean (
				value,
				e_source_get_remote_creatable (
				E_SOURCE (object)));
			return;

		case PROP_REMOTE_DELETABLE:
			g_value_set_boolean (
				value,
				e_source_get_remote_deletable (
				E_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_WRITABLE:
			g_value_set_boolean (
				value,
				e_source_get_writable (
				E_SOURCE (object)));
			return;

		case PROP_WRITE_DIRECTORY:
			g_value_set_string (
				value,
				e_server_side_source_get_write_directory (
				E_SERVER_SIDE_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;
	}

	g_weak_ref_set (&priv->oauth2_support, 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->file_contents);
	g_free (priv->write_directory);

	/* 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;

	/* Do not write changes to disk until the source has been exported. */
	if (!e_server_side_source_get_exported (E_SERVER_SIDE_SOURCE (source)))
		return;

	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)
{
	EAsyncClosure *closure;
	GAsyncResult *result;
	gboolean success;

	closure = e_async_closure_new ();

	e_source_remove (
		source, cancellable, e_async_closure_callback, closure);

	result = e_async_closure_wait (closure);

	success = e_source_remove_finish (source, result, error);

	e_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;
	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)
			g_file_delete (file, cancellable, &error);

		/* XXX Even though e_source_registry_server_remove_source()
		 *     is called first, the object path is unexported from
		 *     an idle callback some time after we have deleted the
		 *     key file.  That creates a small window of time where
		 *     the file is deleted but the object is still exported.
		 *
		 *     If a client calls e_source_remove() during that small
		 *     window of time, we still want to report a successful
		 *     removal, so disregard G_IO_ERROR_NOT_FOUND. */
		if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
			g_clear_error (&error);

		if (error != NULL)
			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)
{
	EAsyncClosure *closure;
	GAsyncResult *result;
	gboolean success;

	closure = e_async_closure_new ();

	e_source_write (
		source, cancellable, e_async_closure_callback, closure);

	result = e_async_closure_wait (closure);

	success = e_source_write_finish (source, result, error);

	e_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
	 * source's specified "write-directory" even if the key file
	 * was originally read from a different directory.  To avoid
	 * polluting the write directory with key files identical to
	 * the original, we check that the data has actually changed
	 * before writing a copy to disk. */

	replace_file =
		G_IS_FILE (priv->file) &&
		(g_strcmp0 (old_data, new_data) != 0);

	if (replace_file) {
		GFile *file;
		GFile *write_directory;
		gchar *basename;

		g_warn_if_fail (priv->write_directory != NULL);

		basename = g_file_get_basename (priv->file);
		write_directory = g_file_new_for_path (priv->write_directory);
		file = g_file_get_child (write_directory, basename);
		g_free (basename);

		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_make_directory_with_parents (
			write_directory, cancellable, &error);

		if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_EXISTS))
			g_clear_error (&error);

		if (error == NULL)
			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 (write_directory);
		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_remote_create_sync (ESource *source,
                                       ESource *scratch_source,
                                       GCancellable *cancellable,
                                       GError **error)
{
	ECollectionBackend *backend;
	ESourceRegistryServer *server;
	EServerSideSource *server_side_source;
	gboolean success;

	if (!e_source_get_remote_creatable (source)) {
		g_set_error (
			error, G_IO_ERROR,
			G_IO_ERROR_NOT_SUPPORTED,
			_("Data source '%s' does not "
			"support creating remote resources"),
			e_source_get_display_name (source));
		return FALSE;
	}

	server_side_source = E_SERVER_SIDE_SOURCE (source);
	server = e_server_side_source_get_server (server_side_source);
	backend = e_source_registry_server_ref_backend (server, source);

	if (backend == NULL) {
		g_set_error (
			error, G_IO_ERROR,
			G_IO_ERROR_NOT_SUPPORTED,
			_("Data source '%s' has no collection "
			"backend to create the remote resource"),
			e_source_get_display_name (source));
		return FALSE;
	}

	success = e_collection_backend_create_resource_sync (
		backend, scratch_source, cancellable, error);

	g_object_unref (backend);

	return success;
}

static gboolean
server_side_source_remote_delete_sync (ESource *source,
                                       GCancellable *cancellable,
                                       GError **error)
{
	ECollectionBackend *backend;
	ESourceRegistryServer *server;
	EServerSideSource *server_side_source;
	gboolean success;

	if (!e_source_get_remote_deletable (source)) {
		g_set_error (
			error, G_IO_ERROR,
			G_IO_ERROR_NOT_SUPPORTED,
			_("Data source '%s' does not "
			"support deleting remote resources"),
			e_source_get_display_name (source));
		return FALSE;
	}

	server_side_source = E_SERVER_SIDE_SOURCE (source);
	server = e_server_side_source_get_server (server_side_source);
	backend = e_source_registry_server_ref_backend (server, source);

	if (backend == NULL) {
		g_set_error (
			error, G_IO_ERROR,
			G_IO_ERROR_NOT_SUPPORTED,
			_("Data source '%s' has no collection "
			"backend to delete the remote resource"),
			e_source_get_display_name (source));
		return FALSE;
	}

	success = e_collection_backend_delete_resource_sync (
		backend, source, cancellable, error);

	g_object_unref (backend);

	return success;
}

static gboolean
server_side_source_get_oauth2_access_token_sync (ESource *source,
                                                 GCancellable *cancellable,
                                                 gchar **out_access_token,
                                                 gint *out_expires_in,
                                                 GError **error)
{
	EOAuth2Support *oauth2_support;
	gboolean success;

	oauth2_support = e_server_side_source_ref_oauth2_support (
		E_SERVER_SIDE_SOURCE (source));

	if (oauth2_support == NULL) {
		g_set_error (
			error, G_IO_ERROR,
			G_IO_ERROR_NOT_SUPPORTED,
			_("Data source '%s' does not "
			"support OAuth 2.0 authentication"),
			e_source_get_display_name (source));
		return FALSE;
	}

	success = e_oauth2_support_get_access_token_sync (
		oauth2_support, source, cancellable,
		out_access_token, out_expires_in, error);

	g_object_unref (oauth2_support);

	return success;
}

static gboolean
server_side_source_initable_init (GInitable *initable,
                                  GCancellable *cancellable,
                                  GError **error)
{
	EServerSideSource *source;
	GDBusObject *dbus_object;
	EDBusSource *dbus_source;
	gchar *uid;

	source = E_SERVER_SIDE_SOURCE (initable);

	dbus_source = e_dbus_source_skeleton_new ();

	uid = e_source_dup_uid (E_SOURCE (source));
	if (uid == NULL)
		uid = e_uid_new ();
	e_dbus_source_set_uid (dbus_source, uid);
	g_free (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;
	source_class->remote_create_sync =
		server_side_source_remote_create_sync;
	source_class->remote_delete_sync =
		server_side_source_remote_delete_sync;
	source_class->get_oauth2_access_token_sync =
		server_side_source_get_oauth2_access_token_sync;

	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_AUTH_SESSION_TYPE,
		g_param_spec_gtype (
			"auth-session-type",
			"Auth Session Type",
			"The type of authentication session "
			"to instantiate for this source",
			E_TYPE_AUTHENTICATION_SESSION,
			G_PARAM_READWRITE |
			G_PARAM_STATIC_STRINGS));

	g_object_class_install_property (
		object_class,
		PROP_EXPORTED,
		g_param_spec_boolean (
			"exported",
			"Exported",
			"Whether the source has been exported over D-Bus",
			FALSE,
			G_PARAM_READABLE |
			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));

	g_object_class_install_property (
		object_class,
		PROP_OAUTH2_SUPPORT,
		g_param_spec_object (
			"oauth2-support",
			"OAuth2 Support",
			"The object providing OAuth 2.0 support",
			E_TYPE_OAUTH2_SUPPORT,
			G_PARAM_READWRITE |
			G_PARAM_STATIC_STRINGS));

	/* This overrides the "remote-creatable" property
	 * in ESourceClass with a writable version. */
	g_object_class_install_property (
		object_class,
		PROP_REMOTE_CREATABLE,
		g_param_spec_boolean (
			"remote-creatable",
			"Remote Creatable",
			"Whether the data source "
			"can create remote resources",
			FALSE,
			G_PARAM_READWRITE |
			G_PARAM_STATIC_STRINGS));

	/* This overrides the "remote-deletable" property
	 * in ESourceClass with a writable version. */
	g_object_class_install_property (
		object_class,
		PROP_REMOTE_DELETABLE,
		g_param_spec_boolean (
			"remote-deletable",
			"Remote Deletable",
			"Whether the data source "
			"can delete remote resources",
			FALSE,
			G_PARAM_READWRITE |
			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 "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));

	/* Do not use G_PARAM_CONSTRUCT.  We initialize the
	 * property ourselves in e_server_side_source_init(). */
	g_object_class_install_property (
		object_class,
		PROP_WRITE_DIRECTORY,
		g_param_spec_string (
			"write-directory",
			"Write Directory",
			"Directory in which to write changes to disk",
			NULL,
			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)
{
	const gchar *user_dir;

	source->priv = E_SERVER_SIDE_SOURCE_GET_PRIVATE (source);

	source->priv->node.data = source;

	user_dir = e_server_side_source_get_user_dir ();
	source->priv->write_directory = g_strdup (user_dir);

	source->priv->auth_session_type = E_TYPE_AUTHENTICATION_SESSION;
}

/**
 * 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;
	gchar *uid = NULL;

	g_return_val_if_fail (E_IS_SOURCE_REGISTRY_SERVER (server), NULL);
	g_return_val_if_fail (file == NULL || G_IS_FILE (file), NULL);

	/* Extract a UID from the GFile, if we were given one. */
	if (file != NULL) {
		uid = e_server_side_source_uid_from_file (file, error);
		if (uid == NULL)
			return 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,
		"uid", uid, NULL);

	g_object_unref (dbus_object);
	g_free (uid);

	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));

	if (source->priv->allow_auth_prompt == allow_auth_prompt)
		return;

	source->priv->allow_auth_prompt = allow_auth_prompt;

	g_object_notify (G_OBJECT (source), "allow-auth-prompt");
}

/**
 * e_server_side_source_get_auth_session_type:
 * @source: an #EServerSideSource
 *
 * Returns the type of authentication session to use for @source,
 * which will get passed to e_source_registry_server_authenticate().
 *
 * This defaults to the #GType for #EAuthenticationSession.
 *
 * Returns: the type of authentication session to use for @source
 *
 * Since: 3.8
 **/
GType
e_server_side_source_get_auth_session_type (EServerSideSource *source)
{
	g_return_val_if_fail (
		E_IS_SERVER_SIDE_SOURCE (source),
		E_TYPE_AUTHENTICATION_SESSION);

	return source->priv->auth_session_type;
}

/**
 * e_server_side_source_set_auth_session_type:
 * @source: an #EServerSideSource
 * @auth_session_type: the type of authentication session to use for @source
 *
 * Sets the type of authentication session to use for @source, which will
 * get passed to e_source_registry_server_authenticate().
 *
 * @auth_session_type must be derived from #EAuthenticationSession, or else
 * the function will reject the #GType with a runtime warning.
 *
 * This defaults to the #GType for #EAuthenticationSession.
 *
 * Since: 3.8
 **/
void
e_server_side_source_set_auth_session_type (EServerSideSource *source,
                                            GType auth_session_type)
{
	g_return_if_fail (E_IS_SERVER_SIDE_SOURCE (source));

	if (!g_type_is_a (auth_session_type, E_TYPE_AUTHENTICATION_SESSION)) {
		g_warning (
			"Invalid auth session type '%s' for source '%s'",
			g_type_name (auth_session_type),
			e_source_get_display_name (E_SOURCE (source)));
		return;
	}

	if (auth_session_type == source->priv->auth_session_type)
		return;

	source->priv->auth_session_type = auth_session_type;

	g_object_notify (G_OBJECT (source), "auth-session-type");
}

/**
 * e_server_side_source_get_exported:
 * @source: an #EServerSideSource
 *
 * Returns whether @source has been exported over D-Bus.
 *
 * The function returns %FALSE after @source is initially created, %TRUE
 * after passing @source to e_source_registry_add_source() (provided that
 * @source's #ESource:parent is also exported), and %FALSE after passing
 * @source to e_source_registry_remove_source().
 *
 * Returns: whether @source has been exported
 *
 * Since: 3.6
 **/
gboolean
e_server_side_source_get_exported (EServerSideSource *source)
{
	ESourceRegistryServer *server;
	ESource *exported_source;
	gboolean exported = FALSE;
	const gchar *uid;

	g_return_val_if_fail (E_IS_SERVER_SIDE_SOURCE (source), FALSE);

	uid = e_source_get_uid (E_SOURCE (source));
	server = e_server_side_source_get_server (source);

	/* We're exported if we can look ourselves up in the registry. */

	exported_source = e_source_registry_server_ref_source (server, uid);
	if (exported_source != NULL) {
		exported = TRUE;
		g_object_unref (exported_source);
	}

	return exported;
}

/**
 * e_server_side_source_get_write_directory:
 * @source: an #EServerSideSource
 *
 * Returns the local directory path where changes to @source are written.
 *
 * By default, changes are written to the local directory path returned by
 * e_server_side_source_get_user_dir(), but an #ECollectionBackend may wish
 * to override this to use its own private cache directory for data sources
 * it creates automatically.
 *
 * Returns: the directory where changes are written
 *
 * Since: 3.6
 **/
const gchar *
e_server_side_source_get_write_directory (EServerSideSource *source)
{
	g_return_val_if_fail (E_IS_SERVER_SIDE_SOURCE (source), NULL);

	return source->priv->write_directory;
}

/**
 * e_server_side_source_set_write_directory:
 * @source: an #EServerSideSource
 * @write_directory: the directory where changes are to be written
 *
 * Sets the local directory path where changes to @source are to be written.
 *
 * By default, changes are written to the local directory path returned by
 * e_server_side_source_get_user_dir(), but an #ECollectionBackend may wish
 * to override this to use its own private cache directory for data sources
 * it creates automatically.
 *
 * Since: 3.6
 **/
void
e_server_side_source_set_write_directory (EServerSideSource *source,
                                          const gchar *write_directory)
{
	g_return_if_fail (E_IS_SERVER_SIDE_SOURCE (source));
	g_return_if_fail (write_directory != NULL);

	if (g_strcmp0 (source->priv->write_directory, write_directory) == 0)
		return;

	g_free (source->priv->write_directory);
	source->priv->write_directory = g_strdup (write_directory);

	g_object_notify (G_OBJECT (source), "write-directory");
}

/**
 * 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_interface = 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_interface =
			e_dbus_source_removable_skeleton_new ();

		g_signal_connect (
			dbus_interface, "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_interface);
	g_object_unref (dbus_object);

	if (dbus_interface != NULL)
		g_object_unref (dbus_interface);

	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_interface = 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_interface =
			e_dbus_source_writable_skeleton_new ();

		g_signal_connect (
			dbus_interface, "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_interface);
	g_object_unref (dbus_object);

	if (dbus_interface != NULL)
		g_object_unref (dbus_interface);

	g_object_notify (G_OBJECT (source), "writable");
}

/**
 * e_server_side_source_set_remote_creatable:
 * @source: an #EServerSideSource
 * @remote_creatable: whether to export the RemoteCreatable interface
 *
 * Indicates whether @source can be used to create resources on a remote
 * server.  Typically this is only set to %TRUE for collection sources.
 *
 * If %TRUE, the RemoteCreatable D-Bus interface is exported at the object
 * path for @source.  If %FALSE, the RemoteCreatable D-Bus interface is
 * unexported at the object path for @source, and any attempt by clients
 * to call e_source_remote_create() will fail.
 *
 * Unlike the #ESource:removable and #ESource:writable properties, this
 * is enforced for both clients of the registry D-Bus service and within
 * the registry D-Bus service itself.
 *
 * Since: 3.6
 **/
void
e_server_side_source_set_remote_creatable (EServerSideSource *source,
                                           gboolean remote_creatable)
{
	EDBusSourceRemoteCreatable *dbus_interface = NULL;
	GDBusObject *dbus_object;
	gboolean currently_remote_creatable;

	g_return_if_fail (E_IS_SERVER_SIDE_SOURCE (source));

	currently_remote_creatable =
		e_source_get_remote_creatable (E_SOURCE (source));

	if (remote_creatable == currently_remote_creatable)
		return;

	if (remote_creatable) {
		dbus_interface =
			e_dbus_source_remote_creatable_skeleton_new ();

		g_signal_connect (
			dbus_interface, "handle-create",
			G_CALLBACK (server_side_source_remote_create_cb),
			source);
	}

	dbus_object = e_source_ref_dbus_object (E_SOURCE (source));
	e_dbus_object_skeleton_set_source_remote_creatable (
		E_DBUS_OBJECT_SKELETON (dbus_object), dbus_interface);
	g_object_unref (dbus_object);

	if (dbus_interface != NULL)
		g_object_unref (dbus_interface);

	g_object_notify (G_OBJECT (source), "remote-creatable");
}

/**
 * e_server_side_source_set_remote_deletable:
 * @source: an #EServerSideSource
 * @remote_deletable: whether to export the RemoteDeletable interface
 *
 * Indicates whether @source can be used to delete resources on a remote
 * server.  Typically this is only set to %TRUE for sources created by an
 * #ECollectionBackend to represent a remote resource.
 *
 * If %TRUE, the RemoteDeletable D-Bus interface is exported at the object
 * path for @source.  If %FALSE, the RemoteDeletable D-Bus interface is
 * unexported at the object path for @source, and any attempt by clients
 * to call e_source_remote_delete() will fail.
 *
 * Unlike the #ESource:removable and #ESource:writable properties, this
 * is enforced for both clients of the registry D-Bus server and within
 * the registry D-Bus service itself.
 *
 * Since: 3.6
 **/
void
e_server_side_source_set_remote_deletable (EServerSideSource *source,
                                           gboolean remote_deletable)
{
	EDBusSourceRemoteDeletable *dbus_interface = NULL;
	GDBusObject *dbus_object;
	gboolean currently_remote_deletable;

	g_return_if_fail (E_IS_SERVER_SIDE_SOURCE (source));

	currently_remote_deletable =
		e_source_get_remote_deletable (E_SOURCE (source));

	if (remote_deletable == currently_remote_deletable)
		return;

	if (remote_deletable) {
		dbus_interface =
			e_dbus_source_remote_deletable_skeleton_new ();

		g_signal_connect (
			dbus_interface, "handle-delete",
			G_CALLBACK (server_side_source_remote_delete_cb),
			source);
	}

	dbus_object = e_source_ref_dbus_object (E_SOURCE (source));
	e_dbus_object_skeleton_set_source_remote_deletable (
		E_DBUS_OBJECT_SKELETON (dbus_object), dbus_interface);
	g_object_unref (dbus_object);

	if (dbus_interface != NULL)
		g_object_unref (dbus_interface);

	g_object_notify (G_OBJECT (source), "remote-deletable");
}

/**
 * e_server_side_source_ref_oauth2_support:
 * @source: an #EServerSideSource
 *
 * Returns the object implementing the #EOAuth2SupportInterface,
 * or %NULL if @source does not support OAuth 2.0 authentication.
 *
 * The returned #EOAuth2Support object is referenced for thread-safety.
 * Unreference the object with g_object_unref() when finished with it.
 *
 * Returns: an #EOAuth2Support object, or %NULL
 *
 * Since: 3.8
 **/
EOAuth2Support *
e_server_side_source_ref_oauth2_support (EServerSideSource *source)
{
	g_return_val_if_fail (E_IS_SERVER_SIDE_SOURCE (source), NULL);

	return g_weak_ref_get (&source->priv->oauth2_support);
}

/**
 * e_server_side_source_set_oauth2_support:
 * @source: an #EServerSideSource
 * @oauth2_support: an #EOAuth2Support object, or %NULL
 *
 * Indicates whether @source supports OAuth 2.0 authentication.
 *
 * If @oauth2_support is non-%NULL, the OAuth2Support D-Bus interface is
 * exported at the object path for @source.  If @oauth2_support is %NULL,
 * the OAuth2Support D-Bus interface is unexported at the object path for
 * @source, and any attempt by clients to call
 * e_source_get_oauth2_access_token() will fail.
 *
 * Requests for OAuth 2.0 access tokens are forwarded to @oauth2_support,
 * which implements the #EOAuth2SupportInterface.
 *
 * Since: 3.8
 **/
void
e_server_side_source_set_oauth2_support (EServerSideSource *source,
                                         EOAuth2Support *oauth2_support)
{
	EDBusSourceOAuth2Support *dbus_interface = NULL;
	GDBusObject *dbus_object;

	g_return_if_fail (E_IS_SERVER_SIDE_SOURCE (source));

	if (oauth2_support != NULL) {
		g_return_if_fail (E_IS_OAUTH2_SUPPORT (oauth2_support));

		dbus_interface =
			e_dbus_source_oauth2_support_skeleton_new ();

		g_signal_connect (
			dbus_interface, "handle-get-access-token",
			G_CALLBACK (server_side_source_get_access_token_cb),
			source);
	}

	g_weak_ref_set (&source->priv->oauth2_support, oauth2_support);

	dbus_object = e_source_ref_dbus_object (E_SOURCE (source));
	e_dbus_object_skeleton_set_source_oauth2_support (
		E_DBUS_OBJECT_SKELETON (dbus_object), dbus_interface);
	g_object_unref (dbus_object);

	if (dbus_interface != NULL)
		g_object_unref (dbus_interface);

	g_object_notify (G_OBJECT (source), "oauth2-support");
}

