1 /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
2 /*
3  * Copyright © 2015 Felipe Borges
4  *
5  * This library is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU Lesser General Public
7  * License as published by the Free Software Foundation; either
8  * version 2 of the License, or (at your option) any later version.
9  *
10  * This library is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  * Lesser General Public License for more details.
14  *
15  * You should have received a copy of the GNU Lesser General
16  * Public License along with this library; if not, see <http://www.gnu.org/licenses/>.
17  */
18 
19 #include "config.h"
20 #include <glib/gi18n-lib.h>
21 
22 #include <rest/rest-proxy.h>
23 #include <json-glib/json-glib.h>
24 
25 #include "goahttpclient.h"
26 #include "goaprovider.h"
27 #include "goaprovider-priv.h"
28 #include "goaoauth2provider.h"
29 #include "goalastfmprovider.h"
30 #include "goarestproxy.h"
31 #include "goautils.h"
32 
33 struct _GoaLastfmProvider
34 {
35   GoaOAuth2Provider parent_instance;
36 };
37 
38 G_DEFINE_TYPE_WITH_CODE (GoaLastfmProvider, goa_lastfm_provider, GOA_TYPE_OAUTH2_PROVIDER,
39                          goa_provider_ensure_extension_points_registered ();
40                          g_io_extension_point_implement (GOA_PROVIDER_EXTENSION_POINT_NAME,
41                                                          g_define_type_id,
42                                                          GOA_LASTFM_NAME,
43                                                          0));
44 
45 /* ---------------------------------------------------------------------------------------------------- */
46 
47 static const gchar *
get_provider_type(GoaProvider * _provider)48 get_provider_type (GoaProvider *_provider)
49 {
50   return GOA_LASTFM_NAME;
51 }
52 
53 static gchar *
get_provider_name(GoaProvider * _provider,GoaObject * object)54 get_provider_name (GoaProvider *_provider,
55                    GoaObject   *object)
56 {
57   return g_strdup (_("Last.fm"));
58 }
59 
60 static GoaProviderGroup
get_provider_group(GoaProvider * _provider)61 get_provider_group (GoaProvider *_provider)
62 {
63   return GOA_PROVIDER_GROUP_BRANDED;
64 }
65 
66 static GoaProviderFeatures
get_provider_features(GoaProvider * _provider)67 get_provider_features (GoaProvider *_provider)
68 {
69   return GOA_PROVIDER_FEATURE_BRANDED | GOA_PROVIDER_FEATURE_MUSIC;
70 }
71 
72 static const gchar *
get_request_uri(GoaProvider * provider)73 get_request_uri (GoaProvider *provider)
74 {
75   return "https://ws.audioscrobbler.com/2.0/";
76 }
77 
78 static const gchar *
get_client_id(GoaOAuth2Provider * provider)79 get_client_id (GoaOAuth2Provider *provider)
80 {
81     return GOA_LASTFM_CLIENT_ID;
82 }
83 
84 static const gchar *
get_client_secret(GoaOAuth2Provider * provider)85 get_client_secret (GoaOAuth2Provider *provider)
86 {
87     return GOA_LASTFM_CLIENT_SECRET;
88 }
89 
90 /* ---------------------------------------------------------------------------------------------------- */
91 
92 static gboolean
build_object(GoaProvider * provider,GoaObjectSkeleton * object,GKeyFile * key_file,const gchar * group,GDBusConnection * connection,gboolean just_added,GError ** error)93 build_object (GoaProvider         *provider,
94               GoaObjectSkeleton   *object,
95               GKeyFile            *key_file,
96               const gchar         *group,
97               GDBusConnection     *connection,
98               gboolean             just_added,
99               GError             **error)
100 {
101   GoaAccount *account;
102   GoaMusic *music = NULL;
103   gboolean music_enabled;
104   gboolean ret = FALSE;
105 
106   account = NULL;
107 
108   /* Chain up */
109   if (!GOA_PROVIDER_CLASS (goa_lastfm_provider_parent_class)->build_object (provider,
110                                                                             object,
111                                                                             key_file,
112                                                                             group,
113                                                                             connection,
114                                                                             just_added,
115                                                                             error))
116     goto out;
117 
118   account = goa_object_get_account (GOA_OBJECT (object));
119 
120   /* Music */
121   music = goa_object_get_music (GOA_OBJECT (object));
122   music_enabled = g_key_file_get_boolean (key_file, group, "MusicEnabled", NULL);
123   if (music_enabled)
124     {
125       if (music == NULL)
126         {
127           music = goa_music_skeleton_new ();
128           goa_object_skeleton_set_music (object, music);
129         }
130     }
131   else
132     {
133       if (music != NULL)
134         goa_object_skeleton_set_music (object, NULL);
135     }
136 
137   if (just_added)
138     {
139       goa_account_set_music_disabled (account, !music_enabled);
140 
141       g_signal_connect (account,
142                         "notify::music-disabled",
143                         G_CALLBACK (goa_util_account_notify_property_cb),
144                         (gpointer) "MusicEnabled");
145     }
146 
147   ret = TRUE;
148 
149  out:
150   g_clear_object (&music);
151   g_clear_object (&account);
152   return ret;
153 }
154 
155 static gboolean
lastfm_login_sync(GoaProvider * provider,const gchar * username,const gchar * password,GError ** error)156 lastfm_login_sync (GoaProvider                  *provider,
157                    const gchar                  *username,
158                    const gchar                  *password,
159                    GError                       **error)
160 {
161   JsonParser *parser;
162   JsonObject *json_obj;
163   JsonObject *session_obj;
164   JsonNode *root;
165   RestProxyCall *call;
166   const gchar *payload;
167   gchar *sig;
168   gchar *sig_md5;
169   gboolean ret;
170 
171   call = NULL;
172   parser = NULL;
173   ret = FALSE;
174 
175   sig = g_strdup_printf ("api_key%s"
176                          "methodauth.getMobileSession"
177                          "password%s"
178                          "username%s"
179                          "%s",
180                          GOA_LASTFM_CLIENT_ID,
181                          password,
182                          username,
183                          GOA_LASTFM_CLIENT_SECRET);
184   sig_md5 = g_compute_checksum_for_string (G_CHECKSUM_MD5, sig, -1);
185 
186   call = rest_proxy_new_call (goa_rest_proxy_new (get_request_uri (provider), FALSE));
187 
188   rest_proxy_call_set_method (call, "POST");
189   rest_proxy_call_add_header (call, "Content-Type", "application/x-www-form-urlencoded");
190   rest_proxy_call_add_param (call, "method", "auth.getMobileSession");
191   rest_proxy_call_add_param (call, "api_key", GOA_LASTFM_CLIENT_ID);
192   rest_proxy_call_add_param (call, "username", username);
193   rest_proxy_call_add_param (call, "password", password);
194   rest_proxy_call_add_param (call, "api_sig", sig_md5);
195   rest_proxy_call_add_param (call, "format", "json");
196 
197   if (!rest_proxy_call_sync (call, error))
198     goto out;
199 
200   parser = json_parser_new ();
201   payload = rest_proxy_call_get_payload (call);
202   if (payload == NULL)
203     {
204       g_set_error (error, GOA_ERROR, GOA_ERROR_FAILED, _("Could not parse response"));
205       goto out;
206     }
207 
208   if (!json_parser_load_from_data (parser,
209                                    payload,
210                                    rest_proxy_call_get_payload_length (call),
211                                    NULL))
212     {
213       g_set_error (error, GOA_ERROR, GOA_ERROR_FAILED, _("Could not parse response"));
214       goto out;
215     }
216 
217   root = json_parser_get_root (parser);
218   json_obj = json_node_get_object (root);
219   if (!json_object_has_member (json_obj, "session"))
220     {
221       g_warning ("Did not find session in JSON data");
222       g_set_error (error, GOA_ERROR, GOA_ERROR_FAILED, _("Could not parse response"));
223       goto out;
224     }
225 
226   session_obj = json_node_get_object (json_object_get_member (json_obj, "session"));
227   if (!json_object_has_member (session_obj, "name"))
228     {
229       g_warning ("Did not find session.name in JSON data");
230       g_set_error (error, GOA_ERROR, GOA_ERROR_FAILED, _("Could not parse response"));
231       goto out;
232     }
233   if (!json_object_has_member (session_obj, "key"))
234     {
235       g_warning ("Did not find session.key in JSON data");
236       g_set_error (error, GOA_ERROR, GOA_ERROR_FAILED, _("Could not parse response"));
237       goto out;
238     }
239 
240   ret = TRUE;
241 
242  out:
243   g_clear_object (&parser);
244   g_clear_object (&call);
245   return ret;
246 }
247 
248 /* ---------------------------------------------------------------------------------------------------- */
249 static gboolean
ensure_credentials_sync(GoaProvider * provider,GoaObject * object,gint * out_expires_in,GCancellable * cancellable,GError ** error)250 ensure_credentials_sync (GoaProvider         *provider,
251                          GoaObject           *object,
252                          gint                *out_expires_in,
253                          GCancellable        *cancellable,
254                          GError             **error)
255 {
256   gchar *username = NULL;
257   gchar *password = NULL;
258   gboolean ret = FALSE;
259 
260   if (!goa_utils_get_credentials (provider, object, "password", &username, &password, cancellable, error))
261     {
262       if (error != NULL)
263         {
264           (*error)->domain = GOA_ERROR;
265           (*error)->code = GOA_ERROR_NOT_AUTHORIZED;
266         }
267       goto out;
268     }
269 
270   if (!lastfm_login_sync (provider, username, password, error))
271     {
272       if (error != NULL)
273         {
274           g_prefix_error (error,
275                           /* Translators: the first %s is the username
276                            * (eg., debarshi.ray@gmail.com or rishi), and the
277                            * (%s, %d) is the error domain and code.
278                            */
279                           _("Invalid password with username “%s” (%s, %d): "),
280                           username,
281                           g_quark_to_string ((*error)->domain),
282                           (*error)->code);
283           (*error)->domain = GOA_ERROR;
284           (*error)->code = GOA_ERROR_NOT_AUTHORIZED;
285         }
286       goto out;
287     }
288 
289   if (out_expires_in != NULL)
290     *out_expires_in = 0;
291 
292   ret = TRUE;
293 
294  out:
295   g_free (username);
296   g_free (password);
297   return ret;
298 }
299 
300 /* ---------------------------------------------------------------------------------------------------- */
301 
302 static void
add_entry(GtkWidget * grid,gint row,const gchar * text,GtkWidget ** out_entry)303 add_entry (GtkWidget     *grid,
304            gint           row,
305            const gchar   *text,
306            GtkWidget    **out_entry)
307 {
308   GtkStyleContext *context;
309   GtkWidget *label;
310   GtkWidget *entry;
311 
312   label = gtk_label_new_with_mnemonic (text);
313   context = gtk_widget_get_style_context (label);
314   gtk_style_context_add_class (context, GTK_STYLE_CLASS_DIM_LABEL);
315   gtk_widget_set_halign (label, GTK_ALIGN_END);
316   gtk_widget_set_hexpand (label, TRUE);
317   gtk_grid_attach (GTK_GRID (grid), label, 0, row, 1, 1);
318 
319   entry = gtk_entry_new ();
320   gtk_widget_set_hexpand (entry, TRUE);
321   gtk_entry_set_activates_default (GTK_ENTRY (entry), TRUE);
322   gtk_grid_attach (GTK_GRID (grid), entry, 1, row, 3, 1);
323 
324   gtk_label_set_mnemonic_widget (GTK_LABEL (label), entry);
325   if (out_entry != NULL)
326     *out_entry = entry;
327 }
328 
329 /* ---------------------------------------------------------------------------------------------------- */
330 
331 typedef struct
332 {
333   GCancellable *cancellable;
334 
335   GtkDialog *dialog;
336   GMainLoop *loop;
337 
338   GtkWidget *cluebar;
339   GtkWidget *cluebar_label;
340   GtkWidget *connect_button;
341   GtkWidget *progress_grid;
342 
343   GtkWidget *username;
344   GtkWidget *password;
345 
346   gchar *account_object_path;
347   gchar *access_token;
348 
349   GError *error;
350 } AddAccountData;
351 
352 /* ---------------------------------------------------------------------------------------------------- */
353 
354 static void
on_username_or_password_changed(GtkEditable * editable,gpointer user_data)355 on_username_or_password_changed (GtkEditable *editable, gpointer user_data)
356 {
357   AddAccountData *data = user_data;
358   gboolean can_add;
359   gchar *username;
360   gchar *password;
361 
362   can_add = FALSE;
363   username = NULL;
364   password = NULL;
365 
366   username = g_strdup (gtk_entry_get_text (GTK_ENTRY (data->username)));
367   password = g_strdup (gtk_entry_get_text (GTK_ENTRY (data->password)));
368   if ((username == NULL) || (password == NULL))
369     goto out;
370 
371   can_add = gtk_entry_get_text_length (GTK_ENTRY (data->username)) != 0
372             && gtk_entry_get_text_length (GTK_ENTRY (data->password)) != 0;
373 
374  out:
375   gtk_dialog_set_response_sensitive (data->dialog, GTK_RESPONSE_OK, can_add);
376   g_free (username);
377   g_free (password);
378 }
379 
380 static void
create_account_details_ui(GoaProvider * provider,GtkDialog * dialog,GtkBox * vbox,gboolean new_account,AddAccountData * data)381 create_account_details_ui (GoaProvider    *provider,
382                            GtkDialog      *dialog,
383                            GtkBox         *vbox,
384                            gboolean        new_account,
385                            AddAccountData *data)
386 {
387   GtkWidget *grid0;
388   GtkWidget *grid1;
389   GtkWidget *label;
390   GtkWidget *spinner;
391   gint row;
392   gint width;
393 
394   goa_utils_set_dialog_title (provider, dialog, new_account);
395 
396   grid0 = gtk_grid_new ();
397   gtk_container_set_border_width (GTK_CONTAINER (grid0), 5);
398   gtk_widget_set_margin_bottom (grid0, 6);
399   gtk_orientable_set_orientation (GTK_ORIENTABLE (grid0), GTK_ORIENTATION_VERTICAL);
400   gtk_grid_set_row_spacing (GTK_GRID (grid0), 12);
401   gtk_container_add (GTK_CONTAINER (vbox), grid0);
402 
403   data->cluebar = gtk_info_bar_new ();
404   gtk_info_bar_set_message_type (GTK_INFO_BAR (data->cluebar), GTK_MESSAGE_ERROR);
405   gtk_widget_set_hexpand (data->cluebar, TRUE);
406   gtk_widget_set_no_show_all (data->cluebar, TRUE);
407   gtk_container_add (GTK_CONTAINER (grid0), data->cluebar);
408 
409   data->cluebar_label = gtk_label_new ("");
410   gtk_label_set_line_wrap (GTK_LABEL (data->cluebar_label), TRUE);
411   gtk_container_add (GTK_CONTAINER (gtk_info_bar_get_content_area (GTK_INFO_BAR (data->cluebar))),
412                      data->cluebar_label);
413 
414   grid1 = gtk_grid_new ();
415   gtk_grid_set_column_spacing (GTK_GRID (grid1), 12);
416   gtk_grid_set_row_spacing (GTK_GRID (grid1), 12);
417   gtk_container_add (GTK_CONTAINER (grid0), grid1);
418 
419   row = 0;
420   add_entry (grid1, row++, _("User_name"), &data->username);
421   add_entry (grid1, row++, _("_Password"), &data->password);
422   gtk_entry_set_visibility (GTK_ENTRY (data->password), FALSE);
423 
424   gtk_widget_grab_focus ((new_account) ? data->username : data->password);
425 
426   g_signal_connect (data->username, "changed", G_CALLBACK (on_username_or_password_changed), data);
427   g_signal_connect (data->password, "changed", G_CALLBACK (on_username_or_password_changed), data);
428 
429   gtk_dialog_add_button (data->dialog, _("_Cancel"), GTK_RESPONSE_CANCEL);
430   data->connect_button = gtk_dialog_add_button (data->dialog, _("C_onnect"), GTK_RESPONSE_OK);
431   gtk_dialog_set_default_response (data->dialog, GTK_RESPONSE_OK);
432   gtk_dialog_set_response_sensitive (data->dialog, GTK_RESPONSE_OK, FALSE);
433 
434   data->progress_grid = gtk_grid_new ();
435   gtk_widget_set_no_show_all (data->progress_grid, TRUE);
436   gtk_orientable_set_orientation (GTK_ORIENTABLE (data->progress_grid), GTK_ORIENTATION_HORIZONTAL);
437   gtk_grid_set_column_spacing (GTK_GRID (data->progress_grid), 3);
438   gtk_container_add (GTK_CONTAINER (grid0), data->progress_grid);
439 
440   spinner = gtk_spinner_new ();
441   gtk_widget_set_size_request (spinner, 20, 20);
442   gtk_widget_show (spinner);
443   gtk_spinner_start (GTK_SPINNER (spinner));
444   gtk_container_add (GTK_CONTAINER (data->progress_grid), spinner);
445 
446   label = gtk_label_new (_("Connecting…"));
447   gtk_widget_show (label);
448   gtk_container_add (GTK_CONTAINER (data->progress_grid), label);
449 
450   if (new_account)
451     {
452       gtk_window_get_size (GTK_WINDOW (data->dialog), &width, NULL);
453       gtk_window_set_default_size (GTK_WINDOW (data->dialog), width, -1);
454     }
455   else
456     {
457       GtkWindow *parent;
458 
459       /* Keep in sync with GoaPanelAddAccountDialog in
460        * gnome-control-center.
461        */
462       parent = gtk_window_get_transient_for (GTK_WINDOW (data->dialog));
463       if (parent != NULL)
464         {
465           gtk_window_get_size (parent, &width, NULL);
466           gtk_window_set_default_size (GTK_WINDOW (data->dialog), (gint) (0.5 * width), -1);
467         }
468     }
469 }
470 
471 /* ---------------------------------------------------------------------------------------------------- */
472 
473 static void
add_account_cb(GoaManager * manager,GAsyncResult * res,gpointer user_data)474 add_account_cb (GoaManager *manager, GAsyncResult *res, gpointer user_data)
475 {
476   AddAccountData *data = user_data;
477   goa_manager_call_add_account_finish (manager,
478                                        &data->account_object_path,
479                                        res,
480                                        &data->error);
481   g_main_loop_quit (data->loop);
482 }
483 
484 static void
check_cb(RestProxyCall * call,const GError * error,GObject * weak_object,gpointer user_data)485 check_cb (RestProxyCall *call,
486           const GError *error,
487           GObject *weak_object,
488           gpointer user_data)
489 {
490   AddAccountData *data = user_data;
491   JsonNode *session;
492   JsonParser *parser;
493   JsonObject *json_obj;
494   JsonObject *session_obj;
495   const gchar *payload;
496 
497   parser = NULL;
498 
499   parser = json_parser_new ();
500   payload = rest_proxy_call_get_payload (call);
501 
502   if (payload == NULL)
503     {
504       g_set_error (&data->error, GOA_ERROR, GOA_ERROR_FAILED, _("Could not parse response"));
505       goto out;
506     }
507 
508   if (!json_parser_load_from_data (parser,
509                                    payload,
510                                    rest_proxy_call_get_payload_length (call),
511                                    &data->error))
512     {
513       g_set_error (&data->error, GOA_ERROR, GOA_ERROR_FAILED, _("Could not parse response"));
514       goto out;
515     }
516 
517   json_obj = json_node_get_object (json_parser_get_root (parser));
518   if (!json_object_has_member (json_obj, "session"))
519     {
520       g_warning ("Did not find session in JSON data");
521       g_set_error (&data->error, GOA_ERROR, GOA_ERROR_FAILED, _("Authentication failed"));
522       goto out;
523     }
524 
525   session = json_object_get_member (json_obj, "session");
526   session_obj = json_node_get_object (session);
527   if (!json_object_has_member (session_obj, "name"))
528     {
529       g_warning ("Did not find session.name in JSON data");
530       g_set_error (&data->error, GOA_ERROR, GOA_ERROR_FAILED, _("Could not parse response"));
531       goto out;
532     }
533   if (!json_object_has_member (session_obj, "key"))
534     {
535       g_warning ("Did not find session.key in JSON data");
536       g_set_error (&data->error, GOA_ERROR, GOA_ERROR_FAILED, _("Could not parse response"));
537       goto out;
538     }
539 
540   data->access_token = g_strdup (json_object_get_string_member (session_obj, "key"));
541 
542  out:
543   g_main_loop_quit (data->loop);
544   gtk_widget_set_sensitive (data->connect_button, TRUE);
545   gtk_widget_hide (data->progress_grid);
546   g_clear_object (&parser);
547 }
548 
549 static void
dialog_response_cb(GtkDialog * dialog,gint response_id,gpointer user_data)550 dialog_response_cb (GtkDialog *dialog, gint response_id, gpointer user_data)
551 {
552   AddAccountData *data = user_data;
553 
554   if (response_id == GTK_RESPONSE_CANCEL)
555     g_cancellable_cancel (data->cancellable);
556 }
557 
558 static void
on_rest_proxy_call_cancelled_cb(GCancellable * cancellable,RestProxyCall * call)559 on_rest_proxy_call_cancelled_cb (GCancellable *cancellable, RestProxyCall *call)
560 {
561   rest_proxy_call_cancel (call);
562 }
563 
564 static void
lastfm_login(GoaProvider * provider,const gchar * username,const gchar * password,GCancellable * cancellable,RestProxyCallAsyncCallback callback,gpointer user_data)565 lastfm_login (GoaProvider                  *provider,
566               const gchar                  *username,
567               const gchar                  *password,
568               GCancellable                 *cancellable,
569               RestProxyCallAsyncCallback   callback,
570               gpointer                     user_data)
571 {
572   AddAccountData *data = user_data;
573   RestProxyCall *call;
574   gchar *sig;
575   gchar *sig_md5;
576 
577   call = NULL;
578 
579   sig = g_strdup_printf ("api_key%s"
580                          "methodauth.getMobileSession"
581                          "password%s"
582                          "username%s"
583                          "%s",
584                          GOA_LASTFM_CLIENT_ID,
585                          password,
586                          username,
587                          GOA_LASTFM_CLIENT_SECRET);
588   sig_md5 = g_compute_checksum_for_string (G_CHECKSUM_MD5, sig, -1);
589 
590   call = rest_proxy_new_call (goa_rest_proxy_new (get_request_uri (provider), FALSE));
591 
592   rest_proxy_call_set_method (call, "POST");
593   rest_proxy_call_add_header (call, "Content-Type", "application/x-www-form-urlencoded");
594   rest_proxy_call_add_param (call, "method", "auth.getMobileSession");
595   rest_proxy_call_add_param (call, "api_key", GOA_LASTFM_CLIENT_ID);
596   rest_proxy_call_add_param (call, "username", username);
597   rest_proxy_call_add_param (call, "password", password);
598   rest_proxy_call_add_param (call, "api_sig", sig_md5);
599   rest_proxy_call_add_param (call, "format", "json");
600 
601   rest_proxy_call_async (call, callback, NULL, data, &data->error);
602 
603   g_signal_connect (cancellable, "cancelled", G_CALLBACK (on_rest_proxy_call_cancelled_cb), call);
604 
605   g_free (sig_md5);
606   g_free (sig);
607   g_object_unref (call);
608 }
609 
610 /* ---------------------------------------------------------------------------------------------------- */
611 
612 static GoaObject *
add_account(GoaProvider * provider,GoaClient * client,GtkDialog * dialog,GtkBox * vbox,GError ** error)613 add_account (GoaProvider    *provider,
614              GoaClient      *client,
615              GtkDialog      *dialog,
616              GtkBox         *vbox,
617              GError        **error)
618 {
619   AddAccountData data;
620   GVariantBuilder credentials;
621   GVariantBuilder details;
622   GoaObject *ret;
623   const gchar *password;
624   const gchar *username;
625   const gchar *provider_type;
626   gint response;
627 
628   ret = NULL;
629 
630   memset (&data, 0, sizeof (AddAccountData));
631   data.cancellable = g_cancellable_new ();
632   data.loop = g_main_loop_new (NULL, FALSE);
633   data.dialog = dialog;
634   data.error = NULL;
635 
636   create_account_details_ui (provider, dialog, vbox, TRUE, &data);
637   gtk_widget_show_all (GTK_WIDGET (vbox));
638   g_signal_connect (dialog, "response", G_CALLBACK (dialog_response_cb), &data);
639 
640  login_again:
641   response = gtk_dialog_run (dialog);
642   if (response != GTK_RESPONSE_OK)
643     {
644       g_set_error (&data.error, GOA_ERROR, GOA_ERROR_DIALOG_DISMISSED, _("Dialog was dismissed"));
645       goto out;
646     }
647 
648   username = gtk_entry_get_text (GTK_ENTRY (data.username));
649   password = gtk_entry_get_text (GTK_ENTRY (data.password));
650 
651   /* See if there's already an account of this type with the
652    * given identity
653    */
654   provider_type = goa_provider_get_provider_type (provider);
655   if (!goa_utils_check_duplicate (client,
656                                   username,
657                                   username,
658                                   provider_type,
659                                   (GoaPeekInterfaceFunc) goa_object_peek_oauth2_based,
660                                   &data.error))
661     goto out;
662 
663   g_cancellable_reset (data.cancellable);
664   lastfm_login (provider,
665                 username,
666                 password,
667                 data.cancellable,
668                 (RestProxyCallAsyncCallback) check_cb,
669                 &data);
670 
671   gtk_widget_set_sensitive (data.connect_button, FALSE);
672   gtk_widget_show (data.progress_grid);
673   g_main_loop_run (data.loop);
674 
675   if (g_cancellable_is_cancelled (data.cancellable))
676     {
677       g_prefix_error (&data.error,
678                       _("Dialog was dismissed (%s, %d): "),
679                       g_quark_to_string (data.error->domain),
680                       data.error->code);
681       data.error->domain = GOA_ERROR;
682       data.error->code = GOA_ERROR_DIALOG_DISMISSED;
683       g_message ("%s", data.error->message);
684       goto out;
685     }
686   else if (data.error != NULL)
687     {
688       gchar *markup;
689 
690       gtk_button_set_label (GTK_BUTTON (data.connect_button), _("_Try Again"));
691 
692       markup = g_strdup_printf ("<b>%s:</b>\n%s", _("Error connecting to Last.fm"), data.error->message);
693       g_clear_error (&data.error);
694 
695       gtk_label_set_markup (GTK_LABEL (data.cluebar_label), markup);
696       g_free (markup);
697 
698       gtk_widget_set_no_show_all (data.cluebar, FALSE);
699       gtk_widget_show_all (data.cluebar);
700       goto login_again;
701     }
702 
703   gtk_widget_hide (GTK_WIDGET (dialog));
704 
705   g_variant_builder_init (&credentials, G_VARIANT_TYPE_VARDICT);
706   g_variant_builder_add (&credentials, "{sv}", "password", g_variant_new_string (password));
707   g_variant_builder_add (&credentials, "{sv}", "access_token", g_variant_new_string (data.access_token));
708 
709   g_variant_builder_init (&details, G_VARIANT_TYPE ("a{ss}"));
710   g_variant_builder_add (&details, "{ss}", "MusicEnabled", "true");
711 
712   /* OK, everything is dandy, add the account */
713   /* we want the GoaClient to update before this method returns (so it
714    * can create a proxy for the new object) so run the mainloop while
715    * waiting for this to complete
716    */
717   goa_manager_call_add_account (goa_client_get_manager (client),
718                                 goa_provider_get_provider_type (provider),
719                                 username,
720                                 username,
721                                 g_variant_builder_end (&credentials),
722                                 g_variant_builder_end (&details),
723                                 NULL, /* GCancellable* */
724                                 (GAsyncReadyCallback) add_account_cb,
725                                 &data);
726   g_main_loop_run (data.loop);
727   if (data.error != NULL)
728     goto out;
729 
730   ret = GOA_OBJECT (g_dbus_object_manager_get_object (goa_client_get_object_manager (client),
731                                                       data.account_object_path));
732 
733  out:
734   /* We might have an object even when data.error is set.
735    * eg., if we failed to store the credentials in the keyring.
736    */
737   if (data.error != NULL)
738     g_propagate_error (error, data.error);
739   else
740     g_assert (ret != NULL);
741 
742   g_signal_handlers_disconnect_by_func (dialog, dialog_response_cb, &data);
743 
744   g_free (data.access_token);
745   g_free (data.account_object_path);
746   g_clear_pointer (&data.loop, g_main_loop_unref);
747   g_clear_object (&data.cancellable);
748   return ret;
749 }
750 
751 /* ---------------------------------------------------------------------------------------------------- */
752 
753 static gboolean
refresh_account(GoaProvider * provider,GoaClient * client,GoaObject * object,GtkWindow * parent,GError ** error)754 refresh_account (GoaProvider    *provider,
755                  GoaClient      *client,
756                  GoaObject      *object,
757                  GtkWindow      *parent,
758                  GError        **error)
759 {
760   AddAccountData data;
761   GVariantBuilder builder;
762   GoaAccount *account;
763   GtkWidget *dialog;
764   GtkWidget *vbox;
765   gboolean ret;
766   const gchar *password;
767   const gchar *username;
768   gint response;
769 
770   g_return_val_if_fail (GOA_IS_LASTFM_PROVIDER (provider), FALSE);
771   g_return_val_if_fail (GOA_IS_CLIENT (client), FALSE);
772   g_return_val_if_fail (GOA_IS_OBJECT (object), FALSE);
773   g_return_val_if_fail (parent == NULL || GTK_IS_WINDOW (parent), FALSE);
774   g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
775 
776   ret = FALSE;
777 
778   dialog = gtk_dialog_new_with_buttons (NULL,
779                                         parent,
780                                         GTK_DIALOG_MODAL
781                                         | GTK_DIALOG_DESTROY_WITH_PARENT
782                                         | GTK_DIALOG_USE_HEADER_BAR,
783                                         NULL,
784                                         NULL);
785   gtk_container_set_border_width (GTK_CONTAINER (dialog), 12);
786   gtk_window_set_modal (GTK_WINDOW (dialog), TRUE);
787   gtk_window_set_resizable (GTK_WINDOW (dialog), FALSE);
788 
789   vbox = gtk_dialog_get_content_area (GTK_DIALOG (dialog));
790   gtk_box_set_spacing (GTK_BOX (vbox), 12);
791 
792   memset (&data, 0, sizeof (AddAccountData));
793   data.cancellable = g_cancellable_new ();
794   data.loop = g_main_loop_new (NULL, FALSE);
795   data.dialog = GTK_DIALOG (dialog);
796   data.error = NULL;
797 
798   create_account_details_ui (provider, GTK_DIALOG (dialog), GTK_BOX (vbox), FALSE, &data);
799 
800   account = goa_object_peek_account (object);
801   username = goa_account_get_identity (account);
802   gtk_entry_set_text (GTK_ENTRY (data.username), username);
803   gtk_editable_set_editable (GTK_EDITABLE (data.username), FALSE);
804 
805   gtk_widget_show_all (dialog);
806   g_signal_connect (dialog, "response", G_CALLBACK (dialog_response_cb), &data);
807 
808  login_again:
809   response = gtk_dialog_run (GTK_DIALOG (dialog));
810   if (response != GTK_RESPONSE_OK)
811     {
812       g_set_error (&data.error, GOA_ERROR, GOA_ERROR_DIALOG_DISMISSED, _("Dialog was dismissed"));
813       goto out;
814     }
815 
816   password = gtk_entry_get_text (GTK_ENTRY (data.password));
817   g_cancellable_reset (data.cancellable);
818   lastfm_login (provider,
819                 username,
820                 password,
821                 data.cancellable,
822                 (RestProxyCallAsyncCallback) check_cb,
823                 &data);
824   gtk_widget_set_sensitive (data.connect_button, FALSE);
825   gtk_widget_show (data.progress_grid);
826   g_main_loop_run (data.loop);
827 
828   if (g_cancellable_is_cancelled (data.cancellable))
829     {
830       g_prefix_error (&data.error,
831                       _("Dialog was dismissed (%s, %d): "),
832                       g_quark_to_string (data.error->domain),
833                       data.error->code);
834       data.error->domain = GOA_ERROR;
835       data.error->code = GOA_ERROR_DIALOG_DISMISSED;
836       goto out;
837     }
838   else if (data.error != NULL)
839     {
840       gchar *markup;
841 
842       markup = g_strdup_printf ("<b>%s:</b>\n%s", _("Error connecting to Last.fm"), data.error->message);
843       g_clear_error (&data.error);
844 
845       gtk_label_set_markup (GTK_LABEL (data.cluebar_label), markup);
846       g_free (markup);
847 
848       gtk_button_set_label (GTK_BUTTON (data.connect_button), _("_Try Again"));
849       gtk_widget_set_no_show_all (data.cluebar, FALSE);
850       gtk_widget_show_all (data.cluebar);
851       goto login_again;
852     }
853 
854   /* TODO: run in worker thread */
855   g_variant_builder_init (&builder, G_VARIANT_TYPE_VARDICT);
856   g_variant_builder_add (&builder, "{sv}", "password", g_variant_new_string (password));
857   g_variant_builder_add (&builder, "{sv}", "access_token", g_variant_new_string (data.access_token));
858 
859   if (!goa_utils_store_credentials_for_object_sync (provider,
860                                                     object,
861                                                     g_variant_builder_end (&builder),
862                                                     NULL, /* GCancellable */
863                                                     &data.error))
864     goto out;
865 
866   goa_account_call_ensure_credentials (account,
867                                        NULL, /* GCancellable */
868                                        NULL, NULL); /* callback, user_data */
869 
870   ret = TRUE;
871  out:
872   if (data.error != NULL)
873     g_propagate_error (error, data.error);
874 
875   gtk_widget_destroy (dialog);
876   g_clear_pointer (&data.loop, g_main_loop_unref);
877   g_free (data.access_token);
878   g_clear_object (&data.cancellable);
879   return ret;
880 }
881 
882 /* ---------------------------------------------------------------------------------------------------- */
883 
884 static void
goa_lastfm_provider_init(GoaLastfmProvider * provider)885 goa_lastfm_provider_init (GoaLastfmProvider *provider)
886 {
887 }
888 
889 static void
goa_lastfm_provider_class_init(GoaLastfmProviderClass * klass)890 goa_lastfm_provider_class_init (GoaLastfmProviderClass *klass)
891 {
892   GoaProviderClass *provider_class;
893   GoaOAuth2ProviderClass *oauth2_class = GOA_OAUTH2_PROVIDER_CLASS (klass);
894 
895   provider_class = GOA_PROVIDER_CLASS (klass);
896   provider_class->get_provider_type          = get_provider_type;
897   provider_class->get_provider_name          = get_provider_name;
898   provider_class->get_provider_group         = get_provider_group;
899   provider_class->get_provider_features      = get_provider_features;
900   provider_class->add_account                = add_account;
901   provider_class->refresh_account            = refresh_account;
902   provider_class->build_object               = build_object;
903   provider_class->ensure_credentials_sync    = ensure_credentials_sync;
904 
905   oauth2_class->get_client_id              = get_client_id;
906   oauth2_class->get_client_secret          = get_client_secret;
907 }
908