// SPDX-License-Identifier: GPL-2.0+
/* NetworkManager Connection editor -- Connection editor for NetworkManager
 *
 * Rodrigo Moya <rodrigo@gnome-db.org>
 * Dan Williams <dcbw@redhat.com>
 * Tambet Ingo <tambet@gmail.com>
 *
 * Copyright 2007 - 2017 Red Hat, Inc.
 * Copyright 2007 - 2008 Novell, Inc.
 */

#include "nm-default.h"

#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
#include <gdk/gdkx.h>

#if WITH_SELINUX
#include <selinux/selinux.h>
#endif

#include "nm-connection-editor.h"
#include "nma-cert-chooser.h"

#include "ce-page.h"
#include "page-general.h"
#include "page-ethernet.h"
#include "page-8021x-security.h"
#include "page-wifi.h"
#include "page-wifi-security.h"
#include "page-proxy.h"
#include "page-ip4.h"
#include "page-ip6.h"
#include "page-ip-tunnel.h"
#include "page-dsl.h"
#include "page-mobile.h"
#include "page-bluetooth.h"
#include "page-ppp.h"
#include "page-vpn.h"
#include "page-infiniband.h"
#include "page-bond.h"
#include "page-team.h"
#include "page-team-port.h"
#include "page-bridge.h"
#include "page-bridge-port.h"
#include "page-vlan.h"
#include "page-dcb.h"
#include "page-macsec.h"
#include "page-wireguard.h"
#include "ce-polkit-button.h"
#include "vpn-helpers.h"
#include "eap-method.h"

extern gboolean nm_ce_keep_above;

G_DEFINE_TYPE (NMConnectionEditor, nm_connection_editor, G_TYPE_OBJECT)

enum {
	EDITOR_DONE,
	NEW_EDITOR,
	EDITOR_LAST_SIGNAL
};

static guint editor_signals[EDITOR_LAST_SIGNAL] = { 0 };

static GHashTable *active_editors;

static gboolean nm_connection_editor_set_connection (NMConnectionEditor *editor,
                                                     NMConnection *connection,
                                                     GError **error);

struct GetSecretsInfo {
	NMConnectionEditor *self;
	CEPage *page;
	char *setting_name;
	gboolean canceled;
};

#define SECRETS_TAG "secrets-setting-name"
#define ORDER_TAG "page-order"

static void
nm_connection_editor_update_title (NMConnectionEditor *editor)
{
	NMSettingConnection *s_con;
	const char *id;

	g_return_if_fail (editor != NULL);

	s_con = nm_connection_get_setting_connection (editor->connection);
	g_assert (s_con);

	id = nm_setting_connection_get_id (s_con);
	if (id && strlen (id)) {
		char *title = g_strdup_printf (_("Editing %s"), id);
		gtk_window_set_title (GTK_WINDOW (editor->window), title);
		g_free (title);
	} else
		gtk_window_set_title (GTK_WINDOW (editor->window), _("Editing un-named connection"));
}

static gboolean
ui_to_setting (NMConnectionEditor *editor, GError **error)
{
	NMSettingConnection *s_con;
	GtkWidget *widget;
	const char *name;

	s_con = nm_connection_get_setting_connection (editor->connection);
	g_assert (s_con);

	widget = GTK_WIDGET (gtk_builder_get_object (editor->builder, "connection_name"));
	name = gtk_entry_get_text (GTK_ENTRY (widget));

	g_object_set (G_OBJECT (s_con), NM_SETTING_CONNECTION_ID, name, NULL);
	nm_connection_editor_update_title (editor);

	if (!name || !strlen (name)) {
		g_set_error_literal (error, NMA_ERROR, NMA_ERROR_GENERIC, _("Missing connection name"));
		return FALSE;
	}

	return TRUE;
}

static gboolean
editor_is_initialized (NMConnectionEditor *editor)
{
	return (g_slist_length (editor->initializing_pages) == 0);
}

static void
update_sensitivity (NMConnectionEditor *editor)
{
	NMSettingConnection *s_con;
	gboolean sensitive = FALSE;
	GtkWidget *widget;
	GSList *iter;

	s_con = nm_connection_get_setting_connection (editor->connection);

	/* Can't modify read-only connections; can't modify anything before the
	 * editor is initialized either.
	 */
	if (   editor_is_initialized (editor)
	    && editor->can_modify
	    && !nm_setting_connection_get_read_only (s_con)) {
		/* If the user cannot ever be authorized to change system connections,
		 * we desensitize the entire dialog.
		 */
		sensitive = ce_polkit_button_get_authorized (CE_POLKIT_BUTTON (editor->ok_button));
	}

	/* Cancel button is always sensitive */
	gtk_widget_set_sensitive (GTK_WIDGET (editor->cancel_button), TRUE);

	widget = GTK_WIDGET (gtk_builder_get_object (editor->builder, "connection_name_label"));
	gtk_widget_set_sensitive (widget, sensitive);

	widget = GTK_WIDGET (gtk_builder_get_object (editor->builder, "connection_name"));
	gtk_widget_set_sensitive (widget, sensitive);

	for (iter = editor->pages; iter; iter = g_slist_next (iter)) {
		widget = ce_page_get_page (CE_PAGE (iter->data));
		gtk_widget_set_sensitive (widget, sensitive);
	}
}

#if WITH_SELINUX
/* This is what the files in ~/.cert would get. */
static const char certcon[] = "unconfined_u:object_r:home_cert_t:s0";

