summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Dywan <christian@twotoasts.de>2019-02-08 18:43:26 +0100
committerGitHub <noreply@github.com>2019-02-08 18:43:26 +0100
commit67b6eb83005c2a9f29a02fe923adcc6833a32b61 (patch)
tree70cc5efc6b53a69eb7ee4d878d8b35609896cec5
parentbbddf921dbe2342491c6a79417751efd67bf7309 (diff)
downloadmidori-git-67b6eb83005c2a9f29a02fe923adcc6833a32b61.tar.gz
Initial support for cross-browser Web Extension API (#250)
* Initial support for cross-browser Web Extension API * Json-GLib is a introduced as a new dependency to read manifest.json * The README contains a new section to summarize the Web Extension API * Web extension support is implemented as a Peas plugin * Web extensions can be built-in, system-wide or user-installed * The Javascript interface is added to the core GLib.Resource * Midori.Tab shares a WebKit.UserContentManager per WebKit.WebContext Fixes: #244 * Only insert loaded web extensions and employ a delay installing the API Otherwise opening a new window with many extensions would result in significant delay.
-rw-r--r--.circleci/config.yml2
-rw-r--r--CMakeLists.txt3
-rw-r--r--README.md44
-rw-r--r--core/CMakeLists.txt1
-rw-r--r--core/tab.vala10
-rw-r--r--data/web-extension-api.js24
-rw-r--r--extensions/CMakeLists.txt6
-rw-r--r--extensions/web-extensions.plugin.in5
-rw-r--r--extensions/web-extensions.vala372
-rw-r--r--gresource.xml1
-rw-r--r--po/POTFILES.in2
-rw-r--r--vapi/javascriptcoregtk-4.0.vapi4
-rw-r--r--vapi/webkit2gtk-4.0.vapi6
13 files changed, 470 insertions, 10 deletions
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 2a987277..0a625924 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -9,7 +9,7 @@ jobs:
name: Install dependencies
command: |
apt-get update
- apt-get install -y cmake xvfb valac libwebkit2gtk-4.0-dev libsoup-gnome2.4-dev libgcr-3-dev libpeas-dev libsqlite3-dev intltool libxml2-utils
+ apt-get install -y cmake xvfb valac libwebkit2gtk-4.0-dev libsoup-gnome2.4-dev libgcr-3-dev libpeas-dev libsqlite3-dev libjson-glib-dev intltool libxml2-utils
- run:
name: Build
command: |
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 5c5f07db..c1884e8d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -54,8 +54,9 @@ pkg_check_modules(DEPS_GTK REQUIRED
webkit2gtk-4.0>=2.16.6
gcr-ui-3>=2.32
libpeas-gtk-1.0
+ json-glib-1.0>=0.12
)
-set(PKGS ${PKGS} gtk+-3.0 libsoup-2.4 gcr-ui-3 libpeas-gtk-1.0)
+set(PKGS ${PKGS} gtk+-3.0 libsoup-2.4 gcr-ui-3 libpeas-gtk-1.0 json-glib-1.0)
set(EXTRA_VAPIS
${CMAKE_SOURCE_DIR}/vapi/config.vapi
${CMAKE_SOURCE_DIR}/vapi/webkit2gtk-4.0.vapi
diff --git a/README.md b/README.md
index 2c534428..26a0b772 100644
--- a/README.md
+++ b/README.md
@@ -30,6 +30,7 @@ Midori is a lightweight yet powerful web browser which runs just as well on litt
* Customizable side panels.
* User scripts and styles a la Greasemonkey.
* Web developer tools powered by WebKit.
+* Cross-browser extensions compatible with Chrome, Firefox, Opera and Vivaldi
Please report comments, suggestions and bugs to:
https://github.com/midori-browser/core/issues
@@ -64,14 +65,15 @@ You can opt-in for the [beta release on the Play Store](https://play.google.com/
* [Vala](https://wiki.gnome.org/Projects/Vala) 0.30
* GCR 2.32
* Peas
+* JSON-Glib 0.12
Install dependencies on Astian OS, Ubuntu, Debian or other Debian-based distros:
- sudo apt install cmake valac libwebkit2gtk-4.0-dev libgcr-3-dev libpeas-dev libsqlite3-dev intltool libxml2-utils
+ sudo apt install cmake valac libwebkit2gtk-4.0-dev libgcr-3-dev libpeas-dev libsqlite3-dev libjson-glib-dev intltool libxml2-utils
Install dependencies on openSUSE:
- sudo zypper in cmake vala gcc webkit2gtk3-devel libgcr-devel libpeas-devel sqlite3-devel fdupes gettext-tools intltool libxml2-devel
+ sudo zypper in cmake vala gcc webkit2gtk3-devel libgcr-devel libpeas-devel sqlite3-devel json-glib-devel fdupes gettext-tools intltool libxml2-devel
Use CMake to build Midori:
@@ -247,6 +249,43 @@ Push your branch and **propose it for merging into master**.
This will automatically request a **review from other developers** who can then comment on it and provide feedback.
+# Extensions
+
+## Cross-browser web extensions
+
+The following API specification is supported by Midori:
+
+ manifest.json
+ name
+ version
+ description
+ background:
+ page: *.html
+ scripts:
+ - *.js
+ browser_action:
+ default_popup: *.html
+ default_icon: *.png
+ default_title
+ content_scripts:
+ js:
+ - *.js
+ css:
+ - *.css
+ manifest_version: 2
+
+ *.js
+ browser (chrome)
+ tabs
+ create
+ - url: uri
+ executeScript
+ - code: string
+ notifications
+ create
+ - title: string
+ message: string
+
# Jargon
* **freeze**: a period of bug fixes eg. 4/2 cycle means 4 weeks of features and 2 weeks of stabilization
@@ -254,6 +293,7 @@ This will automatically request a **review from other developers** who can then
* **ninja**: an internal tab, usually empty label, used for taking screenshots
* **fortress**: user of an ancient release like 0.4.3 as found on Raspberry Pie, Debian, Ubuntu
* **katze, sokoke, tabby**: legacy API names and coincidentally cat breeds
+* web extension: a cross-browser extension (plugin) - or in a webkit context, the multi-process api
# Midori for Android
diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt
index 23761abd..0fb9d09f 100644
--- a/core/CMakeLists.txt
+++ b/core/CMakeLists.txt
@@ -48,6 +48,7 @@ add_custom_command(OUTPUT resources.c
${CMAKE_SOURCE_DIR}/gresource.xml
DEPENDS ${CMAKE_SOURCE_DIR}/gresource.xml
DEPENDS ${CMAKE_SOURCE_DIR}/data/gtk3.css
+ DEPENDS ${CMAKE_SOURCE_DIR}/data/web-extension-api.js
DEPENDS ${UI_FILES}
)
diff --git a/core/tab.vala b/core/tab.vala
index e8197d6c..fd574827 100644
--- a/core/tab.vala
+++ b/core/tab.vala
@@ -59,7 +59,15 @@ namespace Midori {
public Tab (Tab? related, WebKit.WebContext web_context,
string? uri = null, string? title = null) {
- Object (related_view: related, web_context: web_context, visible: true);
+
+ // One content manager per web context
+ var content = web_context.get_data<WebKit.UserContentManager> ("user-content-manager");
+ if (content == null) {
+ content = new WebKit.UserContentManager ();
+ web_context.set_data<WebKit.UserContentManager> ("user-content-manager", content);
+ }
+
+ Object (related_view: related, web_context: web_context, user_content_manager: content, visible: true);
var settings = get_settings ();
settings.user_agent += " %s".printf (Config.CORE_USER_AGENT_VERSION);
diff --git a/data/web-extension-api.js b/data/web-extension-api.js
new file mode 100644
index 00000000..83e85bd3
--- /dev/null
+++ b/data/web-extension-api.js
@@ -0,0 +1,24 @@
+// Promise-based message handler
+var promises = [];
+var last_promise = 0;
+var m = function (fn, args, cb) {
+ var promise = new Promise (function (resolve, reject) {
+ window.webkit.messageHandlers.midori.postMessage ({fn: fn, args: args, promise: last_promise});
+ last_promise = promises.push({resolve: resolve, reject: reject});
+ });
+ return promise;
+}
+
+// Browser API
+window.browser = {
+ tabs: {
+ create: function (args, cb) { return m ('tabs.create', args, cb); },
+ executeScript: function (args, cb) { return m ('tabs.executeScript', args, cb); },
+ },
+ notifications: {
+ create: function (args, cb) { return m ('notifications.create', args, cb); },
+ }
+}
+
+// Compatibility with Chrome
+window.chrome = window.browser;
diff --git a/extensions/CMakeLists.txt b/extensions/CMakeLists.txt
index 8537e920..f2b63686 100644
--- a/extensions/CMakeLists.txt
+++ b/extensions/CMakeLists.txt
@@ -15,6 +15,12 @@ foreach(UNIT_SRC ${EXTENSIONS})
string(FIND ${UNIT_SRC} "." UNIT_EXTENSION)
if (UNIT_EXTENSION EQUAL -1)
set(UNIT ${UNIT_SRC})
+ file(GLOB MANIFEST RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "${UNIT_SRC}/manifest.json")
+ # Web Extension
+ if (MANIFEST MATCHES "(manifest.json)$")
+ execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink ${CMAKE_CURRENT_SOURCE_DIR}/${UNIT} ${CMAKE_CURRENT_BINARY_DIR}/${UNIT})
+ continue()
+ endif ()
file(GLOB UNIT_SRC RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "${UNIT_SRC}/*.vala")
elseif (${UNIT_SRC} MATCHES "(.vala)$")
string(REPLACE ".vala" "" UNIT ${UNIT_SRC})
diff --git a/extensions/web-extensions.plugin.in b/extensions/web-extensions.plugin.in
new file mode 100644
index 00000000..ad759848
--- /dev/null
+++ b/extensions/web-extensions.plugin.in
@@ -0,0 +1,5 @@
+[Plugin]
+Module=web-extensions
+IAge=3
+Builtin=true
+Name=Web Extensions
diff --git a/extensions/web-extensions.vala b/extensions/web-extensions.vala
new file mode 100644
index 00000000..155248d6
--- /dev/null
+++ b/extensions/web-extensions.vala
@@ -0,0 +1,372 @@
+/*
+ Copyright (C) 2019 Christian Dywan <christian@twotoats.de>
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ See the file COPYING for the full license text.
+*/
+
+namespace WebExtension {
+ public class Extension : Object {
+ public File file { get; protected set; }
+ public string name { get; set; }
+ public string description { get; set; }
+ public string? background_page { get; owned set; }
+ public List<string> background_scripts { get; owned set; }
+ public List<string> content_scripts { get; owned set; }
+ public List<string> content_styles { get; owned set; }
+ public Action? browser_action { get; set; }
+
+ public Extension (File file) {
+ Object (file: file, name: file.get_basename ());
+ }
+ }
+
+ public class Action : Object {
+ public string? icon { get; protected set; }
+ public string? title { get; protected set; }
+ public string? popup { get; protected set; }
+
+ public Action (string? icon, string? title, string? popup) {
+ Object (icon: icon, title: title, popup: popup);
+ }
+ }
+
+ public class ExtensionManager : Object {
+ static ExtensionManager? _default = null;
+ HashTable<string, Extension> extensions;
+
+ public delegate void ExtensionManagerForeachFunc (Extension extension);
+ public void @foreach (ExtensionManagerForeachFunc func) {
+ extensions.foreach ((key, value) => {
+ func (value);
+ });
+ }
+
+ // Note: Can't use the actual type here
+ // parameter 1 of type 'WebExtensionExtension' for signal
+ // "WebExtensionExtensionManager::extension_added" is not a value type
+ public signal void extension_added (Object extension);
+
+ public static ExtensionManager get_default () {
+ if (_default == null) {
+ _default = new ExtensionManager ();
+ _default.extensions = new HashTable<string, Extension> (str_hash, str_equal);
+ }
+ return _default;
+ }
+
+ public async void load_from_folder (WebKit.UserContentManager content, File folder) throws Error {
+ debug ("Load web extensions from %s", folder.get_path ());
+ var enumerator = yield folder.enumerate_children_async (FileAttribute.STANDARD_NAME, 0);
+ FileInfo info;
+ while ((info = enumerator.next_file ()) != null) {
+ var file = folder.get_child (info.get_name ());
+ string id = Checksum.compute_for_string (ChecksumType.MD5, file.get_path ());
+ var extension = extensions.lookup (id);
+ if (extension == null) {
+ extension = new Extension (file);
+
+ // If we find a manifest, this is a web extension
+ var manifest_file = file.get_child ("manifest.json");
+ if (!manifest_file.query_exists ()) {
+ continue;
+ }
+
+ try {
+ var json = new Json.Parser ();
+ yield json.load_from_stream_async (new DataInputStream (manifest_file.read ()));
+ var manifest = json.get_root ().get_object ();
+ if (manifest.has_member ("name")) {
+ extension.name = manifest.get_string_member ("name");
+ }
+
+ if (manifest.has_member ("background")) {
+ var background = manifest.get_object_member ("background");
+ if (background != null) {
+ if (background.has_member ("page")) {
+ extension.background_page = background.get_string_member ("page");
+ }
+
+ if (background.has_member ("scripts")) {
+ foreach (var element in background.get_array_member ("scripts").get_elements ()) {
+ extension.background_scripts.append (element.get_string ());
+ }
+ }
+ }
+ }
+
+ if (manifest.has_member ("browser_action")) {
+ var action = manifest.get_object_member ("browser_action");
+ if (action != null) {
+ extension.browser_action = new Action (
+ action.has_member ("default_icon") ? action.get_string_member ("default_icon") : null,
+ action.has_member ("default_title") ? action.get_string_member ("default_title") : null,
+ action.has_member ("default_popup") ? action.get_string_member ("default_popup") : null);
+ }
+ }
+
+ if (manifest.has_member ("content_scripts")) {
+ var content_scripts = manifest.get_object_member ("content_scripts");
+ if (content_scripts != null && content_scripts.has_member ("js")) {
+ foreach (var element in content_scripts.get_array_member ("js").get_elements ()) {
+ extension.content_scripts.append (element.get_string ());
+ }
+ }
+
+ if (content_scripts != null && content_scripts.has_member ("css")) {
+ foreach (var element in content_scripts.get_array_member ("css").get_elements ()) {
+ extension.content_styles.append (element.get_string ());
+ }
+ }
+ }
+
+ debug ("Loaded %s from %s", extension.name, file.get_path ());
+ extensions.insert (id, extension);
+ extension_added (extension);
+ } catch (Error error) {
+ warning ("Failed to load extension '%s': %s\n", extension.name, error.message);
+ }
+ }
+
+ foreach (var filename in extension.content_scripts) {
+ uint8[] script;
+ yield file.get_child (filename).load_contents_async (null, out script, null);
+ content.add_script (new WebKit.UserScript ((string)script,
+ WebKit.UserContentInjectedFrames.TOP_FRAME,
+ WebKit.UserScriptInjectionTime.END,
+ null, null));
+ }
+ foreach (var filename in extension.content_styles) {
+ uint8[] stylesheet;
+ yield file.get_child (filename).load_contents_async (null, out stylesheet, null);
+ content.add_style_sheet (new WebKit.UserStyleSheet ((string)stylesheet,
+ WebKit.UserContentInjectedFrames.TOP_FRAME,
+ WebKit.UserStyleLevel.USER,
+ null, null));
+ }
+ }
+ }
+
+ Midori.App app { get { return Application.get_default () as Midori.App; } }
+ Midori.Browser browser { get { return app.active_window as Midori.Browser; } }
+
+ void web_extension_message_received (WebKit.WebView web_view, WebKit.JavascriptResult result) {
+ unowned JS.GlobalContext context = result.get_global_context ();
+ unowned JS.Value value = result.get_value ();
+ if (value.is_object (context)) {
+ var object = value.to_object (context);
+ string? fn = js_to_string (context, object.get_property (context, new JS.String.create_with_utf8_cstring ("fn")));
+ if (fn != null && fn.has_prefix ("tabs.create")) {
+ var args = object.get_property (context, new JS.String.create_with_utf8_cstring ("args")).to_object (context);
+ string? url = js_to_string (context, args.get_property (context, new JS.String.create_with_utf8_cstring ("url")));
+ var tab = new Midori.Tab (null, browser.tab.web_context, url);
+ browser.add (tab);
+ var promise = object.get_property (context, new JS.String.create_with_utf8_cstring ("promise")).to_number (context);
+ debug ("Calling back to promise #%.f".printf (promise));
+ web_view.run_javascript.begin ("promises[%.f].resolve({id:%s});".printf (promise, tab.id));
+ } else if (fn != null && fn.has_prefix ("tabs.executeScript")) {
+ var args = object.get_property (context, new JS.String.create_with_utf8_cstring ("args")).to_object (context);
+ string? results = null;
+ string? code = js_to_string (context, args.get_property (context, new JS.String.create_with_utf8_cstring ("code")));
+ if (code != null) {
+ results = "[true]";
+ browser.tab.run_javascript.begin (code);
+ }
+ var promise = object.get_property (context, new JS.String.create_with_utf8_cstring ("promise")).to_number (context);
+ debug ("Calling back to promise #%.f".printf (promise));
+ web_view.run_javascript.begin ("promises[%.f].resolve(%s);".printf (promise, results ?? "[undefined]"));
+ } else if (fn != null && fn.has_prefix ("notifications.create")) {
+ var args = object.get_property (context, new JS.String.create_with_utf8_cstring ("args")).to_object (context);
+ string? message = js_to_string (context, args.get_property (context, new JS.String.create_with_utf8_cstring ("message")));
+ string? title = js_to_string (context, args.get_property (context, new JS.String.create_with_utf8_cstring ("title")));
+ var notification = new Notification (title);
+ notification.set_body (message);
+ // Use per-extension ID to avoid collisions
+ string extension_uri = web_view.uri;
+ app.send_notification (extension_uri, notification);
+ } else {
+ warning ("Unsupported Web Extension API: %s", fn);
+ }
+ } else {
+ warning ("Unexpected non-object value posted to Web Extension API: %s", js_to_string (context, value));
+ }
+ }
+
+ public void install_api (WebKit.WebView web_view) {
+ web_view.get_settings ().enable_write_console_messages_to_stdout = true; // XXX
+
+ var content = web_view.get_user_content_manager ();
+ if (content.register_script_message_handler ("midori")) {
+ content.script_message_received.connect ((result) => {
+ web_extension_message_received (web_view, result);
+ });
+ try {
+ string script = (string)resources_lookup_data ("/data/web-extension-api.js",
+ ResourceLookupFlags.NONE).get_data ();
+ content.add_script (new WebKit.UserScript ((string)script,
+ WebKit.UserContentInjectedFrames.ALL_FRAMES,
+ WebKit.UserScriptInjectionTime.START,
+ null, null));
+
+ } catch (Error error) {
+ critical ("Failed to setup WebExtension API: %s", error.message);
+ }
+ } else {
+ warning ("Failed to install WebExtension API handler");
+ }
+
+ }
+ }
+
+ public class WebView : WebKit.WebView {
+ public WebView (Extension extension, string? uri = null) {
+ Object (visible: true);
+
+ var manager = ExtensionManager.get_default ();
+ manager.install_api (this);
+
+ if (uri != null) {
+ load_uri (extension.file.get_child (uri).get_uri ());
+ } else {
+ load_html ("<body></body>", extension.file.get_uri ());
+ }
+ }
+
+ public override bool context_menu (WebKit.ContextMenu menu,
+ Gdk.Event event, WebKit.HitTestResult hit) {
+
+ if (hit.context_is_editable ()) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public override void close () {
+ destroy ();
+ }
+
+ public override bool web_process_crashed () {
+ load_alternate_html ("<body><button onclick='location.reload();'>Reload</button></body>", uri, uri);
+ return true;
+ }
+ }
+
+ public class Button : Gtk.MenuButton {
+ public Button (Extension extension) {
+ tooltip_text = extension.browser_action.title ?? extension.name;
+ visible = true;
+ focus_on_click = false;
+ var icon = new Gtk.Image.from_icon_name ("midori-symbolic", Gtk.IconSize.BUTTON);
+ icon.use_fallback = true;
+ icon.visible = true;
+ if (extension.browser_action.icon != null) {
+ debug ("Icon for %s: %s\n",
+ extension.name,
+ extension.file.get_child (extension.browser_action.icon).get_path ());
+ // Ensure the icon fits the size of a button in the toolbar
+ int icon_width = 16, icon_height = 16;
+ Gtk.icon_size_lookup (Gtk.IconSize.BUTTON, out icon_width, out icon_height);
+ // Take scale factor into account
+ icon_width *= scale_factor;
+ icon_height *= scale_factor;
+ try {
+ string filename = extension.file.get_child (extension.browser_action.icon).get_path ();
+ icon.pixbuf = new Gdk.Pixbuf.from_file_at_scale (filename, icon_width, icon_height, true);
+ } catch (Error error) {
+ warning ("Failed to set icon for %s: %s", extension.name, error.message);
+ }
+ }
+ if (extension.browser_action.popup != null) {
+ popover = new Gtk.Popover (this);
+ popover.add (new WebView (extension, extension.browser_action.popup));
+ }
+ add (icon);
+ }
+ }
+
+ static string? js_to_string (JS.GlobalContext context, JS.Value value) {
+ if (!value.is_string (context)) {
+ return null;
+ }
+ var str = value.to_string_copy (context);
+ uint8[] buffer = new uint8[str.get_maximum_utf8_cstring_size ()];
+ str.get_utf8_cstring (buffer);
+ return ((string)buffer);
+ }
+
+ public class Browser : Object, Midori.BrowserActivatable {
+ public Midori.Browser browser { owned get; set; }
+
+ async void install_extension (Extension extension) throws Error {
+ if (extension.browser_action != null) {
+ browser.add_button (new Button (extension as Extension));
+ }
+
+ // Employ a delay to avoid delaying startup with many extensions
+ uint src = Timeout.add (500, install_extension.callback);
+ yield;
+ Source.remove (src);
+
+ // Insert the background page in the browser, as a hidden widget
+ var background = new WebView (extension, extension.background_page);
+ (((Gtk.Container)browser.get_child ())).add (background);
+
+ foreach (var filename in extension.background_scripts) {
+ uint8[] script;
+ yield extension.file.get_child (filename).load_contents_async (null, out script, null);
+ background.get_user_content_manager ().add_script (new WebKit.UserScript ((string)script,
+ WebKit.UserContentInjectedFrames.TOP_FRAME,
+ WebKit.UserScriptInjectionTime.END,
+ null, null));
+ }
+ }
+
+ public void activate () {
+ if (browser.is_locked) {
+ return;
+ }
+
+ var manager = ExtensionManager.get_default ();
+ manager.extension_added.connect ((extension) => {
+ install_extension.begin ((Extension)extension);
+ });
+ manager.foreach ((extension) => {
+ install_extension.begin ((Extension)extension);
+ });
+
+ browser.tabs.add.connect (tab_added);
+ if (browser.tab != null) {
+ tab_added (browser.tab);
+ }
+ }
+
+ void tab_added (Gtk.Widget widget) {
+ browser.tabs.add.disconnect (tab_added);
+
+ var manager = ExtensionManager.get_default ();
+ var tab = widget as Midori.Tab;
+
+ var content = tab.get_user_content_manager ();
+ // Try and load plugins from build folder
+ var builtin_path = ((Midori.App)Application.get_default ()).exec_path.get_parent ().get_child ("extensions");
+ manager.load_from_folder.begin (content, builtin_path);
+ // System-wide plugins
+ manager.load_from_folder.begin (content, File.new_for_path (Config.PLUGINDIR));
+ // Plugins installed by the user
+ string user_path = Path.build_path (Path.DIR_SEPARATOR_S,
+ Environment.get_user_data_dir (), Config.PROJECT_NAME, "extensions");
+ manager.load_from_folder.begin (content, File.new_for_path (user_path));
+ }
+ }
+}
+
+[ModuleInit]
+public void peas_register_types(TypeModule module) {
+ ((Peas.ObjectModule)module).register_extension_type (
+ typeof (Midori.BrowserActivatable), typeof (WebExtension.Browser));
+}
diff --git a/gresource.xml b/gresource.xml
index 92418a7c..3d41bdd5 100644
--- a/gresource.xml
+++ b/gresource.xml
@@ -16,6 +16,7 @@
<file compressed="true">data/speed-dial.html</file>
<file compressed="true">data/gtk3.css</file>
<file compressed="true">data/logo-shade.svg</file>
+ <file compressed="true">data/web-extension-api.js</file>
<file compressed="true">data/bookmarks/Create.sql</file>
<file compressed="true">data/history/Create.sql</file>
<file compressed="true">data/history/Day.sql</file>
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 1ed62a84..9a4fb710 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -43,6 +43,8 @@ extensions/colorful-tabs.plugin.in
extensions/colorful-tabs.vala
extensions/session.plugin.in
extensions/session.vala
+extensions/web-extensions.plugin.in
+extensions/web-extensions.vala
ui/bookmarks-button.ui
ui/browser.ui
ui/clear-private-data.ui
diff --git a/vapi/javascriptcoregtk-4.0.vapi b/vapi/javascriptcoregtk-4.0.vapi
index c937cae1..05be292b 100644
--- a/vapi/javascriptcoregtk-4.0.vapi
+++ b/vapi/javascriptcoregtk-4.0.vapi
@@ -38,7 +38,7 @@ namespace JS {
[CCode (cname = "JSObjectCallAsFunction", instance_pos = 1.1)]
public JS.Value call_as_function (JS.Context ctx, JS.Object? this_object, [CCode (array_length_pos = 2.5)] JS.Value[]? arguments, out JS.Value? exception);
[CCode (cname = "JSObjectGetProperty", instance_pos = 1.1)]
- public JS.Value get_property (JS.Context ctx, JS.String property_name, out JS.Value? exception);
+ public JS.Value get_property (JS.Context ctx, JS.String property_name, out JS.Value? exception = null);
[CCode (cname = "JSObjectHasProperty", instance_pos = 1.1)]
public bool has_property (JS.Context ctx, JS.String property_name);
[CCode (cname = "JSObjectMakeFunction")]
@@ -54,7 +54,7 @@ namespace JS {
[CCode (cname = "JSStringGetMaximumUTF8CStringSize")]
public size_t get_maximum_utf8_cstring_size ();
[CCode (cname = "JSStringGetUTF8CString")]
- public size_t get_utf8_cstring ([CCode (array_length_type = "gsize")] ref uint8[] buffer);
+ public size_t get_utf8_cstring ([CCode (array_length_type = "gsize")] uint8[] buffer);
[CCode (cname = "JSStringIsEqual")]
public bool is_equal (JS.String b);
[CCode (cname = "JSStringIsEqualToUTF8CString")]
diff --git a/vapi/webkit2gtk-4.0.vapi b/vapi/webkit2gtk-4.0.vapi
index 8bdeac94..ad16d717 100644
--- a/vapi/webkit2gtk-4.0.vapi
+++ b/vapi/webkit2gtk-4.0.vapi
@@ -809,15 +809,15 @@ namespace WebKit {
public bool is_editable ();
public void load_alternate_html (string content, string content_uri, string? base_uri);
public void load_bytes (GLib.Bytes bytes, string? mime_type, string? encoding, string? base_uri);
- public void load_html (string content, string? base_uri);
+ public void load_html (string content, string? base_uri = null);
public void load_plain_text (string plain_text);
public void load_request (WebKit.URIRequest request);
public void load_uri (string uri);
public void reload ();
public void reload_bypass_cache ();
public void restore_session_state (WebKit.WebViewSessionState state);
- public async WebKit.JavascriptResult run_javascript (string script, GLib.Cancellable? cancellable) throws GLib.Error;
- public async WebKit.JavascriptResult run_javascript_from_gresource (string resource, GLib.Cancellable? cancellable) throws GLib.Error;
+ public async WebKit.JavascriptResult run_javascript (string script, GLib.Cancellable? cancellable = null) throws GLib.Error;
+ public async WebKit.JavascriptResult run_javascript_from_gresource (string resource, GLib.Cancellable? cancellable = null) throws GLib.Error;
public async GLib.InputStream save (WebKit.SaveMode save_mode, GLib.Cancellable? cancellable) throws GLib.Error;
public async bool save_to_file (GLib.File file, WebKit.SaveMode save_mode, GLib.Cancellable? cancellable) throws GLib.Error;
public void set_background_color (Gdk.RGBA rgba);