[epiphany/pgriffis/web-extension/downloads] WebExtensions: Implement the downloads API




commit aa5a2548dc1c16448b50cb6847aabbefd600b7f9
Author: Patrick Griffis <pgriffis igalia com>
Date:   Sun Jun 19 15:48:02 2022 -0500

    WebExtensions: Implement the downloads API
    
    This is a almost entire implementation of this API.
    
    Some limitations:
    
    - WebKit doesn't support pause/resume
    - WebKit doesn't inform us of writes, so we don't know mid-download the size on disk
    - Download information is persistent across sessions in other browsers, they are lost with Epiphany
    - I haven't fully mapped all GIO errors to WebExtension errors

 embed/ephy-download.c                              | 245 ++++++-
 embed/ephy-download.h                              |  23 +
 embed/ephy-downloads-manager.c                     |  16 +
 embed/ephy-downloads-manager.h                     |   3 +
 .../resources/js/webextensions.js                  |  18 +
 src/webextension/api/api-utils.c                   |  94 +++
 src/webextension/api/api-utils.h                   |  47 ++
 src/webextension/api/downloads.c                   | 809 +++++++++++++++++++++
 src/webextension/api/downloads.h                   |  39 +
 src/webextension/ephy-web-extension-manager.c      |  58 ++
 src/webextension/meson.build                       |   2 +
 11 files changed, 1344 insertions(+), 10 deletions(-)
---
diff --git a/embed/ephy-download.c b/embed/ephy-download.c
index b8f294dd2..d837756c1 100644
--- a/embed/ephy-download.c
+++ b/embed/ephy-download.c
@@ -42,13 +42,26 @@ struct _EphyDownload {
   WebKitDownload *download;
 
   char *content_type;
+  char *suggested_directory;
+  char *suggested_filename;
 
   gboolean show_notification;
+  gboolean always_ask_destination;
+  gboolean choose_filename;
 
   EphyDownloadActionType action;
   gboolean finished;
   GError *error;
   GFileMonitor *file_monitor;
+
+  guint64 uid;
+
+  char *initiated_by_extension_id;
+  char *initiated_by_extension_name;
+
+  GDateTime *start_time;
+  GDateTime *end_time;
+  gboolean was_moved;
 };
 
 G_DEFINE_TYPE (EphyDownload, ephy_download, G_TYPE_OBJECT)
