1 /* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /*
3  *  Copyright © 2013 Red Hat, Inc.
4  *
5  *  This file is part of Epiphany.
6  *
7  *  Epiphany is free software: you can redistribute it and/or modify
8  *  it under the terms of the GNU General Public License as published by
9  *  the Free Software Foundation, either version 3 of the License, or
10  *  (at your option) any later version.
11  *
12  *  Epiphany is distributed in the hope that it will be useful,
13  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
14  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  *  GNU General Public License for more details.
16  *
17  *  You should have received a copy of the GNU General Public License
18  *  along with Epiphany.  If not, see <http://www.gnu.org/licenses/>.
19  */
20 
21 #include "config.h"
22 
23 #include <dazzle.h>
24 #include <glib/gi18n.h>
25 #include <gtk/gtk.h>
26 #include <handy.h>
27 #include <string.h>
28 
29 #define SECRET_API_SUBJECT_TO_CHANGE
30 #include <libsecret/secret.h>
31 
32 #include "ephy-shell.h"
33 #include "ephy-gui.h"
34 #include "ephy-uri-helpers.h"
35 #include "passwords-view.h"
36 
37 struct _EphyPasswordsView {
38   EphyDataView parent_instance;
39 
40   EphyPasswordManager *manager;
41   GList *records;
42   GtkWidget *listbox;
43   GtkWidget *confirmation_dialog;
44 
45   GActionGroup *action_group;
46   GCancellable *cancellable;
47 };
48 
49 G_DEFINE_TYPE (EphyPasswordsView, ephy_passwords_view, EPHY_TYPE_DATA_VIEW)
50 
51 static void populate_model (EphyPasswordsView *passwords_view);
52 
53 static void
ephy_passwords_view_dispose(GObject * object)54 ephy_passwords_view_dispose (GObject *object)
55 {
56   EphyPasswordsView *passwords_view = EPHY_PASSWORDS_VIEW (object);
57 
58   g_list_free_full (passwords_view->records, g_object_unref);
59   passwords_view->records = NULL;
60 
61   g_cancellable_cancel (passwords_view->cancellable);
62   g_clear_object (&passwords_view->cancellable);
63 
64   G_OBJECT_CLASS (ephy_passwords_view_parent_class)->dispose (object);
65 }
66 
67 static void
clear_listbox(GtkWidget * listbox)68 clear_listbox (GtkWidget *listbox)
69 {
70   GList *children, *iter;
71 
72   children = gtk_container_get_children (GTK_CONTAINER (listbox));
73 
74   for (iter = children; iter; iter = g_list_next (iter)) {
75     gtk_widget_destroy (GTK_WIDGET (iter->data));
76   }
77 
78   g_list_free (children);
79 }
80 
81 static void
on_search_text_changed(EphyPasswordsView * passwords_view)82 on_search_text_changed (EphyPasswordsView *passwords_view)
83 {
84   ephy_data_view_set_has_search_results (EPHY_DATA_VIEW (passwords_view), FALSE);
85   gtk_list_box_invalidate_filter (GTK_LIST_BOX (passwords_view->listbox));
86 }
87 
88 static void
forget_operation_finished_cb(GObject * source_object,GAsyncResult * result,gpointer user_data)89 forget_operation_finished_cb (GObject      *source_object,
90                               GAsyncResult *result,
91                               gpointer      user_data)
92 {
93   EphyPasswordsView *passwords_view = user_data;
94   g_autoptr (GError) error = NULL;
95 
96   if (!ephy_password_manager_forget_finish (EPHY_PASSWORD_MANAGER (source_object), result, &error)) {
97     if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
98       g_warning ("Failed to forget password: %s", error->message);
99     return;
100   }
101 
102   /* Repopulate the list */
103   ephy_data_view_set_has_data (EPHY_DATA_VIEW (passwords_view), FALSE);
104   populate_model (passwords_view);
105 }
106 
107 static void
forget_clicked(GtkWidget * button,gpointer user_data)108 forget_clicked (GtkWidget *button,
109                 gpointer   user_data)
110 {
111   EphyPasswordRecord *record = EPHY_PASSWORD_RECORD (user_data);
112   EphyPasswordsView *passwords_view = g_object_get_data (G_OBJECT (record), "passwords-view");
113 
114   ephy_password_manager_forget (passwords_view->manager,
115                                 ephy_password_record_get_id (record),
116                                 passwords_view->cancellable,
117                                 forget_operation_finished_cb,
118                                 passwords_view);
119 
120   /* Clear internal state */
121   clear_listbox (passwords_view->listbox);
122   g_list_free_full (passwords_view->records, g_object_unref);
123   passwords_view->records = NULL;
124 
125   /* Present loading spinner while waiting for the async forget op to finish */
126   ephy_data_view_set_is_loading (EPHY_DATA_VIEW (passwords_view), TRUE);
127 }
128 
129 
130 static void
copy_password_clicked(GtkWidget * button,gpointer user_data)131 copy_password_clicked (GtkWidget *button,
132                        gpointer   user_data)
133 {
134   const char *password = user_data;
135 
136   if (password)
137     gtk_clipboard_set_text (gtk_widget_get_clipboard (GTK_WIDGET (button), GDK_SELECTION_CLIPBOARD), password, -1);
138 }
139 
140 static void
copy_username_clicked(GtkWidget * button,gpointer user_data)141 copy_username_clicked (GtkWidget *button,
142                        gpointer   user_data)
143 {
144   const char *username = user_data;
145 
146   if (username)
147     gtk_clipboard_set_text (gtk_widget_get_clipboard (GTK_WIDGET (button), GDK_SELECTION_CLIPBOARD), username, -1);
148 }
149 
150 static void
ephy_passwords_view_class_init(EphyPasswordsViewClass * klass)151 ephy_passwords_view_class_init (EphyPasswordsViewClass *klass)
152 {
153   GObjectClass *object_class = G_OBJECT_CLASS (klass);
154   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
155 
156   object_class->dispose = ephy_passwords_view_dispose;
157 
158   gtk_widget_class_set_template_from_resource (widget_class,
159                                                "/org/gnome/epiphany/gtk/passwords-view.ui");
160 
161   gtk_widget_class_bind_template_child (widget_class, EphyPasswordsView, listbox);
162   gtk_widget_class_bind_template_callback (widget_class, on_search_text_changed);
163 }
164 
165 static void
confirmation_dialog_response_cb(GtkWidget * dialog,int response,EphyPasswordsView * self)166 confirmation_dialog_response_cb (GtkWidget         *dialog,
167                                  int                response,
168                                  EphyPasswordsView *self)
169 {
170   gtk_widget_destroy (dialog);
171 
172   if (response == GTK_RESPONSE_ACCEPT) {
173     ephy_password_manager_forget_all (self->manager);
174 
175     clear_listbox (self->listbox);
176     ephy_data_view_set_has_data (EPHY_DATA_VIEW (self), FALSE);
177 
178     g_list_free_full (self->records, g_object_unref);
179     self->records = NULL;
180   }
181 }
182 
183 static GtkWidget *
confirmation_dialog_construct(EphyPasswordsView * self)184 confirmation_dialog_construct (EphyPasswordsView *self)
185 {
186   GtkWidget *dialog;
187   GtkWidget *button;
188   GtkWidget *window;
189 
190   window = gtk_widget_get_toplevel (GTK_WIDGET (self));
191 
192   dialog = gtk_message_dialog_new (GTK_WINDOW (window),
193                                    GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
194                                    GTK_MESSAGE_WARNING,
195                                    GTK_BUTTONS_CANCEL,
196                                    _("Delete All Passwords?"));
197 
198   gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
199                                             _("This will clear all locally stored passwords, and can not be undone."));
200 
201   gtk_window_group_add_window (ephy_gui_ensure_window_group (GTK_WINDOW (window)),
202                                GTK_WINDOW (dialog));
203 
204   button = gtk_button_new_with_mnemonic (_("_Delete"));
205   gtk_widget_show (button);
206   gtk_dialog_add_action_widget (GTK_DIALOG (dialog), button, GTK_RESPONSE_ACCEPT);
207 
208   gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_CANCEL);
209 
210   g_signal_connect (dialog, "response",
211                     G_CALLBACK (confirmation_dialog_response_cb),
212                     self);
213 
214   return dialog;
215 }
216 
217 static void
forget_all(GSimpleAction * action,GVariant * parameter,gpointer user_data)218 forget_all (GSimpleAction *action,
219             GVariant      *parameter,
220             gpointer       user_data)
221 {
222   EphyPasswordsView *self = EPHY_PASSWORDS_VIEW (user_data);
223 
224   if (!self->confirmation_dialog) {
225     GtkWidget **confirmation_dialog;
226 
227     self->confirmation_dialog = confirmation_dialog_construct (self);
228     confirmation_dialog = &self->confirmation_dialog;
229     g_object_add_weak_pointer (G_OBJECT (self->confirmation_dialog),
230                                (gpointer *)confirmation_dialog);
231   }
232 
233   gtk_widget_show (self->confirmation_dialog);
234 }
235 
236 static void
populate_model_cb(GList * records,gpointer user_data)237 populate_model_cb (GList    *records,
238                    gpointer  user_data)
239 {
240   EphyPasswordsView *passwords_view = EPHY_PASSWORDS_VIEW (user_data);
241 
242   ephy_data_view_set_is_loading (EPHY_DATA_VIEW (passwords_view), FALSE);
243 
244   for (GList *l = records; l && l->data; l = l->next) {
245     EphyPasswordRecord *record = EPHY_PASSWORD_RECORD (l->data);
246     GtkWidget *row;
247     GtkWidget *sub_row;
248     GtkWidget *separator;
249     GtkWidget *button;
250     GtkWidget *image;
251     GtkWidget *entry;
252     const char *text;
253 
254     row = hdy_expander_row_new ();
255     g_object_set_data (G_OBJECT (row), "record", record);
256     hdy_preferences_row_set_title (HDY_PREFERENCES_ROW (row), ephy_password_record_get_origin (record));
257     hdy_expander_row_set_subtitle (HDY_EXPANDER_ROW (row), ephy_password_record_get_username (record));
258     hdy_expander_row_set_show_enable_switch (HDY_EXPANDER_ROW (row), FALSE);
259 
260     button = gtk_button_new_from_icon_name ("edit-copy-symbolic", GTK_ICON_SIZE_BUTTON);
261     gtk_widget_set_valign (button, GTK_ALIGN_CENTER);
262     gtk_widget_set_tooltip_text (button, _("Copy password"));
263     hdy_expander_row_add_action (HDY_EXPANDER_ROW (row), button);
264     g_signal_connect (button, "clicked", G_CALLBACK (copy_password_clicked), (void *)(ephy_password_record_get_password (record)));
265 
266     /* Username */
267     sub_row = hdy_action_row_new ();
268     hdy_preferences_row_set_title (HDY_PREFERENCES_ROW (sub_row), _("Username"));
269     gtk_container_add (GTK_CONTAINER (row), sub_row);
270 
271     entry = gtk_entry_new ();
272     gtk_widget_set_hexpand (entry, TRUE);
273     gtk_widget_set_valign (entry, GTK_ALIGN_CENTER);
274     gtk_editable_set_editable (GTK_EDITABLE (entry), FALSE);
275     gtk_entry_set_alignment (GTK_ENTRY (entry), 1.0f);
276     gtk_entry_set_has_frame (GTK_ENTRY (entry), FALSE);
277 
278     text = ephy_password_record_get_username (record);
279     if (text)
280       gtk_entry_set_text (GTK_ENTRY (entry), text);
281 
282     gtk_container_add (GTK_CONTAINER (sub_row), entry);
283 
284     separator = gtk_separator_new (GTK_ORIENTATION_VERTICAL);
285     gtk_widget_set_margin_top (separator, 8);
286     gtk_widget_set_margin_bottom (separator, 8);
287     gtk_container_add (GTK_CONTAINER (sub_row), separator);
288 
289     button = gtk_button_new_from_icon_name ("edit-copy-symbolic", GTK_ICON_SIZE_BUTTON);
290     g_signal_connect (button, "clicked", G_CALLBACK (copy_username_clicked), (void *)(ephy_password_record_get_username (record)));
291     gtk_widget_set_tooltip_text (button, _("Copy username"));
292     gtk_widget_set_valign (button, GTK_ALIGN_CENTER);
293     gtk_container_add (GTK_CONTAINER (sub_row), button);
294 
295     /* Password */
296     sub_row = hdy_action_row_new ();
297     hdy_preferences_row_set_title (HDY_PREFERENCES_ROW (sub_row), _("Password"));
298     gtk_container_add (GTK_CONTAINER (row), sub_row);
299 
300     entry = gtk_entry_new ();
301     gtk_widget_set_hexpand (entry, TRUE);
302     gtk_widget_set_valign (entry, GTK_ALIGN_CENTER);
303     gtk_editable_set_editable (GTK_EDITABLE (entry), FALSE);
304     gtk_entry_set_alignment (GTK_ENTRY (entry), 1.0f);
305     gtk_entry_set_has_frame (GTK_ENTRY (entry), FALSE);
306     gtk_entry_set_visibility (GTK_ENTRY (entry), FALSE);
307 
308     text = ephy_password_record_get_password (record);
309     if (text)
310       gtk_entry_set_text (GTK_ENTRY (entry), text);
311 
312     gtk_container_add (GTK_CONTAINER (sub_row), entry);
313 
314     separator = gtk_separator_new (GTK_ORIENTATION_VERTICAL);
315     gtk_widget_set_margin_top (separator, 8);
316     gtk_widget_set_margin_bottom (separator, 8);
317     gtk_container_add (GTK_CONTAINER (sub_row), separator);
318 
319     button = gtk_toggle_button_new ();
320     image = gtk_image_new_from_icon_name ("dialog-password-symbolic", GTK_ICON_SIZE_BUTTON);
321     gtk_button_set_image (GTK_BUTTON (button), image);
322     gtk_widget_set_tooltip_text (button, _("Reveal password"));
323     gtk_widget_set_valign (button, GTK_ALIGN_CENTER);
324 
325     g_object_bind_property (G_OBJECT (button), "active", G_OBJECT (entry), "visibility", G_BINDING_DEFAULT);
326     gtk_container_add (GTK_CONTAINER (sub_row), button);
327 
328     /* Remove button */
329     sub_row = hdy_action_row_new ();
330     gtk_container_add (GTK_CONTAINER (row), sub_row);
331 
332     button = gtk_button_new_with_label (_("Remove Password"));
333     gtk_widget_set_valign (button, GTK_ALIGN_CENTER);
334     dzl_gtk_widget_add_style_class (button, GTK_STYLE_CLASS_DESTRUCTIVE_ACTION);
335     g_signal_connect (button, "clicked", G_CALLBACK (forget_clicked), record);
336     gtk_container_add (GTK_CONTAINER (sub_row), button);
337 
338     g_object_set_data (G_OBJECT (record), "passwords-view", passwords_view);
339 
340     gtk_list_box_insert (GTK_LIST_BOX (passwords_view->listbox), row, -1);
341   }
342 
343   if (g_list_length (records)) {
344     ephy_data_view_set_has_data (EPHY_DATA_VIEW (passwords_view), TRUE);
345     gtk_widget_show_all (passwords_view->listbox);
346   }
347 
348   g_assert (!passwords_view->records);
349   passwords_view->records = g_list_copy_deep (records, (GCopyFunc)g_object_ref, NULL);
350 }
351 
352 static void
populate_model(EphyPasswordsView * passwords_view)353 populate_model (EphyPasswordsView *passwords_view)
354 {
355   g_assert (EPHY_IS_PASSWORDS_VIEW (passwords_view));
356   g_assert (!ephy_data_view_get_has_data (EPHY_DATA_VIEW (passwords_view)));
357 
358   ephy_data_view_set_is_loading (EPHY_DATA_VIEW (passwords_view), TRUE);
359   /* Ask for all password records. */
360   ephy_password_manager_query (passwords_view->manager,
361                                NULL, NULL, NULL, NULL, NULL, NULL,
362                                populate_model_cb, passwords_view);
363 }
364 
365 static GActionGroup *
create_action_group(EphyPasswordsView * passwords_view)366 create_action_group (EphyPasswordsView *passwords_view)
367 {
368   const GActionEntry entries[] = {
369     { "forget-all", forget_all },
370   };
371 
372   GSimpleActionGroup *group;
373 
374   group = g_simple_action_group_new ();
375   g_action_map_add_action_entries (G_ACTION_MAP (group), entries, G_N_ELEMENTS (entries), passwords_view);
376 
377   return G_ACTION_GROUP (group);
378 }
379 
380 static void
show_dialog_cb(GtkWidget * widget,gpointer user_data)381 show_dialog_cb (GtkWidget *widget,
382                 gpointer   user_data)
383 {
384   EphyPasswordsView *passwords_view = EPHY_PASSWORDS_VIEW (widget);
385 
386   populate_model (passwords_view);
387 }
388 
389 static gboolean
password_filter(GtkListBoxRow * row,gpointer user_data)390 password_filter (GtkListBoxRow *row,
391                  gpointer       user_data)
392 {
393   EphyPasswordsView *passwords_view = EPHY_PASSWORDS_VIEW (user_data);
394   EphyPasswordRecord *record = g_object_get_data (G_OBJECT (row), "record");
395   const char *username;
396   const char *origin;
397   gboolean visible = FALSE;
398   const char *search_text = ephy_data_view_get_search_text (EPHY_DATA_VIEW (passwords_view));
399 
400   if (search_text == NULL) {
401     gtk_widget_show (GTK_WIDGET (row));
402 
403     return TRUE;
404   }
405 
406   origin = ephy_password_record_get_origin (record);
407   username = ephy_password_record_get_username (record);
408 
409   if (origin != NULL && g_strrstr (origin, search_text) != NULL)
410     visible = TRUE;
411   else if (username != NULL && g_strrstr (username, search_text) != NULL)
412     visible = TRUE;
413 
414   if (visible)
415     ephy_data_view_set_has_search_results (EPHY_DATA_VIEW (passwords_view), TRUE);
416 
417   gtk_widget_set_visible (GTK_WIDGET (row), visible);
418 
419   return visible;
420 }
421 
422 static void
ephy_passwords_view_init(EphyPasswordsView * passwords_view)423 ephy_passwords_view_init (EphyPasswordsView *passwords_view)
424 {
425   passwords_view->manager = ephy_embed_shell_get_password_manager (EPHY_EMBED_SHELL (ephy_shell_get_default ()));
426 
427   gtk_widget_init_template (GTK_WIDGET (passwords_view));
428 
429   passwords_view->action_group = create_action_group (passwords_view);
430   gtk_widget_insert_action_group (GTK_WIDGET (passwords_view), "passwords", passwords_view->action_group);
431 
432   passwords_view->cancellable = g_cancellable_new ();
433 
434   g_signal_connect (GTK_WIDGET (passwords_view), "show", G_CALLBACK (show_dialog_cb), NULL);
435 
436   gtk_list_box_set_filter_func (GTK_LIST_BOX (passwords_view->listbox), password_filter, passwords_view, NULL);
437   gtk_list_box_set_selection_mode (GTK_LIST_BOX (passwords_view->listbox), GTK_SELECTION_NONE);
438 }
439