diff options
author | Jan-Michael Brummer <jan.brummer@tabos.org> | 2019-05-02 09:53:25 +0200 |
---|---|---|
committer | Jan-Michael Brummer <jan.brummer@tabos.org> | 2021-01-17 13:55:36 +0100 |
commit | 5410fec78bc699b593d327e6a98b105f6cef7cd5 (patch) | |
tree | 7e7f379bc6e9677d0ea8015c5c97996d9d49f920 | |
parent | da8757af88925c803be2201784a6821410150608 (diff) | |
download | epiphany-5410fec78bc699b593d327e6a98b105f6cef7cd5.tar.gz |
Add initial WebExtension support
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 |