[epiphany/pgriffis/web-extension/downloads] WebExtensions: Implement the downloads API
- From: Patrick Griffis <pgriffis src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [epiphany/pgriffis/web-extension/downloads] WebExtensions: Implement the downloads API
- Date: Mon, 20 Jun 2022 00:49:36 +0000 (UTC)
commit 5fcabb04e350344977aad8a7d737dd16104ea0ff
Author: Patrick Griffis <pgriffis igalia com>
Date: Sun Jun 19 15:48:02 2022 -0500
WebExtensions: Implement the downloads API
This is an 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
- Returned download lists aren't sorted
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..678733e02 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..4be48ea1a
--- /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;
+}
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..60ad98635
--- /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]