From 6162785d997fcfa4efaf6ec83670b2fab8cca6bd Mon Sep 17 00:00:00 2001
From: Michael Gratton <mike@vee.net>
Date: Fri, 28 Aug 2020 09:49:46 +1000
Subject: [PATCH 073/124] 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.
---
 .../components/components-web-view.vala       | 55 ++++++++++++++++
 .../web-process/web-process-extension.vala    | 63 +++++++++++++++++++
 ui/components-web-view.js                     |  9 +++
 3 files changed, 127 insertions(+)

diff --git a/src/client/components/components-web-view.vala b/src/client/components/components-web-view.vala
index 2b373170..c746c441 100644
--- a/src/client/components/components-web-view.vala
+++ b/src/client/components/components-web-view.vala
@@ -198,6 +198,24 @@ public abstract class Components.WebView : WebKit.WebView, Geary.BaseInterface {
     /** Delegate for UserContentManager message callbacks. */
     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.
      *
@@ -263,6 +281,8 @@ public abstract class Components.WebView : WebKit.WebView, Geary.BaseInterface {
 
     private Gee.List<ulong> registered_message_handlers =
         new Gee.LinkedList<ulong>();
+    private Gee.Map<string,MessageCallable> message_handlers =
+        new Gee.HashMap<string,MessageCallable>();
 
     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.registered_message_handlers.clear();
+        this.message_handlers.clear();
         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) {
         // 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
         );
 
+        this.user_message_received.connect(this.on_message_received);
+
         // Manage zoom level, ensure it's sane
         config.bind(Application.Configuration.CONVERSATION_VIEWER_ZOOM_KEY, this, "zoom_level");
         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
diff --git a/src/client/web-process/web-process-extension.vala b/src/client/web-process/web-process-extension.vala
index 89d9a1e3..31f2b0f0 100644
--- a/src/client/web-process/web-process-extension.vala
+++ b/src/client/web-process/web-process-extension.vala
@@ -38,6 +38,8 @@ public class GearyWebExtension : Object {
 
     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 WebKit.WebExtension extension;
@@ -180,6 +182,25 @@ public class GearyWebExtension : Object {
                                  WebKit.WebPage page) {
         WebKit.Frame frame = page.get_main_frame();
         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(
             REMOTE_LOAD_VAR,
             new JSC.Value.boolean(context, false)
@@ -259,4 +280,46 @@ public class GearyWebExtension : Object {
         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;
+    }
+
 }
diff --git a/ui/components-web-view.js b/ui/components-web-view.js
index 0f932a19..35e82dfc 100644
--- a/ui/components-web-view.js
+++ b/ui/components-web-view.js
@@ -200,3 +200,12 @@ PageState.prototype = {
         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));
+    };
+};
-- 
2.29.2

