1/*
2 * Copyright (C) 2009 Igalia S.L.
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Library General Public
6 * License as published by the Free Software Foundation; either
7 * version 2 of the License, or (at your option) any later version.
8 *
9 * This library 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 * Library General Public License for more details.
13 *
14 * You should have received a copy of the GNU Library General Public License
15 * along with this library; see the file COPYING.LIB.  If not, write to
16 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17 * Boston, MA 02110-1301, USA.
18 */
19
20#include "config.h"
21
22#define LIBSOUP_I_HAVE_READ_BUG_594377_AND_KNOW_SOUP_PASSWORD_MANAGER_MIGHT_GO_AWAY
23
24#include <glib/gi18n-lib.h>
25#include <gtk/gtk.h>
26#include <libsoup/soup.h>
27
28#include "GtkVersioning.h"
29#include "webkitmarshal.h"
30#include "webkitsoupauthdialog.h"
31
32/**
33 * SECTION:webkitsoupauthdialog
34 * @short_description: A #SoupSessionFeature to provide a simple
35 * authentication dialog for HTTP basic auth support.
36 *
37 * #WebKitSoupAuthDialog is a #SoupSessionFeature that you can attach to your
38 * #SoupSession to provide a simple authentication dialog while
39 * handling HTTP basic auth. It is built as a simple C-only module
40 * to ease reuse.
41 */
42
43static void webkit_soup_auth_dialog_session_feature_init(SoupSessionFeatureInterface* feature_interface, gpointer interface_data);
44static void attach(SoupSessionFeature* manager, SoupSession* session);
45static void detach(SoupSessionFeature* manager, SoupSession* session);
46
47enum {
48    CURRENT_TOPLEVEL,
49    LAST_SIGNAL
50};
51
52static guint signals[LAST_SIGNAL] = { 0 };
53
54G_DEFINE_TYPE_WITH_CODE(WebKitSoupAuthDialog, webkit_soup_auth_dialog, G_TYPE_OBJECT,
55                        G_IMPLEMENT_INTERFACE(SOUP_TYPE_SESSION_FEATURE,
56                                              webkit_soup_auth_dialog_session_feature_init))
57
58static void webkit_soup_auth_dialog_class_init(WebKitSoupAuthDialogClass* klass)
59{
60    GObjectClass* object_class = G_OBJECT_CLASS(klass);
61
62    /**
63     * WebKitSoupAuthDialog::current-toplevel:
64     * @authDialog: the object on which the signal is emitted
65     * @message: the #SoupMessage being used in the authentication process
66     *
67     * This signal is emitted by the @authDialog when it needs to know
68     * the current toplevel widget in order to correctly set the
69     * transiency for the authentication dialog.
70     *
71     * Return value: (transfer none): the current toplevel #GtkWidget or %NULL if there's none
72     *
73     * Since: 1.1.1
74     */
75    signals[CURRENT_TOPLEVEL] =
76      g_signal_new("current-toplevel",
77                   G_OBJECT_CLASS_TYPE(object_class),
78                   G_SIGNAL_RUN_LAST,
79                   G_STRUCT_OFFSET(WebKitSoupAuthDialogClass, current_toplevel),
80                   NULL, NULL,
81                   webkit_marshal_OBJECT__OBJECT,
82                   GTK_TYPE_WIDGET, 1,
83                   SOUP_TYPE_MESSAGE);
84}
85
86static void webkit_soup_auth_dialog_init(WebKitSoupAuthDialog* instance)
87{
88}
89
90static void webkit_soup_auth_dialog_session_feature_init(SoupSessionFeatureInterface *feature_interface,
91                                                         gpointer interface_data)
92{
93    feature_interface->attach = attach;
94    feature_interface->detach = detach;
95}
96
97typedef struct _WebKitAuthData {
98    SoupMessage* msg;
99    SoupAuth* auth;
100    SoupSession* session;
101    SoupSessionFeature* manager;
102    GtkWidget* loginEntry;
103    GtkWidget* passwordEntry;
104    GtkWidget* checkButton;
105    char *username;
106    char *password;
107} WebKitAuthData;
108
109static void free_authData(WebKitAuthData* authData)
110{
111    g_object_unref(authData->msg);
112    g_free(authData->username);
113    g_free(authData->password);
114    g_slice_free(WebKitAuthData, authData);
115}
116
117#ifdef SOUP_TYPE_PASSWORD_MANAGER
118static void save_password_callback(SoupMessage* msg, WebKitAuthData* authData)
119{
120    /* Anything but 401 and 5xx means the password was accepted */
121    if (msg->status_code != 401 && msg->status_code < 500)
122        soup_auth_save_password(authData->auth, authData->username, authData->password);
123
124    /* Disconnect the callback. If the authentication succeeded we are
125     * done, and if it failed we'll create a new authData and we'll
126     * connect to 'got-headers' again in response_callback */
127    g_signal_handlers_disconnect_by_func(msg, save_password_callback, authData);
128
129    free_authData(authData);
130}
131#endif
132
133static void response_callback(GtkDialog* dialog, gint response_id, WebKitAuthData* authData)
134{
135    gboolean freeAuthData = TRUE;
136
137    if (response_id == GTK_RESPONSE_OK) {
138        authData->username = g_strdup(gtk_entry_get_text(GTK_ENTRY(authData->loginEntry)));
139        authData->password = g_strdup(gtk_entry_get_text(GTK_ENTRY(authData->passwordEntry)));
140
141        soup_auth_authenticate(authData->auth, authData->username, authData->password);
142
143#ifdef SOUP_TYPE_PASSWORD_MANAGER
144        if (authData->checkButton &&
145            gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(authData->checkButton))) {
146            g_signal_connect(authData->msg, "got-headers", G_CALLBACK(save_password_callback), authData);
147            freeAuthData = FALSE;
148        }
149#endif
150    }
151
152    soup_session_unpause_message(authData->session, authData->msg);
153    if (freeAuthData)
154        free_authData(authData);
155    gtk_widget_destroy(GTK_WIDGET(dialog));
156}
157
158static GtkWidget *
159table_add_entry(GtkWidget*  table,
160                int         row,
161                const char* label_text,
162                const char* value,
163                gpointer    user_data)
164{
165    GtkWidget* entry;
166    GtkWidget* label;
167
168    label = gtk_label_new(label_text);
169    gtk_misc_set_alignment(GTK_MISC(label), 0.0, 0.5);
170
171    entry = gtk_entry_new();
172    gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE);
173
174    if (value)
175        gtk_entry_set_text(GTK_ENTRY(entry), value);
176
177    gtk_table_attach(GTK_TABLE(table), label,
178                     0, 1, row, row + 1,
179                     GTK_FILL, GTK_EXPAND | GTK_FILL, 0, 0);
180    gtk_table_attach_defaults(GTK_TABLE(table), entry,
181                              1, 2, row, row + 1);
182
183    return entry;
184}
185
186static gboolean session_can_save_passwords(SoupSession* session)
187{
188#ifdef SOUP_TYPE_PASSWORD_MANAGER
189    return soup_session_get_feature(session, SOUP_TYPE_PASSWORD_MANAGER) != NULL;
190#else
191    return FALSE;
192#endif
193}
194
195static void show_auth_dialog(WebKitAuthData* authData, const char* login, const char* password)
196{
197    GtkWidget* toplevel;
198    GtkWidget* widget;
199    GtkDialog* dialog;
200    GtkWindow* window;
201    GtkWidget* entryContainer;
202    GtkWidget* hbox;
203    GtkWidget* mainVBox;
204    GtkWidget* vbox;
205    GtkWidget* icon;
206    GtkWidget* table;
207    GtkWidget* serverMessageDescriptionLabel;
208    GtkWidget* serverMessageLabel;
209    GtkWidget* descriptionLabel;
210    char* description;
211    const char* realm;
212    gboolean hasRealm;
213    SoupURI* uri;
214    GtkWidget* rememberBox;
215    GtkWidget* checkButton;
216
217    /* From GTK+ gtkmountoperation.c, modified and simplified. LGPL 2 license */
218
219    widget = gtk_dialog_new();
220    window = GTK_WINDOW(widget);
221    dialog = GTK_DIALOG(widget);
222
223    gtk_dialog_add_buttons(dialog,
224                           GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
225                           GTK_STOCK_OK, GTK_RESPONSE_OK,
226                           NULL);
227
228    /* Set the dialog up with HIG properties */
229    gtk_container_set_border_width(GTK_CONTAINER(dialog), 5);
230    gtk_box_set_spacing(GTK_BOX(gtk_dialog_get_content_area(dialog)), 2); /* 2 * 5 + 2 = 12 */
231    gtk_container_set_border_width(GTK_CONTAINER(gtk_dialog_get_action_area(dialog)), 5);
232    gtk_box_set_spacing(GTK_BOX(gtk_dialog_get_action_area(dialog)), 6);
233
234    gtk_window_set_resizable(window, FALSE);
235    gtk_window_set_title(window, "");
236    gtk_window_set_icon_name(window, GTK_STOCK_DIALOG_AUTHENTICATION);
237
238    gtk_dialog_set_default_response(dialog, GTK_RESPONSE_OK);
239
240    /* Get the current toplevel */
241    g_signal_emit(authData->manager, signals[CURRENT_TOPLEVEL], 0, authData->msg, &toplevel);
242
243    if (toplevel)
244        gtk_window_set_transient_for(window, GTK_WINDOW(toplevel));
245
246    /* Build contents */
247    hbox = gtk_hbox_new(FALSE, 12);
248    gtk_container_set_border_width(GTK_CONTAINER(hbox), 5);
249    gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(dialog)), hbox, TRUE, TRUE, 0);
250
251    icon = gtk_image_new_from_stock(GTK_STOCK_DIALOG_AUTHENTICATION,
252                                    GTK_ICON_SIZE_DIALOG);
253
254    gtk_misc_set_alignment(GTK_MISC(icon), 0.5, 0.0);
255    gtk_box_pack_start(GTK_BOX(hbox), icon, FALSE, FALSE, 0);
256
257    mainVBox = gtk_vbox_new(FALSE, 18);
258    gtk_box_pack_start(GTK_BOX(hbox), mainVBox, TRUE, TRUE, 0);
259
260    uri = soup_message_get_uri(authData->msg);
261    description = g_strdup_printf(_("A username and password are being requested by the site %s"), uri->host);
262    descriptionLabel = gtk_label_new(description);
263    g_free(description);
264    gtk_misc_set_alignment(GTK_MISC(descriptionLabel), 0.0, 0.5);
265    gtk_label_set_line_wrap(GTK_LABEL(descriptionLabel), TRUE);
266    gtk_box_pack_start(GTK_BOX(mainVBox), GTK_WIDGET(descriptionLabel),
267                       FALSE, FALSE, 0);
268
269    vbox = gtk_vbox_new(FALSE, 6);
270    gtk_box_pack_start(GTK_BOX(mainVBox), vbox, FALSE, FALSE, 0);
271
272    /* The table that holds the entries */
273    entryContainer = gtk_alignment_new(0.0, 0.0, 1.0, 1.0);
274
275    gtk_alignment_set_padding(GTK_ALIGNMENT(entryContainer),
276                              0, 0, 0, 0);
277
278    gtk_box_pack_start(GTK_BOX(vbox), entryContainer,
279                       FALSE, FALSE, 0);
280
281    realm = soup_auth_get_realm(authData->auth);
282    // Checking that realm is not an empty string
283    hasRealm = (realm && (strlen(realm) > 0));
284
285    table = gtk_table_new(hasRealm ? 3 : 2, 2, FALSE);
286    gtk_table_set_col_spacings(GTK_TABLE(table), 12);
287    gtk_table_set_row_spacings(GTK_TABLE(table), 6);
288    gtk_container_add(GTK_CONTAINER(entryContainer), table);
289
290    if (hasRealm) {
291        serverMessageDescriptionLabel = gtk_label_new(_("Server message:"));
292        serverMessageLabel = gtk_label_new(realm);
293        gtk_misc_set_alignment(GTK_MISC(serverMessageDescriptionLabel), 0.0, 0.5);
294        gtk_label_set_line_wrap(GTK_LABEL(serverMessageDescriptionLabel), TRUE);
295        gtk_misc_set_alignment(GTK_MISC(serverMessageLabel), 0.0, 0.5);
296        gtk_label_set_line_wrap(GTK_LABEL(serverMessageLabel), TRUE);
297
298        gtk_table_attach_defaults(GTK_TABLE(table), serverMessageDescriptionLabel,
299                                  0, 1, 0, 1);
300        gtk_table_attach_defaults(GTK_TABLE(table), serverMessageLabel,
301                                  1, 2, 0, 1);
302    }
303
304    authData->loginEntry = table_add_entry(table, hasRealm ? 1 : 0, _("Username:"),
305                                           login, NULL);
306    authData->passwordEntry = table_add_entry(table, hasRealm ? 2 : 1, _("Password:"),
307                                              password, NULL);
308
309    gtk_entry_set_visibility(GTK_ENTRY(authData->passwordEntry), FALSE);
310
311    if (session_can_save_passwords(authData->session)) {
312        rememberBox = gtk_vbox_new(FALSE, 6);
313        gtk_box_pack_start(GTK_BOX(vbox), rememberBox,
314                           FALSE, FALSE, 0);
315        checkButton = gtk_check_button_new_with_mnemonic(_("_Remember password"));
316        if (login && password)
317            gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(checkButton), TRUE);
318        gtk_label_set_line_wrap(GTK_LABEL(gtk_bin_get_child(GTK_BIN(checkButton))), TRUE);
319        gtk_box_pack_start(GTK_BOX(rememberBox), checkButton, FALSE, FALSE, 0);
320        authData->checkButton = checkButton;
321    }
322
323    g_signal_connect(dialog, "response", G_CALLBACK(response_callback), authData);
324    gtk_widget_show_all(widget);
325}
326
327static void session_authenticate(SoupSession* session, SoupMessage* msg, SoupAuth* auth, gboolean retrying, gpointer user_data)
328{
329    SoupURI* uri;
330    WebKitAuthData* authData;
331    SoupSessionFeature* manager = (SoupSessionFeature*)user_data;
332#ifdef SOUP_TYPE_PASSWORD_MANAGER
333    GSList* users;
334#endif
335    const char *login, *password;
336
337    soup_session_pause_message(session, msg);
338    /* We need to make sure the message sticks around when pausing it */
339    g_object_ref(msg);
340
341    uri = soup_message_get_uri(msg);
342    authData = g_slice_new0(WebKitAuthData);
343    authData->msg = msg;
344    authData->auth = auth;
345    authData->session = session;
346    authData->manager = manager;
347
348    login = password = NULL;
349
350#ifdef SOUP_TYPE_PASSWORD_MANAGER
351    users = soup_auth_get_saved_users(auth);
352    if (users) {
353        login = users->data;
354        password = soup_auth_get_saved_password(auth, login);
355        g_slist_free(users);
356    }
357#endif
358
359    show_auth_dialog(authData, login, password);
360}
361
362static void attach(SoupSessionFeature* manager, SoupSession* session)
363{
364    g_signal_connect(session, "authenticate", G_CALLBACK(session_authenticate), manager);
365}
366
367static void detach(SoupSessionFeature* manager, SoupSession* session)
368{
369    g_signal_handlers_disconnect_by_func(session, session_authenticate, manager);
370}
371
372
373