1 /* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /*
3 * Copyright © 2019-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 /**
22 * - Load a web_extension as described at https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/
23 * - Prepare the internal structure so that they can be easily applied to its destination (webview/browser) with the help of extension manager.
24 */
25
26 #include "config.h"
27
28 #include "ephy-embed-shell.h"
29 #include "ephy-file-helpers.h"
30 #include "ephy-shell.h"
31 #include "ephy-string.h"
32 #include "ephy-web-extension.h"
33 #include "ephy-window.h"
34
35 #include <archive.h>
36 #include <archive_entry.h>
37 #include <glib/gstdio.h>
38 #include <json-glib/json-glib.h>
39
40 typedef struct {
41 gint64 size;
42 char *file;
43 GdkPixbuf *pixbuf;
44 } WebExtensionIcon;
45
46 typedef struct {
47 GPtrArray *allow_list;
48 GPtrArray *block_list;
49 GPtrArray *js;
50
51 WebKitUserContentInjectedFrames injected_frames;
52 WebKitUserScriptInjectionTime injection_time;
53 GList *user_scripts;
54 } WebExtensionContentScript;
55
56 typedef struct {
57 GList *default_icons;
58 GtkWidget *widget;
59 } WebExtensionPageAction;
60
61 typedef struct {
62 char *title;
63 GList *default_icons;
64 char *popup;
65 } WebExtensionBrowserAction;
66
67 typedef struct {
68 GPtrArray *scripts;
69 char *page;
70 } WebExtensionBackground;
71
72 typedef struct {
73 char *page;
74 } WebExtensionOptionsUI;
75
76 typedef struct {
77 char *name;
78 GBytes *bytes;
79 } WebExtensionResource;
80
81 typedef struct {
82 char *code;
83 WebKitUserStyleSheet *style;
84 } WebExtensionCustomCSS;
85
86 struct _EphyWebExtension {
87 GObject parent_instance;
88
89 gboolean xpi;
90 char *base_location;
91 char *manifest;
92
93 char *description;
94 gint64 manifest_version;
95 char *guid;
96 char *author;
97 char *name;
98 char *version;
99 char *homepage_url;
100 GList *icons;
101 GList *content_scripts;
102 WebExtensionBackground *background;
103 GHashTable *page_action_map;
104 WebExtensionPageAction *page_action;
105 WebExtensionBrowserAction *browser_action;
106 WebExtensionOptionsUI *options_ui;
107 GList *resources;
108 GList *custom_css;
109 GPtrArray *permissions;
110 GCancellable *cancellable;
111 };
112
G_DEFINE_TYPE(EphyWebExtension,ephy_web_extension,G_TYPE_OBJECT)113 G_DEFINE_TYPE (EphyWebExtension, ephy_web_extension, G_TYPE_OBJECT)
114
115 gboolean
116 ephy_web_extension_has_resource (EphyWebExtension *self,
117 const char *name)
118 {
119 for (GList *list = self->resources; list && list->data; list = list->next) {
120 WebExtensionResource *resource = list->data;
121
122 if (g_strcmp0 (resource->name, name) == 0)
123 return TRUE;
124 }
125
126 return FALSE;
127 }
128
129 gconstpointer
ephy_web_extension_get_resource(EphyWebExtension * self,const char * name,gsize * length)130 ephy_web_extension_get_resource (EphyWebExtension *self,
131 const char *name,
132 gsize *length)
133 {
134 if (length)
135 *length = 0;
136
137 for (GList *list = self->resources; list && list->data; list = list->next) {
138 WebExtensionResource *resource = list->data;
139
140 if (g_strcmp0 (resource->name, name) == 0)
141 return g_bytes_get_data (resource->bytes, length);
142 }
143
144 g_debug ("Could not find web_extension resource: %s\n", name);
145 return NULL;
146 }
147
148 char *
ephy_web_extension_get_resource_as_string(EphyWebExtension * self,const char * name)149 ephy_web_extension_get_resource_as_string (EphyWebExtension *self,
150 const char *name)
151 {
152 gsize len;
153 gconstpointer data = ephy_web_extension_get_resource (self, name, &len);
154 g_autofree char *out = NULL;
155
156 if (data && len) {
157 out = g_malloc0 (len + 1);
158 memcpy (out, data, len);
159 }
160
161 return g_steal_pointer (&out);
162 }
163
164 static WebExtensionIcon *
web_extension_icon_new(EphyWebExtension * self,const char * file,gint64 size)165 web_extension_icon_new (EphyWebExtension *self,
166 const char *file,
167 gint64 size)
168 {
169 WebExtensionIcon *icon = NULL;
170 g_autoptr (GInputStream) stream = NULL;
171 g_autoptr (GError) error = NULL;
172 g_autoptr (GdkPixbuf) pixbuf = NULL;
173 const unsigned char *data = NULL;
174 gsize length;
175
176 data = ephy_web_extension_get_resource (self, file, &length);
177 if (!data) {
178 if (!self->xpi) {
179 g_autofree char *path = NULL;
180 path = g_build_filename (self->base_location, file, NULL);
181 pixbuf = gdk_pixbuf_new_from_file (path, NULL);
182 }
183 } else {
184 stream = g_memory_input_stream_new_from_data (data, length, NULL);
185 pixbuf = gdk_pixbuf_new_from_stream (stream, NULL, &error);
186 }
187
188 if (!pixbuf) {
189 g_warning ("Could not read web_extension icon: %s", error ? error->message : "");
190 return NULL;
191 }
192
193 icon = g_malloc0 (sizeof (WebExtensionIcon));
194 icon->file = g_strdup (file);
195 icon->size = size;
196 icon->pixbuf = g_steal_pointer (&pixbuf);
197
198 return icon;
199 }
200
201 static void
web_extension_icon_free(WebExtensionIcon * icon)202 web_extension_icon_free (WebExtensionIcon *icon)
203 {
204 g_clear_pointer (&icon->file, g_free);
205 g_clear_object (&icon->pixbuf);
206 g_free (icon);
207 }
208
209 static WebExtensionContentScript *
web_extension_content_script_new(WebKitUserContentInjectedFrames injected_frames,WebKitUserScriptInjectionTime injection_time)210 web_extension_content_script_new (WebKitUserContentInjectedFrames injected_frames,
211 WebKitUserScriptInjectionTime injection_time)
212 {
213 WebExtensionContentScript *content_script = g_malloc0 (sizeof (WebExtensionContentScript));
214
215 content_script->injected_frames = injected_frames;
216 content_script->injection_time = injection_time;
217 content_script->allow_list = g_ptr_array_new_full (1, g_free);
218 content_script->block_list = g_ptr_array_new_full (1, g_free);
219 content_script->js = g_ptr_array_new_full (1, g_free);
220
221 return content_script;
222 }
223
224 static void
web_extension_content_script_free(WebExtensionContentScript * content_script)225 web_extension_content_script_free (WebExtensionContentScript *content_script)
226 {
227 g_clear_pointer (&content_script->allow_list, g_ptr_array_unref);
228 g_clear_pointer (&content_script->block_list, g_ptr_array_unref);
229 g_clear_pointer (&content_script->js, g_ptr_array_unref);
230 g_clear_list (&content_script->user_scripts, (GDestroyNotify)webkit_user_script_unref);
231 g_free (content_script);
232 }
233
234 static WebExtensionOptionsUI *
web_extension_options_ui_new(const char * page)235 web_extension_options_ui_new (const char *page)
236 {
237 WebExtensionOptionsUI *options_ui = g_malloc0 (sizeof (WebExtensionOptionsUI));
238
239 options_ui->page = g_strdup (page);
240
241 return options_ui;
242 }
243
244 static void
web_extension_options_ui_free(WebExtensionOptionsUI * options_ui)245 web_extension_options_ui_free (WebExtensionOptionsUI *options_ui)
246 {
247 g_clear_pointer (&options_ui->page, g_free);
248 g_free (options_ui);
249 }
250
251 static WebExtensionBackground *
web_extension_background_new(void)252 web_extension_background_new (void)
253 {
254 WebExtensionBackground *background = g_malloc0 (sizeof (WebExtensionBackground));
255
256 background->scripts = g_ptr_array_new_full (1, g_free);
257
258 return background;
259 }
260
261 static void
web_extension_background_free(WebExtensionBackground * background)262 web_extension_background_free (WebExtensionBackground *background)
263 {
264 g_clear_pointer (&background->scripts, g_ptr_array_unref);
265 g_clear_pointer (&background->page, g_free);
266 g_free (background);
267 }
268
269 static void
web_extension_add_icon(JsonObject * object,const char * member_name,JsonNode * member_node,gpointer user_data)270 web_extension_add_icon (JsonObject *object,
271 const char *member_name,
272 JsonNode *member_node,
273 gpointer user_data)
274 {
275 EphyWebExtension *self = EPHY_WEB_EXTENSION (user_data);
276 WebExtensionIcon *icon;
277 const char *file = json_node_get_string (member_node);
278 gint64 size;
279
280 size = g_ascii_strtoll (member_name, NULL, 0);
281 if (size == 0) {
282 LOG ("Skipping %s as web extension icon as size is 0", file);
283 return;
284 }
285
286 icon = web_extension_icon_new (self, file, size);
287
288 if (icon)
289 self->icons = g_list_append (self->icons, icon);
290 }
291
292 static void
web_extension_add_browser_icons(JsonObject * object,const char * member_name,JsonNode * member_node,gpointer user_data)293 web_extension_add_browser_icons (JsonObject *object,
294 const char *member_name,
295 JsonNode *member_node,
296 gpointer user_data)
297 {
298 EphyWebExtension *self = EPHY_WEB_EXTENSION (user_data);
299 WebExtensionIcon *icon;
300 const char *file = json_node_get_string (member_node);
301 gint64 size;
302
303 size = g_ascii_strtoll (member_name, NULL, 0);
304 if (size == 0) {
305 LOG ("Skipping %s as web extension browser icon as size is 0", file);
306 return;
307 }
308 icon = web_extension_icon_new (self, file, size);
309
310 if (icon)
311 self->browser_action->default_icons = g_list_append (self->browser_action->default_icons, icon);
312 }
313
314 GdkPixbuf *
ephy_web_extension_get_icon(EphyWebExtension * self,gint64 size)315 ephy_web_extension_get_icon (EphyWebExtension *self,
316 gint64 size)
317 {
318 WebExtensionIcon *icon_fallback = NULL;
319
320 for (GList *list = self->icons; list && list->data; list = list->next) {
321 WebExtensionIcon *icon = list->data;
322
323 if (icon->size == size)
324 return gdk_pixbuf_scale_simple (icon->pixbuf, size, size, GDK_INTERP_BILINEAR);
325
326 if (!icon_fallback || icon->size > icon_fallback->size)
327 icon_fallback = icon;
328 }
329
330 /* Fallback */
331 if (icon_fallback && icon_fallback->pixbuf)
332 return gdk_pixbuf_scale_simple (icon_fallback->pixbuf, size, size, GDK_INTERP_BILINEAR);
333
334 return NULL;
335 }
336
337 const char *
ephy_web_extension_get_name(EphyWebExtension * self)338 ephy_web_extension_get_name (EphyWebExtension *self)
339 {
340 return self->name;
341 }
342
343 const char *
ephy_web_extension_get_version(EphyWebExtension * self)344 ephy_web_extension_get_version (EphyWebExtension *self)
345 {
346 return self->version;
347 }
348
349 const char *
ephy_web_extension_get_description(EphyWebExtension * self)350 ephy_web_extension_get_description (EphyWebExtension *self)
351 {
352 return self->description;
353 }
354
355 const char *
ephy_web_extension_get_homepage_url(EphyWebExtension * self)356 ephy_web_extension_get_homepage_url (EphyWebExtension *self)
357 {
358 return self->homepage_url;
359 }
360
361 const char *
ephy_web_extension_get_author(EphyWebExtension * self)362 ephy_web_extension_get_author (EphyWebExtension *self)
363 {
364 return self->author;
365 }
366
367 const char *
ephy_web_extension_get_manifest(EphyWebExtension * self)368 ephy_web_extension_get_manifest (EphyWebExtension *self)
369 {
370 return self->manifest;
371 }
372
373 const char *
ephy_web_extension_get_base_location(EphyWebExtension * self)374 ephy_web_extension_get_base_location (EphyWebExtension *self)
375 {
376 return self->base_location;
377 }
378
379 static void
web_extension_add_allow_list(JsonArray * array,guint index,JsonNode * element_node,gpointer user_data)380 web_extension_add_allow_list (JsonArray *array,
381 guint index,
382 JsonNode *element_node,
383 gpointer user_data)
384 {
385 WebExtensionContentScript *content_script = user_data;
386
387 g_ptr_array_add (content_script->allow_list, g_strdup (json_node_get_string (element_node)));
388 }
389
390 static void
web_extension_add_block_list(JsonArray * array,guint index,JsonNode * element_node,gpointer user_data)391 web_extension_add_block_list (JsonArray *array,
392 guint index,
393 JsonNode *element_node,
394 gpointer user_data)
395 {
396 WebExtensionContentScript *content_script = user_data;
397
398 g_ptr_array_add (content_script->block_list, g_strdup (json_node_get_string (element_node)));
399 }
400
401 static void
web_extension_add_js(JsonArray * array,guint index_,JsonNode * element_node,gpointer user_data)402 web_extension_add_js (JsonArray *array,
403 guint index_,
404 JsonNode *element_node,
405 gpointer user_data)
406 {
407 WebExtensionContentScript *content_script = user_data;
408
409 g_ptr_array_add (content_script->js, g_strdup (json_node_get_string (element_node)));
410 }
411
412 static void
web_extension_content_script_build(EphyWebExtension * self,WebExtensionContentScript * content_script)413 web_extension_content_script_build (EphyWebExtension *self,
414 WebExtensionContentScript *content_script)
415 {
416 if (!content_script->js)
417 return;
418
419 for (guint i = 0; i < content_script->js->len; i++) {
420 WebKitUserScript *user_script;
421 char *js_data;
422
423 js_data = ephy_web_extension_get_resource_as_string (self, g_ptr_array_index (content_script->js, i));
424 if (!js_data)
425 continue;
426
427 user_script = webkit_user_script_new_for_world (js_data,
428 content_script->injected_frames,
429 content_script->injection_time,
430 ephy_embed_shell_get_guid (ephy_embed_shell_get_default ()),
431 (const char * const *)content_script->allow_list->pdata,
432 (const char * const *)content_script->block_list->pdata);
433
434 content_script->user_scripts = g_list_append (content_script->user_scripts, user_script);
435 g_free (js_data);
436 }
437 }
438
439 static void
web_extension_add_content_script(JsonArray * array,guint index_,JsonNode * element_node,gpointer user_data)440 web_extension_add_content_script (JsonArray *array,
441 guint index_,
442 JsonNode *element_node,
443 gpointer user_data)
444 {
445 EphyWebExtension *self = EPHY_WEB_EXTENSION (user_data);
446 WebKitUserContentInjectedFrames injected_frames = WEBKIT_USER_CONTENT_INJECT_TOP_FRAME;
447 WebKitUserScriptInjectionTime injection_time = WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_END;
448 WebExtensionContentScript *content_script;
449 JsonObject *object = json_node_get_object (element_node);
450 JsonArray *child_array;
451 const char *run_at;
452 gboolean all_frames;
453
454 /* TODO: The default value is "document_idle", which in WebKit term is document_end */
455 run_at = json_object_get_string_member_with_default (object, "run_at", "document_idle");
456 if (strcmp (run_at, "document_start") == 0) {
457 injection_time = WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START;
458 } else if (strcmp (run_at, "document_end") == 0) {
459 injection_time = WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_END;
460 } else if (strcmp (run_at, "document_idle") == 0) {
461 g_warning ("run_at: document_idle not supported by WebKit, falling back to document_end");
462 injection_time = WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_END;
463 } else {
464 g_warning ("Unhandled run_at '%s' in web_extension, ignoring.", run_at);
465 return;
466 }
467
468 /* all_frames */
469 all_frames = json_object_get_boolean_member_with_default (object, "all_frames", FALSE);
470 injected_frames = all_frames ? WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES : WEBKIT_USER_CONTENT_INJECT_TOP_FRAME;
471
472 content_script = web_extension_content_script_new (injected_frames, injection_time);
473 if (json_object_has_member (object, "matches")) {
474 child_array = json_object_get_array_member (object, "matches");
475 json_array_foreach_element (child_array, web_extension_add_allow_list, content_script);
476 }
477 g_ptr_array_add (content_script->allow_list, NULL);
478
479 if (json_object_has_member (object, "exclude_matches")) {
480 child_array = json_object_get_array_member (object, "exclude_matches");
481 json_array_foreach_element (child_array, web_extension_add_block_list, content_script);
482 }
483 g_ptr_array_add (content_script->block_list, NULL);
484
485 if (json_object_has_member (object, "js")) {
486 child_array = json_object_get_array_member (object, "js");
487 if (child_array)
488 json_array_foreach_element (child_array, web_extension_add_js, content_script);
489 }
490 g_ptr_array_add (content_script->js, NULL);
491
492 /* Create user scripts so that we can unload them if necessary */
493 web_extension_content_script_build (self, content_script);
494
495 self->content_scripts = g_list_append (self->content_scripts, content_script);
496 }
497
498 static void
web_extension_add_scripts(JsonArray * array,guint index_,JsonNode * element_node,gpointer user_data)499 web_extension_add_scripts (JsonArray *array,
500 guint index_,
501 JsonNode *element_node,
502 gpointer user_data)
503 {
504 EphyWebExtension *self = EPHY_WEB_EXTENSION (user_data);
505
506 g_ptr_array_add (self->background->scripts, g_strdup (json_node_get_string (element_node)));
507 }
508
509 static void
web_extension_add_background(JsonObject * object,const char * member_name,JsonNode * member_node,gpointer user_data)510 web_extension_add_background (JsonObject *object,
511 const char *member_name,
512 JsonNode *member_node,
513 gpointer user_data)
514 {
515 /* https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/background
516 * Limitations:
517 * - persistent with false is not supported yet.
518 */
519 EphyWebExtension *self = EPHY_WEB_EXTENSION (user_data);
520 JsonArray *child_array;
521
522 if (!json_object_has_member (object, "scripts") && !json_object_has_member (object, "page") && !json_object_has_member (object, "persistent")) {
523 g_warning ("Invalid background section, it must be either scripts, page or persistent entry.");
524 return;
525 }
526
527 if (!self->background)
528 self->background = web_extension_background_new ();
529
530 if (json_object_has_member (object, "scripts")) {
531 child_array = json_object_get_array_member (object, "scripts");
532 json_array_foreach_element (child_array, web_extension_add_scripts, self);
533 } else if (!self->background->page && json_object_has_member (object, "page")) {
534 self->background->page = g_strdup (json_object_get_string_member (object, "page"));
535 } else if (json_object_has_member (object, "persistent")) {
536 LOG ("persistent background setting is not handled in Epiphany");
537 }
538 }
539
540 static void
web_extension_add_page_action(JsonObject * object,gpointer user_data)541 web_extension_add_page_action (JsonObject *object,
542 gpointer user_data)
543 {
544 EphyWebExtension *self = EPHY_WEB_EXTENSION (user_data);
545 WebExtensionPageAction *page_action = g_malloc0 (sizeof (WebExtensionPageAction));
546
547 self->page_action = page_action;
548
549 if (json_object_has_member (object, "default_icon")) {
550 WebExtensionIcon *icon = g_malloc (sizeof (WebExtensionIcon));
551 const char *default_icon = json_object_get_string_member (object, "default_icon");
552 g_autofree char *path = NULL;
553
554 icon->size = -1;
555 icon->file = g_strdup (default_icon);
556
557 path = g_build_filename (self->base_location, icon->file, NULL);
558 icon->pixbuf = gdk_pixbuf_new_from_file (path, NULL);
559
560 self->page_action->default_icons = g_list_append (self->page_action->default_icons, icon);
561 }
562 }
563
564 static void
web_extension_page_action_free(WebExtensionPageAction * page_action)565 web_extension_page_action_free (WebExtensionPageAction *page_action)
566 {
567 g_clear_list (&page_action->default_icons, (GDestroyNotify)web_extension_icon_free);
568 g_free (page_action);
569 }
570
571 /* TODO: Load translation for current locale during init */
572 static char *
web_extension_get_translation(EphyWebExtension * self,const char * locale,const char * key)573 web_extension_get_translation (EphyWebExtension *self,
574 const char *locale,
575 const char *key)
576 {
577 g_autoptr (JsonParser) parser = NULL;
578 g_autoptr (GError) error = NULL;
579 g_autofree char *path = g_strdup_printf ("_locales/%s/messages.json", locale);
580 JsonNode *root = NULL;
581 JsonObject *root_object = NULL;
582 JsonObject *name = NULL;
583 const unsigned char *data = NULL;
584 gsize length;
585
586 if (!ephy_web_extension_has_resource (self, path))
587 return NULL;
588
589 data = ephy_web_extension_get_resource (self, path, &length);
590
591 parser = json_parser_new ();
592 if (!json_parser_load_from_data (parser, (char *)data, length, &error)) {
593 g_warning ("Could not load WebExtension translation: %s", error->message);
594 return NULL;
595 }
596
597 root = json_parser_get_root (parser);
598 if (!root) {
599 g_warning ("WebExtension translation root is NULL, return NULL.");
600 return NULL;
601 }
602
603 root_object = json_node_get_object (root);
604 if (!root_object) {
605 g_warning ("WebExtension translation root object is NULL, return NULL.");
606 return NULL;
607 }
608
609 name = json_object_get_object_member (root_object, key);
610 if (name)
611 return g_strdup (json_object_get_string_member (name, "message"));
612
613 return NULL;
614 }
615
616 char *
ephy_web_extension_manifest_get_key(EphyWebExtension * self,JsonObject * object,char * key)617 ephy_web_extension_manifest_get_key (EphyWebExtension *self,
618 JsonObject *object,
619 char *key)
620 {
621 char *value = NULL;
622
623 if (json_object_has_member (object, key)) {
624 g_autofree char *ret = g_strdup (json_object_get_string_member (object, key));
625
626 /* Translation are requested with a unique string, e.g.:
627 * __MSG_unique_name__ but stored as unique_name in messages.json.
628 * Let's check for this prefix and suffix and extract the unique name
629 */
630 if (g_str_has_prefix (ret, "__MSG_") && g_str_has_suffix (ret, "__")) {
631 /* FIXME: Set current locale */
632 g_autofree char *locale = g_strdup ("en");
633
634 /* Remove trailing __ */
635 ret[strlen (ret) - 2] = '\0';
636 value = web_extension_get_translation (self, locale, ret + strlen ("__MSG_"));
637 } else {
638 value = g_strdup (ret);
639 }
640 }
641
642 return value;
643 }
644
645 static void
web_extension_add_browser_action(JsonObject * object,gpointer user_data)646 web_extension_add_browser_action (JsonObject *object,
647 gpointer user_data)
648 {
649 EphyWebExtension *self = EPHY_WEB_EXTENSION (user_data);
650 WebExtensionBrowserAction *browser_action = g_malloc0 (sizeof (WebExtensionBrowserAction));
651
652 g_clear_object (&self->browser_action);
653 self->browser_action = browser_action;
654
655 if (json_object_has_member (object, "default_title")) {
656 self->browser_action->title = ephy_web_extension_manifest_get_key (self, object, "default_title");
657 }
658
659 if (json_object_has_member (object, "default_icon")) {
660 /* defaullt_icon can be Object or String */
661 JsonNode *icon_node = json_object_get_member (object, "default_icon");
662
663 if (json_node_get_node_type (icon_node) == JSON_NODE_OBJECT) {
664 JsonObject *icon_object = json_object_get_object_member (object, "default_icon");
665 json_object_foreach_member (icon_object, web_extension_add_browser_icons, self);
666 } else {
667 const char *default_icon = json_object_get_string_member (object, "default_icon");
668 WebExtensionIcon *icon = web_extension_icon_new (self, default_icon, -1);
669
670 self->browser_action->default_icons = g_list_append (self->browser_action->default_icons, icon);
671 }
672 }
673
674 if (json_object_has_member (object, "default_popup"))
675 self->browser_action->popup = g_strdup (json_object_get_string_member (object, "default_popup"));
676 }
677
678 static void
web_extension_browser_action_free(WebExtensionBrowserAction * browser_action)679 web_extension_browser_action_free (WebExtensionBrowserAction *browser_action)
680 {
681 g_clear_pointer (&browser_action->title, g_free);
682 g_clear_pointer (&browser_action->popup, g_free);
683 g_clear_list (&browser_action->default_icons, (GDestroyNotify)web_extension_icon_free);
684 g_free (browser_action);
685 }
686
687 static void
web_extension_add_options_ui(JsonObject * object,gpointer user_data)688 web_extension_add_options_ui (JsonObject *object,
689 gpointer user_data)
690 {
691 EphyWebExtension *self = EPHY_WEB_EXTENSION (user_data);
692 const char *page = json_object_get_string_member (object, "page");
693 WebExtensionOptionsUI *options_ui = web_extension_options_ui_new (page);
694
695 g_clear_pointer (&self->options_ui, web_extension_options_ui_free);
696 self->options_ui = options_ui;
697 }
698
699 static void
web_extension_add_permission(JsonArray * array,guint index_,JsonNode * element_node,gpointer user_data)700 web_extension_add_permission (JsonArray *array,
701 guint index_,
702 JsonNode *element_node,
703 gpointer user_data)
704 {
705 EphyWebExtension *self = EPHY_WEB_EXTENSION (user_data);
706
707 g_ptr_array_add (self->permissions, g_strdup (json_node_get_string (element_node)));
708 }
709
710 static void
web_extension_resource_free(WebExtensionResource * resource)711 web_extension_resource_free (WebExtensionResource *resource)
712 {
713 g_clear_pointer (&resource->bytes, g_bytes_unref);
714 g_clear_pointer (&resource->name, g_free);
715 g_free (resource);
716 }
717
718 static void
ephy_web_extension_dispose(GObject * object)719 ephy_web_extension_dispose (GObject *object)
720 {
721 EphyWebExtension *self = EPHY_WEB_EXTENSION (object);
722
723 g_clear_pointer (&self->base_location, g_free);
724 g_clear_pointer (&self->manifest, g_free);
725 g_clear_pointer (&self->guid, g_free);
726 g_clear_pointer (&self->description, g_free);
727 g_clear_pointer (&self->author, g_free);
728 g_clear_pointer (&self->name, g_free);
729 g_clear_pointer (&self->version, g_free);
730 g_clear_pointer (&self->homepage_url, g_free);
731
732 g_clear_list (&self->icons, (GDestroyNotify)web_extension_icon_free);
733 g_clear_list (&self->content_scripts, (GDestroyNotify)web_extension_content_script_free);
734 g_clear_list (&self->resources, (GDestroyNotify)web_extension_resource_free);
735 g_clear_pointer (&self->background, web_extension_background_free);
736 g_clear_pointer (&self->options_ui, web_extension_options_ui_free);
737 g_clear_pointer (&self->permissions, g_ptr_array_unref);
738
739 g_clear_pointer (&self->page_action, web_extension_page_action_free);
740 g_clear_pointer (&self->browser_action, web_extension_browser_action_free);
741 g_clear_list (&self->custom_css, (GDestroyNotify)webkit_user_style_sheet_unref);
742
743 g_hash_table_destroy (self->page_action_map);
744
745 G_OBJECT_CLASS (ephy_web_extension_parent_class)->dispose (object);
746 }
747
748 static void
ephy_web_extension_class_init(EphyWebExtensionClass * klass)749 ephy_web_extension_class_init (EphyWebExtensionClass *klass)
750 {
751 GObjectClass *object_class = G_OBJECT_CLASS (klass);
752
753 object_class->dispose = ephy_web_extension_dispose;
754 }
755
756 static void
ephy_web_extension_init(EphyWebExtension * self)757 ephy_web_extension_init (EphyWebExtension *self)
758 {
759 self->page_action_map = g_hash_table_new (NULL, NULL);
760 self->permissions = g_ptr_array_new_full (1, g_free);
761
762 self->guid = g_uuid_string_random ();
763 }
764
765 static EphyWebExtension *
ephy_web_extension_new(void)766 ephy_web_extension_new (void)
767 {
768 return g_object_new (EPHY_TYPE_WEB_EXTENSION, NULL);
769 }
770
771 static void
web_extension_add_resource(EphyWebExtension * self,const char * name,gpointer data,guint len)772 web_extension_add_resource (EphyWebExtension *self,
773 const char *name,
774 gpointer data,
775 guint len)
776 {
777 WebExtensionResource *resource = g_malloc0 (sizeof (WebExtensionResource));
778
779 resource->name = g_strdup (name);
780 resource->bytes = g_bytes_new (data, len);
781
782 self->resources = g_list_append (self->resources, resource);
783 }
784
785 static gboolean
web_extension_read_directory(EphyWebExtension * self,char * base,char * path)786 web_extension_read_directory (EphyWebExtension *self,
787 char *base,
788 char *path)
789 {
790 g_autoptr (GError) error = NULL;
791 g_autoptr (GDir) dir = NULL;
792 const char *dirent;
793 gboolean ret = TRUE;
794
795 dir = g_dir_open (path, 0, &error);
796 if (!dir) {
797 g_warning ("Could not open web_extension directory: %s", error->message);
798 return FALSE;
799 }
800
801 while ((dirent = g_dir_read_name (dir))) {
802 GFileType type;
803 g_autofree gchar *filename = g_build_filename (path, dirent, NULL);
804 g_autoptr (GFile) file = g_file_new_for_path (filename);
805
806 type = g_file_query_file_type (file, G_FILE_QUERY_INFO_NONE, NULL);
807 if (type == G_FILE_TYPE_DIRECTORY) {
808 web_extension_read_directory (self, base, filename);
809 } else {
810 g_autofree char *data = NULL;
811 gsize len;
812
813 if (g_file_get_contents (filename, &data, &len, NULL))
814 web_extension_add_resource (self, filename + strlen (base) + 1, data, len);
815 }
816 }
817
818 return ret;
819 }
820
821 static EphyWebExtension *
ephy_web_extension_load_directory(char * filename)822 ephy_web_extension_load_directory (char *filename)
823 {
824 EphyWebExtension *self = ephy_web_extension_new ();
825
826 web_extension_read_directory (self, filename, filename);
827
828 return self;
829 }
830
831 static EphyWebExtension *
ephy_web_extension_load_xpi(GFile * target)832 ephy_web_extension_load_xpi (GFile *target)
833 {
834 EphyWebExtension *self = NULL;
835 struct archive *pkg;
836 struct archive_entry *entry;
837 int res;
838
839 pkg = archive_read_new ();
840 archive_read_support_format_zip (pkg);
841
842 res = archive_read_open_filename (pkg, g_file_get_path (target), 10240);
843 if (res == ARCHIVE_OK) {
844 self = ephy_web_extension_new ();
845 self->xpi = TRUE;
846
847 while (archive_read_next_header (pkg, &entry) == ARCHIVE_OK) {
848 int64_t size = archive_entry_size (entry);
849 gsize total_len = 0;
850 g_autofree char *data = NULL;
851
852 data = g_malloc0 (size);
853 total_len = archive_read_data (pkg, data, size);
854
855 if (total_len > 0)
856 web_extension_add_resource (self, archive_entry_pathname (entry), data, total_len);
857 }
858
859 res = archive_read_free (pkg);
860 if (res != ARCHIVE_OK)
861 g_warning ("Error freeing archive: %s", archive_error_string (pkg));
862 } else {
863 g_warning ("Could not open archive %s", archive_error_string (pkg));
864 }
865
866 return self;
867 }
868
869 EphyWebExtension *
ephy_web_extension_load(GFile * target)870 ephy_web_extension_load (GFile *target)
871 {
872 g_autoptr (GError) error = NULL;
873 g_autoptr (GFile) source = g_file_dup (target);
874 g_autoptr (GFile) parent = NULL;
875 g_autoptr (JsonObject) icons_object = NULL;
876 g_autoptr (JsonArray) content_scripts_array = NULL;
877 g_autoptr (JsonObject) background_object = NULL;
878 JsonParser *parser = NULL;
879 JsonNode *root = NULL;
880 JsonObject *root_object = NULL;
881 EphyWebExtension *self = NULL;
882 GFileType type;
883 gsize length = 0;
884 const unsigned char *manifest;
885
886 type = g_file_query_file_type (source, G_FILE_QUERY_INFO_NONE, NULL);
887 if (type == G_FILE_TYPE_DIRECTORY) {
888 g_autofree char *path = g_file_get_path (source);
889 self = ephy_web_extension_load_directory (path);
890 } else
891 self = ephy_web_extension_load_xpi (source);
892
893 if (!self)
894 return NULL;
895
896 manifest = ephy_web_extension_get_resource (self, "manifest.json", &length);
897 if (!manifest)
898 return NULL;
899
900 parser = json_parser_new ();
901 if (!json_parser_load_from_data (parser, (char *)manifest, length, &error)) {
902 g_warning ("Could not load web extension manifest: %s", error->message);
903 return NULL;
904 }
905
906 root = json_parser_get_root (parser);
907 if (!root) {
908 g_warning ("WebExtension manifest json root is NULL, return NULL.");
909 return NULL;
910 }
911
912 root_object = json_node_get_object (root);
913 if (!root_object) {
914 g_warning ("WebExtension manifest json root is NULL, return NULL.");
915 return NULL;
916 }
917
918 self->manifest = g_strndup ((char *)manifest, length);
919 self->base_location = parent ? g_file_get_path (parent) : g_file_get_path (target);
920 self->description = ephy_web_extension_manifest_get_key (self, root_object, "description");
921 self->manifest_version = json_object_get_int_member (root_object, "manifest_version");
922 self->name = ephy_web_extension_manifest_get_key (self, root_object, "name");
923 self->version = ephy_web_extension_manifest_get_key (self, root_object, "version");
924 self->homepage_url = ephy_web_extension_manifest_get_key (self, root_object, "homepage_url");
925 self->author = ephy_web_extension_manifest_get_key (self, root_object, "author");
926
927 if (json_object_has_member (root_object, "icons")) {
928 icons_object = json_object_get_object_member (root_object, "icons");
929
930 json_object_foreach_member (icons_object, web_extension_add_icon, self);
931 }
932
933 if (json_object_has_member (root_object, "content_scripts")) {
934 content_scripts_array = json_object_get_array_member (root_object, "content_scripts");
935
936 json_array_foreach_element (content_scripts_array, web_extension_add_content_script, self);
937 }
938
939 if (json_object_has_member (root_object, "background")) {
940 background_object = json_object_get_object_member (root_object, "background");
941
942 json_object_foreach_member (background_object, web_extension_add_background, self);
943 }
944 if (self->background)
945 g_ptr_array_add (self->background->scripts, NULL);
946
947 if (json_object_has_member (root_object, "page_action")) {
948 g_autoptr (JsonObject) page_action_object = json_object_get_object_member (root_object, "page_action");
949
950 web_extension_add_page_action (page_action_object, self);
951 }
952
953 if (json_object_has_member (root_object, "browser_action")) {
954 g_autoptr (JsonObject) browser_action_object = json_object_get_object_member (root_object, "browser_action");
955
956 web_extension_add_browser_action (browser_action_object, self);
957 }
958
959 if (json_object_has_member (root_object, "options_ui")) {
960 g_autoptr (JsonObject) browser_action_object = json_object_get_object_member (root_object, "options_ui");
961
962 web_extension_add_options_ui (browser_action_object, self);
963 }
964
965 if (json_object_has_member (root_object, "permissions")) {
966 g_autoptr (JsonArray) array = json_object_get_array_member (root_object, "permissions");
967
968 json_array_foreach_element (array, web_extension_add_permission, self);
969 }
970 if (self->permissions)
971 g_ptr_array_add (self->permissions, NULL);
972
973 return self;
974 }
975
976 EphyWebExtension *
ephy_web_extension_load_finished(GObject * unused,GAsyncResult * result,GError ** error)977 ephy_web_extension_load_finished (GObject *unused,
978 GAsyncResult *result,
979 GError **error)
980 {
981 g_assert (g_task_is_valid (result, unused));
982
983 return g_task_propagate_pointer (G_TASK (result), error);
984 }
985
986 static void
load_web_extension_thread(GTask * task,gpointer * unused,GFile * target,GCancellable * cancellable)987 load_web_extension_thread (GTask *task,
988 gpointer *unused,
989 GFile *target,
990 GCancellable *cancellable)
991 {
992 EphyWebExtension *self = ephy_web_extension_load (target);
993
994 g_task_return_pointer (task, self, NULL);
995 }
996
997 void
ephy_web_extension_load_async(GFile * target,GCancellable * cancellable,GAsyncReadyCallback callback,gpointer user_data)998 ephy_web_extension_load_async (GFile *target,
999 GCancellable *cancellable,
1000 GAsyncReadyCallback callback,
1001 gpointer user_data)
1002 {
1003 GTask *task;
1004
1005 g_assert (target);
1006
1007 task = g_task_new (NULL, cancellable, callback, user_data);
1008 g_task_set_priority (task, G_PRIORITY_DEFAULT);
1009 g_task_set_task_data (task,
1010 g_file_dup (target),
1011 (GDestroyNotify)g_object_unref);
1012 g_task_run_in_thread (task, (GTaskThreadFunc)load_web_extension_thread);
1013 g_object_unref (task);
1014 }
1015
1016
1017 GdkPixbuf *
ephy_web_extension_load_pixbuf(EphyWebExtension * self,char * file)1018 ephy_web_extension_load_pixbuf (EphyWebExtension *self,
1019 char *file)
1020 {
1021 g_autofree gchar *path = NULL;
1022
1023 path = g_build_filename (self->base_location, file, NULL);
1024
1025 return gdk_pixbuf_new_from_file (path, NULL);
1026 }
1027
1028 void
ephy_web_extension_remove(EphyWebExtension * self)1029 ephy_web_extension_remove (EphyWebExtension *self)
1030 {
1031 g_autoptr (GError) error = NULL;
1032
1033 if (!self->xpi) {
1034 if (!ephy_file_delete_dir_recursively (self->base_location, &error))
1035 g_warning ("Could not delete web_extension from %s: %s", self->base_location, error->message);
1036 } else {
1037 g_unlink (self->base_location);
1038 }
1039 }
1040
1041 gboolean
ephy_web_extension_has_page_action(EphyWebExtension * self)1042 ephy_web_extension_has_page_action (EphyWebExtension *self)
1043 {
1044 return !!self->page_action;
1045 }
1046
1047 gboolean
ephy_web_extension_has_browser_action(EphyWebExtension * self)1048 ephy_web_extension_has_browser_action (EphyWebExtension *self)
1049 {
1050 return !!self->browser_action;
1051 }
1052
1053 gboolean
ephy_web_extension_has_background_web_view(EphyWebExtension * self)1054 ephy_web_extension_has_background_web_view (EphyWebExtension *self)
1055 {
1056 return !!self->background;
1057 }
1058
1059 const char *
ephy_web_extension_background_web_view_get_page(EphyWebExtension * self)1060 ephy_web_extension_background_web_view_get_page (EphyWebExtension *self)
1061 {
1062 return self->background->page;
1063 }
1064
1065 GPtrArray *
ephy_web_extension_background_web_view_get_scripts(EphyWebExtension * self)1066 ephy_web_extension_background_web_view_get_scripts (EphyWebExtension *self)
1067 {
1068 return self->background->scripts;
1069 }
1070
1071 GList *
ephy_web_extension_get_content_scripts(EphyWebExtension * self)1072 ephy_web_extension_get_content_scripts (EphyWebExtension *self)
1073 {
1074 return self->content_scripts;
1075 }
1076
1077 GList *
ephy_web_extension_get_content_script_js(EphyWebExtension * self,gpointer content_script)1078 ephy_web_extension_get_content_script_js (EphyWebExtension *self,
1079 gpointer content_script)
1080 {
1081 WebExtensionContentScript *script = content_script;
1082 return script->user_scripts;
1083 }
1084
1085 GdkPixbuf *
ephy_web_extension_browser_action_get_icon(EphyWebExtension * self,int size)1086 ephy_web_extension_browser_action_get_icon (EphyWebExtension *self,
1087 int size)
1088 {
1089 WebExtensionIcon *icon_fallback = NULL;
1090
1091 if (!self->browser_action || !self->browser_action->default_icons)
1092 return NULL;
1093
1094 for (GList *list = self->browser_action->default_icons; list && list->data; list = list->next) {
1095 WebExtensionIcon *icon = list->data;
1096
1097 if (icon->size == size)
1098 return gdk_pixbuf_copy (icon->pixbuf);
1099
1100 if (!icon_fallback || icon->size > icon_fallback->size)
1101 icon_fallback = icon;
1102 }
1103
1104 /* Fallback */
1105 if (icon_fallback)
1106 return gdk_pixbuf_scale_simple (icon_fallback->pixbuf, size, size, GDK_INTERP_BILINEAR);
1107
1108 return NULL;
1109 }
1110
1111 const char *
ephy_web_extension_get_browser_popup(EphyWebExtension * self)1112 ephy_web_extension_get_browser_popup (EphyWebExtension *self)
1113 {
1114 return self->browser_action->popup;
1115 }
1116
1117 const char *
ephy_web_extension_browser_action_get_tooltip(EphyWebExtension * self)1118 ephy_web_extension_browser_action_get_tooltip (EphyWebExtension *self)
1119 {
1120 return self->browser_action->title;
1121 }
1122
1123 WebExtensionCustomCSS *
web_extension_custom_css_new(EphyWebExtension * self,const char * code)1124 web_extension_custom_css_new (EphyWebExtension *self,
1125 const char *code)
1126
1127 {
1128 WebExtensionCustomCSS *css = g_malloc0 (sizeof (WebExtensionCustomCSS));
1129
1130 css->code = g_strdup (code);
1131 css->style = webkit_user_style_sheet_new (css->code, WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, WEBKIT_USER_STYLE_LEVEL_USER, NULL, NULL);
1132
1133 self->custom_css = g_list_append (self->custom_css, css);
1134
1135 return css;
1136 }
1137
1138 WebKitUserStyleSheet *
ephy_web_extension_get_custom_css(EphyWebExtension * self,const char * code)1139 ephy_web_extension_get_custom_css (EphyWebExtension *self,
1140 const char *code)
1141 {
1142 WebExtensionCustomCSS *css = NULL;
1143
1144 for (GList *list = self->custom_css; list && list->data; list = list->data) {
1145 css = list->data;
1146
1147 if (strcmp (css->code, code) == 0)
1148 return css->style;
1149 }
1150
1151 return NULL;
1152 }
1153
1154 WebKitUserStyleSheet *
ephy_web_extension_add_custom_css(EphyWebExtension * self,const char * code)1155 ephy_web_extension_add_custom_css (EphyWebExtension *self,
1156 const char *code)
1157 {
1158 WebKitUserStyleSheet *style;
1159 WebExtensionCustomCSS *css = NULL;
1160
1161 style = ephy_web_extension_get_custom_css (self, code);
1162 if (style)
1163 return style;
1164
1165 css = web_extension_custom_css_new (self, code);
1166
1167 return css->style;
1168 }
1169
1170 GList *
ephy_web_extension_get_custom_css_list(EphyWebExtension * self)1171 ephy_web_extension_get_custom_css_list (EphyWebExtension *self)
1172 {
1173 return self->custom_css;
1174 }
1175
1176 WebKitUserStyleSheet *
ephy_web_extension_custom_css_style(EphyWebExtension * self,gpointer custom_css)1177 ephy_web_extension_custom_css_style (EphyWebExtension *self,
1178 gpointer custom_css)
1179 {
1180 WebExtensionCustomCSS *css = custom_css;
1181
1182 return css->style;
1183 }
1184
1185 char *
ephy_web_extension_get_option_ui_page(EphyWebExtension * self)1186 ephy_web_extension_get_option_ui_page (EphyWebExtension *self)
1187 {
1188 if (!self->options_ui)
1189 return NULL;
1190
1191 return ephy_web_extension_get_resource_as_string (self, self->options_ui->page);
1192 }
1193
1194 const char *
ephy_web_extension_get_guid(EphyWebExtension * self)1195 ephy_web_extension_get_guid (EphyWebExtension *self)
1196 {
1197 return self->guid;
1198 }
1199
1200 GPtrArray *
ephy_web_extension_get_permissions(EphyWebExtension * self)1201 ephy_web_extension_get_permissions (EphyWebExtension *self)
1202 {
1203 return self->permissions;
1204 }
1205