static gboolean
clear_name_if_present (GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
{
	gchar **filename = data;
	gs_free char *existing = NULL;

	gtk_tree_model_get (model, iter, 2, &existing, -1);
	if (g_strcmp0 (existing, *filename) == 0) {
		*filename = NULL;
		return TRUE;
	}

	return FALSE;
}

static void
update_relabel_list_filename (GtkListStore *store, char *filename)
{
	GtkTreeIter iter;
	gboolean writable;
	char *tcon;
	/* Any kind of VPN would do. If OpenVPN can't access the files
	 * no VPN likely can.  NetworkManager policy currently allows
	 * accessing home. It may make sense to tighten it some point. */
	static const char scon[] = "system_u:system_r:openvpn_t:s0";

	gtk_tree_model_foreach (GTK_TREE_MODEL (store), clear_name_if_present, &filename);
	if (filename == NULL)
		return;

	if (getfilecon (filename, &tcon) == -1) {
		/* Don't warn here, just ignore it. Perhaps the file
		 * is not on a SELinux-capable filesystem or something. */
		return;
	}

	if (g_strcmp0 (certcon, tcon) == 0)
		return;

	writable = (access (filename, W_OK) == 0);

	if (selinux_check_access (scon, tcon, "file", "open", NULL) == -1) {
		gtk_list_store_append (store, &iter);
		gtk_list_store_set (store, &iter,
		                    0, writable,
		                    1, writable,
		                    2, filename,
		                    -1);
	}

	freecon (tcon);
}

static void
update_relabel_list (GtkWidget *widget, GtkListStore *store)
{
	gchar *filename = NULL;
	NMSetting8021xCKScheme scheme;

	if (!gtk_widget_is_sensitive (widget))
		return;

	if (NMA_IS_CERT_CHOOSER (widget)) {
		filename = nma_cert_chooser_get_cert (NMA_CERT_CHOOSER (widget), &scheme);
		if (filename && scheme == NM_SETTING_802_1X_CK_SCHEME_PATH) {
			update_relabel_list_filename (store, filename);
			g_free (filename);
		}

		filename = nma_cert_chooser_get_key (NMA_CERT_CHOOSER (widget), &scheme);
		if (filename && scheme == NM_SETTING_802_1X_CK_SCHEME_PATH) {
			update_relabel_list_filename (store, filename);
			g_free (filename);
		}
	} else if (GTK_IS_CONTAINER (widget)) {
		gtk_container_foreach (GTK_CONTAINER (widget),
		                       (GtkCallback) update_relabel_list,
		                       store);
	}
}

static void
recheck_relabel (NMConnectionEditor *editor)
{
	gtk_list_store_clear (editor->relabel_list);
	update_relabel_list (editor->window, editor->relabel_list);

	if (gtk_tree_model_iter_n_children (GTK_TREE_MODEL (editor->relabel_list), NULL))
		gtk_widget_show (editor->relabel_info);
	else
		gtk_widget_hide (editor->relabel_info);
}

static void
relabel_toggled (GtkCellRendererToggle *cell_renderer, gchar *path, gpointer user_data)
{
	NMConnectionEditor *editor = user_data;
	GtkTreeIter iter;
	gboolean relabel;

	if (!gtk_tree_model_get_iter_from_string (GTK_TREE_MODEL (editor->relabel_list), &iter, path))
		g_return_if_reached ();

	gtk_tree_model_get (GTK_TREE_MODEL (editor->relabel_list), &iter, 0, &relabel, -1);
	gtk_list_store_set (editor->relabel_list, &iter, 0, !relabel, -1);
}

static gboolean
maybe_relabel (GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
{
	gboolean relabel;
	gchar *filename;

	gtk_tree_model_get (model, iter, 0, &relabel, 2, &filename, -1);
	if (relabel) {
		if (setfilecon (filename, certcon) == -1)
			g_warning ("setfilecon: %s\n", g_strerror (errno));
	}

	g_free (filename);
	return FALSE;
}

static void
relabel_button_clicked_cb (GtkWidget *widget, gpointer user_data)
{
	NMConnectionEditor *editor = NM_CONNECTION_EDITOR (user_data);

	if (gtk_dialog_run (GTK_DIALOG (editor->relabel_dialog)) == GTK_RESPONSE_APPLY) {
		gtk_tree_model_foreach (GTK_TREE_MODEL (editor->relabel_list), maybe_relabel, NULL);
		recheck_relabel (editor);
	}
	gtk_widget_hide (editor->relabel_dialog);
}
#else /* !WITH_SELINUX */
static void
recheck_relabel (NMConnectionEditor *editor)
{
}

static void
relabel_toggled (GtkCellRendererToggle *cell_renderer, gchar *path, gpointer user_data)
{
	g_return_if_reached ();
}

static void
relabel_button_clicked_cb (GtkWidget *widget, gpointer user_data)
{
	g_return_if_reached ();
}
#endif /* WITH_SELINUX */

static void
connection_editor_validate (NMConnectionEditor *editor)
{
	NMSettingConnection *s_con;
	GSList *iter;
	gs_free char *validation_error = NULL;
	GError *error = NULL;

	if (!editor_is_initialized (editor)) {
		validation_error = g_strdup (_("Editor initializing…"));
		goto done_silent;
	}

	s_con = nm_connection_get_setting_connection (editor->connection);
	g_assert (s_con);
	if (nm_setting_connection_get_read_only (s_con)) {
		validation_error = g_strdup (_("Connection cannot be modified"));
		goto done;
	}

	if (!ui_to_setting (editor, &error)) {
		validation_error = g_strdup (error->message);
		g_clear_error (&error);
		goto done;
	}

	recheck_relabel (editor);

	for (iter = editor->pages; iter; iter = g_slist_next (iter)) {
		if (!ce_page_validate (CE_PAGE (iter->data), editor->connection, &error)) {
			if (!validation_error) {
				validation_error = g_strdup_printf (_("Invalid setting %s: %s"),
				                                    CE_PAGE (iter->data)->title,
				                                    error->message);
			}
			g_clear_error (&error);
		}
	}

done:
	if (g_strcmp0 (validation_error, editor->last_validation_error) != 0) {
		if (editor->last_validation_error && !validation_error)
			g_message ("Connection validates and can be saved");
		else if (validation_error)
			g_message ("Cannot save connection due to error: %s", validation_error);
		g_free (editor->last_validation_error);
		editor->last_validation_error = g_strdup (validation_error);
	}

done_silent:
	ce_polkit_button_set_validation_error (CE_POLKIT_BUTTON (editor->ok_button), validation_error);
	gtk_widget_set_sensitive (editor->export_button, !validation_error);

	update_sensitivity (editor);
}

static void
ok_button_actionable_cb (GtkWidget *button,
                         gboolean actionable,
                         NMConnectionEditor *editor)
{
	connection_editor_validate (editor);
}

static void
permissions_changed_cb (NMClient *client,
                        NMClientPermission permission,
                        NMClientPermissionResult result,
                        NMConnectionEditor *editor)
{
	if (permission != NM_CLIENT_PERMISSION_SETTINGS_MODIFY_SYSTEM)
		return;

	if (result == NM_CLIENT_PERMISSION_RESULT_YES || result == NM_CLIENT_PERMISSION_RESULT_AUTH)
		editor->can_modify = TRUE;
	else
		editor->can_modify = FALSE;

	connection_editor_validate (editor);
}

static void
destroy_inter_page_item (gpointer data)
{
	return;
}

static void
nm_connection_editor_init (NMConnectionEditor *editor)
{
	GtkWidget *dialog;
	GError *error = NULL;
	const char *objects[] = { "nm-connection-editor", "relabel_dialog", "relabel_list", NULL };

	editor->builder = gtk_builder_new ();

	if (!gtk_builder_add_objects_from_resource (editor->builder,
	                                            "/org/gnome/nm_connection_editor/nm-connection-editor.ui",
	                                            (char **) objects,
	                                            &error)) {
		g_warning ("Couldn't load builder resource " "/org/gnome/nm_connection_editor/nm-connection-editor.ui: %s", error->message);
		g_error_free (error);

		dialog = gtk_message_dialog_new (NULL, 0,
		                                 GTK_MESSAGE_ERROR,
		                                 GTK_BUTTONS_OK,
		                                 "%s",
		                                 _("The connection editor could not find some required resources (the .ui file was not found)."));
		gtk_dialog_run (GTK_DIALOG (dialog));
		gtk_widget_destroy (dialog);
		gtk_main_quit ();
		return;
	}

	editor->window = GTK_WIDGET (gtk_builder_get_object (editor->builder, "nm-connection-editor"));
	if (nm_ce_keep_above)
		gtk_window_set_keep_above (GTK_WINDOW (editor->window), TRUE);

	editor->cancel_button = GTK_WIDGET (gtk_builder_get_object (editor->builder, "cancel_button"));
	editor->export_button = GTK_WIDGET (gtk_builder_get_object (editor->builder, "export_button"));
	editor->relabel_info = GTK_WIDGET (gtk_builder_get_object (editor->builder, "relabel_info"));
	editor->relabel_dialog = GTK_WIDGET (gtk_builder_get_object (editor->builder, "relabel_dialog"));
	editor->relabel_button = GTK_WIDGET (gtk_builder_get_object (editor->builder, "relabel_button"));
	editor->relabel_list = GTK_LIST_STORE (gtk_builder_get_object (editor->builder, "relabel_list"));
	gtk_builder_add_callback_symbol (editor->builder, "relabel_toggled", G_CALLBACK (relabel_toggled));

	gtk_builder_connect_signals (editor->builder, editor);

	editor->inter_page_hash = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, (GDestroyNotify) destroy_inter_page_item);
}

static void
get_secrets_info_free (GetSecretsInfo *info)
{
	g_free (info->setting_name);
	g_free (info);
}

static void
dispose (GObject *object)
{
	NMConnectionEditor *editor = NM_CONNECTION_EDITOR (object);

	editor->disposed = TRUE;

	if (active_editors && editor->orig_connection)
		g_hash_table_remove (active_editors, editor->orig_connection);

	g_slist_free_full (editor->initializing_pages, g_object_unref);
	editor->initializing_pages = NULL;

	g_slist_free_full (editor->pages, g_object_unref);
	editor->pages = NULL;

	/* Mark any in-progress secrets call as canceled; it will clean up after itself. */
	if (editor->secrets_call)
		editor->secrets_call->canceled = TRUE;

	while (editor->pending_secrets_calls) {
		get_secrets_info_free ((GetSecretsInfo *) editor->pending_secrets_calls->data);
		editor->pending_secrets_calls = g_slist_delete_link (editor->pending_secrets_calls, editor->pending_secrets_calls);
	}

	nm_clear_g_source (&editor->validate_id);

	g_clear_object (&editor->connection);
	g_clear_object (&editor->orig_connection);

	if (editor->window) {
		gtk_widget_destroy (editor->window);
		editor->window = NULL;
	}
	g_clear_object (&editor->parent_window);
	g_clear_object (&editor->builder);

	nm_clear_g_signal_handler (editor->client, &editor->permission_id);
	g_clear_object (&editor->client);

	g_clear_pointer (&editor->last_validation_error, g_free);

	if (editor->inter_page_hash) {
		g_hash_table_destroy (editor->inter_page_hash);
		editor->inter_page_hash = NULL;
	}

	g_slist_free_full (editor->unsupported_properties, g_free);
	editor->unsupported_properties = NULL;

	G_OBJECT_CLASS (nm_connection_editor_parent_class)->dispose (object);
}

static void
nm_connection_editor_class_init (NMConnectionEditorClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);

	/* virtual methods */
	object_class->dispose = dispose;

	/* Signals */
	editor_signals[EDITOR_DONE] =
		g_signal_new (NM_CONNECTION_EDITOR_DONE,
		              G_OBJECT_CLASS_TYPE (object_class),
		              G_SIGNAL_RUN_FIRST,
		              0, NULL, NULL, NULL,
		              G_TYPE_NONE, 1, GTK_TYPE_RESPONSE_TYPE);

	editor_signals[NEW_EDITOR] =
		g_signal_new (NM_CONNECTION_EDITOR_NEW_EDITOR,
		              G_OBJECT_CLASS_TYPE (object_class),
		              G_SIGNAL_RUN_FIRST,
		              0, NULL, NULL, NULL,
		              G_TYPE_NONE, 1, G_TYPE_POINTER);
}

