1 /*
2 * e-editor-web-extension.c
3 *
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2 of the License, or (at your option) version 3.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public
15 * License along with the program; if not, see <http://www.gnu.org/licenses/>
16 *
17 */
18
19 #include "evolution-config.h"
20
21 #include <webkit2/webkit-web-extension.h>
22
23 #include <libedataserver/libedataserver.h>
24
25 #define E_UTIL_INCLUDE_WITHOUT_WEBKIT 1
26 #include "e-util/e-util.h"
27 #undef E_UTIL_INCLUDE_WITHOUT_WEBKIT
28
29 #include "e-editor-web-extension.h"
30
31 struct _EEditorWebExtensionPrivate {
32 WebKitWebExtension *wk_extension;
33 ESpellChecker *spell_checker;
34 };
35
G_DEFINE_TYPE_WITH_PRIVATE(EEditorWebExtension,e_editor_web_extension,G_TYPE_OBJECT)36 G_DEFINE_TYPE_WITH_PRIVATE (EEditorWebExtension, e_editor_web_extension, G_TYPE_OBJECT)
37
38 static void
39 e_editor_web_extension_dispose (GObject *object)
40 {
41 EEditorWebExtension *extension = E_EDITOR_WEB_EXTENSION (object);
42
43 g_clear_object (&extension->priv->wk_extension);
44 g_clear_object (&extension->priv->spell_checker);
45
46 /* Chain up to parent's dispose() method. */
47 G_OBJECT_CLASS (e_editor_web_extension_parent_class)->dispose (object);
48 }
49
50 static void
e_editor_web_extension_class_init(EEditorWebExtensionClass * class)51 e_editor_web_extension_class_init (EEditorWebExtensionClass *class)
52 {
53 GObjectClass *object_class = G_OBJECT_CLASS (class);
54
55 object_class->dispose = e_editor_web_extension_dispose;
56 }
57
58 static void
e_editor_web_extension_init(EEditorWebExtension * extension)59 e_editor_web_extension_init (EEditorWebExtension *extension)
60 {
61 extension->priv = e_editor_web_extension_get_instance_private (extension);
62 extension->priv->spell_checker = NULL;
63 }
64
65 static gpointer
e_editor_web_extension_create_instance(gpointer data)66 e_editor_web_extension_create_instance (gpointer data)
67 {
68 return g_object_new (E_TYPE_EDITOR_WEB_EXTENSION, NULL);
69 }
70
71 EEditorWebExtension *
e_editor_web_extension_get_default(void)72 e_editor_web_extension_get_default (void)
73 {
74 static GOnce once_init = G_ONCE_INIT;
75 return E_EDITOR_WEB_EXTENSION (g_once (&once_init, e_editor_web_extension_create_instance, NULL));
76 }
77
78 static gboolean
use_sources_js_file(void)79 use_sources_js_file (void)
80 {
81 static gint res = -1;
82
83 if (res == -1)
84 res = g_strcmp0 (g_getenv ("E_HTML_EDITOR_TEST_SOURCES"), "1") == 0 ? 1 : 0;
85
86 return res;
87 }
88
89 static void
load_javascript_file(JSCContext * jsc_context,const gchar * js_filename)90 load_javascript_file (JSCContext *jsc_context,
91 const gchar *js_filename)
92 {
93 JSCValue *result;
94 JSCException *exception;
95 gchar *content, *filename = NULL, *resource_uri;
96 gsize length = 0;
97 GError *error = NULL;
98
99 g_return_if_fail (jsc_context != NULL);
100
101 if (use_sources_js_file ()) {
102 const gchar *source_webkitdatadir;
103
104 source_webkitdatadir = g_getenv ("EVOLUTION_SOURCE_WEBKITDATADIR");
105
106 if (source_webkitdatadir && *source_webkitdatadir) {
107 filename = g_build_filename (source_webkitdatadir, js_filename, NULL);
108
109 if (!g_file_test (filename, G_FILE_TEST_EXISTS)) {
110 g_warning ("Cannot find '%s', using installed file '%s/%s' instead", filename, EVOLUTION_WEBKITDATADIR, js_filename);
111
112 g_clear_pointer (&filename, g_free);
113 }
114 } else {
115 g_warning ("Environment variable 'EVOLUTION_SOURCE_WEBKITDATADIR' not set or invalid value, using installed file '%s/%s' instead", EVOLUTION_WEBKITDATADIR, js_filename);
116 }
117 }
118
119 if (!filename)
120 filename = g_build_filename (EVOLUTION_WEBKITDATADIR, js_filename, NULL);
121
122 if (!g_file_get_contents (filename, &content, &length, &error)) {
123 g_warning ("Failed to load '%s': %s", filename, error ? error->message : "Unknown error");
124
125 g_clear_error (&error);
126 g_free (filename);
127
128 return;
129 }
130
131 resource_uri = g_strconcat ("resource:///", js_filename, NULL);
132
133 result = jsc_context_evaluate_with_source_uri (jsc_context, content, length, resource_uri, 1);
134
135 g_free (resource_uri);
136
137 exception = jsc_context_get_exception (jsc_context);
138
139 if (exception) {
140 g_warning ("Failed to call script '%s': %d:%d: %s",
141 filename,
142 jsc_exception_get_line_number (exception),
143 jsc_exception_get_column_number (exception),
144 jsc_exception_get_message (exception));
145
146 jsc_context_clear_exception (jsc_context);
147 }
148
149 g_clear_object (&result);
150 g_free (filename);
151 g_free (content);
152 }
153
154 static void
evo_editor_find_pattern(const gchar * text,const gchar * pattern,gint * out_start,gint * out_end)155 evo_editor_find_pattern (const gchar *text,
156 const gchar *pattern,
157 gint *out_start,
158 gint *out_end)
159 {
160 GRegex *regex;
161
162 g_return_if_fail (out_start != NULL);
163 g_return_if_fail (out_end != NULL);
164
165 *out_start = -1;
166 *out_end = -1;
167
168 regex = g_regex_new (pattern, 0, 0, NULL);
169 if (regex) {
170 GMatchInfo *match_info = NULL;
171 gint start = -1, end = -1;
172
173 if (g_regex_match_all (regex, text, G_REGEX_MATCH_NOTEMPTY, &match_info) &&
174 g_match_info_fetch_pos (match_info, 0, &start, &end) &&
175 start >= 0 && end >= 0) {
176 *out_start = start;
177 *out_end = end;
178 }
179
180 if (match_info)
181 g_match_info_free (match_info);
182 g_regex_unref (regex);
183 }
184 }
185
186 /* Returns 'null', when no match for magicLinks in 'text' were found, otherwise
187 returns an array of 'object { text : string, [ href : string] };' with the text
188 split into parts, where those with also 'href' property defined are meant
189 to be anchors. */
190 static JSCValue *
evo_editor_jsc_split_text_with_links(const gchar * text,JSCContext * jsc_context)191 evo_editor_jsc_split_text_with_links (const gchar *text,
192 JSCContext *jsc_context)
193 {
194 /* stephenhay from https://mathiasbynens.be/demo/url-regex */
195 const gchar *URL_PATTERN = "((?:(?:(?:"
196 "news|telnet|nntp|file|https?|s?ftp|webcal|localhost|ssh"
197 ")\\:\\/\\/)|(?:www\\.|ftp\\.))[^\\s\\/\\$\\.\\?#].[^\\s]*+)";
198 /* from camel-url-scanner.c */
199 const gchar *URL_INVALID_TRAILING_CHARS = ",.:;?!-|}])\">";
200 /* http://www.w3.org/TR/html5/forms.html#valid-e-mail-address */
201 const gchar *EMAIL_PATTERN = "[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}"
202 "[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*+";
203 JSCValue *array = NULL;
204 guint array_len = 0;
205 gboolean done = FALSE;
206
207 if (!text || !*text)
208 return jsc_value_new_null (jsc_context);
209
210 #define add_to_array(_obj) G_STMT_START { \
211 if (!array) \
212 array = jsc_value_new_array (jsc_context, G_TYPE_NONE); \
213 jsc_value_object_set_property_at_index (array, array_len, _obj); \
214 array_len++; \
215 } G_STMT_END
216
217 while (!done) {
218 gboolean is_email;
219 gint start = -1, end = -1;
220
221 done = TRUE;
222
223 is_email = strchr (text, '@') && !strstr (text, "://");
224
225 evo_editor_find_pattern (text, is_email ? EMAIL_PATTERN : URL_PATTERN, &start, &end);
226
227 if (start >= 0 && end >= 0) {
228 const gchar *url_end, *ptr;
229
230 url_end = text + end - 1;
231
232 /* Stop on the angle brackets, which cannot be part of the URL (see RFC 3986 Appendix C) */
233 for (ptr = text + start; ptr <= url_end; ptr++) {
234 if (*ptr == '<' || *ptr == '>') {
235 end = ptr - text;
236 url_end = text + end - 1;
237 break;
238 }
239 }
240
241 /* URLs are extremely unlikely to end with any punctuation, so
242 * strip any trailing punctuation off from link and put it after
243 * the link. Do the same for any closing double-quotes as well. */
244 while (end > start && *url_end && strchr (URL_INVALID_TRAILING_CHARS, *url_end)) {
245 gchar open_bracket = 0, close_bracket = *url_end;
246
247 if (close_bracket == ')')
248 open_bracket = '(';
249 else if (close_bracket == '}')
250 open_bracket = '{';
251 else if (close_bracket == ']')
252 open_bracket = '[';
253 else if (close_bracket == '>')
254 open_bracket = '<';
255
256 if (open_bracket != 0) {
257 gint n_opened = 0, n_closed = 0;
258
259 for (ptr = text + start; ptr <= url_end; ptr++) {
260 if (*ptr == open_bracket)
261 n_opened++;
262 else if (*ptr == close_bracket)
263 n_closed++;
264 }
265
266 /* The closing bracket can match one inside the URL,
267 thus keep it there. */
268 if (n_opened > 0 && n_opened - n_closed >= 0)
269 break;
270 }
271
272 url_end--;
273 end--;
274 }
275
276 if (end > start) {
277 JSCValue *object, *string;
278 gchar *url, *tmp;
279
280 if (start > 0) {
281 tmp = g_strndup (text, start);
282
283 object = jsc_value_new_object (jsc_context, NULL, NULL);
284
285 string = jsc_value_new_string (jsc_context, tmp);
286 jsc_value_object_set_property (object, "text", string);
287 g_clear_object (&string);
288
289 add_to_array (object);
290
291 g_clear_object (&object);
292 g_free (tmp);
293 }
294
295 tmp = g_strndup (text + start, end - start);
296
297 if (is_email)
298 url = g_strconcat ("mailto:", tmp, NULL);
299 else if (g_str_has_prefix (tmp, "www."))
300 url = g_strconcat ("https://", tmp, NULL);
301 else
302 url = NULL;
303
304 object = jsc_value_new_object (jsc_context, NULL, NULL);
305
306 string = jsc_value_new_string (jsc_context, tmp);
307 jsc_value_object_set_property (object, "text", string);
308 g_clear_object (&string);
309
310 string = jsc_value_new_string (jsc_context, url ? url : tmp);
311 jsc_value_object_set_property (object, "href", string);
312 g_clear_object (&string);
313
314 add_to_array (object);
315
316 g_clear_object (&object);
317 g_free (tmp);
318 g_free (url);
319
320 text = text + end;
321 done = FALSE;
322 }
323 }
324 }
325
326 if (array && *text) {
327 JSCValue *object, *string;
328
329 object = jsc_value_new_object (jsc_context, NULL, NULL);
330
331 string = jsc_value_new_string (jsc_context, text);
332 jsc_value_object_set_property (object, "text", string);
333 g_clear_object (&string);
334
335 add_to_array (object);
336
337 g_clear_object (&object);
338 }
339
340 #undef add_to_array
341
342 return array ? array : jsc_value_new_null (jsc_context);
343 }
344
345 /* Returns 'null' or an object { text : string, imageUri : string, width : nnn, height : nnn }
346 where only the 'text' is required, describing an emoticon. */
347 static JSCValue *
evo_editor_jsc_lookup_emoticon(const gchar * iconName,gboolean use_unicode_smileys,JSCContext * jsc_context)348 evo_editor_jsc_lookup_emoticon (const gchar *iconName,
349 gboolean use_unicode_smileys,
350 JSCContext *jsc_context)
351 {
352 JSCValue *object = NULL;
353
354 if (iconName && *iconName) {
355 const EEmoticon *emoticon;
356
357 emoticon = e_emoticon_chooser_lookup_emoticon (iconName);
358
359 if (emoticon) {
360 JSCValue *value;
361
362 object = jsc_value_new_object (jsc_context, NULL, NULL);
363
364 if (use_unicode_smileys) {
365 value = jsc_value_new_string (jsc_context, emoticon->unicode_character);
366 jsc_value_object_set_property (object, "text", value);
367 g_clear_object (&value);
368 } else {
369 gchar *image_uri;
370
371 value = jsc_value_new_string (jsc_context, emoticon->text_face);
372 jsc_value_object_set_property (object, "text", value);
373 g_clear_object (&value);
374
375 image_uri = e_emoticon_get_uri ((EEmoticon *) emoticon);
376
377 if (image_uri) {
378 value = jsc_value_new_string (jsc_context, image_uri);
379 jsc_value_object_set_property (object, "imageUri", value);
380 g_clear_object (&value);
381
382 value = jsc_value_new_number (jsc_context, 16);
383 jsc_value_object_set_property (object, "width", value);
384 g_clear_object (&value);
385
386 value = jsc_value_new_number (jsc_context, 16);
387 jsc_value_object_set_property (object, "height", value);
388 g_clear_object (&value);
389
390 g_free (image_uri);
391 }
392 }
393 }
394 }
395
396 return object ? object : jsc_value_new_null (jsc_context);
397 }
398
399 static void
evo_editor_jsc_set_spell_check_languages(const gchar * langs,GWeakRef * wkrf_extension)400 evo_editor_jsc_set_spell_check_languages (const gchar *langs,
401 GWeakRef *wkrf_extension)
402 {
403 EEditorWebExtension *extension;
404 gchar **strv;
405
406 g_return_if_fail (wkrf_extension != NULL);
407
408 extension = g_weak_ref_get (wkrf_extension);
409
410 if (!extension)
411 return;
412
413 if (langs && *langs)
414 strv = g_strsplit (langs, "|", -1);
415 else
416 strv = NULL;
417
418 if (!extension->priv->spell_checker)
419 extension->priv->spell_checker = e_spell_checker_new ();
420
421 e_spell_checker_set_active_languages (extension->priv->spell_checker, (const gchar * const *) strv);
422
423 g_object_unref (extension);
424 g_strfreev (strv);
425 }
426
427 /* Returns whether the 'word' is a properly spelled word. It checks
428 with languages previously set by EvoEditor.SetSpellCheckLanguages(). */
429 static gboolean
evo_editor_jsc_spell_check_word(const gchar * word,GWeakRef * wkrf_extension)430 evo_editor_jsc_spell_check_word (const gchar *word,
431 GWeakRef *wkrf_extension)
432 {
433 EEditorWebExtension *extension;
434 gboolean is_correct;
435
436 g_return_val_if_fail (wkrf_extension != NULL, FALSE);
437
438 extension = g_weak_ref_get (wkrf_extension);
439
440 if (!extension)
441 return TRUE;
442
443 /* It should be created as part of EvoEditor.SetSpellCheckLanguages(). */
444 g_warn_if_fail (extension->priv->spell_checker != NULL);
445
446 if (!extension->priv->spell_checker)
447 extension->priv->spell_checker = e_spell_checker_new ();
448
449 is_correct = e_spell_checker_check_word (extension->priv->spell_checker, word, -1);
450
451 g_object_unref (extension);
452
453 return is_correct;
454 }
455
456 static void
window_object_cleared_cb(WebKitScriptWorld * world,WebKitWebPage * page,WebKitFrame * frame,gpointer user_data)457 window_object_cleared_cb (WebKitScriptWorld *world,
458 WebKitWebPage *page,
459 WebKitFrame *frame,
460 gpointer user_data)
461 {
462 EEditorWebExtension *extension = user_data;
463 JSCContext *jsc_context;
464 JSCValue *jsc_editor;
465
466 g_return_if_fail (E_IS_EDITOR_WEB_EXTENSION (extension));
467
468 /* Load the javascript files only to the main frame, not to the subframes */
469 if (!webkit_frame_is_main_frame (frame))
470 return;
471
472 jsc_context = webkit_frame_get_js_context (frame);
473
474 /* Read in order approximately as each other uses the previous */
475 load_javascript_file (jsc_context, "e-convert.js");
476 load_javascript_file (jsc_context, "e-selection.js");
477 load_javascript_file (jsc_context, "e-undo-redo.js");
478 load_javascript_file (jsc_context, "e-editor.js");
479
480 jsc_editor = jsc_context_get_value (jsc_context, "EvoEditor");
481
482 if (jsc_editor) {
483 JSCValue *jsc_function;
484 const gchar *func_name;
485
486 /* EvoEditor.splitTextWithLinks(text) */
487 func_name = "splitTextWithLinks";
488 jsc_function = jsc_value_new_function (jsc_context, func_name,
489 G_CALLBACK (evo_editor_jsc_split_text_with_links), g_object_ref (jsc_context), g_object_unref,
490 JSC_TYPE_VALUE, 1, G_TYPE_STRING);
491
492 jsc_value_object_set_property (jsc_editor, func_name, jsc_function);
493
494 g_clear_object (&jsc_function);
495
496 /* EvoEditor.lookupEmoticon(iconName, useUnicodeSmileys) */
497 func_name = "lookupEmoticon";
498 jsc_function = jsc_value_new_function (jsc_context, func_name,
499 G_CALLBACK (evo_editor_jsc_lookup_emoticon), g_object_ref (jsc_context), g_object_unref,
500 JSC_TYPE_VALUE, 2, G_TYPE_STRING, G_TYPE_BOOLEAN);
501
502 jsc_value_object_set_property (jsc_editor, func_name, jsc_function);
503
504 g_clear_object (&jsc_function);
505
506 /* EvoEditor.SetSpellCheckLanguages(langs) */
507 func_name = "SetSpellCheckLanguages";
508 jsc_function = jsc_value_new_function (jsc_context, func_name,
509 G_CALLBACK (evo_editor_jsc_set_spell_check_languages), e_weak_ref_new (extension), (GDestroyNotify) e_weak_ref_free,
510 G_TYPE_NONE, 1, G_TYPE_STRING);
511
512 jsc_value_object_set_property (jsc_editor, func_name, jsc_function);
513
514 g_clear_object (&jsc_function);
515
516 /* EvoEditor.SpellCheckWord(word) */
517 func_name = "SpellCheckWord";
518 jsc_function = jsc_value_new_function (jsc_context, func_name,
519 G_CALLBACK (evo_editor_jsc_spell_check_word), e_weak_ref_new (extension), (GDestroyNotify) e_weak_ref_free,
520 G_TYPE_BOOLEAN, 1, G_TYPE_STRING);
521
522 jsc_value_object_set_property (jsc_editor, func_name, jsc_function);
523
524 g_clear_object (&jsc_function);
525 g_clear_object (&jsc_editor);
526 }
527
528 g_clear_object (&jsc_context);
529 }
530
531 static gboolean
web_page_send_request_cb(WebKitWebPage * web_page,WebKitURIRequest * request,WebKitURIResponse * redirected_response,EEditorWebExtension * extension)532 web_page_send_request_cb (WebKitWebPage *web_page,
533 WebKitURIRequest *request,
534 WebKitURIResponse *redirected_response,
535 EEditorWebExtension *extension)
536 {
537 const gchar *request_uri;
538 const gchar *page_uri;
539
540 request_uri = webkit_uri_request_get_uri (request);
541 page_uri = webkit_web_page_get_uri (web_page);
542
543 /* Always load the main resource. */
544 if (g_strcmp0 (request_uri, page_uri) == 0)
545 return FALSE;
546
547 if (g_str_has_prefix (request_uri, "http:") ||
548 g_str_has_prefix (request_uri, "https:")) {
549 gchar *new_uri;
550
551 new_uri = g_strconcat ("evo-", request_uri, NULL);
552
553 webkit_uri_request_set_uri (request, new_uri);
554
555 g_free (new_uri);
556 }
557
558 return FALSE;
559 }
560
561 static void
web_page_document_loaded_cb(WebKitWebPage * web_page,gpointer user_data)562 web_page_document_loaded_cb (WebKitWebPage *web_page,
563 gpointer user_data)
564 {
565 g_return_if_fail (WEBKIT_IS_WEB_PAGE (web_page));
566
567 window_object_cleared_cb (NULL, web_page, webkit_web_page_get_main_frame (web_page), user_data);
568 }
569
570 static void
web_page_created_cb(WebKitWebExtension * wk_extension,WebKitWebPage * web_page,EEditorWebExtension * extension)571 web_page_created_cb (WebKitWebExtension *wk_extension,
572 WebKitWebPage *web_page,
573 EEditorWebExtension *extension)
574 {
575 g_return_if_fail (WEBKIT_IS_WEB_PAGE (web_page));
576 g_return_if_fail (E_IS_EDITOR_WEB_EXTENSION (extension));
577
578 window_object_cleared_cb (NULL, web_page, webkit_web_page_get_main_frame (web_page), extension);
579
580 g_signal_connect (
581 web_page, "send-request",
582 G_CALLBACK (web_page_send_request_cb), extension);
583
584 g_signal_connect (
585 web_page, "document-loaded",
586 G_CALLBACK (web_page_document_loaded_cb), extension);
587 }
588
589 void
e_editor_web_extension_initialize(EEditorWebExtension * extension,WebKitWebExtension * wk_extension)590 e_editor_web_extension_initialize (EEditorWebExtension *extension,
591 WebKitWebExtension *wk_extension)
592 {
593 WebKitScriptWorld *script_world;
594
595 g_return_if_fail (E_IS_EDITOR_WEB_EXTENSION (extension));
596
597 extension->priv->wk_extension = g_object_ref (wk_extension);
598
599 g_signal_connect (
600 wk_extension, "page-created",
601 G_CALLBACK (web_page_created_cb), extension);
602
603 script_world = webkit_script_world_get_default ();
604
605 g_signal_connect (script_world, "window-object-cleared",
606 G_CALLBACK (window_object_cleared_cb), extension);
607 }
608