No issues found
1 /*
2 * e-google-chooser.c
3 *
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2 of the License, or (at your option) version 3.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public
15 * License along with the program; if not, see <webcal://www.gnu.org/licenses/>
16 *
17 */
18
19 #include "e-google-chooser.h"
20
21 #include <config.h>
22 #include <string.h>
23 #include <gdata/gdata.h>
24 #include <glib/gi18n-lib.h>
25
26 #include <libedataserverui/libedataserverui.h>
27
28 #define E_GOOGLE_CHOOSER_GET_PRIVATE(obj) \
29 (G_TYPE_INSTANCE_GET_PRIVATE \
30 ((obj), E_TYPE_GOOGLE_CHOOSER, EGoogleChooserPrivate))
31
32 #define CALDAV_EVENTS_PATH_FORMAT "/calendar/dav/%s/events"
33
34 typedef struct _Context Context;
35
36 struct _EGoogleChooserPrivate {
37 ESource *source;
38 };
39
40 struct _Context {
41 GCancellable *cancellable;
42 GDataCalendarService *service;
43 GDataClientLoginAuthorizer *authorizer;
44 ESource *source;
45 };
46
47 enum {
48 PROP_0,
49 PROP_SOURCE
50 };
51
52 enum {
53 COLUMN_COLOR,
54 COLUMN_PATH,
55 COLUMN_TITLE,
56 COLUMN_WRITABLE,
57 NUM_COLUMNS
58 };
59
60 G_DEFINE_DYNAMIC_TYPE (
61 EGoogleChooser,
62 e_google_chooser,
63 GTK_TYPE_TREE_VIEW)
64
65 static void
66 context_free (Context *context)
67 {
68 if (context->cancellable != NULL)
69 g_object_unref (context->cancellable);
70
71 if (context->service != NULL)
72 g_object_unref (context->service);
73
74 if (context->authorizer != NULL)
75 g_object_unref (context->authorizer);
76
77 if (context->source != NULL)
78 g_object_unref (context->source);
79
80 g_slice_free (Context, context);
81 }
82
83 static gchar *
84 google_chooser_extract_caldav_events_path (const gchar *uri)
85 {
86 SoupURI *soup_uri;
87 gchar *resource_name;
88 gchar *path;
89 gchar *cp;
90
91 soup_uri = soup_uri_new (uri);
92 g_return_val_if_fail (soup_uri != NULL, NULL);
93
94 /* Isolate the resource name in the "feeds" URI. */
95
96 cp = strstr (soup_uri->path, "/feeds/");
97 g_return_val_if_fail (cp != NULL, NULL);
98
99 /* strlen("/feeds/) == 7 */
100 resource_name = g_strdup (cp + 7);
101 cp = strchr (resource_name, '/');
102 if (cp != NULL)
103 *cp = '\0';
104
105 /* Decode any encoded 'at' symbols ('%40' -> '@'). */
106 if (strstr (resource_name, "%40") != NULL) {
107 gchar **segments;
108
109 segments = g_strsplit (resource_name, "%40", 0);
110 g_free (resource_name);
111 resource_name = g_strjoinv ("@", segments);
112 g_strfreev (segments);
113 }
114
115 /* Use the decoded resource name in the CalDAV events path. */
116 path = g_strdup_printf (CALDAV_EVENTS_PATH_FORMAT, resource_name);
117
118 g_free (resource_name);
119
120 soup_uri_free (soup_uri);
121
122 return path;
123 }
124
125 static void
126 google_chooser_set_source (EGoogleChooser *chooser,
127 ESource *source)
128 {
129 g_return_if_fail (E_IS_SOURCE (source));
130 g_return_if_fail (chooser->priv->source == NULL);
131
132 chooser->priv->source = g_object_ref (source);
133 }
134
135 static void
136 google_chooser_set_property (GObject *object,
137 guint property_id,
138 const GValue *value,
139 GParamSpec *pspec)
140 {
141 switch (property_id) {
142 case PROP_SOURCE:
143 google_chooser_set_source (
144 E_GOOGLE_CHOOSER (object),
145 g_value_get_object (value));
146 return;
147 }
148
149 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
150 }
151
152 static void
153 google_chooser_get_property (GObject *object,
154 guint property_id,
155 GValue *value,
156 GParamSpec *pspec)
157 {
158 switch (property_id) {
159 case PROP_SOURCE:
160 g_value_set_object (
161 value, e_google_chooser_get_source (
162 E_GOOGLE_CHOOSER (object)));
163 return;
164 }
165
166 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
167 }
168
169 static void
170 google_chooser_dispose (GObject *object)
171 {
172 EGoogleChooserPrivate *priv;
173
174 priv = E_GOOGLE_CHOOSER_GET_PRIVATE (object);
175
176 if (priv->source != NULL) {
177 g_object_unref (priv->source);
178 priv->source = NULL;
179 }
180
181 /* Chain up parent's dispose() method. */
182 G_OBJECT_CLASS (e_google_chooser_parent_class)->dispose (object);
183 }
184
185 static void
186 google_chooser_constructed (GObject *object)
187 {
188 GtkTreeView *tree_view;
189 GtkListStore *list_store;
190 GtkCellRenderer *renderer;
191 GtkTreeViewColumn *column;
192
193 tree_view = GTK_TREE_VIEW (object);
194
195 /* Chain up to parent's constructed() method. */
196 G_OBJECT_CLASS (e_google_chooser_parent_class)->constructed (object);
197
198 list_store = gtk_list_store_new (
199 NUM_COLUMNS,
200 GDK_TYPE_COLOR, /* COLUMN_COLOR */
201 G_TYPE_STRING, /* COLUMN_PATH */
202 G_TYPE_STRING, /* COLUMN_TITLE */
203 G_TYPE_BOOLEAN); /* COLUMN_WRITABLE */
204
205 gtk_tree_view_set_model (tree_view, GTK_TREE_MODEL (list_store));
206
207 column = gtk_tree_view_column_new ();
208 gtk_tree_view_column_set_expand (column, TRUE);
209 gtk_tree_view_column_set_title (column, _("Name"));
210 gtk_tree_view_insert_column (tree_view, column, -1);
211
212 renderer = e_cell_renderer_color_new ();
213 gtk_tree_view_column_pack_start (column, renderer, FALSE);
214 gtk_tree_view_column_add_attribute (
215 column, renderer, "color", COLUMN_COLOR);
216
217 renderer = gtk_cell_renderer_text_new ();
218 gtk_tree_view_column_pack_start (column, renderer, TRUE);
219 gtk_tree_view_column_add_attribute (
220 column, renderer, "text", COLUMN_TITLE);
221 }
222
223 static void
224 e_google_chooser_class_init (EGoogleChooserClass *class)
225 {
226 GObjectClass *object_class;
227
228 g_type_class_add_private (class, sizeof (EGoogleChooserPrivate));
229
230 object_class = G_OBJECT_CLASS (class);
231 object_class->set_property = google_chooser_set_property;
232 object_class->get_property = google_chooser_get_property;
233 object_class->dispose = google_chooser_dispose;
234 object_class->constructed = google_chooser_constructed;
235
236 g_object_class_install_property (
237 object_class,
238 PROP_SOURCE,
239 g_param_spec_object (
240 "source",
241 "Source",
242 "Google data source",
243 E_TYPE_SOURCE,
244 G_PARAM_READWRITE |
245 G_PARAM_CONSTRUCT_ONLY));
246 }
247
248 static void
249 e_google_chooser_class_finalize (EGoogleChooserClass *class)
250 {
251 }
252
253 static void
254 e_google_chooser_init (EGoogleChooser *chooser)
255 {
256 chooser->priv = E_GOOGLE_CHOOSER_GET_PRIVATE (chooser);
257 }
258
259 void
260 e_google_chooser_type_register (GTypeModule *type_module)
261 {
262 /* XXX G_DEFINE_DYNAMIC_TYPE declares a static type registration
263 * function, so we have to wrap it with a public function in
264 * order to register types from a separate compilation unit. */
265 e_google_chooser_register_type (type_module);
266 }
267
268 GtkWidget *
269 e_google_chooser_new (ESource *source)
270 {
271 g_return_val_if_fail (E_IS_SOURCE (source), NULL);
272
273 return g_object_new (E_TYPE_GOOGLE_CHOOSER, "source", source, NULL);
274 }
275
276 ESource *
277 e_google_chooser_get_source (EGoogleChooser *chooser)
278 {
279 g_return_val_if_fail (E_IS_GOOGLE_CHOOSER (chooser), NULL);
280
281 return chooser->priv->source;
282 }
283
284 gchar *
285 e_google_chooser_get_decoded_user (EGoogleChooser *chooser)
286 {
287 ESource *source;
288 ESourceAuthentication *authentication_extension;
289 const gchar *user;
290 gchar *decoded_user;
291
292 g_return_val_if_fail (E_IS_GOOGLE_CHOOSER (chooser), NULL);
293
294 source = e_google_chooser_get_source (chooser);
295
296 authentication_extension = e_source_get_extension (
297 source, E_SOURCE_EXTENSION_AUTHENTICATION);
298
299 user = e_source_authentication_get_user (authentication_extension);
300 if (user == NULL || *user == '\0')
301 return NULL;
302
303 /* Decode any encoded 'at' symbols ('%40' -> '@'). */
304 if (strstr (user, "%40") != NULL) {
305 gchar **segments;
306
307 segments = g_strsplit (user, "%40", 0);
308 decoded_user = g_strjoinv ("@", segments);
309 g_strfreev (segments);
310
311 /* If no domain is given, append "@gmail.com". */
312 } else if (strstr (user, "@") == NULL) {
313 decoded_user = g_strconcat (user, "@gmail.com", NULL);
314
315 /* Otherwise the user name should be fine as is. */
316 } else {
317 decoded_user = g_strdup (user);
318 }
319
320 return decoded_user;
321 }
322
323 static void
324 google_chooser_query_cb (GDataService *service,
325 GAsyncResult *result,
326 GSimpleAsyncResult *simple)
327 {
328 GObject *object;
329 GDataFeed *feed;
330 GList *list, *link;
331 GtkTreeView *tree_view;
332 GtkListStore *list_store;
333 GtkTreeModel *tree_model;
334 GError *error = NULL;
335
336 feed = gdata_service_query_finish (service, result, &error);
337
338 if (error != NULL) {
339 g_warn_if_fail (feed == NULL);
340 g_simple_async_result_set_from_error (simple, error);
341 g_simple_async_result_complete (simple);
342 g_object_unref (simple);
343 return;
344 }
345
346 g_return_if_fail (GDATA_IS_FEED (feed));
347
348 list = gdata_feed_get_entries (feed);
349
350 /* This returns a new reference, for reasons passing understanding. */
351 object = g_async_result_get_source_object (G_ASYNC_RESULT (simple));
352
353 tree_view = GTK_TREE_VIEW (object);
354 tree_model = gtk_tree_view_get_model (tree_view);
355 list_store = GTK_LIST_STORE (tree_model);
356
357 gtk_list_store_clear (list_store);
358
359 for (link = list; link != NULL; link = g_list_next (link)) {
360 GDataCalendarCalendar *calendar;
361 GDataEntry *entry;
362 GDataLink *alt;
363 GDataColor color;
364 GdkColor gdkcolor;
365 GtkTreeIter iter;
366 const gchar *uri;
367 const gchar *title;
368 const gchar *access;
369 gboolean writable;
370 gchar *path;
371
372 entry = GDATA_ENTRY (link->data);
373 calendar = GDATA_CALENDAR_CALENDAR (entry);
374
375 /* Skip hidden entries. */
376 if (gdata_calendar_calendar_is_hidden (calendar))
377 continue;
378
379 /* Look up the alternate link, skip if there is none. */
380 alt = gdata_entry_look_up_link (entry, GDATA_LINK_ALTERNATE);
381 if (alt == NULL)
382 continue;
383
384 uri = gdata_link_get_uri (alt);
385 title = gdata_entry_get_title (entry);
386 gdata_calendar_calendar_get_color (calendar, &color);
387 access = gdata_calendar_calendar_get_access_level (calendar);
388
389 if (uri == NULL || *uri == '\0')
390 continue;
391
392 if (title == NULL || *title == '\0')
393 continue;
394
395 path = google_chooser_extract_caldav_events_path (uri);
396
397 gdkcolor.pixel = 0;
398 gdkcolor.red = color.red * 256;
399 gdkcolor.green = color.green * 256;
400 gdkcolor.blue = color.blue * 256;
401
402 if (access == NULL)
403 writable = TRUE;
404 else if (g_ascii_strcasecmp (access, "owner") == 0)
405 writable = TRUE;
406 else if (g_ascii_strcasecmp (access, "contributor") == 0)
407 writable = TRUE;
408 else
409 writable = FALSE;
410
411 gtk_list_store_append (list_store, &iter);
412
413 gtk_list_store_set (
414 list_store, &iter,
415 COLUMN_COLOR, &gdkcolor,
416 COLUMN_PATH, path,
417 COLUMN_TITLE, title,
418 COLUMN_WRITABLE, writable,
419 -1);
420
421 g_free (path);
422 }
423
424 g_object_unref (object);
425 g_object_unref (feed);
426
427 g_simple_async_result_complete (simple);
428 g_object_unref (simple);
429 }
430
431 static void
432 google_chooser_authenticate_cb (GDataClientLoginAuthorizer *authorizer,
433 GAsyncResult *result,
434 GSimpleAsyncResult *simple)
435 {
436 Context *context;
437 GError *error = NULL;
438
439 context = g_simple_async_result_get_op_res_gpointer (simple);
440
441 gdata_client_login_authorizer_authenticate_finish (
442 authorizer, result, &error);
443
444 if (error != NULL) {
445 g_simple_async_result_set_from_error (simple, error);
446 g_simple_async_result_complete (simple);
447 g_object_unref (simple);
448 return;
449 }
450
451 /* We're authenticated, now query for all calendars. */
452
453 gdata_calendar_service_query_all_calendars_async (
454 context->service, NULL, context->cancellable,
455 NULL, NULL, NULL, (GAsyncReadyCallback)
456 google_chooser_query_cb, simple);
457 }
458
459 void
460 e_google_chooser_populate (EGoogleChooser *chooser,
461 GCancellable *cancellable,
462 GAsyncReadyCallback callback,
463 gpointer user_data)
464 {
465 GDataClientLoginAuthorizer *authorizer;
466 GDataCalendarService *service;
467 GSimpleAsyncResult *simple;
468 Context *context;
469 ESource *source;
470 gpointer parent;
471 gchar *password;
472 gchar *prompt;
473 gchar *user;
474
475 g_return_if_fail (E_IS_GOOGLE_CHOOSER (chooser));
476
477 source = e_google_chooser_get_source (chooser);
478
479 authorizer = gdata_client_login_authorizer_new (
480 PACKAGE_NAME, GDATA_TYPE_CALENDAR_SERVICE);
481
482 service = gdata_calendar_service_new (GDATA_AUTHORIZER (authorizer));
483
484 context = g_slice_new0 (Context);
485 context->service = service; /* takes ownership */
486 context->source = g_object_ref (source);
487
488 if (G_IS_CANCELLABLE (cancellable))
489 context->cancellable = g_object_ref (cancellable);
490 else
491 context->cancellable = g_cancellable_new ();
492
493 simple = g_simple_async_result_new (
494 G_OBJECT (chooser), callback,
495 user_data, e_google_chooser_populate);
496
497 g_simple_async_result_set_op_res_gpointer (
498 simple, context, (GDestroyNotify) context_free);
499
500 /* Prompt for a password. */
501
502 user = e_google_chooser_get_decoded_user (chooser);
503
504 parent = gtk_widget_get_toplevel (GTK_WIDGET (chooser));
505 parent = gtk_widget_is_toplevel (parent) ? parent : NULL;
506
507 prompt = g_strdup_printf (
508 _("Enter Google password for user '%s'."), user);
509
510 /* XXX The 'key' (3rd) argument doesn't matter since we're
511 * passing E_PASSWORDS_REMEMBER_NEVER, it just needs to
512 * be non-NULL. This API is degenerating rapidly. */
513 password = e_passwords_ask_password (
514 "", NULL, "bogus key", prompt,
515 E_PASSWORDS_REMEMBER_NEVER |
516 E_PASSWORDS_DISABLE_REMEMBER |
517 E_PASSWORDS_SECRET, NULL, parent);
518
519 g_free (prompt);
520
521 if (password == NULL) {
522 g_cancellable_cancel (context->cancellable);
523 g_simple_async_result_set_error (
524 simple, G_IO_ERROR, G_IO_ERROR_CANCELLED,
525 "%s", _("User declined to provide a password"));
526 g_simple_async_result_complete (simple);
527 g_object_unref (authorizer);
528 g_object_unref (simple);
529 g_free (user);
530 return;
531 }
532
533 /* Try authenticating. */
534
535 gdata_client_login_authorizer_authenticate_async (
536 authorizer, user, password,
537 context->cancellable, (GAsyncReadyCallback)
538 google_chooser_authenticate_cb, simple);
539
540 g_free (password);
541 g_free (user);
542
543 g_object_unref (authorizer);
544 }
545
546 gboolean
547 e_google_chooser_populate_finish (EGoogleChooser *chooser,
548 GAsyncResult *result,
549 GError **error)
550 {
551 GSimpleAsyncResult *simple;
552
553 g_return_val_if_fail (
554 g_simple_async_result_is_valid (
555 result, G_OBJECT (chooser),
556 e_google_chooser_populate), FALSE);
557
558 simple = G_SIMPLE_ASYNC_RESULT (result);
559
560 /* Assume success unless a GError is set. */
561 return !g_simple_async_result_propagate_error (simple, error);
562 }
563
564 gboolean
565 e_google_chooser_apply_selected (EGoogleChooser *chooser)
566 {
567 ESourceSelectable *selectable_extension;
568 ESourceWebdav *webdav_extension;
569 GtkTreeSelection *selection;
570 GtkTreeModel *model;
571 GtkTreeIter iter;
572 ESource *source;
573 GdkColor *color;
574 SoupURI *soup_uri;
575 gchar *color_spec;
576 gchar *title;
577 gchar *path;
578
579 g_return_val_if_fail (E_IS_GOOGLE_CHOOSER (chooser), FALSE);
580
581 source = e_google_chooser_get_source (chooser);
582 selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (chooser));
583
584 if (!gtk_tree_selection_get_selected (selection, &model, &iter))
585 return FALSE;
586
587 gtk_tree_model_get (
588 model, &iter,
589 COLUMN_COLOR, &color,
590 COLUMN_PATH, &path,
591 COLUMN_TITLE, &title,
592 -1);
593
594 selectable_extension = e_source_get_extension (
595 source, E_SOURCE_EXTENSION_CALENDAR);
596
597 webdav_extension = e_source_get_extension (
598 source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
599
600 e_source_set_display_name (source, title);
601
602 e_source_webdav_set_display_name (webdav_extension, title);
603
604 /* XXX Might be easier to expose get/set_path functions? */
605 soup_uri = e_source_webdav_dup_soup_uri (webdav_extension);
606 soup_uri_set_path (soup_uri, path);
607 e_source_webdav_set_soup_uri (webdav_extension, soup_uri);
608 soup_uri_free (soup_uri);
609
610 color_spec = gdk_color_to_string (color);
611 e_source_selectable_set_color (selectable_extension, color_spec);
612 g_free (color_spec);
613
614 gdk_color_free (color);
615 g_free (title);
616 g_free (path);
617
618 return TRUE;
619 }