diff --git a/.gitignore b/.gitignore index 6bf49fb..26847d8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ *~ +/python-webextension/*.o +/python-webextension/*.so +/python-webextension/*.py /eolie-cli /po /help diff --git a/python-webextension/Makefile b/python-webextension/Makefile new file mode 100644 index 0000000..a0041ba --- /dev/null +++ b/python-webextension/Makefile @@ -0,0 +1,31 @@ +# +# Makefile +# Adrian Perez, 2015-08-25 11:58 +# + +CFLAGS ?= -Os -Wall + +PYTHON ?= python3 +PKG_MODULES := pygobject-3.0 webkit2gtk-web-extension-4.0 ${PYTHON} +WEB_EXT_FLAGS := $(shell pkg-config ${PKG_MODULES} --cflags) +WEB_EXT_LIBS := $(shell pkg-config ${PKG_MODULES} --libs) + +CPPFLAGS += ${WEB_EXT_FLAGS} +LDLIBS += ${WEB_EXT_LIBS} + +all: pythonloader.so + +pythonloader.so: pythonloader.o + ${LD} ${LDFLAGS} -fPIC -shared -o $@ $^ ${LDLIBS} +pythonloader.so: CFLAGS += -fPIC + +install: + /bin/true +uninstall: + /bin/true + +clean: + ${RM} pythonloader.o pythonloader.so + +# vim:ft=make +# diff --git a/python-webextension/README b/python-webextension/README new file mode 100644 index 0000000..93e1be2 --- /dev/null +++ b/python-webextension/README @@ -0,0 +1,50 @@ +WebKit2GTK+ Python WebExtension loader +====================================== + +This is some exploratory code towards finding a good solution which can be +shipped built-in to allow +[loading of Python extensions in WebKit2GTK+](https://bugs.webkit.org/show_bug.cgi?id=140745) + + +How does it work? +----------------- + +The [pythonloader.c](pythonloader.c) file contains a small WebExtension +written in C, which is a thin shim that will: + +1. Initialize an embedded Python interpreter. +2. Import the `gi.repository.WebKit2WebExtension` module, to ensure that the + types used to implement WebExtensions are registered into PyGObject. +3. Import the `extension` Python module ([extension.py](extension.py)). +4. Invoke the `extension.initialize()` function, passing as arguments a + [WebKitWebExtension](http://webkitgtk.org/reference/webkit2gtk/stable/WebKitWebExtension.html) + instance, and a + [GVariant](https://developer.gnome.org/glib/stable/glib-GVariant.html) + which contains the additional the value previously set with + [webkit_web_context_set_web_extensions_initialization_user_data()](http://webkitgtk.org/reference/webkit2gtk/stable/WebKitWebContext.html#webkit-web-context-set-web-extensions-initialization-user-data). + +The Python extension can use all the functionality exposed via +GObject-Introspection, except for the `WebKit`, and `WebKit2` modules (web +extensions run in a process separate from the normal WebKit library, and using +them is unsupported — and will most likely crash your program). In particular, +the following modules included with WebKit2GTK+ can be used: + +* [Web Extensions](http://webkitgtk.org/reference/webkit2gtk/stable/ch02.html) +* [DOM bindings](http://webkitgtk.org/reference/webkitdomgtk/stable/index.html) + +Any other module exposed by GObject-Introspection can be used, as long as they +do not use the `WebKit`, or `WebKit2` modules. + + +Trying it out +------------- + +You will need the following components installed, including their development +headers and libraries: + +* WebKit2GTK+, version 2.4, or newer. +* Python 3.2, or newer. +* A working PyGObject installation. +* GNU Make. +* `pkg-config` + diff --git a/python-webextension/extension.py.in b/python-webextension/extension.py.in new file mode 100644 index 0000000..4839623 --- /dev/null +++ b/python-webextension/extension.py.in @@ -0,0 +1,66 @@ +# Copyright (c) 2014-2016 Cedric Bellegarde +# This program 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. +# This program 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 this program. If not, see . + +import sys +# Make sure we'll find the pygobject module, even in JHBuild +sys.path.insert(1, '@pyexecdir@') +# Make sure we'll find the eolie modules, even in JHBuild +sys.path.insert(1, '@pythondir@') + +from gi.repository import Gio + +from eolie.settings import Settings +from eolie.database_adblock import DatabaseAdblock +from eolie.sqlcursor import SqlCursor + + +class Application(Gio.Application): + def new(): + """ + Return a new Settings object + """ + app = Gio.Application.new(None, Gio.ApplicationFlags.IS_SERVICE) + app.__class__ = Application + app.cursors = {} + return app + +app = Application.new() +settings = Settings.new() +adblock = DatabaseAdblock() + + +def on_send_request(webpage, request, redirect): + """ + Filter based on adblock db + @param webpage as WebKit2WebExtension.WebPage + @param request as WebKitURIRequest + @param redirect as WebKit2WebExtension.URIResponse + """ + uri = request.get_uri() + if settings.get_value('adblock') and adblock.is_blocked(uri): + return True + +def on_page_created(extension, webpage): + """ + Connect to send request + @param extension as WebKit2WebExtension + @param webpage as WebKit2WebExtension.WebPage + """ + webpage.connect("send-request", on_send_request) + + +def initialize(extension, arguments): + """ + Connect to page created + @param extension as WebKit2WebExtension + """ + extension.connect("page-created", on_page_created) diff --git a/python-webextension/pythonloader.c b/python-webextension/pythonloader.c new file mode 100644 index 0000000..db68be7 --- /dev/null +++ b/python-webextension/pythonloader.c @@ -0,0 +1,161 @@ +/* + * pythonloader.c + * Copyright (C) 2015-2016 Adrian Perez + * Copyright (C) 2016 Nathan Hoad + * + * Distributed under terms of the MIT license. + */ + +#include +#include +#include + +/* + * XXX: Hacky workaround ahead! + * + * PyGObject internally uses _pygi_struct_new_from_g_type() to wrap + * GVariant instances into a gi.Struct, but the function is not in the + * public API. Instead, we temporarily wrap the GVariant into a GValue + * (ugh!), and use pyg_value_as_pyobject(), which in turn calls function + * pygi_value_to_py_structured_type() —also private—, and finally that + * in turn calls _pygi_struct_new_from_g_type() as initially desired. + */ +static PyObject* +g_variant_to_pyobject (GVariant *variant) +{ + GValue value = { 0, }; + g_value_init (&value, G_TYPE_VARIANT); + g_value_set_variant (&value, variant); + return pyg_value_as_pyobject (&value, FALSE); +} + +#define py_auto __attribute__((cleanup(py_object_cleanup))) + +static void +py_object_cleanup(void *ptr) +{ + PyObject **py_obj_location = ptr; + if (py_obj_location) { + Py_DECREF (*py_obj_location); + *py_obj_location = NULL; + } +} + + +#define PY_CHECK_ACT(expr, act, err_fmt, ...) \ + do { \ + if (!(expr)) { \ + g_printerr (err_fmt, ##__VA_ARGS__); \ + if (PyErr_Occurred ()) { \ + g_printerr (": "); \ + PyErr_Print (); \ + } else { \ + g_printerr (" (no error given)"); \ + } \ + act; \ + } \ + } while (0) + +#define PY_CHECK(expr, err_fmt, ...) \ + PY_CHECK_ACT (expr, return, err_fmt, ##__VA_ARGS__) + + +/* This would be "extension.py" from the source directory. */ +static const char *extension_name = "extension"; + + +static gboolean +pygi_require (const gchar *module, ...) +{ + PyObject py_auto *gi_module = PyImport_ImportModule ("gi"); + PY_CHECK_ACT (gi_module, return FALSE, "Could not import 'gi'"); + + PyObject py_auto *func = PyObject_GetAttrString (gi_module, "require_version"); + PY_CHECK_ACT (func, return FALSE, + "Could not obtain 'gi.require_version'"); + PY_CHECK_ACT (PyCallable_Check (func), return FALSE, + "Object 'gi.require_version' is not callable"); + + gboolean result = TRUE; + va_list arglist; + va_start (arglist, module); + while (module) { + /* + * For each module and version, call: gi.require(module, version) + */ + const gchar *version = va_arg (arglist, const gchar*); + { + PyObject py_auto *args = Py_BuildValue ("(ss)", module, version); + PyObject py_auto *rval = PyObject_CallObject (func, args); + PY_CHECK_ACT (rval, result = FALSE; break, + "Error calling 'gi.require_version(\"%s\", \"%s\")'", + module, version); + } + module = va_arg (arglist, const gchar*); + } + va_end (arglist); + return result; +} + + +G_MODULE_EXPORT void +webkit_web_extension_initialize_with_user_data (WebKitWebExtension *extension, + GVariant *user_data) +{ + Py_Initialize (); + +#if PY_VERSION_HEX < 0x03000000 + const char *argv[] = { "", NULL }; +#else + wchar_t *argv[] = { L"", NULL }; +#endif + + PySys_SetArgvEx (1, argv, 0); + + pygobject_init (-1, -1, -1); + if (PyErr_Occurred ()) { + g_printerr ("Could not initialize PyGObject"); + return; + } + + pyg_enable_threads (); + PyEval_InitThreads (); + + if (!pygi_require ("GLib", "2.0", + "WebKit2WebExtension", "4.0", + NULL)) + return; + + PyObject py_auto *web_ext_module = + PyImport_ImportModule ("gi.repository.WebKit2WebExtension"); + PY_CHECK (web_ext_module, + "Could not import 'gi.repository.WebKit2WebExtension'"); + + /* + * TODO: Instead of assuming that the Python import path contains the + * directory where the extension is, manually load and compile the + * extension code, then use PyImport_AddModule() to programmatically + * create a new module and PyImport_ExecCodeModule() to import it + * from a bytecode object. + */ + PyObject py_auto *py_filename = PyUnicode_FromString (extension_name); + PyObject py_auto *py_module = PyImport_Import (py_filename); + PY_CHECK (py_module, "Could not import '%s'", extension_name); + + PyObject py_auto *py_func = PyObject_GetAttrString (py_module, "initialize"); + PY_CHECK (py_func, "Could not obtain '%s.initialize'", extension_name); + PY_CHECK (PyCallable_Check (py_func), + "Object '%s.initialize' is not callable", extension_name); + + PyObject py_auto *py_extension = pygobject_new (G_OBJECT (extension)); + PyObject py_auto *py_extra_args = g_variant_to_pyobject (user_data); + + PY_CHECK (py_extra_args, "Cannot create GLib.Variant"); + + PyObject py_auto py_auto *func_args = PyTuple_New (2); + PyTuple_SetItem (func_args, 0, py_extension); + PyTuple_SetItem (func_args, 1, py_extra_args); + + PyObject py_auto *py_retval = PyObject_CallObject (py_func, func_args); + PY_CHECK (py_retval, "Error calling '%s.initialize'", extension_name); +}