[epiphany/pgriffis/web-extension-fixes-2] WebExtensions: Run content-scripts in the right world with WebExtension APIs available




commit ebe53366f69fd0ee96bbef177ff92332d33616b3
Author: Patrick Griffis <pgriffis igalia com>
Date:   Tue May 17 16:10:27 2022 -0500

    WebExtensions: Run content-scripts in the right world with WebExtension APIs available
    
    Fixes #1754

 .../ephy-web-process-extension.c                   |  63 +++++++++++
 .../web-process-extension/ephy-webextension-api.c  | 125 ++-------------------
 .../ephy-webextension-common.c                     | 124 ++++++++++++++++++++
 .../ephy-webextension-common.h                     |  30 +++++
 embed/web-process-extension/meson.build            |  10 +-
 .../resources/js/webextensions.js                  |  33 +++---
 src/webextension/ephy-web-extension-manager.c      |   9 +-
 src/webextension/ephy-web-extension.c              |   2 +-
 8 files changed, 251 insertions(+), 145 deletions(-)
---
diff --git a/embed/web-process-extension/ephy-web-process-extension.c 
b/embed/web-process-extension/ephy-web-process-extension.c
index f739c1236..fe216a589 100644
--- a/embed/web-process-extension/ephy-web-process-extension.c
+++ b/embed/web-process-extension/ephy-web-process-extension.c
@@ -29,6 +29,7 @@
 #include "ephy-settings.h"
 #include "ephy-uri-helpers.h"
 #include "ephy-web-overview-model.h"
+#include "ephy-webextension-common.h"
 
 #include <gio/gio.h>
 #include <glib/gi18n.h>
@@ -52,6 +53,7 @@ struct _EphyWebProcessExtension {
   EphyPermissionsManager *permissions_manager;
 
   WebKitScriptWorld *script_world;
+  GHashTable *content_script_worlds;
 
   gboolean should_remember_passwords;
   gboolean is_private_profile;
@@ -225,6 +227,58 @@ web_page_context_menu (WebKitWebPage          *web_page,
   return TRUE;
 }
 
+static void
+content_script_window_object_cleared_cb (WebKitScriptWorld *world,
+                                         WebKitWebPage     *page,
+                                         WebKitFrame       *frame,
+                                         gpointer           user_data)
+{
+  g_autoptr (JSCContext) js_context = NULL;
+
+  js_context = webkit_frame_get_js_context_for_script_world (frame, world);
+  ephy_webextension_install_common_apis (js_context, webkit_script_world_get_name (world));
+}
+
+static void
+create_content_script_world (EphyWebProcessExtension *extension,
+                             const char              *guid)
+{
+  WebKitScriptWorld *world = webkit_script_world_new_with_name (guid);
+
+  g_hash_table_insert (extension->content_script_worlds, g_strdup (guid), world);
+
+  g_signal_connect (world,
+                    "window-object-cleared",
+                    G_CALLBACK (content_script_window_object_cleared_cb),
+                    NULL);
+}
+
+static gboolean
+web_page_received_message (WebKitWebPage     *web_page,
+                           WebKitUserMessage *message,
+                           gpointer           user_data)
+{
+  EphyWebProcessExtension *extension = user_data;
+  const char *name = webkit_user_message_get_name (message);
+
+  if (g_strcmp0 (name, "WebExtension.Initialize") == 0) {
+    GVariant *parameters;
+    const char *guid;
+
+    parameters = webkit_user_message_get_parameters (message);
+    if (!parameters)
+      return FALSE;
+
+    g_variant_get (parameters, "&s", &guid);
+    create_content_script_world (extension, guid);
+  } else {
+    g_warning ("Unhandled page message: %s", name);
+    return FALSE;
+  }
+
+  return TRUE;
+}
+
 static void
 ephy_web_process_extension_page_created_cb (EphyWebProcessExtension *extension,
                                             WebKitWebPage           *web_page)
@@ -240,6 +294,9 @@ ephy_web_process_extension_page_created_cb (EphyWebProcessExtension *extension,
   g_signal_connect (web_page, "form-controls-associated-for-frame",
                     G_CALLBACK (web_page_form_controls_associated),
                     extension);
+  g_signal_connect (web_page, "user-message-received",
+                    G_CALLBACK (web_page_received_message),
+                    extension);
 }
 
 static void
@@ -330,6 +387,8 @@ ephy_web_process_extension_user_message_received_cb (EphyWebProcessExtension *ex
       return;
 
     g_variant_get (parameters, "b", &extension->should_remember_passwords);
+  } else {
+    g_warning ("Unhandled user-message: %s", name);
   }
 }
 
