[epiphany/pgriffis/web-extension/manifest-parsing] WebExtensions: Greatly improve robustness of manifest parsing




commit a27ca1eb8b53b3be1c42b0b251fe60295e55a839
Author: Patrick Griffis <pgriffis igalia com>
Date:   Sat Jun 25 20:32:19 2022 -0500

    WebExtensions: Greatly improve robustness of manifest parsing
    
    This handles completely invalid manifest files safely and reports
    more useful errors.

 src/webextension/ephy-json-utils.c            | 198 ++++++++++++++
 src/webextension/ephy-json-utils.h            |  46 ++++
 src/webextension/ephy-web-extension-manager.c |   3 +-
 src/webextension/ephy-web-extension.c         | 363 ++++++++++++--------------
 src/webextension/meson.build                  |   1 +
 5 files changed, 419 insertions(+), 192 deletions(-)
---
diff --git a/src/webextension/ephy-json-utils.c b/src/webextension/ephy-json-utils.c
new file mode 100644
index 000000000..68d6a3e3d
--- /dev/null
+++ b/src/webextension/ephy-json-utils.c
@@ -0,0 +1,198 @@
+/* -*- 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 "ephy-json-utils.h"
+
+/**
+ * ephy_json_object_get_string:
+ * @object: A valid #JsonObject
+ * @name: The member name
+ *
+ * This safely looks up a string member of a #JsonObject.
+ *
+ * Returns: (transfer none): A string or %NULL if member was missing or not a string.
+ */
+const char *
+ephy_json_object_get_string (JsonObject *object,
+                             const char *name)
+{
+  JsonNode *node = json_object_get_member (object, name);
+
+  if (!node || !JSON_NODE_HOLDS_VALUE (node))
+    return NULL;
+
+  if (json_node_get_value_type (node) != G_TYPE_STRING)
+    return NULL;
+
+  return json_node_get_string (node);
+}
+
+/**
+ * ephy_json_object_get_int:
+ * @object: A valid #JsonObject
+ * @name: The member name
+ *
+ * This safely looks up an int member of a #JsonObject.
+ *
+ * Returns: A number or `-1` if member was missing or not a number.
+ */
+gint64
+ephy_json_object_get_int (JsonObject *object,
+                          const char *name)
+{
+  JsonNode *node = json_object_get_member (object, name);
+
+  if (!node || !JSON_NODE_HOLDS_VALUE (node))
+    return -1;
+
+  if (json_node_get_value_type (node) != G_TYPE_INT64)
+    return -1;
+
+  return json_node_get_int (node);
+}
+
+/**
+ * ephy_json_object_get_double:
+ * @object: A valid #JsonObject
+ * @name: The member name
+ *
+ * This safely looks up a double member of a #JsonObject.
+ *
+ * Returns: A number or `-1.0` if member was missing or not a number.
+ */
+double
+ephy_json_object_get_double (JsonObject *object,
+                             const char *name)
+{
+  JsonNode *node = json_object_get_member (object, name);
+
+  if (!node || !JSON_NODE_HOLDS_VALUE (node))
+    return -1;
+
+  if (json_node_get_value_type (node) != G_TYPE_DOUBLE)
+    return -1;
+
+  return json_node_get_int (node);
+}
+
+/**
+ * ephy_json_object_get_array:
+ * @object: A valid #JsonObject
+ * @name: The member name
+ *
+ * This safely looks up an array member of a #JsonObject.
+ *
+ * Returns: (transfer none): An array or %NULL if not an array or not found.
+ */
+JsonArray *
+ephy_json_object_get_array (JsonObject *object,
+                            const char *name)
+{
+  JsonNode *node = json_object_get_member (object, name);
+
+  if (!node || !JSON_NODE_HOLDS_ARRAY (node))
+    return NULL;
+
+  return json_node_get_array (node);
+}
+
+/**
+ * ephy_json_object_get_object:
+ * @object: A valid #JsonObject
+ * @name: The member name
+ *
+ * This safely looks up an object member of a #JsonObject.
+ *
+ * Returns: (transfer none): An object or %NULL if not an object or not found.
+ */
+JsonObject *
+ephy_json_object_get_object (JsonObject *object,
+                             const char *name)
+{
+  JsonNode *node = json_object_get_member (object, name);
+
+  if (!node || !JSON_NODE_HOLDS_OBJECT (node))
+    return NULL;
+
+  return json_node_get_object (node);
+}
+
+/**
+ * ephy_json_object_get_boolean:
+ * @object: A valid #JsonObject
+ * @name: The member name
+ * @default_value: The default value
+ *
+ * This safely looks up a boolean member of a #JsonObject.
+ *
+ * Returns: A number or @default if member was missing or not a boolean.
+ */
+gboolean
+ephy_json_object_get_boolean (JsonObject *object,
+                              const char *name,
+                              gboolean    default_value)
+{
+  JsonNode *node = json_object_get_member (object, name);
+
+  if (!node || !JSON_NODE_HOLDS_VALUE (node))
+    return default_value;
+
+  if (json_node_get_value_type (node) != G_TYPE_BOOLEAN)
+    return default_value;
+
+  return json_node_get_boolean (node);
+}
+
+/**
+ * ephy_json_node_get_object:
+ * @node: (nullable): A #JsonNode or %NULL
+ *
+ * This safely turns a #JsonNode into a #JsonObject.
+ *
+ * Returns: (transfer none): A #JsonObject or %NULL if not an object.
+ */
+JsonObject *
+ephy_json_node_get_object (JsonNode *node)
+{
+  if (!node || !JSON_NODE_HOLDS_OBJECT (node))
+    return NULL;
+
+  return json_node_get_object (node);
+}
+
+/**
+ * ephy_json_node_to_string:
+ * @node: (nullable): A #JsonNode or %NULL
+ *
+ * This safely turns a #JsonNode into a string.
+ *
+ * Returns: (transfer none): A string or %NULL if not a string.
+ */
+const char *
+ephy_json_node_to_string (JsonNode *node)
+{
+  if (!node || !JSON_NODE_HOLDS_VALUE (node))
+    return NULL;
+
+  if (json_node_get_value_type (node) != G_TYPE_STRING)
+    return NULL;
+
+  return json_node_get_string (node);
+}
diff --git a/src/webextension/ephy-json-utils.h b/src/webextension/ephy-json-utils.h
new file mode 100644
index 000000000..a2fad2057
--- /dev/null
+++ b/src/webextension/ephy-json-utils.h
@@ -0,0 +1,46 @@
+/* -*- 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/>.
+ */
+
+#pragma once
+
+#include <json-glib/json-glib.h>
+
+const char    *ephy_json_object_get_string           (JsonObject          *object,
+                                                      const char          *name);
+
+gboolean      ephy_json_object_get_boolean          (JsonObject          *object,
+                                                     const char          *name,
+                                                     gboolean             default_value);
+
+double        ephy_json_object_get_double           (JsonObject          *object,
+                                                     const char          *name);
+
+gint64        ephy_json_object_get_int              (JsonObject          *object,
+                                                     const char          *name);
+
+JsonArray    *ephy_json_object_get_array            (JsonObject          *object,
+                                                     const char          *name);
+
+JsonObject   *ephy_json_object_get_object           (JsonObject          *object,
+                                                     const char          *name);
+
+JsonObject   *ephy_json_node_get_object             (JsonNode            *node);
+
+const char   *ephy_json_node_to_string              (JsonNode            *node);
\ No newline at end of file
diff --git a/src/webextension/ephy-web-extension-manager.c b/src/webextension/ephy-web-extension-manager.c
index ede69c9b1..1112e371d 100644
--- a/src/webextension/ephy-web-extension-manager.c
+++ b/src/webextension/ephy-web-extension-manager.c
@@ -138,13 +138,14 @@ on_web_extension_loaded (GObject      *source_object,
                          GAsyncResult *result,
                          gpointer      user_data)
 {
+  GFile *target = G_FILE (source_object);
   g_autoptr (GError) error = NULL;
   EphyWebExtension *web_extension;
   EphyWebExtensionManager *self = EPHY_WEB_EXTENSION_MANAGER (user_data);
 
   web_extension = ephy_web_extension_load_finished (source_object, result, &error);
   if (!web_extension) {
-    g_warning ("Failed to load extension: %s", error->message);
+    g_warning ("Failed to load extension %s: %s", g_file_peek_path (target), error->message);
     return;
   }
 
diff --git a/src/webextension/ephy-web-extension.c b/src/webextension/ephy-web-extension.c
index bdba26505..1e229af6d 100644
--- a/src/webextension/ephy-web-extension.c
+++ b/src/webextension/ephy-web-extension.c
@@ -27,6 +27,7 @@
 
 #include "ephy-embed-shell.h"
 #include "ephy-file-helpers.h"
+#include "ephy-json-utils.h"
 #include "ephy-shell.h"
 #include "ephy-string.h"
 #include "ephy-web-extension.h"
@@ -66,10 +67,6 @@ typedef struct {
   char *popup;
 } WebExtensionBrowserAction;
 
-typedef struct {
-  char *page;
-} WebExtensionBackground;
-
 typedef struct {
   char *page;
 } WebExtensionOptionsUI;
@@ -95,7 +92,7 @@ struct _EphyWebExtension {
   char *homepage_url;
   GList *icons;
   GList *content_scripts;
-  WebExtensionBackground *background;
+  char *background_page;
   GHashTable *page_action_map;
   WebExtensionPageAction *page_action;
   WebExtensionBrowserAction *browser_action;
@@ -116,16 +113,6 @@ G_DEFINE_TYPE (EphyWebExtension, ephy_web_extension, G_TYPE_OBJECT)
 
 static gboolean is_supported_scheme (const char *scheme);
 
-static const char *
-get_string_member (JsonObject *object,
-                   const char *name)
-{
-  JsonNode *node = json_object_get_member (object, name);
-  if (!node || !JSON_NODE_HOLDS_VALUE (node))
-    return NULL;
-  return json_node_get_string (node);
-}
-
 gboolean
 ephy_web_extension_has_resource (EphyWebExtension *self,
                                  const char       *name)
@@ -184,7 +171,7 @@ web_extension_icon_new (EphyWebExtension *self,
     if (!self->xpi) {
       g_autofree char *path = NULL;
       path = g_build_filename (self->base_location, file, NULL);
-      pixbuf = gdk_pixbuf_new_from_file (path, NULL);
+      pixbuf = gdk_pixbuf_new_from_file (path, &error);
     }
   } else {
     stream = g_memory_input_stream_new_from_data (data, length, NULL);
@@ -254,21 +241,6 @@ web_extension_options_ui_free (WebExtensionOptionsUI *options_ui)
   g_free (options_ui);
 }
 
-static WebExtensionBackground *
-web_extension_background_new (void)
-{
-  WebExtensionBackground *background = g_malloc0 (sizeof (WebExtensionBackground));
-
-  return background;
-}
-
-static void
-web_extension_background_free (WebExtensionBackground *background)
-{
-  g_clear_pointer (&background->page, g_free);
-  g_free (background);
-}
-
 static void
 web_extension_add_icon (JsonObject *object,
                         const char *member_name,
@@ -277,9 +249,15 @@ web_extension_add_icon (JsonObject *object,
 {
   EphyWebExtension *self = EPHY_WEB_EXTENSION (user_data);
   WebExtensionIcon *icon;
-  const char *file = json_node_get_string (member_node);
+  const char *file;
   gint64 size;
 
+  file = ephy_json_node_to_string (member_node);
+  if (!file) {
+    LOG ("Skipping icon as value is invalid");
+    return;
+  }
+
   size = g_ascii_strtoll (member_name, NULL, 0);
   if (size == 0) {
     LOG ("Skipping %s as web extension icon as size is 0", file);
@@ -287,7 +265,6 @@ web_extension_add_icon (JsonObject *object,
   }
 
   icon = web_extension_icon_new (self, file, size);
-
   if (icon)
     self->icons = g_list_append (self->icons, icon);
 }
@@ -300,16 +277,22 @@ web_extension_add_browser_icons (JsonObject *object,
 {
   EphyWebExtension *self = EPHY_WEB_EXTENSION (user_data);
   WebExtensionIcon *icon;
-  const char *file = json_node_get_string (member_node);
+  const char *file;
   gint64 size;
 
+  file = ephy_json_node_to_string (member_node);
+  if (!file) {
+    LOG ("Skipping browser icon as value is invalid");
+    return;
+  }
+
   size = g_ascii_strtoll (member_name, NULL, 0);
   if (size == 0) {
     LOG ("Skipping %s as web extension browser icon as size is 0", file);
     return;
   }
-  icon = web_extension_icon_new (self, file, size);
 
+  icon = web_extension_icon_new (self, file, size);
   if (icon)
     self->browser_action->default_icons = g_list_append (self->browser_action->default_icons, icon);
 }
@@ -386,7 +369,12 @@ web_extension_add_allow_list (JsonArray *array,
                               gpointer   user_data)
 {
   WebExtensionContentScript *content_script = user_data;
-  const char *match = json_node_get_string (element_node);
+  const char *match = ephy_json_node_to_string (element_node);
+
+  if (!match) {
+    LOG ("Skipping invalid content_script match rule");
+    return;
+  }
 
   if (g_strcmp0 (match, "<all_urls>") == 0) {
     g_ptr_array_add (content_script->allow_list, g_strdup ("https://*/*";));
@@ -404,8 +392,14 @@ web_extension_add_block_list (JsonArray *array,
                               gpointer   user_data)
 {
   WebExtensionContentScript *content_script = user_data;
+  const char *exclude = ephy_json_node_to_string (element_node);
+
+  if (!exclude) {
+    LOG ("Skipping invalid content_script exclude rule");
+    return;
+  }
 
-  g_ptr_array_add (content_script->block_list, g_strdup (json_node_get_string (element_node)));
+  g_ptr_array_add (content_script->block_list, g_strdup (exclude));
 }
 
 static void
@@ -415,8 +409,14 @@ web_extension_add_js (JsonArray *array,
                       gpointer   user_data)
 {
   WebExtensionContentScript *content_script = user_data;
+  const char *file = ephy_json_node_to_string (element_node);
 
-  g_ptr_array_add (content_script->js, g_strdup (json_node_get_string (element_node)));
+  if (!file) {
+    LOG ("Skipping invalid content_script js file");
+    return;
+  }
+
+  g_ptr_array_add (content_script->js, g_strdup (file));
 }
 
 static void
@@ -456,11 +456,16 @@ web_extension_add_content_script (JsonArray *array,
   WebKitUserContentInjectedFrames injected_frames = WEBKIT_USER_CONTENT_INJECT_TOP_FRAME;
   WebKitUserScriptInjectionTime injection_time = WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_END;
   WebExtensionContentScript *content_script;
-  JsonObject *object = json_node_get_object (element_node);
+  JsonObject *object = ephy_json_node_get_object (element_node);
   JsonArray *child_array;
   const char *run_at;
   gboolean all_frames;
 
+  if (!object) {
+    LOG ("Skipping content script as invalid object");
+    return;
+  }
+
   /* TODO: The default value is "document_idle", which in WebKit term is document_end */
   run_at = json_object_get_string_member_with_default (object, "run_at", "document_idle");
   if (strcmp (run_at, "document_start") == 0) {
@@ -468,35 +473,30 @@ web_extension_add_content_script (JsonArray *array,
   } else if (strcmp (run_at, "document_end") == 0) {
     injection_time = WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_END;
   } else if (strcmp (run_at, "document_idle") == 0) {
-    g_warning ("run_at: document_idle not supported by WebKit, falling back to document_end");
+    LOG ("run_at: document_idle not supported by WebKit, falling back to document_end");
     injection_time = WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_END;
   } else {
-    g_warning ("Unhandled run_at '%s' in web_extension, ignoring.", run_at);
+    LOG ("Unhandled run_at '%s' in web_extension, ignoring.", run_at);
     return;
   }
 
   /* all_frames */
-  all_frames = json_object_get_boolean_member_with_default (object, "all_frames", FALSE);
+  all_frames = ephy_json_object_get_boolean (object, "all_frames", FALSE);
   injected_frames = all_frames ? WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES : 
WEBKIT_USER_CONTENT_INJECT_TOP_FRAME;
 
   content_script = web_extension_content_script_new (injected_frames, injection_time);
-  if (json_object_has_member (object, "matches")) {
-    child_array = json_object_get_array_member (object, "matches");
+
+  if ((child_array = ephy_json_object_get_array (object, "matches")))
     json_array_foreach_element (child_array, web_extension_add_allow_list, content_script);
-  }
-  g_ptr_array_add (content_script->allow_list, NULL);
+  g_ptr_array_add (content_script->allow_list, NULL); /* This is a GStrv later. */
 
-  if (json_object_has_member (object, "exclude_matches")) {
-    child_array = json_object_get_array_member (object, "exclude_matches");
+  if ((child_array = ephy_json_object_get_array (object, "exclude_matches")))
     json_array_foreach_element (child_array, web_extension_add_block_list, content_script);
-  }
-  g_ptr_array_add (content_script->block_list, NULL);
+  g_ptr_array_add (content_script->block_list, NULL); /* This is a GStrv later. */
 
-  if (json_object_has_member (object, "js")) {
-    child_array = json_object_get_array_member (object, "js");
-    if (child_array)
-      json_array_foreach_element (child_array, web_extension_add_js, content_script);
-  }
+
+  if ((child_array = ephy_json_object_get_array (object, "js")))
+    json_array_foreach_element (child_array, web_extension_add_js, content_script);
 
   /* Create user scripts so that we can unload them if necessary */
   web_extension_content_script_build (self, content_script);
@@ -534,46 +534,53 @@ generate_background_page (EphyWebExtension *self,
 }
 
 static void
-web_extension_add_background (JsonObject *object,
-                              const char *member_name,
-                              JsonNode   *member_node,
-                              gpointer    user_data)
+web_extension_parse_background (EphyWebExtension *self,
+                                JsonObject       *object)
 {
   /* https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/background
    * Limitations:
    *  - persistent with false is not supported yet.
    */
-  EphyWebExtension *self = EPHY_WEB_EXTENSION (user_data);
+  const char *page;
+  JsonArray *scripts;
 
-  if (!json_object_has_member (object, "scripts") && !json_object_has_member (object, "page") && 
!json_object_has_member (object, "persistent")) {
-    g_warning ("Invalid background section, it must be either scripts, page or persistent entry.");
-    return;
-  }
+  if ((page = ephy_json_object_get_string (object, "page")))
+    self->background_page = g_strdup (page);
 
-  if (!self->background)
-    self->background = web_extension_background_new ();
+  if ((scripts = ephy_json_object_get_array (object, "scripts"))) {
+    if (self->background_page)
+      LOG ("background already has page set, ignoring scripts");
+    else
+      self->background_page = generate_background_page (self, scripts);
+  }
 
-  if (json_object_has_member (object, "scripts")) {
-    self->background->page = generate_background_page (self, json_object_get_array_member (object, 
"scripts"));
-  } else if (!self->background->page && json_object_has_member (object, "page")) {
-    self->background->page = g_strdup (json_object_get_string_member (object, "page"));
-  } else if (json_object_has_member (object, "persistent")) {
+  if (json_object_has_member (object, "persistent"))
     LOG ("persistent background setting is not handled in Epiphany");
-  }
+
+  if (!self->background_page)
+    LOG ("Invalid background object. Missing either scripts or page");
 }
 
 static void
-web_extension_add_page_action (JsonObject *object,
-                               gpointer    user_data)
+web_extension_parse_page_action (EphyWebExtension *self,
+                                 JsonObject       *object)
 {
-  EphyWebExtension *self = EPHY_WEB_EXTENSION (user_data);
-  const char *default_icon = get_string_member (object, "default_icon");
+  const char *default_icon = ephy_json_object_get_string (object, "default_icon");
   g_autofree char *path = NULL;
   WebExtensionPageAction *page_action;
   WebExtensionIcon *icon;
+  g_autoptr (GFile) base = NULL;
+  g_autoptr (GFile) icon_file = NULL;
 
   if (!default_icon) {
-    g_debug ("We only support page_action's default_icon as a string currently.");
+    LOG ("We only support page_action's default_icon as a string currently.");
+    return;
+  }
+  base = g_file_new_for_path (self->base_location);
+  icon_file = g_file_resolve_relative_path (base, default_icon);
+
+  if (!g_file_has_prefix (icon_file, base)) {
+    LOG ("page_action's default_icon is outside of the extension");
     return;
   }
 
@@ -583,9 +590,7 @@ web_extension_add_page_action (JsonObject *object,
   icon = g_malloc (sizeof (WebExtensionIcon));
   icon->size = -1;
   icon->file = g_strdup (default_icon);
-
-  path = g_build_filename (self->base_location, icon->file, NULL);
-  icon->pixbuf = gdk_pixbuf_new_from_file (path, NULL);
+  icon->pixbuf = gdk_pixbuf_new_from_file (g_file_peek_path (icon_file), NULL);
 
   self->page_action->default_icons = g_list_append (self->page_action->default_icons, icon);
 }
@@ -643,65 +648,56 @@ web_extension_get_translation (EphyWebExtension *self,
 }
 
 char *
-ephy_web_extension_manifest_get_key (EphyWebExtension *self,
-                                     JsonObject       *object,
-                                     char             *key)
-{
-  char *value = NULL;
-
-  if (json_object_has_member (object, key)) {
-    g_autofree char *ret = g_strdup (json_object_get_string_member (object, key));
-
-    /* Translation are requested with a unique string, e.g.:
-     * __MSG_unique_name__ but stored as unique_name in messages.json.
-     * Let's check for this prefix and suffix and extract the unique name
-     */
-    if (g_str_has_prefix (ret, "__MSG_") && g_str_has_suffix (ret, "__")) {
-      /* FIXME: Set current locale */
-      g_autofree char *locale = g_strdup ("en");
-
-      /* Remove trailing __ */
-      ret[strlen (ret) - 2] = '\0';
-      value = web_extension_get_translation (self, locale, ret + strlen ("__MSG_"));
-    } else {
-      value = g_strdup (ret);
-    }
+ephy_web_extension_manifest_get_localized_string (EphyWebExtension *self,
+                                                  JsonObject       *object,
+                                                  char             *name)
+{
+  g_autofree char *value = g_strdup (ephy_json_object_get_string (object, name));
+  if (!value)
+    return g_strdup ("");
+
+  /* Translation are requested with a unique string, e.g.:
+    * __MSG_unique_name__ but stored as unique_name in messages.json.
+    * Let's check for this prefix and suffix and extract the unique name
+    */
+  if (g_str_has_prefix (value, "__MSG_") && g_str_has_suffix (value, "__")) {
+    /* FIXME: Set current locale */
+    g_autofree char *locale = g_strdup ("en");
+
+    /* Remove trailing __ */
+    value[strlen (value) - 2] = '\0';
+    return web_extension_get_translation (self, locale, value + strlen ("__MSG_"));
   }
 
-  return value;
+  return g_steal_pointer (&value);
 }
 
 static void
-web_extension_add_browser_action (JsonObject *object,
-                                  gpointer    user_data)
+web_extension_parse_browser_action (EphyWebExtension *self,
+                                    JsonObject       *object)
 {
-  EphyWebExtension *self = EPHY_WEB_EXTENSION (user_data);
   WebExtensionBrowserAction *browser_action = g_malloc0 (sizeof (WebExtensionBrowserAction));
 
-  g_clear_object (&self->browser_action);
   self->browser_action = browser_action;
-
-  if (json_object_has_member (object, "default_title")) {
-    self->browser_action->title = ephy_web_extension_manifest_get_key (self, object, "default_title");
-  }
+  self->browser_action->title = ephy_web_extension_manifest_get_localized_string (self, object, 
"default_title");
+  self->browser_action->popup = g_strdup (ephy_json_object_get_string (object, "default_popup"));
 
   if (json_object_has_member (object, "default_icon")) {
-    /* defaullt_icon can be Object or String */
     JsonNode *icon_node = json_object_get_member (object, "default_icon");
 
+    /* default_icon can be Object or String */
     if (json_node_get_node_type (icon_node) == JSON_NODE_OBJECT) {
       JsonObject *icon_object = json_object_get_object_member (object, "default_icon");
       json_object_foreach_member (icon_object, web_extension_add_browser_icons, self);
-    } else {
+    } else if (JSON_NODE_HOLDS_VALUE (icon_node) && json_node_get_value_type (icon_node) == G_TYPE_STRING) {
       const char *default_icon = json_object_get_string_member (object, "default_icon");
       WebExtensionIcon *icon = web_extension_icon_new (self, default_icon, -1);
 
       self->browser_action->default_icons = g_list_append (self->browser_action->default_icons, icon);
+    } else {
+      LOG ("browser_action's default_icon is invalid");
     }
   }
-
-  if (json_object_has_member (object, "default_popup"))
-    self->browser_action->popup = g_strdup (json_object_get_string_member (object, "default_popup"));
 }
 
 static void
@@ -714,15 +710,17 @@ web_extension_browser_action_free (WebExtensionBrowserAction *browser_action)
 }
 
 static void
-web_extension_add_options_ui (JsonObject *object,
-                              gpointer    user_data)
+web_extension_parse_options_ui (EphyWebExtension *self,
+                                JsonObject       *object)
 {
-  EphyWebExtension *self = EPHY_WEB_EXTENSION (user_data);
-  const char *page = json_object_get_string_member (object, "page");
-  WebExtensionOptionsUI *options_ui = web_extension_options_ui_new (page);
+  const char *page = ephy_json_object_get_string (object, "page");
 
-  g_clear_pointer (&self->options_ui, web_extension_options_ui_free);
-  self->options_ui = options_ui;
+  if (!page) {
+    LOG ("Skipping options_ui without page");
+    return;
+  }
+
+  self->options_ui = web_extension_options_ui_new (page);
 }
 
 static void
@@ -732,12 +730,17 @@ web_extension_add_permission (JsonArray *array,
                               gpointer   user_data)
 {
   EphyWebExtension *self = EPHY_WEB_EXTENSION (user_data);
-  const char *permission = json_node_get_string (element_node);
+  const char *permission = ephy_json_node_to_string (element_node);
+
+  if (!permission) {
+    LOG ("Skipping invalid permission");
+    return;
+  }
 
   if (strstr (permission, "://") != NULL) {
     if (!g_str_has_prefix (permission, "*://") &&
         !is_supported_scheme (g_uri_peek_scheme (permission))) {
-      g_warning ("Unsupported host permission: %s", permission);
+      LOG ("Unsupported host permission: %s", permission);
       return;
     }
     g_ptr_array_insert (self->host_permissions, 0, g_strdup (permission));
@@ -760,7 +763,7 @@ web_extension_add_web_accessible_resource (JsonArray *array,
                                            gpointer   user_data)
 {
   EphyWebExtension *self = EPHY_WEB_EXTENSION (user_data);
-  const char *web_accessible_resource = json_node_get_string (element_node);
+  const char *web_accessible_resource = ephy_json_node_to_string (element_node);
 
   if (!web_accessible_resource)
     return;
@@ -789,7 +792,7 @@ ephy_web_extension_dispose (GObject *object)
   g_clear_list (&self->icons, (GDestroyNotify)web_extension_icon_free);
   g_clear_list (&self->content_scripts, (GDestroyNotify)web_extension_content_script_free);
   g_clear_pointer (&self->resources, g_hash_table_unref);
-  g_clear_pointer (&self->background, web_extension_background_free);
+  g_clear_pointer (&self->background_page, g_free);
   g_clear_pointer (&self->options_ui, web_extension_options_ui_free);
   g_clear_pointer (&self->permissions, g_hash_table_unref);
   g_clear_pointer (&self->host_permissions, g_ptr_array_unref);
@@ -833,13 +836,13 @@ ephy_web_extension_parse_manifest (EphyWebExtension  *self,
 {
   g_autoptr (GError) local_error = NULL;
   g_autoptr (JsonParser) parser = NULL;
-  JsonObject *icons_object = NULL;
-  JsonArray *content_scripts_array = NULL;
-  JsonObject *background_object = NULL;
-  JsonNode *root = NULL;
-  JsonObject *root_object = NULL;
+  JsonNode *root;
+  JsonObject *root_object;
+  JsonObject *child_object;
+  JsonArray *child_array;
   gsize length = 0;
   const guchar *manifest;
+  g_autofree char *extension_basename = NULL;
   g_autofree char *local_storage_contents = NULL;
 
   manifest = ephy_web_extension_get_resource (self, "manifest.json", &length);
@@ -866,17 +869,27 @@ ephy_web_extension_parse_manifest (EphyWebExtension  *self,
     return FALSE;
   }
 
-  /* FIXME: Implement i18n: 
https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Internationalization#retrieving_localized_strings_in_manifests
 */
   self->manifest = g_strndup ((char *)manifest, length);
-  self->description = ephy_web_extension_manifest_get_key (self, root_object, "description");
-  self->manifest_version = json_object_get_int_member (root_object, "manifest_version");
-  self->name = ephy_web_extension_manifest_get_key (self, root_object, "name");
-  self->version = ephy_web_extension_manifest_get_key (self, root_object, "version");
-  self->homepage_url = ephy_web_extension_manifest_get_key (self, root_object, "homepage_url");
-  self->author = ephy_web_extension_manifest_get_key (self, root_object, "author");
+  self->manifest_version = ephy_json_object_get_int (root_object, "manifest_version");
+  if (self->manifest_version != 2) {
+    g_set_error (error, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_INVALID_MANIFEST, "Only manifest_version 2 
is supported");
+    return FALSE;
+  }
+
+  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->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");
+
+  if (!*self->version || !*self->name) {
+    g_set_error (error, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_INVALID_MANIFEST, "Missing name or 
version");
+    return FALSE;
+  }
 
+  extension_basename = g_path_get_basename (self->base_location);
   self->local_storage_path = g_build_filename (ephy_config_dir (), "web_extensions",
-                                               g_path_get_basename (self->base_location), 
"local-storage.json", NULL);
+                                               extension_basename, "local-storage.json", NULL);
 
   if (g_file_get_contents (self->local_storage_path, &local_storage_contents, NULL, NULL)) {
     self->local_storage = json_from_string (local_storage_contents, &local_error);
@@ -889,61 +902,29 @@ ephy_web_extension_parse_manifest (EphyWebExtension  *self,
   if (!self->local_storage)
     self->local_storage = json_node_init_object (json_node_alloc (), json_object_new ());
 
-  if (json_object_has_member (root_object, "icons")) {
-    icons_object = json_object_get_object_member (root_object, "icons");
-
-    json_object_foreach_member (icons_object, web_extension_add_icon, self);
-  }
-
-  if (json_object_has_member (root_object, "content_scripts")) {
-    content_scripts_array = json_object_get_array_member (root_object, "content_scripts");
-
-    json_array_foreach_element (content_scripts_array, web_extension_add_content_script, self);
-  }
-
-  if (json_object_has_member (root_object, "background")) {
-    background_object = json_object_get_object_member (root_object, "background");
-
-    json_object_foreach_member (background_object, web_extension_add_background, self);
-  }
-
-  if (json_object_has_member (root_object, "page_action")) {
-    JsonObject *page_action_object = json_object_get_object_member (root_object, "page_action");
-
-    web_extension_add_page_action (page_action_object, self);
-  }
-
-  if (json_object_has_member (root_object, "browser_action")) {
-    JsonObject *browser_action_object = json_object_get_object_member (root_object, "browser_action");
-
-    web_extension_add_browser_action (browser_action_object, self);
-  }
+  if ((child_object = ephy_json_object_get_object (root_object, "icons")))
+    json_object_foreach_member (child_object, web_extension_add_icon, self);
 
-  if (json_object_has_member (root_object, "options_ui")) {
-    JsonObject *browser_action_object = json_object_get_object_member (root_object, "options_ui");
+  if ((child_array = ephy_json_object_get_array (root_object, "content_scripts")))
+    json_array_foreach_element (child_array, web_extension_add_content_script, self);
 
-    web_extension_add_options_ui (browser_action_object, self);
-  }
+  if ((child_object = ephy_json_object_get_object (root_object, "background")))
+    web_extension_parse_background (self, child_object);
 
-  if (json_object_has_member (root_object, "permissions")) {
-    JsonArray *array = json_object_get_array_member (root_object, "permissions");
+  if ((child_object = ephy_json_object_get_object (root_object, "page_action")))
+    web_extension_parse_page_action (self, child_object);
 
-    json_array_foreach_element (array, web_extension_add_permission, self);
-  }
+  if ((child_object = ephy_json_object_get_object (root_object, "browser_action")))
+    web_extension_parse_browser_action (self, child_object);
 
-  if (json_object_has_member (root_object, "web_accessible_resources")) {
-    JsonNode *node;
-    JsonArray *array;
+  if ((child_object = ephy_json_object_get_object (root_object, "options_ui")))
+    web_extension_parse_options_ui (self, child_object);
 
-    node = json_object_get_member (root_object, "web_accessible_resources");
-    if (!JSON_NODE_HOLDS_ARRAY (node)) {
-      g_set_error (error, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_INVALID_MANIFEST, 
"web_accessible_resources is not an array");
-      return FALSE;
-    }
+  if ((child_array = ephy_json_object_get_array (root_object, "permissions")))
+    json_array_foreach_element (child_array, web_extension_add_permission, self);
 
-    array = json_node_get_array (node);
-    json_array_foreach_element (array, web_extension_add_web_accessible_resource, self);
-  }
+  if ((child_array = ephy_json_object_get_array (root_object, "web_accessible_resources")))
+    json_array_foreach_element (child_array, web_extension_add_web_accessible_resource, self);
 
   return TRUE;
 }
@@ -1184,13 +1165,13 @@ ephy_web_extension_has_browser_action (EphyWebExtension *self)
 gboolean
 ephy_web_extension_has_background_web_view (EphyWebExtension *self)
 {
-  return !!self->background;
+  return !!self->background_page;
 }
 
 const char *
 ephy_web_extension_background_web_view_get_page (EphyWebExtension *self)
 {
-  return self->background->page;
+  return self->background_page;
 }
 
 GList *
diff --git a/src/webextension/meson.build b/src/webextension/meson.build
index eb6bdc157..0ed2fceaf 100644
--- a/src/webextension/meson.build
+++ b/src/webextension/meson.build
@@ -9,6 +9,7 @@ ephywebextension_src = [
   'webextension/api/storage.c',
   'webextension/api/tabs.c',
   'webextension/api/windows.c',
+  'webextension/ephy-json-utils.c',
   'webextension/ephy-web-extension-manager.c',
   'webextension/ephy-web-extension.c',
 ]


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