[epiphany] Add initial WebExtension support



commit 5410fec78bc699b593d327e6a98b105f6cef7cd5
Author: Jan-Michael Brummer <jan brummer tabos org>
Date:   Thu May 2 09:53:25 2019 +0200

    Add initial WebExtension support

 data/org.gnome.Epiphany.desktop.in.in              |    2 +-
 data/org.gnome.epiphany.gschema.xml                |   10 +
 embed/ephy-web-view.c                              |   30 +
 embed/ephy-web-view.h                              |    4 +
 .../ephy-web-process-extension-main.c              |    4 +-
 .../ephy-web-process-extension.c                   |   37 +-
 .../ephy-web-process-extension.h                   |    4 +
 .../web-process-extension/ephy-webextension-api.c  |  170 +++
 .../web-process-extension/ephy-webextension-api.h  |   37 +
 embed/web-process-extension/meson.build            |    1 +
 .../epiphany-web-process-extension.gresource.xml   |    1 +
 .../resources/js/webextensions.js                  |  115 ++
 lib/ephy-file-helpers.c                            |   50 +
 lib/ephy-file-helpers.h                            |    3 +
 lib/ephy-prefs.h                                   |    3 +
 lib/ephy-string.c                                  |   29 -
 lib/ephy-string.h                                  |    2 -
 lib/widgets/ephy-location-entry.c                  |   67 +-
 lib/widgets/ephy-location-entry.h                  |    4 +
 meson.build                                        |    5 +-
 src/ephy-action-bar-end.c                          |   11 +
 src/ephy-action-bar-end.h                          |    3 +
 src/ephy-header-bar.c                              |    8 +
 src/ephy-header-bar.h                              |    3 +
 src/ephy-shell.c                                   |   60 +
 src/ephy-shell.h                                   |    8 +
 src/ephy-web-extension-dialog.c                    |  291 +++++
 src/ephy-web-extension-dialog.h                    |   35 +
 src/ephy-window.c                                  |   16 +
 src/meson.build                                    |    8 +
 src/resources/epiphany.gresource.xml               |    1 +
 src/resources/gtk/action-bar-end.ui                |   12 +
 src/resources/gtk/page-menu-popover.ui             |    8 +
 src/resources/gtk/web-extensions-dialog.ui         |   65 ++
 src/webextension/README.md                         |   59 +
 src/webextension/api/notifications.c               |   73 ++
 src/webextension/api/notifications.h               |   32 +
 src/webextension/api/pageaction.c                  |  166 +++
 src/webextension/api/pageaction.h                  |   32 +
 src/webextension/api/runtime.c                     |  122 ++
 src/webextension/api/runtime.h                     |   32 +
 src/webextension/api/tabs.c                        |  203 ++++
 src/webextension/api/tabs.h                        |   34 +
 src/webextension/ephy-web-extension-manager.c      |  968 ++++++++++++++++
 src/webextension/ephy-web-extension-manager.h      |   72 ++
 src/webextension/ephy-web-extension.c              | 1203 ++++++++++++++++++++
 src/webextension/ephy-web-extension.h              |  129 +++
 src/webextension/meson.build                       |    8 +
 src/window-commands.c                              |   14 +
 src/window-commands.h                              |    3 +
 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
@@ -315,35 +315,6 @@ ephy_string_remove_trailing (char *string,
   return 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
@@ -2,6 +2,18 @@
 <interface>
   <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>
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


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]