GearyWebExtension: Add support for sending messages from JS to client

Define a vala-backed JS class in the extension and make that available
to pages when they are registered. Add some helper JS to PageState for
defining message sending functions. Listen for these in
Components.WebView and dispatch to the registered callback for it.
This commit is contained in:
Michael Gratton 2020-08-28 09:49:46 +10:00 committed by Michael James Gratton
parent db69807836
commit 6162785d99
3 changed files with 127 additions and 0 deletions

View file

@ -198,6 +198,24 @@ public abstract class Components.WebView : WebKit.WebView, Geary.BaseInterface {
/** Delegate for UserContentManager message callbacks. */ /** Delegate for UserContentManager message callbacks. */
public delegate void JavaScriptMessageHandler(WebKit.JavascriptResult js_result); public delegate void JavaScriptMessageHandler(WebKit.JavascriptResult js_result);
/**
* Delegate for message handler callbacks.
*
* @see register_message_callback
*/
protected delegate void MessageCallback(GLib.Variant? parameters);
// Work around for not being able to put delegates in a Gee collection.
private class MessageCallable {
public unowned MessageCallback handler;
public MessageCallable(MessageCallback handler) {
this.handler = handler;
}
}
/** /**
* Determines if the view's content has been fully loaded. * Determines if the view's content has been fully loaded.
* *
@ -263,6 +281,8 @@ public abstract class Components.WebView : WebKit.WebView, Geary.BaseInterface {
private Gee.List<ulong> registered_message_handlers = private Gee.List<ulong> registered_message_handlers =
new Gee.LinkedList<ulong>(); new Gee.LinkedList<ulong>();
private Gee.Map<string,MessageCallable> message_handlers =
new Gee.HashMap<string,MessageCallable>();
private double webkit_reported_height = 0; private double webkit_reported_height = 0;
@ -359,6 +379,7 @@ public abstract class Components.WebView : WebKit.WebView, Geary.BaseInterface {
this.user_content_manager.disconnect(id); this.user_content_manager.disconnect(id);
} }
this.registered_message_handlers.clear(); this.registered_message_handlers.clear();
this.message_handlers.clear();
base.destroy(); base.destroy();
} }
@ -568,6 +589,14 @@ public abstract class Components.WebView : WebKit.WebView, Geary.BaseInterface {
} }
} }
/**
* Registers a callback for a specific WebKit user message.
*/
protected void register_message_callback(string name,
MessageCallback handler) {
this.message_handlers.set(name, new MessageCallable(handler));
}
private void init(Application.Configuration config) { private void init(Application.Configuration config) {
// XXX get the allow prefix from the extension somehow // XXX get the allow prefix from the extension somehow
@ -595,6 +624,8 @@ public abstract class Components.WebView : WebKit.WebView, Geary.BaseInterface {
SELECTION_CHANGED, on_selection_changed SELECTION_CHANGED, on_selection_changed
); );
this.user_message_received.connect(this.on_message_received);
// Manage zoom level, ensure it's sane // Manage zoom level, ensure it's sane
config.bind(Application.Configuration.CONVERSATION_VIEWER_ZOOM_KEY, this, "zoom_level"); config.bind(Application.Configuration.CONVERSATION_VIEWER_ZOOM_KEY, this, "zoom_level");
if (this.zoom_level < ZOOM_MIN) { if (this.zoom_level < ZOOM_MIN) {
@ -803,6 +834,30 @@ public abstract class Components.WebView : WebKit.WebView, Geary.BaseInterface {
} }
} }
private bool on_message_received(WebKit.UserMessage message) {
if (message.name == MESSAGE_EXCEPTION_NAME) {
var detail = new GLib.VariantDict(message.parameters);
var name = detail.lookup_value("name", GLib.VariantType.STRING) as string;
var log_message = detail.lookup_value("message", GLib.VariantType.STRING) as string;
warning(
"Error sending message from JS: %s: %s",
name ?? "unknown",
log_message ?? "unknown"
);
} else if (this.message_handlers.has_key(message.name)) {
debug(
"Message received: %s(%s)",
message.name,
message.parameters != null ? message.parameters.print(true) : ""
);
MessageCallable callback = this.message_handlers.get(message.name);
callback.handler(message.parameters);
} else {
warning("Message with unknown handler received: %s", message.name);
}
return true;
}
} }
// XXX this needs to be moved into the libsoup bindings // XXX this needs to be moved into the libsoup bindings

View file

@ -38,6 +38,8 @@ public class GearyWebExtension : Object {
private const string[] ALLOWED_SCHEMES = { "cid", "geary", "data", "blob" }; private const string[] ALLOWED_SCHEMES = { "cid", "geary", "data", "blob" };
private const string EXTENSION_CLASS_VAR = "_GearyWebExtension";
private const string EXTENSION_CLASS_SEND = "send";
private const string REMOTE_LOAD_VAR = "_gearyAllowRemoteResourceLoads"; private const string REMOTE_LOAD_VAR = "_gearyAllowRemoteResourceLoads";
private WebKit.WebExtension extension; private WebKit.WebExtension extension;
@ -180,6 +182,25 @@ public class GearyWebExtension : Object {
WebKit.WebPage page) { WebKit.WebPage page) {
WebKit.Frame frame = page.get_main_frame(); WebKit.Frame frame = page.get_main_frame();
JSC.Context context = frame.get_js_context(); JSC.Context context = frame.get_js_context();
var extension_class = context.register_class(
this.get_type().name(),
null,
null,
null
);
extension_class.add_method(
EXTENSION_CLASS_SEND,
(instance, values) => {
return this.on_page_send_message(page, values);
},
GLib.Type.NONE
);
context.set_value(
EXTENSION_CLASS_VAR,
new JSC.Value.object(context, extension_class, extension_class)
);
context.set_value( context.set_value(
REMOTE_LOAD_VAR, REMOTE_LOAD_VAR,
new JSC.Value.boolean(context, false) new JSC.Value.boolean(context, false)
@ -259,4 +280,46 @@ public class GearyWebExtension : Object {
return true; return true;
} }
private bool on_page_send_message(WebKit.WebPage page,
GLib.GenericArray<JSC.Value> args) {
WebKit.UserMessage? message = null;
if (args.length > 0) {
var name = args.get(0).to_string();
GLib.Variant? parameters = null;
if (args.length > 1) {
JSC.Value param_value = args.get(1);
try {
int len = Util.JS.to_int32(
param_value.object_get_property("length")
);
if (len == 1) {
parameters = Util.JS.value_to_variant(
param_value.object_get_property_at_index(0)
);
} else if (len > 1) {
parameters = Util.JS.value_to_variant(param_value);
}
} catch (Util.JS.Error err) {
message = to_exception_message(
this.get_type().name(), err.message
);
}
}
if (message == null) {
message = new WebKit.UserMessage(name, parameters);
}
}
if (message == null) {
var log_message = "Not enough parameters for JS call to %s.%s()".printf(
EXTENSION_CLASS_VAR,
EXTENSION_CLASS_SEND
);
debug(log_message);
message = to_exception_message(this.get_type().name(), log_message);
}
page.send_message_to_view.begin(message, null);
return true;
}
} }

View file

@ -200,3 +200,12 @@ PageState.prototype = {
throw this.testResult; throw this.testResult;
} }
}; };
let MessageSender = function(name) {
return function() {
// Since typeof(arguments) == 'object', convert to an array so
// that Components.WebView.MessageCallback callbacks get
// arrays or tuples rather than dicts as arguments
_GearyWebExtension.send(name, Array.from(arguments));
};
};