[epiphany/pgriffis/web-extension/menus: 4/4] WebExtensions: Implement menus API




commit 38675bd699cba84751ea5db4659333aed821e212
Author: Patrick Griffis <pgriffis igalia com>
Date:   Sat Jun 11 21:09:02 2022 -0500

    WebExtensions: Implement menus API
    
    Part-of: <https://gitlab.gnome.org/GNOME/epiphany/-/merge_requests/1154>

 .../ephy-web-process-extension.c                   |  15 +-
 embed/web-process-extension/resources/js/ephy.js   |  11 +
 .../resources/js/webextensions.js                  |  10 +
 src/ephy-shell.c                                   |  10 +
 src/ephy-window.c                                  |   8 +-
 src/webextension/README.md                         |  22 +
 src/webextension/api/menus.c                       | 707 +++++++++++++++++++++
 src/webextension/api/menus.h                       |  43 ++
 src/webextension/ephy-json-utils.c                 |  30 +
 src/webextension/ephy-json-utils.h                 |   6 +
 src/webextension/ephy-web-extension-manager.c      |  49 ++
 src/webextension/ephy-web-extension-manager.h      |  11 +
 src/webextension/ephy-web-extension.c              |  47 +-
 src/webextension/ephy-web-extension.h              |  17 +-
 src/webextension/meson.build                       |   1 +
 15 files changed, 963 insertions(+), 24 deletions(-)
