[evolution/gnome-41] Load JavaScript plugins for message preview and WebKit editor



commit c715e5af6d58a974e9e49a1a969772c5ebafa43d
Author: Milan Crha <mcrha redhat com>
Date:   Thu Nov 25 11:01:38 2021 +0100

    Load JavaScript plugins for message preview and WebKit editor
    
    This allows to save JavaScript plugins, files with .js extension,
    into `$PREFIX/share/evolution/webkit/preview-plugins/` or into
    `~/.local/share/evolution/preview-plugins/` for the message preview
    and into `$PREFIX/share/evolution/webkit/webkit-editor-plugins/` or
    into `~/.local/share/evolution/webkit-editor-plugins/` for
    the WebKit composer.
    
    The plugin is an object, which contains two members:
    * `name` - a string with a name of the plugin
    * `setup(doc)` - a setup function
    
    The argument of the `setup` function is a `document`, which can
    be either the main document or an iframe document. The function
    can be called multiple times for the same document.
    
    The preview plugin registers itself by calling `Evo.RegisterPlugin(plugin_instance)`.
    
    The WebKit editor plugin registers itself by calling `EvoEditor.RegisterPlugin(plugin_instance)`.

 data/webkit/e-editor.js                            |  42 ++++++
 data/webkit/e-web-view.js                          |  44 ++++++-
 .../web-extension/e-editor-web-extension.c         | 145 ++++++++++++++++-----
 src/web-extensions/e-web-extension.c               | 145 ++++++++++++++++-----
 4 files changed, 312 insertions(+), 64 deletions(-)
---
diff --git a/data/webkit/e-editor.js b/data/webkit/e-editor.js
index 5a3701519f..bf363edf7e 100644
--- a/data/webkit/e-editor.js
+++ b/data/webkit/e-editor.js
@@ -87,6 +87,7 @@ var EvoEditor = {
        MODE_PLAIN_TEXT : 0,
        MODE_HTML : 1,
 
+       plugins : null,
        mode : 1, // one of the MODE constants
        storedSelection : null,
        propertiesSelection : null, // dedicated to Properties dialogs
@@ -117,6 +118,45 @@ var EvoEditor = {
        }
 };
 
+EvoEditor.RegisterPlugin = function(plugin)
+{
+       if (plugin == null)
+               return;
+
+       if (plugin.name === undefined) {
+               console.error("Evo.RegisterPlugin: Plugin '" + plugin + "' has missing 'name' member");
+               return;
+       }
+
+       if (plugin.setup === undefined) {
+               console.error("Evo.RegisterPlugin: Plugin '" + plugin.name + "' has missing 'setup' 
function");
+               return;
+       }
+
+       if (EvoEditor.plugins == null)
+               EvoEditor.plugins = [];
+
+       EvoEditor.plugins.push(plugin);
+}
+
+EvoEditor.setupPlugins = function(doc)
+{
+       if (EvoEditor.plugins == null)
+               return;
+
+       var ii;
+
+       for (ii = 0; ii < EvoEditor.plugins.length; ii++) {
+               try {
+                       if (EvoEditor.plugins[ii] != null)
+                               EvoEditor.plugins[ii].setup(doc);
+               } catch (err) {
+                       console.error("Failed to setup plugin '" + EvoEditor.plugins[ii].name + "': " + 
err.name + ": " + err.message);
+                       EvoEditor.plugins[ii] = null;
+               }
+       }
+}
+
 EvoEditor.maybeUpdateFormattingState = function(force)
 {
        var anchorElem = null;
@@ -1778,6 +1818,8 @@ EvoEditor.initializeContent = function()
                        document.getSelection().setPosition(document.body.firstChild ? 
document.body.firstChild : document.body, 0);
                }
        }
+
+       EvoEditor.setupPlugins(document);
 }
 
 EvoEditor.getNextNodeInHierarchy = function(node, upToNode)
diff --git a/data/webkit/e-web-view.js b/data/webkit/e-web-view.js
index 2e87eaed1d..73fce72680 100644
--- a/data/webkit/e-web-view.js
+++ b/data/webkit/e-web-view.js
@@ -24,9 +24,49 @@ var Evo = {
        hasSelection : false,
        blockquoteStyle : "margin:0 0 0 .8ex; border-left:2px #729fcf solid;padding-left:1ex",
        magicSpacebarState: -1,
-       markCitationColor : null
+       markCitationColor : null,
+       plugins : null
 };
 