NMConnectionEditor *
nm_connection_editor_new (GtkWindow *parent_window,
                          NMConnection *connection,
                          NMClient *client)
{
	NMConnectionEditor *editor;
	GtkWidget *hbox;
	gboolean is_new;
	GError *error = NULL;

	g_return_val_if_fail (NM_IS_CONNECTION (connection), NULL);

	is_new = !nm_client_get_connection_by_uuid (client, nm_connection_get_uuid (connection));

	editor = g_object_new (NM_TYPE_CONNECTION_EDITOR, NULL);
	editor->parent_window = parent_window ? g_object_ref (parent_window) : NULL;
	editor->client = g_object_ref (client);
	editor->is_new_connection = is_new;

	editor->can_modify = nm_client_get_permission_result (client, NM_CLIENT_PERMISSION_SETTINGS_MODIFY_SYSTEM);
	editor->permission_id = g_signal_connect (editor->client,
	                                          "permission-changed",
	                                          G_CALLBACK (permissions_changed_cb),
	                                          editor);

	editor->ok_button = ce_polkit_button_new (_("_Save"),
	                                          _("Save any changes made to this connection."),
	                                          _("Authenticate to save this connection for all users of this machine."),
	                                          "emblem-ok-symbolic",
	                                          client,
	                                          NM_CLIENT_PERMISSION_SETTINGS_MODIFY_SYSTEM);
	gtk_button_set_use_underline (GTK_BUTTON (editor->ok_button), TRUE);

	g_signal_connect (editor->ok_button, "actionable",
	                  G_CALLBACK (ok_button_actionable_cb), editor);
	g_signal_connect (editor->ok_button, "authorized",
	                  G_CALLBACK (ok_button_actionable_cb), editor);
	hbox = GTK_WIDGET (gtk_builder_get_object (editor->builder, "action_area_hbox"));
	gtk_box_pack_end (GTK_BOX (hbox), editor->ok_button, TRUE, TRUE, 0);
	gtk_widget_show_all (editor->ok_button);

	if (!nm_connection_editor_set_connection (editor, connection, &error)) {
		nm_connection_editor_error (parent_window,
		                            is_new ? _("Could not create connection") : _("Could not edit connection"),
		                            "%s",
		                            error ? error->message : _("Unknown error creating connection editor dialog."));
		g_clear_error (&error);
		g_object_unref (editor);
		return NULL;
	}

	if (!active_editors)
		active_editors = g_hash_table_new_full (NULL, NULL, g_object_unref, NULL);
	g_hash_table_insert (active_editors, g_object_ref (connection), editor);

	return editor;
}

