1 /**
2  * @file liferea_web_extension.c  Control WebKit2 via DBUS from Liferea
3  *
4  * Copyright (C) 2016 Leiaz <leiaz@free.fr>
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 2 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, write to the Free Software
18  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19  */
20 
21 #include <webkit2/webkit-web-extension.h>
22 #define WEBKIT_DOM_USE_UNSTABLE_API
23 #include <webkitdom/WebKitDOMDOMWindowUnstable.h>
24 
25 #include "liferea_web_extension.h"
26 #include "liferea_web_extension_names.h"
27 
28 struct _LifereaWebExtension {
29 	GObject 		parent;
30 
31 	GDBusConnection 	*connection;
32 	WebKitWebExtension 	*webkit_extension;
33 	GArray 			*pending_pages_created;
34 	gboolean 		initialized;
35 
36 	GSettings 		*liferea_settings;
37 };
38 
39 struct _LifereaWebExtensionClass {
40 	GObjectClass parent_class;
41 };
42 
43 G_DEFINE_TYPE (LifereaWebExtension, liferea_web_extension, G_TYPE_OBJECT)
44 
45 static const char introspection_xml[] =
46   "<node>"
47   " <interface name='net.sf.liferea.WebExtension'>"
48   "  <method name='ScrollPageDown'>"
49   "   <arg type='t' name='page_id' direction='in'/>"
50   "   <arg type='b' name='scrolled' direction='out'/>"
51   "  </method>"
52   "  <signal name='PageCreated'>"
53   "   <arg type='t' name='page_id' direction='out'/>"
54   "  </signal>"
55   " </interface>"
56   "</node>";
57 
58 static void
liferea_web_extension_dispose(GObject * object)59 liferea_web_extension_dispose (GObject *object)
60 {
61 	LifereaWebExtension *extension = LIFEREA_WEB_EXTENSION (object);
62 
63 	g_clear_object (&extension->connection);
64 	g_clear_object (&extension->webkit_extension);
65 	g_clear_object (&extension->liferea_settings);
66 }
67 
68 static void
liferea_web_extension_class_init(LifereaWebExtensionClass * klass)69 liferea_web_extension_class_init (LifereaWebExtensionClass *klass)
70 {
71 	GObjectClass *object_class = G_OBJECT_CLASS (klass);
72 	object_class->dispose = liferea_web_extension_dispose;
73 }
74 
75 static void
liferea_web_extension_init(LifereaWebExtension * self)76 liferea_web_extension_init (LifereaWebExtension *self)
77 {
78 	self->webkit_extension = NULL;
79 	self->connection = NULL;
80 	self->pending_pages_created = NULL;
81 	self->initialized = FALSE;
82 	self->liferea_settings = g_settings_new ("net.sf.liferea");
83 }
84 
85 static WebKitDOMDOMWindow*
liferea_web_extension_get_dom_window(LifereaWebExtension * self,guint64 page_id)86 liferea_web_extension_get_dom_window (LifereaWebExtension *self, guint64 page_id)
87 {
88 	WebKitWebPage *page;
89 	WebKitDOMDocument *document;
90 	WebKitDOMDOMWindow *window;
91 
92 	page = webkit_web_extension_get_page (self->webkit_extension, page_id);
93 	document = webkit_web_page_get_dom_document (page);
94 	window = webkit_dom_document_get_default_view (document);
95 
96 	return window;
97 }
98 
99 /*
100  * \returns TRUE if scrolling happened, FALSE if the end was reached
101  */
102 static gboolean
liferea_web_extension_scroll_page_down(LifereaWebExtension * self,guint64 page_id)103 liferea_web_extension_scroll_page_down (LifereaWebExtension *self, guint64 page_id)
104 {
105 	glong old_scroll_y, new_scroll_y, increment;
106 	WebKitDOMDOMWindow *window;
107 
108 	window = liferea_web_extension_get_dom_window (self, page_id);
109 
110 	old_scroll_y = webkit_dom_dom_window_get_scroll_y (window);
111 	increment = webkit_dom_dom_window_get_inner_height (window);
112 	webkit_dom_dom_window_scroll_by (window, 0, increment);
113 	new_scroll_y = webkit_dom_dom_window_get_scroll_y (window);
114 
115 	return (new_scroll_y > old_scroll_y);
116 }
117 
118 static gboolean
on_authorize_authenticated_peer(GDBusAuthObserver * observer,GIOStream * stream,GCredentials * credentials,gpointer extension)119 on_authorize_authenticated_peer (GDBusAuthObserver 	*observer,
120 				 GIOStream		*stream,
121 				 GCredentials		*credentials,
122 				 gpointer		extension)
123 {
124 	gboolean authorized = FALSE;
125 	GCredentials *own_credentials = NULL;
126 	GError *error = NULL;
127 
128 	if (!credentials) {
129 		g_warning ("No credentials received from Liferea.\n");
130 		return FALSE;
131 	}
132 
133 	own_credentials = g_credentials_new ();
134 
135 	if (g_credentials_is_same_user (credentials, own_credentials, &error)) {
136 		authorized = TRUE;
137 	} else {
138 		g_warning ("Error authorizing connection to Liferea : %s\n", error->message);
139 		g_error_free (error);
140 	}
141 	g_object_unref (own_credentials);
142 
143 	return authorized;
144 }
145 
146 static void
handle_dbus_method_call(GDBusConnection * connection,const gchar * sender,const gchar * object_path,const gchar * interface_name,const gchar * method_name,GVariant * parameters,GDBusMethodInvocation * invocation,gpointer user_data)147 handle_dbus_method_call (GDBusConnection 	*connection,
148 			 const gchar 		*sender,
149 			 const gchar 		*object_path,
150 			 const gchar 		*interface_name,
151 			 const gchar 		*method_name,
152 			 GVariant 		*parameters,
153 			 GDBusMethodInvocation 	*invocation,
154 			 gpointer 		user_data)
155 {
156 	if (g_strcmp0 (method_name, "ScrollPageDown") == 0) {
157 		guint64 page_id;
158 		gboolean scrolled;
159 
160 		g_variant_get (parameters, "(t)", &page_id);
161 		scrolled = liferea_web_extension_scroll_page_down (LIFEREA_WEB_EXTENSION (user_data), page_id);
162 		g_dbus_method_invocation_return_value (invocation, g_variant_new ("(b)", scrolled));
163 	}
164 }
165 
166 static const GDBusInterfaceVTable interface_vtable = {
167 	handle_dbus_method_call,
168 	NULL,
169 	NULL
170 };
171 
172 static void
liferea_web_extension_emit_page_created(LifereaWebExtension * extension,guint64 page_id)173 liferea_web_extension_emit_page_created (LifereaWebExtension *extension, guint64 page_id)
174 {
175 	g_dbus_connection_emit_signal (
176 		extension->connection,
177 		NULL,
178 		LIFEREA_WEB_EXTENSION_OBJECT_PATH,
179 		LIFEREA_WEB_EXTENSION_INTERFACE_NAME,
180 		"PageCreated",
181 		g_variant_new ("(t)", page_id),
182 		NULL);
183 }
184 
185 static void
liferea_web_extension_queue_page_created(LifereaWebExtension * extension,guint64 page_id)186 liferea_web_extension_queue_page_created (LifereaWebExtension *extension, guint64 page_id)
187 {
188 	if (!extension->pending_pages_created) {
189 		extension->pending_pages_created = g_array_new (FALSE, FALSE, sizeof (guint64));
190 	}
191 
192 	g_array_append_val (extension->pending_pages_created, page_id);
193 }
194 
195 static void
liferea_web_extension_emit_pending_pages_created(LifereaWebExtension * extension)196 liferea_web_extension_emit_pending_pages_created (LifereaWebExtension *extension)
197 {
198 	guint i;
199 
200 	if (!extension->pending_pages_created)
201 		return;
202 
203 	for (i = 0;i<extension->pending_pages_created->len;++i) {
204 		guint64 page_id = g_array_index (extension->pending_pages_created, guint64, i);
205 		liferea_web_extension_emit_page_created (extension, page_id);
206 	}
207 	g_array_free (extension->pending_pages_created, TRUE);
208 	extension->pending_pages_created = NULL;
209 }
210 
211 static gboolean
on_send_request(WebKitWebPage * web_page,WebKitURIRequest * request,WebKitURIResponse * redirected_response,gpointer web_extension)212 on_send_request (WebKitWebPage 		*web_page,
213 		 WebKitURIRequest 	*request,
214 		 WebKitURIResponse 	*redirected_response,
215 		 gpointer 		web_extension)
216 {
217 	SoupMessageHeaders *headers = webkit_uri_request_get_http_headers (request);
218 	gboolean do_not_track;
219 
220 	do_not_track = g_settings_get_boolean (
221 	    LIFEREA_WEB_EXTENSION (web_extension)->liferea_settings,
222 	    "do-not-track");
223 
224 	if (do_not_track && headers) {
225 		soup_message_headers_append (headers, "DNT", "1");
226 	}
227 
228 	return FALSE;
229 }
230 
231 static void
on_page_created(WebKitWebExtension * webkit_extension,WebKitWebPage * web_page,gpointer extension)232 on_page_created (WebKitWebExtension *webkit_extension,
233 		 WebKitWebPage      *web_page,
234 		 gpointer            extension)
235 {
236 	guint64 page_id;
237 
238 	g_signal_connect (
239 		web_page,
240 		"send-request",
241 		G_CALLBACK (on_send_request),
242 		extension
243 	);
244 
245 	page_id = webkit_web_page_get_id (web_page);
246 	if (LIFEREA_WEB_EXTENSION (extension)->connection) {
247 		liferea_web_extension_emit_page_created (LIFEREA_WEB_EXTENSION (extension), page_id);
248 	} else {
249 		liferea_web_extension_queue_page_created (LIFEREA_WEB_EXTENSION (extension), page_id);
250 	}
251 }
252 
253 static void
on_dbus_connection_created(GObject * source_object,GAsyncResult * result,gpointer user_data)254 on_dbus_connection_created (GObject 		*source_object,
255 			    GAsyncResult 	*result,
256 			    gpointer	 	user_data)
257 {
258 	GDBusNodeInfo *introspection_data = NULL;
259 	GDBusConnection *connection = NULL;
260 	guint registration_id = 0;
261 	GError *error = NULL;
262 	LifereaWebExtension *extension = LIFEREA_WEB_EXTENSION (user_data);
263 
264 	introspection_data = g_dbus_node_info_new_for_xml (introspection_xml, NULL);
265 
266 	connection = g_dbus_connection_new_for_address_finish (result, &error);
267 	if (error) {
268 		g_warning ("Extension failed to connect : %s", error->message);
269 		g_error_free (error);
270 		return;
271 	}
272 
273 	registration_id = g_dbus_connection_register_object (connection,
274 		LIFEREA_WEB_EXTENSION_OBJECT_PATH,
275 		introspection_data->interfaces[0],
276 		&interface_vtable,
277 		extension,
278 		NULL,
279 		&error);
280 
281 	g_dbus_node_info_unref (introspection_data);
282 	if (!registration_id) {
283 		g_warning ("Failed to register web extension object: %s\n", error->message);
284 		g_error_free (error);
285 		g_object_unref (connection);
286 		return;
287 	}
288 
289 	extension->connection = connection;
290 	liferea_web_extension_emit_pending_pages_created (extension);
291 }
292 
293 static gpointer
liferea_web_extension_new(gpointer data)294 liferea_web_extension_new (gpointer data)
295 {
296 	return g_object_new (LIFEREA_TYPE_WEB_EXTENSION, NULL);
297 }
298 
299 LifereaWebExtension *
liferea_web_extension_get(void)300 liferea_web_extension_get (void)
301 {
302 	static GOnce init_once = G_ONCE_INIT;
303 
304 	g_once (&init_once, liferea_web_extension_new, NULL);
305 
306 	return init_once.retval;
307 }
308 
309 void
liferea_web_extension_initialize(LifereaWebExtension * extension,WebKitWebExtension * webkit_extension,const gchar * server_address)310 liferea_web_extension_initialize (LifereaWebExtension 	*extension,
311 				  WebKitWebExtension 	*webkit_extension,
312 				  const gchar 		*server_address)
313 {
314 
315 	if (extension->initialized)
316 		return;
317 
318 	g_signal_connect (
319 		webkit_extension,
320 		"page-created",
321 		G_CALLBACK (on_page_created),
322 		extension);
323 
324 	GDBusAuthObserver	*observer;
325 
326 	extension->initialized = TRUE;
327 	extension->webkit_extension = g_object_ref (webkit_extension);
328 
329 	observer = g_dbus_auth_observer_new ();
330 
331 	g_signal_connect (
332 		observer,
333 		"authorize-authenticated-peer",
334 		G_CALLBACK (on_authorize_authenticated_peer),
335 		extension);
336 
337 	g_dbus_connection_new_for_address (
338 		server_address,
339 		G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT,
340 		observer,
341 		NULL,
342 		(GAsyncReadyCallback)on_dbus_connection_created,
343 		extension);
344 
345 	g_object_unref (observer);
346 }
347