summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJan-Michael Brummer <jan.brummer@tabos.org>2019-05-02 09:53:25 +0200
committerJan-Michael Brummer <jan.brummer@tabos.org>2021-01-17 13:55:36 +0100
commit5410fec78bc699b593d327e6a98b105f6cef7cd5 (patch)
tree7e7f379bc6e9677d0ea8015c5c97996d9d49f920
parentda8757af88925c803be2201784a6821410150608 (diff)
downloadepiphany-5410fec78bc699b593d327e6a98b105f6cef7cd5.tar.gz
Add initial WebExtension support
-rw-r--r--data/org.gnome.Epiphany.desktop.in.in2
-rw-r--r--data/org.gnome.epiphany.gschema.xml10
-rw-r--r--embed/ephy-web-view.c30
-rw-r--r--embed/ephy-web-view.h4
-rw-r--r--embed/web-process-extension/ephy-web-process-extension-main.c4
-rw-r--r--embed/web-process-extension/ephy-web-process-extension.c37
-rw-r--r--embed/web-process-extension/ephy-web-process-extension.h4
-rw-r--r--embed/web-process-extension/ephy-webextension-api.c170
-rw-r--r--embed/web-process-extension/ephy-webextension-api.h37
-rw-r--r--embed/web-process-extension/meson.build1
-rw-r--r--embed/web-process-extension/resources/epiphany-web-process-extension.gresource.xml1
-rw-r--r--embed/web-process-extension/resources/js/webextensions.js115
-rw-r--r--lib/ephy-file-helpers.c50
-rw-r--r--lib/ephy-file-helpers.h3
-rw-r--r--lib/ephy-prefs.h3
-rw-r--r--lib/ephy-string.c29
-rw-r--r--lib/ephy-string.h2
-rw-r--r--lib/widgets/ephy-location-entry.c67
-rw-r--r--lib/widgets/ephy-location-entry.h4
-rw-r--r--meson.build5
-rw-r--r--src/ephy-action-bar-end.c11
-rw-r--r--src/ephy-action-bar-end.h3
-rw-r--r--src/ephy-header-bar.c8
-rw-r--r--src/ephy-header-bar.h3
-rw-r--r--src/ephy-shell.c60
-rw-r--r--src/ephy-shell.h8
-rw-r--r--src/ephy-web-extension-dialog.c291
-rw-r--r--src/ephy-web-extension-dialog.h35
-rw-r--r--src/ephy-window.c16
-rw-r--r--src/meson.build8
-rw-r--r--src/resources/epiphany.gresource.xml1
-rw-r--r--src/resources/gtk/action-bar-end.ui12
-rw-r--r--src/resources/gtk/page-menu-popover.ui8
-rw-r--r--src/resources/gtk/web-extensions-dialog.ui65
-rw-r--r--src/webextension/README.md59
-rw-r--r--src/webextension/api/notifications.c73
-rw-r--r--src/webextension/api/notifications.h32
-rw-r--r--src/webextension/api/pageaction.c166
-rw-r--r--src/webextension/api/pageaction.h32
-rw-r--r--src/webextension/api/runtime.c122
-rw-r--r--src/webextension/api/runtime.h32
-rw-r--r--src/webextension/api/tabs.c203
-rw-r--r--src/webextension/api/tabs.h34
-rw-r--r--src/webextension/ephy-web-extension-manager.c968
-rw-r--r--src/webextension/ephy-web-extension-manager.h72
-rw-r--r--src/webextension/ephy-web-extension.c1203
-rw-r--r--src/webextension/ephy-web-extension.h129
-rw-r--r--src/webextension/meson.build8
-rw-r--r--src/window-commands.c14
-rw-r--r--src/window-commands.h3
50 files changed, 4210 insertions, 47 deletions
diff --git a/data/org.gnome.Epiphany.desktop.in.in b/data/org.gnome.Epiphany.desktop.in.in
index c41ea8947..26cf2dcf9 100644
--- a/data/org.gnome.Epiphany.desktop.in.in
+++ b/data/org.gnome.Epiphany.desktop.in.in
@@ -11,7 +11,7 @@ Type=Application
# Translators: Do NOT translate or transliterate this text (this is an icon file name)!
Icon=@icon@
Categories=Network;GNOME;GTK;WebBrowser;
-MimeType=text/html;text/xml;application/xhtml+xml;x-scheme-handler/http;x-scheme-handler/https;multipart/related;application/x-mimearchive;message/rfc822;
+MimeType=text/html;text/xml;application/xhtml+xml;x-scheme-handler/http;x-scheme-handler/https;multipart/related;application/x-mimearchive;message/rfc822;application/x-xpinstall;
Actions=new-window;Incognito;
# Translators: Do NOT translate or transliterate this text (these are enum types)!
X-Purism-FormFactor=Workstation;Mobile;
diff --git a/data/org.gnome.epiphany.gschema.xml b/data/org.gnome.epiphany.gschema.xml
index 99800b3f2..35ed1993c 100644
--- a/data/org.gnome.epiphany.gschema.xml
+++ b/data/org.gnome.epiphany.gschema.xml
@@ -245,6 +245,16 @@
<summary>Enable immediately switch to new open tab</summary>
<description>Whether to automatically switch to a new open tab.</description>
</key>
+ <key type="b" name="enable-webextensions">
+ <default>false</default>
+ <summary>Enable WebExtensions</summary>
+ <description>Whether to enable WebExtensions. WebExtensions is a cross-browser system for extensions.</description>
+ </key>
+ <key type="as" name="webextensions-active">
+ <default>[]</default>
+ <summary>Active WebExtensions</summary>
+ <description>Indicates which WebExtensions are set to active.</description>
+ </key>
</schema>
<schema id="org.gnome.Epiphany.webapp">
<key type="as" name="additional-urls">
diff --git a/embed/ephy-web-view.c b/embed/ephy-web-view.c
index 1b5f04c66..814d05989 100644
--- a/embed/ephy-web-view.c
+++ b/embed/ephy-web-view.c
@@ -67,6 +67,8 @@
#define EPHY_PAGE_TEMPLATE_ERROR "/org/gnome/epiphany/page-templates/error.html"
#define EPHY_PAGE_TEMPLATE_ERROR_CSS "/org/gnome/epiphany/page-templates/error.css"
+static guint64 web_view_uid = 1;
+
struct _EphyWebView {
WebKitWebView parent_instance;
@@ -125,6 +127,8 @@ struct _EphyWebView {
char *tls_error_failing_uri;
EphyWebViewErrorPage error_page;
+
+ guint64 uid;
};
enum {
@@ -1427,6 +1431,11 @@ update_security_status_for_committed_load (EphyWebView *view,
if (view->loading_error_page)
return;
+ if (g_str_has_prefix (uri, "webextension://")) {
+ /* Hidden WebExtension webview, ignoring */
+ return;
+ }
+
toplevel = gtk_widget_get_toplevel (GTK_WIDGET (view));
if (EPHY_IS_EMBED_CONTAINER (toplevel))
embed = EPHY_GET_EMBED_FROM_EPHY_WEB_VIEW (view);
@@ -2725,6 +2734,19 @@ scale_factor_changed_cb (EphyWebView *web_view,
_ephy_web_view_update_icon (web_view);
}
+GtkWidget *
+ephy_web_view_new_with_user_content_manager (WebKitUserContentManager *ucm)
+{
+ EphyEmbedShell *shell = ephy_embed_shell_get_default ();
+
+ return g_object_new (EPHY_TYPE_WEB_VIEW,
+ "web-context", ephy_embed_shell_get_web_context (shell),
+ "user-content-manager", ucm,
+ "settings", ephy_embed_prefs_get_settings (),
+ "is-controlled-by-automation", ephy_embed_shell_get_mode (shell) == EPHY_EMBED_SHELL_MODE_AUTOMATION,
+ NULL);
+}
+
/**
* ephy_web_view_load_request:
* @view: the #EphyWebView in which to load the request
@@ -3771,6 +3793,8 @@ ephy_web_view_init (EphyWebView *web_view)
shell = ephy_embed_shell_get_default ();
+ web_view->uid = web_view_uid++;
+
web_view->is_blank = TRUE;
web_view->ever_committed = FALSE;
web_view->document_type = EPHY_WEB_VIEW_DOCUMENT_HTML;
@@ -4120,3 +4144,9 @@ ephy_web_view_new_with_related_view (WebKitWebView *related_view)
"settings", ephy_embed_prefs_get_settings (),
NULL);
}
+
+guint64
+ephy_web_view_get_uid (EphyWebView *web_view)
+{
+ return web_view->uid;
+}
diff --git a/embed/ephy-web-view.h b/embed/ephy-web-view.h
index 45a58b2d3..0153b4b2f 100644
--- a/embed/ephy-web-view.h
+++ b/embed/ephy-web-view.h
@@ -182,4 +182,8 @@ void ephy_web_view_show_auth_form_save_request (EphyWebVie
gpointer response_data,
GDestroyNotify response_destroy);
+GtkWidget *ephy_web_view_new_with_user_content_manager (WebKitUserContentManager *ucm);
+
+guint64 ephy_web_view_get_uid (EphyWebView *web_view);
+
G_END_DECLS
diff --git a/embed/web-process-extension/ephy-web-process-extension-main.c b/embed/web-process-extension/ephy-web-process-extension-main.c
index 3a155a13c..8f3298198 100644
--- a/embed/web-process-extension/ephy-web-process-extension-main.c
+++ b/embed/web-process-extension/ephy-web-process-extension-main.c
@@ -63,8 +63,10 @@ webkit_web_extension_initialize_with_user_data (WebKitWebExtension *webkit_exten
static void __attribute__((destructor))
ephy_web_process_extension_shutdown (void)
{
- if (extension)
+ if (extension) {
+ ephy_web_process_extension_deinitialize (extension);
g_object_unref (extension);
+ }
ephy_settings_shutdown ();
ephy_file_helpers_shutdown ();
diff --git a/embed/web-process-extension/ephy-web-process-extension.c b/embed/web-process-extension/ephy-web-process-extension.c
index 54392025a..de1cd6302 100644
--- a/embed/web-process-extension/ephy-web-process-extension.c
+++ b/embed/web-process-extension/ephy-web-process-extension.c
@@ -20,6 +20,7 @@
#include "config.h"
#include "ephy-web-process-extension.h"
+#include "ephy-webextension-api.h"
#include "ephy-debug.h"
#include "ephy-file-helpers.h"
@@ -56,10 +57,17 @@ struct _EphyWebProcessExtension {
gboolean is_private_profile;
GHashTable *frames_map;
+ GHashTable *translation_table;
};
G_DEFINE_TYPE (EphyWebProcessExtension, ephy_web_process_extension, G_TYPE_OBJECT)
+GHashTable *
+ephy_web_process_extension_get_translations (EphyWebProcessExtension *extension)
+{
+ return extension->translation_table;
+}
+
static void
web_page_will_submit_form (WebKitWebPage *web_page,
WebKitDOMHTMLFormElement *dom_form,
@@ -326,6 +334,18 @@ ephy_web_process_extension_user_message_received_cb (EphyWebProcessExtension *ex
return;
g_variant_get (parameters, "b", &extension->should_remember_passwords);
+ } else if (g_strcmp0 (name, "WebExtension.Add") == 0) {
+ GVariant *parameters;
+ const char *name;
+ const char *data;
+ guint64 length;
+
+ parameters = webkit_user_message_get_parameters (message);
+ if (!parameters)
+ return;
+
+ g_variant_get (parameters, "(&s&st)", &name, &data, &length);
+ webextensions_add_translation (extension, name, data, length);
}
}
@@ -645,6 +665,8 @@ window_object_cleared_cb (WebKitScriptWorld *world,
js_context = webkit_frame_get_js_context_for_script_world (frame, world);
jsc_context_push_exception_handler (js_context, (JSCExceptionHandler)js_exception_handler, NULL, NULL);
+ set_up_webextensions (extension, page, js_context);
+
bytes = g_resources_lookup_data ("/org/gnome/epiphany-web-process-extension/js/ephy.js", G_RESOURCE_LOOKUP_FLAGS_NONE, NULL);
data = g_bytes_get_data (bytes, &data_size);
result = jsc_context_evaluate_with_source_uri (js_context, data, data_size, "resource:///org/gnome/epiphany-web-process-extension/js/ephy.js", 1);
@@ -771,7 +793,12 @@ ephy_web_process_extension_initialize (EphyWebProcessExtension *extension,
extension->initialized = TRUE;
- extension->script_world = webkit_script_world_new_with_name (guid);
+ /* Note: An empty guid is used ONLY for WebExtensions which do have an own initialization function */
+ if (strlen (guid) > 0)
+ extension->script_world = webkit_script_world_new_with_name (guid);
+ else
+ extension->script_world = webkit_script_world_get_default ();
+
g_signal_connect (extension->script_world,
"window-object-cleared",
G_CALLBACK (window_object_cleared_cb),
@@ -793,4 +820,12 @@ ephy_web_process_extension_initialize (EphyWebProcessExtension *extension,
extension->frames_map = g_hash_table_new_full (g_int64_hash, g_int64_equal,
g_free, NULL);
+
+ extension->translation_table = g_hash_table_new (g_str_hash, NULL);
+}
+
+void
+ephy_web_process_extension_deinitialize (EphyWebProcessExtension *extension)
+{
+ g_clear_pointer (&extension->translation_table, g_hash_table_destroy);
}
diff --git a/embed/web-process-extension/ephy-web-process-extension.h b/embed/web-process-extension/ephy-web-process-extension.h
index 45467dcf2..faa577c6f 100644
--- a/embed/web-process-extension/ephy-web-process-extension.h
+++ b/embed/web-process-extension/ephy-web-process-extension.h
@@ -36,4 +36,8 @@ void ephy_web_process_extension_initialize (EphyWebProcessEx
gboolean should_remember_passwords,
gboolean is_private_profile);
+void ephy_web_process_extension_deinitialize (EphyWebProcessExtension *extension);
+
+GHashTable *ephy_web_process_extension_get_translations (EphyWebProcessExtension *extension);
+
G_END_DECLS
diff --git a/embed/web-process-extension/ephy-webextension-api.c b/embed/web-process-extension/ephy-webextension-api.c
new file mode 100644
index 000000000..281cc4b29
--- /dev/null
+++ b/embed/web-process-extension/ephy-webextension-api.c
@@ -0,0 +1,170 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Copyright © 2020 Jan-Michael Brummer <jan.brummer@tabos.org>
+ *
+ * This file is part of Epiphany.
+ *
+ * Epiphany is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Epiphany is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Epiphany. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+#include "ephy-web-process-extension.h"
+
+#include <locale.h>
+#include <json-glib/json-glib.h>
+#include <webkit2/webkit-web-extension.h>
+#include <JavaScriptCore/JavaScript.h>
+
+static char *
+js_getmessage (const char *message,
+ gpointer user_data)
+{
+ EphyWebProcessExtension *extension = EPHY_WEB_PROCESS_EXTENSION (user_data);
+ GHashTable *translations = ephy_web_process_extension_get_translations (extension);
+ JsonObject *translation = NULL;
+ g_autoptr (JsonObject) name = NULL;
+ GList *list = NULL;
+
+ if (!extension)
+ return g_strdup (message);
+
+ list = g_hash_table_get_values (translations);
+ if (list && list->data)
+ translation = list->data;
+
+ if (!translation) {
+ return g_strdup (message);
+ }
+
+ name = json_object_get_object_member (translation, message);
+ if (name) {
+ const char *trans = json_object_get_string_member (name, "message");
+ return g_strdup (trans);
+ }
+
+ return g_strdup (message);
+}
+
+static char *
+js_getuilanguage (void)
+{
+ char *locale = setlocale (LC_MESSAGES, NULL);
+
+ if (locale) {
+ locale[2] = '\0';
+
+ return g_strdup (locale);
+ }
+
+ return g_strdup ("en");
+}
+
+static char *
+js_geturl (const char *path,
+ gpointer user_data)
+{
+ return g_strdup_printf ("webextension:///%s", path);
+}
+
+void
+set_up_webextensions (EphyWebProcessExtension *extension,
+ WebKitWebPage *page,
+ JSCContext *js_context)
+{
+ g_autoptr (JSCValue) js_browser = NULL;
+ g_autoptr (JSCValue) js_i18n = NULL;
+ g_autoptr (JSCValue) js_extension = NULL;
+ g_autoptr (JSCValue) js_function = NULL;
+ g_autoptr (GBytes) bytes = NULL;
+ g_autoptr (JSCValue) result = NULL;
+ const char *data;
+ gsize data_size;
+ static gboolean setup = FALSE;
+
+ if (setup)
+ return;
+
+ setup = TRUE;
+
+ js_browser = jsc_value_new_object (js_context, NULL, NULL);
+ jsc_context_set_value (js_context, "browser", js_browser);
+
+ /* i18n */
+ js_i18n = jsc_value_new_object (js_context, NULL, NULL);
+ jsc_value_object_set_property (js_browser, "i18n", js_i18n);
+
+ js_function = jsc_value_new_function (js_context,
+ "getUILanguage",
+ G_CALLBACK (js_getuilanguage), extension, NULL,
+ G_TYPE_STRING,
+ 0);
+ jsc_value_object_set_property (js_i18n, "getUILanguage", js_function);
+ g_clear_object (&js_function);
+
+ js_function = jsc_value_new_function (js_context,
+ "getMessage",
+ G_CALLBACK (js_getmessage), extension, NULL,
+ G_TYPE_STRING, 1,
+ G_TYPE_STRING);
+ jsc_value_object_set_property (js_i18n, "getMessage", js_function);
+ g_clear_object (&js_function);
+
+ /* extension */
+ js_extension = jsc_value_new_object (js_context, NULL, NULL);
+ jsc_value_object_set_property (js_browser, "extension", js_extension);
+
+ js_function = jsc_value_new_function (js_context,
+ "getURL",
+ G_CALLBACK (js_geturl), extension, NULL,
+ G_TYPE_STRING,
+ 1,
+ G_TYPE_STRING);
+ jsc_value_object_set_property (js_extension, "getURL", js_function);
+ g_clear_object (&js_function);
+
+ bytes = g_resources_lookup_data ("/org/gnome/epiphany-web-process-extension/js/webextensions.js", G_RESOURCE_LOOKUP_FLAGS_NONE, NULL);
+ data = g_bytes_get_data (bytes, &data_size);
+ result = jsc_context_evaluate_with_source_uri (js_context, data, data_size, "resource:///org/gnome/epiphany-web-process-extension/js/webextensions.js", 1);
+ g_clear_object (&result);
+}
+
+void
+webextensions_add_translation (EphyWebProcessExtension *extension,
+ const char *name,
+ const char *data,
+ guint64 length)
+{
+ GHashTable *translations = ephy_web_process_extension_get_translations (extension);
+ JsonParser *parser = NULL;
+ JsonNode *root;
+ JsonObject *root_object;
+ g_autoptr (GError) error = NULL;
+
+ g_hash_table_remove (translations, name);
+
+ if (!data || strlen (data) == 0)
+ return;
+
+ parser = json_parser_new ();
+ if (json_parser_load_from_data (parser, data, length, &error)) {
+ root = json_parser_get_root (parser);
+ g_assert (root);
+ root_object = json_node_get_object (root);
+ g_assert (root_object);
+
+ g_hash_table_insert (translations, (char *)name, json_object_ref (root_object));
+ } else {
+ g_warning ("Could not read translation json data: %s. '%s'", error->message, data);
+ }
+}
diff --git a/embed/web-process-extension/ephy-webextension-api.h b/embed/web-process-extension/ephy-webextension-api.h
new file mode 100644
index 000000000..64e21e30f
--- /dev/null
+++ b/embed/web-process-extension/ephy-webextension-api.h
@@ -0,0 +1,37 @@
+ /* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Copyright © 2020 Jan-Michael Brummer <jan.brummer@tabos.org>
+ *
+ * This file is part of Epiphany.
+ *
+ * Epiphany is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Epiphany is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Epiphany. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <glib-object.h>
+#include <jsc/jsc.h>
+
+G_BEGIN_DECLS
+
+void set_up_webextensions (EphyWebProcessExtension *extension,
+ WebKitWebPage *page,
+ JSCContext *js_context);
+
+void webextensions_add_translation (EphyWebProcessExtension *extension,
+ const char *name,
+ const char *data,
+ guint64 length);
+
+G_END_DECLS
diff --git a/embed/web-process-extension/meson.build b/embed/web-process-extension/meson.build
index 398aa5ad1..b50cb4079 100644
--- a/embed/web-process-extension/meson.build
+++ b/embed/web-process-extension/meson.build
@@ -9,6 +9,7 @@ web_process_extension_sources = [
'ephy-web-process-extension.c',
'ephy-web-process-extension-main.c',
'ephy-web-overview-model.c',
+ 'ephy-webextension-api.c',
resources
]
diff --git a/embed/web-process-extension/resources/epiphany-web-process-extension.gresource.xml b/embed/web-process-extension/resources/epiphany-web-process-extension.gresource.xml
index 5c0d16f59..8cdf15d0c 100644
--- a/embed/web-process-extension/resources/epiphany-web-process-extension.gresource.xml
+++ b/embed/web-process-extension/resources/epiphany-web-process-extension.gresource.xml
@@ -3,5 +3,6 @@
<gresource prefix="/org/gnome/epiphany-web-process-extension">
<file compressed="true">js/ephy.js</file>
<file compressed="true">js/overview.js</file>
+ <file compressed="true">js/webextensions.js</file>
</gresource>
</gresources>
diff --git a/embed/web-process-extension/resources/js/webextensions.js b/embed/web-process-extension/resources/js/webextensions.js
new file mode 100644
index 000000000..1ed0ff801
--- /dev/null
+++ b/embed/web-process-extension/resources/js/webextensions.js
@@ -0,0 +1,115 @@
+'use strict';
+
+let promises = [];
+let last_promise = 0;
+
+let tabs_listeners = [];
+let page_listeners = [];
+let browser_listeners = [];
+let runtime_listeners = [];
+let runtime_onmessage_listeners = [];
+let runtime_onmessageexternal_listeners = [];
+let runtime_onconnect_listeners = [];
+let windows_onremoved_listeners = [];
+
+let ephy_message = function (fn, args, cb) {
+ let promise = new Promise (function (resolve, reject) {
+ window.webkit.messageHandlers.epiphany.postMessage ({fn: fn, args: args, promise: last_promise});
+ last_promise = promises.push({resolve: resolve, reject: reject});
+ });
+ return promise;
+}
+
+let pageActionOnClicked = function(x) {
+ for (let listener of page_listeners)
+ listener.callback(x);
+}
+
+let browserActionClicked = function(x) {
+ for (let listener of browser_listeners)
+ listener.callback(x);
+}
+
+let tabsOnUpdated = function(x) {
+ for (let listener of tabs_listeners)
+ listener.callback(x);
+}
+
+let runtimeSendMessage = function(x) {
+ for (let listener of runtime_onmessage_listeners)
+ listener.callback(x);
+}
+
+let runtimeOnConnect = function(x) {
+ for (let listener of runtime_onconnect_listeners)
+ listener.callback(x);
+}
+
+// Browser async API
+window.browser.alarms = {
+ clearAll: function (args, cb) { return ephy_message ('alarms.clearAll', args, cb); },
+};
+
+window.browser.windows = {
+ onRemoved: {
+ addListener: function (cb) { windows_onremoved_listeners.push({callback: cb}) }
+ }
+};
+
+window.browser.tabs = {
+ create: function (args, cb) { return ephy_message ('tabs.create', args, cb); },
+ executeScript: function (...args) { return ephy_message ('tabs.executeScript', args, null); },
+ query: function (args, cb) { return ephy_message ('tabs.query', args, cb); },
+ get: function (args, cb) { return ephy_message ('tabs.get', args, cb); },
+ insertCSS: function (args, cb) { return ephy_message ('tabs.insertCSS', args, cb); },
+ removeCSS: function (args, cb) { return ephy_message ('tabs.removeCSS', args, cb); },
+ onUpdated: {
+ addListener: function (cb) { tabs_listeners.push({callback: cb}) }
+ }
+};
+
+window.browser.notifications = {
+ create: function (args, cb) { return ephy_message ('notifications.create', args, cb); },
+};
+
+window.browser.runtime = {
+ getManifest: function (args, cb) { return "[]"; },
+ getBrowserInfo: function (args, cb) { return ephy_message ('runtime.getBrowserInfo', args, cb); },
+ onInstalled: {
+ addListener: function (cb) { runtime_listeners.push({callback: cb}); }
+ },
+ onMessage: {
+ addListener: function (cb) { runtime_onmessage_listeners.push({callback: cb}); }
+ },
+ onMessageExternal: {
+ addListener: function (cb) { runtime_onmessageexternal_listeners.push({callback: cb}); }
+ },
+ onConnect: {
+ addListener: function (cb) { runtime_onconnect_listeners.push({callback: cb}); }
+ },
+ connectNative: function (args, cb) { return ephy_message ('runtime.connectNative', args, cb); },
+ sendMessage: function (args, cb) { return ephy_message ('runtime.sendMessage', args, cb); },
+ openOptionsPage: function (args, cb) { return ephy_message ('runtime.openOptionsPage', args, cb); },
+ setUninstallURL: function (args, cb) { return ephy_message ('runtime.setUninstallURL', args, cb); },
+};
+
+window.browser.pageAction = {
+ setIcon: function (args, cb) { return ephy_message ('pageAction.setIcon', args, cb); },
+ setTitle: function (args, cb) { return ephy_message ('pageAction.setTitle', args, cb); },
+ getTitle: function (args, cb) { return ephy_message ('pageAction.getTitle', args, cb); },
+ show: function (args, cb) { return ephy_message ('pageAction.show', args, cb); },
+ hide: function (args, cb) { return ephy_message ('pageAction.hide', args, cb); },
+ onClicked: {
+ addListener: function (cb) { page_listeners.push({callback: cb}); }
+ }
+};
+
+window.browser.browserAction = {
+ onClicked: {
+ addListener: function (cb) { browser_listeners.push({callback: cb}); }
+ }
+};
+
+// Compatibility with Chrome
+window.chrome = window.browser;
+
diff --git a/lib/ephy-file-helpers.c b/lib/ephy-file-helpers.c
index 8cd382afd..8bc2a494d 100644
--- a/lib/ephy-file-helpers.c
+++ b/lib/ephy-file-helpers.c
@@ -854,3 +854,53 @@ ephy_open_incognito_window (const char *uri)
g_free (command);
}
+
+void
+ephy_copy_directory (const char *source,
+ const char *target)
+{
+ g_autoptr (GError) error = NULL;
+ GFileType type;
+ g_autoptr (GFile) src_file = g_file_new_for_path (source);
+ g_autoptr (GFile) dest_file = g_file_new_for_path (target);
+
+ type = g_file_query_file_type (src_file, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL);
+
+ if (type == G_FILE_TYPE_DIRECTORY) {
+ g_autoptr (GFileEnumerator) enumerator = NULL;
+ g_autoptr (GFileInfo) info = NULL;
+
+ if (!g_file_make_directory_with_parents (dest_file, NULL, &error)) {
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_EXISTS)) {
+ g_warning ("Could not create target directory for webextension: %s", error->message);
+ return;
+ }
+
+ g_error_free (error);
+ }
+
+ if (!g_file_copy_attributes (src_file, dest_file, G_FILE_COPY_NONE, NULL, &error)) {
+ g_warning ("Could not copy file attributes for webextension: %s", error->message);
+ return;
+ }
+
+ enumerator = g_file_enumerate_children (src_file, G_FILE_ATTRIBUTE_STANDARD_NAME, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL, &error);
+ if (!enumerator) {
+ g_warning ("Could not create file enumberator for webextensions: %s", error->message);
+ return;
+ }
+
+ for (info = g_file_enumerator_next_file (enumerator, NULL, NULL); info != NULL; info = g_file_enumerator_next_file (enumerator, NULL, NULL)) {
+ ephy_copy_directory (
+ g_build_filename (source, g_file_info_get_name (info), NULL),
+ g_build_filename (target, g_file_info_get_name (info), NULL));
+ }
+ } else if (type == G_FILE_TYPE_REGULAR) {
+ if (!g_file_copy (src_file, dest_file, G_FILE_COPY_NONE, NULL, NULL, NULL, &error)) {
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_EXISTS)) {
+ g_warning ("Could not copy file for webextensions: %s", error->message);
+ return;
+ }
+ }
+ }
+}
diff --git a/lib/ephy-file-helpers.h b/lib/ephy-file-helpers.h
index 42477b745..c09d145c8 100644
--- a/lib/ephy-file-helpers.h
+++ b/lib/ephy-file-helpers.h
@@ -87,4 +87,7 @@ gboolean ephy_file_open_uri_in_default_browser (const char
gboolean ephy_file_browse_to (GFile *file,
guint32 user_time);
+void ephy_copy_directory (const char *source,
+ const char *target);
+
G_END_DECLS
diff --git a/lib/ephy-prefs.h b/lib/ephy-prefs.h
index fb0f37ef8..89d567e6c 100644
--- a/lib/ephy-prefs.h
+++ b/lib/ephy-prefs.h
@@ -117,6 +117,8 @@ static const char * const ephy_prefs_state_schema[] = {
#define EPHY_PREFS_WEB_HARDWARE_ACCELERATION_POLICY "hardware-acceleration-policy"
#define EPHY_PREFS_WEB_ASK_ON_DOWNLOAD "ask-on-download"
#define EPHY_PREFS_WEB_SWITCH_TO_NEW_TAB "switch-to-new-tab"
+#define EPHY_PREFS_WEB_ENABLE_WEBEXTENSIONS "enable-webextensions"
+#define EPHY_PREFS_WEB_WEBEXTENSIONS_ACTIVE "webextensions-active"
static const char * const ephy_prefs_web_schema[] = {
EPHY_PREFS_WEB_FONT_MIN_SIZE,
@@ -146,6 +148,7 @@ static const char * const ephy_prefs_web_schema[] = {
EPHY_PREFS_WEB_HARDWARE_ACCELERATION_POLICY,
EPHY_PREFS_WEB_ASK_ON_DOWNLOAD,
EPHY_PREFS_WEB_SWITCH_TO_NEW_TAB,
+ EPHY_PREFS_WEB_ENABLE_WEBEXTENSIONS,
};
#define EPHY_PREFS_SCHEMA "org.gnome.Epiphany"
diff --git a/lib/ephy-string.c b/lib/ephy-string.c
index 509490c86..5dbf66c9b 100644
--- a/lib/ephy-string.c
+++ b/lib/ephy-string.c
@@ -316,35 +316,6 @@ ephy_string_remove_trailing (char *string,
}
char **
-ephy_strv_append (const char * const *strv,
- const char *str)
-{
- char **new_strv;
- char **n;
- const char * const *s;
- guint len;
-
- if (g_strv_contains (strv, str))
- return g_strdupv ((char **)strv);
-
- /* Needs room for one more string than before, plus one for trailing NULL. */
- len = g_strv_length ((char **)strv);
- new_strv = g_malloc ((len + 1 + 1) * sizeof (char *));
- n = new_strv;
- s = strv;
-
- while (*s != NULL) {
- *n = g_strdup (*s);
- n++;
- s++;
- }
- new_strv[len] = g_strdup (str);
- new_strv[len + 1] = NULL;
-
- return new_strv;
-}
-
-char **
ephy_strv_remove (const char * const *strv,
const char *str)
{
diff --git a/lib/ephy-string.h b/lib/ephy-string.h
index 2ad0a0c90..14515fa6d 100644
--- a/lib/ephy-string.h
+++ b/lib/ephy-string.h
@@ -49,8 +49,6 @@ char *ephy_string_remove_leading (char *string,
char *ephy_string_remove_trailing (char *string,
char ch);
-char **ephy_strv_append (const char * const *strv,
- const char *str);
char **ephy_strv_remove (const char * const *strv,
const char *str);
diff --git a/lib/widgets/ephy-location-entry.c b/lib/widgets/ephy-location-entry.c
index 4c8ea0a36..17055fc00 100644
--- a/lib/widgets/ephy-location-entry.c
+++ b/lib/widgets/ephy-location-entry.c
@@ -57,6 +57,8 @@ struct _EphyLocationEntry {
GtkOverlay parent_instance;
GtkWidget *url_entry;
+ GtkWidget *button_box;
+ GtkWidget *page_action_box;
GtkWidget *bookmark;
GtkWidget *bookmark_event_box;
GtkWidget *reader_mode;
@@ -990,7 +992,6 @@ static void
ephy_location_entry_construct_contents (EphyLocationEntry *entry)
{
GtkWidget *event;
- GtkWidget *box;
GtkStyleContext *context;
DzlShortcutController *controller;
@@ -1030,14 +1031,26 @@ ephy_location_entry_construct_contents (EphyLocationEntry *entry)
gtk_overlay_add_overlay (GTK_OVERLAY (entry), event);
/* Button Box */
- box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
- gtk_container_add (GTK_CONTAINER (event), box);
- g_signal_connect (G_OBJECT (box), "size-allocate", G_CALLBACK (button_box_size_allocated_cb), entry);
- gtk_widget_set_halign (box, GTK_ALIGN_END);
- gtk_widget_set_valign (box, GTK_ALIGN_CENTER);
- gtk_widget_show (box);
-
- context = gtk_widget_get_style_context (box);
+ entry->button_box = gtk_button_box_new (GTK_ORIENTATION_HORIZONTAL);
+ gtk_container_add (GTK_CONTAINER (event), entry->button_box);
+ gtk_box_set_homogeneous (GTK_BOX (entry->button_box), FALSE);
+ g_signal_connect (G_OBJECT (entry->button_box), "size-allocate", G_CALLBACK (button_box_size_allocated_cb), entry);
+ gtk_button_box_set_layout (GTK_BUTTON_BOX (entry->button_box), GTK_BUTTONBOX_EXPAND);
+ gtk_widget_set_valign (entry->button_box, GTK_ALIGN_CENTER);
+ gtk_widget_set_halign (entry->button_box, GTK_ALIGN_END);
+ gtk_widget_set_margin_end (entry->button_box, 5);
+ gtk_widget_show (entry->button_box);
+
+ /* Page action box */
+ entry->page_action_box = gtk_button_box_new (GTK_ORIENTATION_HORIZONTAL);
+ gtk_box_set_homogeneous (GTK_BOX (entry->page_action_box), FALSE);
+ gtk_widget_show (entry->page_action_box);
+ gtk_button_box_set_layout (GTK_BUTTON_BOX (entry->page_action_box), GTK_BUTTONBOX_EXPAND);
+ gtk_widget_set_valign (entry->page_action_box, GTK_ALIGN_CENTER);
+ gtk_widget_set_halign (entry->page_action_box, GTK_ALIGN_END);
+ gtk_box_pack_start (GTK_BOX (entry->button_box), entry->page_action_box, FALSE, FALSE, 0);
+
+ context = gtk_widget_get_style_context (entry->button_box);
gtk_style_context_add_class (context, "entry_icon_box");
/* Bookmark */
@@ -1048,7 +1061,7 @@ ephy_location_entry_construct_contents (EphyLocationEntry *entry)
gtk_widget_show (entry->bookmark);
g_signal_connect (G_OBJECT (entry->bookmark_event_box), "button_press_event", G_CALLBACK (bookmark_icon_button_press_event_cb), entry);
gtk_container_add (GTK_CONTAINER (entry->bookmark_event_box), entry->bookmark);
- gtk_box_pack_end (GTK_BOX (box), entry->bookmark_event_box, FALSE, FALSE, 0);
+ gtk_box_pack_end (GTK_BOX (entry->button_box), entry->bookmark_event_box, FALSE, FALSE, 6);
context = gtk_widget_get_style_context (entry->bookmark);
gtk_style_context_add_class (context, "entry_icon");
@@ -1066,7 +1079,7 @@ ephy_location_entry_construct_contents (EphyLocationEntry *entry)
gtk_widget_set_valign (entry->reader_mode, GTK_ALIGN_CENTER);
gtk_widget_show (entry->reader_mode);
gtk_container_add (GTK_CONTAINER (entry->reader_mode_event_box), entry->reader_mode);
- gtk_box_pack_end (GTK_BOX (box), entry->reader_mode_event_box, FALSE, FALSE, 0);
+ gtk_box_pack_end (GTK_BOX (entry->button_box), entry->reader_mode_event_box, FALSE, FALSE, 6);
context = gtk_widget_get_style_context (entry->reader_mode);
gtk_style_context_add_class (context, "entry_icon");
@@ -1505,3 +1518,35 @@ ephy_location_entry_set_mobile_popdown (EphyLocationEntry *entry,
else
dzl_suggestion_entry_set_position_func (DZL_SUGGESTION_ENTRY (entry->url_entry), position_func, NULL, NULL);
}
+
+void
+ephy_location_entry_page_action_add (EphyLocationEntry *entry,
+ GtkWidget *action)
+{
+ GtkStyleContext *context;
+
+ context = gtk_widget_get_style_context (action);
+ gtk_style_context_add_class (context, "entry_icon");
+
+ gtk_box_pack_end (GTK_BOX (entry->page_action_box), action, FALSE, FALSE, 6);
+}
+
+static
+void clear_page_actions (GtkWidget *child,
+ gpointer user_data)
+{
+ EphyLocationEntry *entry = EPHY_LOCATION_ENTRY (user_data);
+ GtkStyleContext *context;
+
+ context = gtk_widget_get_style_context (child);
+
+ gtk_style_context_remove_class (context, "entry_icon");
+
+ gtk_container_remove (GTK_CONTAINER (entry->page_action_box), child);
+}
+
+void
+ephy_location_entry_page_action_clear (EphyLocationEntry *entry)
+{
+ gtk_container_foreach (GTK_CONTAINER (entry->page_action_box), clear_page_actions, entry);
+}
diff --git a/lib/widgets/ephy-location-entry.h b/lib/widgets/ephy-location-entry.h
index fb8074b83..a9ce24084 100644
--- a/lib/widgets/ephy-location-entry.h
+++ b/lib/widgets/ephy-location-entry.h
@@ -79,6 +79,10 @@ gboolean ephy_location_entry_get_reader_mode_state (EphyLocationEntr
void ephy_location_entry_set_progress (EphyLocationEntry *entry,
gdouble progress,
gboolean loading);
+void ephy_location_entry_page_action_add (EphyLocationEntry *entry,
+ GtkWidget *action);
+
+void ephy_location_entry_page_action_clear (EphyLocationEntry *entry);
void ephy_location_entry_set_mobile_popdown (EphyLocationEntry *entry,
gboolean mobile_popdown);
diff --git a/meson.build b/meson.build
index 2ef46a8b3..2872cd348 100644
--- a/meson.build
+++ b/meson.build
@@ -72,10 +72,10 @@ gsb_api_key = get_option('gsb_api_key')
conf.set_quoted('GSB_API_KEY', gsb_api_key)
conf.set10('ENABLE_GSB', gsb_api_key != '')
-glib_requirement = '>= 2.61.2'
+glib_requirement = '>= 2.64.0'
gtk_requirement = '>= 3.24.0'
nettle_requirement = '>= 3.4'
-webkitgtk_requirement = '>= 2.29.3'
+webkitgtk_requirement = '>= 2.31.1'
cairo_dep = dependency('cairo', version: '>= 1.2')
gcr_dep = dependency('gcr-3', version: '>= 3.5.5')
@@ -90,6 +90,7 @@ gtk_unix_print_dep = dependency('gtk+-unix-print-3.0', version: gtk_requirement)
hogweed_dep = dependency('hogweed', version: nettle_requirement)
iso_codes_dep = dependency('iso-codes', version: '>= 0.35')
json_glib_dep = dependency('json-glib-1.0', version: '>= 1.2.4')
+libarchive_dep = dependency('libarchive')
libdazzle_dep = dependency('libdazzle-1.0', version: '>= 3.37.1')
libhandy_dep = dependency('libhandy-1', version: '>= 1.0.0')
libsecret_dep = dependency('libsecret-1', version: '>= 0.19.0')
diff --git a/src/ephy-action-bar-end.c b/src/ephy-action-bar-end.c
index 7775e0ce2..c979ffa41 100644
--- a/src/ephy-action-bar-end.c
+++ b/src/ephy-action-bar-end.c
@@ -39,6 +39,7 @@ struct _EphyActionBarEnd {
GtkWidget *downloads_popover;
GtkWidget *downloads_icon;
GtkWidget *downloads_progress;
+ GtkWidget *browser_action_box;
guint downloads_button_attention_timeout_id;
};
@@ -242,6 +243,9 @@ ephy_action_bar_end_class_init (EphyActionBarEndClass *klass)
gtk_widget_class_bind_template_child (widget_class,
EphyActionBarEnd,
downloads_progress);
+ gtk_widget_class_bind_template_child (widget_class,
+ EphyActionBarEnd,
+ browser_action_box);
}
static void
@@ -319,3 +323,10 @@ ephy_action_bar_end_get_downloads_revealer (EphyActionBarEnd *action_bar_end)
{
return action_bar_end->downloads_revealer;
}
+
+void
+ephy_action_bar_end_add_browser_action (EphyActionBarEnd *action_bar_end,
+ GtkWidget *action)
+{
+ gtk_container_add (GTK_CONTAINER (action_bar_end->browser_action_box), action);
+}
diff --git a/src/ephy-action-bar-end.h b/src/ephy-action-bar-end.h
index a0d397bc9..2d4f540c0 100644
--- a/src/ephy-action-bar-end.h
+++ b/src/ephy-action-bar-end.h
@@ -34,4 +34,7 @@ void ephy_action_bar_end_set_show_bookmarks_button (EphyActionBarEn
gboolean show);
GtkWidget *ephy_action_bar_end_get_downloads_revealer (EphyActionBarEnd *action_bar_end);
+void ephy_action_bar_end_add_browser_action (EphyActionBarEnd *action_bar_end,
+ GtkWidget *action);
+
G_END_DECLS
diff --git a/src/ephy-header-bar.c b/src/ephy-header-bar.c
index ddf1bf69e..15569a365 100644
--- a/src/ephy-header-bar.c
+++ b/src/ephy-header-bar.c
@@ -305,6 +305,7 @@ ephy_header_bar_constructed (GObject *object)
gtk_image_new_from_icon_name ("open-menu",
GTK_ICON_SIZE_LARGE_TOOLBAR));
}
+ g_settings_bind (EPHY_SETTINGS_WEB, EPHY_PREFS_WEB_ENABLE_WEBEXTENSIONS, gtk_builder_get_object (builder, "extensions-button"), "visible", G_SETTINGS_BIND_DEFAULT);
gtk_menu_button_set_popover (GTK_MENU_BUTTON (button), page_menu_popover);
g_object_unref (builder);
@@ -458,3 +459,10 @@ ephy_header_bar_set_zoom_level (EphyHeaderBar *header_bar,
gtk_label_set_label (GTK_LABEL (header_bar->zoom_level_label), zoom_level);
}
+
+void
+ephy_header_bar_add_browser_action (EphyHeaderBar *header_bar,
+ GtkWidget *action)
+{
+ ephy_action_bar_end_add_browser_action (header_bar->action_bar_end, action);
+}
diff --git a/src/ephy-header-bar.h b/src/ephy-header-bar.h
index 5227bcbff..cdc585801 100644
--- a/src/ephy-header-bar.h
+++ b/src/ephy-header-bar.h
@@ -49,4 +49,7 @@ void ephy_header_bar_start_change_combined_stop_reload_state (Eph
void ephy_header_bar_set_zoom_level (EphyHeaderBar *header_bar,
gdouble zoom);
+void ephy_header_bar_add_browser_action (EphyHeaderBar *header_bar,
+ GtkWidget *action);
+
G_END_DECLS
diff --git a/src/ephy-shell.c b/src/ephy-shell.c
index 15236cadd..18ac9ff84 100644
--- a/src/ephy-shell.c
+++ b/src/ephy-shell.c
@@ -61,6 +61,7 @@ struct _EphyShell {
EphyBookmarksManager *bookmarks_manager;
EphyHistoryManager *history_manager;
EphyOpenTabsManager *open_tabs_manager;
+ EphyWebExtensionManager *web_extension_manager;
GNetworkMonitor *network_monitor;
GtkWidget *history_dialog;
GtkWidget *firefox_sync_dialog;
@@ -1535,3 +1536,62 @@ ephy_shell_startup_finished (EphyShell *shell)
{
return shell->startup_finished;
}
+
+EphyWebExtensionManager *
+ephy_shell_get_web_extension_manager (EphyShell *shell)
+{
+ g_assert (EPHY_IS_SHELL (shell));
+
+ if (shell->web_extension_manager == NULL)
+ shell->web_extension_manager = ephy_web_extension_manager_new ();
+
+ return shell->web_extension_manager;
+}
+
+
+/* Helper functions: better place for this? */
+EphyWebView *
+ephy_shell_get_web_view (EphyShell *shell,
+ guint64 id)
+{
+ GList *windows;
+ GtkWindow *window;
+ GtkWidget *notebook;
+
+ windows = gtk_application_get_windows (GTK_APPLICATION (shell));
+
+ for (GList *list = windows; list && list->data; list = list->next) {
+ window = GTK_WINDOW (list->data);
+ notebook = ephy_window_get_notebook (EPHY_WINDOW (window));
+
+ for (int i = 0; i < gtk_notebook_get_n_pages (GTK_NOTEBOOK (notebook)); i++) {
+ GtkWidget *page = gtk_notebook_get_nth_page (GTK_NOTEBOOK (notebook), i);
+ EphyWebView *web_view = ephy_embed_get_web_view (EPHY_EMBED (page));
+
+ if (ephy_web_view_get_uid (web_view) == id)
+ return web_view;
+ }
+ }
+
+ return NULL;
+}
+
+EphyWebView *
+ephy_shell_get_active_web_view (EphyShell *shell)
+{
+ GtkWindow *window;
+ GtkWidget *notebook;
+ GtkWidget *page;
+ gint page_num;
+
+ window = gtk_application_get_active_window (GTK_APPLICATION (shell));
+ if (!window)
+ return NULL;
+
+ notebook = ephy_window_get_notebook (EPHY_WINDOW (window));
+
+ page_num = gtk_notebook_get_current_page (GTK_NOTEBOOK (notebook));
+ page = gtk_notebook_get_nth_page (GTK_NOTEBOOK (notebook), page_num);
+
+ return ephy_embed_get_web_view (EPHY_EMBED (page));
+}
diff --git a/src/ephy-shell.h b/src/ephy-shell.h
index ed39f695c..4abeed70e 100644
--- a/src/ephy-shell.h
+++ b/src/ephy-shell.h
@@ -30,6 +30,7 @@
#include "ephy-password-manager.h"
#include "ephy-session.h"
#include "ephy-sync-service.h"
+#include "ephy-web-extension-manager.h"
#include "ephy-window.h"
#include <webkit2/webkit2.h>
@@ -130,4 +131,11 @@ void ephy_shell_send_notification (EphyShell *s
gboolean ephy_shell_startup_finished (EphyShell *shell);
+EphyWebExtensionManager *ephy_shell_get_web_extension_manager (EphyShell *shell);
+
+EphyWebView *ephy_shell_get_web_view (EphyShell *shell,
+ guint64 id);
+
+EphyWebView *ephy_shell_get_active_web_view (EphyShell *shell);
+
G_END_DECLS
diff --git a/src/ephy-web-extension-dialog.c b/src/ephy-web-extension-dialog.c
new file mode 100644
index 000000000..d1da9f0dc
--- /dev/null
+++ b/src/ephy-web-extension-dialog.c
@@ -0,0 +1,291 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Copyright © 2019-2020 Jan-Michael Brummer <jan.brummer@tabos.org>
+ *
+ * This file is part of Epiphany.
+ *
+ * Epiphany is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Epiphany is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Epiphany. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "ephy-file-helpers.h"
+#include "ephy-shell.h"
+#include "ephy-web-extension.h"
+#include "ephy-web-extension-dialog.h"
+#include "ephy-web-extension-manager.h"
+
+#include <gtk/gtk.h>
+
+struct _EphyWebExtensionDialog {
+ HdyWindow parent_instance;
+
+ EphyWebExtensionManager *web_extension_manager;
+
+ GtkWidget *listbox;
+ GtkWidget *add_button;
+ GtkWidget *remove_button;
+};
+
+G_DEFINE_TYPE (EphyWebExtensionDialog, ephy_web_extension_dialog, HDY_TYPE_WINDOW)
+
+static void
+clear_listbox (GtkWidget *listbox)
+{
+ GList *children, *iter;
+
+ children = gtk_container_get_children (GTK_CONTAINER (listbox));
+
+ for (iter = children; iter && iter->data; iter = g_list_next (iter))
+ gtk_widget_destroy (GTK_WIDGET (iter->data));
+
+ g_list_free (children);
+}
+
+static void
+on_remove_button_clicked (GtkButton *button,
+ gpointer user_data)
+{
+ EphyWebExtensionDialog *self = EPHY_WEB_EXTENSION_DIALOG (user_data);
+ GtkWidget *dialog = NULL;
+ GtkListBoxRow *row;
+ GtkWidget *widget;
+ gint res;
+
+ row = gtk_list_box_get_selected_row (GTK_LIST_BOX (self->listbox));
+ if (!row)
+ return;
+
+ dialog = gtk_message_dialog_new (GTK_WINDOW (self),
+ GTK_DIALOG_MODAL | GTK_DIALOG_USE_HEADER_BAR,
+ GTK_MESSAGE_QUESTION,
+ GTK_BUTTONS_NONE,
+ _("Do you really want to remove this extension?"));
+ gtk_dialog_add_buttons (GTK_DIALOG (dialog),
+ _("_Cancel"),
+ GTK_RESPONSE_CANCEL,
+ _("_Remove"),
+ GTK_RESPONSE_OK,
+ NULL);
+
+ widget = gtk_dialog_get_widget_for_response (GTK_DIALOG (dialog), GTK_RESPONSE_OK);
+ gtk_style_context_add_class (gtk_widget_get_style_context (widget), GTK_STYLE_CLASS_DESTRUCTIVE_ACTION);
+
+ res = gtk_dialog_run (GTK_DIALOG (dialog));
+ if (res == GTK_RESPONSE_OK) {
+ EphyWebExtension *web_extension = g_object_get_data (G_OBJECT (row), "web_extension");
+
+ g_assert (web_extension);
+ ephy_web_extension_manager_uninstall (self->web_extension_manager, web_extension);
+ }
+
+ gtk_widget_destroy (dialog);
+}
+
+static void
+toggle_state_set_cb (GtkSwitch *widget,
+ gboolean state,
+ gpointer user_data)
+{
+ EphyWebExtensionManager *manager = ephy_shell_get_web_extension_manager (ephy_shell_get_default ());
+ EphyWebExtension *web_extension = EPHY_WEB_EXTENSION (user_data);
+
+ ephy_web_extension_manager_set_active (manager, web_extension, state);
+}
+
+static GtkWidget *
+create_row (EphyWebExtensionDialog *self,
+ EphyWebExtension *web_extension)
+{
+ GtkWidget *row;
+ GtkWidget *sub_row;
+ GtkWidget *image;
+ GtkWidget *toggle;
+ GtkWidget *button;
+ GtkWidget *homepage;
+ GtkWidget *author;
+ GtkWidget *version;
+ g_autoptr (GdkPixbuf) icon = NULL;
+ EphyWebExtensionManager *manager = ephy_shell_get_web_extension_manager (ephy_shell_get_default ());
+
+ row = hdy_expander_row_new ();
+ g_object_set_data (G_OBJECT (row), "web_extension", web_extension);
+
+ /* Tooltip */
+ gtk_widget_set_tooltip_text (GTK_WIDGET (row), ephy_web_extension_get_name (web_extension));
+
+ /* Icon */
+ icon = ephy_web_extension_get_icon (web_extension, 48);
+ image = icon ? gtk_image_new_from_pixbuf (icon) : gtk_image_new_from_icon_name ("application-x-addon-symbolic", GTK_ICON_SIZE_DIALOG);
+ hdy_expander_row_add_prefix (HDY_EXPANDER_ROW (row), image);
+
+ /* Titles */
+ hdy_preferences_row_set_title (HDY_PREFERENCES_ROW (row), ephy_web_extension_get_name (web_extension));
+ hdy_expander_row_set_subtitle (HDY_EXPANDER_ROW (row), ephy_web_extension_get_description (web_extension));
+ hdy_expander_row_set_show_enable_switch (HDY_EXPANDER_ROW (row), FALSE);
+
+ toggle = gtk_switch_new ();
+ gtk_switch_set_active (GTK_SWITCH (toggle), ephy_web_extension_manager_is_active (manager, web_extension));
+ g_signal_connect (toggle, "state-set", G_CALLBACK (toggle_state_set_cb), web_extension);
+ gtk_widget_set_valign (toggle, GTK_ALIGN_CENTER);
+ hdy_expander_row_add_action (HDY_EXPANDER_ROW (row), toggle);
+
+ /* Author */
+ if (ephy_web_extension_get_author (web_extension)) {
+ sub_row = hdy_action_row_new ();
+ gtk_container_add (GTK_CONTAINER (row), sub_row);
+ hdy_preferences_row_set_title (HDY_PREFERENCES_ROW (sub_row), _("Author"));
+ author = gtk_label_new (ephy_web_extension_get_author (web_extension));
+ gtk_label_set_line_wrap (GTK_LABEL (author), TRUE);
+ gtk_container_add (GTK_CONTAINER (sub_row), author);
+ }
+
+ /* Version */
+ sub_row = hdy_action_row_new ();
+ gtk_container_add (GTK_CONTAINER (row), sub_row);
+ hdy_preferences_row_set_title (HDY_PREFERENCES_ROW (sub_row), _("Version"));
+ version = gtk_label_new (ephy_web_extension_get_version (web_extension));
+ gtk_container_add (GTK_CONTAINER (sub_row), version);
+
+ /* Homepage url */
+ if (ephy_web_extension_get_homepage_url (web_extension)) {
+ sub_row = hdy_action_row_new ();
+ gtk_container_add (GTK_CONTAINER (row), sub_row);
+ hdy_preferences_row_set_title (HDY_PREFERENCES_ROW (sub_row), _("Homepage"));
+ homepage = gtk_link_button_new_with_label (ephy_web_extension_get_homepage_url (web_extension), _("Open"));
+ gtk_container_add (GTK_CONTAINER (sub_row), homepage);
+ }
+
+ /* Remove button */
+ sub_row = hdy_action_row_new ();
+ gtk_container_add (GTK_CONTAINER (row), sub_row);
+
+ button = gtk_button_new_with_label (_("Remove"));
+ gtk_widget_set_valign (GTK_WIDGET (button), GTK_ALIGN_CENTER);
+ dzl_gtk_widget_add_style_class (button, GTK_STYLE_CLASS_DESTRUCTIVE_ACTION);
+ g_signal_connect (button, "clicked", G_CALLBACK (on_remove_button_clicked), self);
+ gtk_widget_set_tooltip_text (button, _("Remove selected WebExtension"));
+ gtk_container_add (GTK_CONTAINER (sub_row), button);
+
+ gtk_widget_show_all (GTK_WIDGET (row));
+
+ return GTK_WIDGET (row);
+}
+
+static void
+ephy_web_extension_dialog_refresh_listbox (EphyWebExtensionDialog *self)
+{
+ GList *extensions = ephy_web_extension_manager_get_web_extensions (self->web_extension_manager);
+
+ clear_listbox (self->listbox);
+
+ for (GList *tmp = extensions; tmp && tmp->data; tmp = tmp->next) {
+ EphyWebExtension *web_extension = tmp->data;
+ GtkWidget *row;
+
+ row = create_row (self, web_extension);
+ gtk_list_box_insert (GTK_LIST_BOX (self->listbox), row, -1);
+ }
+}
+
+static void
+on_add_button_clicked (GtkButton *button,
+ gpointer user_data)
+{
+ EphyWebExtensionDialog *self = EPHY_WEB_EXTENSION_DIALOG (user_data);
+ GtkWidget *dialog = NULL;
+ GtkFileFilter *filter;
+ gint res;
+
+ dialog = gtk_file_chooser_dialog_new (_("Open File (manifest.json/xpi)"),
+ GTK_WINDOW (self),
+ GTK_FILE_CHOOSER_ACTION_OPEN,
+ _("_Cancel"),
+ GTK_RESPONSE_CANCEL,
+ _("_Open"),
+ GTK_RESPONSE_ACCEPT,
+ NULL);
+
+ filter = gtk_file_filter_new ();
+ gtk_file_filter_set_name (GTK_FILE_FILTER (filter), "WebExtensions");
+ gtk_file_filter_add_mime_type (GTK_FILE_FILTER (filter), "application/json");
+ gtk_file_filter_add_mime_type (GTK_FILE_FILTER (filter), "application/x-xpinstall");
+ gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (dialog), g_steal_pointer (&filter));
+
+ res = gtk_dialog_run (GTK_DIALOG (dialog));
+ if (res == GTK_RESPONSE_ACCEPT) {
+ g_autoptr (GFile) file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (dialog));
+
+ ephy_web_extension_manager_install (self->web_extension_manager, file);
+ }
+
+ gtk_widget_destroy (dialog);
+}
+
+static void
+on_web_extension_manager_changed (EphyWebExtensionManager *manager,
+ gpointer user_data)
+{
+ EphyWebExtensionDialog *self = EPHY_WEB_EXTENSION_DIALOG (user_data);
+
+ ephy_web_extension_dialog_refresh_listbox (self);
+}
+
+static void
+ephy_web_extension_dialog_dispose (GObject *object)
+{
+ EphyWebExtensionDialog *self = EPHY_WEB_EXTENSION_DIALOG (object);
+
+ g_clear_weak_pointer (&self->web_extension_manager);
+
+ G_OBJECT_CLASS (ephy_web_extension_dialog_parent_class)->dispose (object);
+}
+
+static void
+ephy_web_extension_dialog_class_init (EphyWebExtensionDialogClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->dispose = ephy_web_extension_dialog_dispose;
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ "/org/gnome/epiphany/gtk/web-extensions-dialog.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, EphyWebExtensionDialog, listbox);
+
+ gtk_widget_class_bind_template_callback (widget_class, on_add_button_clicked);
+}
+
+static void
+ephy_web_extension_dialog_init (EphyWebExtensionDialog *self)
+{
+ EphyWebExtensionManager *manager;
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ manager = ephy_shell_get_web_extension_manager (ephy_shell_get_default ());
+ g_assert (manager != NULL);
+
+ g_set_weak_pointer (&self->web_extension_manager, manager);
+ g_signal_connect_object (self->web_extension_manager, "changed", G_CALLBACK (on_web_extension_manager_changed), self, 0);
+
+ ephy_web_extension_dialog_refresh_listbox (self);
+}
+
+GtkWidget *
+ephy_web_extension_dialog_new (void)
+{
+ return g_object_new (EPHY_TYPE_WEB_EXTENSION_DIALOG, NULL);
+}
diff --git a/src/ephy-web-extension-dialog.h b/src/ephy-web-extension-dialog.h
new file mode 100644
index 000000000..b8418f0f8
--- /dev/null
+++ b/src/ephy-web-extension-dialog.h
@@ -0,0 +1,35 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Copyright © 2019-2020 Jan-Michael Brummer <jan.brummer@tabos.org>
+ *
+ * This file is part of Epiphany.
+ *
+ * Epiphany is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Epiphany is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Epiphany. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+#include "ephy-window.h"
+
+G_BEGIN_DECLS
+
+#define EPHY_TYPE_WEB_EXTENSION_DIALOG (ephy_web_extension_dialog_get_type ())
+
+G_DECLARE_FINAL_TYPE (EphyWebExtensionDialog, ephy_web_extension_dialog, EPHY, WEB_EXTENSION_DIALOG, HdyWindow)
+
+GtkWidget *ephy_web_extension_dialog_new (void);
+
+G_END_DECLS
diff --git a/src/ephy-window.c b/src/ephy-window.c
index 431f96742..57af66537 100644
--- a/src/ephy-window.c
+++ b/src/ephy-window.c
@@ -117,6 +117,7 @@ const struct {
{ "win.location-search", {"<Primary>K", NULL} },
{ "win.home", { "<alt>Home", NULL } },
{ "win.content", { "Escape", NULL } },
+ { "win.extensions", { NULL } },
/* Toggle actions */
{ "win.browse-with-caret", { "F7", NULL } },
@@ -859,6 +860,7 @@ static const GActionEntry window_entries [] = {
{ "page-source", window_cmd_page_source },
{ "toggle-inspector", window_cmd_toggle_inspector },
{ "toggle-reader-mode", window_cmd_toggle_reader_mode },
+ { "extensions", window_cmd_extensions },
{ "select-all", window_cmd_select_all },
@@ -1270,6 +1272,17 @@ sync_tab_title (EphyEmbed *embed,
ephy_embed_get_title (embed));
}
+static void
+sync_tab_page_action (EphyWebView *view,
+ GParamSpec *pspec,
+ EphyWindow *window)
+{
+ EphyWebExtensionManager *manager;
+
+ manager = ephy_shell_get_web_extension_manager (ephy_shell_get_default ());
+ ephy_web_extension_manager_update_location_entry (manager, window);
+}
+
static gboolean
idle_unref_context_event (EphyWindow *window)
{
@@ -2419,6 +2432,7 @@ ephy_window_connect_active_embed (EphyWindow *window)
sync_tab_popup_windows (view, NULL, window);
sync_tab_zoom (web_view, NULL, window);
+ sync_tab_page_action (view, NULL, window);
title_widget = ephy_header_bar_get_title_widget (EPHY_HEADER_BAR (window->header_bar));
@@ -3946,6 +3960,8 @@ ephy_window_constructed (GObject *object)
window->mouse_gesture_controller = ephy_mouse_gesture_controller_new (window);
ephy_window_set_chrome (window, chrome);
+
+ ephy_web_extension_manager_install_actions (ephy_shell_get_web_extension_manager (ephy_shell_get_default ()), window);
}
static void
diff --git a/src/meson.build b/src/meson.build
index 7113fd976..fa0cc3183 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -10,6 +10,8 @@ enums = gnome.mkenums_simple('ephy-type-builtins',
sources: types_headers
)
+subdir('webextension')
+
libephymain_sources = [
'bookmarks/ephy-add-bookmark-popover.c',
'bookmarks/ephy-bookmark.c',
@@ -43,6 +45,7 @@ libephymain_sources = [
'ephy-suggestion-model.c',
'ephy-tab-header-bar.c',
'ephy-tab-label.c',
+ 'ephy-web-extension-dialog.c',
'ephy-window.c',
'popup-commands.c',
'preferences/clear-data-view.c',
@@ -58,6 +61,7 @@ libephymain_sources = [
'preferences/webapp-additional-urls-dialog.c',
'synced-tabs-dialog.c',
'window-commands.c',
+ ephywebextension_src,
compile_schemas,
enums
]
@@ -70,13 +74,17 @@ libephymain_deps = [
ephywidgets_dep,
gdk_dep,
gvdb_dep,
+ libarchive_dep,
libhandy_dep
]
libephymain_includes = include_directories(
'.',
+ '..',
'bookmarks',
'preferences',
+ 'webextension',
+ 'webextension/api',
)
libephymain = shared_library('ephymain',
diff --git a/src/resources/epiphany.gresource.xml b/src/resources/epiphany.gresource.xml
index 7c5ff5ae1..28e9a7a0b 100644
--- a/src/resources/epiphany.gresource.xml
+++ b/src/resources/epiphany.gresource.xml
@@ -43,6 +43,7 @@
<file preprocess="xml-stripblanks" compressed="true">gtk/shortcuts-dialog.ui</file>
<file preprocess="xml-stripblanks" compressed="true">gtk/tab-label.ui</file>
<file preprocess="xml-stripblanks" compressed="true">gtk/webapp-additional-urls-dialog.ui</file>
+ <file preprocess="xml-stripblanks" compressed="true">gtk/web-extensions-dialog.ui</file>
</gresource>
<gresource prefix="/org/gnome/Epiphany/icons">
<file compressed="true" alias="scalable/actions/ephy-download-symbolic.svg" preprocess="xml-stripblanks">ephy-download-symbolic.svg</file>
diff --git a/src/resources/gtk/action-bar-end.ui b/src/resources/gtk/action-bar-end.ui
index 6e03dbfc6..e0082d589 100644
--- a/src/resources/gtk/action-bar-end.ui
+++ b/src/resources/gtk/action-bar-end.ui
@@ -3,6 +3,18 @@
<template class="EphyActionBarEnd" parent="GtkBox">
<property name="spacing">6</property>
<child>
+ <object class="GtkButtonBox" id="browser_action_box">
+ <property name="visible">True</property>
+ <property name="valign">center</property>
+ <property name="halign">end</property>
+ <property name="homogeneous">False</property>
+ <property name="layout-style">expand</property>
+ </object>
+ <packing>
+ <property name="pack-type">start</property>
+ </packing>
+ </child>
+ <child>
<object class="GtkMenuButton" id="bookmarks_button">
<property name="visible">True</property>
<property name="valign">center</property>
diff --git a/src/resources/gtk/page-menu-popover.ui b/src/resources/gtk/page-menu-popover.ui
index eab4f6322..fa9cfb129 100644
--- a/src/resources/gtk/page-menu-popover.ui
+++ b/src/resources/gtk/page-menu-popover.ui
@@ -271,6 +271,14 @@
<property name="visible">True</property>
</object>
</child>
+ <child>
+ <object class="GtkModelButton" id="extensions-button">
+ <property name="can_focus">True</property>
+ <property name="text" translatable="yes">_Extensions</property>
+ <property name="action-name">win.extensions</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
<!-- FRAGILE: These buttons are manually removed for app mode in ephy-header-bar.c. -->
<child>
<object class="GtkSeparator" id="override-text-encoding-separator">
diff --git a/src/resources/gtk/web-extensions-dialog.ui b/src/resources/gtk/web-extensions-dialog.ui
new file mode 100644
index 000000000..ae8284869
--- /dev/null
+++ b/src/resources/gtk/web-extensions-dialog.ui
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.38.0 -->
+<interface>
+ <requires lib="gtk+" version="3.20"/>
+ <template class="EphyWebExtensionDialog" parent="HdyWindow">
+ <property name="can-focus">False</property>
+ <property name="modal">True</property>
+ <property name="window-position">center-on-parent</property>
+ <property name="default-width">640</property>
+ <property name="default-height">400</property>
+ <property name="destroy-with_parent">True</property>
+ <property name="type-hint">dialog</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="HdyHeaderBar">
+ <property name="visible">True</property>
+ <property name="decoration-layout">:close</property>
+ <property name="show-close-button">True</property>
+ <property name="title" translatable="yes">Extensions</property>
+ <child>
+ <object class="GtkButton" id="add_button">
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">True</property>
+ <property name="label" translatable="yes">Add…</property>
+ <signal name="clicked" handler="on_add_button_clicked" object="EphyWebExtensionDialog" swapped="no"/>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="vexpand">True</property>
+ <child>
+ <object class="HdyClamp">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin_start">6</property>
+ <property name="margin_end">6</property>
+ <property name="maximum_size">1024</property>
+ <child>
+ <object class="GtkListBox" id="listbox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_top">6</property>
+ <property name="margin_bottom">6</property>
+ <property name="valign">start</property>
+ <style>
+ <class name="content"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/src/webextension/README.md b/src/webextension/README.md
new file mode 100644
index 000000000..780b87374
--- /dev/null
+++ b/src/webextension/README.md
@@ -0,0 +1,59 @@
+https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API
+
+https://github.com/mdn/webextensions-examples
+
+
+# Working extensions
+
+- Borderify
+- Apply CSS
+- Page to extension messaging
+
+# QUESTIONS
+ - Should we use **self** as current module parameter name for consistency or name it like module?
+ - Clear definition if get/set functions should be used instead of direct struct access
+ - Enfore g_auto free functions implementation?
+ - Alignment in header files
+ - Should every function of a file has a certain prefix or only non static functions?
+ - EphyWebExtensionManager as a singleton?
+
+# PLAN
+
+## First release
+Feature set:
+ - Un/Load/Enable/Disable xpi and extracted extensions
+ - Works for existing and new views
+ - Manifest file:
+ - initial content_scripts
+ - initial background page
+ - initial background scripts
+ - API:
+ - notifications:
+ - create
+ - pageaction:
+ - setIcon
+ - setTitle
+ - show
+ - getTitle
+ - tabs:
+ - insertCSS
+ - removeCSS
+ - initial query
+
+ - Test extensions:
+ - apply-css
+ - borderify
+
+## Second release
+Feature set:
+ - API:
+ - i18n:
+ - getMessage
+ - getUILanguage
+ - runtime:
+ - sendMessage
+ - onMessage.addListener
+
+ - Test extensions:
+ - notify-link-clicks-i18n
+
diff --git a/src/webextension/api/notifications.c b/src/webextension/api/notifications.c
new file mode 100644
index 000000000..d63c119ef
--- /dev/null
+++ b/src/webextension/api/notifications.c
@@ -0,0 +1,73 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Copyright © 2019-2020 Jan-Michael Brummer <jan.brummer@tabos.org>
+ *
+ * This file is part of Epiphany.
+ *
+ * Epiphany is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Epiphany is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Epiphany. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "ephy-notification.h"
+#include "ephy-web-extension.h"
+
+#include "notifications.h"
+
+static char *
+notifications_handler_create (EphyWebExtension *self,
+ char *name,
+ JSCValue *args)
+{
+ g_autofree char *title_str = NULL;
+ g_autofree char *message_str = NULL;
+ g_autoptr (JSCValue) title = NULL;
+ g_autoptr (JSCValue) message = NULL;
+ EphyNotification *notify;
+
+ title = jsc_value_object_get_property (args, "title");
+ title_str = jsc_value_to_string (title);
+
+ message = jsc_value_object_get_property (args, "message");
+ message_str = jsc_value_to_string (message);
+
+ notify = ephy_notification_new (g_strdup (title_str), g_strdup (message_str));
+ ephy_notification_show (notify);
+
+ return NULL;
+}
+
+static EphyWebExtensionApiHandler notifications_handlers[] = {
+ {"create", notifications_handler_create},
+ {NULL, NULL},
+};
+
+char *
+ephy_web_extension_api_notifications_handler (EphyWebExtension *self,
+ char *name,
+ JSCValue *args)
+{
+ guint idx;
+
+ for (idx = 0; idx < G_N_ELEMENTS (notifications_handlers); idx++) {
+ EphyWebExtensionApiHandler handler = notifications_handlers[idx];
+
+ if (g_strcmp0 (handler.name, name) == 0)
+ return handler.execute (self, name, args);
+ }
+
+ g_warning ("%s(): '%s' not implemented by Epiphany!", __FUNCTION__, name);
+
+ return NULL;
+}
diff --git a/src/webextension/api/notifications.h b/src/webextension/api/notifications.h
new file mode 100644
index 000000000..f0f5434a7
--- /dev/null
+++ b/src/webextension/api/notifications.h
@@ -0,0 +1,32 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Copyright © 2019-2020 Jan-Michael Brummer <jan.brummer@tabos.org>
+ *
+ * This file is part of Epiphany.
+ *
+ * Epiphany is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Epiphany is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Epiphany. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+#pragma once
+
+#include "ephy-web-extension.h"
+
+G_BEGIN_DECLS
+
+char *ephy_web_extension_api_notifications_handler (EphyWebExtension *self,
+ char *name,
+ JSCValue *args);
+
+G_END_DECLS
diff --git a/src/webextension/api/pageaction.c b/src/webextension/api/pageaction.c
new file mode 100644
index 000000000..1f2d53ff4
--- /dev/null
+++ b/src/webextension/api/pageaction.c
@@ -0,0 +1,166 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Copyright © 2019-2020 Jan-Michael Brummer <jan.brummer@tabos.org>
+ *
+ * This file is part of Epiphany.
+ *
+ * Epiphany is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Epiphany is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Epiphany. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "ephy-shell.h"
+#include "ephy-web-extension.h"
+#include "ephy-window.h"
+
+#include "pageaction.h"
+
+static GtkWidget *
+pageaction_get_action (EphyWebExtension *self,
+ JSCValue *args)
+{
+ EphyWebView *web_view = NULL;
+ EphyShell *shell = ephy_shell_get_default ();
+ EphyWebExtensionManager *manager = ephy_shell_get_web_extension_manager (shell);
+ g_autoptr (JSCValue) tab_id = NULL;
+ gint32 nr;
+
+ if (jsc_value_object_has_property (args, "tabId")) {
+ tab_id = jsc_value_object_get_property (args, "tabId");
+ nr = jsc_value_to_int32 (tab_id);
+ web_view = ephy_shell_get_web_view (shell, nr);
+ if (!web_view) {
+ LOG ("%s(): Invalid tabId '%d', abort\n", __FUNCTION__, nr);
+ return NULL;
+ }
+ }
+
+ return ephy_web_extension_manager_get_page_action (manager, self, web_view);
+}
+
+static char *
+pageaction_handler_seticon (EphyWebExtension *self,
+ char *name,
+ JSCValue *args)
+{
+ GtkWidget *action;
+ g_autoptr (JSCValue) path = NULL;
+ g_autoptr (GdkPixbuf) pixbuf = NULL;
+
+ action = pageaction_get_action (self, args);
+ if (!action)
+ return NULL;
+
+ path = jsc_value_object_get_property (args, "path");
+ pixbuf = ephy_web_extension_load_pixbuf (self, jsc_value_to_string (path));
+
+ gtk_image_set_from_pixbuf (GTK_IMAGE (gtk_bin_get_child (GTK_BIN (action))), pixbuf);
+
+ return NULL;
+}
+
+static char *
+pageaction_handler_settitle (EphyWebExtension *self,
+ char *name,
+ JSCValue *args)
+{
+ GtkWidget *action;
+ g_autoptr (JSCValue) title = NULL;
+
+ action = pageaction_get_action (self, args);
+ if (!action)
+ return NULL;
+
+ title = jsc_value_object_get_property (args, "title");
+ gtk_widget_set_tooltip_text (action, jsc_value_to_string (title));
+
+ return NULL;
+}
+
+static char *
+pageaction_handler_gettitle (EphyWebExtension *self,
+ char *name,
+ JSCValue *args)
+{
+ GtkWidget *action;
+ g_autofree char *title = NULL;
+
+ action = pageaction_get_action (self, args);
+ if (!action)
+ return NULL;
+
+ title = gtk_widget_get_tooltip_text (action);
+
+ return g_strdup_printf ("\"%s\"", title ? title : "");
+}
+
+static char *
+pageaction_handler_show (EphyWebExtension *self,
+ char *name,
+ JSCValue *args)
+{
+ GtkWidget *action;
+
+ action = pageaction_get_action (self, args);
+ if (!action)
+ return NULL;
+
+ gtk_widget_set_visible (action, TRUE);
+
+ return NULL;
+}
+
+static char *
+pageaction_handler_hide (EphyWebExtension *self,
+ char *name,
+ JSCValue *args)
+{
+ GtkWidget *action;
+
+ action = pageaction_get_action (self, args);
+ if (!action)
+ return NULL;
+
+ gtk_widget_set_visible (action, FALSE);
+
+ return NULL;
+}
+
+static EphyWebExtensionApiHandler pageaction_handlers[] = {
+ {"setIcon", pageaction_handler_seticon},
+ {"setTitle", pageaction_handler_settitle},
+ {"getTitle", pageaction_handler_gettitle},
+ {"show", pageaction_handler_show},
+ {"hide", pageaction_handler_hide},
+ {NULL, NULL},
+};
+
+char *
+ephy_web_extension_api_pageaction_handler (EphyWebExtension *self,
+ char *name,
+ JSCValue *args)
+{
+ guint idx;
+
+ for (idx = 0; idx < G_N_ELEMENTS (pageaction_handlers); idx++) {
+ EphyWebExtensionApiHandler handler = pageaction_handlers[idx];
+
+ if (g_strcmp0 (handler.name, name) == 0)
+ return handler.execute (self, name, args);
+ }
+
+ g_warning ("%s(): '%s' not implemented by Epiphany!", __FUNCTION__, name);
+
+ return NULL;
+}
diff --git a/src/webextension/api/pageaction.h b/src/webextension/api/pageaction.h
new file mode 100644
index 000000000..af3ce840b
--- /dev/null
+++ b/src/webextension/api/pageaction.h
@@ -0,0 +1,32 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Copyright © 2019-2020 Jan-Michael Brummer <jan.brummer@tabos.org>
+ *
+ * This file is part of Epiphany.
+ *
+ * Epiphany is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Epiphany is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Epiphany. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+#pragma once
+
+#include "ephy-web-extension.h"
+
+G_BEGIN_DECLS
+
+char *ephy_web_extension_api_pageaction_handler (EphyWebExtension *self,
+ char *name,
+ JSCValue *args);
+
+G_END_DECLS
diff --git a/src/webextension/api/runtime.c b/src/webextension/api/runtime.c
new file mode 100644
index 000000000..ec67b6600
--- /dev/null
+++ b/src/webextension/api/runtime.c
@@ -0,0 +1,122 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Copyright © 2019-2020 Jan-Michael Brummer <jan.brummer@tabos.org>
+ *
+ * This file is part of Epiphany.
+ *
+ * Epiphany is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Epiphany is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Epiphany. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "runtime.h"
+
+#include "ephy-web-extension-manager.h"
+
+#include "ephy-embed-utils.h"
+#include "ephy-shell.h"
+
+static char *
+runtime_handler_get_browser_info (EphyWebExtension *self,
+ char *name,
+ JSCValue *args)
+{
+ g_autoptr (JsonBuilder) builder = json_builder_new ();
+ g_autoptr (JsonNode) root = NULL;
+
+ json_builder_begin_object (builder);
+ json_builder_set_member_name (builder, "name");
+ json_builder_add_string_value (builder, "GNOME Web (Epiphany)");
+ json_builder_end_object (builder);
+
+ root = json_builder_get_root (builder);
+
+ return json_to_string (root, FALSE);
+}
+
+static char *
+runtime_handler_send_message (EphyWebExtension *self,
+ char *name,
+ JSCValue *args)
+{
+ EphyShell *shell = ephy_shell_get_default ();
+ EphyWebExtensionManager *manager = ephy_shell_get_web_extension_manager (shell);
+ WebKitWebView *view = WEBKIT_WEB_VIEW (ephy_web_extension_manager_get_background_web_view (manager, self));
+ g_autofree char *script = NULL;
+
+ script = g_strdup_printf ("runtimeSendMessage(%s);", jsc_value_to_json (args, 2));
+ webkit_web_view_run_javascript_in_world (view, script, ephy_embed_shell_get_guid (EPHY_EMBED_SHELL (shell)), NULL, NULL, NULL);
+
+ return NULL;
+}
+
+static char *
+runtime_handler_open_options_page (EphyWebExtension *self,
+ char *name,
+ JSCValue *args)
+{
+ const char *data = ephy_web_extension_get_option_ui_page (self);
+
+ if (data) {
+ EphyEmbed *embed;
+ EphyShell *shell = ephy_shell_get_default ();
+ WebKitWebView *web_view;
+ GtkWindow *window = gtk_application_get_active_window (GTK_APPLICATION (shell));
+
+ embed = ephy_shell_new_tab (shell,
+ EPHY_WINDOW (window),
+ NULL,
+ EPHY_NEW_TAB_JUMP);
+
+ web_view = EPHY_GET_WEBKIT_WEB_VIEW_FROM_EMBED (embed);
+ webkit_web_view_load_html (web_view, data, NULL);
+ }
+
+ return NULL;
+}
+
+static char *
+runtime_handler_set_uninstall_url (EphyWebExtension *self,
+ char *name,
+ JSCValue *args)
+{
+ return NULL;
+}
+
+static EphyWebExtensionApiHandler runtime_handlers[] = {
+ {"getBrowserInfo", runtime_handler_get_browser_info},
+ {"sendMessage", runtime_handler_send_message},
+ {"openOptionsPage", runtime_handler_open_options_page},
+ {"setUninstallURL", runtime_handler_set_uninstall_url},
+ {NULL, NULL},
+};
+
+char *
+ephy_web_extension_api_runtime_handler (EphyWebExtension *self,
+ char *name,
+ JSCValue *args)
+{
+ guint idx;
+
+ for (idx = 0; idx < G_N_ELEMENTS (runtime_handlers); idx++) {
+ EphyWebExtensionApiHandler handler = runtime_handlers[idx];
+
+ if (g_strcmp0 (handler.name, name) == 0)
+ return handler.execute (self, name, args);
+ }
+
+ g_warning ("%s(): '%s' not implemented by Epiphany!", __FUNCTION__, name);
+
+ return NULL;
+}
diff --git a/src/webextension/api/runtime.h b/src/webextension/api/runtime.h
new file mode 100644
index 000000000..207901700
--- /dev/null
+++ b/src/webextension/api/runtime.h
@@ -0,0 +1,32 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Copyright © 2019-2020 Jan-Michael Brummer <jan.brummer@tabos.org>
+ *
+ * This file is part of Epiphany.
+ *
+ * Epiphany is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Epiphany is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Epiphany. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+#pragma once
+
+#include "ephy-web-extension.h"
+
+G_BEGIN_DECLS
+
+char *ephy_web_extension_api_runtime_handler (EphyWebExtension *self,
+ char *name,
+ JSCValue *args);
+
+G_END_DECLS
diff --git a/src/webextension/api/tabs.c b/src/webextension/api/tabs.c
new file mode 100644
index 000000000..162724508
--- /dev/null
+++ b/src/webextension/api/tabs.c
@@ -0,0 +1,203 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Copyright © 2019-2020 Jan-Michael Brummer <jan.brummer@tabos.org>
+ *
+ * This file is part of Epiphany.
+ *
+ * Epiphany is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Epiphany is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Epiphany. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "ephy-shell.h"
+#include "ephy-window.h"
+
+#include "tabs.h"
+
+static void
+add_web_view_to_json (JsonBuilder *builder,
+ EphyWebView *web_view)
+{
+ json_builder_begin_object (builder);
+ json_builder_set_member_name (builder, "url");
+ json_builder_add_string_value (builder, ephy_web_view_get_address (web_view));
+ json_builder_set_member_name (builder, "id");
+ json_builder_add_int_value (builder, ephy_web_view_get_uid (web_view));
+ json_builder_end_object (builder);
+}
+
+static char *
+tabs_handler_query (EphyWebExtension *self,
+ char *name,
+ JSCValue *args)
+{
+ g_autoptr (JsonBuilder) builder = json_builder_new ();
+ g_autoptr (JsonNode) root = NULL;
+ EphyShell *shell = ephy_shell_get_default ();
+ GtkWindow *window;
+ GtkWidget *notebook;
+ gboolean current_window = TRUE;
+ gboolean active = TRUE;
+
+ if (jsc_value_object_has_property (args, "active")) {
+ g_autoptr (JSCValue) value = NULL;
+
+ value = jsc_value_object_get_property (args, "active");
+ active = jsc_value_to_boolean (value);
+ }
+
+ if (jsc_value_object_has_property (args, "currentWindow")) {
+ g_autoptr (JSCValue) value = NULL;
+
+ value = jsc_value_object_get_property (args, "currentWindow");
+ current_window = jsc_value_to_boolean (value);
+ }
+
+ if (current_window) {
+ window = gtk_application_get_active_window (GTK_APPLICATION (shell));
+ notebook = ephy_window_get_notebook (EPHY_WINDOW (window));
+
+ json_builder_begin_array (builder);
+
+ if (active) {
+ GtkWidget *page = gtk_notebook_get_nth_page (GTK_NOTEBOOK (notebook), gtk_notebook_get_current_page (GTK_NOTEBOOK (notebook)));
+ EphyWebView *tmp_webview = ephy_embed_get_web_view (EPHY_EMBED (page));
+
+ add_web_view_to_json (builder, tmp_webview);
+ } else {
+ for (int i = 0; i < gtk_notebook_get_n_pages (GTK_NOTEBOOK (notebook)); i++) {
+ GtkWidget *page = gtk_notebook_get_nth_page (GTK_NOTEBOOK (notebook), i);
+ EphyWebView *tmp_webview = ephy_embed_get_web_view (EPHY_EMBED (page));
+
+ add_web_view_to_json (builder, tmp_webview);
+ }
+ }
+
+ json_builder_end_array (builder);
+ }
+
+ root = json_builder_get_root (builder);
+
+ return json_to_string (root, FALSE);
+}
+
+static char *
+tabs_handler_insert_css (EphyWebExtension *self,
+ char *name,
+ JSCValue *args)
+{
+ EphyShell *shell = ephy_shell_get_default ();
+ WebKitUserContentManager *ucm = webkit_web_view_get_user_content_manager (WEBKIT_WEB_VIEW (ephy_shell_get_active_web_view (shell)));
+ WebKitUserStyleSheet *css = NULL;
+ g_autoptr (JSCValue) code = NULL;
+
+ code = jsc_value_object_get_property (args, "code");
+ css = ephy_web_extension_add_custom_css (self, jsc_value_to_string (code));
+
+ if (css)
+ webkit_user_content_manager_add_style_sheet (ucm, css);
+
+ return NULL;
+}
+
+static char *
+tabs_handler_remove_css (EphyWebExtension *self,
+ char *name,
+ JSCValue *args)
+{
+ EphyShell *shell = ephy_shell_get_default ();
+ JSCValue *code;
+ WebKitUserStyleSheet *css = NULL;
+ WebKitUserContentManager *ucm = webkit_web_view_get_user_content_manager (WEBKIT_WEB_VIEW (ephy_shell_get_active_web_view (shell)));
+
+ code = jsc_value_object_get_property (args, "code");
+ css = ephy_web_extension_get_custom_css (self, jsc_value_to_string (code));
+ if (css)
+ webkit_user_content_manager_remove_style_sheet (ucm, css);
+
+ return NULL;
+}
+
+static char *
+tabs_handler_get (EphyWebExtension *self,
+ char *name,
+ JSCValue *args)
+{
+ EphyShell *shell = ephy_shell_get_default ();
+ g_autoptr (JsonBuilder) builder = json_builder_new ();
+ g_autoptr (JsonNode) root = NULL;
+ EphyWebView *tmp_webview = ephy_shell_get_active_web_view (shell);
+
+ add_web_view_to_json (builder, tmp_webview);
+ root = json_builder_get_root (builder);
+
+ return json_to_string (root, FALSE);
+}
+
+static char *
+tabs_handler_execute_script (EphyWebExtension *self,
+ char *name,
+ JSCValue *args)
+{
+ g_autoptr (JSCValue) code_value = NULL;
+ g_autoptr (JSCValue) obj = NULL;
+ EphyShell *shell = ephy_shell_get_default ();
+
+ if (jsc_value_is_array (args)) {
+ obj = jsc_value_object_get_property_at_index (args, 1);
+ } else {
+ obj = args;
+ }
+
+ code_value = jsc_value_object_get_property (obj, "code");
+ if (code_value) {
+ g_autofree char *code = jsc_value_to_string (code_value);
+ webkit_web_view_run_javascript_in_world (WEBKIT_WEB_VIEW (ephy_shell_get_active_web_view (shell)),
+ code,
+ ephy_embed_shell_get_guid (ephy_embed_shell_get_default ()),
+ NULL,
+ NULL,
+ NULL);
+ }
+
+ return NULL;
+}
+
+static EphyWebExtensionApiHandler tabs_handlers[] = {
+ {"query", tabs_handler_query},
+ {"insertCSS", tabs_handler_insert_css},
+ {"removeCSS", tabs_handler_remove_css},
+ {"get", tabs_handler_get},
+ {"executeScript", tabs_handler_execute_script},
+ {NULL, NULL},
+};
+
+char *
+ephy_web_extension_api_tabs_handler (EphyWebExtension *self,
+ char *name,
+ JSCValue *args)
+{
+ guint idx;
+
+ for (idx = 0; idx < G_N_ELEMENTS (tabs_handlers); idx++) {
+ EphyWebExtensionApiHandler handler = tabs_handlers[idx];
+
+ if (g_strcmp0 (handler.name, name) == 0)
+ return handler.execute (self, name, args);
+ }
+
+ g_warning ("%s(): '%s' not implemented by Epiphany!", __FUNCTION__, name);
+
+ return NULL;
+}
diff --git a/src/webextension/api/tabs.h b/src/webextension/api/tabs.h
new file mode 100644
index 000000000..92ba490cc
--- /dev/null
+++ b/src/webextension/api/tabs.h
@@ -0,0 +1,34 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Copyright © 2019-2020 Jan-Michael Brummer <jan.brummer@tabos.org>
+ *
+ * This file is part of Epiphany.
+ *
+ * Epiphany is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Epiphany is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Epiphany. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+#pragma once
+
+#include "ephy-web-extension.h"
+
+#include <webkit2/webkit2.h>
+
+G_BEGIN_DECLS
+
+char *ephy_web_extension_api_tabs_handler (EphyWebExtension *self,
+ char *name,
+ JSCValue *value);
+
+G_END_DECLS
diff --git a/src/webextension/ephy-web-extension-manager.c b/src/webextension/ephy-web-extension-manager.c
new file mode 100644
index 000000000..4208c50d2
--- /dev/null
+++ b/src/webextension/ephy-web-extension-manager.c
@@ -0,0 +1,968 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Copyright © 2019-2020 Jan-Michael Brummer <jan.brummer@tabos.org>
+ *
+ * This file is part of Epiphany.
+ *
+ * Epiphany is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Epiphany is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Epiphany. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "ephy-debug.h"
+#include "ephy-embed-shell.h"
+#include "ephy-embed-prefs.h"
+#include "ephy-embed-utils.h"
+#include "ephy-file-helpers.h"
+#include "ephy-header-bar.h"
+#include "ephy-location-entry.h"
+#include "ephy-notification.h"
+#include "ephy-settings.h"
+#include "ephy-shell.h"
+#include "ephy-string.h"
+#include "ephy-web-extension.h"
+#include "ephy-web-extension-manager.h"
+#include "ephy-web-view.h"
+
+#include "api/notifications.h"
+#include "api/pageaction.h"
+#include "api/runtime.h"
+#include "api/tabs.h"
+
+#include <json-glib/json-glib.h>
+
+struct _EphyWebExtensionManager {
+ GObject parent_instance;
+
+ GCancellable *cancellable;
+ GList *web_extensions;
+ GHashTable *page_action_map;
+ GHashTable *browser_action_map;
+ GHashTable *background_web_views;
+};
+
+G_DEFINE_TYPE (EphyWebExtensionManager, ephy_web_extension_manager, G_TYPE_OBJECT)
+
+EphyWebExtensionApiHandler api_handlers[] = {
+ {"notifications", ephy_web_extension_api_notifications_handler},
+ {"pageAction", ephy_web_extension_api_pageaction_handler},
+ {"runtime", ephy_web_extension_api_runtime_handler},
+ {"tabs", ephy_web_extension_api_tabs_handler},
+ {NULL, NULL},
+};
+
+enum {
+ CHANGED,
+ LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL];
+
+static void
+ephy_web_extension_manager_add_to_list (EphyWebExtensionManager *self,
+ EphyWebExtension *web_extension)
+{
+ self->web_extensions = g_list_append (self->web_extensions, g_object_ref (web_extension));
+
+ g_signal_emit (self, signals[CHANGED], 0);
+}
+
+static void
+ephy_web_extension_manager_remove_from_list (EphyWebExtensionManager *self,
+ EphyWebExtension *web_extension)
+{
+ self->web_extensions = g_list_remove (self->web_extensions, web_extension);
+ g_object_unref (web_extension);
+
+ g_signal_emit (self, signals[CHANGED], 0);
+}
+
+void
+on_web_extension_loaded (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ g_autoptr (GError) error = NULL;
+ EphyWebExtension *web_extension;
+ EphyWebExtensionManager *self = EPHY_WEB_EXTENSION_MANAGER (user_data);
+
+
+ web_extension = ephy_web_extension_load_finished (source_object, result, &error);
+ if (!web_extension) {
+ return;
+ }
+
+ ephy_web_extension_manager_add_to_list (self, web_extension);
+ g_object_unref (web_extension);
+
+ if (ephy_web_extension_manager_is_active (self, web_extension))
+ ephy_web_extension_manager_set_active (self, web_extension, TRUE);
+}
+
+static void
+ephy_web_extension_manager_scan_directory (EphyWebExtensionManager *self,
+ const char *extension_dir)
+{
+ g_autoptr (GDir) dir = NULL;
+ g_autoptr (GError) error = NULL;
+ const char *directory;
+
+ if (g_mkdir_with_parents (extension_dir, 0700) != 0)
+ g_warning ("Failed to create %s: %s", extension_dir, g_strerror (errno));
+
+ if (!g_file_test (extension_dir, G_FILE_TEST_EXISTS))
+ g_mkdir_with_parents (extension_dir, 0700);
+
+ dir = g_dir_open (extension_dir, 0, &error);
+ if (!dir) {
+ g_warning ("Could not open %s: %s", extension_dir, error->message);
+ return;
+ }
+
+ errno = 0;
+ while ((directory = g_dir_read_name (dir))) {
+ g_autofree char *filename = NULL;
+ g_autoptr (GFile) file = NULL;
+
+ if (errno != 0) {
+ g_warning ("Problem reading %s: %s", extension_dir, g_strerror (errno));
+ break;
+ }
+
+ filename = g_build_filename (extension_dir, directory, NULL);
+ file = g_file_new_for_path (filename);
+
+ ephy_web_extension_load_async (file, self->cancellable, on_web_extension_loaded, self);
+
+ errno = 0;
+ }
+}
+
+static void
+ephy_web_extension_manager_constructed (GObject *object)
+{
+ EphyWebExtensionManager *self = EPHY_WEB_EXTENSION_MANAGER (object);
+ g_autofree char *dir = g_build_filename (ephy_default_profile_dir (), "web_extensions", NULL);
+
+ self->background_web_views = g_hash_table_new (NULL, NULL);
+ self->page_action_map = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_hash_table_destroy);
+ self->browser_action_map = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)gtk_widget_destroy);
+ self->web_extensions = NULL;
+
+ ephy_web_extension_manager_scan_directory (self, dir);
+}
+
+static void
+ephy_web_extension_manager_dispose (GObject *object)
+{
+ EphyWebExtensionManager *self = EPHY_WEB_EXTENSION_MANAGER (object);
+
+ g_clear_pointer (&self->background_web_views, g_hash_table_destroy);
+ g_clear_pointer (&self->page_action_map, g_hash_table_destroy);
+ g_list_free_full (g_steal_pointer (&self->web_extensions), g_object_unref);
+}
+
+static void
+ephy_web_extension_manager_class_init (EphyWebExtensionManagerClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->constructed = ephy_web_extension_manager_constructed;
+ object_class->dispose = ephy_web_extension_manager_dispose;
+
+ signals[CHANGED] =
+ g_signal_new ("changed",
+ G_OBJECT_CLASS_TYPE (object_class),
+ G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
+ 0, NULL, NULL, NULL,
+ G_TYPE_NONE, 0);
+}
+
+static void
+ephy_web_extension_manager_init (EphyWebExtensionManager *self)
+{
+}
+
+EphyWebExtensionManager *ephy_web_extension_manager_new (void)
+{
+ return g_object_new (EPHY_TYPE_WEB_EXTENSION_MANAGER, NULL);
+}
+
+GList *
+ephy_web_extension_manager_get_web_extensions (EphyWebExtensionManager *self)
+{
+ return self->web_extensions;
+}
+
+/**
+ * Installs/Adds all web_extensions to new EphyWindow.
+ */
+void
+ephy_web_extension_manager_install_actions (EphyWebExtensionManager *self,
+ EphyWindow *window)
+{
+ for (GList *list = self->web_extensions; list && list->data; list = list->next)
+ ephy_web_extension_manager_add_web_extension_to_window (self, list->data, window);
+}
+
+void
+on_new_web_extension_loaded (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ g_autoptr (GError) error = NULL;
+ EphyWebExtension *web_extension;
+ EphyWebExtensionManager *self = EPHY_WEB_EXTENSION_MANAGER (user_data);
+
+ web_extension = ephy_web_extension_load_finished (source_object, result, &error);
+ if (!web_extension) {
+ return;
+ }
+
+ ephy_web_extension_manager_add_to_list (self, web_extension);
+}
+/**
+ * Install a new web web_extension into the local web_extension directory.
+ * File should only point to a manifest.json or a .xpi file
+ */
+void
+ephy_web_extension_manager_install (EphyWebExtensionManager *self,
+ GFile *file)
+{
+ g_autoptr (GFile) target = NULL;
+ g_autofree char *basename = NULL;
+ gboolean is_xpi = FALSE;
+
+ basename = g_file_get_basename (file);
+ is_xpi = g_str_has_suffix (basename, ".xpi");
+
+ if (!is_xpi) {
+ g_autoptr (GFile) source = NULL;
+
+ /* Get parent directory */
+ source = g_file_get_parent (file);
+ target = g_file_new_build_filename (ephy_default_profile_dir (), "web_extensions", g_file_get_basename (source), NULL);
+
+ ephy_copy_directory (g_file_get_path (source), g_file_get_path (target));
+ } else {
+ g_autoptr (GError) error = NULL;
+ target = g_file_new_build_filename (ephy_default_profile_dir (), "web_extensions", g_file_get_basename (file), NULL);
+
+ if (!g_file_copy (file, target, G_FILE_COPY_NONE, NULL, NULL, NULL, &error)) {
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_EXISTS)) {
+ g_warning ("Could not copy file for web_extensions: %s", error->message);
+ return;
+ }
+ }
+ }
+
+ if (target)
+ ephy_web_extension_load_async (g_steal_pointer (&target), self->cancellable, on_new_web_extension_loaded, self);
+}
+
+void
+ephy_web_extension_manager_uninstall (EphyWebExtensionManager *self,
+ EphyWebExtension *web_extension)
+{
+ if (ephy_web_extension_manager_is_active (self, web_extension))
+ ephy_web_extension_manager_set_active (self, web_extension, FALSE);
+
+ ephy_web_extension_remove (web_extension);
+ ephy_web_extension_manager_remove_from_list (self, web_extension);
+}
+
+void
+ephy_web_extension_manager_update_location_entry (EphyWebExtensionManager *self,
+ EphyWindow *window)
+{
+ GtkWidget *title_widget;
+ EphyLocationEntry *lentry;
+ GtkWidget *notebook = ephy_window_get_notebook (EPHY_WINDOW (window));
+ int current_page = gtk_notebook_get_current_page (GTK_NOTEBOOK (notebook));
+ GtkWidget *page = gtk_notebook_get_nth_page (GTK_NOTEBOOK (notebook), current_page);
+ EphyWebView *web_view;
+
+ if (!page)
+ return;
+
+ web_view = ephy_embed_get_web_view (EPHY_EMBED (page));
+ title_widget = GTK_WIDGET (ephy_header_bar_get_title_widget (EPHY_HEADER_BAR (ephy_window_get_header_bar (window))));
+ if (!EPHY_IS_LOCATION_ENTRY (title_widget))
+ return;
+
+ lentry = EPHY_LOCATION_ENTRY (title_widget);
+
+ ephy_location_entry_page_action_clear (lentry);
+
+ for (GList *list = ephy_web_extension_manager_get_web_extensions (self); list && list->data; list = list->next) {
+ EphyWebExtension *web_extension = EPHY_WEB_EXTENSION (list->data);
+ GtkWidget *action = ephy_web_extension_manager_get_page_action (self, web_extension, web_view);
+
+ if (action)
+ ephy_location_entry_page_action_add (lentry, action);
+ }
+}
+
+EphyWebView *
+ephy_web_extension_manager_get_background_web_view (EphyWebExtensionManager *self,
+ EphyWebExtension *web_extension)
+{
+ return g_hash_table_lookup (self->background_web_views, web_extension);
+}
+
+static void
+ephy_web_extension_manager_set_background_web_view (EphyWebExtensionManager *self,
+ EphyWebExtension *web_extension,
+ EphyWebView *web_view)
+{
+ g_hash_table_insert (self->background_web_views, web_extension, web_view);
+}
+
+static gboolean
+page_action_clicked (GtkWidget *event_box,
+ GdkEventButton *event,
+ gpointer user_data)
+{
+ EphyWebExtension *web_extension = EPHY_WEB_EXTENSION (user_data);
+ EphyShell *shell = ephy_shell_get_default ();
+ EphyWebView *view = EPHY_WEB_VIEW (ephy_shell_get_active_web_view (shell));
+ g_autoptr (JsonBuilder) builder = json_builder_new ();
+ g_autoptr (JsonNode) root = NULL;
+ g_autofree char *json = NULL;
+ g_autofree char *script = NULL;
+ EphyWebExtensionManager *self = ephy_shell_get_web_extension_manager (shell);
+ WebKitWebView *web_view = WEBKIT_WEB_VIEW (ephy_web_extension_manager_get_background_web_view (self, web_extension));
+
+ json_builder_begin_object (builder);
+ json_builder_set_member_name (builder, "url");
+ json_builder_add_string_value (builder, ephy_web_view_get_address (view));
+ json_builder_set_member_name (builder, "id");
+ json_builder_add_int_value (builder, ephy_web_view_get_uid (view));
+ json_builder_end_object (builder);
+
+ root = json_builder_get_root (builder);
+
+ json = json_to_string (root, FALSE);
+
+ script = g_strdup_printf ("pageActionOnClicked(%s);", json);
+ webkit_web_view_run_javascript_in_world (web_view,
+ script,
+ ephy_embed_shell_get_guid (EPHY_EMBED_SHELL (shell)),
+ NULL,
+ NULL,
+ NULL);
+
+ return GDK_EVENT_STOP;
+}
+
+static GtkWidget *
+create_page_action_widget (EphyWebExtensionManager *self,
+ EphyWebExtension *web_extension)
+{
+ GtkWidget *image;
+ GtkWidget *event_box;
+
+ /* Create new event box with page action */
+ event_box = gtk_event_box_new ();
+ image = gtk_image_new ();
+ gtk_container_add (GTK_CONTAINER (event_box), image);
+ g_signal_connect_object (event_box, "button_press_event", G_CALLBACK (page_action_clicked), web_extension, 0);
+ gtk_widget_show_all (event_box);
+
+ return g_object_ref (event_box);
+}
+
+static void
+ephy_web_extension_handle_background_script_message (WebKitUserContentManager *ucm,
+ WebKitJavascriptResult *js_result,
+ gpointer user_data)
+{
+ EphyWebExtension *web_extension = EPHY_WEB_EXTENSION (user_data);
+ JSCValue *value = webkit_javascript_result_get_js_value (js_result);
+ EphyWebExtensionManager *self = ephy_shell_get_web_extension_manager (ephy_shell_get_default ());
+ WebKitWebView *web_view = WEBKIT_WEB_VIEW (ephy_web_extension_manager_get_background_web_view (self, web_extension));
+ g_autofree char *name_str = NULL;
+ g_autoptr (JSCValue) name = NULL;
+ g_autoptr (JSCValue) promise = NULL;
+ g_auto (GStrv) split = NULL;
+ GPtrArray *permissions = ephy_web_extension_get_permissions (web_extension);
+ unsigned int idx;
+
+ if (!jsc_value_is_object (value))
+ return;
+
+ if (!jsc_value_object_has_property (value, "promise"))
+ return;
+
+ promise = jsc_value_object_get_property (value, "promise");
+ if (!jsc_value_is_number (promise))
+ return;
+
+ name = jsc_value_object_get_property (value, "fn");
+ if (!name)
+ return;
+
+ name_str = jsc_value_to_string (name);
+ LOG ("%s(): Called for %s, function %s\n", __FUNCTION__, ephy_web_extension_get_name (web_extension), name_str);
+
+ split = g_strsplit (name_str, ".", 2);
+ if (g_strv_length (split) != 2) {
+ g_warning ("Invalid function call, aborting: %s", name_str);
+ return;
+ }
+
+ for (idx = 0; idx < G_N_ELEMENTS (api_handlers); idx++) {
+ EphyWebExtensionApiHandler handler = api_handlers[idx];
+
+ if (!g_ptr_array_find (permissions, split[0], NULL)) {
+ LOG ("%s(): Requested api is not part of the permissions, aborting\n", __FUNCTION__);
+ /* TODO: Permissions are not working yet */
+ /*return; */
+ }
+
+ if (g_strcmp0 (handler.name, split[0]) == 0) {
+ g_autofree char *ret = NULL;
+ g_autofree char *script = NULL;
+ g_autoptr (JSCValue) args = jsc_value_object_get_property (value, "args");
+
+ ret = handler.execute (web_extension, split[1], args);
+ script = g_strdup_printf ("promises[%.f].resolve(%s);", jsc_value_to_double (promise), ret ? ret : "");
+ webkit_web_view_run_javascript_in_world (web_view, script, ephy_embed_shell_get_guid (ephy_embed_shell_get_default ()), NULL, NULL, NULL);
+
+ return;
+ }
+ }
+
+ g_warning ("%s(): '%s' not implemented by Epiphany!", __FUNCTION__, name_str);
+}
+
+static void
+add_content_scripts (EphyWebExtension *web_extension,
+ EphyWebView *web_view)
+{
+ GList *content_scripts = ephy_web_extension_get_content_scripts (web_extension);
+ WebKitUserContentManager *ucm;
+
+ if (!content_scripts)
+ return;
+
+ ucm = webkit_web_view_get_user_content_manager (WEBKIT_WEB_VIEW (web_view));
+ g_signal_connect_object (ucm, "script-message-received", G_CALLBACK (ephy_web_extension_handle_background_script_message), web_extension, 0);
+ webkit_user_content_manager_register_script_message_handler_in_world (ucm, "epiphany", ephy_embed_shell_get_guid (ephy_embed_shell_get_default ()));
+
+ for (GList *list = content_scripts; list && list->data; list = list->next) {
+ GList *js_list = ephy_web_extension_get_content_script_js (web_extension, list->data);
+
+ for (GList *tmp_list = js_list; tmp_list && tmp_list->data; tmp_list = tmp_list->next) {
+ webkit_user_content_manager_add_script (WEBKIT_USER_CONTENT_MANAGER (ucm), tmp_list->data);
+ }
+ }
+}
+
+static void
+remove_content_scripts (EphyWebExtension *self,
+ EphyWebView *web_view)
+{
+ GList *content_scripts = ephy_web_extension_get_content_scripts (self);
+ WebKitUserContentManager *ucm;
+
+ if (!content_scripts)
+ return;
+
+ ucm = webkit_web_view_get_user_content_manager (WEBKIT_WEB_VIEW (web_view));
+
+ for (GList *list = content_scripts; list && list->data; list = list->next) {
+ GList *js_list = ephy_web_extension_get_content_script_js (self, list->data);
+
+ for (GList *tmp_list = js_list; tmp_list && tmp_list->data; tmp_list = tmp_list->next)
+ webkit_user_content_manager_remove_script (WEBKIT_USER_CONTENT_MANAGER (ucm), tmp_list->data);
+ }
+
+ g_signal_handlers_disconnect_by_func (ucm, G_CALLBACK (ephy_web_extension_handle_background_script_message), self);
+}
+
+static void
+remove_custom_css (EphyWebExtension *self,
+ EphyWebView *web_view)
+{
+ GList *custom_css = ephy_web_extension_get_custom_css_list (self);
+ GList *list;
+ WebKitUserContentManager *ucm;
+
+ if (!custom_css)
+ return;
+
+ ucm = webkit_web_view_get_user_content_manager (WEBKIT_WEB_VIEW (web_view));
+
+ for (list = custom_css; list && list->data; list = list->next)
+ webkit_user_content_manager_remove_style_sheet (WEBKIT_USER_CONTENT_MANAGER (ucm), ephy_web_extension_custom_css_style (self, list->data));
+}
+
+static void
+update_translations (EphyWebExtension *web_extension)
+{
+ /* TODO: Use current locale and fallback to default web_extension locale if necessary */
+ g_autofree char *path = g_strdup_printf ("_locales/%s/messages.json", "en");
+ g_autofree char *data = NULL;
+ gint length = 0;
+
+ data = ephy_web_extension_get_resource_as_string (web_extension, path);
+ if (data)
+ length = strlen (data);
+
+ webkit_web_context_send_message_to_all_extensions (ephy_embed_shell_get_web_context (ephy_embed_shell_get_default ()),
+ webkit_user_message_new ("WebExtension.Add",
+ g_variant_new ("(sst)", ephy_web_extension_get_name (web_extension), data ? (char *)data : "", length)));
+}
+
+static void
+ephy_web_extension_manager_add_web_extension_to_webview (EphyWebExtensionManager *self,
+ EphyWebExtension *web_extension,
+ EphyWindow *window,
+ EphyWebView *web_view)
+{
+ GtkWidget *title_widget = GTK_WIDGET (ephy_header_bar_get_title_widget (EPHY_HEADER_BAR (ephy_window_get_header_bar (window))));
+ EphyLocationEntry *lentry = NULL;
+
+ if (EPHY_IS_LOCATION_ENTRY (title_widget)) {
+ lentry = EPHY_LOCATION_ENTRY (title_widget);
+
+ if (lentry && ephy_web_extension_has_page_action (web_extension)) {
+ GtkWidget *page_action = create_page_action_widget (self, web_extension);
+ GHashTable *table;
+
+ table = g_hash_table_lookup (self->page_action_map, web_extension);
+ if (!table) {
+ table = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)gtk_widget_destroy);
+ g_hash_table_insert (self->page_action_map, web_extension, table);
+ }
+
+ g_hash_table_insert (table, web_view, g_steal_pointer (&page_action));
+ }
+ }
+
+ update_translations (web_extension);
+ add_content_scripts (web_extension, web_view);
+}
+
+static void
+page_added_cb (GtkNotebook *notebook,
+ GtkWidget *child,
+ guint page_num,
+ gpointer user_data)
+{
+ EphyWebExtension *web_extension = EPHY_WEB_EXTENSION (user_data);
+ EphyWebView *web_view = ephy_embed_get_web_view (EPHY_EMBED (child));
+ EphyWindow *window = EPHY_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (notebook)));
+ EphyWebExtensionManager *self = ephy_shell_get_web_extension_manager (ephy_shell_get_default ());
+
+
+ ephy_web_extension_manager_add_web_extension_to_webview (self, web_extension, window, web_view);
+ ephy_web_extension_manager_update_location_entry (self, window);
+}
+
+static void
+web_extension_cb (WebKitURISchemeRequest *request,
+ gpointer user_data)
+{
+ EphyWebExtension *web_extension = EPHY_WEB_EXTENSION (user_data);
+ const char *path;
+ const unsigned char *data;
+ gsize length;
+ g_autoptr (GInputStream) stream = NULL;
+
+ path = webkit_uri_scheme_request_get_path (request);
+
+ data = ephy_web_extension_get_resource (web_extension, path + 1, &length);
+ if (!data)
+ return;
+
+ stream = g_memory_input_stream_new_from_data (data, length, NULL);
+ webkit_uri_scheme_request_finish (request, stream, length, NULL);
+}
+
+static void
+init_web_extension_api (WebKitWebContext *web_context,
+ EphyWebExtension *web_extension)
+{
+ g_autoptr (GVariant) user_data = NULL;
+
+#if DEVELOPER_MODE
+ webkit_web_context_set_web_extensions_directory (web_context, BUILD_ROOT "/embed/web-process-extension");
+#else
+ webkit_web_context_set_web_extensions_directory (web_context, EPHY_WEB_PROCESS_EXTENSIONS_DIR);
+#endif
+
+ user_data = g_variant_new ("(smsbb)",
+ "",
+ ephy_profile_dir_is_default () ? NULL : ephy_profile_dir (),
+ FALSE,
+ FALSE);
+ webkit_web_context_set_web_extensions_initialization_user_data (web_context, g_steal_pointer (&user_data));
+}
+
+static GtkWidget *
+create_web_extensions_webview (EphyWebExtension *web_extension,
+ gboolean custom_web_context)
+{
+ WebKitUserContentManager *ucm;
+ WebKitWebContext *web_context;
+ WebKitSettings *settings;
+ GtkWidget *web_view;
+
+ /* Create an own ucm so new scripts/css are only applied to this web_view */
+ ucm = webkit_user_content_manager_new ();
+ g_signal_connect_object (ucm, "script-message-received", G_CALLBACK (ephy_web_extension_handle_background_script_message), web_extension, 0);
+
+ if (!custom_web_context) {
+ /* Get webcontext and register web_extension scheme */
+ webkit_user_content_manager_register_script_message_handler_in_world (ucm,
+ "epiphany",
+ ephy_embed_shell_get_guid (ephy_embed_shell_get_default ()));
+ web_context = ephy_embed_shell_get_web_context (ephy_embed_shell_get_default ());
+ webkit_web_context_register_uri_scheme (web_context, "webextension", web_extension_cb, web_extension, NULL);
+ webkit_security_manager_register_uri_scheme_as_secure (webkit_web_context_get_security_manager (web_context),
+ "webextension");
+ web_view = ephy_web_view_new_with_user_content_manager (ucm);
+ } else {
+ webkit_user_content_manager_register_script_message_handler (ucm, "epiphany");
+ web_context = webkit_web_context_new ();
+ webkit_web_context_register_uri_scheme (web_context, "webextension", web_extension_cb, web_extension, NULL);
+ g_signal_connect_object (web_context, "initialize-web_extensions", G_CALLBACK (init_web_extension_api), web_extension, 0);
+ webkit_security_manager_register_uri_scheme_as_secure (webkit_web_context_get_security_manager (web_context),
+ "webextension");
+ web_view = g_object_new (EPHY_TYPE_WEB_VIEW,
+ "web-context", web_context,
+ "user-content-manager", ucm,
+ "settings", ephy_embed_prefs_get_settings (),
+ NULL);
+ }
+
+ settings = webkit_web_view_get_settings (WEBKIT_WEB_VIEW (web_view));
+ webkit_settings_set_enable_write_console_messages_to_stdout (settings, TRUE);
+
+ update_translations (web_extension);
+
+ return web_view;
+}
+
+static GtkWidget *
+create_browser_popup (EphyWebExtension *web_extension)
+{
+ GtkWidget *web_view;
+ GtkWidget *popover;
+ g_autofree char *data = NULL;
+ g_autofree char *base_uri = NULL;
+ g_autofree char *dir_name = NULL;
+ const char *popup;
+
+ popover = gtk_popover_new (NULL);
+
+ web_view = create_web_extensions_webview (web_extension, TRUE);
+
+ gtk_widget_set_hexpand (web_view, TRUE);
+ gtk_widget_set_vexpand (web_view, TRUE);
+
+ popup = ephy_web_extension_get_browser_popup (web_extension);
+ dir_name = g_path_get_dirname (popup);
+ base_uri = g_strdup_printf ("webextension:///%s/", dir_name);
+ data = ephy_web_extension_get_resource_as_string (web_extension, popup);
+ webkit_web_view_load_html (WEBKIT_WEB_VIEW (web_view), (char *)data, base_uri);
+ gtk_container_add (GTK_CONTAINER (popover), web_view);
+ gtk_widget_show_all (web_view);
+
+ return popover;
+}
+
+static gboolean
+on_browser_action_clicked (GtkWidget *event_box,
+ gpointer user_data)
+{
+ EphyShell *shell = ephy_shell_get_default ();
+ EphyWebExtension *web_extension = EPHY_WEB_EXTENSION (user_data);
+ EphyWebExtensionManager *self = ephy_shell_get_web_extension_manager (ephy_shell_get_default ());
+ g_autofree char *script = NULL;
+ WebKitWebView *web_view = NULL;
+ gboolean own_web_view = !!ephy_web_extension_background_web_view_get_page (web_extension);
+
+ if (!own_web_view)
+ web_view = WEBKIT_WEB_VIEW (ephy_web_extension_manager_get_background_web_view (self, web_extension));
+ else
+ web_view = WEBKIT_WEB_VIEW (ephy_shell_get_active_web_view (shell));
+
+ script = g_strdup_printf ("browserActionClicked();");
+
+ webkit_web_view_run_javascript_in_world (web_view,
+ script,
+ ephy_embed_shell_get_guid (ephy_embed_shell_get_default ()),
+ NULL,
+ NULL,
+ NULL);
+
+ return GDK_EVENT_STOP;
+}
+
+
+GtkWidget *
+create_browser_action (EphyWebExtension *web_extension)
+{
+ GtkWidget *button;
+ GtkWidget *image;
+ GtkWidget *popover;
+
+ if (ephy_web_extension_get_browser_popup (web_extension)) {
+ button = gtk_menu_button_new ();
+ image = gtk_image_new_from_pixbuf (ephy_web_extension_browser_action_get_icon (web_extension, 16));
+ popover = create_browser_popup (web_extension);
+ gtk_menu_button_set_popover (GTK_MENU_BUTTON (button), popover);
+
+ gtk_button_set_image (GTK_BUTTON (button), image);
+ gtk_widget_set_visible (button, TRUE);
+ } else {
+ GdkPixbuf *pixbuf = ephy_web_extension_browser_action_get_icon (web_extension, 16);
+
+ button = gtk_button_new ();
+
+ if (pixbuf)
+ image = gtk_image_new_from_pixbuf (pixbuf);
+ else
+ image = gtk_image_new_from_icon_name ("application-x-addon-symbolic", GTK_ICON_SIZE_BUTTON);
+
+ g_signal_connect_object (button, "clicked", G_CALLBACK (on_browser_action_clicked), web_extension, 0);
+ gtk_button_set_image (GTK_BUTTON (button), image);
+ gtk_widget_set_visible (button, TRUE);
+ }
+
+ return button;
+}
+
+void
+ephy_web_extension_manager_add_web_extension_to_window (EphyWebExtensionManager *self,
+ EphyWebExtension *web_extension,
+ EphyWindow *window)
+{
+ GtkWidget *notebook = ephy_window_get_notebook (EPHY_WINDOW (window));
+
+ if (!ephy_web_extension_manager_is_active (self, web_extension))
+ return;
+
+ /* Add page actions and add content script */
+ for (int i = 0; i < gtk_notebook_get_n_pages (GTK_NOTEBOOK (notebook)); i++) {
+ GtkWidget *page = gtk_notebook_get_nth_page (GTK_NOTEBOOK (notebook), i);
+ EphyWebView *web_view = ephy_embed_get_web_view (EPHY_EMBED (page));
+
+ ephy_web_extension_manager_add_web_extension_to_webview (self, web_extension, window, web_view);
+ }
+
+ if (ephy_web_extension_has_browser_action (web_extension)) {
+ GtkWidget *browser_action_widget = create_browser_action (web_extension);
+ ephy_header_bar_add_browser_action (EPHY_HEADER_BAR (ephy_window_get_header_bar (window)), browser_action_widget);
+ g_hash_table_insert (self->browser_action_map, web_extension, browser_action_widget);
+ }
+
+ ephy_web_extension_manager_update_location_entry (self, window);
+ g_signal_connect_object (notebook, "page-added", G_CALLBACK (page_added_cb), web_extension, 0);
+}
+
+static gboolean
+remove_page_action (gpointer key,
+ gpointer value,
+ gpointer user_data)
+{
+ return TRUE;
+}
+
+void
+ephy_web_extension_manager_remove_web_extension_from_webview (EphyWebExtensionManager *self,
+ EphyWebExtension *web_extension,
+ EphyWindow *window,
+ EphyWebView *web_view)
+{
+ GtkWidget *title_widget = GTK_WIDGET (ephy_header_bar_get_title_widget (EPHY_HEADER_BAR (ephy_window_get_header_bar (window))));
+ EphyLocationEntry *lentry = NULL;
+
+ if (EPHY_IS_LOCATION_ENTRY (title_widget))
+ lentry = EPHY_LOCATION_ENTRY (title_widget);
+
+ g_hash_table_foreach_remove (self->page_action_map, remove_page_action, web_view);
+
+ if (lentry)
+ ephy_location_entry_page_action_clear (lentry);
+
+ remove_content_scripts (web_extension, web_view);
+ remove_custom_css (web_extension, web_view);
+}
+
+void
+ephy_web_extension_manager_remove_web_extension_from_window (EphyWebExtensionManager *self,
+ EphyWebExtension *web_extension,
+ EphyWindow *window)
+{
+ GtkWidget *notebook = ephy_window_get_notebook (EPHY_WINDOW (window));
+ GtkWidget *browser_action_widget;
+
+ if (ephy_web_extension_manager_is_active (self, web_extension))
+ return;
+
+ for (int i = 0; i < gtk_notebook_get_n_pages (GTK_NOTEBOOK (notebook)); i++) {
+ GtkWidget *page = gtk_notebook_get_nth_page (GTK_NOTEBOOK (notebook), i);
+ EphyWebView *web_view = ephy_embed_get_web_view (EPHY_EMBED (page));
+
+ ephy_web_extension_manager_remove_web_extension_from_webview (self, web_extension, window, web_view);
+ }
+
+ browser_action_widget = g_hash_table_lookup (self->browser_action_map, web_extension);
+ if (browser_action_widget) {
+ g_hash_table_remove (self->browser_action_map, web_extension);
+ }
+
+ ephy_web_extension_manager_update_location_entry (self, window);
+
+ g_signal_handlers_disconnect_by_data (notebook, web_extension);
+}
+
+gboolean
+ephy_web_extension_manager_is_active (EphyWebExtensionManager *self,
+ EphyWebExtension *web_extension)
+{
+ g_auto (GStrv) web_extensions_active = g_settings_get_strv (EPHY_SETTINGS_WEB, EPHY_PREFS_WEB_WEBEXTENSIONS_ACTIVE);
+
+ return g_strv_contains ((const char * const *)web_extensions_active, ephy_web_extension_get_name (web_extension));
+}
+
+static void
+run_background_script (EphyWebExtensionManager *self,
+ EphyWebExtension *web_extension)
+{
+ WebKitUserContentManager *ucm;
+ GtkWidget *background;
+ g_autofree char *base_uri = NULL;
+ const char *page;
+
+ if (!ephy_web_extension_has_background_web_view (web_extension) || ephy_web_extension_manager_get_background_web_view (self, web_extension))
+ return;
+
+ page = ephy_web_extension_background_web_view_get_page (web_extension);
+
+ /* Create new background web_view */
+ background = create_web_extensions_webview (web_extension, page != NULL);
+ ephy_web_extension_manager_set_background_web_view (self, web_extension, EPHY_WEB_VIEW (background));
+
+ if (page) {
+ g_autofree char *data = ephy_web_extension_get_resource_as_string (web_extension, page);
+
+ base_uri = g_strdup_printf ("webextension://%s/%s/", ephy_web_extension_get_guid (web_extension), g_path_get_dirname (page));
+ webkit_web_view_load_html (WEBKIT_WEB_VIEW (background), (char *)data, base_uri);
+ } else {
+ GPtrArray *scripts = ephy_web_extension_background_web_view_get_scripts (web_extension);
+
+ ucm = webkit_web_view_get_user_content_manager (WEBKIT_WEB_VIEW (background));
+
+ base_uri = g_strdup_printf ("webextension://%s/", ephy_web_extension_get_guid (web_extension));
+ for (unsigned int i = 0; i < scripts->len; i++) {
+ char *script_file = g_ptr_array_index (scripts, i);
+ g_autofree char *data = NULL;
+ WebKitUserScript *user_script;
+
+ data = ephy_web_extension_get_resource_as_string (web_extension, script_file);
+ user_script = webkit_user_script_new_for_world (data,
+ WEBKIT_USER_CONTENT_INJECT_TOP_FRAME,
+ WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_END,
+ ephy_embed_shell_get_guid (ephy_embed_shell_get_default ()),
+ NULL,
+ NULL);
+
+ webkit_user_content_manager_add_script (ucm, user_script);
+ }
+ webkit_web_view_load_html (WEBKIT_WEB_VIEW (background), "<body></body>", base_uri);
+ }
+}
+
+static GPtrArray *
+strv_to_ptr_array (char **strv)
+{
+ GPtrArray *array = g_ptr_array_new ();
+
+ for (char **str = strv; *str; ++str) {
+ g_ptr_array_add (array, g_strdup (*str));
+ }
+
+ return array;
+}
+
+static gboolean
+extension_equal (gconstpointer a,
+ gconstpointer b)
+{
+ return g_strcmp0 (a, b) == 0;
+}
+
+void
+ephy_web_extension_manager_set_active (EphyWebExtensionManager *self,
+ EphyWebExtension *web_extension,
+ gboolean active)
+{
+ g_auto (GStrv) web_extensions_active = g_settings_get_strv (EPHY_SETTINGS_WEB, EPHY_PREFS_WEB_WEBEXTENSIONS_ACTIVE);
+ EphyShell *shell = ephy_shell_get_default ();
+ GList *windows = gtk_application_get_windows (GTK_APPLICATION (shell));
+ GList *list;
+ g_autoptr (GPtrArray) array = strv_to_ptr_array (web_extensions_active);
+ const char *name = ephy_web_extension_get_name (web_extension);
+ gboolean found;
+ guint idx;
+
+ /* Update settings */
+ found = g_ptr_array_find_with_equal_func (array, name, extension_equal, &idx);
+ if (active) {
+ if (!found)
+ g_ptr_array_add (array, (gpointer)name);
+ } else {
+ if (found)
+ g_ptr_array_remove_index (array, idx);
+ }
+
+ g_ptr_array_add (array, NULL);
+
+ g_settings_set_strv (EPHY_SETTINGS_WEB, EPHY_PREFS_WEB_WEBEXTENSIONS_ACTIVE, (const gchar * const *)array->pdata);
+
+ /* Update window web_extension state */
+ for (list = windows; list && list->data; list = list->next) {
+ EphyWindow *window = EPHY_WINDOW (list->data);
+
+ if (active)
+ ephy_web_extension_manager_add_web_extension_to_window (self, web_extension, window);
+ else
+ ephy_web_extension_manager_remove_web_extension_from_window (self, web_extension, window);
+ }
+
+ if (active) {
+ if (ephy_web_extension_has_background_web_view (web_extension))
+ run_background_script (self, web_extension);
+ }
+}
+
+GtkWidget *
+ephy_web_extension_manager_get_page_action (EphyWebExtensionManager *self,
+ EphyWebExtension *web_extension,
+ EphyWebView *web_view)
+{
+ GHashTable *table;
+ GtkWidget *ret = NULL;
+
+ table = g_hash_table_lookup (self->page_action_map, web_extension);
+ if (table)
+ ret = g_hash_table_lookup (table, web_view);
+
+ return ret;
+}
diff --git a/src/webextension/ephy-web-extension-manager.h b/src/webextension/ephy-web-extension-manager.h
new file mode 100644
index 000000000..963f3aaaf
--- /dev/null
+++ b/src/webextension/ephy-web-extension-manager.h
@@ -0,0 +1,72 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Copyright © 2019-2020 Jan-Michael Brummer <jan.brummer@tabos.org>
+ *
+ * This file is part of Epiphany.
+ *
+ * Epiphany is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Epiphany is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Epiphany. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+#pragma once
+
+G_BEGIN_DECLS
+
+#include <glib.h>
+
+#include "ephy-web-extension.h"
+
+#define EPHY_TYPE_WEB_EXTENSION_MANAGER (ephy_web_extension_manager_get_type ())
+
+G_DECLARE_FINAL_TYPE (EphyWebExtensionManager, ephy_web_extension_manager, EPHY, WEB_EXTENSION_MANAGER, GObject)
+
+EphyWebExtensionManager *ephy_web_extension_manager_new (void);
+
+GList *ephy_web_extension_manager_get_web_extensions (EphyWebExtensionManager *self);
+
+void ephy_web_extension_manager_install_actions (EphyWebExtensionManager *self,
+ EphyWindow *window);
+
+void ephy_web_extension_manager_install (EphyWebExtensionManager *self,
+ GFile *file);
+
+void ephy_web_extension_manager_uninstall (EphyWebExtensionManager *self,
+ EphyWebExtension *web_extension);
+
+void ephy_web_extension_manager_update_location_entry (EphyWebExtensionManager *self,
+ EphyWindow *window);
+
+void ephy_web_extension_manager_add_web_extension_to_window (EphyWebExtensionManager *self,
+ EphyWebExtension *web_extension,
+ EphyWindow *window);
+
+void ephy_web_extension_manager_remove_web_extension_from_window (EphyWebExtensionManager *self,
+ EphyWebExtension *web_extension,
+ EphyWindow *window);
+
+gboolean ephy_web_extension_manager_is_active (EphyWebExtensionManager *self,
+ EphyWebExtension *web_extension);
+
+void ephy_web_extension_manager_set_active (EphyWebExtensionManager *self,
+ EphyWebExtension *web_extension,
+ gboolean active);
+
+GtkWidget *ephy_web_extension_manager_get_page_action (EphyWebExtensionManager *self,
+ EphyWebExtension *web_extension,
+ EphyWebView *web_view);
+
+EphyWebView *ephy_web_extension_manager_get_background_web_view (EphyWebExtensionManager *self,
+ EphyWebExtension *web_extension);
+
+G_END_DECLS
diff --git a/src/webextension/ephy-web-extension.c b/src/webextension/ephy-web-extension.c
new file mode 100644
index 000000000..0a76f0550
--- /dev/null
+++ b/src/webextension/ephy-web-extension.c
@@ -0,0 +1,1203 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Copyright © 2019-2020 Jan-Michael Brummer <jan.brummer@tabos.org>
+ *
+ * This file is part of Epiphany.
+ *
+ * Epiphany is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Epiphany is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Epiphany. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * - Load a web_extension as described at https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/
+ * - Prepare the internal structure so that they can be easily applied to its destination (webview/browser) with the help of extension manager.
+ */
+
+#include "config.h"
+
+#include "ephy-embed-shell.h"
+#include "ephy-file-helpers.h"
+#include "ephy-shell.h"
+#include "ephy-string.h"
+#include "ephy-web-extension.h"
+#include "ephy-window.h"
+
+#include <archive.h>
+#include <archive_entry.h>
+#include <glib/gstdio.h>
+#include <json-glib/json-glib.h>
+
+typedef struct {
+ gint64 size;
+ char *file;
+ GdkPixbuf *pixbuf;
+} WebExtensionIcon;
+
+typedef struct {
+ GPtrArray *allow_list;
+ GPtrArray *block_list;
+ GPtrArray *js;
+
+ WebKitUserContentInjectedFrames injected_frames;
+ WebKitUserScriptInjectionTime injection_time;
+ GList *user_scripts;
+} WebExtensionContentScript;
+
+typedef struct {
+ GList *default_icons;
+ GtkWidget *widget;
+} WebExtensionPageAction;
+
+typedef struct {
+ char *title;
+ GList *default_icons;
+ char *popup;
+} WebExtensionBrowserAction;
+
+typedef struct {
+ GPtrArray *scripts;
+ char *page;
+} WebExtensionBackground;
+
+typedef struct {
+ char *page;
+} WebExtensionOptionsUI;
+
+typedef struct {
+ char *name;
+ GBytes *bytes;
+} WebExtensionResource;
+
+typedef struct {
+ char *code;
+ WebKitUserStyleSheet *style;
+} WebExtensionCustomCSS;
+
+struct _EphyWebExtension {
+ GObject parent_instance;
+
+ gboolean xpi;
+ char *base_location;
+ char *manifest;
+
+ char *description;
+ gint64 manifest_version;
+ char *guid;
+ char *author;
+ char *name;
+ char *version;
+ char *homepage_url;
+ GList *icons;
+ GList *content_scripts;
+ WebExtensionBackground *background;
+ GHashTable *page_action_map;
+ WebExtensionPageAction *page_action;
+ WebExtensionBrowserAction *browser_action;
+ WebExtensionOptionsUI *options_ui;
+ GList *resources;
+ GList *custom_css;
+ GPtrArray *permissions;
+ GCancellable *cancellable;
+};
+
+G_DEFINE_TYPE (EphyWebExtension, ephy_web_extension, G_TYPE_OBJECT)
+
+gboolean
+ephy_web_extension_has_resource (EphyWebExtension *self,
+ const char *name)
+{
+ for (GList *list = self->resources; list && list->data; list = list->next) {
+ WebExtensionResource *resource = list->data;
+
+ if (g_strcmp0 (resource->name, name) == 0)
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+gconstpointer
+ephy_web_extension_get_resource (EphyWebExtension *self,
+ const char *name,
+ gsize *length)
+{
+ if (length)
+ *length = 0;
+
+ for (GList *list = self->resources; list && list->data; list = list->next) {
+ WebExtensionResource *resource = list->data;
+
+ if (g_strcmp0 (resource->name, name) == 0)
+ return g_bytes_get_data (resource->bytes, length);
+ }
+
+ g_debug ("Could not find web_extension resource: %s\n", name);
+ return NULL;
+}
+
+char *
+ephy_web_extension_get_resource_as_string (EphyWebExtension *self,
+ const char *name)
+{
+ gsize len;
+ gconstpointer data = ephy_web_extension_get_resource (self, name, &len);
+ g_autofree char *out = NULL;
+
+ if (data && len) {
+ out = g_malloc0 (len + 1);
+ memcpy (out, data, len);
+ }
+
+ return g_steal_pointer (&out);
+}
+
+static WebExtensionIcon *
+web_extension_icon_new (EphyWebExtension *self,
+ const char *file,
+ gint64 size)
+{
+ WebExtensionIcon *icon = NULL;
+ g_autoptr (GInputStream) stream = NULL;
+ g_autoptr (GError) error = NULL;
+ g_autoptr (GdkPixbuf) pixbuf = NULL;
+ const unsigned char *data = NULL;
+ gsize length;
+
+ data = ephy_web_extension_get_resource (self, file, &length);
+ if (!data) {
+ if (!self->xpi) {
+ g_autofree char *path = NULL;
+ path = g_build_filename (self->base_location, file, NULL);
+ pixbuf = gdk_pixbuf_new_from_file (path, NULL);
+ }
+ } else {
+ stream = g_memory_input_stream_new_from_data (data, length, NULL);
+ pixbuf = gdk_pixbuf_new_from_stream (stream, NULL, &error);
+ }
+
+ if (!pixbuf) {
+ g_warning ("Could not read web_extension icon: %s", error ? error->message : "");
+ return NULL;
+ }
+
+ icon = g_malloc0 (sizeof (WebExtensionIcon));
+ icon->file = g_strdup (file);
+ icon->size = size;
+ icon->pixbuf = g_steal_pointer (&pixbuf);
+
+ return icon;
+}
+
+static void
+web_extension_icon_free (WebExtensionIcon *icon)
+{
+ g_clear_pointer (&icon->file, g_free);
+ g_clear_object (&icon->pixbuf);
+ g_free (icon);
+}
+
+static WebExtensionContentScript *
+web_extension_content_script_new (WebKitUserContentInjectedFrames injected_frames,
+ WebKitUserScriptInjectionTime injection_time)
+{
+ WebExtensionContentScript *content_script = g_malloc0 (sizeof (WebExtensionContentScript));
+
+ content_script->injected_frames = injected_frames;
+ content_script->injection_time = injection_time;
+ content_script->allow_list = g_ptr_array_new_full (1, g_free);
+ content_script->block_list = g_ptr_array_new_full (1, g_free);
+ content_script->js = g_ptr_array_new_full (1, g_free);
+
+ return content_script;
+}
+
+static void
+web_extension_content_script_free (WebExtensionContentScript *content_script)
+{
+ g_clear_pointer (&content_script->allow_list, g_ptr_array_unref);
+ g_clear_pointer (&content_script->block_list, g_ptr_array_unref);
+ g_clear_pointer (&content_script->js, g_ptr_array_unref);
+ g_clear_list (&content_script->user_scripts, (GDestroyNotify)webkit_user_script_unref);
+ g_free (content_script);
+}
+
+static WebExtensionOptionsUI *
+web_extension_options_ui_new (const char *page)
+{
+ WebExtensionOptionsUI *options_ui = g_malloc0 (sizeof (WebExtensionOptionsUI));
+
+ options_ui->page = g_strdup (page);
+
+ return options_ui;
+}
+
+static void
+web_extension_options_ui_free (WebExtensionOptionsUI *options_ui)
+{
+ g_clear_pointer (&options_ui->page, g_free);
+ g_free (options_ui);
+}
+
+static WebExtensionBackground *
+web_extension_background_new (void)
+{
+ WebExtensionBackground *background = g_malloc0 (sizeof (WebExtensionBackground));
+
+ background->scripts = g_ptr_array_new_full (1, g_free);
+
+ return background;
+}
+
+static void
+web_extension_background_free (WebExtensionBackground *background)
+{
+ g_clear_pointer (&background->scripts, g_ptr_array_unref);
+ g_clear_pointer (&background->page, g_free);
+ g_free (background);
+}
+
+static void
+web_extension_add_icon (JsonObject *object,
+ const char *member_name,
+ JsonNode *member_node,
+ gpointer user_data)
+{
+ EphyWebExtension *self = EPHY_WEB_EXTENSION (user_data);
+ WebExtensionIcon *icon;
+ const char *file = json_node_get_string (member_node);
+ gint64 size;
+
+ size = g_ascii_strtoll (member_name, NULL, 0);
+ if (size == 0) {
+ LOG ("Skipping %s as web extension icon as size is 0", file);
+ return;
+ }
+
+ icon = web_extension_icon_new (self, file, size);
+
+ if (icon)
+ self->icons = g_list_append (self->icons, icon);
+}
+
+static void
+web_extension_add_browser_icons (JsonObject *object,
+ const char *member_name,
+ JsonNode *member_node,
+ gpointer user_data)
+{
+ EphyWebExtension *self = EPHY_WEB_EXTENSION (user_data);
+ WebExtensionIcon *icon;
+ const char *file = json_node_get_string (member_node);
+ gint64 size;
+
+ size = g_ascii_strtoll (member_name, NULL, 0);
+ if (size == 0) {
+ LOG ("Skipping %s as web extension browser icon as size is 0", file);
+ return;
+ }
+ icon = web_extension_icon_new (self, file, size);
+
+ if (icon)
+ self->browser_action->default_icons = g_list_append (self->browser_action->default_icons, icon);
+}
+
+GdkPixbuf *
+ephy_web_extension_get_icon (EphyWebExtension *self,
+ gint64 size)
+{
+ WebExtensionIcon *icon_fallback = NULL;
+
+ for (GList *list = self->icons; list && list->data; list = list->next) {
+ WebExtensionIcon *icon = list->data;
+
+ if (icon->size == size)
+ return gdk_pixbuf_scale_simple (icon->pixbuf, size, size, GDK_INTERP_BILINEAR);
+
+ if (!icon_fallback || icon->size > icon_fallback->size)
+ icon_fallback = icon;
+ }
+
+ /* Fallback */
+ if (icon_fallback && icon_fallback->pixbuf)
+ return gdk_pixbuf_scale_simple (icon_fallback->pixbuf, size, size, GDK_INTERP_BILINEAR);
+
+ return NULL;
+}
+
+const char *
+ephy_web_extension_get_name (EphyWebExtension *self)
+{
+ return self->name;
+}
+
+const char *
+ephy_web_extension_get_version (EphyWebExtension *self)
+{
+ return self->version;
+}
+
+const char *
+ephy_web_extension_get_description (EphyWebExtension *self)
+{
+ return self->description;
+}
+
+const char *
+ephy_web_extension_get_homepage_url (EphyWebExtension *self)
+{
+ return self->homepage_url;
+}
+
+const char *
+ephy_web_extension_get_author (EphyWebExtension *self)
+{
+ return self->author;
+}
+
+const char *
+ephy_web_extension_get_manifest (EphyWebExtension *self)
+{
+ return self->manifest;
+}
+
+const char *
+ephy_web_extension_get_base_location (EphyWebExtension *self)
+{
+ return self->base_location;
+}
+
+static void
+web_extension_add_allow_list (JsonArray *array,
+ guint index,
+ JsonNode *element_node,
+ gpointer user_data)
+{
+ WebExtensionContentScript *content_script = user_data;
+
+ g_ptr_array_add (content_script->allow_list, g_strdup (json_node_get_string (element_node)));
+}
+
+static void
+web_extension_add_block_list (JsonArray *array,
+ guint index,
+ JsonNode *element_node,
+ gpointer user_data)
+{
+ WebExtensionContentScript *content_script = user_data;
+
+ g_ptr_array_add (content_script->block_list, g_strdup (json_node_get_string (element_node)));
+}
+
+static void
+web_extension_add_js (JsonArray *array,
+ guint index_,
+ JsonNode *element_node,
+ gpointer user_data)
+{
+ WebExtensionContentScript *content_script = user_data;
+
+ g_ptr_array_add (content_script->js, g_strdup (json_node_get_string (element_node)));
+}
+
+static void
+web_extension_content_script_build (EphyWebExtension *self,
+ WebExtensionContentScript *content_script)
+{
+ if (!content_script->js)
+ return;
+
+ for (guint i = 0; i < content_script->js->len; i++) {
+ WebKitUserScript *user_script;
+ char *js_data;
+
+ js_data = ephy_web_extension_get_resource_as_string (self, g_ptr_array_index (content_script->js, i));
+ if (!js_data)
+ continue;
+
+ user_script = webkit_user_script_new_for_world (js_data,
+ content_script->injected_frames,
+ content_script->injection_time,
+ ephy_embed_shell_get_guid (ephy_embed_shell_get_default ()),
+ (const char * const *)content_script->allow_list->pdata,
+ (const char * const *)content_script->block_list->pdata);
+
+ content_script->user_scripts = g_list_append (content_script->user_scripts, user_script);
+ g_free (js_data);
+ }
+}
+
+static void
+web_extension_add_content_script (JsonArray *array,
+ guint index_,
+ JsonNode *element_node,
+ gpointer user_data)
+{
+ EphyWebExtension *self = EPHY_WEB_EXTENSION (user_data);
+ WebKitUserContentInjectedFrames injected_frames = WEBKIT_USER_CONTENT_INJECT_TOP_FRAME;
+ WebKitUserScriptInjectionTime injection_time = WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_END;
+ WebExtensionContentScript *content_script;
+ JsonObject *object = json_node_get_object (element_node);
+ JsonArray *child_array;
+ const char *run_at;
+ gboolean all_frames;
+
+ /* TODO: The default value is "document_idle", which in WebKit term is document_end */
+ run_at = json_object_get_string_member_with_default (object, "run_at", "document_idle");
+ if (strcmp (run_at, "document_start") == 0) {
+ injection_time = WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START;
+ } else if (strcmp (run_at, "document_end") == 0) {
+ injection_time = WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_END;
+ } else if (strcmp (run_at, "document_idle") == 0) {
+ g_warning ("run_at: document_idle not supported by WebKit, falling back to document_end");
+ injection_time = WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_END;
+ } else {
+ g_warning ("Unhandled run_at '%s' in web_extension, ignoring.", run_at);
+ return;
+ }
+
+ /* all_frames */
+ all_frames = json_object_get_boolean_member_with_default (object, "all_frames", FALSE);
+ injected_frames = all_frames ? WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES : WEBKIT_USER_CONTENT_INJECT_TOP_FRAME;
+
+ content_script = web_extension_content_script_new (injected_frames, injection_time);
+ if (json_object_has_member (object, "matches")) {
+ child_array = json_object_get_array_member (object, "matches");
+ json_array_foreach_element (child_array, web_extension_add_allow_list, content_script);
+ }
+ g_ptr_array_add (content_script->allow_list, NULL);
+
+ if (json_object_has_member (object, "exclude_matches")) {
+ child_array = json_object_get_array_member (object, "exclude_matches");
+ json_array_foreach_element (child_array, web_extension_add_block_list, content_script);
+ }
+ g_ptr_array_add (content_script->block_list, NULL);
+
+ if (json_object_has_member (object, "js")) {
+ child_array = json_object_get_array_member (object, "js");
+ if (child_array)
+ json_array_foreach_element (child_array, web_extension_add_js, content_script);
+ }
+ g_ptr_array_add (content_script->js, NULL);
+
+ /* Create user scripts so that we can unload them if necessary */
+ web_extension_content_script_build (self, content_script);
+
+ self->content_scripts = g_list_append (self->content_scripts, content_script);
+}
+
+static void
+web_extension_add_scripts (JsonArray *array,
+ guint index_,
+ JsonNode *element_node,
+ gpointer user_data)
+{
+ EphyWebExtension *self = EPHY_WEB_EXTENSION (user_data);
+
+ g_ptr_array_add (self->background->scripts, g_strdup (json_node_get_string (element_node)));
+}
+
+static void
+web_extension_add_background (JsonObject *object,
+ const char *member_name,
+ JsonNode *member_node,
+ gpointer user_data)
+{
+ /* https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/background
+ * Limitations:
+ * - persistent with false is not supported yet.
+ */
+ EphyWebExtension *self = EPHY_WEB_EXTENSION (user_data);
+ JsonArray *child_array;
+
+ if (!json_object_has_member (object, "scripts") && !json_object_has_member (object, "page") && !json_object_has_member (object, "persistent")) {
+ g_warning ("Invalid background section, it must be either scripts, page or persistent entry.");
+ return;
+ }
+
+ if (!self->background)
+ self->background = web_extension_background_new ();
+
+ if (json_object_has_member (object, "scripts")) {
+ child_array = json_object_get_array_member (object, "scripts");
+ json_array_foreach_element (child_array, web_extension_add_scripts, self);
+ } else if (!self->background->page && json_object_has_member (object, "page")) {
+ self->background->page = g_strdup (json_object_get_string_member (object, "page"));
+ } else if (json_object_has_member (object, "persistent")) {
+ LOG ("persistent background setting is not handled in Epiphany");
+ }
+}
+
+static void
+web_extension_add_page_action (JsonObject *object,
+ gpointer user_data)
+{
+ EphyWebExtension *self = EPHY_WEB_EXTENSION (user_data);
+ WebExtensionPageAction *page_action = g_malloc0 (sizeof (WebExtensionPageAction));
+
+ self->page_action = page_action;
+
+ if (json_object_has_member (object, "default_icon")) {
+ WebExtensionIcon *icon = g_malloc (sizeof (WebExtensionIcon));
+ const char *default_icon = json_object_get_string_member (object, "default_icon");
+ g_autofree char *path = NULL;
+
+ icon->size = -1;
+ icon->file = g_strdup (default_icon);
+
+ path = g_build_filename (self->base_location, icon->file, NULL);
+ icon->pixbuf = gdk_pixbuf_new_from_file (path, NULL);
+
+ self->page_action->default_icons = g_list_append (self->page_action->default_icons, icon);
+ }
+}
+
+static void
+web_extension_page_action_free (WebExtensionPageAction *page_action)
+{
+ g_clear_list (&page_action->default_icons, (GDestroyNotify)web_extension_icon_free);
+ g_free (page_action);
+}
+
+/* TODO: Load translation for current locale during init */
+static char *
+web_extension_get_translation (EphyWebExtension *self,
+ const char *locale,
+ const char *key)
+{
+ g_autoptr (JsonParser) parser = NULL;
+ g_autoptr (GError) error = NULL;
+ g_autofree char *path = g_strdup_printf ("_locales/%s/messages.json", locale);
+ JsonNode *root = NULL;
+ JsonObject *root_object = NULL;
+ JsonObject *name = NULL;
+ const unsigned char *data = NULL;
+ gsize length;
+
+ if (!ephy_web_extension_has_resource (self, path))
+ return NULL;
+
+ data = ephy_web_extension_get_resource (self, path, &length);
+
+ parser = json_parser_new ();
+ if (!json_parser_load_from_data (parser, (char *)data, length, &error)) {
+ g_warning ("Could not load WebExtension translation: %s", error->message);
+ return NULL;
+ }
+
+ root = json_parser_get_root (parser);
+ if (!root) {
+ g_warning ("WebExtension translation root is NULL, return NULL.");
+ return NULL;
+ }
+
+ root_object = json_node_get_object (root);
+ if (!root_object) {
+ g_warning ("WebExtension translation root object is NULL, return NULL.");
+ return NULL;
+ }
+
+ name = json_object_get_object_member (root_object, key);
+ if (name)
+ return g_strdup (json_object_get_string_member (name, "message"));
+
+ return NULL;
+}
+
+char *
+ephy_web_extension_manifest_get_key (EphyWebExtension *self,
+ JsonObject *object,
+ char *key)
+{
+ char *value = NULL;
+
+ if (json_object_has_member (object, key)) {
+ g_autofree char *ret = g_strdup (json_object_get_string_member (object, key));
+
+ /* Translation are requested with a unique string, e.g.:
+ * __MSG_unique_name__ but stored as unique_name in messages.json.
+ * Let's check for this prefix and suffix and extract the unique name
+ */
+ if (g_str_has_prefix (ret, "__MSG_") && g_str_has_suffix (ret, "__")) {
+ /* FIXME: Set current locale */
+ g_autofree char *locale = g_strdup ("en");
+
+ /* Remove trailing __ */
+ ret[strlen (ret) - 2] = '\0';
+ value = web_extension_get_translation (self, locale, ret + strlen ("__MSG_"));
+ } else {
+ value = g_strdup (ret);
+ }
+ }
+
+ return value;
+}
+
+static void
+web_extension_add_browser_action (JsonObject *object,
+ gpointer user_data)
+{
+ EphyWebExtension *self = EPHY_WEB_EXTENSION (user_data);
+ WebExtensionBrowserAction *browser_action = g_malloc0 (sizeof (WebExtensionBrowserAction));
+
+ g_clear_object (&self->browser_action);
+ self->browser_action = browser_action;
+
+ if (json_object_has_member (object, "default_title")) {
+ self->browser_action->title = ephy_web_extension_manifest_get_key (self, object, "default_title");
+ }
+
+ if (json_object_has_member (object, "default_icon")) {
+ /* defaullt_icon can be Object or String */
+ JsonNode *icon_node = json_object_get_member (object, "default_icon");
+
+ if (json_node_get_node_type (icon_node) == JSON_NODE_OBJECT) {
+ JsonObject *icon_object = json_object_get_object_member (object, "default_icon");
+ json_object_foreach_member (icon_object, web_extension_add_browser_icons, self);
+ } else {
+ const char *default_icon = json_object_get_string_member (object, "default_icon");
+ WebExtensionIcon *icon = web_extension_icon_new (self, default_icon, -1);
+
+ self->browser_action->default_icons = g_list_append (self->browser_action->default_icons, icon);
+ }
+ }
+
+ if (json_object_has_member (object, "default_popup"))
+ self->browser_action->popup = g_strdup (json_object_get_string_member (object, "default_popup"));
+}
+
+static void
+web_extension_browser_action_free (WebExtensionBrowserAction *browser_action)
+{
+ g_clear_pointer (&browser_action->title, g_free);
+ g_clear_pointer (&browser_action->popup, g_free);
+ g_clear_list (&browser_action->default_icons, (GDestroyNotify)web_extension_icon_free);
+ g_free (browser_action);
+}
+
+static void
+web_extension_add_options_ui (JsonObject *object,
+ gpointer user_data)
+{
+ EphyWebExtension *self = EPHY_WEB_EXTENSION (user_data);
+ const char *page = json_object_get_string_member (object, "page");
+ WebExtensionOptionsUI *options_ui = web_extension_options_ui_new (page);
+
+ g_clear_pointer (&self->options_ui, web_extension_options_ui_free);
+ self->options_ui = options_ui;
+}
+
+static void
+web_extension_add_permission (JsonArray *array,
+ guint index_,
+ JsonNode *element_node,
+ gpointer user_data)
+{
+ EphyWebExtension *self = EPHY_WEB_EXTENSION (user_data);
+
+ g_ptr_array_add (self->permissions, g_strdup (json_node_get_string (element_node)));
+}
+
+static void
+web_extension_resource_free (WebExtensionResource *resource)
+{
+ g_clear_pointer (&resource->bytes, g_bytes_unref);
+ g_clear_pointer (&resource->name, g_free);
+ g_free (resource);
+}
+
+static void
+ephy_web_extension_dispose (GObject *object)
+{
+ EphyWebExtension *self = EPHY_WEB_EXTENSION (object);
+
+ g_clear_pointer (&self->base_location, g_free);
+ g_clear_pointer (&self->manifest, g_free);
+ g_clear_pointer (&self->guid, g_free);
+ g_clear_pointer (&self->description, g_free);
+ g_clear_pointer (&self->author, g_free);
+ g_clear_pointer (&self->name, g_free);
+ g_clear_pointer (&self->version, g_free);
+ g_clear_pointer (&self->homepage_url, g_free);
+
+ g_clear_list (&self->icons, (GDestroyNotify)web_extension_icon_free);
+ g_clear_list (&self->content_scripts, (GDestroyNotify)web_extension_content_script_free);
+ g_clear_list (&self->resources, (GDestroyNotify)web_extension_resource_free);
+ g_clear_pointer (&self->background, web_extension_background_free);
+ g_clear_pointer (&self->options_ui, web_extension_options_ui_free);
+ g_clear_pointer (&self->permissions, g_ptr_array_unref);
+
+ g_clear_pointer (&self->page_action, web_extension_page_action_free);
+ g_clear_pointer (&self->browser_action, web_extension_browser_action_free);
+ g_clear_list (&self->custom_css, (GDestroyNotify)webkit_user_style_sheet_unref);
+
+ g_hash_table_destroy (self->page_action_map);
+
+ G_OBJECT_CLASS (ephy_web_extension_parent_class)->dispose (object);
+}
+
+static void
+ephy_web_extension_class_init (EphyWebExtensionClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->dispose = ephy_web_extension_dispose;
+}
+
+static void
+ephy_web_extension_init (EphyWebExtension *self)
+{
+ self->page_action_map = g_hash_table_new (NULL, NULL);
+ self->permissions = g_ptr_array_new_full (1, g_free);
+
+ self->guid = g_uuid_string_random ();
+}
+
+static EphyWebExtension *
+ephy_web_extension_new (void)
+{
+ return g_object_new (EPHY_TYPE_WEB_EXTENSION, NULL);
+}
+
+static void
+web_extension_add_resource (EphyWebExtension *self,
+ const char *name,
+ gpointer data,
+ guint len)
+{
+ WebExtensionResource *resource = g_malloc0 (sizeof (WebExtensionResource));
+
+ resource->name = g_strdup (name);
+ resource->bytes = g_bytes_new (data, len);
+
+ self->resources = g_list_append (self->resources, resource);
+}
+
+static gboolean
+web_extension_read_directory (EphyWebExtension *self,
+ char *base,
+ char *path)
+{
+ g_autoptr (GError) error = NULL;
+ g_autoptr (GDir) dir = NULL;
+ const char *dirent;
+ gboolean ret = TRUE;
+
+ dir = g_dir_open (path, 0, &error);
+ if (!dir) {
+ g_warning ("Could not open web_extension directory: %s", error->message);
+ return FALSE;
+ }
+
+ while ((dirent = g_dir_read_name (dir))) {
+ GFileType type;
+ g_autofree gchar *filename = g_build_filename (path, dirent, NULL);
+ g_autoptr (GFile) file = g_file_new_for_path (filename);
+
+ type = g_file_query_file_type (file, G_FILE_QUERY_INFO_NONE, NULL);
+ if (type == G_FILE_TYPE_DIRECTORY) {
+ web_extension_read_directory (self, base, filename);
+ } else {
+ g_autofree char *data = NULL;
+ gsize len;
+
+ if (g_file_get_contents (filename, &data, &len, NULL))
+ web_extension_add_resource (self, filename + strlen (base) + 1, data, len);
+ }
+ }
+
+ return ret;
+}
+
+static EphyWebExtension *
+ephy_web_extension_load_directory (char *filename)
+{
+ EphyWebExtension *self = ephy_web_extension_new ();
+
+ web_extension_read_directory (self, filename, filename);
+
+ return self;
+}
+
+static EphyWebExtension *
+ephy_web_extension_load_xpi (GFile *target)
+{
+ EphyWebExtension *self = NULL;
+ struct archive *pkg;
+ struct archive_entry *entry;
+ int res;
+
+ pkg = archive_read_new ();
+ archive_read_support_format_zip (pkg);
+
+ res = archive_read_open_filename (pkg, g_file_get_path (target), 10240);
+ if (res == ARCHIVE_OK) {
+ self = ephy_web_extension_new ();
+ self->xpi = TRUE;
+
+ while (archive_read_next_header (pkg, &entry) == ARCHIVE_OK) {
+ int64_t size = archive_entry_size (entry);
+ gsize total_len = 0;
+ g_autofree char *data = NULL;
+
+ data = g_malloc0 (size);
+ total_len = archive_read_data (pkg, data, size);
+
+ if (total_len > 0)
+ web_extension_add_resource (self, archive_entry_pathname (entry), data, total_len);
+ }
+
+ res = archive_read_free (pkg);
+ if (res != ARCHIVE_OK)
+ g_warning ("Error freeing archive: %s", archive_error_string (pkg));
+ } else {
+ g_warning ("Could not open archive %s", archive_error_string (pkg));
+ }
+
+ return self;
+}
+
+EphyWebExtension *
+ephy_web_extension_load (GFile *target)
+{
+ g_autoptr (GError) error = NULL;
+ g_autoptr (GFile) source = g_file_dup (target);
+ g_autoptr (GFile) parent = NULL;
+ g_autoptr (JsonObject) icons_object = NULL;
+ g_autoptr (JsonArray) content_scripts_array = NULL;
+ g_autoptr (JsonObject) background_object = NULL;
+ JsonParser *parser = NULL;
+ JsonNode *root = NULL;
+ JsonObject *root_object = NULL;
+ EphyWebExtension *self = NULL;
+ GFileType type;
+ gsize length = 0;
+ const unsigned char *manifest;
+
+ type = g_file_query_file_type (source, G_FILE_QUERY_INFO_NONE, NULL);
+ if (type == G_FILE_TYPE_DIRECTORY) {
+ g_autofree char *path = g_file_get_path (source);
+ self = ephy_web_extension_load_directory (path);
+ } else
+ self = ephy_web_extension_load_xpi (source);
+
+ if (!self)
+ return NULL;
+
+ manifest = ephy_web_extension_get_resource (self, "manifest.json", &length);
+ if (!manifest)
+ return NULL;
+
+ parser = json_parser_new ();
+ if (!json_parser_load_from_data (parser, (char *)manifest, length, &error)) {
+ g_warning ("Could not load web extension manifest: %s", error->message);
+ return NULL;
+ }
+
+ root = json_parser_get_root (parser);
+ if (!root) {
+ g_warning ("WebExtension manifest json root is NULL, return NULL.");
+ return NULL;
+ }
+
+ root_object = json_node_get_object (root);
+ if (!root_object) {
+ g_warning ("WebExtension manifest json root is NULL, return NULL.");
+ return NULL;
+ }
+
+ self->manifest = g_strndup ((char *)manifest, length);
+ self->base_location = parent ? g_file_get_path (parent) : g_file_get_path (target);
+ self->description = ephy_web_extension_manifest_get_key (self, root_object, "description");
+ self->manifest_version = json_object_get_int_member (root_object, "manifest_version");
+ self->name = ephy_web_extension_manifest_get_key (self, root_object, "name");
+ self->version = ephy_web_extension_manifest_get_key (self, root_object, "version");
+ self->homepage_url = ephy_web_extension_manifest_get_key (self, root_object, "homepage_url");
+ self->author = ephy_web_extension_manifest_get_key (self, root_object, "author");
+
+ if (json_object_has_member (root_object, "icons")) {
+ icons_object = json_object_get_object_member (root_object, "icons");
+
+ json_object_foreach_member (icons_object, web_extension_add_icon, self);
+ }
+
+ if (json_object_has_member (root_object, "content_scripts")) {
+ content_scripts_array = json_object_get_array_member (root_object, "content_scripts");
+
+ json_array_foreach_element (content_scripts_array, web_extension_add_content_script, self);
+ }
+
+ if (json_object_has_member (root_object, "background")) {
+ background_object = json_object_get_object_member (root_object, "background");
+
+ json_object_foreach_member (background_object, web_extension_add_background, self);
+ }
+ if (self->background)
+ g_ptr_array_add (self->background->scripts, NULL);
+
+ if (json_object_has_member (root_object, "page_action")) {
+ g_autoptr (JsonObject) page_action_object = json_object_get_object_member (root_object, "page_action");
+
+ web_extension_add_page_action (page_action_object, self);
+ }
+
+ if (json_object_has_member (root_object, "browser_action")) {
+ g_autoptr (JsonObject) browser_action_object = json_object_get_object_member (root_object, "browser_action");
+
+ web_extension_add_browser_action (browser_action_object, self);
+ }
+
+ if (json_object_has_member (root_object, "options_ui")) {
+ g_autoptr (JsonObject) browser_action_object = json_object_get_object_member (root_object, "options_ui");
+
+ web_extension_add_options_ui (browser_action_object, self);
+ }
+
+ if (json_object_has_member (root_object, "permissions")) {
+ g_autoptr (JsonArray) array = json_object_get_array_member (root_object, "permissions");
+
+ json_array_foreach_element (array, web_extension_add_permission, self);
+ }
+ if (self->permissions)
+ g_ptr_array_add (self->permissions, NULL);
+
+ return self;
+}
+
+EphyWebExtension *
+ephy_web_extension_load_finished (GObject *unused,
+ GAsyncResult *result,
+ GError **error)
+{
+ g_assert (g_task_is_valid (result, unused));
+
+ return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+static void
+load_web_extension_thread (GTask *task,
+ gpointer *unused,
+ GFile *target,
+ GCancellable *cancellable)
+{
+ EphyWebExtension *self = ephy_web_extension_load (target);
+
+ g_task_return_pointer (task, self, NULL);
+}
+
+void
+ephy_web_extension_load_async (GFile *target,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GTask *task;
+
+ g_assert (target);
+
+ task = g_task_new (NULL, cancellable, callback, user_data);
+ g_task_set_priority (task, G_PRIORITY_DEFAULT);
+ g_task_set_task_data (task,
+ g_file_dup (target),
+ (GDestroyNotify)g_object_unref);
+ g_task_run_in_thread (task, (GTaskThreadFunc)load_web_extension_thread);
+ g_object_unref (task);
+}
+
+
+GdkPixbuf *
+ephy_web_extension_load_pixbuf (EphyWebExtension *self,
+ char *file)
+{
+ g_autofree gchar *path = NULL;
+
+ path = g_build_filename (self->base_location, file, NULL);
+
+ return gdk_pixbuf_new_from_file (path, NULL);
+}
+
+void
+ephy_web_extension_remove (EphyWebExtension *self)
+{
+ g_autoptr (GError) error = NULL;
+
+ if (!self->xpi) {
+ if (!ephy_file_delete_dir_recursively (self->base_location, &error))
+ g_warning ("Could not delete web_extension from %s: %s", self->base_location, error->message);
+ } else {
+ g_unlink (self->base_location);
+ }
+}
+
+gboolean
+ephy_web_extension_has_page_action (EphyWebExtension *self)
+{
+ return !!self->page_action;
+}
+
+gboolean
+ephy_web_extension_has_browser_action (EphyWebExtension *self)
+{
+ return !!self->browser_action;
+}
+
+gboolean
+ephy_web_extension_has_background_web_view (EphyWebExtension *self)
+{
+ return !!self->background;
+}
+
+const char *
+ephy_web_extension_background_web_view_get_page (EphyWebExtension *self)
+{
+ return self->background->page;
+}
+
+GPtrArray *
+ephy_web_extension_background_web_view_get_scripts (EphyWebExtension *self)
+{
+ return self->background->scripts;
+}
+
+GList *
+ephy_web_extension_get_content_scripts (EphyWebExtension *self)
+{
+ return self->content_scripts;
+}
+
+GList *
+ephy_web_extension_get_content_script_js (EphyWebExtension *self,
+ gpointer content_script)
+{
+ WebExtensionContentScript *script = content_script;
+ return script->user_scripts;
+}
+
+GdkPixbuf *
+ephy_web_extension_browser_action_get_icon (EphyWebExtension *self,
+ int size)
+{
+ WebExtensionIcon *icon_fallback = NULL;
+
+ if (!self->browser_action || !self->browser_action->default_icons)
+ return NULL;
+
+ for (GList *list = self->browser_action->default_icons; list && list->data; list = list->next) {
+ WebExtensionIcon *icon = list->data;
+
+ if (icon->size == size)
+ return gdk_pixbuf_copy (icon->pixbuf);
+
+ if (!icon_fallback || icon->size > icon_fallback->size)
+ icon_fallback = icon;
+ }
+
+ /* Fallback */
+ if (icon_fallback)
+ return gdk_pixbuf_scale_simple (icon_fallback->pixbuf, size, size, GDK_INTERP_BILINEAR);
+
+ return NULL;
+}
+
+const char *
+ephy_web_extension_get_browser_popup (EphyWebExtension *self)
+{
+ return self->browser_action->popup;
+}
+
+const char *
+ephy_web_extension_browser_action_get_tooltip (EphyWebExtension *self)
+{
+ return self->browser_action->title;
+}
+
+WebExtensionCustomCSS *web_extension_custom_css_new (EphyWebExtension *self,
+ const char *code)
+
+{
+ WebExtensionCustomCSS *css = g_malloc0 (sizeof (WebExtensionCustomCSS));
+
+ css->code = g_strdup (code);
+ css->style = webkit_user_style_sheet_new (css->code, WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, WEBKIT_USER_STYLE_LEVEL_USER, NULL, NULL);
+
+ self->custom_css = g_list_append (self->custom_css, css);
+
+ return css;
+}
+
+WebKitUserStyleSheet *
+ephy_web_extension_get_custom_css (EphyWebExtension *self,
+ const char *code)
+{
+ WebExtensionCustomCSS *css = NULL;
+
+ for (GList *list = self->custom_css; list && list->data; list = list->data) {
+ css = list->data;
+
+ if (strcmp (css->code, code) == 0)
+ return css->style;
+ }
+
+ return NULL;
+}
+
+WebKitUserStyleSheet *
+ephy_web_extension_add_custom_css (EphyWebExtension *self,
+ const char *code)
+{
+ WebKitUserStyleSheet *style;
+ WebExtensionCustomCSS *css = NULL;
+
+ style = ephy_web_extension_get_custom_css (self, code);
+ if (style)
+ return style;
+
+ css = web_extension_custom_css_new (self, code);
+
+ return css->style;
+}
+
+GList *
+ephy_web_extension_get_custom_css_list (EphyWebExtension *self)
+{
+ return self->custom_css;
+}
+
+WebKitUserStyleSheet *
+ephy_web_extension_custom_css_style (EphyWebExtension *self,
+ gpointer custom_css)
+{
+ WebExtensionCustomCSS *css = custom_css;
+
+ return css->style;
+}
+
+char *
+ephy_web_extension_get_option_ui_page (EphyWebExtension *self)
+{
+ if (!self->options_ui)
+ return NULL;
+
+ return ephy_web_extension_get_resource_as_string (self, self->options_ui->page);
+}
+
+const char *
+ephy_web_extension_get_guid (EphyWebExtension *self)
+{
+ return self->guid;
+}
+
+GPtrArray *
+ephy_web_extension_get_permissions (EphyWebExtension *self)
+{
+ return self->permissions;
+}
diff --git a/src/webextension/ephy-web-extension.h b/src/webextension/ephy-web-extension.h
new file mode 100644
index 000000000..57d78a331
--- /dev/null
+++ b/src/webextension/ephy-web-extension.h
@@ -0,0 +1,129 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Copyright © 2019-2020 Jan-Michael Brummer <jan.brummer@tabos.org>
+ *
+ * This file is part of Epiphany.
+ *
+ * Epiphany is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Epiphany is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Epiphany. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+#pragma once
+
+#include "ephy-debug.h"
+#include "ephy-window.h"
+
+#include <gdk-pixbuf/gdk-pixbuf.h>
+#include <gio/gio.h>
+#include <string.h>
+#include <webkit2/webkit2.h>
+
+G_BEGIN_DECLS
+
+#define EPHY_TYPE_WEB_EXTENSION (ephy_web_extension_get_type ())
+
+G_DECLARE_FINAL_TYPE (EphyWebExtension, ephy_web_extension, EPHY, WEB_EXTENSION, GObject)
+
+typedef char *(*executeHandler)(EphyWebExtension *web_extension,
+ char *name,
+ JSCValue *args);
+
+typedef struct {
+ char *name;
+ executeHandler execute;
+} EphyWebExtensionApiHandler;
+
+GdkPixbuf *ephy_web_extension_get_icon (EphyWebExtension *self,
+ gint64 size);
+
+const char *ephy_web_extension_get_name (EphyWebExtension *self);
+
+const char *ephy_web_extension_get_version (EphyWebExtension *self);
+
+const char *ephy_web_extension_get_description (EphyWebExtension *self);
+
+const char *ephy_web_extension_get_homepage_url (EphyWebExtension *self);
+
+const char *ephy_web_extension_get_author (EphyWebExtension *self);
+
+GList *ephy_web_extensions_get (void);
+
+EphyWebExtension *ephy_web_extension_load (GFile *file);
+
+void ephy_web_extension_load_async (GFile *target,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+
+EphyWebExtension *ephy_web_extension_load_finished (GObject *unused,
+ GAsyncResult *result,
+ GError **error);
+
+GdkPixbuf *ephy_web_extension_load_pixbuf (EphyWebExtension *self,
+ char *file);
+
+gboolean ephy_web_extension_has_page_action (EphyWebExtension *self);
+
+gboolean ephy_web_extension_has_browser_action (EphyWebExtension *self);
+
+gboolean ephy_web_extension_has_background_web_view (EphyWebExtension *self);
+
+void ephy_web_extension_remove (EphyWebExtension *self);
+
+const char *ephy_web_extension_get_manifest (EphyWebExtension *self);
+
+const char *ephy_web_extension_background_web_view_get_page (EphyWebExtension *self);
+
+GdkPixbuf *ephy_web_extension_browser_action_get_icon (EphyWebExtension *self,
+ int size);
+
+const char *ephy_web_extension_browser_action_get_tooltip (EphyWebExtension *self);
+
+const char *ephy_web_extension_get_browser_popup (EphyWebExtension *self);
+
+GPtrArray *ephy_web_extension_background_web_view_get_scripts (EphyWebExtension *self);
+
+GList *ephy_web_extension_get_content_scripts (EphyWebExtension *self);
+
+GList *ephy_web_extension_get_content_script_js (EphyWebExtension *self,
+ gpointer content_script);
+
+const char *ephy_web_extension_get_base_location (EphyWebExtension *self);
+
+gconstpointer ephy_web_extension_get_resource (EphyWebExtension *self,
+ const char *name,
+ gsize *length);
+
+char *ephy_web_extension_get_resource_as_string (EphyWebExtension *self,
+ const char *name);
+
+WebKitUserStyleSheet *ephy_web_extension_add_custom_css (EphyWebExtension *self,
+ const char *code);
+
+WebKitUserStyleSheet *ephy_web_extension_get_custom_css (EphyWebExtension *self,
+ const char *code);
+
+GList *ephy_web_extension_get_custom_css_list (EphyWebExtension *self);
+
+WebKitUserStyleSheet *ephy_web_extension_custom_css_style (EphyWebExtension *self,
+ gpointer custom_css);
+
+char *ephy_web_extension_get_option_ui_page (EphyWebExtension *self);
+
+const char *ephy_web_extension_get_guid (EphyWebExtension *self);
+
+GPtrArray *ephy_web_extension_get_permissions (EphyWebExtension *self);
+
+G_END_DECLS
+
diff --git a/src/webextension/meson.build b/src/webextension/meson.build
new file mode 100644
index 000000000..921cc68bc
--- /dev/null
+++ b/src/webextension/meson.build
@@ -0,0 +1,8 @@
+ephywebextension_src = [
+ 'webextension/api/notifications.c',
+ 'webextension/api/pageaction.c',
+ 'webextension/api/runtime.c',
+ 'webextension/api/tabs.c',
+ 'webextension/ephy-web-extension-manager.c',
+ 'webextension/ephy-web-extension.c',
+]
diff --git a/src/window-commands.c b/src/window-commands.c
index 7ea0aba02..daa9135dd 100644
--- a/src/window-commands.c
+++ b/src/window-commands.c
@@ -55,6 +55,7 @@
#include "ephy-string.h"
#include "ephy-view-source-handler.h"
#include "ephy-web-app-utils.h"
+#include "ephy-web-extension-dialog.h"
#include "ephy-zoom.h"
#include <gio/gio.h>
@@ -3077,3 +3078,16 @@ window_cmd_change_tabs_mute_state (GSimpleAction *action,
g_simple_action_set_state (action, g_variant_new_boolean (mute));
}
+
+void
+window_cmd_extensions (GSimpleAction *action,
+ GVariant *parameter,
+ gpointer user_data)
+{
+ EphyWindow *window = EPHY_WINDOW (user_data);
+ GtkWidget *dialog;
+
+ dialog = ephy_web_extension_dialog_new ();
+ gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (window));
+ gtk_widget_show_all (dialog);
+}
diff --git a/src/window-commands.h b/src/window-commands.h
index 353251e7f..25a3b1da3 100644
--- a/src/window-commands.h
+++ b/src/window-commands.h
@@ -245,5 +245,8 @@ void window_cmd_change_tabs_mute_state (GSimpleAction *action,
void window_cmd_import_passwords (GSimpleAction *action,
GVariant *parameter,
gpointer user_data);
+void window_cmd_extensions (GSimpleAction *action,
+ GVariant *parameter,
+ gpointer user_data);
G_END_DECLS