summaryrefslogtreecommitdiff
path: root/webhelper
diff options
context:
space:
mode:
Diffstat (limited to 'webhelper')
-rw-r--r--webhelper/Makefile.am.inc12
-rw-r--r--webhelper/lib/wh2private.c36
-rw-r--r--webhelper/lib/wh2private.h20
-rw-r--r--webhelper/webextensions/wh2extension.c524
-rw-r--r--webhelper/webextensions/wh2jscutil.c65
-rw-r--r--webhelper/webextensions/wh2jscutil.h25
-rw-r--r--webhelper/webhelper2.js500
-rw-r--r--webhelper/webhelper_private/config.js.in1
8 files changed, 1171 insertions, 12 deletions
diff --git a/webhelper/Makefile.am.inc b/webhelper/Makefile.am.inc
deleted file mode 100644
index 3531000..0000000
--- a/webhelper/Makefile.am.inc
+++ /dev/null
@@ -1,12 +0,0 @@
-# Copyright 2013 Endless Mobile, Inc.
-
-# # # INSTALL RULES # # #
-
-webhelper_sources = webhelper/webhelper.js
-
-webhelperdir = $(gjsmodulesdir)
-dist_webhelper_DATA = \
- $(webhelper_sources) \
- $(NULL)
-
-EOS_JS_COVERAGE_FILES = $(webhelper_sources)
diff --git a/webhelper/lib/wh2private.c b/webhelper/lib/wh2private.c
new file mode 100644
index 0000000..04a7710
--- /dev/null
+++ b/webhelper/lib/wh2private.c
@@ -0,0 +1,36 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+
+/* Copyright 2015 Endless Mobile, Inc. */
+
+#include <glib.h>
+#include <webkit2/webkit2.h>
+
+#include "wh2private.h"
+
+/**
+ * wh2_private_register_global_uri_scheme:
+ * @scheme: the network scheme to register
+ * @callback: a #WebKitURISchemeRequestCallback.
+ * @user_data: (closure): user data for the @callback
+ * @notify: destroy notify function for the @callback
+ *
+ * Workaround for https://bugzilla.gnome.org/show_bug.cgi?id=729611
+ *
+ * Registers a URI scheme handler with the default WebContext. Does not pass the
+ * GDestroyNotifyFunc, which GJS uses to shim a destructor for @callback, along
+ * to the the web context.
+ *
+ * The default web context is a global object which does not get destroyed
+ * until a atexit handler after the javascript runtime has been torn down.
+ * Calling into the GJS function destructor at that point would be a
+ * mistake.
+ */
+void
+wh2_register_uri_scheme (const gchar *scheme,
+ WebKitURISchemeRequestCallback callback,
+ gpointer user_data,
+ GDestroyNotify notify)
+{
+ WebKitWebContext *context = webkit_web_context_get_default ();
+ webkit_web_context_register_uri_scheme (context, scheme, callback, NULL, NULL);
+}
diff --git a/webhelper/lib/wh2private.h b/webhelper/lib/wh2private.h
new file mode 100644
index 0000000..a20fe87
--- /dev/null
+++ b/webhelper/lib/wh2private.h
@@ -0,0 +1,20 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+
+/* Copyright 2015 Endless Mobile, Inc. */
+
+#ifndef WH2_PRIVATE_H
+#define WH2_PRIVATE_H
+
+#include <glib.h>
+#include <webkit2/webkit2.h>
+
+G_BEGIN_DECLS
+
+void wh2_register_uri_scheme (const gchar *scheme,
+ WebKitURISchemeRequestCallback callback,
+ gpointer user_data,
+ GDestroyNotify notify);
+
+G_END_DECLS
+
+#endif /* WH2_PRIVATE_H */
diff --git a/webhelper/webextensions/wh2extension.c b/webhelper/webextensions/wh2extension.c
new file mode 100644
index 0000000..5b4ad06
--- /dev/null
+++ b/webhelper/webextensions/wh2extension.c
@@ -0,0 +1,524 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+
+/* Copyright 2015 Endless Mobile, Inc. */
+
+#include <math.h>
+#include <string.h>
+
+#include <glib.h>
+#include <JavaScriptCore/JavaScript.h>
+#include <webkit2/webkit-web-extension.h>
+#include <webkitdom/webkitdom.h>
+
+#include "wh2jscutil.h"
+
+#define PRIVATE_NAME "_webhelper_private"
+#define WH2_DBUS_INTERFACE_NAME "com.endlessm.WebHelper2.Translation"
+#define MAIN_PROGRAM_OBJECT_PATH "/com/endlessm/gettext"
+#define MAIN_PROGRAM_INTERFACE_NAME "com.endlessm.WebHelper2.Gettext"
+
+/* Declaration of externally visible symbol */
+void webkit_web_extension_initialize_with_user_data (WebKitWebExtension *, const GVariant *);
+
+typedef struct {
+ WebKitWebExtension *extension; /* unowned */
+ GDBusConnection *connection; /* unowned */
+ GDBusNodeInfo *node; /* owned */
+ GDBusInterfaceInfo *interface; /* owned by node */
+ GSList *bus_ids; /* GSList<guint>; owned */
+ GArray *unregistered_pages; /* GArray<guint64>; owned */
+ gchar *main_program_name; /* owned; well-known-name of main program */
+} Context;
+
+typedef struct {
+ Context *ctxt; /* unowned */
+ guint64 page_id;
+} PageContext;
+
+static const gchar introspection_xml[] =
+ "<node>"
+ "<interface name='" WH2_DBUS_INTERFACE_NAME "'>"
+ "<method name='Translate'/>"
+ "</interface>"
+ "</node>";
+
+static void
+context_free (Context *ctxt)
+{
+ g_clear_pointer (&ctxt->node, g_dbus_node_info_unref);
+ g_clear_pointer (&ctxt->bus_ids, g_slist_free);
+ if (ctxt->unregistered_pages != NULL)
+ g_array_free (ctxt->unregistered_pages, TRUE);
+ ctxt->unregistered_pages = NULL;
+ g_clear_pointer (&ctxt->main_program_name, g_free);
+ g_free (ctxt);
+}
+
+static gchar *
+translation_function (const gchar *message,
+ Context *ctxt)
+{
+ GError *error = NULL;
+ GVariant *result =
+ g_dbus_connection_call_sync (ctxt->connection, ctxt->main_program_name,
+ MAIN_PROGRAM_OBJECT_PATH,
+ MAIN_PROGRAM_INTERFACE_NAME, "Gettext",
+ g_variant_new ("(s)", message),
+ (GVariantType *) "(s)",
+ G_DBUS_CALL_FLAGS_NO_AUTO_START,
+ -1 /* timeout */, NULL /* cancellable */,
+ &error);
+ if (result == NULL)
+ {
+ g_warning ("No return value from gettext: %s", error->message);
+ g_clear_error (&error);
+ return g_strdup (message);
+ }
+
+ gchar *retval;
+ g_variant_get (result, "(s)", &retval);
+ g_variant_unref (result);
+ return retval;
+}
+
+static gchar *
+ngettext_translation_function (const gchar *singular,
+ const gchar *plural,
+ gulong number,
+ Context *ctxt)
+{
+ GError *error = NULL;
+ GVariant *result =
+ g_dbus_connection_call_sync (ctxt->connection, ctxt->main_program_name,
+ MAIN_PROGRAM_OBJECT_PATH,
+ MAIN_PROGRAM_INTERFACE_NAME, "NGettext",
+ g_variant_new ("(sst)", singular, plural,
+ number),
+ (GVariantType *) "(s)",
+ G_DBUS_CALL_FLAGS_NO_AUTO_START,
+ -1 /* timeout */, NULL /* cancellable */,
+ &error);
+ if (result == NULL)
+ {
+ g_warning ("No return value from ngettext: %s", error->message);
+ g_clear_error (&error);
+ return g_strdup (number == 1 ? singular : plural);
+ }
+
+ gchar *retval;
+ g_variant_get (result, "(s)", &retval);
+ g_variant_unref (result);
+ return retval;
+}
+
+static JSValueRef
+gettext_shim (JSContextRef js,
+ JSObjectRef function,
+ JSObjectRef this_object,
+ size_t n_args,
+ const JSValueRef args[],
+ JSValueRef *exception)
+{
+ if (n_args != 1)
+ {
+ gchar *errmsg = g_strdup_printf ("Expected one argument to gettext(),"
+ "but got %d.", n_args);
+ *exception = throw_exception (js, errmsg);
+ g_free (errmsg);
+ return NULL;
+ }
+ if (!JSValueIsString (js, args[0]))
+ {
+ *exception = throw_exception (js,
+ "Expected a string argument to gettext().");
+ return NULL;
+ }
+
+ JSObjectRef window = JSContextGetGlobalObject (js);
+ JSStringRef private_name = JSStringCreateWithUTF8CString (PRIVATE_NAME);
+ JSValueRef private_data = JSObjectGetProperty (js, window, private_name,
+ exception);
+ if (JSValueIsUndefined (js, private_data))
+ return NULL; /* propagate exception */
+ Context *ctxt = (Context *) JSObjectGetPrivate ((JSObjectRef) private_data);
+
+ JSStringRef message_ref = JSValueToStringCopy (js, args[0], exception);
+ if (message_ref == NULL)
+ return NULL; /* propagate exception */
+ gchar *message = string_ref_to_string (message_ref);
+ JSStringRelease (message_ref);
+
+ gchar *translation = translation_function (message, ctxt);
+ g_free (message);
+
+ JSValueRef retval = string_to_value_ref (js, translation);
+ g_free (translation);
+ return retval;
+}
+
+static JSValueRef
+ngettext_shim (JSContextRef js,
+ JSObjectRef function,
+ JSObjectRef this_object,
+ size_t n_args,
+ const JSValueRef args[],
+ JSValueRef *exception)
+{
+ if (n_args != 3)
+ {
+ gchar *errmsg = g_strdup_printf ("Expected three arguments to ngettext(),"
+ "but got %d.", n_args);
+ *exception = throw_exception (js, errmsg);
+ g_free (errmsg);
+ return NULL;
+ }
+ if (!JSValueIsString (js, args[0]))
+ {
+ *exception = throw_exception (js, "The first argument to ngettext() "
+ "must be a string.");
+ return NULL;
+ }
+ if (!JSValueIsString (js, args[1]))
+ {
+ *exception = throw_exception (js, "The second argument to ngettext() "
+ "must be a string.");
+ return NULL;
+ }
+ if (!JSValueIsNumber (js, args[2]))
+ {
+ *exception = throw_exception (js, "The third argument to ngettext() "
+ "must be a number.");
+ return NULL;
+ }
+
+ JSObjectRef window = JSContextGetGlobalObject (js);
+ JSStringRef private_name = JSStringCreateWithUTF8CString (PRIVATE_NAME);
+ JSValueRef private_data = JSObjectGetProperty (js, window, private_name,
+ exception);
+ if (JSValueIsUndefined (js, private_data))
+ return NULL; /* propagate exception */
+ Context *ctxt = (Context *) JSObjectGetPrivate ((JSObjectRef) private_data);
+
+ JSStringRef singular_ref = JSValueToStringCopy (js, args[0], exception);
+ if (singular_ref == NULL)
+ return NULL; /* propagate exception */
+ gchar *singular_msg = string_ref_to_string (singular_ref);
+ JSStringRelease (singular_ref);
+
+ JSStringRef plural_ref = JSValueToStringCopy (js, args[1], exception);
+ if (plural_ref == NULL)
+ {
+ g_free (singular_msg);
+ return NULL; /* propagate exception */
+ }
+ gchar *plural_msg = string_ref_to_string (plural_ref);
+ JSStringRelease (plural_ref);
+
+ double number = JSValueToNumber (js, args[2], exception);
+ if (isnan (number))
+ {
+ g_free (singular_msg);
+ g_free (plural_msg);
+ return NULL; /* propagate exception */
+ }
+
+ gchar *translation = ngettext_translation_function (singular_msg, plural_msg,
+ (gulong) number, ctxt);
+ g_free (singular_msg);
+ g_free (plural_msg);
+
+ JSValueRef retval = string_to_value_ref (js, translation);
+ g_free (translation);
+ return retval;
+}
+
+static void
+translate_html (WebKitDOMDocument *dom,
+ Context *ctxt)
+{
+ WebKitDOMNodeList *translatable;
+ GError *error = NULL;
+
+ translatable = webkit_dom_document_get_elements_by_name (dom, "translatable");
+
+ gulong index, length = webkit_dom_node_list_get_length (translatable);
+ for (index = 0; index < length; index++)
+ {
+ WebKitDOMNode *element = webkit_dom_node_list_item (translatable, index);
+
+ /* Translate the text */
+ if (WEBKIT_DOM_IS_HTML_ELEMENT (element))
+ {
+ WebKitDOMHTMLElement *el_html = WEBKIT_DOM_HTML_ELEMENT (element);
+ gchar *inner_html = webkit_dom_html_element_get_inner_html (el_html);
+ gchar *translated_html = translation_function (inner_html, ctxt);
+ webkit_dom_html_element_set_inner_html (el_html, translated_html,
+ &error);
+ if (error != NULL)
+ {
+ g_warning ("There was a problem translating '%s' to '%s': %s",
+ inner_html, translated_html, error->message);
+ g_clear_error (&error);
+ }
+
+ g_free (translated_html);
+ g_free (inner_html);
+ }
+ else
+ {
+ gchar *text = webkit_dom_node_get_text_content (element);
+ gchar *translated_text = translation_function (text, ctxt);
+ webkit_dom_node_set_text_content (element, translated_text, &error);
+ if (error != NULL)
+ {
+ g_warning ("There was a problem translating '%s' to '%s': %s",
+ text, translated_text, error->message);
+ g_clear_error (&error);
+ }
+
+ g_free (translated_text);
+ g_free (text);
+ }
+ }
+
+ g_object_unref (translatable);
+}
+
+static void
+on_wh2_method_call (GDBusConnection *connection,
+ const gchar *sender,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *method_name,
+ GVariant *parameters,
+ GDBusMethodInvocation *invocation,
+ PageContext *pctxt)
+{
+ if (strcmp (method_name, "Translate") != 0)
+ {
+ g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR,
+ G_DBUS_ERROR_UNKNOWN_METHOD,
+ "Unknown method %s invoked on interface %s",
+ method_name, interface_name);
+ return;
+ }
+
+ WebKitWebPage *page = webkit_web_extension_get_page (pctxt->ctxt->extension,
+ pctxt->page_id);
+ if (page == NULL)
+ return;
+ /* The page may have been destroyed, but WebKit doesn't let us find out. */
+
+ WebKitDOMDocument *document = webkit_web_page_get_dom_document (page);
+ if (document == NULL)
+ {
+ g_dbus_method_invocation_return_error_literal (invocation, G_IO_ERROR,
+ G_IO_ERROR_NOT_INITIALIZED,
+ "The web page has not loaded a document yet");
+ return;
+ }
+
+ translate_html (document, pctxt->ctxt);
+
+ g_dbus_method_invocation_return_value (invocation, NULL);
+}
+
+static GDBusInterfaceVTable dbus_impl_vtable = {
+ (GDBusInterfaceMethodCallFunc) on_wh2_method_call,
+ NULL, /* get property */
+ NULL /* set property */
+};
+
+static void
+register_object (guint64 page_id,
+ Context *ctxt)
+{
+ GError *error = NULL;
+
+ g_assert (ctxt->connection != NULL);
+
+ gchar *object_path = g_strdup_printf("/com/endlessm/webview/%"
+ G_GUINT64_FORMAT, page_id);
+
+ /* This struct is owned by the registered DBus object */
+ PageContext *pctxt = g_new0 (PageContext, 1);
+ pctxt->ctxt = ctxt;
+ pctxt->page_id = page_id;
+
+ guint bus_id =
+ g_dbus_connection_register_object (ctxt->connection, object_path,
+ ctxt->interface, &dbus_impl_vtable,
+ pctxt, (GDestroyNotify) g_free, &error);
+ g_free (object_path);
+ if (bus_id == 0)
+ {
+ g_critical ("Failed to export webview object on bus: %s", error->message);
+ g_clear_error (&error);
+ goto fail;
+ }
+
+ ctxt->bus_ids = g_slist_prepend (ctxt->bus_ids, GUINT_TO_POINTER (bus_id));
+ return;
+
+fail:
+ g_free (pctxt);
+}
+
+static void
+on_page_created (WebKitWebExtension *extension,
+ WebKitWebPage *page,
+ Context *ctxt)
+{
+ /* The ID is known to the main process and the web process. So we can address
+ a specific web page over DBus. */
+ guint64 id = webkit_web_page_get_id (page);
+
+ if (ctxt->connection == NULL)
+ {
+ /* The connection is not ready yet. Save the page ID in a list of pages
+ for which we need to register objects later. */
+ g_array_append_val (ctxt->unregistered_pages, id);
+ return;
+ }
+
+ register_object (id, ctxt);
+}
+
+/* window-object-cleared is the best time to define properties on the page's
+window object, according to the documentation. */
+static void
+on_window_object_cleared (WebKitScriptWorld *script_world,
+ WebKitWebPage *page,
+ WebKitFrame *frame,
+ Context *ctxt)
+{
+ JSGlobalContextRef js =
+ webkit_frame_get_javascript_context_for_script_world (frame, script_world);
+ JSObjectRef window = JSContextGetGlobalObject (js);
+
+ /* First we need to create a custom class for a private data object to store
+ our context in, because you can't pass callback data to JavaScriptCore
+ callbacks. You also can't set private data on a Javascript object if it's not
+ of a custom class, because the built-in classes don't allocate space for a
+ private pointer. */
+ JSClassDefinition class_def = {
+ .className = "PrivateContextObject"
+ };
+ JSClassRef klass = JSClassCreate (&class_def);
+ JSObjectRef private_data = JSObjectMake (js, klass, ctxt);
+ JSClassRelease (klass);
+
+ if (!set_object_property (js, window, PRIVATE_NAME, (JSValueRef) private_data,
+ kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontEnum | kJSPropertyAttributeDontDelete))
+ return;
+
+ JSObjectRef gettext_func =
+ JSObjectMakeFunctionWithCallback (js, NULL, gettext_shim);
+ if (!set_object_property (js, window, "gettext", (JSValueRef) gettext_func,
+ kJSPropertyAttributeNone))
+ return;
+
+ JSObjectRef ngettext_func =
+ JSObjectMakeFunctionWithCallback (js, NULL, ngettext_shim);
+ if (!set_object_property (js, window, "ngettext", (JSValueRef) ngettext_func,
+ kJSPropertyAttributeNone))
+ return;
+}
+
+static void
+on_bus_acquired (GDBusConnection *connection,
+ const gchar *name,
+ Context *ctxt)
+{
+ GError *error = NULL;
+
+ ctxt->connection = connection;
+
+ /* Get a notification when Javascript is ready */
+ WebKitScriptWorld *script_world = webkit_script_world_get_default ();
+ g_signal_connect (script_world, "window-object-cleared",
+ G_CALLBACK (on_window_object_cleared), ctxt);
+
+ /* Export our interface on the bus */
+ ctxt->node = g_dbus_node_info_new_for_xml (introspection_xml, &error);
+ if (ctxt->node == NULL)
+ goto fail;
+ ctxt->interface = g_dbus_node_info_lookup_interface (ctxt->node,
+ WH2_DBUS_INTERFACE_NAME);
+ if (ctxt->interface == NULL)
+ goto fail;
+
+ /* Register DBus objects for any pages that were created before we got here */
+ guint ix;
+ for (ix = 0; ix < ctxt->unregistered_pages->len; ix++)
+ {
+ guint64 id = g_array_index (ctxt->unregistered_pages, guint64, ix);
+ register_object (id, ctxt);
+ }
+ g_array_remove_range (ctxt->unregistered_pages, 0,
+ ctxt->unregistered_pages->len);
+
+ return;
+
+fail:
+ if (error != NULL)
+ {
+ g_critical ("Error hooking up web extension DBus interface: %s",
+ error->message);
+ g_clear_error (&error);
+ }
+ else
+ {
+ g_critical ("Unknown error hooking up web extension DBus interface");
+ }
+}
+
+static void
+unregister_object (gpointer data,
+ GDBusConnection *connection)
+{
+ guint bus_id = GPOINTER_TO_UINT (data);
+ if (!g_dbus_connection_unregister_object (connection, bus_id))
+ g_critical ("Trouble unregistering object");
+}
+
+static void
+on_name_lost (GDBusConnection *connection,
+ const gchar *name,
+ Context *ctxt)
+{
+ if (connection == NULL)
+ {
+ g_warning ("Could not initialize DBus interface for WebHelper2 "
+ "extension; the name %s was lost.", name);
+ return;
+ }
+
+ g_slist_foreach (ctxt->bus_ids, (GFunc) unregister_object, connection);
+}
+
+/* Receives the main program's unique DBus name as user data. */
+G_MODULE_EXPORT void
+webkit_web_extension_initialize_with_user_data (WebKitWebExtension *extension,
+ const GVariant *data_from_app)
+{
+ const gchar *name = g_variant_get_string ((GVariant *) data_from_app, NULL);
+
+ Context *ctxt = g_new0 (Context, 1);
+ ctxt->extension = extension;
+ ctxt->unregistered_pages = g_array_new (FALSE, FALSE, sizeof (guint64));
+ ctxt->main_program_name = g_strdup (name);
+ gchar *well_known_name = g_strconcat (name, ".webhelper", NULL);
+
+ g_signal_connect (extension, "page-created",
+ G_CALLBACK (on_page_created), ctxt);
+
+ g_bus_own_name (G_BUS_TYPE_SESSION, well_known_name,
+ G_BUS_NAME_OWNER_FLAGS_NONE,
+ (GBusAcquiredCallback) on_bus_acquired,
+ NULL, /* name acquired callback */
+ (GBusNameLostCallback) on_name_lost,
+ ctxt, (GDestroyNotify) context_free);
+
+ g_free (well_known_name);
+}
diff --git a/webhelper/webextensions/wh2jscutil.c b/webhelper/webextensions/wh2jscutil.c
new file mode 100644
index 0000000..34f325f
--- /dev/null
+++ b/webhelper/webextensions/wh2jscutil.c
@@ -0,0 +1,65 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+
+/* Copyright 2015 Endless Mobile, Inc. */
+
+#include <glib.h>
+#include <JavaScriptCore/JavaScript.h>
+
+#include "wh2jscutil.h"
+
+G_GNUC_INTERNAL
+gboolean
+set_object_property (JSContextRef js,
+ JSObjectRef object,
+ const gchar *property_name,
+ JSValueRef property_value,
+ JSPropertyAttributes flags)
+{
+ JSValueRef exception = NULL;
+ JSStringRef property_name_ref = JSStringCreateWithUTF8CString (property_name);
+ JSObjectSetProperty (js, object, property_name_ref, property_value, flags,
+ &exception);
+ JSStringRelease (property_name_ref);
+ if (exception != NULL)
+ {
+ g_critical ("There was a problem setting the property '%s'.",
+ property_name);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+/* Returns a newly allocated string. */
+G_GNUC_INTERNAL
+gchar *
+string_ref_to_string (JSStringRef string_ref)
+{
+ size_t bufsize = JSStringGetMaximumUTF8CStringSize (string_ref);
+ gchar *string = g_new0 (gchar, bufsize);
+ JSStringGetUTF8CString (string_ref, string, bufsize);
+ return string;
+}
+
+G_GNUC_INTERNAL
+JSValueRef
+string_to_value_ref (JSContextRef js,
+ const gchar *string)
+{
+ JSStringRef string_ref = JSStringCreateWithUTF8CString (string);
+ JSValueRef value_ref = JSValueMakeString (js, string_ref);
+ /* value_ref owns string_ref now */
+ return value_ref;
+}
+
+G_GNUC_INTERNAL
+JSValueRef
+throw_exception (JSContextRef js,
+ const gchar *message)
+{
+ JSValueRef msgval = string_to_value_ref (js, message);
+ JSValueRef inner_error = NULL;
+ JSObjectRef exception = JSObjectMakeError (js, 1, &msgval, &inner_error);
+ if (inner_error != NULL)
+ return inner_error;
+ return (JSValueRef) exception;
+}
diff --git a/webhelper/webextensions/wh2jscutil.h b/webhelper/webextensions/wh2jscutil.h
new file mode 100644
index 0000000..af800e5
--- /dev/null
+++ b/webhelper/webextensions/wh2jscutil.h
@@ -0,0 +1,25 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+
+/* Copyright 2015 Endless Mobile, Inc. */
+
+#ifndef WH2_JSC_UTIL_H
+#define WH2_JSC_UTIL_H
+
+#include <glib.h>
+#include <JavaScriptCore/JavaScript.h>
+
+gboolean set_object_property (JSContextRef js,
+ JSObjectRef object,
+ const gchar *property_name,
+ JSValueRef property_value,
+ JSPropertyAttributes flags);
+
+gchar *string_ref_to_string (JSStringRef string_ref);
+
+JSValueRef string_to_value_ref (JSContextRef js,
+ const gchar *string);
+
+JSValueRef throw_exception (JSContextRef js,
+ const gchar *message);
+
+#endif /* WH2_JSC_UTIL_H */
diff --git a/webhelper/webhelper2.js b/webhelper/webhelper2.js
new file mode 100644
index 0000000..b5f87f8
--- /dev/null
+++ b/webhelper/webhelper2.js
@@ -0,0 +1,500 @@
+// Copyright 2015 Endless Mobile, Inc.
+
+imports.gi.versions.WebKit2 = '4.0';
+
+const Format = imports.format;
+const Gio = imports.gi.Gio;
+const GLib = imports.gi.GLib;
+const GObject = imports.gi.GObject;
+const Lang = imports.lang;
+const WebHelper2Private = imports.gi.WebHelper2Private;
+const WebKit2 = imports.gi.WebKit2;
+
+const Config = imports.webhelper_private.config;
+
+String.prototype.format = Format.format;
+
+const WH2_URI_SCHEME = 'webhelper';
+const DBUS_WEBVIEW_EXPORT_PATH = '/com/endlessm/webview/';
+const WH2_DBUS_EXTENSION_INTERFACE = '\
+ <node> \
+ <interface name="com.endlessm.WebHelper2.Translation"> \
+ <method name="Translate"/> \
+ </interface> \
+ </node>';
+const WH2_DBUS_MAIN_PROGRAM_INTERFACE = '\
+ <node> \
+ <interface name="com.endlessm.WebHelper2.Gettext"> \
+ <method name="Gettext"> \
+ <arg name="message" type="s" direction="in"/> \
+ <arg name="translation" type="s" direction="out"/> \
+ </method> \
+ <method name="NGettext"> \
+ <arg name="message_singular" type="s" direction="in"/> \
+ <arg name="message_plural" type="s" direction="in"/> \
+ <arg name="number" type="t" direction="in"/> \
+ <arg name="translation" type="s" direction="out"/> \
+ </method> \
+ </interface> \
+ </node>';
+
+/**
+ * Namespace: WebHelper2
+ * Convenience library for running web applications
+ *
+ * WebHelper is a convenience library for displaying web applications inside a
+ * GTK container running WebKitGTK.
+ * WebHelper2 is the WebKit2 version.
+ *
+ * Although WebKit provides an easy way of communicating from GTK code to
+ * the in-browser Javascript, through the execute_script() method, it is not so
+ * easy to communicate the other way around.
+ *
+ * WebHelper solves that problem by detecting when the web page navigates to a
+ * custom action URI.
+ * The custom URI corresponds to a function that you define using
+ * <WebHelper.define_web_action()>, and you can pass parameters to the
+ * function.
+ *
+ * Another often-encountered problem is localizing text through the same API as
+ * your main GTK program.
+ * WebHelper solves this problem by allowing you to mark strings in your HTML
+ * page and translating them through a function of your choice when you run
+ * <WebHelper.translate_html()>.
+ * It also exposes a *gettext()* function in the client-side Javascript.
+ */
+
+/**
+ * Class: WebHelper
+ * Helper object for a WebKit2 web application
+ *
+ * Constructor parameters:
+ * props - a dictionary of construction properties and values (default {})
+ *
+ * The application class for your web application should create <WebHelper> in
+ * its *vfunc_dbus_register()* implementation, with appropriate
+ * <well-known-name> and <connection> parameters.
+ * After that, you can do further setup on it, such as defining web actions, in
+ * your *vfunc_startup()* implementation.
+ *
+ * There is no need to set up specially any web views that you create, unlike
+ * WebKit1 where you must set up <Application.web_actions_handler()>.
+ *
+ * Example:
+ * > const TestApplication = new Lang.Class({
+ * > Name: 'TestApplication',
+ * > Extends: Gtk.Application,
+ * >
+ * > vfunc_dbus_register: function (connection, object_path) {
+ * > this._webhelper = new WebHelper2.WebHelper({
+ * > well_known_name: this.application_id,
+ * > connection: connection,
+ * > });
+ * > return this.parent(connection, object_path);
+ * > },
+ * >
+ * > vfunc_startup: function () {
+ * > this.parent();
+ * >
+ * > this._webhelper.set_gettext(Gettext.dgettext.bind(null,
+ * > GETTEXT_DOMAIN));
+ * >
+ * > let window = new Gtk.Window();
+ * > let webview = new WebKit2.WebView();
+ * > webview.connect('load-changed', (webview, event) => {
+ * > if (event === WebKit2.LoadEvent.FINISHED)
+ * > this._webhelper.translate_html(webview, null, (obj, res) => {
+ * > this._webhelper.translate_html_finish(res);
+ * > window.show_all();
+ * > });
+ * > });
+ * > window.add(webview);
+ * > webview.load_uri('file:///path/to/my/page.html');
+ * > },
+ * >
+ * > vfunc_dbus_unregister: function (connection, object_path) {
+ * > this.parent(connection, object_path);
+ * > this._webhelper.unregister();
+ * > },
+ * >});
+ * >
+ * >let app = new TestApplication({
+ * > application_id: 'com.example.SmokeGrinder',
+ * >});
+ * >app.run(ARGV);
+ */
+const WebHelper = new Lang.Class({
+ Name: 'WebHelper',
+ GTypeName: 'Wh2WebHelper',
+ Extends: GObject.Object,
+ Properties: {
+ /**
+ * Property: well-known-name
+ * Well-known bus name owned by the calling program
+ *
+ * Type:
+ * string
+ *
+ * This property is required at construction time.
+ * It must conform to <the rules for well-known bus names at
+ * http://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names>.
+ *
+ * This must be a well-known bus name that your program owns.
+ * The easiest way to ensure that is to use your application's ID, since
+ * every application class registers its ID as a bus name.
+ */
+ 'well-known-name': GObject.ParamSpec.string('well-known-name',
+ 'Well-known name',
+ 'Well-known bus name owned by the calling program',
+ GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
+ ''),
+ /**
+ * Property: connection
+ * DBus connection owned by the calling program
+ *
+ * Type:
+ * *Gio.DBusConnection*
+ *
+ * This property is required at construction time.
+ *
+ * This must be a DBus connection object that your program owns.
+ * The easiest way to ensure that is to use the connection object passed
+ * in to your application's *vfunc_dbus_register()* function.
+ */
+ 'connection': GObject.ParamSpec.object('connection', 'Connection',
+ 'DBus connection owned by the calling program',
+ GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
+ Gio.DBusConnection.$gtype),
+ },
+
+ _init: function (props={}) {
+ this._web_actions = {};
+ this._gettext = null;
+ this._ngettext = null;
+ this._ProxyConstructor =
+ Gio.DBusProxy.makeProxyWrapper(WH2_DBUS_EXTENSION_INTERFACE);
+ this.parent(props);
+
+ if (this.well_known_name === '')
+ throw new Error('The "well-known-name" parameter is required.');
+ this._extension_name = this.well_known_name + '.webhelper';
+
+ // Set up Webkit to load our web extension
+ let context = WebKit2.WebContext.get_default();
+ context.connect('initialize-web-extensions', () => {
+ let libexec = Gio.File.new_for_path(Config.LIBEXECDIR);
+ let path = libexec.get_child('webhelper2').get_path();
+
+ let localpath = GLib.getenv('WEBHELPER_UNINSTALLED_EXTENSION_DIR');
+ if (localpath)
+ path = localpath;
+
+ context.set_web_extensions_directory(path);
+ context.set_web_extensions_initialization_user_data(new GLib.Variant('s',
+ this.well_known_name));
+ });
+
+ // Export our own DBus interface
+ this._dbus_impl =
+ Gio.DBusExportedObject.wrapJSObject(WH2_DBUS_MAIN_PROGRAM_INTERFACE,
+ this);
+ this._dbus_impl.export(this.connection, '/com/endlessm/gettext');
+
+ // Set up handling for webhelper:// URIs
+ WebHelper2Private.register_uri_scheme(WH2_URI_SCHEME,
+ this._on_endless_uri_request.bind(this));
+ },
+
+ _on_endless_uri_request: function (request) {
+ let uri = request.get_uri();
+
+ // get the name and parameters for the desired function
+ let f_call = uri.substr((WH2_URI_SCHEME + '://').length).split('?');
+ let function_name = decodeURI(f_call[0]);
+
+ if (!this._web_actions.hasOwnProperty(function_name))
+ throw new Error(('Undefined WebHelper action "%s". Did you define it with ' +
+ 'WebHelper.Application.define_web_action()?').format(function_name));
+
+ let parameters = {};
+ if (f_call[1]) {
+ // there are parameters
+ let params = f_call[1].split('&');
+ params.forEach(function (entry) {
+ let param = entry.split('=');
+
+ if (param.length == 2) {
+ param[0] = decodeURIComponent(param[0]);
+ param[1] = decodeURIComponent(param[1]);
+ // and now we add it...
+ parameters[param[0]] = param[1];
+ }
+ });
+ }
+
+ (this._web_actions[function_name].bind(this))(parameters);
+
+ // Don't call request.finish(), because we don't want to finish the
+ // action, which would involve loading a new page. The request dies
+ // if we return from this function without calling ref() or finish()
+ // on it.
+ },
+
+ // DBus implementations
+
+ Gettext: function (string) {
+ return this._gettext(string);
+ },
+
+ NGettext: function (singular, plural, number) {
+ return this._ngettext(singular, plural, number);
+ },
+
+ // Public API
+
+ /**
+ * Method: set_gettext
+ * Define function which translates text
+ *
+ * Parameters:
+ * gettext_func - a function, or null
+ *
+ * When you plan to translate text in your web application, set this
+ * property to the translation function.
+ * The function must take one parameter, a string, and also return a
+ * string.
+ * The canonical example is gettext().
+ *
+ * This function will be called with each string to translate when you call
+ * <translate_html()>.
+ * The function is also made available directly to the browser-side
+ * Javascript as *gettext()*, a property of the global object.
+ *
+ * Pass null for _gettext_func_ to unset the translation function.
+ *
+ * If this function has not been called, or has most recently been called
+ * with null, then it is illegal to call <translate_html()>.
+ *
+ * Example:
+ * > const Gettext = imports.gettext;
+ * > const GETTEXT_DOMAIN = 'smoke-grinder';
+ * >
+ * > webhelper.set_gettext(Gettext.dgettext.bind(null, GETTEXT_DOMAIN));
+ */
+ set_gettext: function (gettext_func) {
+ if (gettext_func !== null && typeof gettext_func !== 'function')
+ throw new Error('The translation function must be a function, or ' +
+ 'null.');
+ this._gettext = gettext_func;
+ },
+
+ /**
+ * Method: get_gettext
+ * Retrieve the currently set translation function
+ *
+ * Returns:
+ * the translation function previously set with <set_gettext()>, or null
+ * if none is currently set.
+ */
+ get_gettext: function () {
+ return this._gettext;
+ },
+
+ /**
+ * Method: set_ngettext
+ * Define function which translates singular/plural text
+ *
+ * Parameters:
+ * ngettext_func - a function, or null
+ *
+ * When you plan to translate text in your web application, set this
+ * property to the translation function.
+ * The function must take three parameters: a string singular message, a
+ * string plural message, and a number for which to generate the message.
+ * The function must return a string.
+ * The canonical example is ngettext().
+ *
+ * This function is made available directly to the browser-side Javascript
+ * as *ngettext()*, a property of the global object.
+ *
+ * Pass null for _ngettext_func_ to unset the translation function.
+ *
+ * If this function has not been called, or has most recently been called
+ * with null, then it is illegal to call *ngettext()* in the client-side
+ * Javascript.
+ *
+ * Example:
+ * > const WebHelper2 = imports.webhelper2;
+ * > const Gettext = imports.gettext;
+ * > const GETTEXT_DOMAIN = 'smoke-grinder';
+ * >
+ * > let webhelper = new WebHelper2.WebHelper('com.example.SmokeGrinder');
+ * > webhelper.set_gettext(Gettext.dngettext.bind(null, GETTEXT_DOMAIN));
+ */
+ set_ngettext: function (ngettext_func) {
+ if (ngettext_func !== null && typeof ngettext_func !== 'function')
+ throw new Error('The translation function must be a function, or ' +
+ 'null.');
+ this._ngettext = ngettext_func;
+ },
+
+ /**
+ * Method: get_ngettext
+ * Retrieve the currently set singular/plural translation function
+ *
+ * Returns:
+ * the translation function previously set with <set_ngettext()>, or null
+ * if none is currently set.
+ */
+ get_ngettext: function () {
+ return this._ngettext;
+ },
+
+ /**
+ * Method: translate_html
+ * Translate the HTML page in a webview asynchronously
+ *
+ * Parameters:
+ * webview - a *WebKit2.WebView* with HTML loaded
+ * cancellable - a *Gio.Cancellable*, or null
+ * callback - a function that takes two parameters: this <WebHelper>
+ * object, and a result object; or null if you don't want a callback
+ *
+ * Perform translation on all HTML elements marked with name="translatable"
+ * in the HTML document displaying in _webview_.
+ * The translation will be performed using the function you have set using
+ * <set_gettext()>.
+ *
+ * When the translation is finished, _callback_ will be called.
+ * You can get the result of the operation by calling
+ * <translate_html_finish()> with the _result_ object passed to _callback_.
+ *
+ * You can optionally pass _cancellable_ if you want to be able to cancel
+ * the operation.
+ *
+ * Example:
+ * > webview.connect('load-changed', (webview, event) => {
+ * > if (event === WebKit2.LoadEvent.FINISHED)
+ * > webhelper.translate_html(webview, null, (obj, res) => {
+ * > webhelper.translate_html_finish(res);
+ * > // Translation done, show the page
+ * > webview.show_all();
+ * > });
+ * > });
+ */
+ translate_html: function (webview, cancellable, callback) {
+ let task = { callback: callback };
+ // Wait for the DBus interface to appear on the bus
+ task.watch_id = Gio.DBus.watch_name(Gio.BusType.SESSION,
+ this._extension_name, Gio.BusNameWatcherFlags.NONE,
+ (connection, name, owner) => {
+ // name appeared
+ let webview_object_path = DBUS_WEBVIEW_EXPORT_PATH +
+ webview.get_page_id();
+ let proxy = new this._ProxyConstructor(connection,
+ this._extension_name, webview_object_path);
+ if (cancellable)
+ proxy.TranslateRemote(cancellable,
+ this._translate_callback.bind(this, task));
+ else
+ proxy.TranslateRemote(this._translate_callback.bind(this,
+ task));
+ },
+ null); // do nothing when name vanishes
+ return task;
+ },
+
+ _translate_callback: function (task, result, error) {
+ Gio.DBus.unwatch_name(task.watch_id);
+ if (error)
+ task.error = error;
+ if (task.callback)
+ task.callback(this, task);
+ },
+
+ /**
+ * Method: translate_html_finish
+ * Get the result of <translate_html()>
+ *
+ * Parameters:
+ * res - result object passed to your callback
+ *
+ * Finishes an operation started by <translate_html()>.
+ * If the operation ended in an error, this function will throw that error.
+ */
+ translate_html_finish: function (res) {
+ if (res.error)
+ throw res.error;
+ },
+
+ /**
+ * Method: define_web_action
+ * Define an action that may be invoked from a WebView
+ *
+ * Parameters:
+ * name - a string, which must be a valid URI location.
+ * implementation - a function (see Callback Parameters below.)
+ *
+ * Callback Parameters:
+ * dict - object containing properties corresponding to the query
+ * parameters that the web action was called with
+ *
+ * Sets up an action that may be invoked from an HTML document inside a
+ * WebView, or from the in-browser Javascript environment inside a WebView.
+ * If you set up an action "setVolume" as follows:
+ * > app.define_web_action('setVolume', function(dict) { ... });
+ * Then you can invoke the action inside the HTML document, e.g. as the
+ * target of a link, as follows:
+ * > <a href="endless://setVolume?volume=11">This one goes to 11</a>
+ * Or from the in-browser Javascript, by navigating to the action URI, as
+ * follows:
+ * > window.location.href = 'endless://setVolume?volume=11';
+ *
+ * In both cases, the function would then be called with the _dict_
+ * parameter equal to
+ * > { "volume": "11" }
+ *
+ * If an action called _name_ is already defined, the new _implementation_
+ * replaces the old one.
+ */
+ define_web_action: function (name, implementation) {
+ if (typeof implementation !== 'function') {
+ throw new Error('The implementation of a web action must be a ' +
+ 'function.');
+ }
+ this._web_actions[name] = implementation;
+ },
+
+ /**
+ * Method: define_web_actions
+ * Define several web actions at once
+ *
+ * Parameters:
+ * dict - an object, with web action names as property names, and their
+ * implementations as values
+ *
+ * Convenience method to define more than one web action at once.
+ * Calls <define_web_action()> on each property of _dict_.
+ *
+ * *Note* This API is Javascript-only. It will not be implemented in C.
+ */
+ define_web_actions: function (dict) {
+ Object.keys(dict).forEach((key) =>
+ this.define_web_action(key, dict[key]));
+ },
+
+ /**
+ * Method: unregister
+ * Break the connection to WebKit
+ *
+ * Breaks the connection to all webviews and removes all DBus objects.
+ * You should call this in your application's *vfunc_dbus_unregister()*
+ * implementation.
+ *
+ * After this function has been called, no WebHelper functionality will
+ * work.
+ */
+ unregister: function () {
+ this._dbus_impl.unexport_from_connection(this.connection);
+ },
+});
diff --git a/webhelper/webhelper_private/config.js.in b/webhelper/webhelper_private/config.js.in
new file mode 100644
index 0000000..f9d87cb
--- /dev/null
+++ b/webhelper/webhelper_private/config.js.in
@@ -0,0 +1 @@
+const LIBEXECDIR = '%libexecdir%';