/* * 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 * */ #include "e-google-chooser.h" #include #include #include #include #include #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 gchar * google_chooser_decode_user (const gchar *user) { gchar *decoded_user; 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_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; 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); return google_chooser_decode_user (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' (2nd) 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 ( "", "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; } void e_google_chooser_construct_default_uri (SoupURI *soup_uri, const gchar *username) { gchar *decoded_user, *path; decoded_user = google_chooser_decode_user (username); if (!decoded_user) return; path = g_strdup_printf (CALDAV_EVENTS_PATH_FORMAT, decoded_user); soup_uri_set_user (soup_uri, decoded_user); soup_uri_set_path (soup_uri, path); g_free (decoded_user); g_free (path); }