summaryrefslogtreecommitdiff
path: root/webhelper/webextensions/wh2extension.c
diff options
context:
space:
mode:
Diffstat (limited to 'webhelper/webextensions/wh2extension.c')
-rw-r--r--webhelper/webextensions/wh2extension.c524
1 files changed, 524 insertions, 0 deletions
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);
+}