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