diff options
author | Matthew Barnes <mbarnes@redhat.com> | 2011-02-04 21:50:58 +0800 |
---|---|---|
committer | Matthew Barnes <mbarnes@redhat.com> | 2012-06-03 11:00:41 +0800 |
commit | 691ab73cd436d43883d7e3a2481f8ded9369af29 (patch) | |
tree | 1f214c45f93af597436b9fa078b5773359062f72 /modules/cal-config-caldav | |
parent | cb1220aff2c8c78246432229b875b7de6d44de84 (diff) | |
download | gsoc2013-evolution-691ab73cd436d43883d7e3a2481f8ded9369af29.tar gsoc2013-evolution-691ab73cd436d43883d7e3a2481f8ded9369af29.tar.gz gsoc2013-evolution-691ab73cd436d43883d7e3a2481f8ded9369af29.tar.bz2 gsoc2013-evolution-691ab73cd436d43883d7e3a2481f8ded9369af29.tar.lz gsoc2013-evolution-691ab73cd436d43883d7e3a2481f8ded9369af29.tar.xz gsoc2013-evolution-691ab73cd436d43883d7e3a2481f8ded9369af29.tar.zst gsoc2013-evolution-691ab73cd436d43883d7e3a2481f8ded9369af29.zip |
Add 'cal-config-caldav' module.
Registers the "CalDAV" backend in ECalSourceConfig widgets.
Replaces the 'caldav' plugin.
Diffstat (limited to 'modules/cal-config-caldav')
-rw-r--r-- | modules/cal-config-caldav/Makefile.am | 30 | ||||
-rw-r--r-- | modules/cal-config-caldav/e-caldav-chooser-dialog.c | 477 | ||||
-rw-r--r-- | modules/cal-config-caldav/e-caldav-chooser-dialog.h | 68 | ||||
-rw-r--r-- | modules/cal-config-caldav/e-caldav-chooser.c | 1643 | ||||
-rw-r--r-- | modules/cal-config-caldav/e-caldav-chooser.h | 81 | ||||
-rw-r--r-- | modules/cal-config-caldav/evolution-cal-config-caldav.c | 381 |
6 files changed, 2680 insertions, 0 deletions
diff --git a/modules/cal-config-caldav/Makefile.am b/modules/cal-config-caldav/Makefile.am new file mode 100644 index 0000000000..8c3d03125a --- /dev/null +++ b/modules/cal-config-caldav/Makefile.am @@ -0,0 +1,30 @@ +module_LTLIBRARIES = module-cal-config-caldav.la + +module_cal_config_caldav_la_CPPFLAGS = \ + $(AM_CPPFLAGS) \ + -I$(top_srcdir) \ + -I$(top_srcdir)/widgets \ + -DG_LOG_DOMAIN=\"evolution-cal-config-caldav\" \ + $(EVOLUTION_DATA_SERVER_CFLAGS) \ + $(GNOME_PLATFORM_CFLAGS) \ + $(LIBSOUP_CFLAGS) + +module_cal_config_caldav_la_SOURCES = \ + evolution-cal-config-caldav.c \ + e-caldav-chooser.c \ + e-caldav-chooser.h \ + e-caldav-chooser-dialog.c \ + e-caldav-chooser-dialog.h + +module_cal_config_caldav_la_LIBADD = \ + $(top_builddir)/e-util/libeutil.la \ + $(top_builddir)/widgets/misc/libemiscwidgets.la \ + $(top_builddir)/calendar/gui/libevolution-calendar.la \ + $(EVOLUTION_DATA_SERVER_LIBS) \ + $(GNOME_PLATFORM_LIBS) \ + $(LIBSOUP_LIBS) + +module_cal_config_caldav_la_LDFLAGS = \ + -module -avoid-version $(NO_UNDEFINED) + +-include $(top_srcdir)/git.mk diff --git a/modules/cal-config-caldav/e-caldav-chooser-dialog.c b/modules/cal-config-caldav/e-caldav-chooser-dialog.c new file mode 100644 index 0000000000..29848a017a --- /dev/null +++ b/modules/cal-config-caldav/e-caldav-chooser-dialog.c @@ -0,0 +1,477 @@ +/* + * e-caldav-chooser-dialog.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-dialog.h" + +#include <config.h> +#include <glib/gi18n-lib.h> + +#define E_CALDAV_CHOOSER_DIALOG_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_CALDAV_CHOOSER_DIALOG, ECaldavChooserDialogPrivate)) + +struct _ECaldavChooserDialogPrivate { + ECaldavChooser *chooser; + GCancellable *cancellable; + + GtkWidget *info_bar; /* not referenced */ + GtkWidget *info_bar_label; /* not referenced */ +}; + +enum { + PROP_0, + PROP_CHOOSER +}; + +/* Forward Declarations */ +static void caldav_chooser_dialog_populated_cb + (GObject *source_object, + GAsyncResult *result, + gpointer user_data); + +G_DEFINE_DYNAMIC_TYPE ( + ECaldavChooserDialog, + e_caldav_chooser_dialog, + GTK_TYPE_DIALOG) + +static void +caldav_chooser_dialog_done (ECaldavChooserDialog *dialog, + const GError *error) +{ + GdkWindow *window; + + /* Reset the mouse cursor to normal. */ + window = gtk_widget_get_window (GTK_WIDGET (dialog)); + gdk_window_set_cursor (window, NULL); + + if (error != NULL) { + GtkLabel *label; + + label = GTK_LABEL (dialog->priv->info_bar_label); + gtk_label_set_text (label, error->message); + gtk_widget_show (dialog->priv->info_bar); + } +} + +static void +caldav_chooser_dialog_authenticate_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + ESourceRegistry *registry; + ECaldavChooserDialog *dialog; + ECaldavChooser *chooser; + GError *error = NULL; + + registry = E_SOURCE_REGISTRY (source_object); + dialog = E_CALDAV_CHOOSER_DIALOG (user_data); + + chooser = e_caldav_chooser_dialog_get_chooser (dialog); + + e_source_registry_authenticate_finish (registry, result, &error); + + /* Ignore cancellations, and leave the mouse cursor alone + * since the GdkWindow may have already been destroyed. */ + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + /* do nothing */ + + /* Successful authentication, so try populating again. */ + } else if (error == NULL) { + e_caldav_chooser_populate ( + chooser, dialog->priv->cancellable, + caldav_chooser_dialog_populated_cb, + g_object_ref (dialog)); + + /* Still not working? Give up and display an error message. */ + } else { + caldav_chooser_dialog_done (dialog, error); + } + + g_clear_error (&error); + g_object_unref (dialog); +} + +static void +caldav_chooser_dialog_populated_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + ECaldavChooserDialog *dialog; + ECaldavChooser *chooser; + GError *error = NULL; + + chooser = E_CALDAV_CHOOSER (source_object); + dialog = E_CALDAV_CHOOSER_DIALOG (user_data); + + e_caldav_chooser_populate_finish (chooser, result, &error); + + /* Ignore cancellations, and leave the mouse cursor alone + * since the GdkWindow may have already been destroyed. */ + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + /* do nothing */ + + /* We will likely get this error on the first try, since WebDAV + * servers generally require authentication. It means we waste a + * round-trip to the server, but we don't want to risk prompting + * for authentication unnecessarily. */ + } else if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED)) { + ESourceRegistry *registry; + ESource *source; + + registry = e_caldav_chooser_get_registry (chooser); + source = e_caldav_chooser_get_source (chooser); + + e_source_registry_authenticate ( + registry, source, + E_SOURCE_AUTHENTICATOR (chooser), + dialog->priv->cancellable, + caldav_chooser_dialog_authenticate_cb, + g_object_ref (dialog)); + + /* We were either successful or got an unexpected error. */ + } else { + caldav_chooser_dialog_done (dialog, error); + } + + g_clear_error (&error); + g_object_unref (dialog); +} + +static void +caldav_chooser_dialog_row_activated_cb (GtkTreeView *tree_view, + GtkTreePath *path, + GtkTreeViewColumn *column, + GtkDialog *dialog) +{ + gtk_dialog_response (dialog, GTK_RESPONSE_APPLY); +} + +static void +caldav_chooser_dialog_selection_changed_cb (GtkTreeSelection *selection, + GtkDialog *dialog) +{ + gboolean sensitive; + + sensitive = (gtk_tree_selection_count_selected_rows (selection) > 0); + + gtk_dialog_set_response_sensitive ( + dialog, GTK_RESPONSE_APPLY, sensitive); +} + +static void +caldav_chooser_dialog_set_chooser (ECaldavChooserDialog *dialog, + ECaldavChooser *chooser) +{ + g_return_if_fail (E_IS_CALDAV_CHOOSER (chooser)); + g_return_if_fail (dialog->priv->chooser == NULL); + + dialog->priv->chooser = g_object_ref_sink (chooser); +} + +static void +caldav_chooser_dialog_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_CHOOSER: + caldav_chooser_dialog_set_chooser ( + E_CALDAV_CHOOSER_DIALOG (object), + g_value_get_object (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +caldav_chooser_dialog_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_CHOOSER: + g_value_set_object ( + value, + e_caldav_chooser_dialog_get_chooser ( + E_CALDAV_CHOOSER_DIALOG (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +caldav_chooser_dialog_dispose (GObject *object) +{ + ECaldavChooserDialogPrivate *priv; + + priv = E_CALDAV_CHOOSER_DIALOG_GET_PRIVATE (object); + + if (priv->chooser != NULL) { + g_signal_handlers_disconnect_by_func ( + priv->chooser, caldav_chooser_dialog_row_activated_cb, + object); + g_object_unref (priv->chooser); + priv->chooser = NULL; + } + + if (priv->cancellable != NULL) { + g_cancellable_cancel (priv->cancellable); + g_object_unref (priv->cancellable); + priv->cancellable = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_caldav_chooser_dialog_parent_class)->dispose (object); +} + +static void +caldav_chooser_dialog_constructed (GObject *object) +{ + ECaldavChooserDialog *dialog; + GtkTreeSelection *selection; + GtkWidget *container; + GtkWidget *widget; + GtkWidget *vbox; + const gchar *title; + + dialog = E_CALDAV_CHOOSER_DIALOG (object); + + /* Chain up to parent's constructed() method. */ + G_OBJECT_CLASS (e_caldav_chooser_dialog_parent_class)-> + constructed (object); + + switch (e_caldav_chooser_get_source_type (dialog->priv->chooser)) { + case E_CAL_CLIENT_SOURCE_TYPE_EVENTS: + title = _("Choose a Calendar"); + break; + case E_CAL_CLIENT_SOURCE_TYPE_MEMOS: + title = _("Choose a Memo List"); + break; + case E_CAL_CLIENT_SOURCE_TYPE_TASKS: + title = _("Choose a Task List"); + break; + default: + g_warn_if_reached (); + title = ""; + } + + gtk_dialog_add_button ( + GTK_DIALOG (dialog), + GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL); + + gtk_dialog_add_button ( + GTK_DIALOG (dialog), + GTK_STOCK_APPLY, GTK_RESPONSE_APPLY); + + gtk_dialog_set_default_response ( + GTK_DIALOG (dialog), GTK_RESPONSE_APPLY); + gtk_dialog_set_response_sensitive ( + GTK_DIALOG (dialog), GTK_RESPONSE_APPLY, FALSE); + + gtk_window_set_title (GTK_WINDOW (dialog), title); + gtk_window_set_default_size (GTK_WINDOW (dialog), 400, 400); + gtk_container_set_border_width (GTK_CONTAINER (dialog), 5); + + container = gtk_dialog_get_content_area (GTK_DIALOG (dialog)); + + widget = gtk_vbox_new (FALSE, 6); + gtk_container_set_border_width (GTK_CONTAINER (widget), 5); + gtk_box_pack_start (GTK_BOX (container), widget, TRUE, TRUE, 0); + gtk_widget_show (widget); + + container = vbox = widget; + + widget = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_policy ( + GTK_SCROLLED_WINDOW (widget), + GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + gtk_scrolled_window_set_shadow_type ( + GTK_SCROLLED_WINDOW (widget), GTK_SHADOW_IN); + gtk_box_pack_start (GTK_BOX (container), widget, TRUE, TRUE, 0); + gtk_widget_show (widget); + + container = widget; + + widget = GTK_WIDGET (dialog->priv->chooser); + gtk_container_add (GTK_CONTAINER (container), widget); + gtk_widget_show (widget); + + g_signal_connect ( + widget, "row-activated", + G_CALLBACK (caldav_chooser_dialog_row_activated_cb), dialog); + + /* Build the info bar, but hide it initially. */ + + container = vbox; + + widget = gtk_info_bar_new (); + gtk_info_bar_set_message_type ( + GTK_INFO_BAR (widget), GTK_MESSAGE_WARNING); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + dialog->priv->info_bar = widget; /* do not reference */ + gtk_widget_hide (widget); + + container = gtk_info_bar_get_content_area (GTK_INFO_BAR (widget)); + + widget = gtk_hbox_new (FALSE, 6); + gtk_box_pack_start (GTK_BOX (container), widget, TRUE, TRUE, 0); + gtk_widget_show (widget); + + container = widget; + + widget = gtk_image_new_from_stock ( + GTK_STOCK_DIALOG_WARNING, GTK_ICON_SIZE_MENU); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + gtk_widget_show (widget); + + widget = gtk_label_new (""); + gtk_misc_set_alignment (GTK_MISC (widget), 0.0, 0.5); + gtk_box_pack_start (GTK_BOX (container), widget, TRUE, TRUE, 0); + dialog->priv->info_bar_label = widget; /* do not reference */ + gtk_widget_show (widget); + + /* Listen for tree view selection changes. */ + + selection = gtk_tree_view_get_selection ( + GTK_TREE_VIEW (dialog->priv->chooser)); + + g_signal_connect ( + selection, "changed", + G_CALLBACK (caldav_chooser_dialog_selection_changed_cb), + dialog); +} + +static void +caldav_chooser_dialog_realize (GtkWidget *widget) +{ + ECaldavChooserDialogPrivate *priv; + GdkCursor *cursor; + GdkWindow *window; + GdkDisplay *display; + + priv = E_CALDAV_CHOOSER_DIALOG_GET_PRIVATE (widget); + + /* Chain up to parent's realize() method. */ + GTK_WIDGET_CLASS (e_caldav_chooser_dialog_parent_class)-> + realize (widget); + + g_return_if_fail (priv->cancellable == NULL); + priv->cancellable = g_cancellable_new (); + + /* Show a busy mouse cursor while populating. */ + window = gtk_widget_get_window (widget); + display = gtk_widget_get_display (widget); + cursor = gdk_cursor_new_for_display (display, GDK_WATCH); + gdk_window_set_cursor (window, cursor); + gdk_cursor_unref (cursor); + + e_caldav_chooser_populate ( + priv->chooser, priv->cancellable, + caldav_chooser_dialog_populated_cb, + g_object_ref (widget)); +} + +static void +caldav_chooser_dialog_response (GtkDialog *dialog, + gint response_id) +{ + ECaldavChooserDialogPrivate *priv; + + priv = E_CALDAV_CHOOSER_DIALOG_GET_PRIVATE (dialog); + + if (response_id == GTK_RESPONSE_APPLY) + e_caldav_chooser_apply_selected (priv->chooser); +} + +static void +e_caldav_chooser_dialog_class_init (ECaldavChooserDialogClass *class) +{ + GObjectClass *object_class; + GtkWidgetClass *widget_class; + GtkDialogClass *dialog_class; + + g_type_class_add_private (class, sizeof (ECaldavChooserDialogPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = caldav_chooser_dialog_set_property; + object_class->get_property = caldav_chooser_dialog_get_property; + object_class->dispose = caldav_chooser_dialog_dispose; + object_class->constructed = caldav_chooser_dialog_constructed; + + widget_class = GTK_WIDGET_CLASS (class); + widget_class->realize = caldav_chooser_dialog_realize; + + dialog_class = GTK_DIALOG_CLASS (class); + dialog_class->response = caldav_chooser_dialog_response; + + g_object_class_install_property ( + object_class, + PROP_CHOOSER, + g_param_spec_object ( + "chooser", + NULL, + NULL, + E_TYPE_CALDAV_CHOOSER, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY)); +} + +static void +e_caldav_chooser_dialog_class_finalize (ECaldavChooserDialogClass *class) +{ +} + +static void +e_caldav_chooser_dialog_init (ECaldavChooserDialog *dialog) +{ + dialog->priv = E_CALDAV_CHOOSER_DIALOG_GET_PRIVATE (dialog); +} + +void +e_caldav_chooser_dialog_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_dialog_register_type (type_module); +} + +GtkWidget * +e_caldav_chooser_dialog_new (ECaldavChooser *chooser, + GtkWindow *parent) +{ + g_return_val_if_fail (E_IS_CALDAV_CHOOSER (chooser), NULL); + g_return_val_if_fail (parent == NULL || GTK_IS_WINDOW (parent), NULL); + + return g_object_new ( + E_TYPE_CALDAV_CHOOSER_DIALOG, + "chooser", chooser, "transient-for", parent, NULL); +} + +ECaldavChooser * +e_caldav_chooser_dialog_get_chooser (ECaldavChooserDialog *dialog) +{ + g_return_val_if_fail (E_IS_CALDAV_CHOOSER_DIALOG (dialog), NULL); + + return dialog->priv->chooser; +} + diff --git a/modules/cal-config-caldav/e-caldav-chooser-dialog.h b/modules/cal-config-caldav/e-caldav-chooser-dialog.h new file mode 100644 index 0000000000..6c5500f8ee --- /dev/null +++ b/modules/cal-config-caldav/e-caldav-chooser-dialog.h @@ -0,0 +1,68 @@ +/* + * e-caldav-chooser-dialog.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <webcal://www.gnu.org/licenses/> + * + */ + +#ifndef E_CALDAV_CHOOSER_DIALOG_H +#define E_CALDAV_CHOOSER_DIALOG_H + +#include "e-caldav-chooser.h" + +/* Standard GObject macros */ +#define E_TYPE_CALDAV_CHOOSER_DIALOG \ + (e_caldav_chooser_dialog_get_type ()) +#define E_CALDAV_CHOOSER_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_CALDAV_CHOOSER_DIALOG, ECaldavChooserDialog)) +#define E_CALDAV_CHOOSER_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_CALDAV_CHOOSER_DIALOG, ECaldavChooserDialogClass)) +#define E_IS_CALDAV_CHOOSER_DIALOG(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_CALDAV_CHOOSER_DIALOG)) +#define E_IS_CALDAV_CHOOSER_DIALOG_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_CALDAV_CHOOSER_DIALOG)) +#define E_CALDAV_CHOOSER_DIALOG_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_CALDAV_CHOOSER_DIALOG, ECaldavChooserDialogClass)) + +G_BEGIN_DECLS + +typedef struct _ECaldavChooserDialog ECaldavChooserDialog; +typedef struct _ECaldavChooserDialogClass ECaldavChooserDialogClass; +typedef struct _ECaldavChooserDialogPrivate ECaldavChooserDialogPrivate; + +struct _ECaldavChooserDialog { + GtkDialog parent; + ECaldavChooserDialogPrivate *priv; +}; + +struct _ECaldavChooserDialogClass { + GtkDialogClass parent_class; +}; + +GType e_caldav_chooser_dialog_get_type (void); +void e_caldav_chooser_dialog_type_register + (GTypeModule *type_module); +GtkWidget * e_caldav_chooser_dialog_new (ECaldavChooser *chooser, + GtkWindow *parent); +ECaldavChooser *e_caldav_chooser_dialog_get_chooser + (ECaldavChooserDialog *dialog); + +G_END_DECLS + +#endif /* E_CALDAV_CHOOSER_DIALOG_H */ diff --git a/modules/cal-config-caldav/e-caldav-chooser.c b/modules/cal-config-caldav/e-caldav-chooser.c new file mode 100644 index 0000000000..841007bc85 --- /dev/null +++ b/modules/cal-config-caldav/e-caldav-chooser.c @@ -0,0 +1,1643 @@ +/* + * 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 <libedataserver/e-source-authentication.h> +#include <libedataserver/e-source-authenticator.h> +#include <libedataserver/e-source-calendar.h> +#include <libedataserver/e-source-webdav.h> +#include <libedataserverui/e-cell-renderer-color.h> +#include <libedataserverui/e-passwords.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, + 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 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 ( + 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; + 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); + + soup_message_set_request ( + message, "application/xml", SOUP_MEMORY_COPY, + (gchar *) output->buffer->content, output->buffer->use); + + 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); + } + + /* 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_NOBLANKS | + XML_PARSE_NSCLEAN | + 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, + GSimpleAsyncResult *simple) +{ + SoupURI *soup_uri; + + soup_uri = soup_uri_copy (soup_message_get_uri (message)); + soup_uri_set_path (soup_uri, path); + + 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 (error != NULL) { + g_simple_async_result_set_from_error (simple, error); + g_simple_async_result_complete (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; +} + diff --git a/modules/cal-config-caldav/e-caldav-chooser.h b/modules/cal-config-caldav/e-caldav-chooser.h new file mode 100644 index 0000000000..b436161fdb --- /dev/null +++ b/modules/cal-config-caldav/e-caldav-chooser.h @@ -0,0 +1,81 @@ +/* + * e-caldav-chooser.h + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <webcal://www.gnu.org/licenses/> + * + */ + +#ifndef E_CALDAV_CHOOSER_H +#define E_CALDAV_CHOOSER_H + +#include <gtk/gtk.h> +#include <libecal/e-cal-client.h> +#include <libedataserver/e-source-registry.h> + +/* Standard GObject macros */ +#define E_TYPE_CALDAV_CHOOSER \ + (e_caldav_chooser_get_type ()) +#define E_CALDAV_CHOOSER(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_CALDAV_CHOOSER, ECaldavChooser)) +#define E_CALDAV_CHOOSER_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_CALDAV_CHOOSER, ECaldavChooserClass)) +#define E_IS_CALDAV_CHOOSER(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_CALDAV_CHOOSER)) +#define E_IS_CALDAV_CHOOSER_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_CALDAV_CHOOSER)) +#define E_CALDAV_CHOOSER_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_CALDAV_CHOOSER, ECaldavChooserClass)) + +G_BEGIN_DECLS + +typedef struct _ECaldavChooser ECaldavChooser; +typedef struct _ECaldavChooserClass ECaldavChooserClass; +typedef struct _ECaldavChooserPrivate ECaldavChooserPrivate; + +struct _ECaldavChooser { + GtkTreeView parent; + ECaldavChooserPrivate *priv; +}; + +struct _ECaldavChooserClass { + GtkTreeViewClass parent_class; +}; + +GType e_caldav_chooser_get_type (void); +void e_caldav_chooser_type_register (GTypeModule *type_module); +GtkWidget * e_caldav_chooser_new (ESourceRegistry *registry, + ESource *source, + ECalClientSourceType source_type); +ESourceRegistry * + e_caldav_chooser_get_registry (ECaldavChooser *chooser); +ESource * e_caldav_chooser_get_source (ECaldavChooser *chooser); +ECalClientSourceType + e_caldav_chooser_get_source_type + (ECaldavChooser *chooser); +void e_caldav_chooser_populate (ECaldavChooser *chooser, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean e_caldav_chooser_populate_finish + (ECaldavChooser *chooser, + GAsyncResult *result, + GError **error); +gboolean e_caldav_chooser_apply_selected (ECaldavChooser *chooser); + +#endif /* E_CALDAV_CHOOSER_H */ diff --git a/modules/cal-config-caldav/evolution-cal-config-caldav.c b/modules/cal-config-caldav/evolution-cal-config-caldav.c new file mode 100644 index 0000000000..0d8d6dd6cf --- /dev/null +++ b/modules/cal-config-caldav/evolution-cal-config-caldav.c @@ -0,0 +1,381 @@ +/* + * evolution-cal-config-caldav.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/> + * + */ + +#include <config.h> +#include <glib/gi18n-lib.h> + +#include <libebackend/e-extension.h> +#include <libedataserver/e-source-authentication.h> +#include <libedataserver/e-source-security.h> +#include <libedataserver/e-source-webdav.h> + +#include <misc/e-interval-chooser.h> +#include <misc/e-source-config-backend.h> +#include <calendar/gui/e-cal-source-config.h> + +#include "e-caldav-chooser.h" +#include "e-caldav-chooser-dialog.h" + +#define HTTP_PORT 80 +#define HTTPS_PORT 443 + +typedef ESourceConfigBackend ECalConfigCalDAV; +typedef ESourceConfigBackendClass ECalConfigCalDAVClass; + +typedef struct _Context Context; + +struct _Context { + ESourceConfigBackend *backend; /* not referenced */ + ESource *scratch_source; /* not referenced */ + + GtkWidget *server_entry; + GtkWidget *path_entry; + GtkWidget *email_entry; + GtkWidget *find_button; + GtkWidget *auto_schedule_toggle; + + GSocketConnectable *connectable; +}; + +/* Module Entry Points */ +void e_module_load (GTypeModule *type_module); +void e_module_unload (GTypeModule *type_module); + +/* Forward Declarations */ +GType e_cal_config_caldav_get_type (void); + +G_DEFINE_DYNAMIC_TYPE ( + ECalConfigCalDAV, + e_cal_config_caldav, + E_TYPE_SOURCE_CONFIG_BACKEND) + +static Context * +cal_config_caldav_context_new (ESourceConfigBackend *backend, + ESource *scratch_source) +{ + Context *context; + + context = g_slice_new0 (Context); + context->backend = backend; + context->scratch_source = scratch_source; + + return context; +} + +static void +cal_config_caldav_context_free (Context *context) +{ + g_object_unref (context->server_entry); + g_object_unref (context->path_entry); + g_object_unref (context->email_entry); + g_object_unref (context->find_button); + g_object_unref (context->auto_schedule_toggle); + + if (context->connectable != NULL) + g_object_unref (context->connectable); + + g_slice_free (Context, context); +} + +static gchar * +cal_config_caldav_get_server (ESource *scratch_source) +{ + ESourceAuthentication *authentication_extension; + ESourceSecurity *security_extension; + const gchar *host; + gboolean secure; + guint16 default_port; + guint16 port; + + authentication_extension = e_source_get_extension ( + scratch_source, E_SOURCE_EXTENSION_AUTHENTICATION); + host = e_source_authentication_get_host (authentication_extension); + port = e_source_authentication_get_port (authentication_extension); + + security_extension = e_source_get_extension ( + scratch_source, E_SOURCE_EXTENSION_SECURITY); + secure = e_source_security_get_secure (security_extension); + default_port = secure ? HTTPS_PORT: HTTP_PORT; + + if (port == 0) + port = default_port; + + if (host == NULL || *host == '\0') + return NULL; + + if (port == default_port) + return g_strdup (host); + + return g_strdup_printf ("%s:%u", host, port); +} + +static void +cal_config_caldav_server_changed_cb (GtkEntry *entry, + Context *context) +{ + ESourceAuthentication *authentication_extension; + ESourceSecurity *security_extension; + const gchar *host_and_port; + const gchar *host; + gboolean secure; + guint16 default_port; + guint16 port; + + if (context->connectable != NULL) { + g_object_unref (context->connectable); + context->connectable = NULL; + } + + authentication_extension = e_source_get_extension ( + context->scratch_source, E_SOURCE_EXTENSION_AUTHENTICATION); + + security_extension = e_source_get_extension ( + context->scratch_source, E_SOURCE_EXTENSION_SECURITY); + + host_and_port = gtk_entry_get_text (entry); + secure = e_source_security_get_secure (security_extension); + default_port = secure ? HTTPS_PORT : HTTP_PORT; + + if (host_and_port != NULL && *host_and_port != '\0') + context->connectable = g_network_address_parse ( + host_and_port, default_port, NULL); + + if (context->connectable != NULL) { + GNetworkAddress *address; + + address = G_NETWORK_ADDRESS (context->connectable); + host = g_network_address_get_hostname (address); + port = g_network_address_get_port (address); + } else { + host = NULL; + port = 0; + } + + e_source_authentication_set_host (authentication_extension, host); + e_source_authentication_set_port (authentication_extension, port); +} + +static void +cal_config_caldav_run_dialog (GtkButton *button, + Context *context) +{ + ESourceConfig *config; + ESourceRegistry *registry; + ECalClientSourceType source_type; + GtkWidget *dialog; + GtkWidget *widget; + gpointer parent; + + config = e_source_config_backend_get_config (context->backend); + registry = e_source_config_get_registry (config); + + parent = gtk_widget_get_toplevel (GTK_WIDGET (config)); + parent = gtk_widget_is_toplevel (parent) ? parent : NULL; + + source_type = e_cal_source_config_get_source_type ( + E_CAL_SOURCE_CONFIG (config)); + + widget = e_caldav_chooser_new ( + registry, context->scratch_source, source_type); + + dialog = e_caldav_chooser_dialog_new ( + E_CALDAV_CHOOSER (widget), parent); + + if (parent != NULL) + g_object_bind_property ( + parent, "icon-name", + dialog, "icon-name", + G_BINDING_SYNC_CREATE); + + gtk_dialog_run (GTK_DIALOG (dialog)); + + gtk_widget_destroy (dialog); +} + +static void +cal_config_caldav_insert_widgets (ESourceConfigBackend *backend, + ESource *scratch_source) +{ + ESourceConfig *config; + ESourceExtension *extension; + ECalClientSourceType source_type; + GtkWidget *widget; + Context *context; + gchar *text; + const gchar *extension_name; + const gchar *label; + const gchar *uid; + + context = cal_config_caldav_context_new (backend, scratch_source); + uid = e_source_get_uid (scratch_source); + config = e_source_config_backend_get_config (backend); + + g_object_set_data_full ( + G_OBJECT (backend), uid, context, + (GDestroyNotify) cal_config_caldav_context_free); + + source_type = e_cal_source_config_get_source_type ( + E_CAL_SOURCE_CONFIG (config)); + + e_cal_source_config_add_offline_toggle ( + E_CAL_SOURCE_CONFIG (config), scratch_source); + + widget = gtk_entry_new (); + e_source_config_insert_widget ( + config, scratch_source, _("Server:"), widget); + context->server_entry = g_object_ref (widget); + gtk_widget_show (widget); + + /* Connect the signal before initializing the entry text. */ + g_signal_connect ( + widget, "changed", + G_CALLBACK (cal_config_caldav_server_changed_cb), context); + + text = cal_config_caldav_get_server (scratch_source); + if (text != NULL) { + gtk_entry_set_text (GTK_ENTRY (context->server_entry), text); + g_free (text); + } + + e_source_config_add_secure_connection_for_webdav ( + config, scratch_source); + + e_source_config_add_user_entry (config, scratch_source); + + switch (source_type) { + case E_CAL_CLIENT_SOURCE_TYPE_EVENTS: + label = _("Find Calendars"); + break; + case E_CAL_CLIENT_SOURCE_TYPE_MEMOS: + label = _("Find Memo Lists"); + break; + case E_CAL_CLIENT_SOURCE_TYPE_TASKS: + label = _("Find Task Lists"); + break; + default: + g_return_if_reached (); + } + + widget = gtk_button_new_with_label (label); + e_source_config_insert_widget ( + config, scratch_source, NULL, widget); + context->find_button = g_object_ref (widget); + gtk_widget_show (widget); + + g_signal_connect ( + widget, "clicked", + G_CALLBACK (cal_config_caldav_run_dialog), context); + + widget = gtk_entry_new (); + e_source_config_insert_widget ( + config, scratch_source, _("Path:"), widget); + context->path_entry = g_object_ref (widget); + gtk_widget_show (widget); + + widget = gtk_entry_new (); + e_source_config_insert_widget ( + config, scratch_source, _("Email:"), widget); + context->email_entry = g_object_ref (widget); + gtk_widget_show (widget); + + widget = gtk_check_button_new_with_label ( + _("Server handles meeting invitations")); + e_source_config_insert_widget ( + config, scratch_source, NULL, widget); + context->auto_schedule_toggle = g_object_ref (widget); + gtk_widget_show (widget); + + e_source_config_add_refresh_interval (config, scratch_source); + + extension_name = E_SOURCE_EXTENSION_WEBDAV_BACKEND; + extension = e_source_get_extension (scratch_source, extension_name); + + g_object_bind_property ( + extension, "calendar-auto-schedule", + context->auto_schedule_toggle, "active", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE); + + g_object_bind_property ( + extension, "email-address", + context->email_entry, "text", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE); + + g_object_bind_property ( + extension, "resource-path", + context->path_entry, "text", + G_BINDING_BIDIRECTIONAL | + G_BINDING_SYNC_CREATE); +} + +static gboolean +cal_config_caldav_check_complete (ESourceConfigBackend *backend, + ESource *scratch_source) +{ + Context *context; + const gchar *uid; + gboolean complete; + + uid = e_source_get_uid (scratch_source); + context = g_object_get_data (G_OBJECT (backend), uid); + g_return_val_if_fail (context != NULL, FALSE); + + complete = (context->connectable != NULL); + + gtk_widget_set_sensitive (context->find_button, complete); + + return complete; +} + +static void +e_cal_config_caldav_class_init (ESourceConfigBackendClass *class) +{ + EExtensionClass *extension_class; + + extension_class = E_EXTENSION_CLASS (class); + extension_class->extensible_type = E_TYPE_CAL_SOURCE_CONFIG; + + class->parent_uid = "caldav-stub"; + class->backend_name = "caldav"; + class->insert_widgets = cal_config_caldav_insert_widgets; + class->check_complete = cal_config_caldav_check_complete; +} + +static void +e_cal_config_caldav_class_finalize (ESourceConfigBackendClass *class) +{ +} + +static void +e_cal_config_caldav_init (ESourceConfigBackend *backend) +{ +} + +G_MODULE_EXPORT void +e_module_load (GTypeModule *type_module) +{ + e_caldav_chooser_type_register (type_module); + e_caldav_chooser_dialog_type_register (type_module); + e_cal_config_caldav_register_type (type_module); +} + +G_MODULE_EXPORT void +e_module_unload (GTypeModule *type_module) +{ +} |