1 /**
2  * vimb - a webkit based vim like browser.
3  *
4  * Copyright (C) 2012-2018 Daniel Carl
5  *
6  * This program is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program. If not, see http://www.gnu.org/licenses/.
18  */
19 
20 #include <JavaScriptCore/JavaScript.h>
21 #include <gio/gio.h>
22 #include <glib.h>
23 #include <libsoup/soup.h>
24 #include <webkit2/webkit-web-extension.h>
25 
26 #include "ext-main.h"
27 #include "ext-dom.h"
28 #include "ext-util.h"
29 
30 static gboolean on_authorize_authenticated_peer(GDBusAuthObserver *observer,
31         GIOStream *stream, GCredentials *credentials, gpointer extension);
32 static void on_dbus_connection_created(GObject *source_object,
33         GAsyncResult *result, gpointer data);
34 static void add_onload_event_observers(WebKitDOMDocument *doc,
35         WebKitWebPage *page);
36 static void on_document_scroll(WebKitDOMEventTarget *target, WebKitDOMEvent *event,
37         WebKitWebPage *page);
38 static void emit_page_created(GDBusConnection *connection, guint64 pageid);
39 static void emit_page_created_pending(GDBusConnection *connection);
40 static void queue_page_created_signal(guint64 pageid);
41 static void dbus_emit_signal(const char *name, GVariant *data);
42 static WebKitWebPage *get_web_page_or_return_dbus_error(GDBusMethodInvocation *invocation,
43         WebKitWebExtension *extension, guint64 pageid);
44 static void dbus_handle_method_call(GDBusConnection *conn, const char *sender,
45         const char *object_path, const char *interface_name, const char *method,
46         GVariant *parameters, GDBusMethodInvocation *invocation, gpointer data);
47 static void on_editable_change_focus(WebKitDOMEventTarget *target,
48         WebKitDOMEvent *event, WebKitWebPage *page);
49 static void on_page_created(WebKitWebExtension *ext, WebKitWebPage *webpage, gpointer data);
50 static void on_web_page_document_loaded(WebKitWebPage *webpage, gpointer extension);
51 static gboolean on_web_page_send_request(WebKitWebPage *webpage, WebKitURIRequest *request,
52         WebKitURIResponse *response, gpointer extension);
53 
54 static const GDBusInterfaceVTable interface_vtable = {
55     dbus_handle_method_call,
56     NULL,
57     NULL
58 };
59 
60 static const char introspection_xml[] =
61     "<node>"
62     " <interface name='" VB_WEBEXTENSION_INTERFACE "'>"
63     "  <method name='EvalJs'>"
64     "   <arg type='t' name='page_id' direction='in'/>"
65     "   <arg type='s' name='js' direction='in'/>"
66     "   <arg type='b' name='success' direction='out'/>"
67     "   <arg type='s' name='result' direction='out'/>"
68     "  </method>"
69     "  <method name='EvalJsNoResult'>"
70     "   <arg type='t' name='page_id' direction='in'/>"
71     "   <arg type='s' name='js' direction='in'/>"
72     "  </method>"
73     "  <method name='FocusInput'>"
74     "   <arg type='t' name='page_id' direction='in'/>"
75     "  </method>"
76     "  <signal name='PageCreated'>"
77     "   <arg type='t' name='page_id' direction='out'/>"
78     "  </signal>"
79     "  <signal name='VerticalScroll'>"
80     "   <arg type='t' name='page_id' direction='out'/>"
81     "   <arg type='t' name='max' direction='out'/>"
82     "   <arg type='q' name='percent' direction='out'/>"
83     "   <arg type='t' name='top' direction='out'/>"
84     "  </signal>"
85     "  <method name='SetHeaderSetting'>"
86     "   <arg type='s' name='headers' direction='in'/>"
87     "  </method>"
88     "  <method name='LockInput'>"
89     "   <arg type='t' name='page_id' direction='in'/>"
90     "   <arg type='s' name='elemend_id' direction='in'/>"
91     "  </method>"
92     "  <method name='UnlockInput'>"
93     "   <arg type='t' name='page_id' direction='in'/>"
94     "   <arg type='s' name='elemend_id' direction='in'/>"
95     "  </method>"
96     " </interface>"
97     "</node>";
98 
99 /* Global struct to hold internal used variables. */
100 struct Ext {
101     guint               regid;
102     GDBusConnection     *connection;
103     GHashTable          *headers;
104     GHashTable          *documents;
105     GArray              *page_created_signals;
106 };
107 struct Ext ext = {0};
108 
109 
110 /**
111  * Webextension entry point.
112  */
113 G_MODULE_EXPORT
webkit_web_extension_initialize_with_user_data(WebKitWebExtension * extension,GVariant * data)114 void webkit_web_extension_initialize_with_user_data(WebKitWebExtension *extension, GVariant *data)
115 {
116     char *server_address;
117     GDBusAuthObserver *observer;
118 
119     g_variant_get(data, "(m&s)", &server_address);
120     if (!server_address) {
121         g_warning("UI process did not start D-Bus server");
122         return;
123     }
124 
125     g_signal_connect(extension, "page-created", G_CALLBACK(on_page_created), NULL);
126 
127     observer = g_dbus_auth_observer_new();
128     g_signal_connect(observer, "authorize-authenticated-peer",
129             G_CALLBACK(on_authorize_authenticated_peer), extension);
130 
131     g_dbus_connection_new_for_address(server_address,
132             G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT, observer, NULL,
133             (GAsyncReadyCallback)on_dbus_connection_created, extension);
134     g_object_unref(observer);
135 }
136 
on_authorize_authenticated_peer(GDBusAuthObserver * observer,GIOStream * stream,GCredentials * credentials,gpointer extension)137 static gboolean on_authorize_authenticated_peer(GDBusAuthObserver *observer,
138         GIOStream *stream, GCredentials *credentials, gpointer extension)
139 {
140     gboolean authorized = FALSE;
141     if (credentials) {
142         GCredentials *own_credentials;
143 
144         GError *error   = NULL;
145         own_credentials = g_credentials_new();
146         if (g_credentials_is_same_user(credentials, own_credentials, &error)) {
147             authorized = TRUE;
148         } else {
149             g_warning("Failed to authorize web extension connection: %s", error->message);
150             g_error_free(error);
151         }
152         g_object_unref(own_credentials);
153     } else {
154         g_warning ("No credentials received from UI process.\n");
155     }
156 
157     return authorized;
158 }
159 
on_dbus_connection_created(GObject * source_object,GAsyncResult * result,gpointer data)160 static void on_dbus_connection_created(GObject *source_object,
161         GAsyncResult *result, gpointer data)
162 {
163     static GDBusNodeInfo *node_info = NULL;
164     GDBusConnection *connection;
165     GError *error = NULL;
166 
167     if (!node_info) {
168         node_info = g_dbus_node_info_new_for_xml(introspection_xml, NULL);
169     }
170 
171     connection = g_dbus_connection_new_for_address_finish(result, &error);
172     if (error) {
173         g_warning("Failed to connect to UI process: %s", error->message);
174         g_error_free(error);
175         return;
176     }
177 
178     /* register the webextension object */
179     ext.regid = g_dbus_connection_register_object(
180             connection,
181             VB_WEBEXTENSION_OBJECT_PATH,
182             node_info->interfaces[0],
183             &interface_vtable,
184             WEBKIT_WEB_EXTENSION(data),
185             NULL,
186             &error);
187 
188     if (!ext.regid) {
189         g_warning("Failed to register web extension object: %s", error->message);
190         g_error_free(error);
191         g_object_unref(connection);
192         return;
193     }
194 
195     emit_page_created_pending(connection);
196     ext.connection = connection;
197 }
198 
199 /**
200  * Add observers to doc event for given document and all the contained iframes
201  * too.
202  */
add_onload_event_observers(WebKitDOMDocument * doc,WebKitWebPage * page)203 static void add_onload_event_observers(WebKitDOMDocument *doc,
204         WebKitWebPage *page)
205 {
206     WebKitDOMEventTarget *target;
207 
208     /* Add the document to the table of known documents or if already exists
209      * return to not apply observers multiple times. */
210     if (!g_hash_table_add(ext.documents, doc)) {
211         return;
212     }
213 
214     /* We have to use default view instead of the document itself in case this
215      * function is called with content document of an iframe. Else the event
216      * observing does not work. */
217     target = WEBKIT_DOM_EVENT_TARGET(webkit_dom_document_get_default_view(doc));
218 
219     webkit_dom_event_target_add_event_listener(target, "focus",
220             G_CALLBACK(on_editable_change_focus), TRUE, page);
221     webkit_dom_event_target_add_event_listener(target, "blur",
222             G_CALLBACK(on_editable_change_focus), TRUE, page);
223     /* Check for focused editable elements also if they where focused before
224      * the event observer where set up. */
225     /* TODO this is not needed for strict-focus=on */
226     on_editable_change_focus(target, NULL, page);
227 
228     /* Observe scroll events to get current position in the document. */
229     webkit_dom_event_target_add_event_listener(target, "scroll",
230             G_CALLBACK(on_document_scroll), FALSE, page);
231     /* Call the callback explicitly to make sure we have the right position
232      * shown in statusbar also in cases the user does not scroll. */
233     on_document_scroll(target, NULL, page);
234 }
235 
236 /**
237  * Callback called when the document is scrolled.
238  */
on_document_scroll(WebKitDOMEventTarget * target,WebKitDOMEvent * event,WebKitWebPage * page)239 static void on_document_scroll(WebKitDOMEventTarget *target, WebKitDOMEvent *event,
240         WebKitWebPage *page)
241 {
242     WebKitDOMDocument *doc;
243 
244     if (WEBKIT_DOM_IS_DOM_WINDOW(target)) {
245         g_object_get(target, "document", &doc, NULL);
246     } else {
247         /* target is a doc document */
248         doc = WEBKIT_DOM_DOCUMENT(target);
249     }
250 
251     if (doc) {
252         WebKitDOMElement *body, *de;
253         glong max = 0, top = 0, scrollTop, scrollHeight, clientHeight;
254         guint percent = 0;
255 
256         de = webkit_dom_document_get_document_element(doc);
257         if (!de) {
258             return;
259         }
260 
261         body = WEBKIT_DOM_ELEMENT(webkit_dom_document_get_body(doc));
262         if (!body) {
263             return;
264         }
265 
266         scrollTop = MAX(webkit_dom_element_get_scroll_top(de),
267                 webkit_dom_element_get_scroll_top(body));
268 
269         clientHeight = webkit_dom_dom_window_get_inner_height(
270                 webkit_dom_document_get_default_view(doc));
271 
272         scrollHeight = MAX(webkit_dom_element_get_scroll_height(de),
273                 webkit_dom_element_get_scroll_height(body));
274 
275         /* Get the maximum scrollable page size. This is the size of the whole
276          * document - height of the viewport. */
277         max = scrollHeight - clientHeight;
278         if (max > 0) {
279             percent = (guint)(0.5 + (scrollTop * 100 / max));
280             top = scrollTop;
281         }
282 
283         dbus_emit_signal("VerticalScroll", g_variant_new("(ttqt)",
284                 webkit_web_page_get_id(page), max, percent, top));
285     }
286 }
287 
288 /**
289  * Emit the page created signal that is used in the UI process to finish the
290  * dbus proxy connection.
291  */
emit_page_created(GDBusConnection * connection,guint64 pageid)292 static void emit_page_created(GDBusConnection *connection, guint64 pageid)
293 {
294     GError *error = NULL;
295 
296     /* propagate the signal over dbus */
297     g_dbus_connection_emit_signal(G_DBUS_CONNECTION(connection), NULL,
298             VB_WEBEXTENSION_OBJECT_PATH, VB_WEBEXTENSION_INTERFACE,
299             "PageCreated", g_variant_new("(t)", pageid), &error);
300 
301     if (error) {
302         g_warning("Failed to emit signal PageCreated: %s", error->message);
303         g_error_free(error);
304     }
305 }
306 
307 /**
308  * Emit queued page created signals.
309  */
emit_page_created_pending(GDBusConnection * connection)310 static void emit_page_created_pending(GDBusConnection *connection)
311 {
312     int i;
313     guint64 pageid;
314 
315     if (!ext.page_created_signals) {
316         return;
317     }
318 
319     for (i = 0; i < ext.page_created_signals->len; i++) {
320         pageid = g_array_index(ext.page_created_signals, guint64, i);
321         emit_page_created(connection, pageid);
322     }
323 
324     g_array_free(ext.page_created_signals, TRUE);
325     ext.page_created_signals = NULL;
326 }
327 
328 /**
329  * Write the page id of the created page to a queue to send them to the ui
330  * process when the dbus connection is established.
331  */
queue_page_created_signal(guint64 pageid)332 static void queue_page_created_signal(guint64 pageid)
333 {
334     if (!ext.page_created_signals) {
335         ext.page_created_signals = g_array_new(FALSE, FALSE, sizeof(guint64));
336     }
337     ext.page_created_signals = g_array_append_val(ext.page_created_signals, pageid);
338 }
339 
340 /**
341  * Emits a signal over dbus.
342  *
343  * @name:   Signal name to emit.
344  * @data:   GVariant value used as value for the signal or NULL.
345  */
dbus_emit_signal(const char * name,GVariant * data)346 static void dbus_emit_signal(const char *name, GVariant *data)
347 {
348     GError *error = NULL;
349 
350     if (!ext.connection) {
351         return;
352     }
353 
354     /* propagate the signal over dbus */
355     g_dbus_connection_emit_signal(ext.connection, NULL,
356             VB_WEBEXTENSION_OBJECT_PATH, VB_WEBEXTENSION_INTERFACE, name,
357             data, &error);
358     if (error) {
359         g_warning("Failed to emit signal '%s': %s", name, error->message);
360         g_error_free(error);
361     }
362 }
363 
get_web_page_or_return_dbus_error(GDBusMethodInvocation * invocation,WebKitWebExtension * extension,guint64 pageid)364 static WebKitWebPage *get_web_page_or_return_dbus_error(GDBusMethodInvocation *invocation,
365         WebKitWebExtension *extension, guint64 pageid)
366 {
367     WebKitWebPage *page = webkit_web_extension_get_page(extension, pageid);
368     if (!page) {
369         g_warning("invalid page id %lu", pageid);
370         g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR,
371                 G_DBUS_ERROR_INVALID_ARGS, "Invalid page ID: %"G_GUINT64_FORMAT, pageid);
372     }
373 
374     return page;
375 }
376 
377 /**
378  * Handle dbus method calls.
379  */
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)380 static void dbus_handle_method_call(GDBusConnection *conn, const char *sender,
381         const char *object_path, const char *interface_name, const char *method,
382         GVariant *parameters, GDBusMethodInvocation *invocation, gpointer extension)
383 {
384     char *value;
385     guint64 pageid;
386     WebKitWebPage *page;
387 
388     if (g_str_has_prefix(method, "EvalJs")) {
389         char *result       = NULL;
390         gboolean success;
391         gboolean no_result;
392         JSValueRef ref     = NULL;
393         JSGlobalContextRef jsContext;
394 
395         g_variant_get(parameters, "(ts)", &pageid, &value);
396         page = get_web_page_or_return_dbus_error(invocation, WEBKIT_WEB_EXTENSION(extension), pageid);
397         if (!page) {
398             return;
399         }
400 
401         no_result = !g_strcmp0(method, "EvalJsNoResult");
402         jsContext = webkit_frame_get_javascript_context_for_script_world(
403             webkit_web_page_get_main_frame(page),
404             webkit_script_world_get_default()
405         );
406 
407         success = ext_util_js_eval(jsContext, value, &ref);
408 
409         if (no_result) {
410             g_dbus_method_invocation_return_value(invocation, NULL);
411         } else {
412             result = ext_util_js_ref_to_string(jsContext, ref);
413             g_dbus_method_invocation_return_value(invocation, g_variant_new("(bs)", success, result));
414             g_free(result);
415         }
416     } else if (!g_strcmp0(method, "FocusInput")) {
417         g_variant_get(parameters, "(t)", &pageid);
418         page = get_web_page_or_return_dbus_error(invocation, WEBKIT_WEB_EXTENSION(extension), pageid);
419         if (!page) {
420             return;
421         }
422         ext_dom_focus_input(webkit_web_page_get_dom_document(page));
423         g_dbus_method_invocation_return_value(invocation, NULL);
424     } else if (!g_strcmp0(method, "SetHeaderSetting")) {
425         g_variant_get(parameters, "(s)", &value);
426 
427         if (ext.headers) {
428             soup_header_free_param_list(ext.headers);
429             ext.headers = NULL;
430         }
431         ext.headers = soup_header_parse_param_list(value);
432         g_dbus_method_invocation_return_value(invocation, NULL);
433     } else if (!g_strcmp0(method, "LockInput")) {
434         g_variant_get(parameters, "(ts)", &pageid, &value);
435         page = get_web_page_or_return_dbus_error(invocation, WEBKIT_WEB_EXTENSION(extension), pageid);
436         if (!page) {
437             return;
438         }
439         ext_dom_lock_input(webkit_web_page_get_dom_document(page), value);
440         g_dbus_method_invocation_return_value(invocation, NULL);
441     } else if (!g_strcmp0(method, "UnlockInput")) {
442         g_variant_get(parameters, "(ts)", &pageid, &value);
443         page = get_web_page_or_return_dbus_error(invocation, WEBKIT_WEB_EXTENSION(extension), pageid);
444         if (!page) {
445             return;
446         }
447         ext_dom_unlock_input(webkit_web_page_get_dom_document(page), value);
448         g_dbus_method_invocation_return_value(invocation, NULL);
449     }
450 }
451 
452 /**
453  * Callback called if a editable element changes it focus state.
454  * Event target may be a WebKitDOMDocument (in case of iframe) or a
455  * WebKitDOMDOMWindow.
456  */
on_editable_change_focus(WebKitDOMEventTarget * target,WebKitDOMEvent * event,WebKitWebPage * page)457 static void on_editable_change_focus(WebKitDOMEventTarget *target,
458         WebKitDOMEvent *event, WebKitWebPage *page)
459 {
460     WebKitDOMDocument *doc;
461     WebKitDOMDOMWindow *dom_window;
462     WebKitDOMElement *active;
463     GVariant *variant;
464     char *message;
465 
466     if (WEBKIT_DOM_IS_DOM_WINDOW(target)) {
467         g_object_get(target, "document", &doc, NULL);
468     } else {
469         /* target is a doc document */
470         doc = WEBKIT_DOM_DOCUMENT(target);
471     }
472 
473     dom_window = webkit_dom_document_get_default_view(doc);
474     if (!dom_window) {
475         return;
476     }
477 
478     active = webkit_dom_document_get_active_element(doc);
479     /* Don't do anything if there is no active element */
480     if (!active) {
481         return;
482     }
483     if (WEBKIT_DOM_IS_HTML_IFRAME_ELEMENT(active)) {
484         WebKitDOMHTMLIFrameElement *iframe;
485         WebKitDOMDocument *subdoc;
486 
487         iframe = WEBKIT_DOM_HTML_IFRAME_ELEMENT(active);
488         subdoc = webkit_dom_html_iframe_element_get_content_document(iframe);
489         add_onload_event_observers(subdoc, page);
490         return;
491     }
492 
493     /* Check if the active element is an editable element. */
494     variant = g_variant_new("(tb)", webkit_web_page_get_id(page),
495             ext_dom_is_editable(active));
496     message = g_variant_print(variant, FALSE);
497     g_variant_unref(variant);
498     if (!webkit_dom_dom_window_webkit_message_handlers_post_message(dom_window, "focus", message)) {
499         g_warning("Error sending focus message");
500     }
501     g_free(message);
502     g_object_unref(dom_window);
503 }
504 
505 /**
506  * Callback for web extensions page-created signal.
507  */
on_page_created(WebKitWebExtension * extension,WebKitWebPage * webpage,gpointer data)508 static void on_page_created(WebKitWebExtension *extension, WebKitWebPage *webpage, gpointer data)
509 {
510     guint64 pageid = webkit_web_page_get_id(webpage);
511 
512     if (ext.connection) {
513         emit_page_created(ext.connection, pageid);
514     } else {
515         queue_page_created_signal(pageid);
516     }
517 
518     g_object_connect(webpage,
519             "signal::send-request", G_CALLBACK(on_web_page_send_request), extension,
520             "signal::document-loaded", G_CALLBACK(on_web_page_document_loaded), extension,
521             NULL);
522 }
523 
524 /**
525  * Callback for web pages document-loaded signal.
526  */
on_web_page_document_loaded(WebKitWebPage * webpage,gpointer extension)527 static void on_web_page_document_loaded(WebKitWebPage *webpage, gpointer extension)
528 {
529     /* If there is a hashtable of known document - detroy this and create a
530      * new hashtable. */
531     if (ext.documents) {
532         g_hash_table_unref(ext.documents);
533     }
534     ext.documents = g_hash_table_new(g_direct_hash, g_direct_equal);
535 
536     add_onload_event_observers(webkit_web_page_get_dom_document(webpage), webpage);
537 }
538 
539 /**
540  * Callback for web pages send-request signal.
541  */
on_web_page_send_request(WebKitWebPage * webpage,WebKitURIRequest * request,WebKitURIResponse * response,gpointer extension)542 static gboolean on_web_page_send_request(WebKitWebPage *webpage, WebKitURIRequest *request,
543         WebKitURIResponse *response, gpointer extension)
544 {
545     char *name, *value;
546     SoupMessageHeaders *headers;
547     GHashTableIter iter;
548 
549     if (!ext.headers) {
550         return FALSE;
551     }
552 
553     /* Change request headers according to the users preferences. */
554     headers = webkit_uri_request_get_http_headers(request);
555     if (!headers) {
556         return FALSE;
557     }
558 
559     g_hash_table_iter_init(&iter, ext.headers);
560     while (g_hash_table_iter_next(&iter, (gpointer*)&name, (gpointer*)&value)) {
561         /* Null value is used to indicate that the header should be
562          * removed completely. */
563         if (value == NULL) {
564             soup_message_headers_remove(headers, name);
565         } else {
566             soup_message_headers_replace(headers, name, value);
567         }
568     }
569 
570     return FALSE;
571 }
572