NMConnectionEditor *
nm_connection_editor_get (NMConnection *connection)
{
	return active_editors ? g_hash_table_lookup (active_editors, connection) : NULL;
}

/* Returns an editor for @port's controller, if any */
NMConnectionEditor *
nm_connection_editor_get_controller (NMConnection *port)
{
	GHashTableIter iter;
	gpointer connection, editor;
	NMSettingConnection *s_con;
	const char *controller;

	if (!active_editors)
		return NULL;

	s_con = nm_connection_get_setting_connection (port);
	controller = nm_setting_connection_get_master (s_con);
	if (!controller)
		return NULL;

	g_hash_table_iter_init (&iter, active_editors);
	while (g_hash_table_iter_next (&iter, &connection, &editor)) {
		if (!g_strcmp0 (controller, nm_connection_get_uuid (connection)))
			return editor;
		if (!g_strcmp0 (controller, nm_connection_get_interface_name (connection)))
			return editor;
	}

	return NULL;
}

NMConnection *
nm_connection_editor_get_connection (NMConnectionEditor *editor)
{
	g_return_val_if_fail (NM_IS_CONNECTION_EDITOR (editor), NULL);

	return editor->orig_connection;
}

static void
populate_connection_ui (NMConnectionEditor *editor)
{
	NMSettingConnection *s_con;
	GtkWidget *name;

	name = GTK_WIDGET (gtk_builder_get_object (editor->builder, "connection_name"));

	s_con = nm_connection_get_setting_connection (editor->connection);
	gtk_entry_set_text (GTK_ENTRY (name), s_con ? nm_setting_connection_get_id (s_con) : NULL);
	gtk_widget_set_tooltip_text (name, nm_connection_get_uuid (editor->connection));

	g_signal_connect_swapped (name, "changed", G_CALLBACK (connection_editor_validate), editor);

	connection_editor_validate (editor);
}

static void
page_changed (CEPage *page, gpointer user_data)
{
	NMConnectionEditor *editor = NM_CONNECTION_EDITOR (user_data);
	GSList *iter;

	/* Do page interdependent changes */
	for (iter = editor->pages; iter; iter = g_slist_next (iter))
		ce_page_inter_page_change (CE_PAGE (iter->data));

	if (editor_is_initialized (editor))
		nm_connection_editor_inter_page_clear_data (editor);

	connection_editor_validate (editor);
}

static gboolean
idle_validate (gpointer user_data)
{
	NMConnectionEditor *editor = NM_CONNECTION_EDITOR (user_data);

	editor->validate_id = 0;
	connection_editor_validate (editor);
	return FALSE;
}

static void
recheck_initialization (NMConnectionEditor *editor)
{
	GtkNotebook *notebook;
	GtkLabel *label;

	if (!editor_is_initialized (editor) || editor->init_run)
		return;

	editor->init_run = TRUE;

	populate_connection_ui (editor);

	/* Show the second page (the connection-type-specific data) first */
	notebook = GTK_NOTEBOOK (gtk_builder_get_object (editor->builder, "notebook"));
	gtk_notebook_set_current_page (notebook, 1);

	/* When everything is initialized, re-present the window to ensure it's on top */
	nm_connection_editor_present (editor);

	/* Validate the connection from an idle handler to ensure that stuff like
	 * GtkFileChoosers have had a chance to asynchronously find their files.
	 */
	if (editor->validate_id)
		g_source_remove (editor->validate_id);
	editor->validate_id = g_idle_add (idle_validate, editor);

	if (editor->unsupported_properties) {
		GString *str;
		GSList *iter;
		gs_free char *tooltip = NULL;

		str = g_string_new ("Unsupported properties: ");

		for (iter = editor->unsupported_properties; iter; iter = g_slist_next (iter)) {
			g_string_append (str, (char *) iter->data);
			if (iter->next)
				g_string_append (str, ", ");
		}
		tooltip = g_string_free (str, FALSE);

		label = GTK_LABEL (gtk_builder_get_object (editor->builder, "message_label"));
		gtk_label_set_text (label,
		                    _("Warning: the connection contains some properties not supported by the editor. "
		                      "They will be cleared upon save."));
		gtk_widget_set_tooltip_text (GTK_WIDGET (label), tooltip);
	}
}

