/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set sw=2 ts=2 sts=2 et: */ /* * Copyright © 2012 Igalia S.L. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2, or (at your option) * any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "config.h" #include "ephy-web-extension.h" #include "ephy-debug.h" #include "ephy-embed-form-auth.h" #include "ephy-form-auth-data.h" #include "ephy-prefs.h" #include "ephy-settings.h" #include "ephy-web-dom-utils.h" #include "uri-tester.h" #include #include #include /* FIXME: These global variables should be freed somehow. */ static UriTester *uri_tester; static EphyFormAuthDataCache *form_auth_data_cache; static GDBusConnection *dbus_connection; static const char introspection_xml[] = "" " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " ""; static gboolean web_page_send_request (WebKitWebPage *web_page, WebKitURIRequest *request, WebKitURIResponse *redirected_response, gpointer user_data) { const char *request_uri; const char *page_uri; if (g_settings_get_boolean (EPHY_SETTINGS_WEB, EPHY_PREFS_WEB_DO_NOT_TRACK)) { SoupMessageHeaders *headers; headers = webkit_uri_request_get_http_headers (request); if (headers) { /* Do Not Track header. '1' means 'opt-out'. See: * http://tools.ietf.org/id/draft-mayer-do-not-track-00.txt */ soup_message_headers_append (headers, "DNT", "1"); } } if (!g_settings_get_boolean (EPHY_SETTINGS_WEB, EPHY_PREFS_WEB_ENABLE_ADBLOCK)) return FALSE; request_uri = webkit_uri_request_get_uri (request); page_uri = webkit_web_page_get_uri (web_page); /* Always load the main resource. */ if (g_strcmp0 (request_uri, page_uri) == 0) return FALSE; return uri_tester_test_uri (uri_tester, request_uri, page_uri, AD_URI_CHECK_TYPE_OTHER); } static GHashTable * get_form_auth_data_save_requests (void) { static GHashTable *form_auth_data_save_requests = NULL; if (!form_auth_data_save_requests) { form_auth_data_save_requests = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, (GDestroyNotify)g_object_unref); } return form_auth_data_save_requests; } static guint form_auth_data_save_request_new_id (void) { static guint form_auth_data_save_request_id = 0; return ++form_auth_data_save_request_id; } static void store_password (EphyEmbedFormAuth *form_auth) { SoupURI *uri; char *uri_str; char *username_field_name = NULL; char *username_field_value = NULL; char *password_field_name = NULL; char *password_field_value = NULL; g_object_get (ephy_embed_form_auth_get_username_node (form_auth), "name", &username_field_name, "value", &username_field_value, NULL); g_object_get (ephy_embed_form_auth_get_password_node (form_auth), "name", &password_field_name, "value", &password_field_value, NULL); uri = ephy_embed_form_auth_get_uri (form_auth); uri_str = soup_uri_to_string (uri, FALSE); ephy_form_auth_data_store (uri_str, username_field_name, password_field_name, username_field_value, password_field_value, NULL, NULL); g_free (uri_str); /* Update internal caching */ ephy_form_auth_data_cache_add (form_auth_data_cache, uri->host, username_field_name, password_field_name, username_field_value); g_free (username_field_name); g_free (username_field_value); g_free (password_field_name); g_free (password_field_value); } static void request_decision_on_storing (EphyEmbedFormAuth *form_auth) { char *username_field_value = NULL; guint request_id; SoupURI *uri; GError *error = NULL; if (!dbus_connection) { g_object_unref (form_auth); return; } request_id = form_auth_data_save_request_new_id (); uri = ephy_embed_form_auth_get_uri (form_auth); g_object_get (ephy_embed_form_auth_get_username_node (form_auth), "value", &username_field_value, NULL); g_dbus_connection_emit_signal (dbus_connection, NULL, EPHY_WEB_EXTENSION_OBJECT_PATH, EPHY_WEB_EXTENSION_INTERFACE, "FormAuthDataSaveConfirmationRequired", g_variant_new ("(utss)", request_id, ephy_embed_form_auth_get_page_id (form_auth), uri ? uri->host : "", username_field_value ? username_field_value : ""), &error); if (error) { g_warning ("Error emitting signal FormAuthDataSaveConfirmationRequired: %s\n", error->message); g_error_free (error); } else { g_hash_table_insert (get_form_auth_data_save_requests (), GINT_TO_POINTER (request_id), g_object_ref (form_auth)); } g_free (username_field_value); g_object_unref (form_auth); } static void should_store_cb (const char *username, const char *password, gpointer user_data) { EphyEmbedFormAuth *form_auth = EPHY_EMBED_FORM_AUTH (user_data); if (username && password) { char *username_field_value = NULL; char *password_field_value = NULL; g_object_get (ephy_embed_form_auth_get_username_node (form_auth), "value", &username_field_value, NULL); g_object_get (ephy_embed_form_auth_get_password_node (form_auth), "value", &password_field_value, NULL); /* FIXME: We use only the first result, for now; We need to do * something smarter here */ if (g_str_equal (username, username_field_value) && g_str_equal (password, password_field_value)) { LOG ("User/password already stored. Not asking about storing."); } else { LOG ("User/password not yet stored. Asking about storing."); request_decision_on_storing (g_object_ref (form_auth)); } g_free (username_field_value); g_free (password_field_value); } else { LOG ("No result on query; asking whether we should store."); request_decision_on_storing (g_object_ref (form_auth)); } } static gboolean form_submitted_cb (WebKitDOMHTMLFormElement *dom_form, WebKitDOMEvent *dom_event, WebKitWebPage *web_page) { EphyEmbedFormAuth *form_auth; SoupURI *uri; WebKitDOMNode *username_node = NULL; WebKitDOMNode *password_node = NULL; char *username_field_name = NULL; char *password_field_name = NULL; char *uri_str; if (!ephy_web_dom_utils_find_form_auth_elements (dom_form, &username_node, &password_node)) return TRUE; /* EphyEmbedFormAuth takes ownership of the nodes */ form_auth = ephy_embed_form_auth_new (web_page, username_node, password_node); uri = ephy_embed_form_auth_get_uri (form_auth); soup_uri_set_query (uri, NULL); g_object_get (username_node, "name", &username_field_name, NULL); g_object_get (password_node, "name", &password_field_name, NULL); uri_str = soup_uri_to_string (uri, FALSE); ephy_form_auth_data_query (uri_str, username_field_name, password_field_name, should_store_cb, form_auth, (GDestroyNotify)g_object_unref); g_free (username_field_name); g_free (password_field_name); g_free (uri_str); return TRUE; } static void fill_form_cb (const char *username, const char *password, gpointer user_data) { EphyEmbedFormAuth *form_auth = EPHY_EMBED_FORM_AUTH (user_data); if (username == NULL && password == NULL) { LOG ("No result"); return; } LOG ("Found: user %s pass (hidden)", username); g_object_set (ephy_embed_form_auth_get_username_node (form_auth), "value", username, NULL); g_object_set (ephy_embed_form_auth_get_password_node (form_auth), "value", password, NULL); } static gint ephy_form_auth_data_compare (EphyFormAuthData *form_data, EphyEmbedFormAuth *form_auth) { char *username_field_name; char *password_field_name; gboolean retval; g_object_get (ephy_embed_form_auth_get_username_node (form_auth), "name", &username_field_name, NULL); g_object_get (ephy_embed_form_auth_get_password_node (form_auth), "name", &password_field_name, NULL); retval = g_strcmp0 (username_field_name, form_data->form_username) == 0 && g_strcmp0 (password_field_name, form_data->form_password) == 0; g_free (username_field_name); g_free (password_field_name); return retval ? 0 : 1; } static void pre_fill_form (EphyEmbedFormAuth *form_auth) { GSList *form_auth_data_list; GSList *l; EphyFormAuthData *form_data; SoupURI *uri; char *uri_str; uri = ephy_embed_form_auth_get_uri (form_auth); if (!uri) return; form_auth_data_list = ephy_form_auth_data_cache_get_list (form_auth_data_cache, uri->host); l = g_slist_find_custom (form_auth_data_list, form_auth, (GCompareFunc)ephy_form_auth_data_compare); if (!l) return; form_data = (EphyFormAuthData *)l->data; uri_str = soup_uri_to_string (uri, FALSE); ephy_form_auth_data_query (uri_str, form_data->form_username, form_data->form_password, fill_form_cb, g_object_ref (form_auth), (GDestroyNotify)g_object_unref); g_free (uri_str); } static void web_page_document_loaded (WebKitWebPage *web_page, gpointer user_data) { WebKitDOMHTMLCollection *forms = NULL; WebKitDOMDocument *document = NULL; gulong forms_n; int i; if (!form_auth_data_cache || !g_settings_get_boolean (EPHY_SETTINGS_MAIN, EPHY_PREFS_REMEMBER_PASSWORDS)) return; document = webkit_web_page_get_dom_document (web_page); forms = webkit_dom_document_get_forms (document); forms_n = webkit_dom_html_collection_get_length (forms); if (forms_n == 0) { LOG ("No forms found."); g_object_unref(forms); return; } for (i = 0; i < forms_n; i++) { WebKitDOMHTMLFormElement *form; WebKitDOMNode *username_node = NULL; WebKitDOMNode *password_node = NULL; form = WEBKIT_DOM_HTML_FORM_ELEMENT (webkit_dom_html_collection_item (forms, i)); /* We have a field that may be the user, and one for a password. */ if (ephy_web_dom_utils_find_form_auth_elements (form, &username_node, &password_node)) { EphyEmbedFormAuth *form_auth; LOG ("Hooking and pre-filling a form"); /* EphyEmbedFormAuth takes ownership of the nodes */ form_auth = ephy_embed_form_auth_new (web_page, username_node, password_node); webkit_dom_event_target_add_event_listener (WEBKIT_DOM_EVENT_TARGET (form), "submit", G_CALLBACK (form_submitted_cb), FALSE, web_page); pre_fill_form (form_auth); g_object_unref (form_auth); } else LOG ("No pre-fillable/hookable form found"); } g_object_unref(forms); } static void web_page_created_callback (WebKitWebExtension *extension, WebKitWebPage *web_page, gpointer user_data) { g_signal_connect_object (web_page, "send-request", G_CALLBACK (web_page_send_request), NULL, 0); g_signal_connect_object (web_page, "document-loaded", G_CALLBACK (web_page_document_loaded), NULL, 0); } static WebKitWebPage * get_webkit_web_page_or_return_dbus_error (GDBusMethodInvocation *invocation, WebKitWebExtension *web_extension, guint64 page_id) { WebKitWebPage *web_page = webkit_web_extension_get_page (web_extension, page_id); if (!web_page) { g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS, "Invalid page ID: %"G_GUINT64_FORMAT, page_id); } return web_page; } static void handle_method_call (GDBusConnection *connection, const char *sender, const char *object_path, const char *interface_name, const char *method_name, GVariant *parameters, GDBusMethodInvocation *invocation, gpointer user_data) { WebKitWebExtension *web_extension = WEBKIT_WEB_EXTENSION (user_data); if (g_strcmp0 (interface_name, EPHY_WEB_EXTENSION_INTERFACE) != 0) return; if (g_strcmp0 (method_name, "HasModifiedForms") == 0) { WebKitWebPage *web_page; WebKitDOMDocument *document; guint64 page_id; gboolean has_modifed_forms; g_variant_get (parameters, "(t)", &page_id); web_page = get_webkit_web_page_or_return_dbus_error (invocation, web_extension, page_id); if (!web_page) return; document = webkit_web_page_get_dom_document (web_page); has_modifed_forms = ephy_web_dom_utils_has_modified_forms (document); g_dbus_method_invocation_return_value (invocation, g_variant_new ("(b)", has_modifed_forms)); } else if (g_strcmp0 (method_name, "GetWebAppTitle") == 0) { WebKitWebPage *web_page; WebKitDOMDocument *document; char *title = NULL; guint64 page_id; g_variant_get (parameters, "(t)", &page_id); web_page = get_webkit_web_page_or_return_dbus_error (invocation, web_extension, page_id); if (!web_page) return; document = webkit_web_page_get_dom_document (web_page); title = ephy_web_dom_utils_get_application_title (document); g_dbus_method_invocation_return_value (invocation, g_variant_new ("(s)", title ? title : "")); } else if (g_strcmp0 (method_name, "GetBestWebAppIcon") == 0) { WebKitWebPage *web_page; WebKitDOMDocument *document; char *base_uri = NULL; char *uri = NULL; char *color = NULL; guint64 page_id; gboolean result; g_variant_get (parameters, "(ts)", &page_id, &base_uri); web_page = get_webkit_web_page_or_return_dbus_error (invocation, web_extension, page_id); if (!web_page) return; if (base_uri == NULL || base_uri == '\0') { g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS, "Base URI cannot be NULL or empty"); return; } document= webkit_web_page_get_dom_document (web_page); result = ephy_web_dom_utils_get_best_icon (document, base_uri, &uri, &color); g_dbus_method_invocation_return_value (invocation, g_variant_new ("(bss)", result, uri ? uri : "", color ? color : "")); } else if (g_strcmp0 (method_name, "FormAuthDataSaveConfirmationResponse") == 0) { EphyEmbedFormAuth *form_auth; guint request_id; gboolean should_store; GHashTable *requests = get_form_auth_data_save_requests (); g_variant_get (parameters, "(ub)", &request_id, &should_store); form_auth = g_hash_table_lookup (requests, GINT_TO_POINTER (request_id)); if (!form_auth) return; if (should_store) store_password (form_auth); g_hash_table_remove (requests, GINT_TO_POINTER (request_id)); } } static const GDBusInterfaceVTable interface_vtable = { handle_method_call, NULL, NULL }; static void bus_acquired_cb (GDBusConnection *connection, const char *name, gpointer user_data) { guint registration_id; GError *error = NULL; static GDBusNodeInfo *introspection_data = NULL; if (!introspection_data) introspection_data = g_dbus_node_info_new_for_xml (introspection_xml, NULL); registration_id = g_dbus_connection_register_object (connection, EPHY_WEB_EXTENSION_OBJECT_PATH, introspection_data->interfaces[0], &interface_vtable, g_object_ref (user_data), (GDestroyNotify)g_object_unref, &error); if (!registration_id) { g_warning ("Failed to register object: %s\n", error->message); g_error_free (error); } else { dbus_connection = connection; g_object_add_weak_pointer (G_OBJECT (connection), (gpointer *)&dbus_connection); } } G_MODULE_EXPORT void webkit_web_extension_initialize (WebKitWebExtension *extension) { char *service_name; ephy_debug_init (); uri_tester = uri_tester_new (g_getenv ("EPHY_DOT_DIR")); if (!g_getenv ("EPHY_PRIVATE_PROFILE")) form_auth_data_cache = ephy_form_auth_data_cache_new (); g_signal_connect (extension, "page-created", G_CALLBACK (web_page_created_callback), NULL); service_name = g_strdup_printf ("%s-%s", EPHY_WEB_EXTENSION_SERVICE_NAME, g_getenv ("EPHY_WEB_EXTENSION_ID")); g_bus_own_name (G_BUS_TYPE_SESSION, service_name, G_BUS_NAME_OWNER_FLAGS_NONE, bus_acquired_cb, NULL, NULL, g_object_ref (extension), (GDestroyNotify)g_object_unref); g_free (service_name); }