@@ -381,6 +440,7 @@ ephy_web_process_extension_dispose (GObject *object)
   }
 
   g_clear_pointer (&extension->translation_table, g_hash_table_destroy);
+  g_clear_pointer (&extension->content_script_worlds, g_hash_table_destroy);
 
   G_OBJECT_CLASS (ephy_web_process_extension_parent_class)->dispose (object);
 }
@@ -808,4 +868,7 @@ ephy_web_process_extension_initialize (EphyWebProcessExtension *extension,
                                                  g_free, NULL);
 
   extension->translation_table = g_hash_table_new (g_str_hash, NULL);
+
+  extension->content_script_worlds = g_hash_table_new_full (g_str_hash, g_str_equal,
+                                                            g_free, g_object_unref);
 }
diff --git a/embed/web-process-extension/ephy-webextension-api.c 
b/embed/web-process-extension/ephy-webextension-api.c
index 5cf5a6d8e..9499899f6 100644
--- a/embed/web-process-extension/ephy-webextension-api.c
+++ b/embed/web-process-extension/ephy-webextension-api.c
@@ -20,8 +20,8 @@
 
 #include "config.h"
 #include "ephy-webextension-api.h"
+#include "ephy-webextension-common.h"
 
-#include <locale.h>
 #include <json-glib/json-glib.h>
 #include <webkit2/webkit-web-extension.h>
 #include <JavaScriptCore/JavaScript.h>
@@ -49,53 +49,6 @@ ephy_web_extension_extension_get_translations (EphyWebExtensionExtension *extens
   return extension->translation_table;
 }
 
-static void
-js_exception_handler (JSCContext   *context,
-                      JSCException *exception)
-{
-  g_autoptr (JSCValue) js_console = NULL;
-  g_autoptr (JSCValue) js_result = NULL;
-  g_autofree char *report = NULL;
-
-  js_console = jsc_context_get_value (context, "console");
-  js_result = jsc_value_object_invoke_method (js_console, "error", JSC_TYPE_EXCEPTION, exception, 
G_TYPE_NONE);
-  (void)js_result;
-  report = jsc_exception_report (exception);
-  g_warning ("%s", report);
-
-  jsc_context_throw_exception (context, exception);
-}
-
-static char *
-js_getmessage (const char *message,
-               gpointer    user_data)
-{
-  return g_strdup (message);
-}
-
-static char *
-js_getuilanguage (void)
-{
-  char *locale = setlocale (LC_MESSAGES, NULL);
-
-  if (locale) {
-    locale[2] = '\0';
-
-    return g_strdup (locale);
-  }
-
-  return g_strdup ("en");
-}
-
-static char *
-js_geturl (const char *path,
-           gpointer    user_data)
-{
-  EphyWebExtensionExtension *extension = EPHY_WEB_EXTENSION_EXTENSION (user_data);
-
-  return g_strdup_printf ("ephy-webextension://%s/%s", extension->guid, path);
-}
-
 static void
 ephy_web_extension_page_user_message_received_cb (WebKitWebPage     *page,
                                                   WebKitUserMessage *message)
@@ -110,6 +63,7 @@ ephy_web_extension_page_user_message_received_cb (WebKitWebPage     *page,
     const char *path;
     const char *code;
     g_autofree char *uri = NULL;
+    /* FIXME: This should run in content-script world of the target tab. */
     JSCContext *context = webkit_frame_get_js_context (frame);
 
     parameters = webkit_user_message_get_parameters (message);
@@ -136,36 +90,6 @@ ephy_web_extension_page_user_message_received_cb (WebKitWebPage     *page,
   }
 }
 
