620 lines
15 KiB
C
620 lines
15 KiB
C
/*
|
|
* e-google-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-google-chooser.h"
|
|
|
|
#include <config.h>
|
|
#include <string.h>
|
|
#include <gdata/gdata.h>
|
|
#include <glib/gi18n-lib.h>
|
|
|
|
#include <libedataserverui/libedataserverui.h>
|
|
|
|
#define E_GOOGLE_CHOOSER_GET_PRIVATE(obj) \
|
|
(G_TYPE_INSTANCE_GET_PRIVATE \
|
|
((obj), E_TYPE_GOOGLE_CHOOSER, EGoogleChooserPrivate))
|
|
|
|
#define CALDAV_EVENTS_PATH_FORMAT "/calendar/dav/%s/events"
|
|
|
|
typedef struct _Context Context;
|
|
|
|
struct _EGoogleChooserPrivate {
|
|
ESource *source;
|
|
};
|
|
|
|
struct _Context {
|
|
GCancellable *cancellable;
|
|
GDataCalendarService *service;
|
|
GDataClientLoginAuthorizer *authorizer;
|
|
ESource *source;
|
|
};
|
|
|
|
enum {
|
|
PROP_0,
|
|
PROP_SOURCE
|
|
};
|
|
|
|
enum {
|
|
COLUMN_COLOR,
|
|
COLUMN_PATH,
|
|
COLUMN_TITLE,
|
|
COLUMN_WRITABLE,
|
|
NUM_COLUMNS
|
|
};
|
|
|
|
G_DEFINE_DYNAMIC_TYPE (
|
|
EGoogleChooser,
|
|
e_google_chooser,
|
|
GTK_TYPE_TREE_VIEW)
|
|
|
|
static void
|
|
context_free (Context *context)
|
|
{
|
|
if (context->cancellable != NULL)
|
|
g_object_unref (context->cancellable);
|
|
|
|
if (context->service != NULL)
|
|
g_object_unref (context->service);
|
|
|
|
if (context->authorizer != NULL)
|
|
g_object_unref (context->authorizer);
|
|
|
|
if (context->source != NULL)
|
|
g_object_unref (context->source);
|
|
|
|
g_slice_free (Context, context);
|
|
}
|
|
|
|
static gchar *
|
|
google_chooser_extract_caldav_events_path (const gchar *uri)
|
|
{
|
|
SoupURI *soup_uri;
|
|
gchar *resource_name;
|
|
gchar *path;
|
|
gchar *cp;
|
|
|
|
soup_uri = soup_uri_new (uri);
|
|
g_return_val_if_fail (soup_uri != NULL, NULL);
|
|
|
|
/* Isolate the resource name in the "feeds" URI. */
|
|
|
|
cp = strstr (soup_uri->path, "/feeds/");
|
|
g_return_val_if_fail (cp != NULL, NULL);
|
|
|
|
/* strlen("/feeds/) == 7 */
|
|
resource_name = g_strdup (cp + 7);
|
|
cp = strchr (resource_name, '/');
|
|
if (cp != NULL)
|
|
*cp = '\0';
|
|
|
|
/* Decode any encoded 'at' symbols ('%40' -> '@'). */
|
|
if (strstr (resource_name, "%40") != NULL) {
|
|
gchar **segments;
|
|
|
|
segments = g_strsplit (resource_name, "%40", 0);
|
|
g_free (resource_name);
|
|
resource_name = g_strjoinv ("@", segments);
|
|
g_strfreev (segments);
|
|
}
|
|
|
|
/* Use the decoded resource name in the CalDAV events path. */
|
|
path = g_strdup_printf (CALDAV_EVENTS_PATH_FORMAT, resource_name);
|
|
|
|
g_free (resource_name);
|
|
|
|
soup_uri_free (soup_uri);
|
|
|
|
return path;
|
|
}
|
|
|
|
static void
|
|
google_chooser_set_source (EGoogleChooser *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
|
|
google_chooser_set_property (GObject *object,
|
|
guint property_id,
|
|
const GValue *value,
|
|
GParamSpec *pspec)
|
|
{
|
|
switch (property_id) {
|
|
case PROP_SOURCE:
|
|
google_chooser_set_source (
|
|
E_GOOGLE_CHOOSER (object),
|
|
g_value_get_object (value));
|
|
return;
|
|
}
|
|
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
|
}
|
|
|
|
static void
|
|
google_chooser_get_property (GObject *object,
|
|
guint property_id,
|
|
GValue *value,
|
|
GParamSpec *pspec)
|
|
{
|
|
switch (property_id) {
|
|
case PROP_SOURCE:
|
|
g_value_set_object (
|
|
value, e_google_chooser_get_source (
|
|
E_GOOGLE_CHOOSER (object)));
|
|
return;
|
|
}
|
|
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
|
}
|
|
|
|
static void
|
|
google_chooser_dispose (GObject *object)
|
|
{
|
|
EGoogleChooserPrivate *priv;
|
|
|
|
priv = E_GOOGLE_CHOOSER_GET_PRIVATE (object);
|
|
|
|
if (priv->source != NULL) {
|
|
g_object_unref (priv->source);
|
|
priv->source = NULL;
|
|
}
|
|
|
|
/* Chain up parent's dispose() method. */
|
|
G_OBJECT_CLASS (e_google_chooser_parent_class)->dispose (object);
|
|
}
|
|
|
|
static void
|
|
google_chooser_constructed (GObject *object)
|
|
{
|
|
GtkTreeView *tree_view;
|
|
GtkListStore *list_store;
|
|
GtkCellRenderer *renderer;
|
|
GtkTreeViewColumn *column;
|
|
|
|
tree_view = GTK_TREE_VIEW (object);
|
|
|
|
/* Chain up to parent's constructed() method. */
|
|
G_OBJECT_CLASS (e_google_chooser_parent_class)->constructed (object);
|
|
|
|
list_store = gtk_list_store_new (
|
|
NUM_COLUMNS,
|
|
GDK_TYPE_COLOR, /* COLUMN_COLOR */
|
|
G_TYPE_STRING, /* COLUMN_PATH */
|
|
G_TYPE_STRING, /* COLUMN_TITLE */
|
|
G_TYPE_BOOLEAN); /* COLUMN_WRITABLE */
|
|
|
|
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_add_attribute (
|
|
column, renderer, "color", COLUMN_COLOR);
|
|
|
|
renderer = gtk_cell_renderer_text_new ();
|
|
gtk_tree_view_column_pack_start (column, renderer, TRUE);
|
|
gtk_tree_view_column_add_attribute (
|
|
column, renderer, "text", COLUMN_TITLE);
|
|
}
|
|
|
|
static void
|
|
e_google_chooser_class_init (EGoogleChooserClass *class)
|
|
{
|
|
GObjectClass *object_class;
|
|
|
|
g_type_class_add_private (class, sizeof (EGoogleChooserPrivate));
|
|
|
|
object_class = G_OBJECT_CLASS (class);
|
|
object_class->set_property = google_chooser_set_property;
|
|
object_class->get_property = google_chooser_get_property;
|
|
object_class->dispose = google_chooser_dispose;
|
|
object_class->constructed = google_chooser_constructed;
|
|
|
|
g_object_class_install_property (
|
|
object_class,
|
|
PROP_SOURCE,
|
|
g_param_spec_object (
|
|
"source",
|
|
"Source",
|
|
"Google data source",
|
|
E_TYPE_SOURCE,
|
|
G_PARAM_READWRITE |
|
|
G_PARAM_CONSTRUCT_ONLY));
|
|
}
|
|
|
|
static void
|
|
e_google_chooser_class_finalize (EGoogleChooserClass *class)
|
|
{
|
|
}
|
|
|
|
static void
|
|
e_google_chooser_init (EGoogleChooser *chooser)
|
|
{
|
|
chooser->priv = E_GOOGLE_CHOOSER_GET_PRIVATE (chooser);
|
|
}
|
|
|
|
void
|
|
e_google_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_google_chooser_register_type (type_module);
|
|
}
|
|
|
|
GtkWidget *
|
|
e_google_chooser_new (ESource *source)
|
|
{
|
|
g_return_val_if_fail (E_IS_SOURCE (source), NULL);
|
|
|
|
return g_object_new (E_TYPE_GOOGLE_CHOOSER, "source", source, NULL);
|
|
}
|
|
|
|
ESource *
|
|
e_google_chooser_get_source (EGoogleChooser *chooser)
|
|
{
|
|
g_return_val_if_fail (E_IS_GOOGLE_CHOOSER (chooser), NULL);
|
|
|
|
return chooser->priv->source;
|
|
}
|
|
|
|
gchar *
|
|
e_google_chooser_get_decoded_user (EGoogleChooser *chooser)
|
|
{
|
|
ESource *source;
|
|
ESourceAuthentication *authentication_extension;
|
|
const gchar *user;
|
|
gchar *decoded_user;
|
|
|
|
g_return_val_if_fail (E_IS_GOOGLE_CHOOSER (chooser), NULL);
|
|
|
|
source = e_google_chooser_get_source (chooser);
|
|
|
|
authentication_extension = e_source_get_extension (
|
|
source, E_SOURCE_EXTENSION_AUTHENTICATION);
|
|
|
|
user = e_source_authentication_get_user (authentication_extension);
|
|
if (user == NULL || *user == '\0')
|
|
return NULL;
|
|
|
|
/* Decode any encoded 'at' symbols ('%40' -> '@'). */
|
|
if (strstr (user, "%40") != NULL) {
|
|
gchar **segments;
|
|
|
|
segments = g_strsplit (user, "%40", 0);
|
|
decoded_user = g_strjoinv ("@", segments);
|
|
g_strfreev (segments);
|
|
|
|
/* If no domain is given, append "@gmail.com". */
|
|
} else if (strstr (user, "@") == NULL) {
|
|
decoded_user = g_strconcat (user, "@gmail.com", NULL);
|
|
|
|
/* Otherwise the user name should be fine as is. */
|
|
} else {
|
|
decoded_user = g_strdup (user);
|
|
}
|
|
|
|
return decoded_user;
|
|
}
|
|
|
|
static void
|
|
google_chooser_query_cb (GDataService *service,
|
|
GAsyncResult *result,
|
|
GSimpleAsyncResult *simple)
|
|
{
|
|
GObject *object;
|
|
GDataFeed *feed;
|
|
GList *list, *link;
|
|
GtkTreeView *tree_view;
|
|
GtkListStore *list_store;
|
|
GtkTreeModel *tree_model;
|
|
GError *error = NULL;
|
|
|
|
feed = gdata_service_query_finish (service, result, &error);
|
|
|
|
if (error != NULL) {
|
|
g_warn_if_fail (feed == NULL);
|
|
g_simple_async_result_set_from_error (simple, error);
|
|
g_simple_async_result_complete (simple);
|
|
g_object_unref (simple);
|
|
return;
|
|
}
|
|
|
|
g_return_if_fail (GDATA_IS_FEED (feed));
|
|
|
|
list = gdata_feed_get_entries (feed);
|
|
|
|
/* This returns a new reference, for reasons passing understanding. */
|
|
object = g_async_result_get_source_object (G_ASYNC_RESULT (simple));
|
|
|
|
tree_view = GTK_TREE_VIEW (object);
|
|
tree_model = gtk_tree_view_get_model (tree_view);
|
|
list_store = GTK_LIST_STORE (tree_model);
|
|
|
|
gtk_list_store_clear (list_store);
|
|
|
|
for (link = list; link != NULL; link = g_list_next (link)) {
|
|
GDataCalendarCalendar *calendar;
|
|
GDataEntry *entry;
|
|
GDataLink *alt;
|
|
GDataColor color;
|
|
GdkColor gdkcolor;
|
|
GtkTreeIter iter;
|
|
const gchar *uri;
|
|
const gchar *title;
|
|
const gchar *access;
|
|
gboolean writable;
|
|
gchar *path;
|
|
|
|
entry = GDATA_ENTRY (link->data);
|
|
calendar = GDATA_CALENDAR_CALENDAR (entry);
|
|
|
|
/* Skip hidden entries. */
|
|
if (gdata_calendar_calendar_is_hidden (calendar))
|
|
continue;
|
|
|
|
/* Look up the alternate link, skip if there is none. */
|
|
alt = gdata_entry_look_up_link (entry, GDATA_LINK_ALTERNATE);
|
|
if (alt == NULL)
|
|
continue;
|
|
|
|
uri = gdata_link_get_uri (alt);
|
|
title = gdata_entry_get_title (entry);
|
|
gdata_calendar_calendar_get_color (calendar, &color);
|
|
access = gdata_calendar_calendar_get_access_level (calendar);
|
|
|
|
if (uri == NULL || *uri == '\0')
|
|
continue;
|
|
|
|
if (title == NULL || *title == '\0')
|
|
continue;
|
|
|
|
path = google_chooser_extract_caldav_events_path (uri);
|
|
|
|
gdkcolor.pixel = 0;
|
|
gdkcolor.red = color.red * 256;
|
|
gdkcolor.green = color.green * 256;
|
|
gdkcolor.blue = color.blue * 256;
|
|
|
|
if (access == NULL)
|
|
writable = TRUE;
|
|
else if (g_ascii_strcasecmp (access, "owner") == 0)
|
|
writable = TRUE;
|
|
else if (g_ascii_strcasecmp (access, "contributor") == 0)
|
|
writable = TRUE;
|
|
else
|
|
writable = FALSE;
|
|
|
|
gtk_list_store_append (list_store, &iter);
|
|
|
|
gtk_list_store_set (
|
|
list_store, &iter,
|
|
COLUMN_COLOR, &gdkcolor,
|
|
COLUMN_PATH, path,
|
|
COLUMN_TITLE, title,
|
|
COLUMN_WRITABLE, writable,
|
|
-1);
|
|
|
|
g_free (path);
|
|
}
|
|
|
|
g_object_unref (object);
|
|
g_object_unref (feed);
|
|
|
|
g_simple_async_result_complete (simple);
|
|
g_object_unref (simple);
|
|
}
|
|
|
|
static void
|
|
google_chooser_authenticate_cb (GDataClientLoginAuthorizer *authorizer,
|
|
GAsyncResult *result,
|
|
GSimpleAsyncResult *simple)
|
|
{
|
|
Context *context;
|
|
GError *error = NULL;
|
|
|
|
context = g_simple_async_result_get_op_res_gpointer (simple);
|
|
|
|
gdata_client_login_authorizer_authenticate_finish (
|
|
authorizer, result, &error);
|
|
|
|
if (error != NULL) {
|
|
g_simple_async_result_set_from_error (simple, error);
|
|
g_simple_async_result_complete (simple);
|
|
g_object_unref (simple);
|
|
return;
|
|
}
|
|
|
|
/* We're authenticated, now query for all calendars. */
|
|
|
|
gdata_calendar_service_query_all_calendars_async (
|
|
context->service, NULL, context->cancellable,
|
|
NULL, NULL, NULL, (GAsyncReadyCallback)
|
|
google_chooser_query_cb, simple);
|
|
}
|
|
|
|
void
|
|
e_google_chooser_populate (EGoogleChooser *chooser,
|
|
GCancellable *cancellable,
|
|
GAsyncReadyCallback callback,
|
|
gpointer user_data)
|
|
{
|
|
GDataClientLoginAuthorizer *authorizer;
|
|
GDataCalendarService *service;
|
|
GSimpleAsyncResult *simple;
|
|
Context *context;
|
|
ESource *source;
|
|
gpointer parent;
|
|
gchar *password;
|
|
gchar *prompt;
|
|
gchar *user;
|
|
|
|
g_return_if_fail (E_IS_GOOGLE_CHOOSER (chooser));
|
|
|
|
source = e_google_chooser_get_source (chooser);
|
|
|
|
authorizer = gdata_client_login_authorizer_new (
|
|
PACKAGE_NAME, GDATA_TYPE_CALENDAR_SERVICE);
|
|
|
|
service = gdata_calendar_service_new (GDATA_AUTHORIZER (authorizer));
|
|
|
|
context = g_slice_new0 (Context);
|
|
context->service = service; /* takes ownership */
|
|
context->source = g_object_ref (source);
|
|
|
|
if (G_IS_CANCELLABLE (cancellable))
|
|
context->cancellable = g_object_ref (cancellable);
|
|
else
|
|
context->cancellable = g_cancellable_new ();
|
|
|
|
simple = g_simple_async_result_new (
|
|
G_OBJECT (chooser), callback,
|
|
user_data, e_google_chooser_populate);
|
|
|
|
g_simple_async_result_set_op_res_gpointer (
|
|
simple, context, (GDestroyNotify) context_free);
|
|
|
|
/* Prompt for a password. */
|
|
|
|
user = e_google_chooser_get_decoded_user (chooser);
|
|
|
|
parent = gtk_widget_get_toplevel (GTK_WIDGET (chooser));
|
|
parent = gtk_widget_is_toplevel (parent) ? parent : NULL;
|
|
|
|
prompt = g_strdup_printf (
|
|
_("Enter Google password for user '%s'."), user);
|
|
|
|
/* XXX The 'key' (3rd) argument doesn't matter since we're
|
|
* passing E_PASSWORDS_REMEMBER_NEVER, it just needs to
|
|
* be non-NULL. This API is degenerating rapidly. */
|
|
password = e_passwords_ask_password (
|
|
"", NULL, "bogus key", prompt,
|
|
E_PASSWORDS_REMEMBER_NEVER |
|
|
E_PASSWORDS_DISABLE_REMEMBER |
|
|
E_PASSWORDS_SECRET, NULL, parent);
|
|
|
|
g_free (prompt);
|
|
|
|
if (password == NULL) {
|
|
g_cancellable_cancel (context->cancellable);
|
|
g_simple_async_result_set_error (
|
|
simple, G_IO_ERROR, G_IO_ERROR_CANCELLED,
|
|
"%s", _("User declined to provide a password"));
|
|
g_simple_async_result_complete (simple);
|
|
g_object_unref (authorizer);
|
|
g_object_unref (simple);
|
|
g_free (user);
|
|
return;
|
|
}
|
|
|
|
/* Try authenticating. */
|
|
|
|
gdata_client_login_authorizer_authenticate_async (
|
|
authorizer, user, password,
|
|
context->cancellable, (GAsyncReadyCallback)
|
|
google_chooser_authenticate_cb, simple);
|
|
|
|
g_free (password);
|
|
g_free (user);
|
|
|
|
g_object_unref (authorizer);
|
|
}
|
|
|
|
gboolean
|
|
e_google_chooser_populate_finish (EGoogleChooser *chooser,
|
|
GAsyncResult *result,
|
|
GError **error)
|
|
{
|
|
GSimpleAsyncResult *simple;
|
|
|
|
g_return_val_if_fail (
|
|
g_simple_async_result_is_valid (
|
|
result, G_OBJECT (chooser),
|
|
e_google_chooser_populate), FALSE);
|
|
|
|
simple = G_SIMPLE_ASYNC_RESULT (result);
|
|
|
|
/* Assume success unless a GError is set. */
|
|
return !g_simple_async_result_propagate_error (simple, error);
|
|
}
|
|
|
|
gboolean
|
|
e_google_chooser_apply_selected (EGoogleChooser *chooser)
|
|
{
|
|
ESourceSelectable *selectable_extension;
|
|
ESourceWebdav *webdav_extension;
|
|
GtkTreeSelection *selection;
|
|
GtkTreeModel *model;
|
|
GtkTreeIter iter;
|
|
ESource *source;
|
|
GdkColor *color;
|
|
SoupURI *soup_uri;
|
|
gchar *color_spec;
|
|
gchar *title;
|
|
gchar *path;
|
|
|
|
g_return_val_if_fail (E_IS_GOOGLE_CHOOSER (chooser), FALSE);
|
|
|
|
source = e_google_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_COLOR, &color,
|
|
COLUMN_PATH, &path,
|
|
COLUMN_TITLE, &title,
|
|
-1);
|
|
|
|
selectable_extension = e_source_get_extension (
|
|
source, E_SOURCE_EXTENSION_CALENDAR);
|
|
|
|
webdav_extension = e_source_get_extension (
|
|
source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
|
|
|
|
e_source_set_display_name (source, title);
|
|
|
|
e_source_webdav_set_display_name (webdav_extension, title);
|
|
|
|
/* XXX Might be easier to expose get/set_path functions? */
|
|
soup_uri = e_source_webdav_dup_soup_uri (webdav_extension);
|
|
soup_uri_set_path (soup_uri, path);
|
|
e_source_webdav_set_soup_uri (webdav_extension, soup_uri);
|
|
soup_uri_free (soup_uri);
|
|
|
|
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 (title);
|
|
g_free (path);
|
|
|
|
return TRUE;
|
|
}
|