/** * vimb - a webkit based vim like browser. * * Copyright (C) 2012-2018 Daniel Carl * * 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 3 of the License, 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 http://www.gnu.org/licenses/. */ #include #include #include #include #include #include "ext-main.h" #include "ext-dom.h" #include "ext-util.h" static gboolean on_authorize_authenticated_peer(GDBusAuthObserver *observer, GIOStream *stream, GCredentials *credentials, gpointer extension); static void on_dbus_connection_created(GObject *source_object, GAsyncResult *result, gpointer data); static void add_onload_event_observers(WebKitDOMDocument *doc, WebKitWebPage *page); static void on_document_scroll(WebKitDOMEventTarget *target, WebKitDOMEvent *event, WebKitWebPage *page); static void emit_page_created(GDBusConnection *connection, guint64 pageid); static void emit_page_created_pending(GDBusConnection *connection); static void queue_page_created_signal(guint64 pageid); static void dbus_emit_signal(const char *name, GVariant *data); static WebKitWebPage *get_web_page_or_return_dbus_error(GDBusMethodInvocation *invocation, WebKitWebExtension *extension, guint64 pageid); static void dbus_handle_method_call(GDBusConnection *conn, const char *sender, const char *object_path, const char *interface_name, const char *method, GVariant *parameters, GDBusMethodInvocation *invocation, gpointer data); static void on_editable_change_focus(WebKitDOMEventTarget *target, WebKitDOMEvent *event, WebKitWebPage *page); static void on_page_created(WebKitWebExtension *ext, WebKitWebPage *webpage, gpointer data); static void on_web_page_document_loaded(WebKitWebPage *webpage, gpointer extension); static gboolean on_web_page_send_request(WebKitWebPage *webpage, WebKitURIRequest *request, WebKitURIResponse *response, gpointer extension); static const GDBusInterfaceVTable interface_vtable = { dbus_handle_method_call, NULL, NULL }; static const char introspection_xml[] = "" " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " ""; /* Global struct to hold internal used variables. */ struct Ext { guint regid; GDBusConnection *connection; GHashTable *headers; GHashTable *documents; GArray *page_created_signals; }; struct Ext ext = {0}; /** * Webextension entry point. */ G_MODULE_EXPORT void webkit_web_extension_initialize_with_user_data(WebKitWebExtension *extension, GVariant *data) { char *server_address; GDBusAuthObserver *observer; g_variant_get(data, "(m&s)", &server_address); if (!server_address) { g_warning("UI process did not start D-Bus server"); return; } g_signal_connect(extension, "page-created", G_CALLBACK(on_page_created), NULL); observer = g_dbus_auth_observer_new(); g_signal_connect(observer, "authorize-authenticated-peer", G_CALLBACK(on_authorize_authenticated_peer), extension); g_dbus_connection_new_for_address(server_address, G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT, observer, NULL, (GAsyncReadyCallback)on_dbus_connection_created, extension); g_object_unref(observer); } static gboolean on_authorize_authenticated_peer(GDBusAuthObserver *observer, GIOStream *stream, GCredentials *credentials, gpointer extension) { gboolean authorized = FALSE; if (credentials) { GCredentials *own_credentials; GError *error = NULL; own_credentials = g_credentials_new(); if (g_credentials_is_same_user(credentials, own_credentials, &error)) { authorized = TRUE; } else { g_warning("Failed to authorize web extension connection: %s", error->message); g_error_free(error); } g_object_unref(own_credentials); } else { g_warning ("No credentials received from UI process.\n"); } return authorized; } static void on_dbus_connection_created(GObject *source_object, GAsyncResult *result, gpointer data) { static GDBusNodeInfo *node_info = NULL; GDBusConnection *connection; GError *error = NULL; if (!node_info) { node_info = 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; } /* register the webextension object */ ext.regid = g_dbus_connection_register_object( connection, VB_WEBEXTENSION_OBJECT_PATH, node_info->interfaces[0], &interface_vtable, WEBKIT_WEB_EXTENSION(data), NULL, &error); if (!ext.regid) { g_warning("Failed to register web extension object: %s", error->message); g_error_free(error); g_object_unref(connection); return; } emit_page_created_pending(connection); ext.connection = connection; } /** * Add observers to doc event for given document and all the contained iframes * too. */ static void add_onload_event_observers(WebKitDOMDocument *doc, WebKitWebPage *page) { WebKitDOMEventTarget *target; /* Add the document to the table of known documents or if already exists * return to not apply observers multiple times. */ if (!g_hash_table_add(ext.documents, doc)) { return; } /* We have to use default view instead of the document itself in case this * function is called with content document of an iframe. Else the event * observing does not work. */ target = WEBKIT_DOM_EVENT_TARGET(webkit_dom_document_get_default_view(doc)); webkit_dom_event_target_add_event_listener(target, "focus", G_CALLBACK(on_editable_change_focus), TRUE, page); webkit_dom_event_target_add_event_listener(target, "blur", G_CALLBACK(on_editable_change_focus), TRUE, page); /* Check for focused editable elements also if they where focused before * the event observer where set up. */ /* TODO this is not needed for strict-focus=on */ on_editable_change_focus(target, NULL, page); /* Observe scroll events to get current position in the document. */ webkit_dom_event_target_add_event_listener(target, "scroll", G_CALLBACK(on_document_scroll), FALSE, page); /* Call the callback explicitly to make sure we have the right position * shown in statusbar also in cases the user does not scroll. */ on_document_scroll(target, NULL, page); } /** * Callback called when the document is scrolled. */ static void on_document_scroll(WebKitDOMEventTarget *target, WebKitDOMEvent *event, WebKitWebPage *page) { WebKitDOMDocument *doc; if (WEBKIT_DOM_IS_DOM_WINDOW(target)) { g_object_get(target, "document", &doc, NULL); } else { /* target is a doc document */ doc = WEBKIT_DOM_DOCUMENT(target); } if (doc) { WebKitDOMElement *body, *de; glong max = 0, top = 0, scrollTop, scrollHeight, clientHeight; guint percent = 0; de = webkit_dom_document_get_document_element(doc); if (!de) { return; } body = WEBKIT_DOM_ELEMENT(webkit_dom_document_get_body(doc)); if (!body) { return; } scrollTop = MAX(webkit_dom_element_get_scroll_top(de), webkit_dom_element_get_scroll_top(body)); clientHeight = webkit_dom_dom_window_get_inner_height( webkit_dom_document_get_default_view(doc)); scrollHeight = MAX(webkit_dom_element_get_scroll_height(de), webkit_dom_element_get_scroll_height(body)); /* Get the maximum scrollable page size. This is the size of the whole * document - height of the viewport. */ max = scrollHeight - clientHeight; if (max > 0) { percent = (guint)(0.5 + (scrollTop * 100 / max)); top = scrollTop; } dbus_emit_signal("VerticalScroll", g_variant_new("(ttqt)", webkit_web_page_get_id(page), max, percent, top)); } } /** * Emit the page created signal that is used in the UI process to finish the * dbus proxy connection. */ static void emit_page_created(GDBusConnection *connection, guint64 pageid) { GError *error = NULL; /* propagate the signal over dbus */ g_dbus_connection_emit_signal(G_DBUS_CONNECTION(connection), NULL, VB_WEBEXTENSION_OBJECT_PATH, VB_WEBEXTENSION_INTERFACE, "PageCreated", g_variant_new("(t)", pageid), &error); if (error) { g_warning("Failed to emit signal PageCreated: %s", error->message); g_error_free(error); } } /** * Emit queued page created signals. */ static void emit_page_created_pending(GDBusConnection *connection) { int i; guint64 pageid; if (!ext.page_created_signals) { return; } for (i = 0; i < ext.page_created_signals->len; i++) { pageid = g_array_index(ext.page_created_signals, guint64, i); emit_page_created(connection, pageid); } g_array_free(ext.page_created_signals, TRUE); ext.page_created_signals = NULL; } /** * Write the page id of the created page to a queue to send them to the ui * process when the dbus connection is established. */ static void queue_page_created_signal(guint64 pageid) { if (!ext.page_created_signals) { ext.page_created_signals = g_array_new(FALSE, FALSE, sizeof(guint64)); } ext.page_created_signals = g_array_append_val(ext.page_created_signals, pageid); } /** * Emits a signal over dbus. * * @name: Signal name to emit. * @data: GVariant value used as value for the signal or NULL. */ static void dbus_emit_signal(const char *name, GVariant *data) { GError *error = NULL; if (!ext.connection) { return; } /* propagate the signal over dbus */ g_dbus_connection_emit_signal(ext.connection, NULL, VB_WEBEXTENSION_OBJECT_PATH, VB_WEBEXTENSION_INTERFACE, name, data, &error); if (error) { g_warning("Failed to emit signal '%s': %s", name, error->message); g_error_free(error); } } static WebKitWebPage *get_web_page_or_return_dbus_error(GDBusMethodInvocation *invocation, WebKitWebExtension *extension, guint64 pageid) { WebKitWebPage *page = webkit_web_extension_get_page(extension, pageid); if (!page) { g_warning("invalid page id %lu", pageid); g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS, "Invalid page ID: %"G_GUINT64_FORMAT, pageid); } return page; } /** * Handle dbus method calls. */ static void dbus_handle_method_call(GDBusConnection *conn, const char *sender, const char *object_path, const char *interface_name, const char *method, GVariant *parameters, GDBusMethodInvocation *invocation, gpointer extension) { char *value; guint64 pageid; WebKitWebPage *page; if (g_str_has_prefix(method, "EvalJs")) { char *result = NULL; gboolean success; gboolean no_result; JSValueRef ref = NULL; JSGlobalContextRef jsContext; g_variant_get(parameters, "(ts)", &pageid, &value); page = get_web_page_or_return_dbus_error(invocation, WEBKIT_WEB_EXTENSION(extension), pageid); if (!page) { return; } no_result = !g_strcmp0(method, "EvalJsNoResult"); jsContext = webkit_frame_get_javascript_context_for_script_world( webkit_web_page_get_main_frame(page), webkit_script_world_get_default() ); success = ext_util_js_eval(jsContext, value, &ref); if (no_result) { g_dbus_method_invocation_return_value(invocation, NULL); } else { result = ext_util_js_ref_to_string(jsContext, ref); g_dbus_method_invocation_return_value(invocation, g_variant_new("(bs)", success, result)); g_free(result); } } else if (!g_strcmp0(method, "FocusInput")) { g_variant_get(parameters, "(t)", &pageid); page = get_web_page_or_return_dbus_error(invocation, WEBKIT_WEB_EXTENSION(extension), pageid); if (!page) { return; } ext_dom_focus_input(webkit_web_page_get_dom_document(page)); g_dbus_method_invocation_return_value(invocation, NULL); } else if (!g_strcmp0(method, "SetHeaderSetting")) { g_variant_get(parameters, "(s)", &value); if (ext.headers) { soup_header_free_param_list(ext.headers); ext.headers = NULL; } ext.headers = soup_header_parse_param_list(value); g_dbus_method_invocation_return_value(invocation, NULL); } else if (!g_strcmp0(method, "LockInput")) { g_variant_get(parameters, "(ts)", &pageid, &value); page = get_web_page_or_return_dbus_error(invocation, WEBKIT_WEB_EXTENSION(extension), pageid); if (!page) { return; } ext_dom_lock_input(webkit_web_page_get_dom_document(page), value); g_dbus_method_invocation_return_value(invocation, NULL); } else if (!g_strcmp0(method, "UnlockInput")) { g_variant_get(parameters, "(ts)", &pageid, &value); page = get_web_page_or_return_dbus_error(invocation, WEBKIT_WEB_EXTENSION(extension), pageid); if (!page) { return; } ext_dom_unlock_input(webkit_web_page_get_dom_document(page), value); g_dbus_method_invocation_return_value(invocation, NULL); } } /** * Callback called if a editable element changes it focus state. * Event target may be a WebKitDOMDocument (in case of iframe) or a * WebKitDOMDOMWindow. */ static void on_editable_change_focus(WebKitDOMEventTarget *target, WebKitDOMEvent *event, WebKitWebPage *page) { WebKitDOMDocument *doc; WebKitDOMDOMWindow *dom_window; WebKitDOMElement *active; GVariant *variant; char *message; if (WEBKIT_DOM_IS_DOM_WINDOW(target)) { g_object_get(target, "document", &doc, NULL); } else { /* target is a doc document */ doc = WEBKIT_DOM_DOCUMENT(target); } dom_window = webkit_dom_document_get_default_view(doc); if (!dom_window) { return; } active = webkit_dom_document_get_active_element(doc); /* Don't do anything if there is no active element */ if (!active) { return; } if (WEBKIT_DOM_IS_HTML_IFRAME_ELEMENT(active)) { WebKitDOMHTMLIFrameElement *iframe; WebKitDOMDocument *subdoc; iframe = WEBKIT_DOM_HTML_IFRAME_ELEMENT(active); subdoc = webkit_dom_html_iframe_element_get_content_document(iframe); add_onload_event_observers(subdoc, page); return; } /* Check if the active element is an editable element. */ variant = g_variant_new("(tb)", webkit_web_page_get_id(page), ext_dom_is_editable(active)); message = g_variant_print(variant, FALSE); g_variant_unref(variant); if (!webkit_dom_dom_window_webkit_message_handlers_post_message(dom_window, "focus", message)) { g_warning("Error sending focus message"); } g_free(message); g_object_unref(dom_window); } /** * Callback for web extensions page-created signal. */ static void on_page_created(WebKitWebExtension *extension, WebKitWebPage *webpage, gpointer data) { guint64 pageid = webkit_web_page_get_id(webpage); if (ext.connection) { emit_page_created(ext.connection, pageid); } else { queue_page_created_signal(pageid); } g_object_connect(webpage, "signal::send-request", G_CALLBACK(on_web_page_send_request), extension, "signal::document-loaded", G_CALLBACK(on_web_page_document_loaded), extension, NULL); } /** * Callback for web pages document-loaded signal. */ static void on_web_page_document_loaded(WebKitWebPage *webpage, gpointer extension) { /* If there is a hashtable of known document - detroy this and create a * new hashtable. */ if (ext.documents) { g_hash_table_unref(ext.documents); } ext.documents = g_hash_table_new(g_direct_hash, g_direct_equal); add_onload_event_observers(webkit_web_page_get_dom_document(webpage), webpage); } /** * Callback for web pages send-request signal. */ static gboolean on_web_page_send_request(WebKitWebPage *webpage, WebKitURIRequest *request, WebKitURIResponse *response, gpointer extension) { char *name, *value; SoupMessageHeaders *headers; GHashTableIter iter; if (!ext.headers) { return FALSE; } /* Change request headers according to the users preferences. */ headers = webkit_uri_request_get_http_headers(request); if (!headers) { return FALSE; } g_hash_table_iter_init(&iter, ext.headers); while (g_hash_table_iter_next(&iter, (gpointer*)&name, (gpointer*)&value)) { /* Null value is used to indicate that the header should be * removed completely. */ if (value == NULL) { soup_message_headers_remove(headers, name); } else { soup_message_headers_replace(headers, name, value); } } return FALSE; }