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