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