/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* * Copyright © 2014 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, see . */ #include "config.h" #include "ephy-web-extension.h" #include "ephy-dbus-util.h" #include "ephy-debug.h" #include "ephy-embed-form-auth.h" #include "ephy-file-helpers.h" #include "ephy-form-auth-data.h" #include "ephy-prefs.h" #include "ephy-settings.h" #include "ephy-web-dom-utils.h" #include "ephy-uri-helpers.h" #include "ephy-uri-tester.h" #include "ephy-web-overview.h" #include "ephy-web-extension-names.h" #include #include #include #include #include #define WEBKIT_DOM_USE_UNSTABLE_API #include #include struct _EphyWebExtension { GObject parent_instance; WebKitWebExtension *extension; gboolean initialized; GDBusConnection *dbus_connection; GCancellable *cancellable; GArray *page_created_signals_pending; EphyUriTester *uri_tester; EphyFormAuthDataCache *form_auth_data_cache; GHashTable *form_auth_data_save_requests; EphyWebOverviewModel *overview_model; }; static const char introspection_xml[] = "" " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " ""; G_DEFINE_TYPE (EphyWebExtension, ephy_web_extension, G_TYPE_OBJECT) static gboolean web_page_send_request (WebKitWebPage *web_page, WebKitURIRequest *request, WebKitURIResponse *redirected_response, EphyWebExtension *extension) { const char *request_uri; const char *page_uri; gboolean ret; request_uri = webkit_uri_request_get_uri (request); if (g_settings_get_boolean (EPHY_SETTINGS_WEB, EPHY_PREFS_WEB_DO_NOT_TRACK)) { SoupMessageHeaders *headers; char *new_uri; 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"); } /* Remove analytics from URL before loading */ new_uri = ephy_remove_tracking_from_uri (request_uri); if (new_uri) { webkit_uri_request_set_uri (request, new_uri); request_uri = webkit_uri_request_get_uri (request); } g_free (new_uri); } if (!g_settings_get_boolean (EPHY_SETTINGS_WEB, EPHY_PREFS_WEB_ENABLE_ADBLOCK)) return FALSE; page_uri = webkit_web_page_get_uri (web_page); /* Always load the main resource. */ if (g_strcmp0 (request_uri, page_uri) == 0) return FALSE; /* Always load data requests, as uri_tester won't do any good here. */ if (g_str_has_prefix (request_uri, SOUP_URI_SCHEME_DATA)) return FALSE; ret = ephy_uri_tester_test_uri (extension->uri_tester, request_uri, page_uri); if (ret) g_debug ("Request '%s' blocked (page: '%s')", request_uri, page_uri); return ret; } static GHashTable * ephy_web_extension_get_form_auth_data_save_requests (EphyWebExtension *extension) { if (!extension->form_auth_data_save_requests) { extension->form_auth_data_save_requests = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, (GDestroyNotify)g_object_unref); } return extension->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; WebKitDOMNode *username_node; EphyWebExtension *extension = ephy_web_extension_get (); username_node = ephy_embed_form_auth_get_username_node (form_auth); if (username_node) g_object_get (username_node, "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 (extension->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; WebKitDOMNode *username_node; WebKitDOMDOMWindow *dom_window; GVariant *variant; gchar *message; dom_window = webkit_dom_document_get_default_view (ephy_embed_form_auth_get_owner_document (form_auth)); if (!dom_window) { g_object_unref (form_auth); return; } request_id = form_auth_data_save_request_new_id (); uri = ephy_embed_form_auth_get_uri (form_auth); username_node = ephy_embed_form_auth_get_username_node (form_auth); if (username_node) g_object_get (username_node, "value", &username_field_value, NULL); variant = g_variant_new ("(utss)", request_id, ephy_embed_form_auth_get_page_id (form_auth), uri ? uri->host : "", username_field_value ? username_field_value : ""); g_free (username_field_value); message = g_variant_print (variant, FALSE); g_variant_unref (variant); if (webkit_dom_dom_window_webkit_message_handlers_post_message (dom_window, "formAuthData", message)) { g_hash_table_insert (ephy_web_extension_get_form_auth_data_save_requests (ephy_web_extension_get ()), GINT_TO_POINTER (request_id), g_object_ref (form_auth)); } else { g_warning ("Error sending formAuthData message"); } g_object_unref (dom_window); g_free (message); 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 (password) { WebKitDOMNode *username_node; char *username_field_value = NULL; char *password_field_value = NULL; username_node = ephy_embed_form_auth_get_username_node (form_auth); if (username_node) g_object_get (username_node, "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_strcmp0 (username, username_field_value) == 0 && 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 *username_field_value = 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; if (username_node) { g_object_get (username_node, "value", &username_field_value, NULL); } /* EphyEmbedFormAuth takes ownership of the nodes */ form_auth = ephy_embed_form_auth_new (web_page, username_node, password_node, username_field_value); uri = ephy_embed_form_auth_get_uri (form_auth); soup_uri_set_query (uri, NULL); if (username_node) 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, username_field_value, should_store_cb, form_auth, (GDestroyNotify)g_object_unref); g_free (username_field_name); g_free (username_field_value); 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); WebKitDOMNode *username_node; if (username == NULL && password == NULL) { LOG ("No result"); return; } username_node = ephy_embed_form_auth_get_username_node (form_auth); LOG ("Found: user %s pass (hidden)", username_node ? username : "(none)"); if (username_node) g_object_set (username_node, "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) { WebKitDOMNode *username_node; char *username_field_name = NULL; char *password_field_name; gboolean retval; username_node = ephy_embed_form_auth_get_username_node (form_auth); if (username_node) g_object_get (username_node, "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; char *username; WebKitDOMNode *username_node; EphyWebExtension *extension; uri = ephy_embed_form_auth_get_uri (form_auth); if (!uri) return; extension = ephy_web_extension_get (); form_auth_data_list = ephy_form_auth_data_cache_get_list (extension->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); username_node = ephy_embed_form_auth_get_username_node (form_auth); if (username_node) g_object_get (username_node, "value", &username, NULL); else username = NULL; /* The username node is empty, so pre-fill with the default. */ if (username != NULL && g_str_equal (username, "")) g_clear_pointer (&username, g_free); ephy_form_auth_data_query (uri_str, form_data->form_username, form_data->form_password, username, fill_form_cb, g_object_ref (form_auth), (GDestroyNotify)g_object_unref); g_free (username); g_free (uri_str); } static void remove_user_choices (WebKitDOMDocument *document) { WebKitDOMHTMLElement *body; WebKitDOMElement *user_choices; body = webkit_dom_document_get_body (document); user_choices = webkit_dom_document_get_element_by_id (document, "ephy-user-choices-container"); if (user_choices) { webkit_dom_node_remove_child (WEBKIT_DOM_NODE (body), WEBKIT_DOM_NODE (user_choices), NULL); } } static gboolean username_changed_cb (WebKitDOMNode *username_node, WebKitDOMEvent *dom_event, EphyEmbedFormAuth *form_auth) { pre_fill_form (form_auth); return TRUE; } static gboolean user_chosen_cb (WebKitDOMNode *li, WebKitDOMEvent *dom_event, WebKitDOMNode *username_node) { WebKitDOMElement *anchor; const char *username; anchor = webkit_dom_element_get_first_element_child (WEBKIT_DOM_ELEMENT (li)); username = webkit_dom_node_get_text_content (WEBKIT_DOM_NODE (anchor)); webkit_dom_html_input_element_set_value (WEBKIT_DOM_HTML_INPUT_ELEMENT (username_node), username); remove_user_choices (webkit_dom_node_get_owner_document (li)); return TRUE; } GtkStyleContext *global_entry_context = NULL; static GtkStyleContext * get_entry_style_context (void) { GtkWidgetPath *path; if (global_entry_context) return global_entry_context; path = gtk_widget_path_new (); gtk_widget_path_append_type (path, GTK_TYPE_ENTRY); gtk_widget_path_iter_set_object_name (path, 0, "entry"); global_entry_context = gtk_style_context_new (); gtk_style_context_set_path (global_entry_context, path); gtk_widget_path_free (path); return global_entry_context; } static char * get_selected_bgcolor (void) { GdkRGBA color; gtk_style_context_set_state (get_entry_style_context (), GTK_STATE_FLAG_SELECTED); gtk_style_context_get_background_color (get_entry_style_context (), GTK_STATE_FLAG_SELECTED, &color); return gdk_rgba_to_string (&color); } static char * get_selected_fgcolor (void) { GdkRGBA color; gtk_style_context_set_state (get_entry_style_context (), GTK_STATE_FLAG_SELECTED); gtk_style_context_get_color (get_entry_style_context (), GTK_STATE_FLAG_SELECTED, &color); return gdk_rgba_to_string (&color); } static char * get_bgcolor (void) { GdkRGBA color; gtk_style_context_set_state (get_entry_style_context (), GTK_STATE_FLAG_NORMAL); gtk_style_context_get_background_color (get_entry_style_context (), GTK_STATE_FLAG_NORMAL, &color); return gdk_rgba_to_string (&color); } static char * get_fgcolor (void) { GdkRGBA color; gtk_style_context_set_state (get_entry_style_context (), GTK_STATE_FLAG_NORMAL); gtk_style_context_get_color (get_entry_style_context (), GTK_STATE_FLAG_NORMAL, &color); return gdk_rgba_to_string (&color); } static char * get_user_choice_style (gboolean selected) { char *style_attribute; char *color; color = selected ? get_selected_bgcolor () : get_bgcolor (); style_attribute = g_strdup_printf ("list-style-type: none ! important;" "background-image: none ! important;" "padding: 3px 6px ! important;" "margin: 0px;" "background-color: %s;", color); g_free (color); return style_attribute; } static char * get_user_choice_anchor_style (gboolean selected) { char *style_attribute; char *color; color = selected ? get_selected_fgcolor () : get_fgcolor (); style_attribute = g_strdup_printf ("font-weight: normal ! important;" "font-family: sans ! important;" "text-decoration: none ! important;" "-webkit-user-modify: read-only ! important;" "color: %s;", color); g_free (color); return style_attribute; } static void show_user_choices (WebKitDOMDocument *document, WebKitDOMNode *username_node) { WebKitDOMNode *body; WebKitDOMElement *main_div; WebKitDOMElement *ul; GSList *iter; GSList *auth_data_list; gboolean username_node_ever_edited; double x, y; double input_width; char *style_attribute; char *username; g_object_get (username_node, "value", &username, NULL); input_width = webkit_dom_element_get_offset_width (WEBKIT_DOM_ELEMENT (username_node)); main_div = webkit_dom_document_create_element (document, "div", NULL); webkit_dom_element_set_attribute (main_div, "id", "ephy-user-choices-container", NULL); ephy_web_dom_utils_get_absolute_bottom_for_element (WEBKIT_DOM_ELEMENT (username_node), &x, &y); /* 2147483647 is the maximum value browsers will take for z-index. * See http://stackoverflow.com/questions/8565821/css-max-z-index-value */ style_attribute = g_strdup_printf ("position: absolute; z-index: 2147483647;" "cursor: default;" "width: %lfpx;" "background-color: white;" "box-shadow: 5px 5px 5px black;" "border-top: 0;" "border-radius: 8px;" "-webkit-user-modify: read-only ! important;" "left: %lfpx; top: %lfpx;", input_width, x, y); webkit_dom_element_set_attribute (main_div, "style", style_attribute, NULL); g_free (style_attribute); ul = webkit_dom_document_create_element (document, "ul", NULL); webkit_dom_element_set_attribute (ul, "tabindex", "-1", NULL); webkit_dom_node_append_child (WEBKIT_DOM_NODE (main_div), WEBKIT_DOM_NODE (ul), NULL); webkit_dom_element_set_attribute (ul, "style", "margin: 0;" "padding: 0;", NULL); auth_data_list = (GSList *)g_object_get_data (G_OBJECT (username_node), "ephy-auth-data-list"); username_node_ever_edited = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (username_node), "ephy-user-ever-edited")); for (iter = auth_data_list; iter; iter = iter->next) { EphyFormAuthData *data; WebKitDOMElement *li; WebKitDOMElement *anchor; char *child_style; gboolean is_selected; data = (EphyFormAuthData *)iter->data; /* Filter out the available names that do not match, but show all options in * case we have been triggered by something other than the user editing the * input. */ if (username_node_ever_edited && !g_str_has_prefix (data->username, username)) continue; is_selected = !g_strcmp0 (username, data->username); li = webkit_dom_document_create_element (document, "li", NULL); webkit_dom_element_set_attribute (li, "tabindex", "-1", NULL); webkit_dom_node_append_child (WEBKIT_DOM_NODE (ul), WEBKIT_DOM_NODE (li), NULL); child_style = get_user_choice_style (is_selected); webkit_dom_element_set_attribute (li, "style", child_style, NULL); g_free (child_style); /* Store the selected node, if any for ease of querying which user * is currently selected. */ if (is_selected) g_object_set_data (G_OBJECT (main_div), "ephy-user-selected", li); anchor = webkit_dom_document_create_element (document, "a", NULL); webkit_dom_node_append_child (WEBKIT_DOM_NODE (li), WEBKIT_DOM_NODE (anchor), NULL); child_style = get_user_choice_anchor_style (is_selected); webkit_dom_element_set_attribute (anchor, "style", child_style, NULL); g_free (child_style); webkit_dom_event_target_add_event_listener (WEBKIT_DOM_EVENT_TARGET (li), "mousedown", G_CALLBACK (user_chosen_cb), TRUE, username_node); webkit_dom_node_set_text_content (WEBKIT_DOM_NODE (anchor), data->username, NULL); } g_free (username); body = WEBKIT_DOM_NODE (webkit_dom_document_get_body (document)); webkit_dom_node_append_child (WEBKIT_DOM_NODE (body), WEBKIT_DOM_NODE (main_div), NULL); } static gboolean username_node_changed_cb (WebKitDOMNode *username_node, WebKitDOMEvent *dom_event, WebKitWebPage *web_page) { WebKitDOMDocument *document; document = webkit_web_page_get_dom_document (web_page); remove_user_choices (document); return TRUE; } static gboolean username_node_clicked_cb (WebKitDOMNode *username_node, WebKitDOMEvent *dom_event, WebKitWebPage *web_page) { WebKitDOMDocument *document; document = webkit_web_page_get_dom_document (web_page); if (webkit_dom_document_get_element_by_id (document, "ephy-user-choices-container")) return TRUE; show_user_choices (document, username_node); return TRUE; } static void clear_password_field (WebKitDOMNode *username_node) { EphyEmbedFormAuth *form_auth; WebKitDOMNode *password_node; form_auth = (EphyEmbedFormAuth *)g_object_get_data (G_OBJECT (username_node), "ephy-form-auth"); password_node = ephy_embed_form_auth_get_password_node (form_auth); webkit_dom_html_input_element_set_value (WEBKIT_DOM_HTML_INPUT_ELEMENT (password_node), ""); } static void pre_fill_password (WebKitDOMNode *username_node) { EphyEmbedFormAuth *form_auth; form_auth = (EphyEmbedFormAuth *)g_object_get_data (G_OBJECT (username_node), "ephy-form-auth"); pre_fill_form (form_auth); } static gboolean username_node_keydown_cb (WebKitDOMNode *username_node, WebKitDOMEvent *dom_event, WebKitWebPage *web_page) { WebKitDOMDocument *document; WebKitDOMElement *main_div; WebKitDOMElement *container; WebKitDOMElement *selected = NULL; WebKitDOMElement *to_select = NULL; WebKitDOMElement *anchor; WebKitDOMKeyboardEvent *keyboard_event; guint keyval = GDK_KEY_VoidSymbol; char *li_style_attribute; char *anchor_style_attribute; const char *username; keyboard_event = WEBKIT_DOM_KEYBOARD_EVENT (dom_event); document = webkit_web_page_get_dom_document (web_page); /* U+001B means the Esc key here; we should find a better way of testing which * key has been pressed. */ if (!g_strcmp0 (webkit_dom_keyboard_event_get_key_identifier (keyboard_event), "Up")) keyval = GDK_KEY_Up; else if (!g_strcmp0 (webkit_dom_keyboard_event_get_key_identifier (keyboard_event), "Down")) keyval = GDK_KEY_Down; else if (!g_strcmp0 (webkit_dom_keyboard_event_get_key_identifier (keyboard_event), "U+001B")) { remove_user_choices (document); return TRUE; } else return TRUE; main_div = webkit_dom_document_get_element_by_id (document, "ephy-user-choices-container"); if (!main_div) { show_user_choices (document, username_node); return TRUE; } /* Grab the selected node. */ selected = WEBKIT_DOM_ELEMENT (g_object_get_data (G_OBJECT (main_div), "ephy-user-selected")); /* Fetch the ul. */ container = webkit_dom_element_get_first_element_child (main_div); /* We have a previous selection already, so perform any selection relative to * it. */ if (selected) { if (keyval == GDK_KEY_Up) to_select = webkit_dom_element_get_previous_element_sibling (selected); else if (keyval == GDK_KEY_Down) to_select = webkit_dom_element_get_next_element_sibling (selected); } if (!to_select) { if (keyval == GDK_KEY_Up) to_select = webkit_dom_element_get_last_element_child (container); else if (keyval == GDK_KEY_Down) to_select = webkit_dom_element_get_first_element_child (container); } /* Unselect the selected node. */ if (selected) { li_style_attribute = get_user_choice_style (FALSE); webkit_dom_element_set_attribute (selected, "style", li_style_attribute, NULL); g_free (li_style_attribute); anchor = webkit_dom_element_get_first_element_child (selected); anchor_style_attribute = get_user_choice_anchor_style (FALSE); webkit_dom_element_set_attribute (anchor, "style", anchor_style_attribute, NULL); g_free (anchor_style_attribute); } /* Selected the new node. */ if (to_select) { g_object_set_data (G_OBJECT (main_div), "ephy-user-selected", to_select); li_style_attribute = get_user_choice_style (TRUE); webkit_dom_element_set_attribute (to_select, "style", li_style_attribute, NULL); g_free (li_style_attribute); anchor = webkit_dom_element_get_first_element_child (to_select); anchor_style_attribute = get_user_choice_anchor_style (TRUE); webkit_dom_element_set_attribute (anchor, "style", anchor_style_attribute, NULL); g_free (anchor_style_attribute); username = webkit_dom_node_get_text_content (WEBKIT_DOM_NODE (anchor)); webkit_dom_html_input_element_set_value (WEBKIT_DOM_HTML_INPUT_ELEMENT (username_node), username); pre_fill_password (username_node); } else clear_password_field (username_node); webkit_dom_event_prevent_default (dom_event); return TRUE; } static gboolean username_node_input_cb (WebKitDOMNode *username_node, WebKitDOMEvent *dom_event, WebKitWebPage *web_page) { WebKitDOMDocument *document; WebKitDOMElement *main_div; g_object_set_data (G_OBJECT (username_node), "ephy-user-ever-edited", GINT_TO_POINTER (TRUE)); document = webkit_web_page_get_dom_document (web_page); remove_user_choices (document); show_user_choices (document, username_node); /* Check if a username has been selected, otherwise clear password field. */ main_div = webkit_dom_document_get_element_by_id (document, "ephy-user-choices-container"); if (g_object_get_data (G_OBJECT (main_div), "ephy-user-selected")) pre_fill_password (username_node); else clear_password_field (username_node); return TRUE; } static void form_destroyed_cb (gpointer form_auth, GObject *form) { g_object_unref (form_auth); } static void web_page_document_loaded (WebKitWebPage *web_page, EphyWebExtension *extension) { WebKitDOMHTMLCollection *forms = NULL; WebKitDOMDocument *document = NULL; gulong forms_n; guint i; if (!extension->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; GSList *auth_data_list; const char *uri_string; SoupURI *uri; 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, NULL); webkit_dom_event_target_add_event_listener (WEBKIT_DOM_EVENT_TARGET (form), "submit", G_CALLBACK (form_submitted_cb), FALSE, web_page); if (username_node) { webkit_dom_event_target_add_event_listener (WEBKIT_DOM_EVENT_TARGET (username_node), "blur", G_CALLBACK (username_changed_cb), FALSE, form_auth); } /* Plug in the user autocomplete */ uri_string = webkit_web_page_get_uri (web_page); uri = soup_uri_new (uri_string); auth_data_list = ephy_form_auth_data_cache_get_list (extension->form_auth_data_cache, uri->host); soup_uri_free (uri); if (auth_data_list && auth_data_list->next && username_node) { LOG ("More than 1 password saved, hooking menu for choosing which on focus"); g_object_set_data (G_OBJECT (username_node), "ephy-auth-data-list", auth_data_list); g_object_set_data (G_OBJECT (username_node), "ephy-form-auth", form_auth); g_object_set_data (G_OBJECT (username_node), "ephy-document", document); webkit_dom_event_target_add_event_listener (WEBKIT_DOM_EVENT_TARGET (username_node), "input", G_CALLBACK (username_node_input_cb), TRUE, web_page); webkit_dom_event_target_add_event_listener (WEBKIT_DOM_EVENT_TARGET (username_node), "keydown", G_CALLBACK (username_node_keydown_cb), FALSE, web_page); webkit_dom_event_target_add_event_listener (WEBKIT_DOM_EVENT_TARGET (username_node), "mouseup", G_CALLBACK (username_node_clicked_cb), FALSE, web_page); webkit_dom_event_target_add_event_listener (WEBKIT_DOM_EVENT_TARGET (username_node), "change", G_CALLBACK (username_node_changed_cb), FALSE, web_page); webkit_dom_event_target_add_event_listener (WEBKIT_DOM_EVENT_TARGET (username_node), "blur", G_CALLBACK (username_node_changed_cb), FALSE, web_page); } else LOG ("No items or a single item in auth_data_list, not hooking menu for choosing."); pre_fill_form (form_auth); g_object_weak_ref (G_OBJECT (form), form_destroyed_cb, form_auth); } else LOG ("No pre-fillable/hookable form found"); } g_object_unref (forms); } static void web_page_uri_changed (WebKitWebPage *web_page, GParamSpec *param_spec, EphyWebExtension *extension) { EphyWebOverview *overview = NULL; if (g_strcmp0 (webkit_web_page_get_uri (web_page), "ephy-about:overview") == 0) overview = ephy_web_overview_new (web_page, extension->overview_model); g_object_set_data_full (G_OBJECT (web_page), "ephy-web-overview", overview, g_object_unref); } static gboolean web_page_context_menu (WebKitWebPage *web_page, WebKitContextMenu *context_menu, WebKitWebHitTestResult *hit_test_result, gpointer user_data) { char *string; GVariantBuilder builder; WebKitDOMDocument *document = webkit_web_page_get_dom_document (web_page); WebKitDOMDOMWindow *window = webkit_dom_document_get_default_view (document); WebKitDOMDOMSelection *selection = webkit_dom_dom_window_get_selection (window); g_object_unref (window); if (!selection) return FALSE; string = ephy_web_dom_utils_get_selection_as_string (selection); g_object_unref (selection); if (!string || *string == '\0') { g_free (string); return FALSE; } g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{sv}")); g_variant_builder_add (&builder, "{sv}", "SelectedText", g_variant_new_string (g_strstrip (string))); webkit_context_menu_set_user_data (context_menu, g_variant_builder_end (&builder)); g_free (string); return TRUE; } static void ephy_web_extension_emit_page_created (EphyWebExtension *extension, guint64 page_id) { GError *error = NULL; g_dbus_connection_emit_signal (extension->dbus_connection, NULL, EPHY_WEB_EXTENSION_OBJECT_PATH, EPHY_WEB_EXTENSION_INTERFACE, "PageCreated", g_variant_new ("(t)", page_id), &error); if (error) { g_warning ("Error emitting signal PageCreated: %s\n", error->message); g_error_free (error); } } static void ephy_web_extension_emit_page_created_signals_pending (EphyWebExtension *extension) { guint i; if (!extension->page_created_signals_pending) return; for (i = 0; i < extension->page_created_signals_pending->len; i++) { guint64 page_id; page_id = g_array_index (extension->page_created_signals_pending, guint64, i); ephy_web_extension_emit_page_created (extension, page_id); } g_array_free (extension->page_created_signals_pending, TRUE); extension->page_created_signals_pending = NULL; } static void ephy_web_extension_queue_page_created_signal_emission (EphyWebExtension *extension, guint64 page_id) { if (!extension->page_created_signals_pending) extension->page_created_signals_pending = g_array_new (FALSE, FALSE, sizeof (guint64)); extension->page_created_signals_pending = g_array_append_val (extension->page_created_signals_pending, page_id); } static void ephy_web_extension_page_created_cb (EphyWebExtension *extension, WebKitWebPage *web_page) { guint64 page_id; page_id = webkit_web_page_get_id (web_page); if (extension->dbus_connection) ephy_web_extension_emit_page_created (extension, page_id); else ephy_web_extension_queue_page_created_signal_emission (extension, page_id); g_signal_connect (web_page, "send-request", G_CALLBACK (web_page_send_request), extension); g_signal_connect (web_page, "document-loaded", G_CALLBACK (web_page_document_loaded), extension); g_signal_connect (web_page, "notify::uri", G_CALLBACK (web_page_uri_changed), extension); g_signal_connect (web_page, "context-menu", G_CALLBACK (web_page_context_menu), extension); } 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) { EphyWebExtension *extension = EPHY_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, extension->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, extension->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; g_variant_get (parameters, "(ts)", &page_id, &base_uri); web_page = get_webkit_web_page_or_return_dbus_error (invocation, extension->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); ephy_web_dom_utils_get_best_icon (document, base_uri, &uri, &color); g_dbus_method_invocation_return_value (invocation, g_variant_new ("(ss)", uri ? uri : "", color ? color : "")); } else if (g_strcmp0 (method_name, "FormAuthDataSaveConfirmationResponse") == 0) { EphyEmbedFormAuth *form_auth; guint request_id; gboolean should_store; GHashTable *requests; requests = ephy_web_extension_get_form_auth_data_save_requests (extension); 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)); } else if (g_strcmp0 (method_name, "HistorySetURLs") == 0) { if (extension->overview_model) { GVariantIter iter; GVariant *array; const char *url; const char *title; GList *items = NULL; g_variant_get (parameters, "(@a(ss))", &array); g_variant_iter_init (&iter, array); while (g_variant_iter_loop (&iter, "(&s&s)", &url, &title)) items = g_list_prepend (items, ephy_web_overview_model_item_new (url, title)); g_variant_unref (array); ephy_web_overview_model_set_urls (extension->overview_model, g_list_reverse (items)); } g_dbus_method_invocation_return_value (invocation, NULL); } else if (g_strcmp0 (method_name, "HistorySetURLThumbnail") == 0) { if (extension->overview_model) { const char *url; const char *path; g_variant_get (parameters, "(&s&s)", &url, &path); ephy_web_overview_model_set_url_thumbnail (extension->overview_model, url, path); } g_dbus_method_invocation_return_value (invocation, NULL); } else if (g_strcmp0 (method_name, "HistorySetURLTitle") == 0) { if (extension->overview_model) { const char *url; const char *title; g_variant_get (parameters, "(&s&s)", &url, &title); ephy_web_overview_model_set_url_title (extension->overview_model, url, title); } g_dbus_method_invocation_return_value (invocation, NULL); } else if (g_strcmp0 (method_name, "HistoryDeleteURL") == 0) { if (extension->overview_model) { const char *url; g_variant_get (parameters, "(&s)", &url); ephy_web_overview_model_delete_url (extension->overview_model, url); } g_dbus_method_invocation_return_value (invocation, NULL); } else if (g_strcmp0 (method_name, "HistoryDeleteHost") == 0) { if (extension->overview_model) { const char *host; g_variant_get (parameters, "(&s)", &host); ephy_web_overview_model_delete_host (extension->overview_model, host); } g_dbus_method_invocation_return_value (invocation, NULL); } else if (g_strcmp0 (method_name, "HistoryClear") == 0) { if (extension->overview_model) ephy_web_overview_model_clear (extension->overview_model); g_dbus_method_invocation_return_value (invocation, NULL); } } static const GDBusInterfaceVTable interface_vtable = { handle_method_call, NULL, NULL }; static void ephy_web_extension_dispose (GObject *object) { EphyWebExtension *extension = EPHY_WEB_EXTENSION (object); g_clear_object (&extension->uri_tester); g_clear_object (&extension->overview_model); g_clear_pointer (&extension->form_auth_data_cache, ephy_form_auth_data_cache_free); if (extension->form_auth_data_save_requests) { g_hash_table_destroy (extension->form_auth_data_save_requests); extension->form_auth_data_save_requests = NULL; } if (extension->page_created_signals_pending) { g_array_free (extension->page_created_signals_pending, TRUE); extension->page_created_signals_pending = NULL; } g_clear_object (&extension->cancellable); g_clear_object (&extension->dbus_connection); g_clear_object (&extension->extension); G_OBJECT_CLASS (ephy_web_extension_parent_class)->dispose (object); } static void ephy_web_extension_class_init (EphyWebExtensionClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->dispose = ephy_web_extension_dispose; } static void ephy_web_extension_init (EphyWebExtension *extension) { extension->overview_model = ephy_web_overview_model_new (); } static gpointer ephy_web_extension_create_instance (gpointer data) { return g_object_new (EPHY_TYPE_WEB_EXTENSION, NULL); } EphyWebExtension * ephy_web_extension_get (void) { static GOnce once_init = G_ONCE_INIT; return EPHY_WEB_EXTENSION (g_once (&once_init, ephy_web_extension_create_instance, NULL)); } static void dbus_connection_created_cb (GObject *source_object, GAsyncResult *result, EphyWebExtension *extension) { static GDBusNodeInfo *introspection_data = NULL; GDBusConnection *connection; guint registration_id; GError *error = NULL; if (!introspection_data) introspection_data = g_dbus_node_info_new_for_xml (introspection_xml, NULL); connection = g_dbus_connection_new_for_address_finish (result, &error); if (error) { g_warning ("Failed to connect to UI process: %s", error->message); g_error_free (error); return; } registration_id = g_dbus_connection_register_object (connection, EPHY_WEB_EXTENSION_OBJECT_PATH, introspection_data->interfaces[0], &interface_vtable, extension, NULL, &error); if (!registration_id) { g_warning ("Failed to register web extension object: %s\n", error->message); g_error_free (error); g_object_unref (connection); return; } extension->dbus_connection = connection; ephy_web_extension_emit_page_created_signals_pending (extension); } static gboolean authorize_authenticated_peer_cb (GDBusAuthObserver *observer, GIOStream *stream, GCredentials *credentials, EphyWebExtension *extension) { return ephy_dbus_peer_is_authorized (credentials); } void ephy_web_extension_initialize (EphyWebExtension *extension, WebKitWebExtension *wk_extension, const char *server_address, const char *dot_dir, gboolean is_private_profile) { GDBusAuthObserver *observer; g_return_if_fail (EPHY_IS_WEB_EXTENSION (extension)); if (extension->initialized) return; extension->initialized = TRUE; extension->extension = g_object_ref (wk_extension); extension->uri_tester = ephy_uri_tester_new (dot_dir); if (!is_private_profile) extension->form_auth_data_cache = ephy_form_auth_data_cache_new (); g_signal_connect_swapped (extension->extension, "page-created", G_CALLBACK (ephy_web_extension_page_created_cb), extension); extension->cancellable = g_cancellable_new (); observer = g_dbus_auth_observer_new (); g_signal_connect (observer, "authorize-authenticated-peer", G_CALLBACK (authorize_authenticated_peer_cb), extension); g_dbus_connection_new_for_address (server_address, G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT, observer, extension->cancellable, (GAsyncReadyCallback)dbus_connection_created_cb, extension); g_object_unref (observer); }