/*
* e-caldav-chooser.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 <webcal://www.gnu.org/licenses/>
*
*/
#include "e-caldav-chooser.h"
#include <config.h>
#include <glib/gi18n-lib.h>
#include <libsoup/soup.h>
#include <libsoup/soup-gnome.h>
#include <libxml/tree.h>
#include <libxml/xpath.h>
#include <libxml/xpathInternals.h>
#include <e-util/e-util.h>
#define E_CALDAV_CHOOSER_GET_PRIVATE(obj) \
(G_TYPE_INSTANCE_GET_PRIVATE \
((obj), E_TYPE_CALDAV_CHOOSER, ECaldavChooserPrivate))
#define XC(string) ((xmlChar *) string)
/* Standard Namespaces */
#define NS_WEBDAV "DAV:"
#define NS_CALDAV "urn:ietf:params:xml:ns:caldav"
/* Application-Specific Namespaces */
#define NS_CALSRV "http://calendarserver.org/ns/"
#define NS_ICAL "http://apple.com/ns/ical/"
typedef struct _Context Context;
struct _ECaldavChooserPrivate {
ESourceRegistry *registry;
ESource *source;
ECalClientSourceType source_type;
SoupSession *session;
GList *user_address_set;
gchar *password;
};
struct _Context {
SoupSession *session;
GCancellable *cancellable;
gulong cancelled_handler_id;
GList *user_address_set;
};
enum {
PROP_0,
PROP_REGISTRY,
PROP_SOURCE,
PROP_SOURCE_TYPE
};
/* Mainly for readability. */
enum {
DEPTH_0,
DEPTH_1
};
typedef enum {
SUPPORTS_VEVENT = 1 << 0,
SUPPORTS_VTODO = 1 << 1,
SUPPORTS_VJOURNAL = 1 << 2,
SUPPORTS_ALL = 0x7
} SupportedComponentSet;
enum {
COLUMN_DISPLAY_NAME, /* G_TYPE_STRING */
COLUMN_PATH_ENCODED, /* G_TYPE_STRING */
COLUMN_PATH_DECODED, /* G_TYPE_STRING */
COLUMN_COLOR, /* GDK_TYPE_COLOR */
COLUMN_HAS_COLOR, /* G_TYPE_BOOLEAN */
NUM_COLUMNS
};
/* Forward Declarations */
static void e_caldav_chooser_authenticator_init
(ESourceAuthenticatorInterface *interface);
static void caldav_chooser_get_collection_details
(SoupSession *session,
SoupMessage *message,
const gchar *path_or_uri,
GSimpleAsyncResult *simple);
G_DEFINE_DYNAMIC_TYPE_EXTENDED (
ECaldavChooser,
e_caldav_chooser,
GTK_TYPE_TREE_VIEW,
0,
G_IMPLEMENT_INTERFACE_DYNAMIC (
E_TYPE_SOURCE_AUTHENTICATOR,
e_caldav_chooser_authenticator_init))
static gconstpointer
compat_libxml_output_buffer_get_content (xmlOutputBufferPtr buf,
gsize *out_len)
{
#ifdef LIBXML2_NEW_BUFFER
*out_len = xmlOutputBufferGetSize (buf);
return xmlOutputBufferGetContent (buf);
#else
*out_len = buf->buffer->use;
return buf->buffer->content;
#endif
}
static void
context_cancel_message (GCancellable *cancellable,
Context *context)
{
soup_session_abort (context->session);
}
static Context *
context_new (ECaldavChooser *chooser,
GCancellable *cancellable)
{
Context *context;
context = g_slice_new0 (Context);
context->session = g_object_ref (chooser->priv->session);
if (cancellable != NULL) {
context->cancellable = g_object_ref (cancellable);
context->cancelled_handler_id = g_cancellable_connect (
context->cancellable,
G_CALLBACK (context_cancel_message),
context, (GDestroyNotify) NULL);
}
return context;
}
static void
context_free (Context *context)
{
if (context->session != NULL)
g_object_unref (context->session);
if (context->cancellable != NULL) {
g_cancellable_disconnect (
context->cancellable,
context->cancelled_handler_id);
g_object_unref (context->cancellable);
}
g_list_free_full (
context->user_address_set,
(GDestroyNotify) g_free);
g_slice_free (Context, context);
}
static void
caldav_chooser_redirect (SoupMessage *message,
SoupSession *session)
{
SoupURI *soup_uri;
const gchar *location;
if (!SOUP_STATUS_IS_REDIRECTION (message->status_code))
return;
location = soup_message_headers_get_list (
message->response_headers, "Location");
if (location == NULL)
return;
soup_uri = soup_uri_new_with_base (
soup_message_get_uri (message), location);
if (soup_uri == NULL) {
soup_message_set_status_full (
message, SOUP_STATUS_MALFORMED,
"Invalid Redirect URL");
return;
}
soup_message_set_uri (message, soup_uri);
soup_session_requeue_message (session, message);
soup_uri_free (soup_uri);
}
static G_GNUC_NULL_TERMINATED SoupMessage *
caldav_chooser_new_propfind (SoupSession *session,
SoupURI *soup_uri,
gint depth,
...)
{
GHashTable *namespaces;
SoupMessage *message;
xmlDocPtr doc;
xmlNodePtr root;
xmlNodePtr node;
xmlNsPtr ns;
xmlOutputBufferPtr output;
gconstpointer content;
gsize length;
gpointer key;
va_list va;
/* Construct the XML content. */
doc = xmlNewDoc (XC ("1.0"));
node = xmlNewDocNode (doc, NULL, XC ("propfind"), NULL);
/* Build a hash table of namespace URIs to xmlNs structs. */
namespaces = g_hash_table_new (NULL, NULL);
ns = xmlNewNs (node, XC (NS_CALDAV), XC ("C"));
g_hash_table_insert (namespaces, (gpointer) NS_CALDAV, ns);
ns = xmlNewNs (node, XC (NS_CALSRV), XC ("CS"));
g_hash_table_insert (namespaces, (gpointer) NS_CALSRV, ns);
ns = xmlNewNs (node, XC (NS_ICAL), XC ("IC"));
g_hash_table_insert (namespaces, (gpointer) NS_ICAL, ns);
/* Add WebDAV last since we use it below. */
ns = xmlNewNs (node, XC (NS_WEBDAV), XC ("D"));
g_hash_table_insert (namespaces, (gpointer) NS_WEBDAV, ns);
xmlSetNs (node, ns);
xmlDocSetRootElement (doc, node);
node = xmlNewTextChild (node, ns, XC ("prop"), NULL);
va_start (va, depth);
while ((key = va_arg (va, gpointer)) != NULL) {
xmlChar *name;
ns = g_hash_table_lookup (namespaces, key);
name = va_arg (va, xmlChar *);
if (ns != NULL && name != NULL)
xmlNewTextChild (node, ns, name, NULL);
else
g_warn_if_reached ();
}
va_end (va);
g_hash_table_destroy (namespaces);
/* Construct the SoupMessage. */
message = soup_message_new_from_uri (SOUP_METHOD_PROPFIND, soup_uri);
soup_message_set_flags (message, SOUP_MESSAGE_NO_REDIRECT);
soup_message_headers_append (
message->request_headers,
"User-Agent", "Evolution/" VERSION);
soup_message_headers_append (
message->request_headers,
"Depth", (depth == 0) ? "0" : "1");
output = xmlAllocOutputBuffer (NULL);
root = xmlDocGetRootElement (doc);
xmlNodeDumpOutput (output, doc, root, 0, 1, NULL);
xmlOutputBufferFlush (output);
content = compat_libxml_output_buffer_get_content (output, &length);
soup_message_set_request (
message, "application/xml", SOUP_MEMORY_COPY,
content, length);
xmlOutputBufferClose (output);
soup_message_add_header_handler (
message, "got-body", "Location",
G_CALLBACK (caldav_chooser_redirect), session);
return message;
}
static void
caldav_chooser_authenticate_cb (SoupSession *session,
SoupMessage *message,
SoupAuth *auth,
gboolean retrying,
ECaldavChooser *chooser)
{
ESource *source;
ESourceAuthentication *extension;
const gchar *extension_name;
const gchar *username;
const gchar *password;
source = e_caldav_chooser_get_source (chooser);
extension_name = E_SOURCE_EXTENSION_AUTHENTICATION;
extension = e_source_get_extension (source, extension_name);
username = e_source_authentication_get_user (extension);
password = chooser->priv->password;
/* If our password was rejected, let the operation fail. */
if (retrying)
return;
/* If we don't have a username, let the operation fail. */
if (username == NULL || *username == '\0')
return;
/* If we don't have a password, let the operation fail. */
if (password == NULL || *password == '\0')
return;
soup_auth_authenticate (auth, username, password);
}
static void
caldav_chooser_configure_session (ECaldavChooser *chooser,
SoupSession *session)
{
ESource *source;
ESourceWebdav *extension;
const gchar *extension_name;
source = e_caldav_chooser_get_source (chooser);
extension_name = E_SOURCE_EXTENSION_WEBDAV_BACKEND;
extension = e_source_get_extension (source, extension_name);
g_object_bind_property (
extension, "ignore-invalid-cert",
session, SOUP_SESSION_SSL_USE_SYSTEM_CA_FILE,
G_BINDING_SYNC_CREATE |
G_BINDING_INVERT_BOOLEAN);
if (g_getenv ("CALDAV_DEBUG") != NULL) {
SoupLogger *logger;
logger = soup_logger_new (
SOUP_LOGGER_LOG_BODY, 100 * 1024 * 1024);
soup_session_add_feature (
session, SOUP_SESSION_FEATURE (logger));
g_object_unref (logger);
}
g_object_set (session, SOUP_SESSION_TIMEOUT, 90, NULL);
/* This adds proxy support. */
soup_session_add_feature_by_type (
session, SOUP_TYPE_GNOME_FEATURES_2_26);
g_signal_connect (
session, "authenticate",
G_CALLBACK (caldav_chooser_authenticate_cb), chooser);
}
static gboolean
caldav_chooser_check_successful (SoupMessage *message,
GError **error)
{
GIOErrorEnum error_code;
/* Loosely copied from the GVFS DAV backend. */
if (SOUP_STATUS_IS_SUCCESSFUL (message->status_code))
return TRUE;
switch (message->status_code) {
case SOUP_STATUS_CANCELLED:
error_code = G_IO_ERROR_CANCELLED;
break;
case SOUP_STATUS_NOT_FOUND:
error_code = G_IO_ERROR_NOT_FOUND;
break;
case SOUP_STATUS_UNAUTHORIZED:
case SOUP_STATUS_PAYMENT_REQUIRED:
case SOUP_STATUS_FORBIDDEN:
error_code = G_IO_ERROR_PERMISSION_DENIED;
break;
case SOUP_STATUS_REQUEST_TIMEOUT:
error_code = G_IO_ERROR_TIMED_OUT;
break;
case SOUP_STATUS_CANT_RESOLVE:
error_code = G_IO_ERROR_HOST_NOT_FOUND;
break;
case SOUP_STATUS_NOT_IMPLEMENTED:
error_code = G_IO_ERROR_NOT_SUPPORTED;
break;
case SOUP_STATUS_INSUFFICIENT_STORAGE:
error_code = G_IO_ERROR_NO_SPACE;
break;
default:
error_code = G_IO_ERROR_FAILED;
break;
}
g_set_error (
error, G_IO_ERROR, error_code,
_("HTTP Error: %s"), message->reason_phrase);
return FALSE;
}
static xmlDocPtr
caldav_chooser_parse_xml (SoupMessage *message,
const gchar *expected_name,
GError **error)
{
xmlDocPtr doc;
xmlNodePtr root;
if (!caldav_chooser_check_successful (message, error))
return NULL;
doc = xmlReadMemory (
message->response_body->data,
message->response_body->length,
"response.xml", NULL,
XML_PARSE_NONET |
XML_PARSE_NOWARNING |
XML_PARSE_NOCDATA |
XML_PARSE_COMPACT);
if (doc == NULL) {
g_set_error_literal (
error, G_IO_ERROR, G_IO_ERROR_FAILED,
_("Could not parse response"));
return NULL;
}
root = xmlDocGetRootElement (doc);
if (root == NULL || root->children == NULL) {
g_set_error_literal (
error, G_IO_ERROR, G_IO_ERROR_FAILED,
_("Empty response"));
xmlFreeDoc (doc);
return NULL;
}
if (g_strcmp0 ((gchar *) root->name, expected_name) != 0) {
g_set_error_literal (
error, G_IO_ERROR, G_IO_ERROR_FAILED,
_("Unexpected reply from server"));
xmlFreeDoc (doc);
return NULL;
}
return doc;
}
static xmlXPathObjectPtr
caldav_chooser_get_xpath (xmlXPathContextPtr xp_ctx,
const gchar *path_format,
...)
{
xmlXPathObjectPtr xp_obj;
va_list va;
gchar *path;
va_start (va, path_format);
path = g_strdup_vprintf (path_format, va);
va_end (va);
xp_obj = xmlXPathEvalExpression (XC (path), xp_ctx);
g_free (path);
if (xp_obj == NULL)
return NULL;
if (xp_obj->type != XPATH_NODESET) {
xmlXPathFreeObject (xp_obj);
return NULL;
}
if (xmlXPathNodeSetGetLength (xp_obj->nodesetval) == 0) {
xmlXPathFreeObject (xp_obj);
return NULL;
}
return xp_obj;
}
static gchar *
caldav_chooser_get_xpath_string (xmlXPathContextPtr xp_ctx,
const gchar *path_format,
...)
{
xmlXPathObjectPtr xp_obj;
va_list va;
gchar *path;
gchar *expression;
gchar *string = NULL;
va_start (va, path_format);
path = g_strdup_vprintf (path_format, va);
va_end (va);
expression = g_strdup_printf ("string(%s)", path);
xp_obj = xmlXPathEvalExpression (XC (expression), xp_ctx);
g_free (expression);
g_free (path);
if (xp_obj == NULL)
return NULL;
if (xp_obj->type == XPATH_STRING)
string = g_strdup ((gchar *) xp_obj->stringval);
/* If the string is empty, return NULL. */
if (string != NULL && *string == '\0') {
g_free (string);
string = NULL;
}
xmlXPathFreeObject (xp_obj);
return string;
}
static void
caldav_chooser_process_user_address_set (xmlXPathContextPtr xp_ctx,
Context *context)
{
xmlXPathObjectPtr xp_obj;
gint ii, length;
/* XXX Is response[1] safe to assume? */
xp_obj = caldav_chooser_get_xpath (
xp_ctx,
"/D:multistatus"
"/D:response"
"/D:propstat"
"/D:prop"
"/C:calendar-user-address-set");
if (xp_obj == NULL)
return;
length = xmlXPathNodeSetGetLength (xp_obj->nodesetval);
for (ii = 0; ii < length; ii++) {
GList *duplicate;
const gchar *address;
gchar *href;
href = caldav_chooser_get_xpath_string (
xp_ctx,
"/D:multistatus"
"/D:response"
"/D:propstat"
"/D:prop"
"/C:calendar-user-address-set"
"/D:href[%d]", ii + 1);
if (href == NULL)
continue;
if (!g_str_has_prefix (href, "mailto:")) {
g_free (href);
continue;
}
/* strlen("mailto:") == 7 */
address = href + 7;
/* Avoid duplicates. */
duplicate = g_list_find_custom (
context->user_address_set,
address, (GCompareFunc) strdup);
if (duplicate != NULL) {
g_free (href);
continue;
}
context->user_address_set = g_list_append (
context->user_address_set, g_strdup (address));
g_free (href);
}
xmlXPathFreeObject (xp_obj);
}
static SupportedComponentSet
caldav_chooser_get_supported_component_set (xmlXPathContextPtr xp_ctx,
gint index)
{
xmlXPathObjectPtr xp_obj;
SupportedComponentSet set = 0;
gint ii, length;
xp_obj = caldav_chooser_get_xpath (
xp_ctx,
"/D:multistatus"
"/D:response[%d]"
"/D:propstat"
"/D:prop"
"/C:supported-calendar-component-set"
"/C:comp", index);
/* If the property is not present, assume all component
* types are supported. (RFC 4791, Section 5.2.3) */
if (xp_obj == NULL)
return SUPPORTS_ALL;
length = xmlXPathNodeSetGetLength (xp_obj->nodesetval);
for (ii = 0; ii < length; ii++) {
gchar *name;
name = caldav_chooser_get_xpath_string (
xp_ctx,
"/D:multistatus"
"/D:response[%d]"
"/D:propstat"
"/D:prop"
"/C:supported-calendar-component-set"
"/C:comp[%d]"
"/@name", index, ii + 1);
if (name == NULL)
continue;
if (g_ascii_strcasecmp (name, "VEVENT"))
set |= SUPPORTS_VEVENT;
else if (g_ascii_strcasecmp (name, "VTODO"))
set |= SUPPORTS_VTODO;
else if (g_ascii_strcasecmp (name, "VJOURNAL"))
set |= SUPPORTS_VJOURNAL;
g_free (name);
}
xmlXPathFreeObject (xp_obj);
return set;
}
static void
caldav_chooser_process_response (SoupSession *session,
SoupMessage *message,
GSimpleAsyncResult *simple,
xmlXPathContextPtr xp_ctx,
gint index)
{
GObject *object;
xmlXPathObjectPtr xp_obj;
SupportedComponentSet comp_set;
ECaldavChooser *chooser;
GtkTreeModel *tree_model;
GtkTreeIter iter;
GdkColor color;
gchar *color_spec;
gchar *display_name;
gchar *href_decoded;
gchar *href_encoded;
gchar *status_line;
guint status;
gboolean has_color;
gboolean success;
/* This returns a new reference, for reasons passing understanding. */
object = g_async_result_get_source_object (G_ASYNC_RESULT (simple));
chooser = E_CALDAV_CHOOSER (object);
tree_model = gtk_tree_view_get_model (GTK_TREE_VIEW (object));
g_object_unref (object);
status_line = caldav_chooser_get_xpath_string (
xp_ctx,
"/D:multistatus"
"/D:response[%d]"
"/D:propstat"
"/D:status",
index);
if (status_line == NULL)
return;
success = soup_headers_parse_status_line (
status_line, NULL, &status, NULL);
g_free (status_line);
if (!success || status != SOUP_STATUS_OK)
return;
href_encoded = caldav_chooser_get_xpath_string (
xp_ctx,
"/D:multistatus"
"/D:response[%d]"
"/D:href",
index);
if (href_encoded == NULL)
return;
href_decoded = soup_uri_decode (href_encoded);
/* Get the display name or fall back to the href. */
display_name = caldav_chooser_get_xpath_string (
xp_ctx,
"/D:multistatus"
"/D:response[%d]"
"/D:propstat"
"/D:prop"
"/D:displayname",
index);
if (display_name == NULL) {
gchar *href_copy, *cp;
href_copy = g_strdup (href_decoded);
/* Use the last non-empty path segment. */
while ((cp = strrchr (href_copy, '/')) != NULL) {
if (*(cp + 1) == '\0')
*cp = '\0';
else {
display_name = g_strdup (cp + 1);
break;
}
}
g_free (href_copy);
}
/* Make sure the resource is a calendar. */
xp_obj = caldav_chooser_get_xpath (
xp_ctx,
"/D:multistatus"
"/D:response[%d]"
"/D:propstat"
"/D:prop"
"/D:resourcetype"
"/C:calendar",
index);
if (xp_obj == NULL)
goto exit;
xmlXPathFreeObject (xp_obj);
/* Get the color specification string. */
color_spec = caldav_chooser_get_xpath_string (
xp_ctx,
"/D:multistatus"
"/D:response[%d]"
"/D:propstat"
"/D:prop"
"/IC:calendar-color",
index);
if (color_spec != NULL)
has_color = gdk_color_parse (color_spec, &color);
else
has_color = FALSE;
g_free (color_spec);
/* Which calendar component types are supported? */
comp_set = caldav_chooser_get_supported_component_set (xp_ctx, index);
switch (e_caldav_chooser_get_source_type (chooser)) {
case E_CAL_CLIENT_SOURCE_TYPE_EVENTS:
if ((comp_set & SUPPORTS_VEVENT) == 0)
goto exit;
break;
case E_CAL_CLIENT_SOURCE_TYPE_MEMOS:
if ((comp_set & SUPPORTS_VJOURNAL) == 0)
goto exit;
break;
case E_CAL_CLIENT_SOURCE_TYPE_TASKS:
if ((comp_set & SUPPORTS_VTODO) == 0)
goto exit;
break;
default:
goto exit;
}
/* Append a new tree model row. */
gtk_list_store_append (GTK_LIST_STORE (tree_model), &iter);
gtk_list_store_set (
GTK_LIST_STORE (tree_model), &iter,
COLUMN_DISPLAY_NAME, display_name,
COLUMN_PATH_ENCODED, href_encoded,
COLUMN_PATH_DECODED, href_decoded,
COLUMN_COLOR, has_color ? &color : NULL,
COLUMN_HAS_COLOR, has_color,
-1);
exit:
g_free (display_name);
g_free (href_decoded);
g_free (href_encoded);
}
static void
caldav_chooser_collection_details_cb (SoupSession *session,
SoupMessage *message,
GSimpleAsyncResult *simple)
{
xmlDocPtr doc;
xmlXPathContextPtr xp_ctx;
xmlXPathObjectPtr xp_obj;
GError *error = NULL;
doc = caldav_chooser_parse_xml (message, "multistatus", &error);
if (error != NULL) {
g_warn_if_fail (doc == NULL);
g_simple_async_result_set_from_error (simple, error);
g_error_free (error);
goto exit;
}
xp_ctx = xmlXPathNewContext (doc);
xmlXPathRegisterNs (xp_ctx, XC ("D"), XC (NS_WEBDAV));
xmlXPathRegisterNs (xp_ctx, XC ("C"), XC (NS_CALDAV));
xmlXPathRegisterNs (xp_ctx, XC ("CS"), XC (NS_CALSRV));
xmlXPathRegisterNs (xp_ctx, XC ("IC"), XC (NS_ICAL));
xp_obj = caldav_chooser_get_xpath (
xp_ctx,
"/D:multistatus"
"/D:response");
if (xp_obj != NULL) {
gint length, ii;
length = xmlXPathNodeSetGetLength (xp_obj->nodesetval);
for (ii = 0; ii < length; ii++)
caldav_chooser_process_response (
session, message, simple, xp_ctx, ii + 1);
xmlXPathFreeObject (xp_obj);
}
xmlXPathFreeContext (xp_ctx);
xmlFreeDoc (doc);
exit:
/* If we were cancelled then we're in a GCancellable::cancelled
* signal handler right now and GCancellable has its mutex locked,
* which means calling g_cancellable_disconnect() now will deadlock
* when it too tries to acquire the mutex. So defer the GAsyncResult
* completion to an idle callback to avoid this deadlock. */
g_simple_async_result_complete_in_idle (simple);
g_object_unref (simple);
}
static void
caldav_chooser_get_collection_details (SoupSession *session,
SoupMessage *message,
const gchar *path_or_uri,
GSimpleAsyncResult *simple)
{
SoupURI *soup_uri;
soup_uri = soup_uri_new (path_or_uri);
if (!soup_uri ||
!soup_uri_get_scheme (soup_uri) ||
!soup_uri_get_host (soup_uri) ||
!soup_uri_get_path (soup_uri) ||
!*soup_uri_get_scheme (soup_uri) ||
!*soup_uri_get_host (soup_uri) ||
!*soup_uri_get_path (soup_uri)) {
/* it's a path only, not full uri */
if (soup_uri)
soup_uri_free (soup_uri);
soup_uri = soup_uri_copy (soup_message_get_uri (message));
soup_uri_set_path (soup_uri, path_or_uri);
}
message = caldav_chooser_new_propfind (
session, soup_uri, DEPTH_1,
NS_WEBDAV, XC ("displayname"),
NS_WEBDAV, XC ("resourcetype"),
NS_CALDAV, XC ("calendar-description"),
NS_CALDAV, XC ("supported-calendar-component-set"),
NS_CALDAV, XC ("calendar-user-address-set"),
NS_CALSRV, XC ("getctag"),
NS_ICAL, XC ("calendar-color"),
NULL);
/* This takes ownership of the message. */
soup_session_queue_message (
session, message, (SoupSessionCallback)
caldav_chooser_collection_details_cb, simple);
soup_uri_free (soup_uri);
}
static void
caldav_chooser_calendar_home_set_cb (SoupSession *session,
SoupMessage *message,
GSimpleAsyncResult *simple)
{
Context *context;
SoupURI *soup_uri;
xmlDocPtr doc;
xmlXPathContextPtr xp_ctx;
xmlXPathObjectPtr xp_obj;
gchar *calendar_home_set;
GError *error = NULL;
context = g_simple_async_result_get_op_res_gpointer (simple);
doc = caldav_chooser_parse_xml (message, "multistatus", &error);
/* If we were cancelled then we're in a GCancellable::cancelled
* signal handler right now and GCancellable has its mutex locked,
* which means calling g_cancellable_disconnect() now will deadlock
* when it too tries to acquire the mutex. So defer the GAsyncResult
* completion to an idle callback to avoid this deadlock. */
if (error != NULL) {
g_simple_async_result_set_from_error (simple, error);
g_simple_async_result_complete_in_idle (simple);
g_object_unref (simple);
g_error_free (error);
return;
}
g_return_if_fail (doc != NULL);
xp_ctx = xmlXPathNewContext (doc);
xmlXPathRegisterNs (xp_ctx, XC ("D"), XC (NS_WEBDAV));
xmlXPathRegisterNs (xp_ctx, XC ("C"), XC (NS_CALDAV));
/* Record any "C:calendar-user-address-set" properties. */
caldav_chooser_process_user_address_set (xp_ctx, context);
/* Try to find the calendar home URL using the
* following properties in order of preference:
*
* "C:calendar-home-set"
* "D:current-user-principal"
* "D:principal-URL"
*
* If the second or third URL preference is used, rerun
* the PROPFIND method on that URL at Depth=1 in hopes
* of getting a proper "C:calendar-home-set" property.
*/
/* FIXME There can be multiple "D:href" elements for a
* "C:calendar-home-set". We're only processing
* the first one. Need to iterate over them. */
calendar_home_set = caldav_chooser_get_xpath_string (
xp_ctx,
"/D:multistatus"
"/D:response"
"/D:propstat"
"/D:prop"
"/C:calendar-home-set"
"/D:href");
if (calendar_home_set != NULL)
goto get_collection_details;
g_free (calendar_home_set);
calendar_home_set = caldav_chooser_get_xpath_string (
xp_ctx,
"/D:multistatus"
"/D:response"
"/D:propstat"
"/D:prop"
"/D:current-user-principal"
"/D:href");
if (calendar_home_set != NULL)
goto retry_propfind;
g_free (calendar_home_set);
calendar_home_set = caldav_chooser_get_xpath_string (
xp_ctx,
"/D:multistatus"
"/D:response"
"/D:propstat"
"/D:prop"
"/D:principal-URL"
"/D:href");
if (calendar_home_set != NULL)
goto retry_propfind;
g_free (calendar_home_set);
calendar_home_set = NULL;
/* None of the aforementioned properties are present. If the
* user-supplied CalDAV URL is a calendar resource, use that. */
xp_obj = caldav_chooser_get_xpath (
xp_ctx,
"/D:multistatus"
"/D:response"
"/D:propstat"
"/D:prop"
"/D:resourcetype"
"/C:calendar");
if (xp_obj != NULL) {
soup_uri = soup_message_get_uri (message);
if (soup_uri->path != NULL && *soup_uri->path != '\0') {
gchar *slash;
soup_uri = soup_uri_copy (soup_uri);
slash = strrchr (soup_uri->path, '/');
while (slash != NULL && slash != soup_uri->path) {
if (slash[1] != '\0') {
slash[1] = '\0';
calendar_home_set =
g_strdup (soup_uri->path);
break;
}
slash[0] = '\0';
slash = strrchr (soup_uri->path, '/');
}
soup_uri_free (soup_uri);
}
xmlXPathFreeObject (xp_obj);
}
if (calendar_home_set == NULL || *calendar_home_set == '\0') {
g_free (calendar_home_set);
g_simple_async_result_set_error (
simple, G_IO_ERROR, G_IO_ERROR_FAILED,
_("Could not locate user's calendars"));
g_simple_async_result_complete (simple);
g_object_unref (simple);
return;
}
get_collection_details:
xmlXPathFreeContext (xp_ctx);
xmlFreeDoc (doc);
caldav_chooser_get_collection_details (
session, message, calendar_home_set, simple);
g_free (calendar_home_set);
return;
retry_propfind:
xmlXPathFreeContext (xp_ctx);
xmlFreeDoc (doc);
soup_uri = soup_uri_copy (soup_message_get_uri (message));
soup_uri_set_path (soup_uri, calendar_home_set);
/* Note that we omit "D:resourcetype", "D:current-user-principal"
* and "D:principal-URL" in order to short-circuit the recursion. */
message = caldav_chooser_new_propfind (
session, soup_uri, DEPTH_1,
NS_CALDAV, XC ("calendar-home-set"),
NS_CALDAV, XC ("calendar-user-address-set"),
NULL);
/* This takes ownership of the message. */
soup_session_queue_message (
session, message, (SoupSessionCallback)
caldav_chooser_calendar_home_set_cb, simple);
soup_uri_free (soup_uri);
g_free (calendar_home_set);
}
static void
caldav_chooser_set_registry (ECaldavChooser *chooser,
ESourceRegistry *registry)
{
g_return_if_fail (E_IS_SOURCE_REGISTRY (registry));
g_return_if_fail (chooser->priv->registry == NULL);
chooser->priv->registry = g_object_ref (registry);
}
static void
caldav_chooser_set_source (ECaldavChooser *chooser,
ESource *source)
{
g_return_if_fail (E_IS_SOURCE (source));
g_return_if_fail (chooser->priv->source == NULL);
chooser->priv->source = g_object_ref (source);
}
static void
caldav_chooser_set_source_type (ECaldavChooser *chooser,
ECalClientSourceType source_type)
{
chooser->priv->source_type = source_type;
}
static void
caldav_chooser_set_property (GObject *object,
guint property_id,
const GValue *value,
GParamSpec *pspec)
{
switch (property_id) {
case PROP_REGISTRY:
caldav_chooser_set_registry (
E_CALDAV_CHOOSER (object),
g_value_get_object (value));
return;
case PROP_SOURCE:
caldav_chooser_set_source (
E_CALDAV_CHOOSER (object),
g_value_get_object (value));
return;
case PROP_SOURCE_TYPE:
caldav_chooser_set_source_type (
E_CALDAV_CHOOSER (object),
g_value_get_enum (value));
return;
}
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}
static void
caldav_chooser_get_property (GObject *object,
guint property_id,
GValue *value,
GParamSpec *pspec)
{
switch (property_id) {
case PROP_REGISTRY:
g_value_set_object (
value, e_caldav_chooser_get_registry (
E_CALDAV_CHOOSER (object)));
return;
case PROP_SOURCE:
g_value_set_object (
value, e_caldav_chooser_get_source (
E_CALDAV_CHOOSER (object)));
return;
case PROP_SOURCE_TYPE:
g_value_set_enum (
value, e_caldav_chooser_get_source_type (
E_CALDAV_CHOOSER (object)));
return;
}
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}
static void
caldav_chooser_dispose (GObject *object)
{
ECaldavChooserPrivate *priv;
priv = E_CALDAV_CHOOSER_GET_PRIVATE (object);
if (priv->registry != NULL) {
g_object_unref (priv->registry);
priv->registry = NULL;
}
if (priv->source != NULL) {
g_object_unref (priv->source);
priv->source = NULL;
}
if (priv->session != NULL) {
g_object_unref (priv->session);
priv->session = NULL;
}
/* Chain up to parent's dispose() method. */
G_OBJECT_CLASS (e_caldav_chooser_parent_class)->dispose (object);
}
static void
caldav_chooser_finalize (GObject *object)
{
ECaldavChooserPrivate *priv;
priv = E_CALDAV_CHOOSER_GET_PRIVATE (object);
g_list_free_full (
priv->user_address_set,
(GDestroyNotify) g_free);
g_free (priv->password);
/* Chain up to parent's finalize() method. */
G_OBJECT_CLASS (e_caldav_chooser_parent_class)->finalize (object);
}
static void
caldav_chooser_constructed (GObject *object)
{
ECaldavChooser *chooser;
GtkTreeView *tree_view;
GtkListStore *list_store;
GtkCellRenderer *renderer;
GtkTreeViewColumn *column;
SoupSession *session;
/* Chain up to parent's constructed() method. */
G_OBJECT_CLASS (e_caldav_chooser_parent_class)->constructed (object);
chooser = E_CALDAV_CHOOSER (object);
session = soup_session_async_new ();
caldav_chooser_configure_session (chooser, session);
chooser->priv->session = session;
tree_view = GTK_TREE_VIEW (object);
list_store = gtk_list_store_new (
NUM_COLUMNS,
G_TYPE_STRING, /* COLUMN_DISPLAY_NAME */
G_TYPE_STRING, /* COLUMN_PATH_ENCODED */
G_TYPE_STRING, /* COLUMN_PATH_DECODED */
GDK_TYPE_COLOR, /* COLUMN_COLOR */
G_TYPE_BOOLEAN); /* COLUMN_HAS_COLOR */
gtk_tree_view_set_model (tree_view, GTK_TREE_MODEL (list_store));
column = gtk_tree_view_column_new ();
gtk_tree_view_column_set_expand (column, TRUE);
gtk_tree_view_column_set_title (column, _("Name"));
gtk_tree_view_insert_column (tree_view, column, -1);
renderer = e_cell_renderer_color_new ();
gtk_tree_view_column_pack_start (column, renderer, FALSE);
gtk_tree_view_column_set_attributes (
column, renderer,
"color", COLUMN_COLOR,
"visible", COLUMN_HAS_COLOR,
NULL);
renderer = gtk_cell_renderer_text_new ();
gtk_tree_view_column_pack_start (column, renderer, TRUE);
gtk_tree_view_column_set_attributes (
column, renderer,
"text", COLUMN_DISPLAY_NAME,
NULL);
column = gtk_tree_view_column_new ();
gtk_tree_view_column_set_expand (column, FALSE);
gtk_tree_view_column_set_title (column, _("Path"));
gtk_tree_view_insert_column (tree_view, column, -1);
renderer = gtk_cell_renderer_text_new ();
gtk_tree_view_column_pack_start (column, renderer, TRUE);
gtk_tree_view_column_set_attributes (
column, renderer,
"text", COLUMN_PATH_DECODED,
NULL);
}
/* Helper for caldav_chooser_try_password_sync() */
static void
caldav_chooser_try_password_cancelled_cb (GCancellable *cancellable,
SoupSession *session)
{
soup_session_abort (session);
}
static ESourceAuthenticationResult
caldav_chooser_try_password_sync (ESourceAuthenticator *auth,
const GString *password,
GCancellable *cancellable,
GError **error)
{
ECaldavChooser *chooser;
ESourceAuthenticationResult result;
SoupMessage *message;
SoupSession *session;
SoupURI *soup_uri;
ESource *source;
ESourceWebdav *extension;
const gchar *extension_name;
gulong cancel_id = 0;
GError *local_error = NULL;
chooser = E_CALDAV_CHOOSER (auth);
/* Cache the password for later use in our
* SoupSession::authenticate signal handler. */
g_free (chooser->priv->password);
chooser->priv->password = g_strdup (password->str);
/* Create our own SoupSession so we
* can try the password synchronously. */
session = soup_session_sync_new ();
caldav_chooser_configure_session (chooser, session);
source = e_caldav_chooser_get_source (chooser);
extension_name = E_SOURCE_EXTENSION_WEBDAV_BACKEND;
extension = e_source_get_extension (source, extension_name);
soup_uri = e_source_webdav_dup_soup_uri (extension);
g_return_val_if_fail (soup_uri != NULL, E_SOURCE_AUTHENTICATION_ERROR);
/* Try some simple PROPFIND query. We don't care about the query
* result, only whether the CalDAV server will accept our password. */
message = caldav_chooser_new_propfind (
session, soup_uri, DEPTH_0,
NS_WEBDAV, XC ("resourcetype"),
NULL);
if (G_IS_CANCELLABLE (cancellable))
cancel_id = g_cancellable_connect (
cancellable,
G_CALLBACK (caldav_chooser_try_password_cancelled_cb),
g_object_ref (session),
(GDestroyNotify) g_object_unref);
soup_session_send_message (session, message);
if (cancel_id > 0)
g_cancellable_disconnect (cancellable, cancel_id);
if (caldav_chooser_check_successful (message, &local_error)) {
result = E_SOURCE_AUTHENTICATION_ACCEPTED;
} else if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED)) {
result = E_SOURCE_AUTHENTICATION_REJECTED;
g_clear_error (&local_error);
} else {
result = E_SOURCE_AUTHENTICATION_ERROR;
}
if (local_error != NULL)
g_propagate_error (error, local_error);
g_object_unref (message);
g_object_unref (session);
soup_uri_free (soup_uri);
return result;
}
static void
e_caldav_chooser_class_init (ECaldavChooserClass *class)
{
GObjectClass *object_class;
g_type_class_add_private (class, sizeof (ECaldavChooserPrivate));
object_class = G_OBJECT_CLASS (class);
object_class->set_property = caldav_chooser_set_property;
object_class->get_property = caldav_chooser_get_property;
object_class->dispose = caldav_chooser_dispose;
object_class->finalize = caldav_chooser_finalize;
object_class->constructed = caldav_chooser_constructed;
g_object_class_install_property (
object_class,
PROP_REGISTRY,
g_param_spec_object (
"registry",
"Registry",
"Data source registry",
E_TYPE_SOURCE_REGISTRY,
G_PARAM_READWRITE |
G_PARAM_CONSTRUCT_ONLY));
g_object_class_install_property (
object_class,
PROP_SOURCE,
g_param_spec_object (
"source",
"Source",
"CalDAV data source",
E_TYPE_SOURCE,
G_PARAM_READWRITE |
G_PARAM_CONSTRUCT_ONLY));
g_object_class_install_property (
object_class,
PROP_SOURCE_TYPE,
g_param_spec_enum (
"source-type",
"Source Type",
"The iCalendar object type",
E_TYPE_CAL_CLIENT_SOURCE_TYPE,
E_CAL_CLIENT_SOURCE_TYPE_EVENTS,
G_PARAM_READWRITE |
G_PARAM_CONSTRUCT_ONLY));
}
static void
e_caldav_chooser_class_finalize (ECaldavChooserClass *class)
{
}
static void
e_caldav_chooser_authenticator_init (ESourceAuthenticatorInterface *interface)
{
interface->try_password_sync = caldav_chooser_try_password_sync;
}
static void
e_caldav_chooser_init (ECaldavChooser *chooser)
{
chooser->priv = E_CALDAV_CHOOSER_GET_PRIVATE (chooser);
}
void
e_caldav_chooser_type_register (GTypeModule *type_module)
{
/* XXX G_DEFINE_DYNAMIC_TYPE declares a static type registration
* function, so we have to wrap it with a public function in
* order to register types from a separate compilation unit. */
e_caldav_chooser_register_type (type_module);
}
GtkWidget *
e_caldav_chooser_new (ESourceRegistry *registry,
ESource *source,
ECalClientSourceType source_type)
{
g_return_val_if_fail (E_IS_SOURCE_REGISTRY (registry), NULL);
g_return_val_if_fail (E_IS_SOURCE (source), NULL);
return g_object_new (
E_TYPE_CALDAV_CHOOSER,
"registry", registry, "source", source,
"source-type", source_type, NULL);
}
ESourceRegistry *
e_caldav_chooser_get_registry (ECaldavChooser *chooser)
{
g_return_val_if_fail (E_IS_CALDAV_CHOOSER (chooser), NULL);
return chooser->priv->registry;
}
ESource *
e_caldav_chooser_get_source (ECaldavChooser *chooser)
{
g_return_val_if_fail (E_IS_CALDAV_CHOOSER (chooser), NULL);
return chooser->priv->source;
}
ECalClientSourceType
e_caldav_chooser_get_source_type (ECaldavChooser *chooser)
{
g_return_val_if_fail (E_IS_CALDAV_CHOOSER (chooser), 0);
return chooser->priv->source_type;
}
void
e_caldav_chooser_populate (ECaldavChooser *chooser,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
Context *context;
ESource *source;
SoupURI *soup_uri;
SoupMessage *message;
ESourceWebdav *extension;
GtkTreeModel *tree_model;
GSimpleAsyncResult *simple;
const gchar *extension_name;
g_return_if_fail (E_IS_CALDAV_CHOOSER (chooser));
tree_model = gtk_tree_view_get_model (GTK_TREE_VIEW (chooser));
gtk_list_store_clear (GTK_LIST_STORE (tree_model));
soup_session_abort (chooser->priv->session);
source = e_caldav_chooser_get_source (chooser);
extension_name = E_SOURCE_EXTENSION_WEBDAV_BACKEND;
extension = e_source_get_extension (source, extension_name);
soup_uri = e_source_webdav_dup_soup_uri (extension);
g_return_if_fail (soup_uri != NULL);
context = context_new (chooser, cancellable);
simple = g_simple_async_result_new (
G_OBJECT (chooser), callback,
user_data, e_caldav_chooser_populate);
g_simple_async_result_set_op_res_gpointer (
simple, context, (GDestroyNotify) context_free);
message = caldav_chooser_new_propfind (
context->session, soup_uri, DEPTH_0,
NS_WEBDAV, XC ("resourcetype"),
NS_CALDAV, XC ("calendar-home-set"),
NS_CALDAV, XC ("calendar-user-address-set"),
NS_WEBDAV, XC ("current-user-principal"),
NS_WEBDAV, XC ("principal-URL"),
NULL);
/* This takes ownership of the message. */
soup_session_queue_message (
context->session, message, (SoupSessionCallback)
caldav_chooser_calendar_home_set_cb, simple);
soup_uri_free (soup_uri);
}
gboolean
e_caldav_chooser_populate_finish (ECaldavChooser *chooser,
GAsyncResult *result,
GError **error)
{
GSimpleAsyncResult *simple;
Context *context;
g_return_val_if_fail (
g_simple_async_result_is_valid (
result, G_OBJECT (chooser),
e_caldav_chooser_populate), FALSE);
simple = G_SIMPLE_ASYNC_RESULT (result);
context = g_simple_async_result_get_op_res_gpointer (simple);
if (g_simple_async_result_propagate_error (simple, error))
return FALSE;
/* Transfer user addresses to the private struct. */
g_list_free_full (
chooser->priv->user_address_set,
(GDestroyNotify) g_free);
chooser->priv->user_address_set = context->user_address_set;
context->user_address_set = NULL;
return TRUE;
}
gboolean
e_caldav_chooser_apply_selected (ECaldavChooser *chooser)
{
ESourceWebdav *webdav_extension;
GtkTreeSelection *selection;
GtkTreeModel *model;
GtkTreeIter iter;
ESource *source;
GdkColor *color;
gboolean has_color;
gchar *display_name;
gchar *path_encoded;
g_return_val_if_fail (E_IS_CALDAV_CHOOSER (chooser), FALSE);
source = e_caldav_chooser_get_source (chooser);
selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (chooser));
if (!gtk_tree_selection_get_selected (selection, &model, &iter))
return FALSE;
gtk_tree_model_get (
model, &iter,
COLUMN_DISPLAY_NAME, &display_name,
COLUMN_PATH_ENCODED, &path_encoded,
COLUMN_HAS_COLOR, &has_color,
COLUMN_COLOR, &color,
-1);
/* Sanity check. */
g_warn_if_fail (
(has_color && color != NULL) ||
(!has_color && color == NULL));
webdav_extension = e_source_get_extension (
source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
e_source_set_display_name (source, display_name);
e_source_webdav_set_display_name (webdav_extension, display_name);
e_source_webdav_set_resource_path (webdav_extension, path_encoded);
/* XXX For now just pick the first user address in the list.
* Might be better to compare the list against our own mail
* accounts and give preference to matches (especially if an
* address matches the default mail account), but I'm not sure
* if multiple user addresses are common enough to justify the
* extra effort. */
if (chooser->priv->user_address_set != NULL)
e_source_webdav_set_email_address (
webdav_extension,
chooser->priv->user_address_set->data);
if (has_color) {
ESourceSelectable *selectable_extension;
const gchar *extension_name;
gchar *color_spec;
switch (e_caldav_chooser_get_source_type (chooser)) {
case E_CAL_CLIENT_SOURCE_TYPE_EVENTS:
extension_name = E_SOURCE_EXTENSION_CALENDAR;
break;
case E_CAL_CLIENT_SOURCE_TYPE_MEMOS:
extension_name = E_SOURCE_EXTENSION_MEMO_LIST;
break;
case E_CAL_CLIENT_SOURCE_TYPE_TASKS:
extension_name = E_SOURCE_EXTENSION_TASK_LIST;
break;
default:
g_return_val_if_reached (TRUE);
}
selectable_extension =
e_source_get_extension (source, extension_name);
color_spec = gdk_color_to_string (color);
e_source_selectable_set_color (
selectable_extension, color_spec);
g_free (color_spec);
gdk_color_free (color);
}
g_free (display_name);
g_free (path_encoded);
return TRUE;
}