+Evo.RegisterPlugin = function(plugin)
+{
+       if (plugin == null)
+               return;
+
+       if (plugin.name === undefined) {
+               console.error("Evo.RegisterPlugin: Plugin '" + plugin + "' has missing 'name' member");
+               return;
+       }
+
+       if (plugin.setup === undefined) {
+               console.error("Evo.RegisterPlugin: Plugin '" + plugin.name + "' has missing 'setup' 
function");
+               return;
+       }
+
+       if (Evo.plugins == null)
+               Evo.plugins = [];
+
+       Evo.plugins.push(plugin);
+}
+
+Evo.setupPlugins = function(doc)
+{
+       if (Evo.plugins == null)
+               return;
+
+       var ii;
+
+       for (ii = 0; ii < Evo.plugins.length; ii++) {
+               try {
+                       if (Evo.plugins[ii] != null)
+                               Evo.plugins[ii].setup(doc);
+               } catch (err) {
+                       console.error("Failed to setup plugin '" + Evo.plugins[ii].name + "': " + err.name + 
": " + err.message);
+                       Evo.plugins[ii] = null;
+               }
+       }
+}
+
 /* The 'traversar_obj' is an object, which implements a callback function:
    boolean exec(doc, iframe_id, level);
    and it returns whether continue the traversar */
@@ -619,6 +659,8 @@ Evo.initialize = function(elem)
        } else
                doc = document;
 
+       Evo.setupPlugins(doc);
+
        elems = doc.getElementsByTagName("iframe");
 
        for (ii = 0; ii < elems.length; ii++) {
diff --git a/src/modules/webkit-editor/web-extension/e-editor-web-extension.c 
b/src/modules/webkit-editor/web-extension/e-editor-web-extension.c
index 5b6cfb9bc5..cf8fdf90cf 100644
--- a/src/modules/webkit-editor/web-extension/e-editor-web-extension.c
+++ b/src/modules/webkit-editor/web-extension/e-editor-web-extension.c
@@ -31,6 +31,7 @@
 struct _EEditorWebExtensionPrivate {
        WebKitWebExtension *wk_extension;
        ESpellChecker *spell_checker;
+       GSList *known_plugins; /* gchar * - full filename to known plugins */
 };
 
 G_DEFINE_TYPE_WITH_PRIVATE (EEditorWebExtension, e_editor_web_extension, G_TYPE_OBJECT)
@@ -43,6 +44,9 @@ e_editor_web_extension_dispose (GObject *object)
        g_clear_object (&extension->priv->wk_extension);
        g_clear_object (&extension->priv->spell_checker);
 
+       g_slist_free_full (extension->priv->known_plugins, g_free);
+       extension->priv->known_plugins = NULL;
+
        /* Chain up to parent's dispose() method. */
        G_OBJECT_CLASS (e_editor_web_extension_parent_class)->dispose (object);
 }
@@ -86,46 +90,26 @@ use_sources_js_file (void)
        return res;
 }
 