static void
page_initialized (CEPage *page, GError *error, gpointer user_data)
{
	NMConnectionEditor *editor = NM_CONNECTION_EDITOR (user_data);
	GtkNotebook *notebook;
	GtkWidget *parent;
	GtkWidget *scrolled;
	GtkWidget *widget;
	GtkWidget *label;
	GList *children, *iter;
	gpointer order, child_order;
	int i;

	if (error) {
		gtk_widget_hide (editor->window);
		nm_connection_editor_error (editor->parent_window,
		                            _("Error initializing editor"),
		                            "%s", error->message);
		g_signal_emit (editor, editor_signals[EDITOR_DONE], 0, GTK_RESPONSE_NONE);
		return;
	}

	/* Add the page to the UI */
	notebook = GTK_NOTEBOOK (gtk_builder_get_object (editor->builder, "notebook"));
	label = gtk_label_new (ce_page_get_title (page));

	widget = ce_page_get_page (page);
	parent = gtk_widget_get_parent (widget);
	if (parent)
		gtk_container_remove (GTK_CONTAINER (parent), widget);

	if (CE_IS_PAGE_VPN (page)) {
		if (ce_page_vpn_can_export (CE_PAGE_VPN (page)))
			gtk_widget_show (editor->export_button);

		scrolled = gtk_scrolled_window_new (NULL, NULL);
		gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
		gtk_container_add (GTK_CONTAINER (scrolled), widget);
		gtk_widget_show (scrolled);
		widget = scrolled;

#if GTK_CHECK_VERSION(3,22,0)
		NM_LIBNM_COMPAT_UNDEPRECATE (
			gtk_scrolled_window_set_propagate_natural_height (GTK_SCROLLED_WINDOW (scrolled),
									  TRUE));
#endif
	}

	order = g_object_get_data (G_OBJECT (page), ORDER_TAG);
	g_object_set_data (G_OBJECT (widget), ORDER_TAG, order);

	children = gtk_container_get_children (GTK_CONTAINER (notebook));
	for (iter = children, i = 0; iter; iter = iter->next, i++) {
		child_order = g_object_get_data (G_OBJECT (iter->data), ORDER_TAG);
		if (child_order > order)
			break;
	}
	g_list_free (children);

	gtk_notebook_insert_page (notebook, widget, label, i);

	/* Move the page from the initializing list to the main page list */
	editor->initializing_pages = g_slist_remove (editor->initializing_pages, page);
	editor->pages = g_slist_append (editor->pages, page);

	recheck_initialization (editor);
}

static void
page_new_editor (CEPage *page, NMConnectionEditor *new_editor, gpointer user_data)
{
	NMConnectionEditor *self = NM_CONNECTION_EDITOR (user_data);

	g_signal_emit (self, editor_signals[NEW_EDITOR], 0, new_editor);
}

static void request_secrets (GetSecretsInfo *info);

static void
get_secrets_cb (GObject *object,
                GAsyncResult *result,
                gpointer user_data)
{
	NMRemoteConnection *connection = NM_REMOTE_CONNECTION (object);
	GetSecretsInfo *info = user_data;
	NMConnectionEditor *self;
	GVariant *secrets;
	GError *error = NULL;

	if (info->canceled) {
		get_secrets_info_free (info);
		return;
	}

	secrets = nm_remote_connection_get_secrets_finish (connection, result, &error);

	self = info->self;

	/* Complete this secrets request; completion can actually dispose of the
	 * dialog if there was an error.
	 */
	self->secrets_call = NULL;
	ce_page_complete_init (info->page, info->setting_name, secrets, error);
	get_secrets_info_free (info);

	/* Kick off the next secrets request if there is one queued; if the dialog
	 * was disposed of by the completion above we don't need to do anything.
	 */
	if (!self->disposed && self->pending_secrets_calls) {
		self->secrets_call = g_slist_nth_data (self->pending_secrets_calls, 0);
		self->pending_secrets_calls = g_slist_remove (self->pending_secrets_calls, self->secrets_call);

		request_secrets (self->secrets_call);
	}
}

static void
request_secrets (GetSecretsInfo *info)
{
	g_return_if_fail (info != NULL);

	nm_remote_connection_get_secrets_async (NM_REMOTE_CONNECTION (info->self->orig_connection),
	                                        info->setting_name, NULL, get_secrets_cb, info);
}

static void
get_secrets_for_page (NMConnectionEditor *self,
                      CEPage *page,
                      const char *setting_name)
{
	GetSecretsInfo *info;

	info = g_malloc0 (sizeof (GetSecretsInfo));
	info->self = self;
	info->page = page;
	info->setting_name = g_strdup (setting_name);

	/* PolicyKit doesn't queue up authorization requests internally.  Instead,
	 * if there's a pending authorization request, subsequent requests for that
	 * same authorization will return NotAuthorized+Challenge.  That's pretty
	 * inconvenient and it would be a lot nicer if PK just queued up subsequent
	 * authorization requests and executed them when the first one was finished.
	 * But it since it doesn't do that, we have to serialize the authorization
	 * requests ourselves to get the right authorization result.
	 */
	/* NOTE: PolicyKit-gnome 0.95 now serializes auth requests as of this commit:
	 * http://git.gnome.org/cgit/PolicyKit-gnome/commit/?id=f32cb7faa7197b9db55b569677732742c3c7fdc1
	 */

	/* If there's already an in-progress call, queue up the new one */
	if (self->secrets_call)
		self->pending_secrets_calls = g_slist_append (self->pending_secrets_calls, info);
	else {
		/* Request secrets for this page */
		self->secrets_call = info;
		request_secrets (info);
	}
}

static gboolean
add_page (NMConnectionEditor *editor,
          CEPageNewFunc func,
          NMConnection *connection,
          GError **error)
{
	CEPage *page;
	const char *secrets_setting_name = NULL;

	g_return_val_if_fail (editor != NULL, FALSE);
	g_return_val_if_fail (func != NULL, FALSE);
	g_return_val_if_fail (connection != NULL, FALSE);

	page = (*func) (editor, connection, GTK_WINDOW (editor->window), editor->client,
	                &secrets_setting_name, error);
	if (page) {
		g_object_set_data_full (G_OBJECT (page),
		                        SECRETS_TAG,
		                        g_strdup (secrets_setting_name),
		                        g_free);
		g_object_set_data (G_OBJECT (page),
		                   ORDER_TAG,
		                   GINT_TO_POINTER (g_slist_length (editor->initializing_pages)));

		editor->initializing_pages = g_slist_append (editor->initializing_pages, page);
		g_signal_connect (page, CE_PAGE_CHANGED, G_CALLBACK (page_changed), editor);
		g_signal_connect (page, CE_PAGE_INITIALIZED, G_CALLBACK (page_initialized), editor);
		g_signal_connect (page, CE_PAGE_NEW_EDITOR, G_CALLBACK (page_new_editor), editor);
	}
	return !!page;
}