@@ -74,6 +87,8 @@ enum {
 
 static guint signals[LAST_SIGNAL];
 
+static guint64 download_uid = 1;
+
 static void
 ephy_download_get_property (GObject    *object,
                             guint       property_id,
@@ -225,7 +240,7 @@ set_destination_uri_for_suggested_filename (EphyDownload *download,
   g_free (dest_name);
 
   /* Append (n) as needed. */
-  if (g_file_test (destination_filename, G_FILE_TEST_EXISTS)) {
+  if (!webkit_download_get_allow_overwrite (download->download) && g_file_test (destination_filename, 
G_FILE_TEST_EXISTS)) {
     int i = 1;
     const char *dot_pos;
     gssize position;
@@ -459,6 +474,12 @@ ephy_download_dispose (GObject *object)
   g_clear_object (&download->file_monitor);
   g_clear_error (&download->error);
   g_clear_pointer (&download->content_type, g_free);
+  g_clear_pointer (&download->suggested_filename, g_free);
+  g_clear_pointer (&download->suggested_directory, g_free);
+  g_clear_pointer (&download->start_time, g_date_time_unref);
+  g_clear_pointer (&download->end_time, g_date_time_unref);
+  g_clear_pointer (&download->initiated_by_extension_id, g_free);
+  g_clear_pointer (&download->initiated_by_extension_name, g_free);
 
   G_OBJECT_CLASS (ephy_download_parent_class)->dispose (object);
 }
@@ -587,17 +608,21 @@ ephy_download_init (EphyDownload *download)
   download->action = EPHY_DOWNLOAD_ACTION_NONE;
 
   download->show_notification = TRUE;
+
+  download->uid = download_uid++;
 }
 
 typedef struct {
   EphyDownload *download;
   WebKitDownload *webkit_download;
+  char *suggested_directory;
   char *suggested_filename;
   GtkWindow *dialog;
   GFile *directory;
   GtkLabel *directory_label;
   GMainLoop *nested_loop;
   gboolean result;
+  gboolean choose_filename;
 } SuggestedFilenameData;
 
 static void
@@ -649,11 +674,21 @@ filename_suggested_button_cb (GtkButton             *button,
 {
   GtkFileChooserNative *chooser;
 
-  chooser = gtk_file_chooser_native_new (_("Select a Directory"),
-                                         data->dialog,
-                                         GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER,
-                                         _("_Select"),
-                                         _("_Cancel"));
+  if (!data->choose_filename) {
+    chooser = gtk_file_chooser_native_new (_("Select a Directory"),
+                                          data->dialog,
+                                          GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER,
+                                          _("_Select"),
+                                          _("_Cancel"));
+  } else {
+    chooser = gtk_file_chooser_native_new (_("Select the Destination"),
+                                          data->dialog,
+                                          GTK_FILE_CHOOSER_ACTION_SAVE,
+                                          _("_Select"),
+                                          _("_Cancel"));
+    gtk_file_chooser_set_current_name (GTK_FILE_CHOOSER (chooser), data->suggested_filename);
+  }
+
   gtk_native_dialog_set_modal (GTK_NATIVE_DIALOG (chooser), TRUE);
 
   gtk_file_chooser_set_current_folder_file (GTK_FILE_CHOOSER (chooser),
@@ -754,12 +789,15 @@ run_download_confirmation_dialog (EphyDownload *download,
   data.webkit_download = webkit_download;
   data.suggested_filename = g_strdup (suggested_filename);
   data.dialog = GTK_WINDOW (dialog);
-  if (!directory_path || !directory_path[0])
+  if (download->suggested_directory)
+    data.directory = g_file_new_for_path (download->suggested_directory);
+  else if (!directory_path || !directory_path[0])
     data.directory = g_file_new_for_path (ephy_file_get_downloads_dir ());
   else
     data.directory = g_file_new_for_path (directory_path);
   data.directory_label = GTK_LABEL (button_label);
   data.nested_loop = g_main_loop_new (NULL, FALSE);
+  data.choose_filename = download->choose_filename;
   data.result = FALSE;
 
   display_name = ephy_file_get_display_name (data.directory);
@@ -810,9 +848,14 @@ download_response_changed_cb (WebKitDownload *wk_download,
 
 static gboolean
 download_decide_destination_cb (WebKitDownload *wk_download,
-                                const gchar    *suggested_filename,
+                                const gchar    *wk_suggestion,
                                 EphyDownload   *download)
 {
+  const char *suggested_filename = wk_suggestion;
+
+  if (download->suggested_filename)
+    suggested_filename = download->suggested_filename;
+
   if (webkit_download_get_destination (wk_download))
     return TRUE;
 
@@ -825,10 +868,11 @@ download_decide_destination_cb (WebKitDownload *wk_download,
     return TRUE;
 
   if (!ephy_is_running_inside_sandbox () &&
-      g_settings_get_boolean (EPHY_SETTINGS_WEB, EPHY_PREFS_WEB_ASK_ON_DOWNLOAD))
+      (g_settings_get_boolean (EPHY_SETTINGS_WEB, EPHY_PREFS_WEB_ASK_ON_DOWNLOAD) ||
+       download->always_ask_destination))
     return run_download_confirmation_dialog (download, suggested_filename);
 
-  return set_destination_uri_for_suggested_filename (download, NULL, suggested_filename);
+  return set_destination_uri_for_suggested_filename (download, download->suggested_directory, 
suggested_filename);
 }
 
 static void
@@ -839,6 +883,8 @@ download_created_destination_cb (WebKitDownload *wk_download,
   char *filename;
   char *content_type;
 
+  download->start_time = g_date_time_new_now_local ();
+
   if (download->content_type && !g_content_type_is_unknown (download->content_type))
     return;
 
@@ -916,6 +962,8 @@ download_file_monitor_changed (GFileMonitor      *monitor,
   if (strcmp (g_file_get_uri (file), webkit_download_get_destination (download->download)) != 0)
     return;
 
+  download->was_moved = TRUE;
+
   if (event_type == G_FILE_MONITOR_EVENT_DELETED || event_type == G_FILE_MONITOR_EVENT_MOVED)
     g_signal_emit (download, signals[MOVED], 0);
 }
@@ -928,6 +976,7 @@ download_finished_cb (WebKitDownload *wk_download,
   g_autoptr (GFile) file = NULL;
 
   download->finished = TRUE;
+  download->end_time = g_date_time_new_now_local ();
 
   ephy_download_do_download_action (download, download->action);
 
@@ -953,6 +1002,7 @@ download_failed_cb (WebKitDownload *wk_download,
 
   LOG ("error (%d - %d)! %s", error->code, 0, error->message);
   download->finished = TRUE;
+  download->end_time = g_date_time_new_now_local ();
   download->error = g_error_copy (error);
   g_signal_emit (download, signals[ERROR], 0, download->error);
 }
@@ -1053,3 +1103,178 @@ ephy_download_disable_desktop_notification (EphyDownload *download)
 
   download->show_notification = FALSE;
 }
+
+guint64
+ephy_download_get_uid (EphyDownload *download)
+{
+  g_assert (EPHY_IS_DOWNLOAD (download));
+
+  return download->uid;
+}
+
+/**
+ * ephy_download_set_always_ask_destination:
+ *
+ * Bypasses the global user preference for prompting for
+ * a save location. This does not bypass EphyDownload:destination
+ * being set.
+ */
+void
+ephy_download_set_always_ask_destination (EphyDownload *download,
+                                          gboolean      always_ask)
+{
+  g_assert (EPHY_IS_DOWNLOAD (download));
+
+  download->always_ask_destination = always_ask;
+}
+
+/**
+ * ephy_download_set_choose_filename:
+ *
+ * Changes the download prompt to select the destination
+ * filename rather than only the directory.
+ */
+void
+ephy_download_set_choose_filename (EphyDownload *download,
+                                   gboolean      choose_filename)
+{
+  g_assert (EPHY_IS_DOWNLOAD (download));
+
+  download->choose_filename = choose_filename;
+}
+
+/**
+ * ephy_download_set_suggested_destination:
+ * @suggested_directory: (nullable): Default download directory
+ * @suggested_filename: (nullable): Default filename
+ *
+ * This sets recommendations for the directory and filename of
+ * the download. If the directory does not exist it will be created.
+ * The filename will be sanitized and possibly renamed if needed.
+ *
+ * If @suggested_directory is %NULL the globally configured download
+ * directory is used.
+ *
+ * If @suggested_filename is %NULL the WebKit recommended filename
+ * is used.
+ *
+ * Note that this does not override EphyDownload:destination and only
+ * provides default suggestions.
+ */
+void
+ephy_download_set_suggested_destination (EphyDownload *download,
+                                         const char   *suggested_directory,
+                                         const char   *suggested_filename)
+{
+  g_assert (EPHY_IS_DOWNLOAD (download));
+
+  g_free (download->suggested_directory);
+  download->suggested_directory = g_strdup (suggested_directory);
+
+  g_free (download->suggested_filename);
+  download->suggested_filename = g_strdup (suggested_filename);
+}
+
+/**
+ * ephy_download_set_allow_overwrite:
+ *
+ * This allows the downloaded file to overwrite files on disk and
+ * also disables the automatic renaming (appending "(1)") when the file
+ * already exists.
+ */
+void
+ephy_download_set_allow_overwrite (EphyDownload *download,
+                                   gboolean      allow_overwrite)
+{
+  g_assert (EPHY_IS_DOWNLOAD (download));
+
+  webkit_download_set_allow_overwrite (download->download, allow_overwrite);
+}
+
+/**
+ * ephy_download_get_was_moved:
+ *
+ * Returns: %TRUE if Epiphany detected the file being moved or deleted
+ */
+gboolean
+ephy_download_get_was_moved (EphyDownload *download)
+{
+  g_assert (EPHY_IS_DOWNLOAD (download));
+
+  return download->was_moved;
+}
+
+/**
+ * ephy_download_get_start_time:
+ *
+ * Returns: (nullable): The time the download was started or %NULL
+ */
+GDateTime *
+ephy_download_get_start_time (EphyDownload *download)
+{
+  g_assert (EPHY_IS_DOWNLOAD (download));
+
+  return download->start_time;
+}
+
+/**
+ * ephy_download_get_end_time:
+ *
+ * Returns: (nullable): The time the download was completed/failed or %NULL if active
+ */
+GDateTime *
+ephy_download_get_end_time (EphyDownload *download)
+{
+  g_assert (EPHY_IS_DOWNLOAD (download));
+
+  return download->end_time;
+}
+
+/**
+ * ephy_download_get_initiating_web_extension_info:
+ * @download: The #EphyDownload
+ * @extension_id_out: (nullable): Place to store the extension ID
+ * @extension_name_out: (nullable): Place to store the extension name
+ *
+ * This returns information on which, if any, WebExtension created the download.
+ *
+ * Returns: %TRUE if web extension info exists
+ */
+gboolean
+ephy_download_get_initiating_web_extension_info (EphyDownload  *download,
+                                                 const char   **extension_id_out,
+                                                 const char   **extension_name_out)
+{
+  g_assert (EPHY_IS_DOWNLOAD (download));
+
+  if (extension_name_out)
+    *extension_name_out = download->initiated_by_extension_name ? download->initiated_by_extension_name : 
NULL;
+
+  if (extension_id_out)
+    *extension_id_out = download->initiated_by_extension_id ? download->initiated_by_extension_id : NULL;
+
+  return download->initiated_by_extension_name || download->initiated_by_extension_id;
+}
+
+/**
+ * ephy_download_set_initiating_web_extension_info:
+ * @download: The #EphyDownload
+ * @extension_id: (nullable): Extension ID
+ * @extension_name: (nullable): Extension name
+ *
+ * This sets that @download was created by a WebExtension. This information is exposed to
+ * other WebExtensions.
+ */
+void
+ephy_download_set_initiating_web_extension_info (EphyDownload *download,
+                                                 const char   *extension_id,
+                                                 const char   *extension_name)
+{
+  g_assert (EPHY_IS_DOWNLOAD (download));
+
+  g_free (download->initiated_by_extension_name);
+  download->initiated_by_extension_name = g_strdup (extension_name);
+
+  g_free (download->initiated_by_extension_id);
+  download->initiated_by_extension_id = g_strdup (extension_id);
+}
diff --git a/embed/ephy-download.h b/embed/ephy-download.h
index 1b6224439..4e153f9a5 100644
--- a/embed/ephy-download.h
+++ b/embed/ephy-download.h
@@ -61,5 +61,28 @@ gboolean      ephy_download_do_download_action    (EphyDownload          *downlo
                                                    EphyDownloadActionType action);
 void          ephy_download_disable_desktop_notification
                                                   (EphyDownload *download);
+guint64       ephy_download_get_uid               (EphyDownload *download);
 
+void          ephy_download_set_always_ask_destination
+                                                  (EphyDownload *download,
+                                                   gboolean      always_ask);
+void          ephy_download_set_choose_filename   (EphyDownload *download,
+                                                   gboolean      choose_filename);
+void          ephy_download_set_suggested_destination
+                                                  (EphyDownload *download,
+                                                   const char   *suggested_directory,
+                                                   const char   *suggested_filename);
+void          ephy_download_set_allow_overwrite   (EphyDownload *download,
+                                                   gboolean      allow_overwrite);
+gboolean      ephy_download_get_was_moved         (EphyDownload *download);
+GDateTime    *ephy_download_get_start_time        (EphyDownload *download);
+GDateTime    *ephy_download_get_end_time          (EphyDownload *download);
+gboolean      ephy_download_get_initiating_web_extension_info
+                                                  (EphyDownload  *download,
+                                                   const char   **extension_id_out,
+                                                   const char   **extension_name_out);
+void          ephy_download_set_initiating_web_extension_info
+                                                  (EphyDownload *download,
+                                                   const char   *extension_id,
+                                                   const char   *extension_name);
 G_END_DECLS
diff --git a/embed/ephy-downloads-manager.c b/embed/ephy-downloads-manager.c
index b9e4675e3..5c9580a7f 100644
--- a/embed/ephy-downloads-manager.c
+++ b/embed/ephy-downloads-manager.c
@@ -261,3 +261,19 @@ ephy_downloads_manager_get_estimated_progress (EphyDownloadsManager *manager)
 
   return n_active > 0 ? progress / n_active : 1;
 }
+
+EphyDownload *
+ephy_downloads_manager_find_download_by_id (EphyDownloadsManager *manager,
+                                            guint64               id)
+{
+  g_assert (EPHY_IS_DOWNLOADS_MANAGER (manager));
+
+  for (GList *l = manager->downloads; l; l = g_list_next (l)) {
+    EphyDownload *download = EPHY_DOWNLOAD (l->data);
+
+    if (ephy_download_get_uid (download) == id)
+      return download;
+  }
+
+  return NULL;
+}
diff --git a/embed/ephy-downloads-manager.h b/embed/ephy-downloads-manager.h
index c35662b41..7452af123 100644
--- a/embed/ephy-downloads-manager.h
+++ b/embed/ephy-downloads-manager.h
@@ -36,4 +36,7 @@ gboolean ephy_downloads_manager_has_active_downloads   (EphyDownloadsManager *ma
 GList   *ephy_downloads_manager_get_downloads          (EphyDownloadsManager *manager);
 gdouble  ephy_downloads_manager_get_estimated_progress (EphyDownloadsManager *manager);
 
+EphyDownload *ephy_downloads_manager_find_download_by_id (EphyDownloadsManager *manager,
+                                                          guint64               id);
+
 G_END_DECLS
diff --git a/embed/web-process-extension/resources/js/webextensions.js 
b/embed/web-process-extension/resources/js/webextensions.js
index 2a971b196..e437e18b3 100644
--- a/embed/web-process-extension/resources/js/webextensions.js
+++ b/embed/web-process-extension/resources/js/webextensions.js
@@ -133,3 +133,21 @@ window.browser.cookies = {
     // This is a stub as WebKitCookieManager::changed doesn't tell us enough information.
     onChanged: new EphyEventListener (),
 };
+
+window.browser.downloads = {
+    download: function (...args) { return ephy_message ('downloads.download', args); },
+    // FIXME: In the query object for search and erase, convert JavaScript Date objects to timestamps.
+    search: function (...args) { return ephy_message ('downloads.search', args); },
+    erase: function (...args) { return ephy_message ('downloads.erase', args); },
+    pause: function (...args) { return ephy_message ('downloads.pause', args); },
+    resume: function (...args) { return ephy_message ('downloads.resume', args); },
+    cancel: function (...args) { return ephy_message ('downloads.cancel', args); },
+    getFileIcon: function (...args) { return ephy_message ('downloads.getFileIcon', args); },
+    open: function (...args) { return ephy_message ('downloads.open', args); },
+    show: function (...args) { return ephy_message ('downloads.show', args); },
+    showDefaultFolder: function (...args) { return ephy_message ('downloads.showDefaultFolder', args); },
+    removeFile: function (...args) { return ephy_message ('downloads.removeFile', args); },
+    onCreated: new EphyEventListener (),
+    onErased: new EphyEventListener (),
+    onChanged: new EphyEventListener (),
+}
\ No newline at end of file
diff --git a/src/webextension/api/api-utils.c b/src/webextension/api/api-utils.c
new file mode 100644
index 000000000..8ab348f9c
--- /dev/null
+++ b/src/webextension/api/api-utils.c
@@ -0,0 +1,94 @@
+/* -*- 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 "api-utils.h"
+
+char *
+api_utils_get_string_property (JSCValue   *obj,
+                               const char *name,
+                               const char *default_value)
+{
+  g_autoptr (JSCValue) value = jsc_value_object_get_property (obj, name);
+
+  if (!jsc_value_is_string (value))
+    return g_strdup (default_value);
+
+  return jsc_value_to_string (value);
+}
+
+ApiTriStateValue
+api_utils_get_tri_state_value_property (JSCValue   *obj,
+                                        const char *name)
+{
+  g_autoptr (JSCValue) value = jsc_value_object_get_property (obj, name);
+
+  if (jsc_value_is_undefined (value))
+    return API_VALUE_UNSET;
+
+  return jsc_value_to_boolean (value);
+}
+
+gboolean
+api_utils_get_boolean_property (JSCValue   *obj,
+                                const char *name,
+                                gboolean    default_value)
+{
+  g_autoptr (JSCValue) value = jsc_value_object_get_property (obj, name);
+
+  g_assert (default_value == TRUE || default_value == FALSE);
+
+  if (jsc_value_is_undefined (value))
+    return default_value;
+
+  return jsc_value_to_boolean (value);
+}
+
+gint32
+api_utils_get_int32_property (JSCValue   *obj,
+                              const char *name,
+                              gint32      default_value)
+{
+  g_autoptr (JSCValue) value = jsc_value_object_get_property (obj, name);
+
+  if (!jsc_value_is_number (value))
+    return default_value;
+
+  return jsc_value_to_int32 (value);
+}
+
+GPtrArray *
+api_utils_get_string_array_property (JSCValue   *obj,
+                                     const char *name)
+{
+  g_autoptr (JSCValue) value = jsc_value_object_get_property (obj, name);
+  GPtrArray *strings = g_ptr_array_new_full (2, g_free);
+
+  if (!jsc_value_is_array (value))
+    return strings;
+
+  for (guint i = 0; ; i++) {
+    g_autoptr (JSCValue) indexed_value = jsc_value_object_get_property_at_index (value, i);
+    if (!jsc_value_is_string (indexed_value))
+      break;
+    g_ptr_array_add (strings, jsc_value_to_string (indexed_value));
+  }
+
+  return strings;
+}
\ No newline at end of file
diff --git a/src/webextension/api/api-utils.h b/src/webextension/api/api-utils.h
new file mode 100644
index 000000000..9b247e2cd
--- /dev/null
+++ b/src/webextension/api/api-utils.h
@@ -0,0 +1,47 @@
+/* -*- 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 <jsc/jsc.h>
+
+typedef enum {
+  API_VALUE_UNSET = -1,
+  API_VALUE_FALSE = 0,
+  API_VALUE_TRUE = 1,
+} ApiTriStateValue;
+
+char *                  api_utils_get_string_property                (JSCValue   *obj,
+                                                                      const char *name,
+                                                                      const char *default_value);
+
+gboolean                api_utils_get_boolean_property               (JSCValue   *obj,
+                                                                      const char *name,
+                                                                      gboolean    default_value);
+
+gint32                  api_utils_get_int32_property                 (JSCValue   *obj,
+                                                                      const char *name,
+                                                                      gint32      default_value);
+
+GPtrArray *             api_utils_get_string_array_property          (JSCValue   *obj,
+                                                                      const char *name);
+
+ApiTriStateValue        api_utils_get_tri_state_value_property       (JSCValue   *obj,
+                                                                      const char *name);
\ No newline at end of file
diff --git a/src/webextension/api/downloads.c b/src/webextension/api/downloads.c
new file mode 100644
index 000000000..7233fdc63
--- /dev/null
+++ b/src/webextension/api/downloads.c
@@ -0,0 +1,809 @@
+/* -*- 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-file-helpers.h"
+
+#include "api-utils.h"
+#include "downloads.h"
+
+static EphyDownloadsManager *
+get_downloads_manager (void)
+{
+  return ephy_embed_shell_get_downloads_manager (ephy_embed_shell_get_default ());
+}
+
+static void
+downloads_handler_download (EphyWebExtension *self,
+                            char             *name,
+                            JSCValue         *args,
+                            WebKitWebView    *web_view,
+                            GTask            *task)
+{
+  g_autoptr (JSCValue) options = jsc_value_object_get_property_at_index (args, 0);
+  EphyDownloadsManager *downloads_manager = get_downloads_manager ();
+  g_autoptr (EphyDownload) download = NULL;
+  g_autofree char *url = NULL;
+  g_autofree char *filename = NULL;
+  g_autofree char *suggested_filename = NULL;
+  g_autofree char *suggested_directory = NULL;
+  g_autofree char *conflict_action = NULL;
+
+  if (!jsc_value_is_object (options)) {
+    g_task_return_new_error (task, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_INVALID_ARGUMENT, 
"downloads.download(): Missing options object");
+    return;
+  }
+
+  url = api_utils_get_string_property (options, "url", NULL);
+  if (!url) {
+    g_task_return_new_error (task, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_INVALID_ARGUMENT, 
"downloads.download(): Missing url");
+    return;
+  }
+
+  filename = api_utils_get_string_property (options, "filename", NULL);
+  if (filename) {
+    g_autoptr (GFile) downloads_dir = g_file_new_for_path (ephy_file_get_downloads_dir ());
+    g_autoptr (GFile) destination = g_file_resolve_relative_path (downloads_dir, filename);
+    g_autoptr (GFile) parent_dir = g_file_get_parent (destination);
+
+    /* Relative paths are allowed however it cannot escape the parent directory. */
+    if (!g_file_has_prefix (destination, downloads_dir)) {
+      g_task_return_new_error (task, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_INVALID_ARGUMENT, 
"downloads.download(): Relative filename cannot contain escape parent directory");
+      return;
+    }
+
+    suggested_filename = g_file_get_basename (destination);
+    suggested_directory = g_file_get_path (parent_dir);
+  }
+
+  conflict_action = api_utils_get_string_property (options, "conflictAction", NULL);
+
+  download = ephy_download_new_for_uri (url);
+  ephy_download_set_allow_overwrite (download, g_strcmp0 (conflict_action, "overwrite") == 0);
+  ephy_download_set_choose_filename (download, TRUE);
+  ephy_download_set_suggested_destination (download, suggested_directory, suggested_filename);
+  ephy_download_set_always_ask_destination (download, api_utils_get_boolean_property (options, "saveAs", 
FALSE));
+  ephy_download_set_initiating_web_extension_info (download, ephy_web_extension_get_guid (self), 
ephy_web_extension_get_name (self));
+  ephy_downloads_manager_add_download (downloads_manager, download);
+
+  /* FIXME: We should wait to return until after the user has been prompted to error if they cancelled it. */
+
+  /* FIXME: The id is supposed to be persistent across sessions. */
+  g_task_return_pointer (task, g_strdup_printf ("%" G_GUINT64_FORMAT, ephy_download_get_uid (download)), 
g_free);
+}
+
+static char *
+downloads_handler_cancel (EphyWebExtension  *self,
+                          char              *name,
+                          JSCValue          *args,
+                          WebKitWebView     *web_view,
+                          GError           **error)
+{
+  g_autoptr (JSCValue) download_id = jsc_value_object_get_property_at_index (args, 0);
+  EphyDownloadsManager *downloads_manager = get_downloads_manager ();
+  EphyDownload *download;
+
+  if (!jsc_value_is_number (download_id)) {
+    g_set_error_literal (error, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_INVALID_ARGUMENT, 
"downloads.cancel(): Missing downloadId");
+    return NULL;
+  }
+
+  download = ephy_downloads_manager_find_download_by_id (downloads_manager, jsc_value_to_int32 
(download_id));
+  /* If we fail to find one its possible it was removed already. So instead of erroring just consider it a 
success. */
+  if (!download)
+    return NULL;
+
+  ephy_download_cancel (download);
+  return NULL;
+}
+
+static char *
+downloads_handler_open_or_show (EphyWebExtension  *self,
+                                char              *name,
+                                JSCValue          *args,
+                                WebKitWebView     *web_view,
+                                GError           **error)
+{
+  g_autoptr (JSCValue) download_id = jsc_value_object_get_property_at_index (args, 0);
+  EphyDownloadsManager *downloads_manager = get_downloads_manager ();
+  EphyDownloadActionType action;
+  EphyDownload *download;
+
+  /* We reuse this method for both downloads.open() and downloads.show() as they are identical other than 
the action. */
+
+  if (!jsc_value_is_number (download_id)) {
+    g_set_error (error, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_INVALID_ARGUMENT, "downloads.%s(): Missing 
downloadId", name);
+    return NULL;
+  }
+
+  download = ephy_downloads_manager_find_download_by_id (downloads_manager, jsc_value_to_int32 
(download_id));
+  if (!download) {
+    g_set_error (error, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_INVALID_ARGUMENT, "downloads.%s(): Failed 
to find downloadId", name);
+    return NULL;
+  }
+
+  if (strcmp (name, "open") == 0)
+    action = EPHY_DOWNLOAD_ACTION_OPEN;
+  else
+    action = EPHY_DOWNLOAD_ACTION_BROWSE_TO;
+
+  if (!ephy_download_do_download_action (download, action)) {
+    g_set_error (error, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_INVALID_ARGUMENT, "downloads.%s(): Failed 
to %s download", name, name);
+    return NULL;
+  }
+
+  return NULL;
+}
+
+static GDateTime *
+get_download_time_property (JSCValue   *obj,
+                            const char *name)
+{
+  /* https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/downloads/DownloadTime */
+  g_autoptr (JSCValue) value = jsc_value_object_get_property (obj, name);
+
+  if (jsc_value_is_string (value)) {
+    g_autofree char *string = jsc_value_to_string (value);
+    char *end = NULL;
+    guint64 timestamp;
+
+    /* This can be a number thats a timestamp. */
+    timestamp = g_ascii_strtoull (string, &end, 10);
+    if ((gsize)(end - string) == strlen (string))
+      return g_date_time_new_from_unix_local (timestamp);
+
+    return g_date_time_new_from_iso8601 (string, NULL);
+  }
+
+  if (jsc_value_is_number (value)) {
+    gint32 timestamp = jsc_value_to_int32 (value);
+    return g_date_time_new_from_unix_local (timestamp);
+  }
+
+  return NULL;
+}
+
+typedef enum {
+  DOWNLOAD_STATE_ANY,
+  DOWNLOAD_STATE_IN_PROGRESS,
+  DOWNLOAD_STATE_INTERRUPTED,
+  DOWNLOAD_STATE_COMPLETE,
+} DownloadState;
+
+typedef struct {
+  GPtrArray *query;
+  GPtrArray *order_by;
+  GDateTime *start_time;
+  GDateTime *started_before;
+  GDateTime *started_after;
+  GDateTime *end_time;
+  GDateTime *ended_before;
+  GDateTime *ended_after;
+  char *filename_regex;
+  char *url_regex;
+  char *filename;
+  char *url;
+  char *content_type;
+  char *interrupt_reason;
+  gint32 limit;
+  gint32 id;
+  gint32 bytes_received;
+  gint32 total_bytes;
+  gint32 file_size;
+  gint32 total_bytes_greater;
+  gint32 total_bytes_less;
+  DownloadState state;
+  ApiTriStateValue paused;
+  ApiTriStateValue exists;
+  ApiTriStateValue dangerous_only;
+} DownloadQuery;
+
+static void
+download_query_free (DownloadQuery *query)
+{
+  g_clear_pointer (&query->start_time, g_date_time_unref);
+  g_clear_pointer (&query->started_before, g_date_time_unref);
+  g_clear_pointer (&query->started_after, g_date_time_unref);
+  g_clear_pointer (&query->end_time, g_date_time_unref);
+  g_clear_pointer (&query->ended_before, g_date_time_unref);
+  g_clear_pointer (&query->ended_after, g_date_time_unref);
+  g_ptr_array_free (query->query, TRUE);
+  g_ptr_array_free (query->order_by, TRUE);
+  g_free (query->filename);
+  g_free (query->filename_regex);
+  g_free (query->url);
+  g_free (query->url_regex);
+  g_free (query->interrupt_reason);
+  g_free (query->content_type);
+  g_free (query);
+}
+
+static DownloadQuery *
+download_query_new (JSCValue *object)
+{
+  DownloadQuery *query = g_new (DownloadQuery, 1);
+  g_autofree char *danger = NULL;
+  g_autofree char *state = NULL;
+  g_autofree char *mime = NULL;
+
+  query->filename = api_utils_get_string_property (object, "filename", NULL);
+  query->filename_regex = api_utils_get_string_property (object, "filenameRegex", NULL);
+  query->url = api_utils_get_string_property (object, "url", NULL);
+  query->url_regex = api_utils_get_string_property (object, "urlRegex", NULL);
+  query->interrupt_reason = api_utils_get_string_property (object, "error", NULL);
+  mime = api_utils_get_string_property (object, "mime", NULL);
+  query->content_type = mime ? g_content_type_from_mime_type (mime) : NULL;
+
+  query->total_bytes_greater = api_utils_get_int32_property (object, "totalBytesGreater", -1);
+  query->total_bytes_less = api_utils_get_int32_property (object, "totalBytesLess", -1);
+  query->limit = api_utils_get_int32_property (object, "limit", -1);
+  query->bytes_received = api_utils_get_int32_property (object, "bytesReceived", -1);
+  query->total_bytes = api_utils_get_int32_property (object, "totalBytes", -1);
+  query->file_size = api_utils_get_int32_property (object, "fileSize", -1);
+  query->id = api_utils_get_int32_property (object, "id", -1);
+
+  query->start_time = get_download_time_property (object, "startTime");
+  query->started_before = get_download_time_property (object, "startedBefore");
+  query->started_after = get_download_time_property (object, "startedAfter");
+  query->end_time = get_download_time_property (object, "endTime");
+  query->ended_before = get_download_time_property (object, "endedBefore");
+  query->ended_after = get_download_time_property (object, "endedAfter");
+
+  query->query = api_utils_get_string_array_property (object, "query");
+  query->order_by = api_utils_get_string_array_property (object, "orderBy");
+
+  query->paused = api_utils_get_tri_state_value_property (object, "paused");
+  query->exists = api_utils_get_tri_state_value_property (object, "exists");
+
+  /* Epiphany doesn't detect dangerous files so we only care if the query wanted to
+   * filter out *safe* files. */
+  danger = api_utils_get_string_property (object, "danger", NULL);
+  query->dangerous_only = danger ? strcmp (danger, "safe") != 0 : API_VALUE_UNSET;
+
+  query->state = DOWNLOAD_STATE_ANY;
+  state = api_utils_get_string_property (object, "state", NULL);
+  if (state) {
+    if (strcmp (state, "in_progress") == 0)
+      query->state = DOWNLOAD_STATE_IN_PROGRESS;
+    else if (strcmp (state, "interrupted") == 0)
+      query->state = DOWNLOAD_STATE_INTERRUPTED;
+    else if (strcmp (state, "complete") == 0)
+      query->state = DOWNLOAD_STATE_COMPLETE;
+  }
+
+  return query;
+}
+
+static char *
+download_get_filename (EphyDownload *download)
+{
+  const char *destination_uri = ephy_download_get_destination_uri (download);
+  g_autoptr (GFile) dest_file = NULL;
+
+  if (!destination_uri)
+    return NULL;
+
+  dest_file = g_file_new_for_uri (destination_uri);
+  return g_file_get_path (dest_file);
+}
+
+static const char *
+download_get_url (EphyDownload *download)
+{
+  WebKitDownload *wk_dl = ephy_download_get_webkit_download (download);
+  WebKitURIRequest *request = webkit_download_get_request (wk_dl);
+  return webkit_uri_request_get_uri (request);
+}
+
+static guint64
+download_get_received_size (EphyDownload *download)
+{
+  WebKitDownload *wk_dl = ephy_download_get_webkit_download (download);
+  return webkit_download_get_received_data_length (wk_dl);
+}
+
+static gboolean
+regex_matches (JSCContext *context,
+               const char *regex,
+               const char *string)
+{
+  /* WebExtensions can include arbitrary regex; To match expectations we need to run this against
+   * the JavaScript implementation of regex rather than PCREs.
+   * Note that this is absolutely untrusted code, however @context is private to this single API call
+   * (created in content_scripts_handle_user_message()) so they cannot actually do anything except
+   * make this match succeed or fail. */
+  /* FIXME: Maybe this can use `jsc_value_constructor_call()` and `jsc_context_evaluate_in_object()` instead 
of
+   * printf to avoid quotes potentially conflicting. */
+  g_autofree char *code = g_strdup_printf ("let re = new RegExp('%s'); re.test('%s');", regex, string);
+  g_autoptr (JSCValue) ret = jsc_context_evaluate (context, code, -1);
+  return jsc_value_to_boolean (ret);
+}
+
+static gboolean
+matches_filename_or_url (EphyDownload  *download,
+                         DownloadQuery *query,
+                         JSCContext    *context)
+{
+  g_autofree char *filename = download_get_filename (download);
+  const char *url = download_get_url (download);
+
+  /* query contains a list of strings that must be in either the URL or the filename.
+   * They may also be prefixed with `-` to require negative matches. */
+  for (guint i = 0; i < query->query->len; i++) {
+    const char *string = g_ptr_array_index (query->query, i);
+    if (*string == '-') {
+      if (strstr (url, string + 1) || strstr (filename, string + 1))
+        return FALSE;
+    } else {
+      if (!strstr (url, string) && !strstr (filename, string))
+        return FALSE;
+    }
+  }
+
+  if (query->filename && g_strcmp0 (query->filename, filename))
+    return FALSE;
+
+  if (query->url && g_strcmp0 (query->url, url))
+    return FALSE;
+
+  if (query->url_regex && !regex_matches (context, query->url_regex, url))
+    return FALSE;
+
+  if (query->filename_regex && !regex_matches (context, query->filename_regex, filename))
+    return FALSE;
+
+  return TRUE;
+}
+
+static gboolean
+matches_times (EphyDownload  *download,
+               DownloadQuery *query)
+{
+  GDateTime *start_time = ephy_download_get_start_time (download);
+  GDateTime *end_time = ephy_download_get_end_time (download);
+
+  if (start_time) {
+    if (query->start_time && g_date_time_compare (query->start_time, start_time) != 0)
+      return FALSE;
+
+    if (query->started_after && g_date_time_compare (query->started_after, start_time) >= 0)
+      return FALSE;
+
+    if (query->started_before && g_date_time_compare (query->started_before, start_time) <= 0)
+      return FALSE;
+  }
+
+  if (end_time) {
+    if (query->end_time && g_date_time_compare (query->end_time, end_time) != 0)
+      return FALSE;
+
+    if (query->ended_after && g_date_time_compare (query->ended_after, end_time) >= 0)
+      return FALSE;
+
+    if (query->ended_before && g_date_time_compare (query->ended_before, end_time) <= 0)
+      return FALSE;
+  }
+
+  return TRUE;
+}
+
+
+static gboolean
+match_error_to_interrupt_reason (GError     *error,
+                                 const char *interrupt_reason)
+{
+  /* https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/downloads/InterruptReason */
+  if (strcmp (interrupt_reason, "USER_CANCELED") == 0)
+    return g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED);
+
+  /* TODO: For now be very liberal, need to track all of these down. */
+  return TRUE;
+}
+
+static const char *
+error_to_interrupt_reason (GError *error)
+{
+  if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+    return "USER_CANCELLED";
+  /* TODO */
+  return "FILE_FAILED";
+}
+
+static int
+order_downloads (EphyDownload *d1,
+                 EphyDownload *d2,
+                 GPtrArray    *order_by)
+{
+  /* TODO: Implement this...
+     An array of strings representing DownloadItem properties the search results should be sorted by.
+     For example, including startTime then totalBytes in the array would sort the DownloadItems by their 
start time, then total bytes — in ascending order.
+     To specify sorting by a property in descending order, prefix it with a hyphen, for example -startTime.
+   */
+
+  return 0;
+}
+
+static GList *
+filter_downloads (GList         *downloads,
+                  DownloadQuery *query,
+                  JSCContext    *context)
+{
+  GList *matches = NULL;
+  GList *extras = NULL;
+
+  for (GList *l = downloads; l; l = g_list_next (l)) {
+    EphyDownload *dl = l->data;
+    guint64 received_size = download_get_received_size (dl);
+
+    if (query->id != -1 && ephy_download_get_uid (dl) != (guint64)query->id)
+      continue;
+
+    if (query->dangerous_only == API_VALUE_TRUE)
+      continue; /* We don't track dangerous files. */
+
+    if (query->content_type && !g_content_type_equals (ephy_download_get_content_type (dl), 
query->content_type))
+      continue;
+
+    if (query->paused == API_VALUE_TRUE)
+      continue; /* We don't support pausing. */
+
+    if (query->exists != API_VALUE_UNSET && query->exists == ephy_download_get_was_moved (dl))
+      continue;
+
+    if (query->state != DOWNLOAD_STATE_ANY) {
+      if (query->state == DOWNLOAD_STATE_IN_PROGRESS && !ephy_download_is_active (dl))
+        continue;
+      if (query->state == DOWNLOAD_STATE_INTERRUPTED && !ephy_download_failed (dl, NULL))
+        continue;
+      if (query->state == DOWNLOAD_STATE_COMPLETE && !ephy_download_succeeded (dl))
+        continue;
+    }
+
+    if (query->bytes_received != -1 && (guint64)query->bytes_received != received_size)
+      continue;
+
+    /* This represents the file size on disk so far. We don't have easy access to this so
+     * for now just treat it as bytes_received. */
+    if (query->total_bytes != -1 && (guint64)query->total_bytes != received_size)
+      continue;
+
+    if (query->total_bytes_greater != -1 && (guint64)query->total_bytes_greater > received_size)
+      continue;
+
+    if (query->total_bytes_less != -1 && (guint64)query->total_bytes_less > received_size)
+      continue;
+
+    if (!matches_filename_or_url (dl, query, context))
+      continue;
+
+    if (!matches_times (dl, query))
+      continue;
+
+    if (query->interrupt_reason) {
+      g_autoptr (GError) error = NULL;
+      if (!ephy_download_failed (dl, &error))
+        continue;
+
+      if (!match_error_to_interrupt_reason (error, query->interrupt_reason))
+        continue;
+    }
+
+    /* TODO: Handle file_size */
+
+    matches = g_list_append (matches, dl);
+  }
+
+  if (query->order_by->len)
+    matches = g_list_sort_with_data (matches, (GCompareDataFunc)order_downloads, query->order_by);
+
+  if (query->limit) {
+    extras = g_list_nth (matches, query->limit + 1);
+    if (extras) {
+      extras = g_list_remove_link (matches, extras);
+      g_list_free (extras);
+    }
+  }
+
+  return matches;
+}
+
+static void
+add_download_to_json (JsonBuilder  *builder,
+                      EphyDownload *download)
+{
+  GDateTime *end_time, *start_time;
+  g_autofree char *end_time_iso8601 = NULL;
+  g_autofree char *start_time_iso8601 = NULL;
+  const char *content_type;
+  g_autofree char *mime_type = NULL;
+  g_autofree char *filename = download_get_filename (download);
+  g_autoptr (GError) error = NULL;
+  const char *extension_id;
+  const char *extension_name;
+
+  if ((start_time = ephy_download_get_start_time (download)))
+    start_time_iso8601 = g_date_time_format_iso8601 (start_time);
+  if ((end_time = ephy_download_get_end_time (download)))
+    end_time_iso8601 = g_date_time_format_iso8601 (end_time);
+
+  content_type = ephy_download_get_content_type (download);
+  if (content_type)
+    mime_type = g_content_type_get_mime_type (content_type);
+
+  json_builder_begin_object (builder);
+  json_builder_set_member_name (builder, "id");
+  json_builder_add_int_value (builder, ephy_download_get_uid (download));
+  json_builder_set_member_name (builder, "canResume");
+  json_builder_add_boolean_value (builder, FALSE);
+  json_builder_set_member_name (builder, "incognito");
+  json_builder_add_boolean_value (builder, TRUE); /* We never remember downloads. */
+  json_builder_set_member_name (builder, "exists");
+  json_builder_add_boolean_value (builder, !ephy_download_get_was_moved (download));
+  json_builder_set_member_name (builder, "danger");
+  json_builder_add_string_value (builder, "safe");
+  json_builder_set_member_name (builder, "url");
+  json_builder_add_string_value (builder, download_get_url (download));
+  json_builder_set_member_name (builder, "state");
+  if (ephy_download_is_active (download))
+    json_builder_add_string_value (builder, "in_progress");
+  else if (ephy_download_failed (download, NULL))
+    json_builder_add_string_value (builder, "interrupted");
+  else
+    json_builder_add_string_value (builder, "complete");
+  if (mime_type) {
+    json_builder_set_member_name (builder, "mime");
+    json_builder_add_string_value (builder, mime_type);
+  }
+  json_builder_set_member_name (builder, "paused");
+  json_builder_add_boolean_value (builder, FALSE);
+  json_builder_set_member_name (builder, "filename");
+  json_builder_add_string_value (builder, filename);
+  if (start_time_iso8601) {
+    json_builder_set_member_name (builder, "startTime");
+    json_builder_add_string_value (builder, start_time_iso8601);
+  }
+  if (end_time_iso8601) {
+    json_builder_set_member_name (builder, "endTime");
+    json_builder_add_string_value (builder, end_time_iso8601);
+  }
+  json_builder_set_member_name (builder, "bytesReceived");
+  json_builder_add_int_value (builder, (gint32)download_get_received_size (download));
+  json_builder_set_member_name (builder, "totalBytes");
+  json_builder_add_int_value (builder, -1);
+  json_builder_set_member_name (builder, "fileSize");
+  json_builder_add_int_value (builder, -1);
+  if (ephy_download_failed (download, &error)) {
+    json_builder_set_member_name (builder, "error");
+    json_builder_add_string_value (builder, error_to_interrupt_reason (error));
+  }
+  if (ephy_download_get_initiating_web_extension_info (download, &extension_id, &extension_name)) {
+    json_builder_set_member_name (builder, "byExtensionId");
+    json_builder_add_string_value (builder, extension_id);
+    json_builder_set_member_name (builder, "byExtensionName");
+    json_builder_add_string_value (builder, extension_name);
+  }
+  json_builder_end_object (builder);
+}
+
+char *
+ephy_web_extension_api_downloads_download_to_json (EphyDownload *download)
+{
+  g_autoptr (JsonBuilder) builder = json_builder_new ();
+  g_autoptr (JsonNode) root = NULL;
+
+  add_download_to_json (builder, download);
+  root = json_builder_get_root (builder);
+
+  return json_to_string (root, FALSE);
+}
+
+static char *
+downloads_handler_search (EphyWebExtension  *self,
+                          char              *name,
+                          JSCValue          *args,
+                          WebKitWebView     *web_view,
+                          GError           **error)
+{
+  g_autoptr (JSCValue) query_object = jsc_value_object_get_property_at_index (args, 0);
+  EphyDownloadsManager *downloads_manager = get_downloads_manager ();
+  g_autoptr (JsonBuilder) builder = json_builder_new ();
+  g_autoptr (JsonNode) root = NULL;
+  DownloadQuery *query;
+  GList *downloads;
+
+  if (!jsc_value_is_object (query_object)) {
+    g_set_error (error, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_INVALID_ARGUMENT, "downloads.query(): 
Missing query");
+    return NULL;
+  }
+
+  query = download_query_new (query_object);
+  downloads = filter_downloads (ephy_downloads_manager_get_downloads (downloads_manager), query, 
jsc_value_get_context (args));
+  download_query_free (query);
+
+  json_builder_begin_array (builder);
+  for (GList *l = downloads; l; l = g_list_next (l))
+    add_download_to_json (builder, l->data);
+  json_builder_end_array (builder);
+
+  root = json_builder_get_root (builder);
+  return json_to_string (root, FALSE);
+}
+
+static char *
+downloads_handler_erase (EphyWebExtension  *self,
+                         char              *name,
+                         JSCValue          *args,
+                         WebKitWebView     *web_view,
+                         GError           **error)
+{
+  g_autoptr (JSCValue) query_object = jsc_value_object_get_property_at_index (args, 0);
+  EphyDownloadsManager *downloads_manager = get_downloads_manager ();
+  g_autoptr (JsonBuilder) builder = json_builder_new ();
+  g_autoptr (JsonNode) root = NULL;
+  DownloadQuery *query;
+  GList *downloads;
+
+  if (!jsc_value_is_object (query_object)) {
+    g_set_error (error, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_INVALID_ARGUMENT, "downloads.erase(): 
Missing query");
+    return NULL;
+  }
+
+  query = download_query_new (query_object);
+  downloads = filter_downloads (ephy_downloads_manager_get_downloads (downloads_manager), query, 
jsc_value_get_context (args));
+  download_query_free (query);
+
+  json_builder_begin_array (builder);
+  for (GList *l = downloads; l; l = g_list_next (l)) {
+    EphyDownload *download = l->data;
+
+    json_builder_add_int_value (builder, ephy_download_get_uid (download));
+    ephy_downloads_manager_remove_download (downloads_manager, download);
+  }
+  json_builder_end_array (builder);
+
+  root = json_builder_get_root (builder);
+  return json_to_string (root, FALSE);
+}
+
+static char *
+downloads_handler_showdefaultfolder (EphyWebExtension  *self,
+                                     char              *name,
+                                     JSCValue          *args,
+                                     WebKitWebView     *web_view,
+                                     GError           **error)
+{
+  g_autoptr (GFile) default_folder = g_file_new_for_path (ephy_file_get_downloads_dir ());
+  ephy_file_browse_to (default_folder);
+  return NULL;
+}
+
+static void
+delete_file_ready_cb (GFile        *file,
+                      GAsyncResult *result,
+                      GTask        *task)
+{
+  g_autoptr (GError) error = NULL;
+
+  g_file_delete_finish (file, result, &error);
+
+  /* The file not existing sounds like a success. */
+  if (error && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) {
+    g_task_return_error (task, g_steal_pointer (&error));
+    return;
+  }
+
+  g_task_return_pointer (task, NULL, NULL);
+}
+
+static void
+downloads_handler_removefile (EphyWebExtension *self,
+                              char             *name,
+                              JSCValue         *args,
+                              WebKitWebView    *web_view,
+                              GTask            *task)
+{
+  g_autoptr (JSCValue) download_id = jsc_value_object_get_property_at_index (args, 0);
+  EphyDownloadsManager *downloads_manager = get_downloads_manager ();
+  const char *destination_uri;
+  g_autoptr (GFile) destination_file = NULL;
+  EphyDownload *download;
+
+  if (!jsc_value_is_number (download_id)) {
+    g_task_return_new_error (task, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_INVALID_ARGUMENT, 
"downloads.removeFile(): Missing downloadId");
+    return;
+  }
+
+  download = ephy_downloads_manager_find_download_by_id (downloads_manager, jsc_value_to_int32 
(download_id));
+  if (!download) {
+    g_task_return_new_error (task, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_INVALID_ARGUMENT, 
"downloads.removeFile(): Failed to find downloadId");
+    return;
+  }
+
+  /* Ensure the download isn't active. */
+  ephy_download_cancel (download);
+
+  destination_uri = ephy_download_get_destination_uri (download);
+  /* If a destination was never chosen this was never written to disk. */
+  if (!destination_uri) {
+    g_task_return_pointer (task, NULL, NULL);
+    return;
+  }
+
+  destination_file = g_file_new_for_uri (destination_uri);
+  g_file_delete_async (destination_file, G_PRIORITY_DEFAULT, NULL, 
(GAsyncReadyCallback)delete_file_ready_cb, task);
+}
+
+static EphyWebExtensionSyncApiHandler downloads_sync_handlers[] = {
+  {"cancel", downloads_handler_cancel},
+  {"open", downloads_handler_open_or_show},
+  {"show", downloads_handler_open_or_show},
+  {"showDefaultFolder", downloads_handler_showdefaultfolder},
+  {"search", downloads_handler_search},
+  {"erase", downloads_handler_erase},
+};
+
+static EphyWebExtensionAsyncApiHandler downloads_async_handlers[] = {
+  {"download", downloads_handler_download},
+  {"removeFile", downloads_handler_removefile},
+};
+
+void
+ephy_web_extension_api_downloads_handler (EphyWebExtension *self,
+                                          char             *name,
+                                          JSCValue         *args,
+                                          WebKitWebView    *web_view,
+                                          GTask            *task)
+{
+  g_autoptr (GError) error = NULL;
+
+  if (!ephy_web_extension_has_permission (self, "downloads")) {
+    g_warning ("Extension %s tried to use downloads without permission.", ephy_web_extension_get_name 
(self));
+    g_task_return_new_error (task, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_PERMISSION_DENIED, "downloads: 
Permission Denied");
+    return;
+  }
+
+  for (guint idx = 0; idx < G_N_ELEMENTS (downloads_async_handlers); idx++) {
+    EphyWebExtensionAsyncApiHandler handler = downloads_async_handlers[idx];
+
+    if (g_strcmp0 (handler.name, name) == 0) {
+      handler.execute (self, name, args, web_view, task);
+      return;
+    }
+  }
+
+  for (guint idx = 0; idx < G_N_ELEMENTS (downloads_sync_handlers); idx++) {
+    EphyWebExtensionSyncApiHandler handler = downloads_sync_handlers[idx];
+    char *ret;
+
+    if (g_strcmp0 (handler.name, name) == 0) {
+      ret = handler.execute (self, name, args, web_view, &error);
+
+      if (error)
+        g_task_return_error (task, g_steal_pointer (&error));
+      else
+        g_task_return_pointer (task, ret, g_free);
+
+      return;
+    }
+  }
+
+  g_task_return_new_error (task, WEB_EXTENSION_ERROR, WEB_EXTENSION_ERROR_NOT_IMPLEMENTED, "downloads.%s(): 
Not Implemented", name);
+}
diff --git a/src/webextension/api/downloads.h b/src/webextension/api/downloads.h
new file mode 100644
index 000000000..85a0e6193
--- /dev/null
+++ b/src/webextension/api/downloads.h
@@ -0,0 +1,39 @@
+/* -*- 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 "ephy-download.h"
+#include "ephy-web-extension.h"
+
+#include <webkit2/webkit2.h>
+
+G_BEGIN_DECLS
+
+void ephy_web_extension_api_downloads_handler (EphyWebExtension *self,
+                                               char             *name,
+                                               JSCValue         *value,
+                                               WebKitWebView    *web_view,
+                                               GTask            *task);
+
+char  *ephy_web_extension_api_downloads_download_to_json (EphyDownload *download);
+
+G_END_DECLS
diff --git a/src/webextension/ephy-web-extension-manager.c b/src/webextension/ephy-web-extension-manager.c
index 9af1d7c97..34973f1bf 100644
--- a/src/webextension/ephy-web-extension-manager.c
+++ b/src/webextension/ephy-web-extension-manager.c
@@ -37,6 +37,7 @@
 
 #include "api/alarms.h"
 #include "api/cookies.h"
+#include "api/downloads.h"
 #include "api/notifications.h"
 #include "api/pageaction.h"
 #include "api/runtime.h"
@@ -68,6 +69,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},
+  {"downloads", ephy_web_extension_api_downloads_handler},
   {"notifications", ephy_web_extension_api_notifications_handler},
   {"pageAction", ephy_web_extension_api_pageaction_handler},
   {"runtime", ephy_web_extension_api_runtime_handler},
@@ -208,6 +210,57 @@ destroy_widget_list (GSList *widget_list)
   g_slist_free_full (widget_list, (GDestroyNotify)gtk_widget_destroy);
 }
 
+static void
+download_added_cb (EphyDownloadsManager    *downloads_manager,
+                   EphyDownload            *download,
+                   EphyWebExtensionManager *manager)
+{
+  for (GList *l = manager->web_extensions; l; l = g_list_next (l)) {
+    EphyWebExtension *extension = l->data;
+    g_autofree char *json = NULL;
+
+    if (!ephy_web_extension_has_permission (extension, "downloads"))
+      continue;
+
+    json = ephy_web_extension_api_downloads_download_to_json (download);
+    ephy_web_extension_manager_emit_in_extension_views (manager, extension, "downloads.onCreated", json);
+  }
+}
+
+static void
+download_completed_cb (EphyDownloadsManager    *downloads_manager,
+                       EphyDownload            *download,
+                       EphyWebExtensionManager *manager)
+{
+  for (GList *l = manager->web_extensions; l; l = g_list_next (l)) {
+    EphyWebExtension *extension = l->data;
+    g_autofree char *json = NULL;
+
+    if (!ephy_web_extension_has_permission (extension, "downloads"))
+      continue;
+
+    json = ephy_web_extension_api_downloads_download_to_json (download);
+    ephy_web_extension_manager_emit_in_extension_views (manager, extension, "downloads.onChanged", json);
+  }
+}
+
+static void
+download_removed_cb (EphyDownloadsManager    *downloads_manager,
+                     EphyDownload            *download,
+                     EphyWebExtensionManager *manager)
+{
+  for (GList *l = manager->web_extensions; l; l = g_list_next (l)) {
+    EphyWebExtension *extension = l->data;
+    g_autofree char *json = NULL;
+
+    if (!ephy_web_extension_has_permission (extension, "downloads"))
+      continue;
+
+    json = g_strdup_printf ("%" G_GUINT64_FORMAT, ephy_download_get_uid (download));
+    ephy_web_extension_manager_emit_in_extension_views (manager, extension, "downloads.onErased", json);
+  }
+}
+
 static void
 ephy_web_extension_manager_constructed (GObject *object)
 {
@@ -255,12 +308,17 @@ ephy_web_extension_manager_class_init (EphyWebExtensionManagerClass *klass)
 static void
 ephy_web_extension_manager_init (EphyWebExtensionManager *self)
 {
+  EphyDownloadsManager *downloads_manager = ephy_embed_shell_get_downloads_manager 
(ephy_embed_shell_get_default ());
   WebKitWebContext *web_context;
 
   web_context = ephy_embed_shell_get_web_context (ephy_embed_shell_get_default ());
   webkit_web_context_register_uri_scheme (web_context, "ephy-webextension", 
main_context_web_extension_scheme_cb, self, NULL);
   webkit_security_manager_register_uri_scheme_as_secure (webkit_web_context_get_security_manager 
(web_context),
                                                          "ephy-webextension");
+
+  g_signal_connect (downloads_manager, "download-added", G_CALLBACK (download_added_cb), self);
+  g_signal_connect (downloads_manager, "download-completed", G_CALLBACK (download_completed_cb), self);
+  g_signal_connect (downloads_manager, "download-removed", G_CALLBACK (download_removed_cb), self);
 }
 
 EphyWebExtensionManager *
diff --git a/src/webextension/meson.build b/src/webextension/meson.build
index ae9cedce0..eb6bdc157 100644
--- a/src/webextension/meson.build
+++ b/src/webextension/meson.build
@@ -1,6 +1,8 @@
 ephywebextension_src = [
   'webextension/api/alarms.c',
+  'webextension/api/api-utils.c',
   'webextension/api/cookies.c',
+  'webextension/api/downloads.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]