1 /* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /*
3 * Copyright © 2020 Jan-Michael Brummer <jan.brummer@tabos.org>
4 *
5 * This file is part of Epiphany.
6 *
7 * Epiphany is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * Epiphany is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with Epiphany. If not, see <http://www.gnu.org/licenses/>.
19 */
20
21 #include "config.h"
22 #include "ephy-reader-handler.h"
23
24 #include "ephy-embed-container.h"
25 #include "ephy-embed-shell.h"
26 #include "ephy-lib-type-builtins.h"
27 #include "ephy-settings.h"
28 #include "ephy-web-view.h"
29
30 #include <gio/gio.h>
31 #include <glib/gi18n.h>
32 #include <string.h>
33
34 struct _EphyReaderHandler {
35 GObject parent_instance;
36
37 GList *outstanding_requests;
38 };
39
40 G_DEFINE_TYPE (EphyReaderHandler, ephy_reader_handler, G_TYPE_OBJECT)
41
42 typedef struct {
43 EphyReaderHandler *source_handler;
44 WebKitURISchemeRequest *scheme_request;
45 WebKitWebView *web_view;
46 GCancellable *cancellable;
47 guint load_changed_id;
48 } EphyReaderRequest;
49
50 static EphyReaderRequest *
ephy_reader_request_new(EphyReaderHandler * handler,WebKitURISchemeRequest * request)51 ephy_reader_request_new (EphyReaderHandler *handler,
52 WebKitURISchemeRequest *request)
53 {
54 EphyReaderRequest *reader_request;
55
56 reader_request = g_new (EphyReaderRequest, 1);
57 reader_request->source_handler = g_object_ref (handler);
58 reader_request->scheme_request = g_object_ref (request);
59 reader_request->web_view = NULL; /* created only if required */
60 reader_request->cancellable = g_cancellable_new ();
61 reader_request->load_changed_id = 0;
62
63 return reader_request;
64 }
65
66 static void
ephy_reader_request_free(EphyReaderRequest * request)67 ephy_reader_request_free (EphyReaderRequest *request)
68 {
69 if (request->load_changed_id > 0)
70 g_signal_handler_disconnect (request->web_view, request->load_changed_id);
71
72 g_object_unref (request->source_handler);
73 g_object_unref (request->scheme_request);
74 g_clear_object (&request->web_view);
75
76 g_cancellable_cancel (request->cancellable);
77 g_object_unref (request->cancellable);
78
79 g_free (request);
80 }
81
82 static void
finish_uri_scheme_request(EphyReaderRequest * request,gchar * data,GError * error)83 finish_uri_scheme_request (EphyReaderRequest *request,
84 gchar *data,
85 GError *error)
86 {
87 GInputStream *stream;
88 gssize data_length;
89
90 g_assert ((data && !error) || (!data && error));
91
92 if (error) {
93 webkit_uri_scheme_request_finish_error (request->scheme_request, error);
94 } else {
95 data_length = MIN (strlen (data), G_MAXSSIZE);
96 stream = g_memory_input_stream_new_from_data (data, data_length, g_free);
97 webkit_uri_scheme_request_finish (request->scheme_request, stream, data_length, "text/html");
98 g_object_unref (stream);
99 }
100
101 request->source_handler->outstanding_requests =
102 g_list_remove (request->source_handler->outstanding_requests,
103 request);
104
105 ephy_reader_request_free (request);
106 }
107
108 static const char *
enum_nick(GType enum_type,int value)109 enum_nick (GType enum_type,
110 int value)
111 {
112 GEnumClass *enum_class;
113 const GEnumValue *enum_value;
114 const char *nick = NULL;
115
116 enum_class = g_type_class_ref (enum_type);
117 enum_value = g_enum_get_value (enum_class, value);
118 if (enum_value)
119 nick = enum_value->value_nick;
120
121 g_type_class_unref (enum_class);
122 return nick;
123 }
124
125 static
126 char *
readability_get_property_string(WebKitJavascriptResult * js_result,char * property)127 readability_get_property_string (WebKitJavascriptResult *js_result,
128 char *property)
129 {
130 JSCValue *jsc_value;
131 char *result = NULL;
132
133 jsc_value = webkit_javascript_result_get_js_value (js_result);
134
135 if (!jsc_value_is_object (jsc_value))
136 return NULL;
137
138 if (jsc_value_object_has_property (jsc_value, property)) {
139 g_autoptr (JSCValue) jsc_content = jsc_value_object_get_property (jsc_value, property);
140
141 result = jsc_value_to_string (jsc_content);
142
143 if (result && strcmp (result, "null") == 0)
144 g_clear_pointer (&result, g_free);
145 }
146
147 return result;
148 }
149
150 static void
readability_js_finish_cb(GObject * object,GAsyncResult * result,gpointer user_data)151 readability_js_finish_cb (GObject *object,
152 GAsyncResult *result,
153 gpointer user_data)
154 {
155 WebKitWebView *web_view = WEBKIT_WEB_VIEW (object);
156 EphyReaderRequest *request = user_data;
157 g_autoptr (WebKitJavascriptResult) js_result = NULL;
158 g_autoptr (GError) error = NULL;
159 g_autofree gchar *byline = NULL;
160 g_autofree gchar *content = NULL;
161 g_autoptr (GString) html = NULL;
162 g_autoptr (GBytes) style_css = NULL;
163 const gchar *title;
164 const gchar *font_style;
165 const gchar *color_scheme;
166
167 js_result = webkit_web_view_run_javascript_finish (web_view, result, &error);
168 if (!js_result) {
169 if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
170 g_warning ("Error running javascript: %s", error->message);
171 g_error_free (error);
172 return;
173 }
174
175 byline = readability_get_property_string (js_result, "byline");
176 content = readability_get_property_string (js_result, "content");
177
178 html = g_string_new ("");
179 style_css = g_resources_lookup_data ("/org/gnome/epiphany/readability/reader.css", G_RESOURCE_LOOKUP_FLAGS_NONE, NULL);
180 title = webkit_web_view_get_title (web_view);
181 font_style = enum_nick (EPHY_TYPE_PREFS_READER_FONT_STYLE,
182 g_settings_get_enum (EPHY_SETTINGS_READER,
183 EPHY_PREFS_READER_FONT_STYLE));
184 color_scheme = enum_nick (EPHY_TYPE_PREFS_READER_COLOR_SCHEME,
185 g_settings_get_enum (EPHY_SETTINGS_READER,
186 EPHY_PREFS_READER_COLOR_SCHEME));
187
188 g_string_append_printf (html, "<style>%s</style>"
189 "<title>%s</title>"
190 "<meta http-equiv=\"Content-Type\" content=\"text/html;\" charset=\"UTF-8\">" \
191 "<body class='%s %s'>"
192 "<article>"
193 "<h2>"
194 "%s"
195 "</h2>"
196 "<i>"
197 "%s"
198 "</i>"
199 "<hr>",
200 (gchar *)g_bytes_get_data (style_css, NULL),
201 title,
202 font_style,
203 color_scheme,
204 title,
205 byline != NULL ? byline : "");
206 g_string_append (html, content);
207 g_string_append (html, "</article>");
208
209 finish_uri_scheme_request (request, g_strdup (html->str), NULL);
210 }
211
212 static void
ephy_reader_request_begin_get_source_from_web_view(EphyReaderRequest * request,WebKitWebView * web_view)213 ephy_reader_request_begin_get_source_from_web_view (EphyReaderRequest *request,
214 WebKitWebView *web_view)
215 {
216 webkit_web_view_run_javascript_from_gresource (web_view,
217 "/org/gnome/epiphany/readability/Readability.js",
218 request->cancellable,
219 readability_js_finish_cb,
220 request);
221 }
222
223 static void
load_changed_cb(WebKitWebView * web_view,WebKitLoadEvent load_event,EphyReaderRequest * request)224 load_changed_cb (WebKitWebView *web_view,
225 WebKitLoadEvent load_event,
226 EphyReaderRequest *request)
227 {
228 if (load_event == WEBKIT_LOAD_FINISHED) {
229 g_signal_handler_disconnect (request->web_view, request->load_changed_id);
230 request->load_changed_id = 0;
231
232 ephy_reader_request_begin_get_source_from_web_view (request, web_view);
233 }
234 }
235
236 static void
ephy_reader_request_begin_get_source_from_uri(EphyReaderRequest * request,const char * uri)237 ephy_reader_request_begin_get_source_from_uri (EphyReaderRequest *request,
238 const char *uri)
239 {
240 EphyEmbedShell *shell = ephy_embed_shell_get_default ();
241 WebKitWebContext *context = ephy_embed_shell_get_web_context (shell);
242
243 g_assert (!request->web_view);
244 request->web_view = WEBKIT_WEB_VIEW (g_object_ref_sink (webkit_web_view_new_with_context (context)));
245
246 g_assert (request->load_changed_id == 0);
247 request->load_changed_id = g_signal_connect (request->web_view, "load-changed",
248 G_CALLBACK (load_changed_cb),
249 request);
250
251 webkit_web_view_load_uri (request->web_view, uri);
252 }
253
254 static void
ephy_reader_request_start(EphyReaderRequest * request)255 ephy_reader_request_start (EphyReaderRequest *request)
256 {
257 g_autoptr (GUri) uri = NULL;
258 const char *original_uri;
259 WebKitWebView *web_view;
260
261 original_uri = webkit_uri_scheme_request_get_uri (request->scheme_request);
262 uri = g_uri_parse (original_uri, G_URI_FLAGS_NONE, NULL);
263
264 if (!uri) {
265 /* Can't assert because user could theoretically input something weird */
266 GError *error = g_error_new (WEBKIT_NETWORK_ERROR,
267 WEBKIT_NETWORK_ERROR_FAILED,
268 _("%s is not a valid URI"),
269 original_uri);
270 finish_uri_scheme_request (request, NULL, error);
271 return;
272 }
273
274 web_view = webkit_uri_scheme_request_get_web_view (request->scheme_request);
275 if (web_view) {
276 gboolean entering_reader_mode;
277
278 g_object_get (G_OBJECT (web_view), "entering-reader-mode", &entering_reader_mode, NULL);
279 if (!entering_reader_mode)
280 web_view = NULL;
281 }
282
283 if (web_view) {
284 ephy_reader_request_begin_get_source_from_web_view (request, web_view);
285 } else {
286 /* Extract URI:
287 * ephy-reader:https://example.com/whatever?xyz into https://example.com/whatever?xyz
288 */
289 g_assert (g_str_has_prefix (original_uri, "ephy-reader:"));
290 ephy_reader_request_begin_get_source_from_uri (request, original_uri + strlen ("ephy-reader:"));
291 }
292
293 request->source_handler->outstanding_requests =
294 g_list_prepend (request->source_handler->outstanding_requests, request);
295 }
296
297 static void
cancel_outstanding_request(EphyReaderRequest * request)298 cancel_outstanding_request (EphyReaderRequest *request)
299 {
300 g_cancellable_cancel (request->cancellable);
301 }
302
303 static void
ephy_reader_handler_dispose(GObject * object)304 ephy_reader_handler_dispose (GObject *object)
305 {
306 EphyReaderHandler *handler = EPHY_READER_HANDLER (object);
307
308 if (handler->outstanding_requests) {
309 g_list_foreach (handler->outstanding_requests, (GFunc)cancel_outstanding_request, NULL);
310 g_list_free (handler->outstanding_requests);
311 handler->outstanding_requests = NULL;
312 }
313
314 G_OBJECT_CLASS (ephy_reader_handler_parent_class)->dispose (object);
315 }
316
317 static void
ephy_reader_handler_init(EphyReaderHandler * handler)318 ephy_reader_handler_init (EphyReaderHandler *handler)
319 {
320 }
321
322 static void
ephy_reader_handler_class_init(EphyReaderHandlerClass * klass)323 ephy_reader_handler_class_init (EphyReaderHandlerClass *klass)
324 {
325 GObjectClass *object_class = G_OBJECT_CLASS (klass);
326
327 object_class->dispose = ephy_reader_handler_dispose;
328 }
329
330 EphyReaderHandler *
ephy_reader_handler_new(void)331 ephy_reader_handler_new (void)
332 {
333 return EPHY_READER_HANDLER (g_object_new (EPHY_TYPE_READER_HANDLER, NULL));
334 }
335
336 void
ephy_reader_handler_handle_request(EphyReaderHandler * handler,WebKitURISchemeRequest * scheme_request)337 ephy_reader_handler_handle_request (EphyReaderHandler *handler,
338 WebKitURISchemeRequest *scheme_request)
339 {
340 EphyReaderRequest *reader_request;
341
342 reader_request = ephy_reader_request_new (handler, scheme_request);
343 ephy_reader_request_start (reader_request);
344 }
345