void
nm_connection_editor_add_unsupported_property (NMConnectionEditor *editor, const char *name)
{
	editor->unsupported_properties = g_slist_append (editor->unsupported_properties, g_strdup (name));
}

void
nm_connection_editor_check_unsupported_properties (NMConnectionEditor *editor,
                                                   NMSetting *setting,
                                                   const char * const *known_props)
{
	gs_free GParamSpec **property_specs = NULL;
	GParamSpec *prop_spec;
	guint n_property_specs;
	guint i;
	char tmp[1024];

	if (!setting)
		return;

	property_specs = g_object_class_list_properties (G_OBJECT_GET_CLASS (setting),
	                                                 &n_property_specs);
	for (i = 0; i < n_property_specs; i++) {
		prop_spec = property_specs[i];
		if (   !g_strv_contains (known_props, prop_spec->name)
		    && !nm_streq0 (prop_spec->name, NM_SETTING_NAME)) {
			nm_auto_unset_gvalue GValue value = G_VALUE_INIT;

			g_value_init (&value, prop_spec->value_type);
			g_object_get_property (G_OBJECT (setting), prop_spec->name, &value);
			if (!g_param_value_defaults (prop_spec, &value)) {
				nm_sprintf_buf (tmp, "%s.%s", nm_setting_get_name (setting), prop_spec->name);
				nm_connection_editor_add_unsupported_property (editor, tmp);
			}
		}
	}
}

static gboolean
nm_connection_editor_set_connection (NMConnectionEditor *editor,
                                     NMConnection *orig_connection,
                                     GError **error)
{
	NMSettingConnection *s_con;
	const char *connection_type;
	const char *port_type;
	gboolean success = FALSE;
	GSList *iter, *copy;

	g_return_val_if_fail (NM_IS_CONNECTION_EDITOR (editor), FALSE);
	g_return_val_if_fail (NM_IS_CONNECTION (orig_connection), FALSE);

	/* clean previous connection */
	if (editor->connection)
		g_object_unref (editor->connection);

	editor->connection = nm_simple_connection_new_clone (orig_connection);

	editor->orig_connection = g_object_ref (orig_connection);
	nm_connection_editor_update_title (editor);

	/* Handle CA cert ignore stuff */
	eap_method_ca_cert_ignore_load (editor->connection);

	s_con = nm_connection_get_setting_connection (editor->connection);
	g_assert (s_con);

	connection_type = nm_setting_connection_get_connection_type (s_con);
	if (!add_page (editor, ce_page_general_new, editor->connection, error))
		goto out;
	if (!strcmp (connection_type, NM_SETTING_WIRED_SETTING_NAME)) {
		if (!add_page (editor, ce_page_ethernet_new, editor->connection, error))
			goto out;
		if (!add_page (editor, ce_page_8021x_security_new, editor->connection, error))
			goto out;
		if (!add_page (editor, ce_page_dcb_new, editor->connection, error))
			goto out;
	} else if (!strcmp (connection_type, NM_SETTING_WIRELESS_SETTING_NAME)) {
		if (!add_page (editor, ce_page_wifi_new, editor->connection, error))
			goto out;
		if (!add_page (editor, ce_page_wifi_security_new, editor->connection, error))
			goto out;
	} else if (!strcmp (connection_type, NM_SETTING_VPN_SETTING_NAME)) {
		if (!add_page (editor, ce_page_vpn_new, editor->connection, error))
			goto out;
	} else if (!strcmp (connection_type, NM_SETTING_IP_TUNNEL_SETTING_NAME)) {
		if (!add_page (editor, ce_page_ip_tunnel_new, editor->connection, error))
			goto out;
	} else if (!strcmp (connection_type, NM_SETTING_PPPOE_SETTING_NAME)) {
		if (!add_page (editor, ce_page_dsl_new, editor->connection, error))
			goto out;
		if (!add_page (editor, ce_page_ppp_new, editor->connection, error))
			goto out;
	} else if (!strcmp (connection_type, NM_SETTING_GSM_SETTING_NAME) || 
	           !strcmp (connection_type, NM_SETTING_CDMA_SETTING_NAME)) {
		if (!add_page (editor, ce_page_mobile_new, editor->connection, error))
			goto out;
		if (!add_page (editor, ce_page_ppp_new, editor->connection, error))
			goto out;
	} else if (!strcmp (connection_type, NM_SETTING_BLUETOOTH_SETTING_NAME)) {
		NMSettingBluetooth *s_bt = nm_connection_get_setting_bluetooth (editor->connection);
		const char *type = nm_setting_bluetooth_get_connection_type (s_bt);
		g_assert (type);

		if (!add_page (editor, ce_page_bluetooth_new, editor->connection, error))
			goto out;
		if (!g_strcmp0 (type, "dun")) {
			if (!add_page (editor, ce_page_mobile_new, editor->connection, error))
				goto out;
			if (!add_page (editor, ce_page_ppp_new, editor->connection, error))
				goto out;
		}
	} else if (!strcmp (connection_type, NM_SETTING_INFINIBAND_SETTING_NAME)) {
		if (!add_page (editor, ce_page_infiniband_new, editor->connection, error))
			goto out;
	} else if (!strcmp (connection_type, NM_SETTING_BOND_SETTING_NAME)) {
		if (!add_page (editor, ce_page_bond_new, editor->connection, error))
			goto out;
	} else if (!strcmp (connection_type, NM_SETTING_TEAM_SETTING_NAME)) {
		if (!add_page (editor, ce_page_team_new, editor->connection, error))
			goto out;
	} else if (!strcmp (connection_type, NM_SETTING_BRIDGE_SETTING_NAME)) {
		if (!add_page (editor, ce_page_bridge_new, editor->connection, error))
			goto out;
	} else if (!strcmp (connection_type, NM_SETTING_VLAN_SETTING_NAME)) {
		if (!add_page (editor, ce_page_vlan_new, editor->connection, error))
			goto out;
	} else if (!strcmp (connection_type, NM_SETTING_MACSEC_SETTING_NAME)) {
		if (!add_page (editor, ce_page_macsec_new, editor->connection, error))
			goto out;
		if (!add_page (editor, ce_page_8021x_security_new, editor->connection, error))
			goto out;
	} else if (!strcmp (connection_type, NM_SETTING_WIREGUARD_SETTING_NAME)) {
		if (!add_page (editor, ce_page_wireguard_new, editor->connection, error))
			goto out;
	} else {
		g_warning ("Unhandled setting type '%s'", connection_type);
	}

	port_type = nm_setting_connection_get_slave_type (s_con);
	if (!g_strcmp0 (port_type, NM_SETTING_TEAM_SETTING_NAME)) {
		if (!add_page (editor, ce_page_team_port_new, editor->connection, error))
			goto out;
	} else if (!g_strcmp0 (port_type, NM_SETTING_BRIDGE_SETTING_NAME)) {
		if (!add_page (editor, ce_page_bridge_port_new, editor->connection, error))
			goto out;
	}

	if (   nm_connection_get_setting_proxy (editor->connection)
	    && !add_page (editor, ce_page_proxy_new, editor->connection, error))
		goto out;
	if (   nm_connection_get_setting_ip4_config (editor->connection)
	    && !add_page (editor, ce_page_ip4_new, editor->connection, error))
		goto out;
	if (   nm_connection_get_setting_ip6_config (editor->connection)
	    && !add_page (editor, ce_page_ip6_new, editor->connection, error))
		goto out;

	/* After all pages are created, then kick off secrets requests that any
	 * the pages may need to make; if they don't need any secrets, then let
	 * them finish initialization.  The list might get modified during the loop
	 * which is why copy the list here.
	 */
	copy = g_slist_copy (editor->initializing_pages);
	for (iter = copy; iter; iter = g_slist_next (iter)) {
		CEPage *page = CE_PAGE (iter->data);
		const char *setting_name = g_object_get_data (G_OBJECT (page), SECRETS_TAG);

		if (!setting_name) {
			/* page doesn't need any secrets */
			ce_page_complete_init (page, NULL, NULL, NULL);
		} else if (!NM_IS_REMOTE_CONNECTION (editor->orig_connection)) {
			/* We want to get secrets using ->orig_connection, since that's the
			 * remote connection which can actually respond to secrets requests.
			 * ->connection is a plain NMConnection copy of ->orig_connection
			 * which is what gets changed when users modify anything.  But when
			 * creating or importing, ->orig_connection will be an NMConnection
			 * since the new connection hasn't been added to NetworkManager yet.
			 * So basically, skip requesting secrets if the connection can't
			 * handle a secrets request.
			 */
			ce_page_complete_init (page, setting_name, NULL, NULL);
		} else {
			/* Page wants secrets, get them */
			get_secrets_for_page (editor, page, setting_name);
		}
		g_object_set_data (G_OBJECT (page), SECRETS_TAG, NULL);
	}
	g_slist_free (copy);

	/* set the UI */
	recheck_initialization (editor);
	success = TRUE;

out:
	return success;
}