-static void
+static gboolean
 load_javascript_file (JSCContext *jsc_context,
-                     const gchar *js_filename)
+                     const gchar *js_filename,
+                     const gchar *filename)
 {
        JSCValue *result;
        JSCException *exception;
-       gchar *content, *filename = NULL, *resource_uri;
+       gchar *content, *resource_uri;
        gsize length = 0;
        GError *error = NULL;
+       gboolean success = TRUE;
 
-       g_return_if_fail (jsc_context != NULL);
-
-       if (use_sources_js_file ()) {
-               const gchar *source_webkitdatadir;
-
-               source_webkitdatadir = g_getenv ("EVOLUTION_SOURCE_WEBKITDATADIR");
-
-               if (source_webkitdatadir && *source_webkitdatadir) {
-                       filename = g_build_filename (source_webkitdatadir, js_filename, NULL);
-
-                       if (!g_file_test (filename, G_FILE_TEST_EXISTS)) {
-                               g_warning ("Cannot find '%s', using installed file '%s/%s' instead", 
filename, EVOLUTION_WEBKITDATADIR, js_filename);
-
-                               g_clear_pointer (&filename, g_free);
-                       }
-               } else {
-                       g_warning ("Environment variable 'EVOLUTION_SOURCE_WEBKITDATADIR' not set or invalid 
value, using installed file '%s/%s' instead", EVOLUTION_WEBKITDATADIR, js_filename);
-               }
-       }
-
-       if (!filename)
-               filename = g_build_filename (EVOLUTION_WEBKITDATADIR, js_filename, NULL);
+       g_return_val_if_fail (jsc_context != NULL, FALSE);
 
        if (!g_file_get_contents (filename, &content, &length, &error)) {
                g_warning ("Failed to load '%s': %s", filename, error ? error->message : "Unknown error");
 
                g_clear_error (&error);
-               g_free (filename);
 
-               return;
+               return FALSE;
        }
 
        resource_uri = g_strconcat ("resource:///", js_filename, NULL);
@@ -144,11 +128,89 @@ load_javascript_file (JSCContext *jsc_context,
                        jsc_exception_get_message (exception));
 
                jsc_context_clear_exception (jsc_context);
+               success = FALSE;
        }
 
        g_clear_object (&result);
-       g_free (filename);
        g_free (content);
+
+       return success;
+}
+
+static void
+load_javascript_plugins (JSCContext *jsc_context,
+                        const gchar *top_path,
+                        GSList **out_loaded_plugins)
+{
+       const gchar *dirfile;
+       gchar *path;
+       GDir *dir;
+
+       g_return_if_fail (jsc_context != NULL);
+
+       /* Do not load plugins during unit tests */
+       if (use_sources_js_file ())
+               return;
+
+       path = g_build_filename (top_path, "webkit-editor-plugins", NULL);
+
+       dir = g_dir_open (path, 0, NULL);
+       if (!dir) {
+               g_free (path);
+               return;
+       }
+
+       while (dirfile = g_dir_read_name (dir), dirfile) {
+               if (g_str_has_suffix (dirfile, ".js") ||
+                   g_str_has_suffix (dirfile, ".Js") ||
+                   g_str_has_suffix (dirfile, ".jS") ||
+                   g_str_has_suffix (dirfile, ".JS")) {
+                       gchar *filename;
+
+                       filename = g_build_filename (path, dirfile, NULL);
+                       if (load_javascript_file (jsc_context, filename, filename))
+                               *out_loaded_plugins = g_slist_prepend (*out_loaded_plugins, filename);
+                       else
+                               g_free (filename);
+               }
+       }
+
+       g_dir_close (dir);
+       g_free (path);
+}
+
+static void
+load_javascript_builtin_file (JSCContext *jsc_context,
+                             const gchar *js_filename)
+{
+       gchar *filename = NULL;
+
+       g_return_if_fail (jsc_context != NULL);
+
+       if (use_sources_js_file ()) {
+               const gchar *source_webkitdatadir;
+
+               source_webkitdatadir = g_getenv ("EVOLUTION_SOURCE_WEBKITDATADIR");
+
+               if (source_webkitdatadir && *source_webkitdatadir) {
+                       filename = g_build_filename (source_webkitdatadir, js_filename, NULL);
+
+                       if (!g_file_test (filename, G_FILE_TEST_EXISTS)) {
+                               g_warning ("Cannot find '%s', using installed file '%s/%s' instead", 
filename, EVOLUTION_WEBKITDATADIR, js_filename);
+
+                               g_clear_pointer (&filename, g_free);
+                       }
+               } else {
+                       g_warning ("Environment variable 'EVOLUTION_SOURCE_WEBKITDATADIR' not set or invalid 
value, using installed file '%s/%s' instead", EVOLUTION_WEBKITDATADIR, js_filename);
+               }
+       }
+
+       if (!filename)
+               filename = g_build_filename (EVOLUTION_WEBKITDATADIR, js_filename, NULL);
+
+       load_javascript_file (jsc_context, js_filename, filename);
+
+       g_free (filename);
 }
 
 static void
@@ -472,10 +534,10 @@ window_object_cleared_cb (WebKitScriptWorld *world,
        jsc_context = webkit_frame_get_js_context (frame);
 
        /* Read in order approximately as each other uses the previous */
-       load_javascript_file (jsc_context, "e-convert.js");
-       load_javascript_file (jsc_context, "e-selection.js");
-       load_javascript_file (jsc_context, "e-undo-redo.js");
-       load_javascript_file (jsc_context, "e-editor.js");
+       load_javascript_builtin_file (jsc_context, "e-convert.js");
+       load_javascript_builtin_file (jsc_context, "e-selection.js");
+       load_javascript_builtin_file (jsc_context, "e-undo-redo.js");
+       load_javascript_builtin_file (jsc_context, "e-editor.js");
 
        jsc_editor = jsc_context_get_value (jsc_context, "EvoEditor");
 
@@ -525,6 +587,25 @@ window_object_cleared_cb (WebKitScriptWorld *world,
                g_clear_object (&jsc_editor);
        }
 
+       if (extension->priv->known_plugins) {
+               GSList *link;
+
+               for (link = extension->priv->known_plugins; link; link = g_slist_next (link)) {
+                       const gchar *filename = link->data;
+
+                       if (filename)
+                               load_javascript_file (jsc_context, filename, filename);
+               }
+       } else {
+               load_javascript_plugins (jsc_context, EVOLUTION_WEBKITDATADIR, 
&extension->priv->known_plugins);
+               load_javascript_plugins (jsc_context, e_get_user_data_dir (), 
&extension->priv->known_plugins);
+
+               if (!extension->priv->known_plugins)
+                       extension->priv->known_plugins = g_slist_prepend (extension->priv->known_plugins, 
NULL);
+               else
+                       extension->priv->known_plugins = g_slist_reverse (extension->priv->known_plugins);
+       }
+
        g_clear_object (&jsc_context);
 }
 
diff --git a/src/web-extensions/e-web-extension.c b/src/web-extensions/e-web-extension.c
index fe0123b789..57ac47042e 100644
--- a/src/web-extensions/e-web-extension.c
+++ b/src/web-extensions/e-web-extension.c
@@ -34,6 +34,7 @@
 
 struct _EWebExtensionPrivate {
        WebKitWebExtension *wk_extension;
+       GSList *known_plugins; /* gchar * - full filename to known plugins */
 
        gboolean initialized;
 };
@@ -56,6 +57,9 @@ e_web_extension_dispose (GObject *object)
 
        g_clear_object (&extension->priv->wk_extension);
 
+       g_slist_free_full (extension->priv->known_plugins, g_free);
+       extension->priv->known_plugins = NULL;
+
        G_OBJECT_CLASS (e_web_extension_parent_class)->dispose (object);
 }
 
