*
* Copyright 2026 Alexander Vanhee
*
* This program 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.
*
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#include "config.h"
#include <bzvala.h>
#include <glib/gi18n.h>
#include "bz-application.h"
#include "bz-flatpak-entry.h"
#include "bz-state-info.h"
#include "bz-transaction-dialog.h"
#include "env.h"
#include "error.h"
#include "safety-calculator.h"
#include "util.h"
#include "io.h"
static GListStore *
collect_selectable_entries (GListModel *store,
gboolean remove)
{
GListStore *selectable = NULL;
guint n_items = 0;
selectable = g_list_store_new (BZ_TYPE_ENTRY);
if (store == NULL)
return selectable;
n_items = g_list_model_get_n_items (store);
for (guint i = 0; i < n_items; i++)
{
g_autoptr (BzEntry) entry = NULL;
gboolean is_installed = FALSE;
gboolean skip = FALSE;
entry = g_list_model_get_item (store, i);
if (bz_entry_is_holding (entry))
skip = TRUE;
else if (!remove && !bz_entry_is_reinstallable (entry))
skip = TRUE;
else
{
is_installed = bz_entry_is_installed (entry);
skip = (!remove && is_installed) || (remove && !is_installed);
}
if (!skip)
g_list_store_append (selectable, entry);
}
return selectable;
}
static GPtrArray *
create_entry_radio_buttons (AdwAlertDialog *alert,
GListModel *selectable,
gboolean has_user_data)
{
g_autoptr (GPtrArray) radios = NULL;
GtkWidget *container = NULL;
guint n_selectable = 0;
container = gtk_box_new (GTK_ORIENTATION_VERTICAL, 12);
radios = g_ptr_array_new ();
n_selectable = selectable != NULL ? g_list_model_get_n_items (selectable) : 0;
if (n_selectable > 1)
{
GtkWidget *listbox = NULL;
GtkCheckButton *dummy_radio = NULL;
listbox = gtk_list_box_new ();
gtk_list_box_set_selection_mode (GTK_LIST_BOX (listbox), GTK_SELECTION_NONE);
gtk_widget_add_css_class (listbox, "boxed-list");
dummy_radio = GTK_CHECK_BUTTON (gtk_check_button_new ());
for (guint i = 0; i < n_selectable; i++)
{
g_autoptr (BzEntry) entry = NULL;
BzStateInfo *state_info = NULL;
GListModel *repositories = NULL;
g_autoptr (BzRepository) repo = NULL;
const char *remote_title = NULL;
GtkWidget *row = NULL;
GtkWidget *radio = NULL;
entry = g_list_model_get_item (selectable, i);
state_info = bz_state_info_get_default ();
repositories = bz_state_info_get_repositories (state_info);
if (repositories != NULL)
repo = bz_entry_get_repository (entry, repositories);
remote_title = repo != NULL ? bz_repository_get_title (repo) : bz_entry_get_remote_repo_name (entry);
row = adw_action_row_new ();
radio = gtk_check_button_new ();
adw_preferences_row_set_title (ADW_PREFERENCES_ROW (row), remote_title != NULL ? remote_title : _ ("Unknown"));
gtk_widget_set_valign (radio, GTK_ALIGN_CENTER);
adw_action_row_add_prefix (ADW_ACTION_ROW (row), radio);
adw_action_row_set_activatable_widget (ADW_ACTION_ROW (row), radio);
gtk_check_button_set_group (GTK_CHECK_BUTTON (radio), dummy_radio);
if (i == 0)
gtk_check_button_set_active (GTK_CHECK_BUTTON (radio), TRUE);
g_ptr_array_add (radios, radio);
gtk_list_box_append (GTK_LIST_BOX (listbox), row);
}
gtk_box_append (GTK_BOX (container), listbox);
}
if (has_user_data)
{
GtkWidget *listbox = NULL;
GtkWidget *keep_data_row = NULL;
GtkWidget *delete_data_row = NULL;
GtkWidget *keep_radio = NULL;
GtkWidget *delete_radio = NULL;
listbox = gtk_list_box_new ();
gtk_list_box_set_selection_mode (GTK_LIST_BOX (listbox), GTK_SELECTION_NONE);
gtk_widget_add_css_class (listbox, "boxed-list");
keep_data_row = adw_action_row_new ();
adw_preferences_row_set_title (ADW_PREFERENCES_ROW (keep_data_row), _ ("Keep User Data"));
adw_action_row_set_subtitle (ADW_ACTION_ROW (keep_data_row), _ ("Allow restoring personal settings & content"));
keep_radio = gtk_check_button_new ();
gtk_widget_set_valign (keep_radio, GTK_ALIGN_CENTER);
gtk_check_button_set_active (GTK_CHECK_BUTTON (keep_radio), TRUE);
adw_action_row_add_prefix (ADW_ACTION_ROW (keep_data_row), keep_radio);
adw_action_row_set_activatable_widget (ADW_ACTION_ROW (keep_data_row), keep_radio);
gtk_list_box_append (GTK_LIST_BOX (listbox), keep_data_row);
delete_data_row = adw_action_row_new ();
adw_preferences_row_set_title (ADW_PREFERENCES_ROW (delete_data_row), _ ("Delete All Data"));
adw_action_row_set_subtitle (ADW_ACTION_ROW (delete_data_row), _ ("Permanently erase user data to save space"));
delete_radio = gtk_check_button_new ();
gtk_widget_set_valign (delete_radio, GTK_ALIGN_CENTER);
gtk_check_button_set_group (GTK_CHECK_BUTTON (delete_radio), GTK_CHECK_BUTTON (keep_radio));
adw_action_row_add_prefix (ADW_ACTION_ROW (delete_data_row), delete_radio);
adw_action_row_set_activatable_widget (ADW_ACTION_ROW (delete_data_row), delete_radio);
gtk_list_box_append (GTK_LIST_BOX (listbox), delete_data_row);
g_ptr_array_add (radios, keep_radio);
g_ptr_array_add (radios, delete_radio);
gtk_box_append (GTK_BOX (container), listbox);
}
if (n_selectable > 1 || has_user_data)
adw_alert_dialog_set_extra_child (alert, container);
else
g_object_unref (g_object_ref_sink (container));
return g_steal_pointer (&radios);
}
static void
configure_remove_dialog (AdwAlertDialog *alert,
const char *title,
const char *id,
gboolean has_multiple_entries)
{
g_autofree char *heading = NULL;
g_autofree char *body = NULL;
heading = g_strdup_printf (_ ("Remove %s?"), title);
if (has_multiple_entries)
body = g_strdup (_ ("Select which version to remove."));
else
body = g_strdup_printf (_ ("It will not be possible to use %s after it is uninstalled."), title);
adw_alert_dialog_set_heading (alert, heading);
adw_alert_dialog_set_body (alert, body);
adw_alert_dialog_add_responses (alert,
"cancel", _ ("_Cancel"),
"remove", _ ("_Remove"),
NULL);
adw_alert_dialog_set_response_appearance (alert, "remove", ADW_RESPONSE_DESTRUCTIVE);
adw_alert_dialog_set_default_response (alert, "remove");
adw_alert_dialog_set_close_response (alert, "cancel");
}
static void
configure_high_risk_warning_dialog (AdwAlertDialog *alert,
const char *title,
BzHighRiskGroup risk_groups)
{
g_autofree char *heading = NULL;
g_autofree char *body = NULL;
heading = g_strdup_printf (_ ("ā%sā is High Risk"), title);
if (risk_groups & BZ_HIGH_RISK_GROUP_DISK)
{
body = g_strdup (_ ("This app has full access to your system, including all "
"<b>your files, browser history, saved passwords</b>, and "
"more. It also has access to the internet, meaning it "
"could send your data to outside parties.\n\n"
"Because the app is proprietary, it can not be audited "
"for what it does with these permissions."));
}
else if (risk_groups & BZ_HIGH_RISK_GROUP_X11)
{
body = g_strdup (_ ("This app uses the legacy X11 windowing system, which "
"allows it to <b>record all keystrokes, capture screenshots, "
"and monitor other applications</b>. It also has access "
"to the internet, meaning it could send your data to "
"outside parties.\n\n"
"Because the app is proprietary, it can not be audited "
"for what it does with these permissions."));
}
adw_alert_dialog_set_heading (alert, heading);
adw_alert_dialog_set_body (alert, body);
adw_alert_dialog_set_body_use_markup (alert, TRUE);
adw_alert_dialog_set_prefer_wide_layout (alert, TRUE);
adw_alert_dialog_add_responses (alert,
"cancel", _ ("_Cancel"),
"install", _ ("_Install Anyway"),
NULL);
adw_alert_dialog_set_response_appearance (alert, "install", ADW_RESPONSE_DESTRUCTIVE);
adw_alert_dialog_set_default_response (alert, "install");
adw_alert_dialog_set_close_response (alert, "cancel");
}
static BzHighRiskGroup
get_entry_high_risk_groups (BzEntry *entry)
{
if (bz_entry_get_is_foss (entry))
return BZ_HIGH_RISK_GROUP_NONE;
return bz_safety_calculator_get_high_risk_groups (entry);
}
static DexFuture *
show_dialog_fiber (GtkWidget *parent,
BzEntry *entry,
BzEntryGroup *group,
gboolean remove,
gboolean auto_confirm)
{
g_autoptr (GError) local_error = NULL;
g_autoptr (GListStore) store = NULL;
g_autoptr (GListStore) selectable = NULL;
const char *title = NULL;
const char *id = NULL;
g_autoptr (AdwDialog) alert = NULL;
g_autoptr (AdwDialog) risk_alert = NULL;
g_autoptr (GPtrArray) radios = NULL;
g_autofree char *dialog_response = NULL;
g_autofree char *risk_response = NULL;
g_autoptr (BzTransactionDialogResult) result = NULL;
g_autoptr (BzEntry) check_entry = NULL;
g_autoptr (BzEntry) selected_entry = NULL;
BzHighRiskGroup risk_groups = BZ_HIGH_RISK_GROUP_NONE;
guint n_selectable = 0;
gboolean confirmed = FALSE;
gboolean has_user_data = FALSE;
result = bz_transaction_dialog_result_new ();
if (group != NULL)
{
title = bz_entry_group_get_title (group);
id = bz_entry_group_get_id (group);
store = dex_await_object (bz_entry_group_dup_all_into_store (group), &local_error);
if (store == NULL)
{
bz_show_error_for_widget (parent, _ ("Failed to load transaction dialog"), local_error->message);
return dex_future_new_for_error (g_steal_pointer (&local_error));
}
if (remove)
{
for (guint i = g_list_model_get_n_items (G_LIST_MODEL (store)); i > 0; i--)
{
g_autoptr (BzEntry) candidate_entry = NULL;
candidate_entry = g_list_model_get_item (G_LIST_MODEL (store), i - 1);
if (!bz_entry_is_reinstallable (candidate_entry) &&
(!remove || !bz_entry_is_installed (candidate_entry)))
g_list_store_remove (store, i - 1);
}
selectable = collect_selectable_entries (G_LIST_MODEL (store), remove);
n_selectable = g_list_model_get_n_items (G_LIST_MODEL (selectable));
}
if (g_list_model_get_n_items (G_LIST_MODEL (store)) > 0)
check_entry = g_list_model_get_item (G_LIST_MODEL (store), 0);
if (check_entry == NULL)
{
g_set_error (&local_error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
"No entries for %s were able to be resolved",
id != NULL ? id : "(unknown)");
bz_show_error_for_widget (parent, _ ("Failed to load transaction dialog"), local_error->message);
return dex_future_new_for_error (g_steal_pointer (&local_error));
}
}
else
{
title = bz_entry_get_title (entry);
id = bz_entry_get_id (entry);
check_entry = g_object_ref (entry);
}
if (remove && id != NULL)
has_user_data = dex_await_boolean (bz_user_data_exists (id), NULL);
if (!remove && check_entry != NULL)
risk_groups = get_entry_high_risk_groups (check_entry);
if (risk_groups != BZ_HIGH_RISK_GROUP_NONE)
{
risk_alert = g_object_ref_sink (adw_alert_dialog_new (NULL, NULL));
configure_high_risk_warning_dialog (ADW_ALERT_DIALOG (risk_alert), title, risk_groups);
adw_dialog_present (risk_alert, parent);
risk_response = dex_await_string (
bz_make_alert_dialog_future (ADW_ALERT_DIALOG (risk_alert)),
&local_error);
if (risk_response == NULL)
return dex_future_new_for_error (g_steal_pointer (&local_error));
if (g_strcmp0 (risk_response, "install") != 0)
{
bz_transaction_dialog_result_set_confirmed (result, FALSE);
return dex_future_new_for_object (result);
}
}
if (remove)
{
alert = g_object_ref_sink (adw_alert_dialog_new (NULL, NULL));
configure_remove_dialog (ADW_ALERT_DIALOG (alert), title, id, n_selectable > 1);
radios = create_entry_radio_buttons (ADW_ALERT_DIALOG (alert), G_LIST_MODEL (selectable), has_user_data);
if (auto_confirm && radios->len <= 1)
{
dialog_response = g_strdup ("remove");
g_ptr_array_set_size (radios, 0);
g_clear_object (&alert);
}
else
{
adw_dialog_present (alert, parent);
dialog_response = dex_await_string (
bz_make_alert_dialog_future (ADW_ALERT_DIALOG (alert)),
&local_error);
if (dialog_response == NULL)
return dex_future_new_for_error (g_steal_pointer (&local_error));
if (has_user_data && radios->len >= 1)
{
GtkCheckButton *delete_radio = NULL;
delete_radio = g_ptr_array_index (radios, radios->len - 1);
bz_transaction_dialog_result_set_delete_user_data (result, gtk_check_button_get_active (delete_radio));
}
}
confirmed = g_strcmp0 (dialog_response, "remove") == 0;
}
else
confirmed = TRUE;
bz_transaction_dialog_result_set_confirmed (result, confirmed);
if (!confirmed)
return dex_future_new_for_object (result);
if (group != NULL)
{
if (selectable != NULL && n_selectable > 0)
{
if (n_selectable > 1)
{
for (guint i = 0; i < n_selectable; i++)
{
GtkCheckButton *check = g_ptr_array_index (radios, i);
if (gtk_check_button_get_active (check))
{
selected_entry = g_list_model_get_item (G_LIST_MODEL (selectable), i);
break;
}
}
}
if (selected_entry == NULL)
selected_entry = g_list_model_get_item (G_LIST_MODEL (selectable), 0);
}
else
selected_entry = check_entry != NULL ? g_object_ref (check_entry) : NULL;
}
else
selected_entry = g_object_ref (entry);
bz_transaction_dialog_result_set_selected_entry (result, selected_entry);
return dex_future_new_for_object (result);
}
DexFuture *
bz_transaction_dialog_show (GtkWidget *parent,
BzEntry *entry,
BzEntryGroup *group,
gboolean remove,
gboolean auto_confirm)
{
g_return_val_if_fail (GTK_IS_WIDGET (parent), NULL);
g_return_val_if_fail (entry != NULL || group != NULL, NULL);
return dex_scheduler_spawnv (
dex_scheduler_get_default (),
bz_get_dex_stack_size (),
G_CALLBACK (show_dialog_fiber),
5,
GTK_TYPE_WIDGET, parent,
BZ_TYPE_ENTRY, entry,
BZ_TYPE_ENTRY_GROUP, group,
G_TYPE_BOOLEAN, remove,
G_TYPE_BOOLEAN, auto_confirm);
}