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