@@ -159,46 +163,26 @@ use_sources_js_file (void)
        return res;
 }
 
-static void
+static gboolean
 load_javascript_file (JSCContext *jsc_context,
-                     const gchar *js_filename)
+                     const gchar *js_filename,
+                     const gchar *filename)
 {
        JSCValue *result;
        JSCException *exception;
-       gchar *content, *filename = NULL, *resource_uri;
+       gchar *content, *resource_uri;
        gsize length = 0;
        GError *error = NULL;
+       gboolean success = TRUE;
 
-       g_return_if_fail (jsc_context != NULL);
-
-       if (use_sources_js_file ()) {
-               const gchar *source_webkitdatadir;
-
-               source_webkitdatadir = g_getenv ("EVOLUTION_SOURCE_WEBKITDATADIR");
-
-               if (source_webkitdatadir && *source_webkitdatadir) {
-                       filename = g_build_filename (source_webkitdatadir, js_filename, NULL);
-
-                       if (!g_file_test (filename, G_FILE_TEST_EXISTS)) {
-                               g_warning ("Cannot find '%s', using installed file '%s/%s' instead", 
filename, EVOLUTION_WEBKITDATADIR, js_filename);
-
-                               g_clear_pointer (&filename, g_free);
-                       }
-               } else {
-                       g_warning ("Environment variable 'EVOLUTION_SOURCE_WEBKITDATADIR' not set or invalid 
value, using installed file '%s/%s' instead", EVOLUTION_WEBKITDATADIR, js_filename);
-               }
-       }
-
-       if (!filename)
-               filename = g_build_filename (EVOLUTION_WEBKITDATADIR, js_filename, NULL);
+       g_return_val_if_fail (jsc_context != NULL, FALSE);
 
        if (!g_file_get_contents (filename, &content, &length, &error)) {
                g_warning ("Failed to load '%s': %s", filename, error ? error->message : "Unknown error");
 
                g_clear_error (&error);
-               g_free (filename);
 
-               return;
+               return FALSE;
        }
 
        resource_uri = g_strconcat ("resource:///", js_filename, NULL);
@@ -217,11 +201,89 @@ load_javascript_file (JSCContext *jsc_context,
                        jsc_exception_get_message (exception));
 
                jsc_context_clear_exception (jsc_context);
+               success = FALSE;
        }
 
        g_clear_object (&result);
-       g_free (filename);
        g_free (content);
+
+       return success;
+}
+
+static void
+load_javascript_plugins (JSCContext *jsc_context,
+                        const gchar *top_path,
+                        GSList **out_loaded_plugins)
+{
+       const gchar *dirfile;
+       gchar *path;
+       GDir *dir;
+
+       g_return_if_fail (jsc_context != NULL);
+
+       /* Do not load plugins during unit tests */
+       if (use_sources_js_file ())
+               return;
+
+       path = g_build_filename (top_path, "preview-plugins", NULL);
+
+       dir = g_dir_open (path, 0, NULL);
+       if (!dir) {
+               g_free (path);
+               return;
+       }
+
+       while (dirfile = g_dir_read_name (dir), dirfile) {
+               if (g_str_has_suffix (dirfile, ".js") ||
+                   g_str_has_suffix (dirfile, ".Js") ||
+                   g_str_has_suffix (dirfile, ".jS") ||
+                   g_str_has_suffix (dirfile, ".JS")) {
+                       gchar *filename;
+
+                       filename = g_build_filename (path, dirfile, NULL);
+                       if (load_javascript_file (jsc_context, filename, filename))
+                               *out_loaded_plugins = g_slist_prepend (*out_loaded_plugins, filename);
+                       else
+                               g_free (filename);
+               }
+       }
+
+       g_dir_close (dir);
+       g_free (path);
+}
+
+static void
+load_javascript_builtin_file (JSCContext *jsc_context,
+                             const gchar *js_filename)
+{
+       gchar *filename = NULL;
+
+       g_return_if_fail (jsc_context != NULL);
+
+       if (use_sources_js_file ()) {
+               const gchar *source_webkitdatadir;
+
+               source_webkitdatadir = g_getenv ("EVOLUTION_SOURCE_WEBKITDATADIR");
+
+               if (source_webkitdatadir && *source_webkitdatadir) {
+                       filename = g_build_filename (source_webkitdatadir, js_filename, NULL);
+
+                       if (!g_file_test (filename, G_FILE_TEST_EXISTS)) {
+                               g_warning ("Cannot find '%s', using installed file '%s/%s' instead", 
filename, EVOLUTION_WEBKITDATADIR, js_filename);
+
+                               g_clear_pointer (&filename, g_free);
+                       }
+               } else {
+                       g_warning ("Environment variable 'EVOLUTION_SOURCE_WEBKITDATADIR' not set or invalid 
value, using installed file '%s/%s' instead", EVOLUTION_WEBKITDATADIR, js_filename);
+               }
+       }
+
+       if (!filename)
+               filename = g_build_filename (EVOLUTION_WEBKITDATADIR, js_filename, NULL);
+
+       load_javascript_file (jsc_context, js_filename, filename);
+
+       g_free (filename);
 }
 
 static void
@@ -230,6 +292,7 @@ window_object_cleared_cb (WebKitScriptWorld *world,
                          WebKitFrame *frame,
                          gpointer user_data)
 {
+       EWebExtension *extension = user_data;
        JSCContext *jsc_context;
        JSCValue *jsc_evo_object;
 
@@ -240,8 +303,8 @@ window_object_cleared_cb (WebKitScriptWorld *world,
        jsc_context = webkit_frame_get_js_context (frame);
 
        /* Read e-convert.js first, because e-web-view.js uses it */
-       load_javascript_file (jsc_context, "e-convert.js");
-       load_javascript_file (jsc_context, "e-web-view.js");
+       load_javascript_builtin_file (jsc_context, "e-convert.js");
+       load_javascript_builtin_file (jsc_context, "e-web-view.js");
 
        jsc_evo_object = jsc_context_get_value (jsc_context, "Evo");
 
@@ -262,6 +325,26 @@ window_object_cleared_cb (WebKitScriptWorld *world,
        }
 
        g_clear_object (&jsc_evo_object);
+
+       if (extension->priv->known_plugins) {
+               GSList *link;
+
+               for (link = extension->priv->known_plugins; link; link = g_slist_next (link)) {
+                       const gchar *filename = link->data;
+
+                       if (filename)
+                               load_javascript_file (jsc_context, filename, filename);
+               }
+       } else {
+               load_javascript_plugins (jsc_context, EVOLUTION_WEBKITDATADIR, 
&extension->priv->known_plugins);
+               load_javascript_plugins (jsc_context, e_get_user_data_dir (), 
&extension->priv->known_plugins);
+
+               if (!extension->priv->known_plugins)
+                       extension->priv->known_plugins = g_slist_prepend (extension->priv->known_plugins, 
NULL);
+               else
+                       extension->priv->known_plugins = g_slist_reverse (extension->priv->known_plugins);
+       }
+
        g_clear_object (&jsc_context);
 }
 
@@ -287,7 +370,7 @@ e_web_extension_initialize (EWebExtension *extension,
        script_world = webkit_script_world_get_default ();
 
        g_signal_connect (script_world, "window-object-cleared",
-               G_CALLBACK (window_object_cleared_cb), NULL);
+               G_CALLBACK (window_object_cleared_cb), extension);
 }
 
 WebKitWebExtension *


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]