[epiphany/pgriffis/web-extension/cookies: 3/3] WebExtensions: Implement cookies API



commit 4951b8467640cf2c3966051c2e22c1817e5262cc
Author: Patrick Griffis <pgriffis igalia com>
Date:   Sat Jun 11 12:21:21 2022 -0500

    WebExtensions: Implement cookies API
    
    This implements nearly all of the cookies API with two exceptions:
    
    - WebKitCookiesManager::changed doesn't give us enough information to
      implement cookies.onChanged.
    - WebKitCookiesManager doesn't expose the ability easily list all cookies
      for every URL.
    
    Part-of: <https://gitlab.gnome.org/GNOME/epiphany/-/merge_requests/1142>

 .../resources/js/webextensions.js                  |  12 +-
 src/webextension/api/cookies.c                     | 620 +++++++++++++++++++++
 src/webextension/api/cookies.h                     |  35 ++
 src/webextension/ephy-web-extension-manager.c      |   2 +
 src/webextension/ephy-web-extension.c              |  56 +-
 src/webextension/ephy-web-extension.h              |   3 +
 src/webextension/meson.build                       |   1 +
 7 files changed, 717 insertions(+), 12 deletions(-)
---
diff --git a/embed/web-process-extension/resources/js/webextensions.js 
b/embed/web-process-extension/resources/js/webextensions.js
index 6d67ec647..0d3f738ce 100644
--- a/embed/web-process-extension/resources/js/webextensions.js
+++ b/embed/web-process-extension/resources/js/webextensions.js
@@ -125,4 +125,14 @@ window.browser.windows = {
     onCreated: new EphyEventListener (),
     onRemoved: new EphyEventListener (),
     onFocusChanged: new EphyEventListener (),
-};
\ No newline at end of file
+};
+
+window.browser.cookies = {
+    get: function (...args) { return ephy_message ('cookies.get', args); },
+    getAll: function (...args) { return ephy_message ('cookies.getAll', args); },
+    getAllCookieStores: function (...args) { return ephy_message ('cookies.getAllCookieStores', args); },
+    remove: function (...args) { return ephy_message ('cookies.remove', args); },
+    set: function (...args) { return ephy_message ('cookies.set', args); },
+    // This is a stub as WebKitCookieManager::changed doesn't tell us enough information.
+    onChanged: new EphyEventListener (),
+};
diff --git a/src/webextension/api/cookies.c b/src/webextension/api/cookies.c
new file mode 100644
index 000000000..b62d57a22
--- /dev/null
+++ b/src/webextension/api/cookies.c
@@ -0,0 +1,620 @@
+/*
+ *  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-shell.h"
+#include "cookies.h"
+
+static WebKitCookieManager *
+get_cookie_manager (void)
+{
+  WebKitWebContext *web_context = ephy_embed_shell_get_web_context (ephy_embed_shell_get_default ());
+  WebKitWebsiteDataManager *data_manager = webkit_web_context_get_website_data_manager (web_context);
+  return webkit_website_data_manager_get_cookie_manager (data_manager);
+}
+
+static char *
+get_string_property (JSCValue   *obj,
+                     const char *name)
+{
+  g_autoptr (JSCValue) value = jsc_value_object_get_property (obj, name);
+  if (!jsc_value_is_string (value))
+    return NULL;
+  return jsc_value_to_string (value);
+}
+
+static gint32
+get_int_property (JSCValue   *args,
+                  const char *name)
+{
+  g_autoptr (JSCValue) value = jsc_value_object_get_property (args, name);
+
+  if (jsc_value_is_undefined (value))
+    return -1;
+
+  return jsc_value_to_int32 (value);
+}
+
+static gboolean
+get_boolean_property (JSCValue   *obj,
+                      const char *name)
+{
+  g_autoptr (JSCValue) value = jsc_value_object_get_property (obj, name);
+  return jsc_value_to_boolean (value);
+}
+
+
+static const char *
+samesite_to_string (SoupSameSitePolicy policy)
+{
+  switch (policy) {
+    case SOUP_SAME_SITE_POLICY_NONE:
+      return "no_restriction";
+    case SOUP_SAME_SITE_POLICY_LAX:
+      return "lax";
+    case SOUP_SAME_SITE_POLICY_STRICT:
+      return "strict";
+  }
+
+  g_assert_not_reached ();
+  return "no_restriction";
+}
+
+static SoupSameSitePolicy
+string_to_samesite (const char *policy)
+{
+  if (g_strcmp0 (policy, "strict") == 0)
+    return SOUP_SAME_SITE_POLICY_STRICT;
+  if (g_strcmp0 (policy, "lax") == 0)
+    return SOUP_SAME_SITE_POLICY_LAX;
+  return SOUP_SAME_SITE_POLICY_NONE;
+}
+
+static void
+add_cookie_to_json (JsonBuilder *builder,
+                    SoupCookie  *cookie)
+{
+#if SOUP_CHECK_VERSION (2, 99, 4)
+  GDateTime *expires = soup_cookie_get_expires (cookie);
+#else
+  SoupDate *expires = soup_cookie_get_expires (cookie);
+#endif
+
+  json_builder_begin_object (builder);
+  json_builder_set_member_name (builder, "name");
+  json_builder_add_string_value (builder, soup_cookie_get_name (cookie));
+  json_builder_set_member_name (builder, "value");
+  json_builder_add_string_value (builder, soup_cookie_get_value (cookie));
+  json_builder_set_member_name (builder, "domain");
+  json_builder_add_string_value (builder, soup_cookie_get_domain (cookie));
+  json_builder_set_member_name (builder, "path");
+  json_builder_add_string_value (builder, soup_cookie_get_path (cookie));
+  json_builder_set_member_name (builder, "httpOnly");
+  json_builder_add_boolean_value (builder, soup_cookie_get_http_only (cookie));
+  json_builder_set_member_name (builder, "secure");
+  json_builder_add_boolean_value (builder, soup_cookie_get_secure (cookie));
+  json_builder_set_member_name (builder, "sameSite");
+  json_builder_add_string_value (builder, samesite_to_string (soup_cookie_get_same_site_policy (cookie)));
+  if (expires) {
+    json_builder_set_member_name (builder, "expirationDate");
+#if SOUP_CHECK_VERSION (2, 99, 4)
+    json_builder_add_int_value (builder, g_date_time_to_unix (expires));
+#else
+    json_builder_add_int_value (builder, soup_date_to_time_t (expires));
+#endif
+  }
+  json_builder_end_object (builder);
+}
+
+static char *
+cookie_to_json (SoupCookie *cookie)
+{
+  g_autoptr (JsonBuilder) builder = json_builder_new ();
+  g_autoptr (JsonNode) root = NULL;
+
+  add_cookie_to_json (builder, cookie);
+
+  root = json_builder_get_root (builder);
+  return json_to_string (root, FALSE);
+}
+
+typedef struct {
+  GTask *task;
+  char *cookie_name;
+  SoupCookie *cookie;
+  gboolean remove_after_find;
+} CookiesCallbackData;
+
+static void
+cookies_callback_data_free (CookiesCallbackData *data)
+{
+  if (data) {
+    g_clear_pointer (&data->cookie_name, g_free);
+    g_clear_pointer (&data->cookie, soup_cookie_free);
+    g_free (data);
+  }
+}
+
+static SoupCookie *
+compare_best_cookie (SoupCookie *previous,
+                     SoupCookie *current)
+{
+  gint64 path_diff;
+
+  /* https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/cookies/get
+   * > If more than one cookie with the same name exists for a given URL, the one with the longest path will 
be returned.
+   * > For cookies with the same path length, the cookie with the earliest creation time will be returned. */
+
+  if (!previous)
+    return current;
+
+  path_diff = strlen (soup_cookie_get_path (previous)) - strlen (soup_cookie_get_path (current));
+
+  if (path_diff < 0)
+    return current;
+  if (path_diff > 0)
+    return previous;
+
+  /* We don't have a creation date, for now assume earlier in list is older. */
+  return previous;
+}
+
+static void
+delete_cookie_ready_cb (WebKitCookieManager *cookie_manager,
+                        GAsyncResult        *result,
+                        CookiesCallbackData *data)
+{
+  g_autoptr (GError) error = NULL;
+  gboolean success;
+
+  success = webkit_cookie_manager_delete_cookie_finish (cookie_manager, result, &error);
+
+  if (!success) {
+    g_task_return_error (data->task, g_steal_pointer (&error));
+    cookies_callback_data_free (data);
+    return;
+  }
+
+  g_task_return_pointer (data->task, cookie_to_json (data->cookie), g_free);
+  cookies_callback_data_free (data);
+}
+
+static void
+get_cookies_ready_cb (WebKitCookieManager *cookie_manager,
+                      GAsyncResult        *result,
+                      CookiesCallbackData *data)
+{
+  g_autoptr (GError) error = NULL;
+  GList *cookies = webkit_cookie_manager_get_cookies_finish (cookie_manager, result, &error);
+  SoupCookie *best_match = NULL;
+
+  if (error) {
+    g_task_return_error (data->task, g_steal_pointer (&error));
+    cookies_callback_data_free (data);
+    return;
+  }
+
+  for (GList *l = cookies; l; l = g_list_next (l)) {
+    SoupCookie *cookie = l->data;
+    if (strcmp (soup_cookie_get_name (cookie), data->cookie_name) != 0)
+      continue;
+
+    best_match = compare_best_cookie (best_match, cookie);
+  }
+
+  if (!best_match)
+    g_task_return_pointer (data->task, g_strdup ("null"), g_free);
+  else if (!data->remove_after_find)
+    g_task_return_pointer (data->task, cookie_to_json (best_match), g_free);
+  else {
+    data->cookie = soup_cookie_copy (best_match);
+    webkit_cookie_manager_delete_cookie (cookie_manager, data->cookie, NULL,
+                                         (GAsyncReadyCallback)delete_cookie_ready_cb,
+                                         data);
+    data = NULL;
+  }
+
+  g_list_free_full (cookies, (GDestroyNotify)soup_cookie_free);
+  cookies_callback_data_free (data);
+}
+
+static void
+cookies_handler_get (EphyWebExtension *self,
+                     char             *name,
+                     JSCValue         *args,
+                     WebKitWebView    *web_view,
+                     GTask            *task)
+{
+  g_autoptr (JSCValue) details = jsc_value_object_get_property_at_index (args, 0);
+  WebKitCookieManager *cookie_manager = get_cookie_manager ();
+  g_autofree char *cookie_name = NULL;
+  g_autofree char *url = NULL;
+  CookiesCallbackData *callback_data;
+
+  if (!jsc_value_is_object (details)) {
+    g_task_return_new_error (task, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_INVALID_ARGUMENT, 
"cookies.get(): Missing details object");
+    return;
+  }
+
+  cookie_name = get_string_property (details, "name");
+  url = get_string_property (details, "url");
+
+  if (!url || !cookie_name) {
+    g_task_return_new_error (task, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_INVALID_ARGUMENT, 
"cookies.get(): details missing url or name");
+    return;
+  }
+
+  if (!ephy_web_extension_has_host_permission (self, url)) {
+    g_task_return_new_error (task, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_PERMISSION_DENIED, 
"cookies.get(): Permission denied for host '%s'", url);
+    return;
+  }
+
+  callback_data = g_new0 (CookiesCallbackData, 1);
+  callback_data->task = task;
+  callback_data->cookie_name = g_steal_pointer (&cookie_name);
+
+  /* FIXME: The WebKit API doesn't expose details like first-party URLs to better filter this. The 
underlying libsoup API does so
+   * this just requires additions to WebKitGTK. */
+  webkit_cookie_manager_get_cookies (cookie_manager, url, NULL, (GAsyncReadyCallback)get_cookies_ready_cb, 
callback_data);
+}
+
+static void
+add_cookie_ready_cb (WebKitCookieManager *cookie_manager,
+                     GAsyncResult        *result,
+                     CookiesCallbackData *data)
+{
+  g_autoptr (GError) error = NULL;
+  gboolean success = webkit_cookie_manager_add_cookie_finish (cookie_manager, result, &error);
+
+  if (!success) {
+    g_task_return_error (data->task, g_steal_pointer (&error));
+    cookies_callback_data_free (data);
+    return;
+  }
+
+  g_task_return_pointer (data->task, cookie_to_json (data->cookie), g_free);
+  cookies_callback_data_free (data);
+}
+
+static void
+cookies_handler_set (EphyWebExtension *self,
+                     char             *name,
+                     JSCValue         *args,
+                     WebKitWebView    *web_view,
+                     GTask            *task)
+{
+  g_autoptr (JSCValue) details = jsc_value_object_get_property_at_index (args, 0);
+  g_autofree char *url = NULL;
+  g_autofree char *domain = NULL;
+  g_autofree char *cookie_name = NULL;
+  g_autofree char *value = NULL;
+  g_autofree char *path = NULL;
+  g_autofree char *same_site_str = NULL;
+  gboolean secure;
+  gboolean http_only;
+  gint32 expiration;
+  g_autoptr (SoupCookie) new_cookie = NULL;
+  g_autoptr (GUri) parsed_uri = NULL;
+  g_autoptr (GError) error = NULL;
+  WebKitCookieManager *cookie_manager = get_cookie_manager ();
+  CookiesCallbackData *callback_data;
+
+  if (!jsc_value_is_object (details)) {
+    g_task_return_new_error (task, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_INVALID_ARGUMENT, 
"cookies.set(): Missing details object");
+    return;
+  }
+
+  url = get_string_property (details, "url");
+  domain = get_string_property (details, "domain");
+  cookie_name = get_string_property (details, "name");
+  value = get_string_property (details, "value");
+  path = get_string_property (details, "path");
+  same_site_str = get_string_property (details, "sameSite");
+  expiration = get_int_property (details, "expirationDate");
+  secure = get_boolean_property (details, "secure");
+  http_only = get_boolean_property (details, "httpOnline");
+
+  if (!url) {
+    g_task_return_new_error (task, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_INVALID_ARGUMENT, 
"cookies.set(): Missing url property");
+    return;
+  }
+
+  if (!ephy_web_extension_has_host_permission (self, url) || (domain && 
!ephy_web_extension_has_host_permission (self, domain))) {
+    g_task_return_new_error (task, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_PERMISSION_DENIED, 
"cookies.set(): Permission denied for host '%s'", url);
+    return;
+  }
+
+  parsed_uri = g_uri_parse (url, G_URI_FLAGS_ENCODED_PATH | G_URI_FLAGS_ENCODED_QUERY | 
G_URI_FLAGS_SCHEME_NORMALIZE, &error);
+  if (error) {
+    g_task_return_new_error (task, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_INVALID_ARGUMENT, 
"cookies.set(): Failed to parse URI '%s': %s", url, error->message);
+    return;
+  }
+
+  new_cookie = soup_cookie_new (cookie_name ? cookie_name : "",
+                                value ? value : "",
+                                domain ? domain : g_uri_get_host (parsed_uri),
+                                path ? path : g_uri_get_path (parsed_uri),
+                                expiration);
+  soup_cookie_set_secure (new_cookie, secure);
+  soup_cookie_set_http_only (new_cookie, http_only);
+  soup_cookie_set_same_site_policy (new_cookie, string_to_samesite (same_site_str));
+
+  callback_data = g_new0 (CookiesCallbackData, 1);
+  callback_data->task = task;
+  callback_data->cookie = g_steal_pointer (&new_cookie);
+
+  webkit_cookie_manager_add_cookie (cookie_manager, callback_data->cookie, NULL, 
(GAsyncReadyCallback)add_cookie_ready_cb, callback_data);
+}
+
+static void
+cookies_handler_remove (EphyWebExtension *self,
+                        char             *name,
+                        JSCValue         *args,
+                        WebKitWebView    *web_view,
+                        GTask            *task)
+{
+  g_autoptr (JSCValue) details = jsc_value_object_get_property_at_index (args, 0);
+  g_autofree char *url = NULL;
+  g_autofree char *cookie_name = NULL;
+  WebKitCookieManager *cookie_manager = get_cookie_manager ();
+  CookiesCallbackData *callback_data;
+
+  if (!jsc_value_is_object (details)) {
+    g_task_return_new_error (task, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_INVALID_ARGUMENT, 
"cookies.remove(): Missing details object");
+    return;
+  }
+
+  url = get_string_property (details, "url");
+  cookie_name = get_string_property (details, "name");
+
+  if (!url || !name) {
+    g_task_return_new_error (task, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_INVALID_ARGUMENT, 
"cookies.remove(): Missing url or name property");
+    return;
+  }
+
+  if (!ephy_web_extension_has_host_permission (self, url)) {
+    g_task_return_new_error (task, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_PERMISSION_DENIED, 
"cookies.remove(): Permission denied for host '%s'", url);
+    return;
+  }
+
+  callback_data = g_new0 (CookiesCallbackData, 1);
+  callback_data->task = task;
+  callback_data->cookie_name = g_steal_pointer (&cookie_name);
+  callback_data->remove_after_find = TRUE;
+
+  webkit_cookie_manager_get_cookies (cookie_manager, url, NULL, (GAsyncReadyCallback)get_cookies_ready_cb, 
callback_data);
+}
+
+typedef struct {
+  GTask *task;
+  char *domain;
+  char *name;
+  char *path;
+  int secure;
+  int session;
+} GetAllCookiesCallbackData;
+
+static void
+get_all_cookies_callback_data_free (GetAllCookiesCallbackData *data)
+{
+  g_free (data->domain);
+  g_free (data->name);
+  g_free (data->path);
+  g_free (data);
+}
+
+static gboolean
+cookie_matches_filter (SoupCookie                *cookie,
+                       GetAllCookiesCallbackData *data)
+{
+  if (data->name && strcmp (soup_cookie_get_name (cookie), data->name) != 0)
+    return FALSE;
+  if (data->domain && !soup_cookie_domain_matches (cookie, data->domain))
+    return FALSE;
+  if (data->path && strcmp (soup_cookie_get_path (cookie), data->path) != 0)
+    return FALSE;
+  if (data->secure != -1 && soup_cookie_get_secure (cookie) != data->secure)
+    return FALSE;
+  if (data->session != -1) {
+    gpointer expires = soup_cookie_get_expires (cookie);
+    if (data->session && expires)
+      return FALSE;
+    if (!data->session && !expires)
+      return FALSE;
+  }
+
+  return TRUE;
+}
+
+static int
+cookie_compare_func (SoupCookie *a,
+                     SoupCookie *b)
+{
+  const char *path_a = soup_cookie_get_path (a);
+  const char *path_b = soup_cookie_get_path (b);
+
+  return strlen (path_b) - strlen (path_a);
+}
+
+static void
+get_all_cookies_ready_cb (WebKitCookieManager       *cookie_manager,
+                          GAsyncResult              *result,
+                          GetAllCookiesCallbackData *data)
+{
+  g_autoptr (GError) error = NULL;
+  g_autoptr (JsonBuilder) builder = json_builder_new ();
+  g_autoptr (JsonNode) root = NULL;
+  GList *cookies = webkit_cookie_manager_get_cookies_finish (cookie_manager, result, &error);
+
+  if (error) {
+    g_task_return_error (data->task, g_steal_pointer (&error));
+    get_all_cookies_callback_data_free (data);
+    return;
+  }
+
+  /* Sort by path length. */
+  cookies = g_list_sort (cookies, (GCompareFunc)cookie_compare_func);
+
+  json_builder_begin_array (builder);
+  for (GList *l = cookies; l; l = g_list_next (l)) {
+    SoupCookie *cookie = l->data;
+
+    if (cookie_matches_filter (cookie, data))
+      add_cookie_to_json (builder, cookie);
+  }
+  json_builder_end_array (builder);
+
+  root = json_builder_get_root (builder);
+  g_task_return_pointer (data->task, json_to_string (root, FALSE), g_free);
+
+  g_list_free_full (cookies, (GDestroyNotify)soup_cookie_free);
+  get_all_cookies_callback_data_free (data);
+}
+
+static void
+cookies_handler_get_all (EphyWebExtension *self,
+                         char             *name,
+                         JSCValue         *args,
+                         WebKitWebView    *web_view,
+                         GTask            *task)
+{
+  g_autoptr (JSCValue) details = jsc_value_object_get_property_at_index (args, 0);
+  WebKitCookieManager *cookie_manager = get_cookie_manager ();
+  g_autofree char *url = NULL;
+  GetAllCookiesCallbackData *callback_data;
+
+  if (!jsc_value_is_object (details)) {
+    g_task_return_new_error (task, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_INVALID_ARGUMENT, 
"cookies.getAll(): Missing details object");
+    return;
+  }
+
+  url = get_string_property (details, "url");
+
+  /* TODO: We can handle the case of no url by using webkit_website_data_manager_fetch() to list all domains 
and then get all cookies
+   * for all domains, but this is rather an ugly amount of work compared to libsoup directly. */
+  if (!url) {
+    g_task_return_new_error (task, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_INVALID_ARGUMENT, 
"cookies.getAll(): details missing url");
+    return;
+  }
+
+  if (!ephy_web_extension_has_host_permission (self, url)) {
+    g_task_return_new_error (task, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_PERMISSION_DENIED, 
"cookies.getAll(): Permission denied for host '%s'", url);
+    return;
+  }
+
+  callback_data = g_new0 (GetAllCookiesCallbackData, 1);
+  callback_data->task = task;
+  callback_data->name = get_string_property (details, "name");
+  callback_data->domain = get_string_property (details, "domain");
+  callback_data->path = get_string_property (details, "path");
+  callback_data->secure = get_int_property (details, "secure");
+  callback_data->session = get_int_property (details, "session");
+
+  /* FIXME: The WebKit API doesn't expose details like first-party URLs to better filter this. The 
underlying libsoup API does so
+   * this just requires additions to WebKitGTK. */
+  webkit_cookie_manager_get_cookies (cookie_manager, url, NULL, 
(GAsyncReadyCallback)get_all_cookies_ready_cb, callback_data);
+}
+
+static JsonNode *
+create_array_of_all_tab_ids (void)
+{
+  JsonNode *node = json_node_init_array (json_node_alloc (), json_array_new ());
+  JsonArray *array = json_node_get_array (node);
+  GList *windows;
+
+  windows = gtk_application_get_windows (GTK_APPLICATION (ephy_shell_get_default ()));
+  for (GList *win_list = windows; win_list; win_list = g_list_next (win_list)) {
+    EphyWindow *window = EPHY_WINDOW (win_list->data);
+    EphyTabView *tab_view = ephy_window_get_tab_view (window);
+
+    for (int i = 0; i < ephy_tab_view_get_n_pages (tab_view); i++) {
+      EphyWebView *web_view = ephy_embed_get_web_view (EPHY_EMBED (ephy_tab_view_get_nth_page (tab_view, 
i)));
+
+      json_array_add_int_element (array, ephy_web_view_get_uid (web_view));
+    }
+  }
+
+  return node;
+}
+
+static void
+cookies_handler_get_all_cookie_stores (EphyWebExtension *self,
+                                       char             *name,
+                                       JSCValue         *args,
+                                       WebKitWebView    *web_view,
+                                       GTask            *task)
+{
+  g_autoptr (JsonBuilder) builder = json_builder_new ();
+  g_autoptr (JsonNode) root = NULL;
+
+  /* We only have a single store so we create a single object with a list
+   * of every tab. */
+  json_builder_begin_array (builder);
+  json_builder_begin_object (builder);
+  json_builder_set_member_name (builder, "id");
+  json_builder_add_string_value (builder, "default");
+  json_builder_set_member_name (builder, "incognito");
+  json_builder_add_boolean_value (builder, ephy_embed_shell_get_mode (ephy_embed_shell_get_default ()) == 
EPHY_EMBED_SHELL_MODE_INCOGNITO);
+  json_builder_set_member_name (builder, "tabIds");
+  json_builder_add_value (builder, create_array_of_all_tab_ids ());
+  json_builder_end_object (builder);
+  json_builder_end_array (builder);
+
+  root = json_builder_get_root (builder);
+  g_task_return_pointer (task, json_to_string (root, FALSE), g_free);
+}
+
+static EphyWebExtensionAsyncApiHandler cookies_async_handlers[] = {
+  {"get", cookies_handler_get},
+  {"getAll", cookies_handler_get_all},
+  {"getAllCookieStores", cookies_handler_get_all_cookie_stores},
+  {"set", cookies_handler_set},
+  {"remove", cookies_handler_remove},
+};
+
+void
+ephy_web_extension_api_cookies_handler (EphyWebExtension *self,
+                                        char             *name,
+                                        JSCValue         *args,
+                                        WebKitWebView    *web_view,
+                                        GTask            *task)
+{
+  g_autoptr (GError) error = NULL;
+
+  if (!ephy_web_extension_has_permission (self, "cookies")) {
+    g_warning ("Extension %s tried to use cookies without permission.", ephy_web_extension_get_name (self));
+    error = g_error_new_literal (WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_PERMISSION_DENIED, "Permission 
Denied");
+    g_task_return_error (task, g_steal_pointer (&error));
+    return;
+  }
+
+  for (guint idx = 0; idx < G_N_ELEMENTS (cookies_async_handlers); idx++) {
+    EphyWebExtensionAsyncApiHandler handler = cookies_async_handlers[idx];
+
+    if (g_strcmp0 (handler.name, name) == 0) {
+      handler.execute (self, name, args, web_view, task);
+      return;
+    }
+  }
+
+  g_warning ("%s(): '%s' not implemented by Epiphany!", __FUNCTION__, name);
+  error = g_error_new_literal (WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_NOT_IMPLEMENTED, "Not Implemented");
+  g_task_return_error (task, g_steal_pointer (&error));
+}
diff --git a/src/webextension/api/cookies.h b/src/webextension/api/cookies.h
new file mode 100644
index 000000000..8c8d1dfdd
--- /dev/null
+++ b/src/webextension/api/cookies.h
@@ -0,0 +1,35 @@
+/*
+ *  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 <webkit2/webkit2.h>
+
+G_BEGIN_DECLS
+
+void         ephy_web_extension_api_cookies_handler                    (EphyWebExtension *self,
+                                                                        char             *name,
+                                                                        JSCValue         *value,
+                                                                        WebKitWebView    *web_view,
+                                                                        GTask            *task);
+
+G_END_DECLS
diff --git a/src/webextension/ephy-web-extension-manager.c b/src/webextension/ephy-web-extension-manager.c
index 0b4269b23..9af1d7c97 100644
--- a/src/webextension/ephy-web-extension-manager.c
+++ b/src/webextension/ephy-web-extension-manager.c
@@ -36,6 +36,7 @@
 #include "ephy-web-view.h"
 
 #include "api/alarms.h"
+#include "api/cookies.h"
 #include "api/notifications.h"
 #include "api/pageaction.h"
 #include "api/runtime.h"
@@ -66,6 +67,7 @@ G_DEFINE_TYPE (EphyWebExtensionManager, ephy_web_extension_manager, G_TYPE_OBJEC
 
 EphyWebExtensionAsyncApiHandler api_handlers[] = {
   {"alarms", ephy_web_extension_api_alarms_handler},
+  {"cookies", ephy_web_extension_api_cookies_handler},
   {"notifications", ephy_web_extension_api_notifications_handler},
   {"pageAction", ephy_web_extension_api_pageaction_handler},
   {"runtime", ephy_web_extension_api_runtime_handler},
diff --git a/src/webextension/ephy-web-extension.c b/src/webextension/ephy-web-extension.c
index bbbb2e012..1af140789 100644
--- a/src/webextension/ephy-web-extension.c
+++ b/src/webextension/ephy-web-extension.c
@@ -1370,6 +1370,22 @@ parse_uri_with_wildcard_scheme (const char  *uri,
   return g_uri_parse (uri_to_check, G_URI_FLAGS_ENCODED_PATH | G_URI_FLAGS_ENCODED_QUERY | 
G_URI_FLAGS_SCHEME_NORMALIZE, error);
 }
 
+static gboolean
+is_default_port (const char *scheme,
+                 int         port)
+{
+  static const char * const default_port_80[] = { "http", "ws", NULL };
+  static const char * const default_port_443[] = { "https", "wss", NULL };
+
+  switch (port) {
+    case 80:
+      return g_strv_contains (default_port_80, scheme);
+    case 443:
+      return g_strv_contains (default_port_443, scheme);
+  }
+  return FALSE;
+}
+
 static gboolean
 permission_matches_uri (const char *permission,
                         GUri       *uri)
@@ -1378,10 +1394,8 @@ permission_matches_uri (const char *permission,
   g_autoptr (GError) error = NULL;
   g_autofree char *permission_path_and_query = NULL;
   g_autofree char *uri_path_and_query = NULL;
-
-  /* https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Match_patterns#all_urls */
-  if (strcmp (permission, "<all_urls>") == 0)
-    return is_supported_scheme (g_uri_get_scheme (uri));
+  const char *permission_scheme;
+  int permission_port;
 
   permission_uri = parse_uri_with_wildcard_scheme (permission, &error);
   if (error) {
@@ -1389,15 +1403,18 @@ permission_matches_uri (const char *permission,
     return FALSE;
   }
 
-  /* Ports are forbidden. */
-  if (g_uri_get_port (permission_uri) != -1)
+  permission_scheme = g_uri_get_scheme (permission_uri);
+  permission_port = g_uri_get_port (permission_uri);
+
+  /* Ports are forbidden, however GUri normalizes these to the default. */
+  if (permission_port != -1 && !is_default_port (permission_scheme, permission_port))
     return FALSE;
 
   /* Empty paths are forbidden. */
   if (strcmp (g_uri_get_path (permission_uri), "") == 0)
     return FALSE;
 
-  if (!scheme_matches (g_uri_get_scheme (permission_uri), g_uri_get_scheme (uri)))
+  if (!scheme_matches (permission_scheme, g_uri_get_scheme (uri)))
     return FALSE;
 
   if (!host_matches (g_uri_get_host (permission_uri), g_uri_get_host (uri)))
@@ -1420,6 +1437,7 @@ ephy_web_extension_has_permission_internal (EphyWebExtension *self,
 {
   EphyWebView *active_web_view = ephy_shell_get_active_web_view (ephy_shell_get_default ());
   gboolean is_active_tab = active_web_view == web_view;
+  GUri *host;
 
   /* https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Match_patterns */
 
@@ -1434,12 +1452,10 @@ ephy_web_extension_has_permission_internal (EphyWebExtension *self,
   }
 
   /* Note this one is NULL terminated. */
+  host = g_uri_parse (ephy_web_view_get_address (web_view), G_URI_FLAGS_ENCODED_PATH | 
G_URI_FLAGS_ENCODED_QUERY | G_URI_FLAGS_SCHEME_NORMALIZE, NULL);
+  g_assert (host); /* WebKitGTK shouldn't ever expose an invalid URI. */
   for (guint i = 0; i < self->host_permissions->len - 1; i++) {
-    GUri *host = g_uri_parse (ephy_web_view_get_address (web_view), G_URI_FLAGS_ENCODED_PATH | 
G_URI_FLAGS_ENCODED_QUERY | G_URI_FLAGS_SCHEME_NORMALIZE, NULL);
     const char *permission = g_ptr_array_index (self->host_permissions, i);
-
-    g_assert (host);
-
     if (permission_matches_uri (permission, host))
       return TRUE;
   }
@@ -1463,6 +1479,24 @@ ephy_web_extension_has_tab_or_host_permission (EphyWebExtension *self,
   return ephy_web_extension_has_permission_internal (self, web_view, is_user_interaction, TRUE);
 }
 
+gboolean
+ephy_web_extension_has_host_permission (EphyWebExtension *self,
+                                        const char       *host)
+{
+  GUri *uri = g_uri_parse (host, G_URI_FLAGS_ENCODED_PATH | G_URI_FLAGS_ENCODED_QUERY | 
G_URI_FLAGS_SCHEME_NORMALIZE, NULL);
+  if (!uri)
+    return FALSE;
+
+  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))
+      return TRUE;
+  }
+
+  return FALSE;
+}
+
+
 gboolean
 ephy_web_extension_has_permission (EphyWebExtension *self,
                                    const char       *permission)
diff --git a/src/webextension/ephy-web-extension.h b/src/webextension/ephy-web-extension.h
index 78cd1e557..9840d05cb 100644
--- a/src/webextension/ephy-web-extension.h
+++ b/src/webextension/ephy-web-extension.h
@@ -159,6 +159,9 @@ gboolean               ephy_web_extension_has_host_or_active_permission   (EphyW
 gboolean               ephy_web_extension_has_permission                  (EphyWebExtension *self,
                                                                            const char       *permission);
 
+gboolean               ephy_web_extension_has_host_permission             (EphyWebExtension *self,
+                                                                           const char       *host);
+
 const char * const    *ephy_web_extension_get_host_permissions            (EphyWebExtension *self);
 
 JsonNode              *ephy_web_extension_get_local_storage               (EphyWebExtension *self);
diff --git a/src/webextension/meson.build b/src/webextension/meson.build
index 80ee092e6..ae9cedce0 100644
--- a/src/webextension/meson.build
+++ b/src/webextension/meson.build
@@ -1,5 +1,6 @@
 ephywebextension_src = [
   'webextension/api/alarms.c',
+  'webextension/api/cookies.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]