void
nm_connection_editor_present (NMConnectionEditor *editor)
{
	g_return_if_fail (NM_IS_CONNECTION_EDITOR (editor));

	gtk_window_present (GTK_WINDOW (editor->window));
}

static void
cancel_button_clicked_cb (GtkWidget *widget, gpointer user_data)
{
	NMConnectionEditor *self = NM_CONNECTION_EDITOR (user_data);

	/* If the dialog is busy waiting for authorization or something,
	 * don't destroy it until authorization returns.
	 */
	if (self->busy)
		return;

	g_signal_emit (self, editor_signals[EDITOR_DONE], 0, GTK_RESPONSE_CANCEL);
}

static void
editor_closed_cb (GtkWidget *widget, GdkEvent *event, gpointer user_data)
{
	cancel_button_clicked_cb (widget, user_data);
}

static gboolean
key_press_cb (GtkWidget *widget, GdkEventKey *event, gpointer user_data)
{
	if (event->keyval == GDK_KEY_Escape) {
		gtk_window_close (GTK_WINDOW (widget));
		return TRUE;
	}

	return FALSE;
}

static void
added_connection_cb (GObject *client,
                     GAsyncResult *result,
                     gpointer user_data)
{
	NMConnectionEditor *self = user_data;
	NMRemoteConnection *connection;
	GError *error = NULL;

	nm_connection_editor_set_busy (self, FALSE);

	connection = nm_client_add_connection_finish (NM_CLIENT (client), result, &error);
	if (error) {
		nm_connection_editor_error (self->parent_window, _("Connection add failed"),
		                            "%s", error->message);
		/* Leave the editor open */
		return;
	}
	g_clear_object (&connection);
	g_clear_error (&error);

	g_signal_emit (self, editor_signals[EDITOR_DONE], 0, GTK_RESPONSE_OK);
}

static void
update_complete (NMConnectionEditor *self, GError *error)
{
	nm_connection_editor_set_busy (self, FALSE);
	g_signal_emit (self, editor_signals[EDITOR_DONE], 0, GTK_RESPONSE_OK);
}

static void
updated_connection_cb (GObject *connection,
                       GAsyncResult *result,
                       gpointer user_data)
{
	NMConnectionEditor *self = NM_CONNECTION_EDITOR (user_data);
	GError *error = NULL;

	if (!nm_remote_connection_commit_changes_finish (NM_REMOTE_CONNECTION (connection),
	                                                 result, &error))
		g_message ("Error saving connection: %s", error->message);

	/* Clear secrets so they don't lay around in memory; they'll get requested
	 * again anyway next time the connection is edited.
	 */
	nm_connection_clear_secrets (NM_CONNECTION (connection));

	update_complete (self, error);
	g_clear_error (&error);
}

static void
ok_button_clicked_save_connection (NMConnectionEditor *self)
{
	/* Copy the modified connection to the original connection */
	nm_connection_replace_settings_from_connection (self->orig_connection,
	                                                self->connection);
	nm_connection_editor_set_busy (self, TRUE);

	/* Save new CA cert ignore values to GSettings */
	eap_method_ca_cert_ignore_save (self->connection);

	if (self->is_new_connection) {
		nm_client_add_connection_async (self->client,
		                                self->orig_connection,
		                                TRUE,
		                                NULL,
		                                added_connection_cb,
		                                self);
	} else {
		nm_remote_connection_commit_changes_async (NM_REMOTE_CONNECTION (self->orig_connection),
		                                           TRUE, NULL, updated_connection_cb, self);
	}
}

