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 #include "config.h"
22 
23 #include "ephy-debug.h"
24 #include "ephy-embed-shell.h"
25 #include "ephy-embed-prefs.h"
26 #include "ephy-embed-utils.h"
27 #include "ephy-file-helpers.h"
28 #include "ephy-header-bar.h"
29 #include "ephy-location-entry.h"
30 #include "ephy-notification.h"
31 #include "ephy-settings.h"
32 #include "ephy-shell.h"
33 #include "ephy-string.h"
34 #include "ephy-web-extension.h"
35 #include "ephy-web-extension-manager.h"
36 #include "ephy-web-view.h"
37 
38 #include "api/notifications.h"
39 #include "api/pageaction.h"
40 #include "api/runtime.h"
41 #include "api/tabs.h"
42 
43 #include <json-glib/json-glib.h>
44 
45 struct _EphyWebExtensionManager {
46   GObject parent_instance;
47 
48   GCancellable *cancellable;
49   GList *web_extensions;
50   GHashTable *page_action_map;
51   GHashTable *browser_action_map;
52   GHashTable *background_web_views;
53 };
54 
55 G_DEFINE_TYPE (EphyWebExtensionManager, ephy_web_extension_manager, G_TYPE_OBJECT)
56 
57 EphyWebExtensionApiHandler api_handlers[] = {
58   {"notifications", ephy_web_extension_api_notifications_handler},
59   {"pageAction", ephy_web_extension_api_pageaction_handler},
60   {"runtime", ephy_web_extension_api_runtime_handler},
61   {"tabs", ephy_web_extension_api_tabs_handler},
62   {NULL, NULL},
63 };
64 
65 enum {
66   CHANGED,
67   LAST_SIGNAL
68 };
69 
70 static guint signals[LAST_SIGNAL];
71 
72 static void
ephy_web_extension_manager_add_to_list(EphyWebExtensionManager * self,EphyWebExtension * web_extension)73 ephy_web_extension_manager_add_to_list (EphyWebExtensionManager *self,
74                                         EphyWebExtension        *web_extension)
75 {
76   self->web_extensions = g_list_append (self->web_extensions, g_object_ref (web_extension));
77 
78   g_signal_emit (self, signals[CHANGED], 0);
79 }
80 
81 static void
ephy_web_extension_manager_remove_from_list(EphyWebExtensionManager * self,EphyWebExtension * web_extension)82 ephy_web_extension_manager_remove_from_list (EphyWebExtensionManager *self,
83                                              EphyWebExtension        *web_extension)
84 {
85   self->web_extensions = g_list_remove (self->web_extensions, web_extension);
86   g_object_unref (web_extension);
87 
88   g_signal_emit (self, signals[CHANGED], 0);
89 }
90 
91 void
on_web_extension_loaded(GObject * source_object,GAsyncResult * result,gpointer user_data)92 on_web_extension_loaded (GObject      *source_object,
93                          GAsyncResult *result,
94                          gpointer      user_data)
95 {
96   g_autoptr (GError) error = NULL;
97   EphyWebExtension *web_extension;
98   EphyWebExtensionManager *self = EPHY_WEB_EXTENSION_MANAGER (user_data);
99 
100 
101   web_extension = ephy_web_extension_load_finished (source_object, result, &error);
102   if (!web_extension) {
103     return;
104   }
105 
106   ephy_web_extension_manager_add_to_list (self, web_extension);
107   g_object_unref (web_extension);
108 
109   if (ephy_web_extension_manager_is_active (self, web_extension))
110     ephy_web_extension_manager_set_active (self, web_extension, TRUE);
111 }
112 
113 static void
ephy_web_extension_manager_scan_directory(EphyWebExtensionManager * self,const char * extension_dir)114 ephy_web_extension_manager_scan_directory (EphyWebExtensionManager *self,
115                                            const char              *extension_dir)
116 {
117   g_autoptr (GDir) dir = NULL;
118   g_autoptr (GError) error = NULL;
119   const char *directory;
120 
121   if (g_mkdir_with_parents (extension_dir, 0700) != 0)
122     g_warning ("Failed to create %s: %s", extension_dir, g_strerror (errno));
123 
124   if (!g_file_test (extension_dir, G_FILE_TEST_EXISTS))
125     g_mkdir_with_parents (extension_dir, 0700);
126 
127   dir = g_dir_open (extension_dir, 0, &error);
128   if (!dir) {
129     g_warning ("Could not open %s: %s", extension_dir, error->message);
130     return;
131   }
132 
133   errno = 0;
134   while ((directory = g_dir_read_name (dir))) {
135     g_autofree char *filename = NULL;
136     g_autoptr (GFile) file = NULL;
137 
138     if (errno != 0) {
139       g_warning ("Problem reading %s: %s", extension_dir, g_strerror (errno));
140       break;
141     }
142 
143     filename = g_build_filename (extension_dir, directory, NULL);
144     file = g_file_new_for_path (filename);
145 
146     ephy_web_extension_load_async (file, self->cancellable, on_web_extension_loaded, self);
147 
148     errno = 0;
149   }
150 }
151 
152 static void
ephy_web_extension_manager_constructed(GObject * object)153 ephy_web_extension_manager_constructed (GObject *object)
154 {
155   EphyWebExtensionManager *self = EPHY_WEB_EXTENSION_MANAGER (object);
156   g_autofree char *dir = g_build_filename (ephy_default_profile_dir (), "web_extensions", NULL);
157 
158   self->background_web_views = g_hash_table_new (NULL, NULL);
159   self->page_action_map = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_hash_table_destroy);
160   self->browser_action_map = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)gtk_widget_destroy);
161   self->web_extensions = NULL;
162 
163   ephy_web_extension_manager_scan_directory (self, dir);
164 }
165 
166 static void
ephy_web_extension_manager_dispose(GObject * object)167 ephy_web_extension_manager_dispose (GObject *object)
168 {
169   EphyWebExtensionManager *self = EPHY_WEB_EXTENSION_MANAGER (object);
170 
171   g_clear_pointer (&self->background_web_views, g_hash_table_destroy);
172   g_clear_pointer (&self->page_action_map, g_hash_table_destroy);
173   g_list_free_full (g_steal_pointer (&self->web_extensions), g_object_unref);
174 }
175 
176 static void
ephy_web_extension_manager_class_init(EphyWebExtensionManagerClass * klass)177 ephy_web_extension_manager_class_init (EphyWebExtensionManagerClass *klass)
178 {
179   GObjectClass *object_class = G_OBJECT_CLASS (klass);
180 
181   object_class->constructed = ephy_web_extension_manager_constructed;
182   object_class->dispose = ephy_web_extension_manager_dispose;
183 
184   signals[CHANGED] =
185     g_signal_new ("changed",
186                   G_OBJECT_CLASS_TYPE (object_class),
187                   G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
188                   0, NULL, NULL, NULL,
189                   G_TYPE_NONE, 0);
190 }
191 
192 static void
ephy_web_extension_manager_init(EphyWebExtensionManager * self)193 ephy_web_extension_manager_init (EphyWebExtensionManager *self)
194 {
195 }
196 
197 EphyWebExtensionManager *
ephy_web_extension_manager_new(void)198 ephy_web_extension_manager_new (void)
199 {
200   return g_object_new (EPHY_TYPE_WEB_EXTENSION_MANAGER, NULL);
201 }
202 
203 GList *
ephy_web_extension_manager_get_web_extensions(EphyWebExtensionManager * self)204 ephy_web_extension_manager_get_web_extensions (EphyWebExtensionManager *self)
205 {
206   return self->web_extensions;
207 }
208 
209 /**
210  * Installs/Adds all web_extensions to new EphyWindow.
211  */
212 void
ephy_web_extension_manager_install_actions(EphyWebExtensionManager * self,EphyWindow * window)213 ephy_web_extension_manager_install_actions (EphyWebExtensionManager *self,
214                                             EphyWindow              *window)
215 {
216   for (GList *list = self->web_extensions; list && list->data; list = list->next)
217     ephy_web_extension_manager_add_web_extension_to_window (self, list->data, window);
218 }
219 
220 void
on_new_web_extension_loaded(GObject * source_object,GAsyncResult * result,gpointer user_data)221 on_new_web_extension_loaded (GObject      *source_object,
222                              GAsyncResult *result,
223                              gpointer      user_data)
224 {
225   g_autoptr (GError) error = NULL;
226   EphyWebExtension *web_extension;
227   EphyWebExtensionManager *self = EPHY_WEB_EXTENSION_MANAGER (user_data);
228 
229   web_extension = ephy_web_extension_load_finished (source_object, result, &error);
230   if (!web_extension) {
231     return;
232   }
233 
234   ephy_web_extension_manager_add_to_list (self, web_extension);
235 }
236 /**
237  * Install a new web web_extension into the local web_extension directory.
238  * File should only point to a manifest.json or a .xpi file
239  */
240 void
ephy_web_extension_manager_install(EphyWebExtensionManager * self,GFile * file)241 ephy_web_extension_manager_install (EphyWebExtensionManager *self,
242                                     GFile                   *file)
243 {
244   g_autoptr (GFile) target = NULL;
245   g_autofree char *basename = NULL;
246   gboolean is_xpi = FALSE;
247 
248   basename = g_file_get_basename (file);
249   is_xpi = g_str_has_suffix (basename, ".xpi");
250 
251   if (!is_xpi) {
252     g_autoptr (GFile) source = NULL;
253 
254     /* Get parent directory */
255     source = g_file_get_parent (file);
256     target = g_file_new_build_filename (ephy_default_profile_dir (), "web_extensions", g_file_get_basename (source), NULL);
257 
258     ephy_copy_directory (g_file_get_path (source), g_file_get_path (target));
259   } else {
260     g_autoptr (GError) error = NULL;
261     target = g_file_new_build_filename (ephy_default_profile_dir (), "web_extensions", g_file_get_basename (file), NULL);
262 
263     if (!g_file_copy (file, target, G_FILE_COPY_NONE, NULL, NULL, NULL, &error)) {
264       if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_EXISTS)) {
265         g_warning ("Could not copy file for web_extensions: %s", error->message);
266         return;
267       }
268     }
269   }
270 
271   if (target)
272     ephy_web_extension_load_async (g_steal_pointer (&target), self->cancellable, on_new_web_extension_loaded, self);
273 }
274 
275 void
ephy_web_extension_manager_uninstall(EphyWebExtensionManager * self,EphyWebExtension * web_extension)276 ephy_web_extension_manager_uninstall (EphyWebExtensionManager *self,
277                                       EphyWebExtension        *web_extension)
278 {
279   if (ephy_web_extension_manager_is_active (self, web_extension))
280     ephy_web_extension_manager_set_active (self, web_extension, FALSE);
281 
282   ephy_web_extension_remove (web_extension);
283   ephy_web_extension_manager_remove_from_list (self, web_extension);
284 }
285 
286 void
ephy_web_extension_manager_update_location_entry(EphyWebExtensionManager * self,EphyWindow * window)287 ephy_web_extension_manager_update_location_entry (EphyWebExtensionManager *self,
288                                                   EphyWindow              *window)
289 {
290   GtkWidget *title_widget;
291   EphyLocationEntry *lentry;
292   EphyTabView *tab_view = ephy_window_get_tab_view (EPHY_WINDOW (window));
293   GtkWidget *page = ephy_tab_view_get_selected_page (tab_view);
294   EphyWebView *web_view;
295 
296   if (!page)
297     return;
298 
299   web_view = ephy_embed_get_web_view (EPHY_EMBED (page));
300   title_widget = GTK_WIDGET (ephy_header_bar_get_title_widget (EPHY_HEADER_BAR (ephy_window_get_header_bar (window))));
301   if (!EPHY_IS_LOCATION_ENTRY (title_widget))
302     return;
303 
304   lentry = EPHY_LOCATION_ENTRY (title_widget);
305 
306   ephy_location_entry_page_action_clear (lentry);
307 
308   for (GList *list = ephy_web_extension_manager_get_web_extensions (self); list && list->data; list = list->next) {
309     EphyWebExtension *web_extension = EPHY_WEB_EXTENSION (list->data);
310     GtkWidget *action = ephy_web_extension_manager_get_page_action (self, web_extension, web_view);
311 
312     if (action)
313       ephy_location_entry_page_action_add (lentry, action);
314   }
315 }
316 
317 EphyWebView *
ephy_web_extension_manager_get_background_web_view(EphyWebExtensionManager * self,EphyWebExtension * web_extension)318 ephy_web_extension_manager_get_background_web_view (EphyWebExtensionManager *self,
319                                                     EphyWebExtension        *web_extension)
320 {
321   return g_hash_table_lookup (self->background_web_views, web_extension);
322 }
323 
324 static void
ephy_web_extension_manager_set_background_web_view(EphyWebExtensionManager * self,EphyWebExtension * web_extension,EphyWebView * web_view)325 ephy_web_extension_manager_set_background_web_view (EphyWebExtensionManager *self,
326                                                     EphyWebExtension        *web_extension,
327                                                     EphyWebView             *web_view)
328 {
329   g_hash_table_insert (self->background_web_views, web_extension, web_view);
330 }
331 
332 static gboolean
page_action_clicked(GtkWidget * event_box,GdkEventButton * event,gpointer user_data)333 page_action_clicked (GtkWidget      *event_box,
334                      GdkEventButton *event,
335                      gpointer        user_data)
336 {
337   EphyWebExtension *web_extension = EPHY_WEB_EXTENSION (user_data);
338   EphyShell *shell = ephy_shell_get_default ();
339   EphyWebView *view = EPHY_WEB_VIEW (ephy_shell_get_active_web_view (shell));
340   g_autoptr (JsonBuilder) builder = json_builder_new ();
341   g_autoptr (JsonNode) root = NULL;
342   g_autofree char *json = NULL;
343   g_autofree char *script = NULL;
344   EphyWebExtensionManager *self = ephy_shell_get_web_extension_manager (shell);
345   WebKitWebView *web_view = WEBKIT_WEB_VIEW (ephy_web_extension_manager_get_background_web_view (self, web_extension));
346 
347   json_builder_begin_object (builder);
348   json_builder_set_member_name (builder, "url");
349   json_builder_add_string_value (builder, ephy_web_view_get_address (view));
350   json_builder_set_member_name (builder, "id");
351   json_builder_add_int_value (builder, ephy_web_view_get_uid (view));
352   json_builder_end_object (builder);
353 
354   root = json_builder_get_root (builder);
355 
356   json = json_to_string (root, FALSE);
357 
358   script = g_strdup_printf ("pageActionOnClicked(%s);", json);
359   webkit_web_view_run_javascript_in_world (web_view,
360                                            script,
361                                            ephy_embed_shell_get_guid (EPHY_EMBED_SHELL (shell)),
362                                            NULL,
363                                            NULL,
364                                            NULL);
365 
366   return GDK_EVENT_STOP;
367 }
368 
369 static GtkWidget *
create_page_action_widget(EphyWebExtensionManager * self,EphyWebExtension * web_extension)370 create_page_action_widget (EphyWebExtensionManager *self,
371                            EphyWebExtension        *web_extension)
372 {
373   GtkWidget *image;
374   GtkWidget *event_box;
375   GtkStyleContext *context;
376 
377   /* Create new event box with page action */
378   event_box = gtk_event_box_new ();
379   image = gtk_image_new ();
380   gtk_container_add (GTK_CONTAINER (event_box), image);
381   g_signal_connect_object (event_box, "button_press_event", G_CALLBACK (page_action_clicked), web_extension, 0);
382   gtk_widget_show_all (event_box);
383 
384   context = gtk_widget_get_style_context (image);
385   gtk_style_context_add_class (context, "entry_icon");
386 
387   return g_object_ref (event_box);
388 }
389 
390 static void
ephy_web_extension_handle_background_script_message(WebKitUserContentManager * ucm,WebKitJavascriptResult * js_result,gpointer user_data)391 ephy_web_extension_handle_background_script_message (WebKitUserContentManager *ucm,
392                                                      WebKitJavascriptResult   *js_result,
393                                                      gpointer                  user_data)
394 {
395   EphyWebExtension *web_extension = EPHY_WEB_EXTENSION (user_data);
396   JSCValue *value = webkit_javascript_result_get_js_value (js_result);
397   EphyWebExtensionManager *self = ephy_shell_get_web_extension_manager (ephy_shell_get_default ());
398   WebKitWebView *web_view = WEBKIT_WEB_VIEW (ephy_web_extension_manager_get_background_web_view (self, web_extension));
399   g_autofree char *name_str = NULL;
400   g_autoptr (JSCValue) name = NULL;
401   g_autoptr (JSCValue) promise = NULL;
402   g_auto (GStrv) split = NULL;
403   GPtrArray *permissions = ephy_web_extension_get_permissions (web_extension);
404   unsigned int idx;
405 
406   if (!jsc_value_is_object (value))
407     return;
408 
409   if (!jsc_value_object_has_property (value, "promise"))
410     return;
411 
412   promise = jsc_value_object_get_property (value, "promise");
413   if (!jsc_value_is_number (promise))
414     return;
415 
416   name = jsc_value_object_get_property (value, "fn");
417   if (!name)
418     return;
419 
420   name_str = jsc_value_to_string (name);
421   LOG ("%s(): Called for %s, function %s\n", __FUNCTION__, ephy_web_extension_get_name (web_extension), name_str);
422 
423   split = g_strsplit (name_str, ".", 2);
424   if (g_strv_length (split) != 2) {
425     g_warning ("Invalid function call, aborting: %s", name_str);
426     return;
427   }
428 
429   for (idx = 0; idx < G_N_ELEMENTS (api_handlers); idx++) {
430     EphyWebExtensionApiHandler handler = api_handlers[idx];
431 
432     if (!g_ptr_array_find (permissions, split[0], NULL)) {
433       LOG ("%s(): Requested api is not part of the permissions, aborting\n", __FUNCTION__);
434       /* TODO: Permissions are not working yet */
435       /*return; */
436     }
437 
438     if (g_strcmp0 (handler.name, split[0]) == 0) {
439       g_autofree char *ret = NULL;
440       g_autofree char *script = NULL;
441       g_autoptr (JSCValue) args = jsc_value_object_get_property (value, "args");
442 
443       ret = handler.execute (web_extension, split[1], args);
444       script = g_strdup_printf ("promises[%.f].resolve(%s);", jsc_value_to_double (promise), ret ? ret : "");
445       webkit_web_view_run_javascript_in_world (web_view, script, ephy_embed_shell_get_guid (ephy_embed_shell_get_default ()), NULL, NULL, NULL);
446 
447       return;
448     }
449   }
450 
451   g_warning ("%s(): '%s' not implemented by Epiphany!", __FUNCTION__, name_str);
452 }
453 
454 static void
add_content_scripts(EphyWebExtension * web_extension,EphyWebView * web_view)455 add_content_scripts (EphyWebExtension *web_extension,
456                      EphyWebView      *web_view)
457 {
458   GList *content_scripts = ephy_web_extension_get_content_scripts (web_extension);
459   WebKitUserContentManager *ucm;
460 
461   if (!content_scripts)
462     return;
463 
464   ucm = webkit_web_view_get_user_content_manager (WEBKIT_WEB_VIEW (web_view));
465   g_signal_connect_object (ucm, "script-message-received", G_CALLBACK (ephy_web_extension_handle_background_script_message), web_extension, 0);
466   webkit_user_content_manager_register_script_message_handler_in_world (ucm, "epiphany", ephy_embed_shell_get_guid (ephy_embed_shell_get_default ()));
467 
468   for (GList *list = content_scripts; list && list->data; list = list->next) {
469     GList *js_list = ephy_web_extension_get_content_script_js (web_extension, list->data);
470 
471     for (GList *tmp_list = js_list; tmp_list && tmp_list->data; tmp_list = tmp_list->next) {
472       webkit_user_content_manager_add_script (WEBKIT_USER_CONTENT_MANAGER (ucm), tmp_list->data);
473     }
474   }
475 }
476 
477 static void
remove_content_scripts(EphyWebExtension * self,EphyWebView * web_view)478 remove_content_scripts (EphyWebExtension *self,
479                         EphyWebView      *web_view)
480 {
481   GList *content_scripts = ephy_web_extension_get_content_scripts (self);
482   WebKitUserContentManager *ucm;
483 
484   if (!content_scripts)
485     return;
486 
487   ucm = webkit_web_view_get_user_content_manager (WEBKIT_WEB_VIEW (web_view));
488 
489   for (GList *list = content_scripts; list && list->data; list = list->next) {
490     GList *js_list = ephy_web_extension_get_content_script_js (self, list->data);
491 
492     for (GList *tmp_list = js_list; tmp_list && tmp_list->data; tmp_list = tmp_list->next)
493       webkit_user_content_manager_remove_script (WEBKIT_USER_CONTENT_MANAGER (ucm), tmp_list->data);
494   }
495 
496   g_signal_handlers_disconnect_by_func (ucm, G_CALLBACK (ephy_web_extension_handle_background_script_message), self);
497 }
498 
499 static void
remove_custom_css(EphyWebExtension * self,EphyWebView * web_view)500 remove_custom_css (EphyWebExtension *self,
501                    EphyWebView      *web_view)
502 {
503   GList *custom_css = ephy_web_extension_get_custom_css_list (self);
504   GList *list;
505   WebKitUserContentManager *ucm;
506 
507   if (!custom_css)
508     return;
509 
510   ucm = webkit_web_view_get_user_content_manager (WEBKIT_WEB_VIEW (web_view));
511 
512   for (list = custom_css; list && list->data; list = list->next)
513     webkit_user_content_manager_remove_style_sheet (WEBKIT_USER_CONTENT_MANAGER (ucm), ephy_web_extension_custom_css_style (self, list->data));
514 }
515 
516 static void
update_translations(EphyWebExtension * web_extension)517 update_translations (EphyWebExtension *web_extension)
518 {
519   /* TODO: Use current locale and fallback to default web_extension locale if necessary */
520   g_autofree char *path = g_strdup_printf ("_locales/%s/messages.json", "en");
521   g_autofree char *data = NULL;
522   gint length = 0;
523 
524   data = ephy_web_extension_get_resource_as_string (web_extension, path);
525   if (data)
526     length = strlen (data);
527 
528   webkit_web_context_send_message_to_all_extensions (ephy_embed_shell_get_web_context (ephy_embed_shell_get_default ()),
529                                                      webkit_user_message_new ("WebExtension.Add",
530                                                                               g_variant_new ("(sst)", ephy_web_extension_get_name (web_extension), data ? (char *)data : "", length)));
531 }
532 
533 static void
ephy_web_extension_manager_add_web_extension_to_webview(EphyWebExtensionManager * self,EphyWebExtension * web_extension,EphyWindow * window,EphyWebView * web_view)534 ephy_web_extension_manager_add_web_extension_to_webview (EphyWebExtensionManager *self,
535                                                          EphyWebExtension        *web_extension,
536                                                          EphyWindow              *window,
537                                                          EphyWebView             *web_view)
538 {
539   GtkWidget *title_widget = GTK_WIDGET (ephy_header_bar_get_title_widget (EPHY_HEADER_BAR (ephy_window_get_header_bar (window))));
540   EphyLocationEntry *lentry = NULL;
541 
542   if (EPHY_IS_LOCATION_ENTRY (title_widget)) {
543     lentry = EPHY_LOCATION_ENTRY (title_widget);
544 
545     if (lentry && ephy_web_extension_has_page_action (web_extension)) {
546       GtkWidget *page_action = create_page_action_widget (self, web_extension);
547       GHashTable *table;
548 
549       table = g_hash_table_lookup (self->page_action_map, web_extension);
550       if (!table) {
551         table = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)gtk_widget_destroy);
552         g_hash_table_insert (self->page_action_map, web_extension, table);
553       }
554 
555       g_hash_table_insert (table, web_view, g_steal_pointer (&page_action));
556     }
557   }
558 
559   update_translations (web_extension);
560   add_content_scripts (web_extension, web_view);
561 }
562 
563 static void
page_attached_cb(HdyTabView * tab_view,HdyTabPage * page,gint position,gpointer user_data)564 page_attached_cb (HdyTabView *tab_view,
565                   HdyTabPage *page,
566                   gint        position,
567                   gpointer    user_data)
568 {
569   EphyWebExtension *web_extension = EPHY_WEB_EXTENSION (user_data);
570   GtkWidget *child = hdy_tab_page_get_child (page);
571   EphyWebView *web_view = ephy_embed_get_web_view (EPHY_EMBED (child));
572   EphyWindow *window = EPHY_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (tab_view)));
573   EphyWebExtensionManager *self = ephy_shell_get_web_extension_manager (ephy_shell_get_default ());
574 
575   ephy_web_extension_manager_add_web_extension_to_webview (self, web_extension, window, web_view);
576   ephy_web_extension_manager_update_location_entry (self, window);
577 }
578 
579 static void
web_extension_cb(WebKitURISchemeRequest * request,gpointer user_data)580 web_extension_cb (WebKitURISchemeRequest *request,
581                   gpointer                user_data)
582 {
583   EphyWebExtension *web_extension = EPHY_WEB_EXTENSION (user_data);
584   const char *path;
585   const unsigned char *data;
586   gsize length;
587   g_autoptr (GInputStream) stream = NULL;
588 
589   path = webkit_uri_scheme_request_get_path (request);
590 
591   data = ephy_web_extension_get_resource (web_extension, path + 1, &length);
592   if (!data)
593     return;
594 
595   stream = g_memory_input_stream_new_from_data (data, length, NULL);
596   webkit_uri_scheme_request_finish (request, stream, length, NULL);
597 }
598 
599 static void
init_web_extension_api(WebKitWebContext * web_context,EphyWebExtension * web_extension)600 init_web_extension_api (WebKitWebContext *web_context,
601                         EphyWebExtension *web_extension)
602 {
603   g_autoptr (GVariant) user_data = NULL;
604 
605 #if DEVELOPER_MODE
606   webkit_web_context_set_web_extensions_directory (web_context, BUILD_ROOT "/embed/web-process-extension");
607 #else
608   webkit_web_context_set_web_extensions_directory (web_context, EPHY_WEB_PROCESS_EXTENSIONS_DIR);
609 #endif
610 
611   user_data = g_variant_new ("(smsbb)",
612                              "",
613                              ephy_profile_dir_is_default () ? NULL : ephy_profile_dir (),
614                              FALSE,
615                              FALSE);
616   webkit_web_context_set_web_extensions_initialization_user_data (web_context, g_steal_pointer (&user_data));
617 }
618 
619 static GtkWidget *
create_web_extensions_webview(EphyWebExtension * web_extension,gboolean custom_web_context)620 create_web_extensions_webview (EphyWebExtension *web_extension,
621                                gboolean          custom_web_context)
622 {
623   WebKitUserContentManager *ucm;
624   WebKitWebContext *web_context;
625   WebKitSettings *settings;
626   GtkWidget *web_view;
627 
628   /* Create an own ucm so new scripts/css are only applied to this web_view */
629   ucm = webkit_user_content_manager_new ();
630   g_signal_connect_object (ucm, "script-message-received", G_CALLBACK (ephy_web_extension_handle_background_script_message), web_extension, 0);
631 
632   if (!custom_web_context) {
633     /* Get webcontext and register web_extension scheme */
634     webkit_user_content_manager_register_script_message_handler_in_world (ucm,
635                                                                           "epiphany",
636                                                                           ephy_embed_shell_get_guid (ephy_embed_shell_get_default ()));
637     web_context = ephy_embed_shell_get_web_context (ephy_embed_shell_get_default ());
638     webkit_web_context_register_uri_scheme (web_context, "ephy-webextension", web_extension_cb, web_extension, NULL);
639     webkit_security_manager_register_uri_scheme_as_secure (webkit_web_context_get_security_manager (web_context),
640                                                            "ephy-webextension");
641     web_view = ephy_web_view_new_with_user_content_manager (ucm);
642   } else {
643     webkit_user_content_manager_register_script_message_handler (ucm, "epiphany");
644     web_context = webkit_web_context_new ();
645     webkit_web_context_register_uri_scheme (web_context, "ephy-webextension", web_extension_cb, web_extension, NULL);
646     g_signal_connect_object (web_context, "initialize-web_extensions", G_CALLBACK (init_web_extension_api), web_extension, 0);
647     webkit_security_manager_register_uri_scheme_as_secure (webkit_web_context_get_security_manager (web_context),
648                                                            "ephy-webextension");
649     web_view = g_object_new (EPHY_TYPE_WEB_VIEW,
650                              "web-context", web_context,
651                              "user-content-manager", ucm,
652                              "settings", ephy_embed_prefs_get_settings (),
653                              NULL);
654   }
655 
656   settings = webkit_web_view_get_settings (WEBKIT_WEB_VIEW (web_view));
657   webkit_settings_set_enable_write_console_messages_to_stdout (settings, TRUE);
658 
659   update_translations (web_extension);
660 
661   return web_view;
662 }
663 
664 static GtkWidget *
create_browser_popup(EphyWebExtension * web_extension)665 create_browser_popup (EphyWebExtension *web_extension)
666 {
667   GtkWidget *web_view;
668   GtkWidget *popover;
669   g_autofree char *data = NULL;
670   g_autofree char *base_uri = NULL;
671   g_autofree char *dir_name = NULL;
672   const char *popup;
673 
674   popover = gtk_popover_new (NULL);
675 
676   web_view = create_web_extensions_webview (web_extension, TRUE);
677 
678   gtk_widget_set_hexpand (web_view, TRUE);
679   gtk_widget_set_vexpand (web_view, TRUE);
680 
681   popup = ephy_web_extension_get_browser_popup (web_extension);
682   dir_name = g_path_get_dirname (popup);
683   base_uri = g_strdup_printf ("ephy-webextension:///%s/", dir_name);
684   data = ephy_web_extension_get_resource_as_string (web_extension, popup);
685   webkit_web_view_load_html (WEBKIT_WEB_VIEW (web_view), (char *)data, base_uri);
686   gtk_container_add (GTK_CONTAINER (popover), web_view);
687   gtk_widget_show_all (web_view);
688 
689   return popover;
690 }
691 
692 static gboolean
on_browser_action_clicked(GtkWidget * event_box,gpointer user_data)693 on_browser_action_clicked (GtkWidget *event_box,
694                            gpointer   user_data)
695 {
696   EphyShell *shell = ephy_shell_get_default ();
697   EphyWebExtension *web_extension = EPHY_WEB_EXTENSION (user_data);
698   EphyWebExtensionManager *self = ephy_shell_get_web_extension_manager (ephy_shell_get_default ());
699   g_autofree char *script = NULL;
700   WebKitWebView *web_view = NULL;
701   gboolean own_web_view = !!ephy_web_extension_background_web_view_get_page (web_extension);
702 
703   if (!own_web_view)
704     web_view = WEBKIT_WEB_VIEW (ephy_web_extension_manager_get_background_web_view (self, web_extension));
705   else
706     web_view = WEBKIT_WEB_VIEW (ephy_shell_get_active_web_view (shell));
707 
708   script = g_strdup_printf ("browserActionClicked();");
709 
710   webkit_web_view_run_javascript_in_world (web_view,
711                                            script,
712                                            ephy_embed_shell_get_guid (ephy_embed_shell_get_default ()),
713                                            NULL,
714                                            NULL,
715                                            NULL);
716 
717   return GDK_EVENT_STOP;
718 }
719 
720 
721 GtkWidget *
create_browser_action(EphyWebExtension * web_extension)722 create_browser_action (EphyWebExtension *web_extension)
723 {
724   GtkWidget *button;
725   GtkWidget *image;
726   GtkWidget *popover;
727 
728   if (ephy_web_extension_get_browser_popup (web_extension)) {
729     button = gtk_menu_button_new ();
730     image = gtk_image_new_from_pixbuf (ephy_web_extension_browser_action_get_icon (web_extension, 16));
731     popover = create_browser_popup (web_extension);
732     gtk_menu_button_set_popover (GTK_MENU_BUTTON (button), popover);
733 
734     gtk_button_set_image (GTK_BUTTON (button), image);
735     gtk_widget_set_visible (button, TRUE);
736   } else {
737     GdkPixbuf *pixbuf = ephy_web_extension_browser_action_get_icon (web_extension, 16);
738 
739     button = gtk_button_new ();
740 
741     if (pixbuf)
742       image = gtk_image_new_from_pixbuf (pixbuf);
743     else
744       image = gtk_image_new_from_icon_name ("application-x-addon-symbolic", GTK_ICON_SIZE_BUTTON);
745 
746     g_signal_connect_object (button, "clicked", G_CALLBACK (on_browser_action_clicked), web_extension, 0);
747     gtk_button_set_image (GTK_BUTTON (button), image);
748     gtk_widget_set_visible (button, TRUE);
749   }
750 
751   return button;
752 }
753 
754 void
ephy_web_extension_manager_add_web_extension_to_window(EphyWebExtensionManager * self,EphyWebExtension * web_extension,EphyWindow * window)755 ephy_web_extension_manager_add_web_extension_to_window (EphyWebExtensionManager *self,
756                                                         EphyWebExtension        *web_extension,
757                                                         EphyWindow              *window)
758 {
759   EphyTabView *tab_view = ephy_window_get_tab_view (EPHY_WINDOW (window));
760   HdyTabView *view = ephy_tab_view_get_tab_view (tab_view);
761 
762   if (!ephy_web_extension_manager_is_active (self, web_extension))
763     return;
764 
765   /* Add page actions and add content script */
766   for (int i = 0; i < ephy_tab_view_get_n_pages (tab_view); i++) {
767     GtkWidget *page = ephy_tab_view_get_nth_page (tab_view, i);
768     EphyWebView *web_view = ephy_embed_get_web_view (EPHY_EMBED (page));
769 
770     ephy_web_extension_manager_add_web_extension_to_webview (self, web_extension, window, web_view);
771   }
772 
773   if (ephy_web_extension_has_browser_action (web_extension)) {
774     GtkWidget *browser_action_widget = create_browser_action (web_extension);
775     ephy_header_bar_add_browser_action (EPHY_HEADER_BAR (ephy_window_get_header_bar (window)), browser_action_widget);
776     g_hash_table_insert (self->browser_action_map, web_extension, browser_action_widget);
777   }
778 
779   ephy_web_extension_manager_update_location_entry (self, window);
780   g_signal_connect_object (view, "page-attached", G_CALLBACK (page_attached_cb), web_extension, 0);
781 }
782 
783 static gboolean
remove_page_action(gpointer key,gpointer value,gpointer user_data)784 remove_page_action (gpointer key,
785                     gpointer value,
786                     gpointer user_data)
787 {
788   return key == user_data;
789 }
790 
791 void
ephy_web_extension_manager_remove_web_extension_from_webview(EphyWebExtensionManager * self,EphyWebExtension * web_extension,EphyWindow * window,EphyWebView * web_view)792 ephy_web_extension_manager_remove_web_extension_from_webview (EphyWebExtensionManager *self,
793                                                               EphyWebExtension        *web_extension,
794                                                               EphyWindow              *window,
795                                                               EphyWebView             *web_view)
796 {
797   GtkWidget *title_widget = GTK_WIDGET (ephy_header_bar_get_title_widget (EPHY_HEADER_BAR (ephy_window_get_header_bar (window))));
798   EphyLocationEntry *lentry = NULL;
799   GHashTableIter iter;
800   gpointer key;
801   GHashTable *table;
802 
803   if (EPHY_IS_LOCATION_ENTRY (title_widget))
804     lentry = EPHY_LOCATION_ENTRY (title_widget);
805 
806   g_hash_table_iter_init (&iter, self->page_action_map);
807   while (g_hash_table_iter_next (&iter, &key, (gpointer) & table)) {
808     if (key != web_extension)
809       continue;
810 
811     g_hash_table_foreach_remove (table, remove_page_action, web_view);
812   }
813 
814   if (lentry)
815     ephy_location_entry_page_action_clear (lentry);
816 
817   remove_content_scripts (web_extension, web_view);
818   remove_custom_css (web_extension, web_view);
819 }
820 
821 void
ephy_web_extension_manager_remove_web_extension_from_window(EphyWebExtensionManager * self,EphyWebExtension * web_extension,EphyWindow * window)822 ephy_web_extension_manager_remove_web_extension_from_window (EphyWebExtensionManager *self,
823                                                              EphyWebExtension        *web_extension,
824                                                              EphyWindow              *window)
825 {
826   EphyTabView *tab_view = ephy_window_get_tab_view (EPHY_WINDOW (window));
827   HdyTabView *view = ephy_tab_view_get_tab_view (tab_view);
828   GtkWidget *browser_action_widget;
829 
830   if (ephy_web_extension_manager_is_active (self, web_extension))
831     return;
832 
833   for (int i = 0; i < ephy_tab_view_get_n_pages (tab_view); i++) {
834     GtkWidget *page = ephy_tab_view_get_nth_page (tab_view, i);
835     EphyWebView *web_view = ephy_embed_get_web_view (EPHY_EMBED (page));
836 
837     ephy_web_extension_manager_remove_web_extension_from_webview (self, web_extension, window, web_view);
838   }
839 
840   browser_action_widget = g_hash_table_lookup (self->browser_action_map, web_extension);
841   if (browser_action_widget) {
842     g_hash_table_remove (self->browser_action_map, web_extension);
843   }
844 
845   ephy_web_extension_manager_update_location_entry (self, window);
846 
847   g_signal_handlers_disconnect_by_data (view, web_extension);
848 }
849 
850 gboolean
ephy_web_extension_manager_is_active(EphyWebExtensionManager * self,EphyWebExtension * web_extension)851 ephy_web_extension_manager_is_active (EphyWebExtensionManager *self,
852                                       EphyWebExtension        *web_extension)
853 {
854   g_auto (GStrv) web_extensions_active = g_settings_get_strv (EPHY_SETTINGS_WEB, EPHY_PREFS_WEB_WEBEXTENSIONS_ACTIVE);
855 
856   return g_strv_contains ((const char * const *)web_extensions_active, ephy_web_extension_get_name (web_extension));
857 }
858 
859 static void
run_background_script(EphyWebExtensionManager * self,EphyWebExtension * web_extension)860 run_background_script (EphyWebExtensionManager *self,
861                        EphyWebExtension        *web_extension)
862 {
863   WebKitUserContentManager *ucm;
864   GtkWidget *background;
865   g_autofree char *base_uri = NULL;
866   const char *page;
867 
868   if (!ephy_web_extension_has_background_web_view (web_extension) || ephy_web_extension_manager_get_background_web_view (self, web_extension))
869     return;
870 
871   page = ephy_web_extension_background_web_view_get_page (web_extension);
872 
873   /* Create new background web_view */
874   background = create_web_extensions_webview (web_extension, page != NULL);
875   ephy_web_extension_manager_set_background_web_view (self, web_extension, EPHY_WEB_VIEW (background));
876 
877   if (page) {
878     g_autofree char *data = ephy_web_extension_get_resource_as_string (web_extension, page);
879 
880     base_uri = g_strdup_printf ("ephy-webextension://%s/%s/", ephy_web_extension_get_guid (web_extension), g_path_get_dirname (page));
881     webkit_web_view_load_html (WEBKIT_WEB_VIEW (background), (char *)data, base_uri);
882   } else {
883     GPtrArray *scripts = ephy_web_extension_background_web_view_get_scripts (web_extension);
884 
885     ucm = webkit_web_view_get_user_content_manager (WEBKIT_WEB_VIEW (background));
886 
887     base_uri = g_strdup_printf ("ephy-webextension://%s/", ephy_web_extension_get_guid (web_extension));
888     for (unsigned int i = 0; i < scripts->len; i++) {
889       const char *script_file = g_ptr_array_index (scripts, i);
890       g_autofree char *data = NULL;
891       WebKitUserScript *user_script;
892 
893       if (!script_file)
894         continue;
895 
896       data = ephy_web_extension_get_resource_as_string (web_extension, script_file);
897       user_script = webkit_user_script_new_for_world (data,
898                                                       WEBKIT_USER_CONTENT_INJECT_TOP_FRAME,
899                                                       WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_END,
900                                                       ephy_embed_shell_get_guid (ephy_embed_shell_get_default ()),
901                                                       NULL,
902                                                       NULL);
903 
904       webkit_user_content_manager_add_script (ucm, user_script);
905     }
906     webkit_web_view_load_html (WEBKIT_WEB_VIEW (background), "<body></body>", base_uri);
907   }
908 }
909 
910 static GPtrArray *
strv_to_ptr_array(char ** strv)911 strv_to_ptr_array (char **strv)
912 {
913   GPtrArray *array = g_ptr_array_new ();
914 
915   for (char **str = strv; *str; ++str) {
916     g_ptr_array_add (array, g_strdup (*str));
917   }
918 
919   return array;
920 }
921 
922 static gboolean
extension_equal(gconstpointer a,gconstpointer b)923 extension_equal (gconstpointer a,
924                  gconstpointer b)
925 {
926   return g_strcmp0 (a, b) == 0;
927 }
928 
929 void
ephy_web_extension_manager_set_active(EphyWebExtensionManager * self,EphyWebExtension * web_extension,gboolean active)930 ephy_web_extension_manager_set_active (EphyWebExtensionManager *self,
931                                        EphyWebExtension        *web_extension,
932                                        gboolean                 active)
933 {
934   g_auto (GStrv) web_extensions_active = g_settings_get_strv (EPHY_SETTINGS_WEB, EPHY_PREFS_WEB_WEBEXTENSIONS_ACTIVE);
935   EphyShell *shell = ephy_shell_get_default ();
936   GList *windows = gtk_application_get_windows (GTK_APPLICATION (shell));
937   GList *list;
938   g_autoptr (GPtrArray) array = strv_to_ptr_array (web_extensions_active);
939   const char *name = ephy_web_extension_get_name (web_extension);
940   gboolean found;
941   guint idx;
942 
943   /* Update settings */
944   found = g_ptr_array_find_with_equal_func (array, name, extension_equal, &idx);
945   if (active) {
946     if (!found)
947       g_ptr_array_add (array, (gpointer)name);
948   } else {
949     if (found)
950       g_ptr_array_remove_index (array, idx);
951   }
952 
953   g_ptr_array_add (array, NULL);
954 
955   g_settings_set_strv (EPHY_SETTINGS_WEB, EPHY_PREFS_WEB_WEBEXTENSIONS_ACTIVE, (const gchar * const *)array->pdata);
956 
957   /* Update window web_extension state */
958   for (list = windows; list && list->data; list = list->next) {
959     EphyWindow *window = EPHY_WINDOW (list->data);
960 
961     if (active)
962       ephy_web_extension_manager_add_web_extension_to_window (self, web_extension, window);
963     else
964       ephy_web_extension_manager_remove_web_extension_from_window (self, web_extension, window);
965   }
966 
967   if (active) {
968     if (ephy_web_extension_has_background_web_view (web_extension))
969       run_background_script (self, web_extension);
970   }
971 }
972 
973 GtkWidget *
ephy_web_extension_manager_get_page_action(EphyWebExtensionManager * self,EphyWebExtension * web_extension,EphyWebView * web_view)974 ephy_web_extension_manager_get_page_action (EphyWebExtensionManager *self,
975                                             EphyWebExtension        *web_extension,
976                                             EphyWebView             *web_view)
977 {
978   GHashTable *table;
979   GtkWidget *ret = NULL;
980 
981   table = g_hash_table_lookup (self->page_action_map, web_extension);
982   if (table)
983     ret = g_hash_table_lookup (table, web_view);
984 
985   return ret;
986 }
987