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