-void
-webextensions_add_translation (EphyWebExtensionExtension *extension,
-                               const char                *name,
-                               const char                *data,
-                               guint64                    length)
-{
-  GHashTable *translations = ephy_web_extension_extension_get_translations (extension);
-  JsonParser *parser = NULL;
-  JsonNode *root;
-  JsonObject *root_object;
-  g_autoptr (GError) error = NULL;
-
-  g_hash_table_remove (translations, name);
-
-  if (!data || strlen (data) == 0)
-    return;
-
-  parser = json_parser_new ();
-  if (json_parser_load_from_data (parser, data, length, &error)) {
-    root = json_parser_get_root (parser);
-    g_assert (root);
-    root_object = json_node_get_object (root);
-    g_assert (root_object);
-
-    g_hash_table_insert (translations, (char *)name, json_object_ref (root_object));
-  } else {
-    g_warning ("Could not read translation json data: %s. '%s'", error->message, data);
-  }
-}
-
 static void
 ephy_web_extension_extension_page_created_cb (EphyWebExtensionExtension *extension,
                                               WebKitWebPage             *web_page)
@@ -238,46 +162,11 @@ window_object_cleared_cb (WebKitScriptWorld         *world,
   gsize data_size;
 
   js_context = webkit_frame_get_js_context_for_script_world (frame, world);
-  jsc_context_push_exception_handler (js_context, (JSCExceptionHandler)js_exception_handler, NULL, NULL);
-
-  result = jsc_context_get_value (js_context, "browser");
-  g_assert (jsc_value_is_undefined (result));
-
-  js_browser = jsc_value_new_object (js_context, NULL, NULL);
-  jsc_context_set_value (js_context, "browser", js_browser);
-
-  /* i18n */
-  js_i18n = jsc_value_new_object (js_context, NULL, NULL);
-  jsc_value_object_set_property (js_browser, "i18n", js_i18n);
-
-  js_function = jsc_value_new_function (js_context,
-                                        "getUILanguage",
-                                        G_CALLBACK (js_getuilanguage), extension, NULL,
-                                        G_TYPE_STRING,
-                                        0);
-  jsc_value_object_set_property (js_i18n, "getUILanguage", js_function);
-  g_clear_object (&js_function);
-
-  js_function = jsc_value_new_function (js_context,
-                                        "getMessage",
-                                        G_CALLBACK (js_getmessage), extension, NULL,
-                                        G_TYPE_STRING, 1,
-                                        G_TYPE_STRING);
-  jsc_value_object_set_property (js_i18n, "getMessage", js_function);
-  g_clear_object (&js_function);
-
-  /* extension */
-  js_extension = jsc_value_new_object (js_context, NULL, NULL);
-  jsc_value_object_set_property (js_browser, "extension", js_extension);
-
-  js_function = jsc_value_new_function (js_context,
-                                        "getURL",
-                                        G_CALLBACK (js_geturl), extension, NULL,
-                                        G_TYPE_STRING,
-                                        1,
-                                        G_TYPE_STRING);
-  jsc_value_object_set_property (js_extension, "getURL", js_function);
-  g_clear_object (&js_function);
+
+  ephy_webextension_install_common_apis (js_context, extension->guid);
+
+  js_browser = jsc_context_get_value (js_context, "browser");
+  g_assert (jsc_value_is_object (js_browser));
 
   bytes = g_resources_lookup_data ("/org/gnome/epiphany-web-extension/js/webextensions.js", 
G_RESOURCE_LOOKUP_FLAGS_NONE, NULL);
   data = g_bytes_get_data (bytes, &data_size);
diff --git a/embed/web-process-extension/ephy-webextension-common.c 
b/embed/web-process-extension/ephy-webextension-common.c
new file mode 100644
index 000000000..0854641bb
--- /dev/null
+++ b/embed/web-process-extension/ephy-webextension-common.c
@@ -0,0 +1,124 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ *  Copyright © 2020 Jan-Michael Brummer <jan brummer tabos org>
+ *
+ *  This file is part of Epiphany.
+ *
+ *  Epiphany is free software: you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation, either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  Epiphany is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with Epiphany.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "ephy-webextension-common.h"
+
+#include <locale.h>
+
+static char *
+js_getmessage (const char *message,
+               gpointer    user_data)
+{
+  return g_strdup (message);
+}
+
+static char *
+js_getuilanguage (void)
+{
+  char *locale = setlocale (LC_MESSAGES, NULL);
+
+  if (locale) {
+    locale[2] = '\0';
+
+    return g_strdup (locale);
+  }
+
+  return g_strdup ("en");
+}
+
+static char *
+js_geturl (const char *path,
+           gpointer    user_data)
+{
+  const char *guid = user_data;
+
+  return g_strdup_printf ("ephy-webextension://%s/%s", guid, path);
+}
+
+static void
+js_exception_handler (JSCContext   *context,
+                      JSCException *exception)
+{
+  g_autoptr (JSCValue) js_console = NULL;
+  g_autoptr (JSCValue) js_result = NULL;
+  g_autofree char *report = NULL;
+
+  js_console = jsc_context_get_value (context, "console");
+  js_result = jsc_value_object_invoke_method (js_console, "error", JSC_TYPE_EXCEPTION, exception, 
G_TYPE_NONE);
+  (void)js_result;
+  report = jsc_exception_report (exception);
+  g_warning ("%s", report);
+
+  jsc_context_throw_exception (context, exception);
+}
+
+void
+ephy_webextension_install_common_apis (JSCContext *js_context,
+                                       const char *guid)
+{
+  g_autoptr (JSCValue) result = NULL;
+  g_autoptr (JSCValue) js_browser = NULL;
+  g_autoptr (JSCValue) js_i18n = NULL;
+  g_autoptr (JSCValue) js_runtime = NULL;
+  g_autoptr (JSCValue) js_function = NULL;
+
+  jsc_context_push_exception_handler (js_context, (JSCExceptionHandler)js_exception_handler, NULL, NULL);
+
+  /* APIs available in content scripts: https://developer.chrome.com/docs/extensions/mv3/content_scripts/ */
+
+  result = jsc_context_get_value (js_context, "browser");
+  g_assert (jsc_value_is_undefined (result));
+
+  js_browser = jsc_value_new_object (js_context, NULL, NULL);
+  jsc_context_set_value (js_context, "browser", js_browser);
+
+  /* i18n */
+  js_i18n = jsc_value_new_object (js_context, NULL, NULL);
+  jsc_value_object_set_property (js_browser, "i18n", js_i18n);
+
+  js_function = jsc_value_new_function (js_context,
+                                        "getUILanguage",
+                                        G_CALLBACK (js_getuilanguage), NULL, NULL,
+                                        G_TYPE_STRING,
+                                        0);
+  jsc_value_object_set_property (js_i18n, "getUILanguage", js_function);
+  g_clear_object (&js_function);
+
+  js_function = jsc_value_new_function (js_context,
+                                        "getMessage",
+                                        G_CALLBACK (js_getmessage), NULL, NULL,
+                                        G_TYPE_STRING, 1,
+                                        G_TYPE_STRING);
+  jsc_value_object_set_property (js_i18n, "getMessage", js_function);
+  g_clear_object (&js_function);
+
+  /* runtime */
+  js_runtime = jsc_value_new_object (js_context, NULL, NULL);
+  jsc_value_object_set_property (js_browser, "runtime", js_runtime);
+
+  js_function = jsc_value_new_function (js_context,
+                                        "getURL",
+                                        G_CALLBACK (js_geturl), g_strdup (guid), g_free,
+                                        G_TYPE_STRING,
+                                        1,
+                                        G_TYPE_STRING);
+  jsc_value_object_set_property (js_runtime, "getURL", js_function);
+  g_clear_object (&js_function);
+}
diff --git a/embed/web-process-extension/ephy-webextension-common.h 
b/embed/web-process-extension/ephy-webextension-common.h
new file mode 100644
index 000000000..d16cebab6
--- /dev/null
+++ b/embed/web-process-extension/ephy-webextension-common.h
@@ -0,0 +1,30 @@
+ /* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ *  Copyright © 2020 Jan-Michael Brummer <jan brummer tabos org>
+ *
+ *  This file is part of Epiphany.
+ *
+ *  Epiphany is free software: you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation, either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  Epiphany is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with Epiphany.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <glib-object.h>
+#include <jsc/jsc.h>
+
+G_BEGIN_DECLS
+
+void ephy_webextension_install_common_apis (JSCContext *js_context, const char *guid);
+
+G_END_DECLS
diff --git a/embed/web-process-extension/meson.build b/embed/web-process-extension/meson.build
index d77fa2af0..0677a90c5 100644
--- a/embed/web-process-extension/meson.build
+++ b/embed/web-process-extension/meson.build
@@ -9,11 +9,16 @@ web_process_extension_resources = gnome.compile_resources('epiphany-web-process-
     source_dir: 'resources'
 )
 
+web_extension_common_sources = [
+  'ephy-webextension-common.c'
+]
+
 web_process_extension_sources = [
   'ephy-web-process-extension.c',
   'ephy-web-process-extension-main.c',
   'ephy-web-overview-model.c',
-  web_process_extension_resources
+  web_process_extension_resources,
+  web_extension_common_sources,
 ]
 
 web_process_extension_deps = [
@@ -44,7 +49,8 @@ web_extension_resources = gnome.compile_resources('epiphany-web-extension-resour
 
 web_extension_sources = [
   'ephy-webextension-api.c',
-  web_extension_resources
+  web_extension_resources,
+  web_extension_common_sources,
 ]
 
 web_extension_deps = [
diff --git a/embed/web-process-extension/resources/js/webextensions.js 
b/embed/web-process-extension/resources/js/webextensions.js
index 56a37d8df..e234494b8 100644
--- a/embed/web-process-extension/resources/js/webextensions.js
+++ b/embed/web-process-extension/resources/js/webextensions.js
@@ -74,28 +74,21 @@ window.browser.notifications = {
     create: function (args, cb) { return ephy_message ('notifications.create', args, cb); },
 };
 
-window.browser.runtime = {
-    getURL: function (args, cb) { return window.browser.extension.getURL(args, cb); },
-    getManifest: function (args, cb) { return '[]'; },
-    getBrowserInfo: function (args, cb) { return ephy_message ('runtime.getBrowserInfo', args, cb); },
-    onInstalled: {
-      addListener: function (cb) { runtime_listeners.push({callback: cb}); }
-    },
-    onMessage: {
-      addListener: function (cb) { runtime_onmessage_listeners.push({callback: cb}); }
-    },
-    onMessageExternal: {
-      addListener: function (cb) { runtime_onmessageexternal_listeners.push({callback: cb}); }
-    },
-    onConnect: {
-      addListener: function (cb) { runtime_onconnect_listeners.push({callback: cb}); }
-    },
-    connectNative: function (args, cb) { return ephy_message ('runtime.connectNative', args, cb); },
-    sendMessage: function (args, cb) { return ephy_message ('runtime.sendMessage', args, cb); },
-    openOptionsPage: function (args, cb) { return ephy_message ('runtime.openOptionsPage', args, cb); },
-    setUninstallURL: function (args, cb) { return ephy_message ('runtime.setUninstallURL', args, cb); },
+window.browser.extension = {
+  getURL: function (args, cb) { return window.browser.runtime.getURL(args, cb); },
 };
 
+window.browser.runtime.getManifest = function (args, cb) { return '[]'; };
+window.browser.runtime.getBrowserInfo = function (args, cb) { return ephy_message ('runtime.getBrowserInfo', 
args, cb); },
+window.browser.runtime.onInstalled = { addListener: function (cb) { runtime_listeners.push({callback: cb}); 
} };
+window.browser.runtime.onMessage = { addListener: function (cb) { 
runtime_onmessage_listeners.push({callback: cb}); } };
+window.browser.runtime.onMessageExternal = { addListener: function (cb) { 
runtime_onmessageexternal_listeners.push({callback: cb}); } };
+window.browser.runtime.onConnect = { addListener: function (cb) { 
runtime_onconnect_listeners.push({callback: cb}); } };
+window.browser.runtime.connectNative = function (args, cb) { return ephy_message ('runtime.connectNative', 
args, cb); },
+window.browser.runtime.sendMessage = function (args, cb) { return ephy_message ('runtime.sendMessage', args, 
cb); },
+window.browser.runtime.openOptionsPage = function (args, cb) { return ephy_message 
('runtime.openOptionsPage', args, cb); },
+window.browser.runtime.setUninstallURL = function (args, cb) { return ephy_message 
('runtime.setUninstallURL', args, cb); },
+
 window.browser.pageAction = {
     setIcon: function (args, cb) { return ephy_message ('pageAction.setIcon', args, cb); },
     setTitle: function (args, cb) { return ephy_message ('pageAction.setTitle', args, cb); },
diff --git a/src/webextension/ephy-web-extension-manager.c b/src/webextension/ephy-web-extension-manager.c
index 86056df5e..3c859b37f 100644
--- a/src/webextension/ephy-web-extension-manager.c
+++ b/src/webextension/ephy-web-extension-manager.c
@@ -461,8 +461,7 @@ add_content_scripts (EphyWebExtension *web_extension,
     return;
 
   ucm = webkit_web_view_get_user_content_manager (WEBKIT_WEB_VIEW (web_view));
-  g_signal_connect_object (ucm, "script-message-received", G_CALLBACK 
(ephy_web_extension_handle_background_script_message), web_extension, 0);
-  webkit_user_content_manager_register_script_message_handler (ucm, "epiphany");
+  /* NOTE: This will have to connect/disconnect script-message-recieved once we implement content-script 
APIs using this. */
 
   for (GList *list = content_scripts; list && list->data; list = list->next) {
     GList *js_list = ephy_web_extension_get_content_script_js (web_extension, list->data);
@@ -491,8 +490,6 @@ remove_content_scripts (EphyWebExtension *self,
     for (GList *tmp_list = js_list; tmp_list && tmp_list->data; tmp_list = tmp_list->next)
       webkit_user_content_manager_remove_script (WEBKIT_USER_CONTENT_MANAGER (ucm), tmp_list->data);
   }
-
-  g_signal_handlers_disconnect_by_func (ucm, G_CALLBACK 
(ephy_web_extension_handle_background_script_message), self);
 }
 
 static void
@@ -555,6 +552,10 @@ ephy_web_extension_manager_add_web_extension_to_webview (EphyWebExtensionManager
     }
   }
 
+  webkit_web_view_send_message_to_page (WEBKIT_WEB_VIEW (web_view),
+                                        webkit_user_message_new ("WebExtension.Initialize", 
g_variant_new_string (ephy_web_extension_get_guid (web_extension))),
+                                        NULL, NULL, NULL);
+
   update_translations (web_extension);
   add_content_scripts (web_extension, web_view);
 }
diff --git a/src/webextension/ephy-web-extension.c b/src/webextension/ephy-web-extension.c
index 6da0035c9..878de29b9 100644
--- a/src/webextension/ephy-web-extension.c
+++ b/src/webextension/ephy-web-extension.c
@@ -427,7 +427,7 @@ web_extension_content_script_build (EphyWebExtension          *self,
     user_script = webkit_user_script_new_for_world (js_data,
                                                     content_script->injected_frames,
                                                     content_script->injection_time,
-                                                    ephy_embed_shell_get_guid (ephy_embed_shell_get_default 
()),
+                                                    ephy_web_extension_get_guid (self),
                                                     (const char * const *)content_script->allow_list->pdata,
                                                     (const char * const *)content_script->block_list->pdata);
 


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