---
diff --git a/embed/web-process-extension/ephy-web-process-extension.c 
b/embed/web-process-extension/ephy-web-process-extension.c
index d0983555a..01afd379a 100644
--- a/embed/web-process-extension/ephy-web-process-extension.c
+++ b/embed/web-process-extension/ephy-web-process-extension.c
@@ -194,6 +194,8 @@ web_page_context_menu (WebKitWebPage          *web_page,
 {
   EphyWebProcessExtension *extension;
   g_autofree char *string = NULL;
+  gboolean is_editable;
+  gboolean is_password;
   GVariantBuilder builder;
   WebKitFrame *frame;
   g_autoptr (JSCContext) js_context = NULL;
@@ -209,12 +211,19 @@ web_page_context_menu (WebKitWebPage          *web_page,
   js_value = jsc_context_evaluate (js_context, "window.getSelection().toString();", -1);
   if (!jsc_value_is_null (js_value) && !jsc_value_is_undefined (js_value))
     string = jsc_value_to_string (js_value);
+  g_object_unref (js_value);
 
-  if (!string || *string == '\0')
-    return FALSE;
+  js_value = jsc_context_evaluate (js_context, "contextMenuElementIsEditable;", -1);
+  is_editable = jsc_value_to_boolean (js_value);
+  g_object_unref (js_value);
+
+  js_value = jsc_context_evaluate (js_context, "contextMenuElementIsPassword;", -1);
+  is_password = jsc_value_to_boolean (js_value);
 
   g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{sv}"));
-  g_variant_builder_add (&builder, "{sv}", "SelectedText", g_variant_new_string (g_strstrip (string)));
+  g_variant_builder_add (&builder, "{sv}", "SelectedText", g_variant_new_string (string ? g_strstrip 
(string) : ""));
+  g_variant_builder_add (&builder, "{sv}", "IsEditable", g_variant_new_boolean (is_editable));
+  g_variant_builder_add (&builder, "{sv}", "IsPassword", g_variant_new_boolean (is_password));
   webkit_context_menu_set_user_data (context_menu,
                                      g_variant_builder_end (&builder));
 
diff --git a/embed/web-process-extension/resources/js/ephy.js 
b/embed/web-process-extension/resources/js/ephy.js
index 38b806f7a..ce553dca4 100644
--- a/embed/web-process-extension/resources/js/ephy.js
+++ b/embed/web-process-extension/resources/js/ephy.js
@@ -1,5 +1,7 @@
 'use strict';
 
+/* exported contextMenuElementIsPassword, contextMenuElementIsEditable */
+
 // eslint-disable-next-line no-var
 var Ephy = {};
 
@@ -766,3 +768,12 @@ Ephy.FormManager = class FormManager
         return formAuth;
     }
 };
+
+let contextMenuElementIsEditable = false;
+let contextMenuElementIsPassword = false;
+
+window.document.addEventListener('contextmenu', (event) => {
+    // isContentEditable is always false, in practice this seems functional enough.
+    contextMenuElementIsEditable = event.target.tagName.toLowerCase() === 'input';
+    contextMenuElementIsPassword = event.target.type === 'password';
+});
\ No newline at end of file
diff --git a/embed/web-process-extension/resources/js/webextensions.js 
b/embed/web-process-extension/resources/js/webextensions.js
index 12ffe25cd..de6454ab6 100644
--- a/embed/web-process-extension/resources/js/webextensions.js
+++ b/embed/web-process-extension/resources/js/webextensions.js
@@ -155,3 +155,13 @@ window.browser.downloads = {
     onChanged: new EphyEventListener (),
 };
 
+window.browser.menus = {
+    create: function (...args) { return ephy_message ('menus.create', args); },
+    remove: function (...args) { return ephy_message ('menus.remove', args); },
+    removeAll: function (...args) { return ephy_message ('menus.removeAll', args); },
+    update: function (...args) { return ephy_message ('menus.update', args); },
+    onClicked: new EphyEventListener (),
+};
+
+// Chrome compat.
+window.browser.contextMenus = window.browser.menus;
diff --git a/src/ephy-shell.c b/src/ephy-shell.c
index 3c8c4464c..5d428ea20 100644
--- a/src/ephy-shell.c
+++ b/src/ephy-shell.c
@@ -361,6 +361,15 @@ webextension_action (GSimpleAction *action,
   ephy_web_extension_manager_handle_notifications_action (manager, parameter);
 }
 
+static void
+webextension_context_menu_action (GSimpleAction *action,
+                                  GVariant      *parameter,
+                                  gpointer       user_data)
+{
+  EphyWebExtensionManager *manager = ephy_web_extension_manager_get_default ();
+  ephy_web_extension_manager_handle_context_menu_action (manager, parameter);
+}
+
 static GActionEntry app_entries[] = {
   { "new-window", new_window, NULL, NULL, NULL },
   { "new-incognito", new_incognito_window, NULL, NULL, NULL },
@@ -378,6 +387,7 @@ static GActionEntry app_entries[] = {
   { "launch-app", launch_app, "s", NULL, NULL },
   { "notification-clicked", notification_clicked, "t", NULL, NULL },
   { "webextension-notification", webextension_action, "(ssi)", NULL, NULL },
+  { "webextension-context-menu", webextension_context_menu_action, "(sss)", NULL, NULL },
 };
 
 static GActionEntry non_incognito_extra_app_entries[] = {
diff --git a/src/ephy-window.c b/src/ephy-window.c
index 650adce80..c1521ffee 100644
--- a/src/ephy-window.c
+++ b/src/ephy-window.c
@@ -1490,6 +1490,7 @@ populate_context_menu (WebKitWebView       *web_view,
                        WebKitHitTestResult *hit_test_result,
                        EphyWindow          *window)
 {
+  EphyWebExtensionManager *extension_manager = ephy_web_extension_manager_get_default ();
   WebKitContextMenuItem *input_methods_item = NULL;
   WebKitContextMenuItem *insert_emoji_item = NULL;
   WebKitContextMenuItem *copy_image_item = NULL;
@@ -1565,7 +1566,7 @@ populate_context_menu (WebKitWebView       *web_view,
   }
 
   parse_context_menu_user_data (context_menu, &selected_text);
-  if (selected_text) {
+  if (selected_text && *selected_text) {
     if (g_uri_is_valid (selected_text, G_URI_FLAGS_NONE, NULL)) {
       GVariant *value;
 
@@ -1807,6 +1808,11 @@ populate_context_menu (WebKitWebView       *web_view,
                                 webkit_context_menu_item_new_from_stock_action 
(WEBKIT_CONTEXT_MENU_ACTION_INSPECT_ELEMENT));
   }
 
+  ephy_web_extension_manager_append_context_menu (extension_manager, web_view,
+                                                  context_menu, hit_test_result,
+                                                  event->type == GDK_BUTTON_PRESS ? ((GdkEventButton 
*)event)->state : 0,
+                                                  is_downloadable_audio, is_downloadable_video);
+
   return GDK_EVENT_PROPAGATE;
 }
 
diff --git a/src/webextension/README.md b/src/webextension/README.md
index 3bfc12a76..9918e8d4f 100644
--- a/src/webextension/README.md
+++ b/src/webextension/README.md
@@ -105,6 +105,28 @@ This is partially implemented. Message passing including replies works including
 - onMessage
 - lastError
 
+#### menus
+
+Notes:
+
+- `contextMenus` is aliased to this API and the same permissions work for both.
+
+Limitations:
+
+- Menus are only shown on web pages and not extension views, tabs, etc. (TODO)
+- Radio and Checkbox menus aren't properly supported yet. (TODO)
+- Commands are not yet supported. (TODO)
+- update() isn't supported yet. (TODO)
+- Frames are not properly handled.
+- Icons are not supported.
+
+APIs:
+
+- create()
+- remove()
+- removeAll()
+- onClicked
+
 #### notifications
 
 Limitations:
diff --git a/src/webextension/api/menus.c b/src/webextension/api/menus.c
new file mode 100644
index 000000000..eb0ef209c
--- /dev/null
+++ b/src/webextension/api/menus.c
@@ -0,0 +1,707 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ *  Copyright 2022 Igalia S.L.
+ *
+ *  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 "config.h"
+
+#include "ephy-json-utils.h"
+#include "ephy-web-view.h"
+#include "ephy-shell.h"
+
+#include "tabs.h"
+#include "menus.h"
+
+typedef enum {
+  MENU_CONTEXT_AUDIO = 1 << 0,
+  MENU_CONTEXT_BOOKMARK = 1 << 1,
+  MENU_CONTEXT_BROWSER_ACTION = 1 << 2,
+  MENU_CONTEXT_EDITABLE = 1 << 3,
+  MENU_CONTEXT_FRAME = 1 << 4,
+  MENU_CONTEXT_IMAGE = 1 << 5,
+  MENU_CONTEXT_LINK = 1 << 6,
+  MENU_CONTEXT_PAGE = 1 << 7,
+  MENU_CONTEXT_PASSWORD = 1 << 8,
+  MENU_CONTEXT_SELECTION = 1 << 9,
+  MENU_CONTEXT_TAB = 1 << 10,
+  MENU_CONTEXT_TOOLS_MENU = 1 << 11,
+  MENU_CONTEXT_VIDEO = 1 << 12,
+  MENU_CONTEXT_PAGE_ACTION = 1 << 13,
+} MenuContext;
+
+typedef enum {
+  MENU_TYPE_NORMAL,
+  MENU_TYPE_CHECKBOX,
+  MENU_TYPE_RADIO,
+  MENU_TYPE_SEPARATOR,
+} MenuType;
+
+typedef enum {
+  VIEW_TYPE_ANY = 0,
+  VIEW_TYPE_TAB = 1 << 0,
+  VIEW_TYPE_POPUP = 1 << 1,
+  VIEW_TYPE_SIDEBAR = 1 << 2,
+} ViewType;
+
+typedef enum {
+  COMMAND_NONE,
+  COMMAND_BROWSER_ACTION,
+  COMMAND_PAGE_ACTION,
+  COMMAND_SIDEBAR_ACTION,
+} Command;
+
+typedef struct {
+  char *id;
+  char *parent_id;
+  char *title;
+  GHashTable *children;
+  GStrv document_url_patterns;
+  GStrv target_url_patterns;
+  MenuType menu_type;
+  ViewType view_type;
+  Command command;
+  MenuContext contexts;
+  gboolean checked;
+  gboolean enabled;
+  gboolean visible;
+} MenuItem;
+
+static GStrv
+get_strv_property (JsonObject *object,
+                   const char *name)
+{
+  JsonNode *node = json_object_get_member (object, name);
+  JsonArray *json_array;
+  GPtrArray *array;
+  GStrv ret;
+
+  if (!node || json_node_get_node_type (node) != JSON_NODE_ARRAY)
+    return NULL;
+
+  json_array = json_node_get_array (node);
+  array = g_ptr_array_new ();
+
+  for (guint i = 0; i < json_array_get_length (json_array); i++) {
+    const char *string = ephy_json_array_get_string (json_array, i);
+    if (!string)
+      continue;
+
+    g_ptr_array_add (array, g_strdup (string));
+  }
+
+  if (array->len == 0) {
+    g_ptr_array_free (array, TRUE);
+    return NULL;
+  }
+
+  g_ptr_array_add (array, NULL);
+  ret = (GStrv)array->pdata;
+  g_ptr_array_free (array, FALSE);
+  return ret;
+}
+
+static Command
+get_command_property (JsonObject *object)
+{
+  JsonNode *node = json_object_get_member (object, "command");
+  const char *command;
+
+  command = ephy_json_node_to_string (node);
+  if (!command)
+    return COMMAND_NONE;
+  if (strcmp (command, "_execute_browser_action") == 0)
+    return COMMAND_BROWSER_ACTION;
+  if (strcmp (command, "_execute_page_action") == 0)
+    return COMMAND_PAGE_ACTION;
+  if (strcmp (command, "_execute_sidebar_action") == 0)
+    return COMMAND_SIDEBAR_ACTION;
+
+  return COMMAND_NONE;
+}
+
+static MenuType
+get_menu_type_property (JsonObject *object)
+{
+  JsonNode *node = json_object_get_member (object, "type");
+  const char *menu_type;
+
+  menu_type = ephy_json_node_to_string (node);
+  if (!menu_type)
+    return MENU_TYPE_NORMAL;
+  if (strcmp (menu_type, "normal") == 0)
+    return MENU_TYPE_NORMAL;
+  if (strcmp (menu_type, "checkbox") == 0)
+    return MENU_TYPE_CHECKBOX;
+  if (strcmp (menu_type, "radio") == 0)
+    return MENU_TYPE_RADIO;
+  if (strcmp (menu_type, "separator") == 0)
+    return MENU_TYPE_SEPARATOR;
+
+  return MENU_TYPE_NORMAL;
+}
+
+static ViewType
+get_view_type_property (JsonObject *object)
+{
+  JsonNode *node = json_object_get_member (object, "viewTypes");
+  JsonArray *view_type_array;
+  ViewType view_type_flags = 0;
+
+  if (!node || json_node_get_node_type (node) != JSON_NODE_ARRAY)
+    return VIEW_TYPE_ANY;
+
+  view_type_array = json_node_get_array (node);
+  for (guint i = 0; i < json_array_get_length (view_type_array); i++) {
+    const char *view_type = ephy_json_array_get_string (view_type_array, i);
+    if (!view_type)
+      continue;
+
+    if (strcmp (view_type, "tab") == 0)
+      view_type_flags |= VIEW_TYPE_TAB;
+    else if (strcmp (view_type, "popup") == 0)
+      view_type_flags |= VIEW_TYPE_POPUP;
+    else if (strcmp (view_type, "sidebar") == 0)
+      view_type_flags |= VIEW_TYPE_SIDEBAR;
+  }
+
+  if (view_type_flags == 0)
+    return VIEW_TYPE_ANY;
+
+  return view_type_flags;
+}
+
+typedef struct {
+  const char *name;
+  MenuContext value;
+} ContextMap;
+
+static const ContextMap context_map[] = {
+  { "all", MENU_CONTEXT_AUDIO | MENU_CONTEXT_BROWSER_ACTION | MENU_CONTEXT_EDITABLE | MENU_CONTEXT_FRAME |
+    MENU_CONTEXT_IMAGE | MENU_CONTEXT_LINK | MENU_CONTEXT_PAGE | MENU_CONTEXT_PASSWORD |
+    MENU_CONTEXT_SELECTION | MENU_CONTEXT_VIDEO | MENU_CONTEXT_PAGE_ACTION },
+  { "audio", MENU_CONTEXT_AUDIO },
+  { "bookmark", MENU_CONTEXT_BOOKMARK },
+  { "browser_action", MENU_CONTEXT_BROWSER_ACTION },
+  { "editable", MENU_CONTEXT_EDITABLE },
+  { "frame", MENU_CONTEXT_FRAME },
+  { "image", MENU_CONTEXT_IMAGE },
+  { "link", MENU_CONTEXT_LINK },
+  { "page", MENU_CONTEXT_PAGE },
+  { "password", MENU_CONTEXT_PASSWORD },
+  { "selection", MENU_CONTEXT_SELECTION },
+  { "tab", MENU_CONTEXT_TAB },
+  { "tools_menu", MENU_CONTEXT_TOOLS_MENU },
+  { "video", MENU_CONTEXT_VIDEO },
+  { "page_action", MENU_CONTEXT_PAGE_ACTION },
+};
+
+static MenuContext
+get_contexts_property (JsonObject *object)
+{
+  JsonNode *node = json_object_get_member (object, "contexts");
+  JsonArray *contexts_array;
+  MenuContext context_flags = 0;
+
+  if (!node || !JSON_NODE_HOLDS_ARRAY (node))
+    return MENU_CONTEXT_PAGE;
+
+  contexts_array = json_node_get_array (node);
+  for (guint i = 0; i < json_array_get_length (contexts_array); i++) {
+    const char *context = ephy_json_array_get_string (contexts_array, i);
+
+    if (!context)
+      continue;
+
+    for (guint j = 0; j < G_N_ELEMENTS (context_map); j++) {
+      if (strcmp (context, context_map[j].name) == 0) {
+        context_flags |= context_map[j].value;
+        break;
+      }
+    }
+  }
+
+  if (context_flags == 0)
+    return MENU_CONTEXT_PAGE;
+
+  return context_flags;
+}
+
+static void
+menu_item_free (MenuItem *item)
+{
+  if (item) {
+    g_hash_table_unref (item->children);
+    g_free (item->id);
+    g_free (item->parent_id);
+    g_free (item->title);
+    g_strfreev (item->document_url_patterns);
+    g_strfreev (item->target_url_patterns);
+    g_free (item);
+  }
+}
+
+static MenuItem *
+menu_item_new (JsonObject *object)
+{
+  MenuItem *item = g_new0 (MenuItem, 1);
+
+  item->id = g_strdup (ephy_json_object_get_string (object, "id"));
+  item->parent_id = g_strdup (ephy_json_object_get_string (object, "parentId"));
+  item->title = g_strdup (ephy_json_object_get_string (object, "title"));
+  item->command = get_command_property (object);
+  item->contexts = get_contexts_property (object);
+  item->menu_type = get_menu_type_property (object);
+  item->view_type = get_view_type_property (object);
+  item->document_url_patterns = get_strv_property (object, "documentUrlPatterns");
+  item->target_url_patterns = get_strv_property (object, "targetUrlPatterns");
+  item->checked = json_object_get_boolean_member_with_default (object, "checked", FALSE);
+  item->enabled = json_object_get_boolean_member_with_default (object, "enabled", TRUE);
+  item->visible = json_object_get_boolean_member_with_default (object, "visible", TRUE);
+  item->children = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, (GDestroyNotify)menu_item_free);
+
+  return item;
+}
+
+static gboolean
+insert_menu_item (GHashTable *menus,
+                  MenuItem   *item)
+{
+  MenuItem *parent;
+  MenuItem *next_item;
+  GHashTableIter iter;
+
+  if (!item->parent_id) {
+    g_hash_table_replace (menus, item->id, item);
+    return TRUE;
+  }
+
+  parent = g_hash_table_lookup (menus, item->parent_id);
+  if (parent) {
+    g_hash_table_replace (parent->children, item->id, item);
+    return TRUE;
+  }
+
+  g_hash_table_iter_init (&iter, menus);
+  while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&next_item)) {
+    if (insert_menu_item (next_item->children, item))
+      return TRUE;
+  }
+
+  return FALSE;
+}
+
+static GHashTable *
+get_menus (EphyWebExtension *extension)
+{
+  GHashTable *menus = g_object_get_data (G_OBJECT (extension), "menus");
+  if (menus)
+    return menus;
+
+  menus = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, (GDestroyNotify)menu_item_free);
+  g_object_set_data_full (G_OBJECT (extension), "menus", menus, (GDestroyNotify)g_hash_table_destroy);
+  return menus;
+}
+
+static void
+menus_handler_create (EphyWebExtension *self,
+                      char             *name,
+                      JsonArray        *args,
+                      WebKitWebView    *web_view,
+                      GTask            *task)
+{
+  JsonObject *create_properties = ephy_json_array_get_object (args, 0);
+  MenuItem *item;
+  GHashTable *menus = get_menus (self);
+
+  if (!create_properties) {
+    g_task_return_new_error (task, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_INVALID_ARGUMENT, 
"menus.create(): Missing createProperties");
+    return;
+  }
+
+  item = menu_item_new (create_properties);
+
+  if (!item->id || (!item->title && item->menu_type != MENU_TYPE_SEPARATOR)) {
+    g_task_return_new_error (task, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_INVALID_ARGUMENT, 
"menus.create(): createProperties is missing an id or title");
+    menu_item_free (item);
+    return;
+  }
+
+  if (!insert_menu_item (menus, item)) {
+    g_task_return_new_error (task, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_INVALID_ARGUMENT, 
"menus.create(): parentId not found");
+    menu_item_free (item);
+    return;
+  }
+
+  g_task_return_pointer (task, g_strdup_printf ("\"%s\"", item->id), g_free);
+}
+
+static gboolean
+menus_remove_by_id (GHashTable *menus,
+                    const char *menu_id)
+{
+  GHashTableIter iter;
+  MenuItem *next_item;
+
+  if (!menus)
+    return FALSE;
+
+  if (g_hash_table_remove (menus, menu_id))
+    return TRUE;
+
+  g_hash_table_iter_init (&iter, menus);
+  while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&next_item)) {
+    if (menus_remove_by_id (next_item->children, menu_id))
+      return TRUE;
+  }
+
+  return FALSE;
+}
+
+static void
+menus_handler_remove (EphyWebExtension *self,
+                      char             *name,
+                      JsonArray        *args,
+                      WebKitWebView    *web_view,
+                      GTask            *task)
+{
+  const char *menu_id = ephy_json_array_get_string (args, 0);
+
+  if (!menu_id) {
+    g_task_return_new_error (task, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_INVALID_ARGUMENT, 
"menus.remove(): Missing menuId");
+    return;
+  }
+
+  if (!menus_remove_by_id (get_menus (self), menu_id)) {
+    g_task_return_new_error (task, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_INVALID_ARGUMENT, 
"menus.remove(): Failed to find menuId '%s'", menu_id);
+    return;
+  }
+
+  g_task_return_pointer (task, NULL, NULL);
+}
+
+static void
+menus_handler_remove_all (EphyWebExtension *self,
+                          char             *name,
+                          JsonArray        *args,
+                          WebKitWebView    *web_view,
+                          GTask            *task)
+{
+  g_object_set_data (G_OBJECT (self), "menus", NULL);
+  g_task_return_pointer (task, NULL, NULL);
+}
+
+static char *
+format_label (const char *label,
+              const char *selected_text)
+{
+  GString *str = g_string_new (label);
+  /* FIXME: Handle & character. */
+  /* Documented here: 
https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/menus/create */
+  g_string_replace (str, "%s", selected_text, 1);
+  return g_string_free (str, FALSE);
+}
+
+static char *
+create_tabdata (EphyWebExtension *web_extension,
+                WebKitWebView    *web_view)
+{
+  g_autoptr (JsonNode) tab_node = NULL;
+
+  if (!EPHY_IS_WEB_VIEW (web_view))
+    return g_strdup ("undefined");
+
+  tab_node = ephy_web_extension_api_tabs_create_tab_object (web_extension, EPHY_WEB_VIEW (web_view));
+  return json_to_string (tab_node, FALSE);
+}
+
+static void
+parse_context_menu_user_data (WebKitContextMenu  *context_menu,
+                              const char        **selected_text,
+                              gboolean           *is_editable,
+                              gboolean           *is_password)
+{
+  GVariantDict dict;
+
+  g_variant_dict_init (&dict, webkit_context_menu_get_user_data (context_menu));
+  g_variant_dict_lookup (&dict, "SelectedText", "&s", selected_text);
+  g_variant_dict_lookup (&dict, "IsEditable", "b", is_editable);
+  g_variant_dict_lookup (&dict, "IsPassword", "b", is_password);
+}
+
+static void
+builder_add_modifier_array (JsonBuilder     *builder,
+                            GdkModifierType  state)
+{
+  json_builder_begin_array (builder);
+  if (state & GDK_CONTROL_MASK)
+    json_builder_add_string_value (builder, "Ctrl");
+  if (state & GDK_SHIFT_MASK)
+    json_builder_add_string_value (builder, "Shift");
+  if (state & GDK_MOD1_MASK)
+    json_builder_add_string_value (builder, "Alt");
+  json_builder_end_array (builder);
+}
+
+static char *
+create_onclickdata (EphyWebExtension    *web_extension,
+                    WebKitWebView       *web_view,
+                    WebKitContextMenu   *context_menu,
+                    GdkModifierType      modifiers,
+                    WebKitHitTestResult *hit_test_result,
+                    MenuItem            *menu_item,
+                    const char          *selected_text,
+                    gboolean             is_editable,
+                    gboolean             is_audio,
+                    gboolean             is_video)
+{
+  g_autoptr (JsonBuilder) builder = json_builder_new ();
+  g_autoptr (JsonNode) root = NULL;
+
+  json_builder_begin_object (builder);
+  json_builder_set_member_name (builder, "menuItemId");
+  json_builder_add_string_value (builder, menu_item->id);
+  if (menu_item->parent_id) {
+    json_builder_set_member_name (builder, "parentMenuItemId");
+    json_builder_add_string_value (builder, menu_item->parent_id);
+  }
+  if (selected_text) {
+    json_builder_set_member_name (builder, "selectionText");
+    json_builder_add_string_value (builder, selected_text);
+  }
+  json_builder_set_member_name (builder, "button");
+  json_builder_add_int_value (builder, GDK_BUTTON_SECONDARY);
+  json_builder_set_member_name (builder, "modifiers");
+  builder_add_modifier_array (builder, modifiers);
+  if (webkit_hit_test_result_context_is_link (hit_test_result)) {
+    json_builder_set_member_name (builder, "linkUrl");
+    json_builder_add_string_value (builder, webkit_hit_test_result_get_link_uri (hit_test_result));
+    if (webkit_hit_test_result_get_link_title (hit_test_result)) {
+      json_builder_set_member_name (builder, "linkTitle");
+      json_builder_add_string_value (builder, webkit_hit_test_result_get_link_title (hit_test_result));
+    }
+  }
+  if (webkit_hit_test_result_context_is_image (hit_test_result)) {
+    json_builder_set_member_name (builder, "mediaType");
+    json_builder_add_string_value (builder, "image");
+  }
+  if (is_audio || is_video) {
+    json_builder_set_member_name (builder, "mediaType");
+    json_builder_add_string_value (builder, is_audio ? "audio" : "video");
+  }
+  json_builder_set_member_name (builder, "editable");
+  json_builder_add_boolean_value (builder, is_editable);
+  json_builder_set_member_name (builder, "pageUrl");
+  json_builder_add_string_value (builder, webkit_web_view_get_uri (web_view));
+  json_builder_end_object (builder);
+
+  root = json_builder_get_root (builder);
+  return json_to_string (root, FALSE);
+}
+
+static gboolean
+item_applies_to_context (MenuContext          context,
+                         gboolean             is_audio,
+                         gboolean             is_video,
+                         gboolean             is_editable,
+                         gboolean             is_password,
+                         gboolean             has_selection,
+                         WebKitHitTestResult *hit_test_result)
+{
+  /* Only pages are currently supported. */
+  if ((context & MENU_CONTEXT_PAGE))
+    return TRUE;
+  if (is_password && (context & MENU_CONTEXT_PASSWORD))
+    return TRUE;
+  if (is_audio && (context & MENU_CONTEXT_AUDIO))
+    return TRUE;
+  if (is_editable && (context & MENU_CONTEXT_EDITABLE))
+    return TRUE;
+  if (webkit_hit_test_result_context_is_image (hit_test_result) && (context & MENU_CONTEXT_IMAGE))
+    return TRUE;
+  if (has_selection && (context & MENU_CONTEXT_SELECTION))
+    return TRUE;
+  if (webkit_hit_test_result_context_is_link (hit_test_result) && (context & MENU_CONTEXT_LINK))
+    return TRUE;
+
+  return FALSE;
+}
+
+static gboolean
+rules_match_uri (GStrv  rules,
+                 GUri  *uri)
+{
+  if (!rules || !*rules)
+    return TRUE;
+
+  if (!uri)
+    return FALSE;
+
+  for (guint i = 0; rules[i]; i++) {
+    if (ephy_web_extension_rule_matches_uri (rules[i], uri))
+      return TRUE;
+  }
+
+  return FALSE;
+}
+
+static WebKitContextMenuItem *
+create_context_menu_item (GHashTable          *menus,
+                          const char          *name,
+                          EphyWebExtension    *self,
+                          WebKitWebView       *web_view,
+                          GdkModifierType      modifiers,
+                          WebKitContextMenu   *context_menu,
+                          WebKitHitTestResult *hit_test_result,
+                          GAction             *action,
+                          gboolean             is_audio,
+                          gboolean             is_video,
+                          gboolean             is_editable,
+                          gboolean             is_password,
+                          const char          *selected_text,
+                          const char          *tab_data,
+                          GUri                *document_uri,
+                          GUri                *target_uri)
+{
+  GHashTableIter iter;
+  MenuItem *item;
+  GList *menu_items = NULL;
+
+  g_hash_table_iter_init (&iter, menus);
+  while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&item)) {
+    g_autofree char *label = NULL;
+    g_autofree char *onclickdata = NULL;
+    WebKitContextMenuItem *menu_item;
+
+    if (!item->visible)
+      continue;
+
+    if (!rules_match_uri (item->document_url_patterns, document_uri) ||
+        !rules_match_uri (item->target_url_patterns, target_uri))
+      continue;
+
+    if (!item_applies_to_context (item->contexts, is_audio, is_video,
+                                  is_editable, is_password,
+                                  selected_text && *selected_text,
+                                  hit_test_result))
+      continue;
+
+    if (item->view_type != VIEW_TYPE_ANY && !(item->view_type & VIEW_TYPE_TAB))
+      continue;
+
+    /* FIXME: We don't properly support checked and radio items but we still show them for now. */
+    if (item->menu_type == MENU_TYPE_SEPARATOR) {
+      menu_item = webkit_context_menu_item_new_separator ();
+    } else if (g_hash_table_size (item->children)) {
+      label = format_label (item->title, selected_text ? selected_text : "");
+      menu_item = create_context_menu_item (item->children, label, self, web_view, modifiers, context_menu, 
hit_test_result, action,
+                                            is_audio, is_video, is_editable, is_password, selected_text, 
tab_data,
+                                            document_uri, target_uri);
+
+      /* If the menus root is an item with the same name as the extension we use it as the root.
+       * otherwise we create a new root below. */
+      if (!item->parent_id && g_hash_table_size (menus) == 1 && strcmp (item->title, name) == 0) {
+        return menu_item;
+      }
+    } else {
+      label = format_label (item->title, selected_text ? selected_text : "");
+      onclickdata = create_onclickdata (self, web_view, context_menu, modifiers, hit_test_result,
+                                        item, selected_text, is_editable, is_audio, is_video);
+      menu_item = webkit_context_menu_item_new_from_gaction (action, label,
+                                                             g_variant_new ("(sss)",
+                                                                            ephy_web_extension_get_guid 
(self),
+                                                                            onclickdata,
+                                                                            tab_data));
+    }
+
+    menu_items = g_list_append (menu_items, menu_item);
+  }
+
+  return webkit_context_menu_item_new_with_submenu (name, webkit_context_menu_new_with_items (menu_items));
+}
+
+WebKitContextMenuItem *
+ephy_web_extension_api_menus_create_context_menu (EphyWebExtension    *self,
+                                                  WebKitWebView       *web_view,
+                                                  WebKitContextMenu   *context_menu,
+                                                  WebKitHitTestResult *hit_test_result,
+                                                  GdkModifierType      modifiers,
+                                                  gboolean             is_audio,
+                                                  gboolean             is_video)
+{
+  GHashTable *menus = g_object_get_data (G_OBJECT (self), "menus");
+  GAction *action;
+  g_autofree char *tab_data = NULL;
+  const char *selected_text;
+  gboolean is_editable;
+  gboolean is_password;
+  GUri *document_uri;
+  GUri *target_uri = NULL;
+
+  if (!menus)
+    return NULL;
+
+  parse_context_menu_user_data (context_menu, &selected_text, &is_editable, &is_password);
+  tab_data = create_tabdata (self, web_view);
+  action = g_action_map_lookup_action (G_ACTION_MAP (ephy_shell_get_default ()), 
"webextension-context-menu");
+  g_assert (action);
+
+  document_uri = g_uri_parse (webkit_web_view_get_uri (web_view), G_URI_FLAGS_ENCODED_PATH | 
G_URI_FLAGS_ENCODED_QUERY | G_URI_FLAGS_SCHEME_NORMALIZE, NULL);
+  if (webkit_hit_test_result_get_link_uri (hit_test_result))
+    target_uri = g_uri_parse (webkit_hit_test_result_get_link_uri (hit_test_result), 
G_URI_FLAGS_ENCODED_PATH | G_URI_FLAGS_ENCODED_QUERY | G_URI_FLAGS_SCHEME_NORMALIZE, NULL);
+
+  return create_context_menu_item (menus, ephy_web_extension_get_short_name (self), self, web_view, 
modifiers,
+                                   context_menu, hit_test_result, action, is_audio, is_video, is_editable,
+                                   is_password, selected_text, tab_data, document_uri, target_uri);
+}
+
+static EphyWebExtensionApiHandler menus_handlers[] = {
+  {"create", menus_handler_create},
+  {"remove", menus_handler_remove},
+  {"remove_all", menus_handler_remove_all},
+};
+
+void
+ephy_web_extension_api_menus_handler (EphyWebExtension *self,
+                                      char             *name,
+                                      JSCValue         *args,
+                                      WebKitWebView    *web_view,
+                                      GTask            *task)
+{
+  /* Temporary conversion as we migrate to json-glib here. */
+  g_autofree char *json = jsc_value_to_json (args, 0);
+  g_autoptr (JsonNode) json_node = json_from_string (json, NULL);
+  JsonArray *json_args = json_node_get_array (json_node);
+  json_array_seal (json_args);
+
+  /* We slightly differ from Firefox here that either permission works for either API but they are 
identical. */
+  if (!ephy_web_extension_has_permission (self, "menus") && !ephy_web_extension_has_permission (self, 
"contextMenus")) {
+    g_task_return_new_error (task, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_PERMISSION_DENIED, "Permission 
Denied");
+    return;
+  }
+
+  for (guint idx = 0; idx < G_N_ELEMENTS (menus_handlers); idx++) {
+    EphyWebExtensionApiHandler handler = menus_handlers[idx];
+
+    if (g_strcmp0 (handler.name, name) == 0) {
+      handler.execute (self, name, json_args, web_view, task);
+      return;
+    }
+  }
+
+  g_task_return_new_error (task, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_NOT_IMPLEMENTED, "Not 
Implemented");
+}
diff --git a/src/webextension/api/menus.h b/src/webextension/api/menus.h
new file mode 100644
index 000000000..419570f76
--- /dev/null
+++ b/src/webextension/api/menus.h
@@ -0,0 +1,43 @@
+/*
+ *  Copyright © 2022 Igalia S.L.
+ *
+ *  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 "ephy-web-extension.h"
+
+#include <json-glib/json-glib.h>
+#include <webkit2/webkit2.h>
+
+G_BEGIN_DECLS
+
+void         ephy_web_extension_api_menus_handler                    (EphyWebExtension *self,
+                                                                      char             *name,
+                                                                      JSCValue         *value,
+                                                                      WebKitWebView    *web_view,
+                                                                      GTask            *task);
+
+WebKitContextMenuItem  *ephy_web_extension_api_menus_create_context_menu        (EphyWebExtension    *self,
+                                                                                 WebKitWebView       
*web_view,
+                                                                                 WebKitContextMenu   
*context_menu,
+                                                                                 WebKitHitTestResult 
*hit_test_result,
+                                                                                 GdkModifierType      
modifiers,
+                                                                                 gboolean             
is_audio,
+                                                                                 gboolean             
is_video);
+
+G_END_DECLS
diff --git a/src/webextension/ephy-json-utils.c b/src/webextension/ephy-json-utils.c
index 68d6a3e3d..1f1cbccb8 100644
--- a/src/webextension/ephy-json-utils.c
+++ b/src/webextension/ephy-json-utils.c
@@ -196,3 +196,33 @@ ephy_json_node_to_string (JsonNode *node)
 
   return json_node_get_string (node);
 }
+
+/**
+ * ephy_json_array_get_string:
+ * @array: A #JsonArray
+ *
+ * This safely gets a string from an array.
+ *
+ * Returns: (transfer none): A string or %NULL if not a string.
+ */
+const char *
+ephy_json_array_get_string (JsonArray *array,
+                            guint      index)
+{
+  return ephy_json_node_to_string (json_array_get_element (array, index));
+}
+
+/**
+ * ephy_json_array_get_object:
+ * @array: A #JsonArray
+ *
+ * This safely gets an object from an array.
+ *
+ * Returns: (transfer none): An object or %NULL if not a object.
+ */
+JsonObject *
+ephy_json_array_get_object (JsonArray *array,
+                            guint      index)
+{
+  return ephy_json_node_get_object (json_array_get_element (array, index));
+}
diff --git a/src/webextension/ephy-json-utils.h b/src/webextension/ephy-json-utils.h
index 967978a1b..04c9f42f5 100644
--- a/src/webextension/ephy-json-utils.h
+++ b/src/webextension/ephy-json-utils.h
@@ -44,3 +44,9 @@ JsonObject   *ephy_json_object_get_object           (JsonObject          *object
 JsonObject   *ephy_json_node_get_object             (JsonNode            *node);
 
 const char   *ephy_json_node_to_string              (JsonNode            *node);
+
+const char   *ephy_json_array_get_string            (JsonArray           *array,
+                                                     guint                index);
+
+JsonObject   *ephy_json_array_get_object            (JsonArray           *array,
+                                                     guint                index);
diff --git a/src/webextension/ephy-web-extension-manager.c b/src/webextension/ephy-web-extension-manager.c
index dae10db98..1a9975ecc 100644
--- a/src/webextension/ephy-web-extension-manager.c
+++ b/src/webextension/ephy-web-extension-manager.c
@@ -38,6 +38,7 @@
 #include "api/alarms.h"
 #include "api/cookies.h"
 #include "api/downloads.h"
+#include "api/menus.h"
 #include "api/notifications.h"
 #include "api/pageaction.h"
 #include "api/runtime.h"
@@ -72,6 +73,7 @@ EphyWebExtensionAsyncApiHandler api_handlers[] = {
   {"alarms", ephy_web_extension_api_alarms_handler},
   {"cookies", ephy_web_extension_api_cookies_handler},
   {"downloads", ephy_web_extension_api_downloads_handler},
+  {"menus", ephy_web_extension_api_menus_handler},
   {"notifications", ephy_web_extension_api_notifications_handler},
   {"pageAction", ephy_web_extension_api_pageaction_handler},
   {"runtime", ephy_web_extension_api_runtime_handler},
@@ -468,6 +470,33 @@ ephy_web_extension_manager_set_background_web_view (EphyWebExtensionManager *sel
   g_hash_table_insert (self->background_web_views, web_extension, web_view);
 }
 
+void
+ephy_web_extension_manager_append_context_menu (EphyWebExtensionManager *self,
+                                                WebKitWebView           *web_view,
+                                                WebKitContextMenu       *context_menu,
+                                                WebKitHitTestResult     *hit_test_result,
+                                                GdkModifierType          modifiers,
+                                                gboolean                 is_audio,
+                                                gboolean                 is_video)
+{
+  gboolean inserted_separator = FALSE;
+
+  for (guint i = 0; i < self->web_extensions->len; i++) {
+    EphyWebExtension *extension = g_ptr_array_index (self->web_extensions, i);
+    WebKitContextMenuItem *item;
+
+    item = ephy_web_extension_api_menus_create_context_menu (extension, web_view, context_menu,
+                                                             hit_test_result, modifiers, is_audio, is_video);
+    if (item) {
+      if (!inserted_separator) {
+        webkit_context_menu_append (context_menu, webkit_context_menu_item_new_separator ());
+        inserted_separator = TRUE;
+      }
+      webkit_context_menu_append (context_menu, item);
+    }
+  }
+}
+
 static gboolean
 page_action_clicked (GtkWidget      *event_box,
                      GdkEventButton *event,
@@ -1359,6 +1388,26 @@ ephy_web_extension_manager_handle_notifications_action (EphyWebExtensionManager
   }
 }
 
+void
+ephy_web_extension_manager_handle_context_menu_action (EphyWebExtensionManager *self,
+                                                       GVariant                *params)
+{
+  EphyWebExtension *web_extension;
+  const char *extension_guid;
+  const char *onclickdata;
+  const char *tabdata;
+  g_autofree char *json = NULL;
+
+  g_variant_get (params, "(&s&s&s)", &extension_guid, &onclickdata, &tabdata);
+
+  web_extension = ephy_web_extension_manager_get_extension_by_guid (self, extension_guid);
+  if (!web_extension)
+    return;
+
+  json = g_strconcat (onclickdata, ", ", tabdata, NULL);
+  ephy_web_extension_manager_emit_in_extension_views (self, web_extension, "menus.onClicked", json);
+}
+
 static void
 handle_message_reply (EphyWebExtension *web_extension,
                       JSCValue         *args)
diff --git a/src/webextension/ephy-web-extension-manager.h b/src/webextension/ephy-web-extension-manager.h
index 92a88f5da..dc66a6991 100644
--- a/src/webextension/ephy-web-extension-manager.h
+++ b/src/webextension/ephy-web-extension-manager.h
@@ -74,6 +74,9 @@ WebKitWebView           *ephy_web_extension_manager_get_background_web_view
 void                     ephy_web_extension_manager_handle_notifications_action     (EphyWebExtensionManager 
*self,
                                                                                      GVariant                
*params);
 
+void                     ephy_web_extension_manager_handle_context_menu_action      (EphyWebExtensionManager 
*self,
+                                                                                     GVariant                
*params);
+
 void                     ephy_web_extension_manager_emit_in_extension_views         (EphyWebExtensionManager 
*self,
                                                                                      EphyWebExtension        
*web_extension,
                                                                                      const char              
*name,
@@ -108,4 +111,12 @@ void                      ephy_web_extension_manager_foreach_extension
                                                                                      
EphyWebExtensionForeachFunc  func,
                                                                                      gpointer                
     user_data);
 
+void                      ephy_web_extension_manager_append_context_menu            (EphyWebExtensionManager 
*self,
+                                                                                     WebKitWebView           
*web_view,
+                                                                                     WebKitContextMenu       
*context_menu,
+                                                                                     WebKitHitTestResult     
*hit_test_result,
+                                                                                     GdkModifierType         
 modifiers,
+                                                                                     gboolean                
 is_audio,
+                                                                                     gboolean                
 is_video);
+
 G_END_DECLS
diff --git a/src/webextension/ephy-web-extension.c b/src/webextension/ephy-web-extension.c
index da364676e..07ce43548 100644
--- a/src/webextension/ephy-web-extension.c
+++ b/src/webextension/ephy-web-extension.c
@@ -87,6 +87,7 @@ struct _EphyWebExtension {
   char *guid;
   char *author;
   char *name;
+  char *short_name;
   char *version;
   char *homepage_url;
   GList *icons;
@@ -306,6 +307,12 @@ ephy_web_extension_get_name (EphyWebExtension *self)
   return self->name;
 }
 
+const char *
+ephy_web_extension_get_short_name (EphyWebExtension *self)
+{
+  return self->short_name ? self->short_name : self->name;
+}
+
 const char *
 ephy_web_extension_get_version (EphyWebExtension *self)
 {
@@ -756,6 +763,7 @@ ephy_web_extension_dispose (GObject *object)
   g_clear_pointer (&self->description, g_free);
   g_clear_pointer (&self->author, g_free);
   g_clear_pointer (&self->name, g_free);
+  g_clear_pointer (&self->short_name, g_free);
   g_clear_pointer (&self->version, g_free);
   g_clear_pointer (&self->homepage_url, g_free);
   g_clear_pointer (&self->local_storage_path, g_free);
@@ -849,6 +857,7 @@ ephy_web_extension_parse_manifest (EphyWebExtension  *self,
 
   self->description = ephy_web_extension_manifest_get_localized_string (self, root_object, "description");
   self->name = ephy_web_extension_manifest_get_localized_string (self, root_object, "name");
+  self->short_name = ephy_web_extension_manifest_get_localized_string (self, root_object, "short_name");
   self->version = ephy_web_extension_manifest_get_localized_string (self, root_object, "version");
   self->homepage_url = ephy_web_extension_manifest_get_localized_string (self, root_object, "homepage_url");
   self->author = ephy_web_extension_manifest_get_localized_string (self, root_object, "author");
@@ -1421,44 +1430,44 @@ is_default_port (const char *scheme,
   return FALSE;
 }
 
-static gboolean
-permission_matches_uri (const char *permission,
-                        GUri       *uri)
+gboolean
+ephy_web_extension_rule_matches_uri (const char *rule,
+                                     GUri       *uri)
 {
-  g_autoptr (GUri) permission_uri = NULL;
+  g_autoptr (GUri) rule_uri = NULL;
   g_autoptr (GError) error = NULL;
-  g_autofree char *permission_path_and_query = NULL;
+  g_autofree char *rule_path_and_query = NULL;
   g_autofree char *uri_path_and_query = NULL;
-  const char *permission_scheme;
-  int permission_port;
+  const char *rule_scheme;
+  int rule_port;
 
-  permission_uri = parse_uri_with_wildcard_scheme (permission, &error);
+  rule_uri = parse_uri_with_wildcard_scheme (rule, &error);
   if (error) {
-    g_message ("Failed to parse permission '%s' as URI: %s", permission, error->message);
+    g_warning ("Failed to parse rule '%s' as URI: %s", rule, error->message);
     return FALSE;
   }
 
-  permission_scheme = g_uri_get_scheme (permission_uri);
-  permission_port = g_uri_get_port (permission_uri);
+  rule_scheme = g_uri_get_scheme (rule_uri);
+  rule_port = g_uri_get_port (rule_uri);
 
   /* Ports are forbidden, however GUri normalizes these to the default. */
-  if (permission_port != -1 && !is_default_port (permission_scheme, permission_port))
+  if (rule_port != -1 && !is_default_port (rule_scheme, rule_port))
     return FALSE;
 
   /* Empty paths are forbidden. */
-  if (strcmp (g_uri_get_path (permission_uri), "") == 0)
+  if (strcmp (g_uri_get_path (rule_uri), "") == 0)
     return FALSE;
 
-  if (!scheme_matches (permission_scheme, g_uri_get_scheme (uri)))
+  if (!scheme_matches (rule_scheme, g_uri_get_scheme (uri)))
     return FALSE;
 
-  if (!host_matches (g_uri_get_host (permission_uri), g_uri_get_host (uri)))
+  if (!host_matches (g_uri_get_host (rule_uri), g_uri_get_host (uri)))
     return FALSE;
 
-  permission_path_and_query = join_path_and_query (permission_uri);
+  rule_path_and_query = join_path_and_query (rule_uri);
   uri_path_and_query = join_path_and_query (uri);
 
-  if (!path_matches (permission_path_and_query, uri_path_and_query))
+  if (!path_matches (rule_path_and_query, uri_path_and_query))
     return FALSE;
 
   return TRUE;
@@ -1487,7 +1496,7 @@ ephy_web_extension_has_permission_internal (EphyWebExtension *self,
   g_assert (host); /* WebKitGTK shouldn't ever expose an invalid URI. */
   for (guint i = 0; i < self->host_permissions->len - 1; i++) {
     const char *permission = g_ptr_array_index (self->host_permissions, i);
-    if (permission_matches_uri (permission, host))
+    if (ephy_web_extension_rule_matches_uri (permission, host))
       return TRUE;
   }
 
@@ -1520,7 +1529,7 @@ ephy_web_extension_has_host_permission (EphyWebExtension *self,
 
   for (guint i = 0; i < self->host_permissions->len - 1; i++) {
     const char *permission = g_ptr_array_index (self->host_permissions, i);
-    if (permission_matches_uri (permission, uri))
+    if (ephy_web_extension_rule_matches_uri (permission, uri))
       return TRUE;
   }
 
diff --git a/src/webextension/ephy-web-extension.h b/src/webextension/ephy-web-extension.h
index 5155ef3b0..6339b1b29 100644
--- a/src/webextension/ephy-web-extension.h
+++ b/src/webextension/ephy-web-extension.h
@@ -47,6 +47,12 @@ typedef char *(*executeHandler)(EphyWebExtension  *web_extension,
                                 WebKitWebView     *web_view,
                                 GError           **error);
 
+typedef void (*EphyApiExecuteFunc)(EphyWebExtension *web_extension,
+                                   char             *name,
+                                   JsonArray        *args,
+                                   WebKitWebView    *web_view,
+                                   GTask            *task);
+
 
 extern GQuark web_extension_error_quark (void);
 #define WEB_EXTENSION_ERROR web_extension_error_quark ()
@@ -60,6 +66,11 @@ typedef enum {
   WEB_EXTENSION_ERROR_INVALID_HOST = 1006,
 } WebExtensionErrorCode;
 
+typedef struct {
+  char *name;
+  EphyApiExecuteFunc execute;
+} EphyWebExtensionApiHandler;
+
 typedef struct {
   char *name;
   executeTaskHandler execute;
@@ -75,6 +86,8 @@ GdkPixbuf             *ephy_web_extension_get_icon                        (EphyW
 
 const char            *ephy_web_extension_get_name                        (EphyWebExtension *self);
 
+const char            *ephy_web_extension_get_short_name                  (EphyWebExtension *self);
+
 const char            *ephy_web_extension_get_version                     (EphyWebExtension *self);
 
 const char            *ephy_web_extension_get_description                 (EphyWebExtension *self);
@@ -168,7 +181,9 @@ void                   ephy_web_extension_save_local_storage              (EphyW
 void                   ephy_web_extension_clear_local_storage             (EphyWebExtension *self);
 
 char                  *ephy_web_extension_create_sender_object            (EphyWebExtension *self,
-                                                                          WebKitWebView     *web_view);
+                                                                           WebKitWebView     *web_view);
+gboolean               ephy_web_extension_rule_matches_uri                (const char       *rule,
+                                                                           GUri             *uri);
 
 gboolean               ephy_web_extension_has_web_accessible_resource     (EphyWebExtension *self,
                                                                            const char       *path);
diff --git a/src/webextension/meson.build b/src/webextension/meson.build
index 0ed2fceaf..006ea40c3 100644
--- a/src/webextension/meson.build
+++ b/src/webextension/meson.build
@@ -3,6 +3,7 @@ ephywebextension_src = [
   'webextension/api/api-utils.c',
   'webextension/api/cookies.c',
   'webextension/api/downloads.c',
+  'webextension/api/menus.c',
   'webextension/api/notifications.c',
   'webextension/api/pageaction.c',
   'webextension/api/runtime.c',


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