static void
ok_button_clicked_cb (GtkWidget *widget, gpointer user_data)
{
	NMConnectionEditor *self = NM_CONNECTION_EDITOR (user_data);
	GSList *iter;

	/* If the dialog is busy waiting for authorization or something,
	 * don't destroy it until authorization returns.
	 */
	if (self->busy)
		return;

	/* Validate one last time to ensure all pages update the connection */
	connection_editor_validate (self);

	/* Perform page specific actions before the connection is saved */
	for (iter = self->pages; iter; iter = g_slist_next (iter))
		ce_page_last_update (CE_PAGE (iter->data), self->connection, NULL);

	ok_button_clicked_save_connection (self);
}

static void
vpn_export_get_secrets_cb (GObject *object,
                           GAsyncResult *result,
                           gpointer user_data)
{
	NMConnection *tmp;
	GVariant *secrets;
	GError *error = NULL;

	secrets = nm_remote_connection_get_secrets_finish (NM_REMOTE_CONNECTION (object),
	                                                   result, &error);

	/* We don't really care about errors; if the user couldn't authenticate
	 * then just let them export everything except secrets.  Duplicate the
	 * connection so that we don't let secrets sit around in the original
	 * one.
	 */
	tmp = nm_simple_connection_new_clone (NM_CONNECTION (object));
	g_assert (tmp);
	if (secrets)
		nm_connection_update_secrets (tmp, NM_SETTING_VPN_SETTING_NAME, secrets, NULL);
	vpn_export (tmp);
	g_object_unref (tmp);
	if (secrets)
		g_variant_ref (secrets);
	g_clear_error (&error);
}

static void
export_button_clicked_cb (GtkWidget *widget, gpointer user_data)
{
	NMConnectionEditor *self = NM_CONNECTION_EDITOR (user_data);

	if (NM_IS_REMOTE_CONNECTION (self->orig_connection)) {
		/* Grab secrets if we can */
		nm_remote_connection_get_secrets_async (NM_REMOTE_CONNECTION (self->orig_connection),
		                                        NM_SETTING_VPN_SETTING_NAME,
		                                        NULL,
		                                        vpn_export_get_secrets_cb,
		                                        self);
	} else
		vpn_export (self->connection);
}

void
nm_connection_editor_run (NMConnectionEditor *self)
{
	g_return_if_fail (NM_IS_CONNECTION_EDITOR (self));

	g_signal_connect (G_OBJECT (self->window), "delete-event",
	                  G_CALLBACK (editor_closed_cb), self);
	g_signal_connect (G_OBJECT (self->window), "key-press-event",
	                  G_CALLBACK (key_press_cb), self);

	g_signal_connect (G_OBJECT (self->ok_button), "clicked",
	                  G_CALLBACK (ok_button_clicked_cb), self);
	g_signal_connect (G_OBJECT (self->cancel_button), "clicked",
	                  G_CALLBACK (cancel_button_clicked_cb), self);
	g_signal_connect (G_OBJECT (self->export_button), "clicked",
	                  G_CALLBACK (export_button_clicked_cb), self);
	g_signal_connect (G_OBJECT (self->relabel_button), "clicked",
	                  G_CALLBACK (relabel_button_clicked_cb), self);

	nm_connection_editor_present (self);
}

GtkWindow *
nm_connection_editor_get_window (NMConnectionEditor *editor)
{
	g_return_val_if_fail (NM_IS_CONNECTION_EDITOR (editor), NULL);

	return GTK_WINDOW (editor->window);
}

gboolean
nm_connection_editor_get_busy (NMConnectionEditor *editor)
{
	g_return_val_if_fail (NM_IS_CONNECTION_EDITOR (editor), FALSE);

	return editor->busy;
}

void
nm_connection_editor_set_busy (NMConnectionEditor *editor, gboolean busy)
{
	g_return_if_fail (NM_IS_CONNECTION_EDITOR (editor));

	if (busy != editor->busy) {
		editor->busy = busy;
		gtk_widget_set_sensitive (editor->window, !busy);
	}
}

static void
nm_connection_editor_dialog (GtkWindow *parent, GtkMessageType type, const char *heading,
                             const char *message)
{
	GtkWidget *dialog;

	dialog = gtk_message_dialog_new (parent,
	                                 GTK_DIALOG_DESTROY_WITH_PARENT,
	                                 type,
	                                 GTK_BUTTONS_CLOSE,
	                                 "%s", heading);

	gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog), "%s", message);

	gtk_widget_show_all (dialog);
	gtk_window_present (GTK_WINDOW (dialog));
	gtk_dialog_run (GTK_DIALOG (dialog));
	gtk_widget_destroy (dialog);
}

void
nm_connection_editor_error (GtkWindow *parent, const char *heading, const char *format, ...)
{
	va_list args;
	gs_free char *message = NULL;

	va_start (args, format);
	message = g_strdup_vprintf (format, args);
	va_end (args);
	nm_connection_editor_dialog (parent, GTK_MESSAGE_ERROR, heading, message);
}

void
nm_connection_editor_warning (GtkWindow *parent, const char *heading, const char *format, ...)
{
	va_list args;
	gs_free char *message = NULL;

	va_start (args, format);
	message = g_strdup_vprintf (format, args);
	va_end (args);
	nm_connection_editor_dialog (parent, GTK_MESSAGE_WARNING, heading, message);
}

void
nm_connection_editor_inter_page_set_value (NMConnectionEditor *editor, InterPageChangeType type, gpointer value)
{
	g_hash_table_insert (editor->inter_page_hash, GUINT_TO_POINTER (type), value);
}

gboolean
nm_connection_editor_inter_page_get_value (NMConnectionEditor *editor, InterPageChangeType type, gpointer *value)
{
	return g_hash_table_lookup_extended (editor->inter_page_hash, GUINT_TO_POINTER (type), NULL, value);
}

void
nm_connection_editor_inter_page_clear_data (NMConnectionEditor *editor)
{
	g_hash_table_remove_all (editor->inter_page_hash);
}

