summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPhilip Chimento <philip@endlessm.com>2015-05-18 17:37:45 -0700
committerPhilip Chimento <philip@endlessm.com>2015-05-21 13:22:21 -0700
commite2729f045a9459c9a320c617ac6e599814744e56 (patch)
tree1da582aae21c2dea1896ab26f8e7102e795c450b
parent6ff70f1a9ae70e3481411d27de7594477fa4d0d2 (diff)
Implement web actions in WebHelper2
This allows communicating with the host program through URIs of the form webhelper://action?param=value&param2=value2. Actions can be defined on the WebHelper object and given a callback in Javascript. Unfortunately we have to use a private C library to register the URI scheme, because of https://bugs.webkit.org/show_bug.cgi?id=116672 [endlessm/eos-sdk#291]
-rw-r--r--.gitignore2
-rw-r--r--Makefile.am22
-rw-r--r--configure.ac3
-rw-r--r--jasmine.json3
-rw-r--r--test/Makefile.am.inc1
-rw-r--r--test/smoke-tests/webhelper/webview2.js47
-rw-r--r--test/webhelper/testWebActions2.js121
-rw-r--r--webhelper/lib/wh2private.c36
-rw-r--r--webhelper/lib/wh2private.h20
-rw-r--r--webhelper/webhelper2.js113
10 files changed, 365 insertions, 3 deletions
diff --git a/.gitignore b/.gitignore
index bffe54c..cba4d8a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,8 @@ test/smoke-tests/hello
test/smoke-tests/images/credits.gresource
Endless-0.gir
Endless-0.typelib
+WebHelper2Private-1.0.gir
+WebHelper2Private-1.0.typelib
endless/eosresource.c
endless/eosresource-private.h
tools/eos-application-manifest/eos-application-manifest
diff --git a/Makefile.am b/Makefile.am
index 5dc8696..0e54fe6 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -100,6 +100,18 @@ dist_webhelper_private_DATA = webhelper/webhelper_private/config.js
EOS_JS_COVERAGE_FILES = $(webhelper_sources)
+## Workaround for https://bugs.webkit.org/show_bug.cgi?id=116672
+## When that is solved, we can eliminate this private library and go back to
+## using pure Javascript in WebHelper.
+lib_LTLIBRARIES += libwebhelper2private.la
+libwebhelper2private_la_SOURCES = \
+ webhelper/lib/wh2private.c \
+ webhelper/lib/wh2private.h \
+ $(NULL)
+libwebhelper2private_la_CPPFLAGS = @WEBHELPER2_PRIVATE_CFLAGS@
+libwebhelper2private_la_LIBADD = @WEBHELPER2_PRIVATE_LIBS@
+libwebhelper2private_la_LDFLAGS = -avoid-version
+
webhelper2extensionsdir = $(libexecdir)/webhelper2
webhelper2extensions_LTLIBRARIES = wh2extension.la
wh2extension_la_SOURCES = webhelper/webextensions/wh2extension.c
@@ -132,6 +144,16 @@ Endless_@EOS_SDK_API_VERSION@_gir_FILES = $(introspection_sources)
Endless_@EOS_SDK_API_VERSION@_gir_EXPORT_PACKAGES = @EOS_SDK_API_NAME@
INTROSPECTION_GIRS += Endless-@EOS_SDK_API_VERSION@.gir
+WebHelper2Private-1.0.gir: libwebhelper2private.la
+WebHelper2Private_1_0_gir_INCLUDES = GObject-2.0 GLib-2.0 WebKit2-4.0
+WebHelper2Private_1_0_gir_SCANNERFLAGS = \
+ --identifier-prefix=Wh2 \
+ --symbol-prefix=wh2 \
+ $(NULL)
+WebHelper2Private_1_0_gir_LIBS = libwebhelper2private.la
+WebHelper2Private_1_0_gir_FILES = $(libwebhelper2private_la_SOURCES)
+INTROSPECTION_GIRS += WebHelper2Private-1.0.gir
+
girdir = $(datadir)/gir-1.0
gir_DATA = $(INTROSPECTION_GIRS)
diff --git a/configure.ac b/configure.ac
index fe37a8b..e6f140f 100644
--- a/configure.ac
+++ b/configure.ac
@@ -221,6 +221,9 @@ PKG_CHECK_MODULES([WEBHELPER2_EXTENSION], [
$GLIB_REQUIREMENT
$GOBJECT_REQUIREMENT
$WEBKIT2_REQUIREMENT])
+PKG_CHECK_MODULES([WEBHELPER2_PRIVATE], [
+ $GLIB_REQUIREMENT
+ $WEBKIT2_REQUIREMENT])
# Check installed GIRs for webhelper JS module
EOS_CHECK_GJS_GIR([GLib], [2.0])
diff --git a/jasmine.json b/jasmine.json
index 4d2ef8b..9c7b40c 100644
--- a/jasmine.json
+++ b/jasmine.json
@@ -14,6 +14,7 @@
"environment": {
"GI_TYPELIB_PATH": ".",
"LD_LIBRARY_PATH": ".libs",
- "XDG_CONFIG_HOME": "/tmp"
+ "XDG_CONFIG_HOME": "/tmp",
+ "WEBHELPER_UNINSTALLED_EXTENSION_DIR": ".libs"
}
}
diff --git a/test/Makefile.am.inc b/test/Makefile.am.inc
index 7e10953..411d006 100644
--- a/test/Makefile.am.inc
+++ b/test/Makefile.am.inc
@@ -46,6 +46,7 @@ javascript_tests = \
test/webhelper/testTranslate.js \
test/webhelper/testTranslate2.js \
test/webhelper/testWebActions.js \
+ test/webhelper/testWebActions2.js \
test/webhelper/testUpdateFontSize.js \
test/endless/testCustomContainer.js \
test/endless/testTopbarNavButton.js \
diff --git a/test/smoke-tests/webhelper/webview2.js b/test/smoke-tests/webhelper/webview2.js
index 604819c..ec029c9 100644
--- a/test/smoke-tests/webhelper/webview2.js
+++ b/test/smoke-tests/webhelper/webview2.js
@@ -2,6 +2,7 @@
const Endless = imports.gi.Endless;
const Gettext = imports.gettext;
+const Gtk = imports.gi.Gtk;
const Lang = imports.lang;
const WebHelper2 = imports.webhelper2;
const WebKit2 = imports.gi.WebKit2;
@@ -26,6 +27,17 @@ body { \
<body> \
<h1>First page</h1> \
\
+<p><a href="webhelper://moveToPage?name=page2">Move to page 2</a></p> \
+\
+<p><a \
+href="webhelper://showMessageFromParameter?msg=This%20is%20a%20message%20from%20the%20URL%20parameter">Show \
+message from parameter in this URL</a></p> \
+\
+<form action="webhelper://showMessageFromParameter"> \
+<input name="msg" value="I am in a form!"/> \
+<input type="submit" value="Show message using a form"/> \
+</form> \
+\
<p><a href="http://wikipedia.org">Regular link to a Web site</a></p> \
\
<p>This is text that will be italicized: <span name="translatable">Hello, \
@@ -50,6 +62,10 @@ const TestApplication = new Lang.Class({
this.parent();
this._webhelper.set_gettext((s) => s.italics());
+ this._webhelper.define_web_actions({
+ moveToPage: this.moveToPage.bind(this),
+ showMessageFromParameter: this.showMessageFromParameter.bind(this),
+ });
this._webview = new WebKit2.WebView();
this._webview.connect('load-changed', (webview, event) => {
@@ -58,7 +74,14 @@ const TestApplication = new Lang.Class({
this._webhelper.translate_html_finish(res);
});
});
- this._webview.load_html(TEST_HTML, 'file://');
+ this._webview.load_html(TEST_HTML, null);
+
+ this._page2 = new Gtk.Grid();
+ let back_button = new Gtk.Button({ label: 'Go back to page 1' });
+ back_button.connect('clicked', () => {
+ this._pm.visible_child_name = 'page1';
+ });
+ this._page2.add(back_button);
this._window = new Endless.Window({
application: this,
@@ -66,7 +89,10 @@ const TestApplication = new Lang.Class({
});
this._pm = this._window.page_manager;
+ this._pm.set_transition_type(Gtk.StackTransitionType.CROSSFADE);
this._pm.add(this._webview, { name: 'page1' });
+ this._pm.add(this._page2, { name: 'page2' });
+ this._pm.visible_child_name = 'page1';
this._window.show_all();
},
@@ -75,6 +101,25 @@ const TestApplication = new Lang.Class({
this.parent(connection, object_path);
this._webhelper.unregister();
},
+
+ // WEB ACTIONS
+
+ // dict['name'] is the name of the page to move to
+ moveToPage: function (dict) {
+ this._pm.visible_child_name = dict['name'];
+ },
+
+ // dict['msg'] is the message to display
+ showMessageFromParameter: function (dict) {
+ let dialog = new Gtk.MessageDialog({
+ buttons: Gtk.ButtonsType.CLOSE,
+ message_type: Gtk.MessageType.INFO,
+ text: dict['msg'],
+ transient_for: this._window,
+ });
+ dialog.run();
+ dialog.destroy();
+ },
});
let app = new TestApplication({
diff --git a/test/webhelper/testWebActions2.js b/test/webhelper/testWebActions2.js
new file mode 100644
index 0000000..5af5e2b
--- /dev/null
+++ b/test/webhelper/testWebActions2.js
@@ -0,0 +1,121 @@
+const Gio = imports.gi.Gio;
+const Gtk = imports.gi.Gtk;
+const Lang = imports.lang;
+const Mainloop = imports.mainloop;
+const WebHelper2 = imports.webhelper2;
+const WebKit2 = imports.gi.WebKit2;
+
+const WELL_KNOWN_NAME = 'com.endlessm.WebHelper.testWebActions2';
+
+Gtk.init(null);
+
+describe('WebKit2 actions bindings', function () {
+ let owner_id, connection, webview, webhelper, web_action_spy;
+
+ beforeAll(function (done) {
+ owner_id = Gio.DBus.own_name(Gio.BusType.SESSION, WELL_KNOWN_NAME,
+ Gio.BusNameOwnerFlags.NONE,
+ null, // bus acquired
+ (con, name) => { // name acquired
+ connection = con;
+ done();
+ },
+ null); // name lost
+ });
+
+ afterAll(function () {
+ Gio.DBus.unown_name(owner_id);
+ });
+
+ function run_loop(action_to_test) {
+ let string = '<html><head><meta http-equiv="refresh" content="0;url=' +
+ action_to_test + '"></head><body></body></html>';
+ webview.load_html(string, null);
+ Mainloop.run('webhelper2');
+ }
+
+ beforeEach(function () {
+ webhelper = new WebHelper2.WebHelper({
+ well_known_name: WELL_KNOWN_NAME,
+ connection: connection,
+ });
+ webview = new WebKit2.WebView();
+ web_action_spy = jasmine.createSpy('web_action_spy').and.callFake(() =>
+ Mainloop.quit('webhelper2'));
+ webhelper.define_web_action('action', web_action_spy);
+ });
+
+ afterEach(function () {
+ webhelper.unregister();
+ });
+
+ it('calls a web action', function () {
+ run_loop('webhelper://action');
+ expect(web_action_spy).toHaveBeenCalled();
+ });
+
+ it('calls a web action with a parameter', function () {
+ run_loop('webhelper://action?param=value');
+ expect(web_action_spy).toHaveBeenCalledWith(jasmine.objectContaining({
+ param: 'value',
+ }));
+ });
+
+ it('calls a web action with many parameters', function () {
+ run_loop('webhelper://action?first=thefirst&second=thesecond&third=thethird');
+ expect(web_action_spy).toHaveBeenCalledWith(jasmine.objectContaining({
+ first: 'thefirst',
+ second: 'thesecond',
+ third: 'thethird',
+ }));
+ });
+
+ it('uri-decodes parameter names', function () {
+ run_loop('webhelper://action?p%C3%A4r%C3%A4m%F0%9F%92%A9=value');
+ expect(web_action_spy).toHaveBeenCalledWith(jasmine.objectContaining({
+ 'päräm💩': 'value',
+ }));
+ });
+
+ it('uri-decodes parameter values', function () {
+ run_loop('webhelper://action?param=v%C3%A1lu%C3%A9%F0%9F%92%A9');
+ expect(web_action_spy).toHaveBeenCalledWith(jasmine.objectContaining({
+ param: 'válué💩',
+ }));
+ });
+
+ // This is commented out because GJS cannot catch exceptions across FFI
+ // interfaces (e.g. in GObject callbacks.)
+ xit('raises an exception on a nonexistent action instead of calling it', function () {
+ expect(function () {
+ run_loop('webhelper://nonexistentAction?param=value');
+ }).toThrow();
+ });
+
+ it('calls a web action with a blank parameter', function () {
+ run_loop('webhelper://action?param=');
+ expect(web_action_spy).toHaveBeenCalledWith(jasmine.objectContaining({
+ param: '',
+ }));
+ });
+
+ it('uri-decodes web action names', function () {
+ webhelper.define_web_action('äction💩Quit', web_action_spy);
+ run_loop('webhelper://%C3%A4ction%F0%9F%92%A9Quit');
+ expect(web_action_spy).toHaveBeenCalled();
+ });
+
+ it('can define more than one action with define_web_actions()', function () {
+ webhelper.define_web_actions({
+ action2: web_action_spy,
+ });
+ run_loop('webhelper://action2');
+ expect(web_action_spy).toBeTruthy();
+ });
+
+ it('complains when defining an action that is not a function', function () {
+ expect(function () {
+ webhelper.define_web_action('badAction', 'not a function');
+ }).toThrow();
+ });
+});
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/webhelper2.js b/webhelper/webhelper2.js
index beb0333..2897a61 100644
--- a/webhelper/webhelper2.js
+++ b/webhelper/webhelper2.js
@@ -2,14 +2,19 @@
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> \
@@ -35,7 +40,17 @@ const WH2_DBUS_MAIN_PROGRAM_INTERFACE = '\
* GTK container running WebKitGTK.
* WebHelper2 is the WebKit2 version.
*
- * One often-encountered problem is localizing text through the same API as
+ * 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
@@ -146,6 +161,7 @@ const WebHelper = new Lang.Class({
},
_init: function (props={}) {
+ this._web_actions = {};
this._gettext = null;
this._ProxyConstructor =
Gio.DBusProxy.makeProxyWrapper(WH2_DBUS_EXTENSION_INTERFACE);
@@ -175,6 +191,45 @@ const WebHelper = new Lang.Class({
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
@@ -306,6 +361,62 @@ const WebHelper = new Lang.Class({
},
/**
+ * 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
*