/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- * * Copyright (C) 2012 Red Hat, Inc * * Licensed under the GNU General Public License Version 2 * * 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 2 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, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "config.h" #include #include #include #include "net-connection-editor.h" #include "net-connection-editor-resources.h" #include "ce-page.h" #include "ce-page-details.h" #include "ce-page-wifi.h" #include "ce-page-ip4.h" #include "ce-page-ip6.h" #include "ce-page-security.h" #include "ce-page-ethernet.h" #include "ce-page-8021x-security.h" #include "ce-page-vpn.h" #include "vpn-helpers.h" #include "eap-method.h" enum { DONE, LAST_SIGNAL }; static guint signals[LAST_SIGNAL] = { 0 }; struct _NetConnectionEditor { GtkDialog parent; GtkBox *add_connection_box; AdwBin *add_connection_frame; GtkButton *apply_button; GtkButton *cancel_button; GtkNotebook *notebook; GtkStack *toplevel_stack; NMClient *client; NMDevice *device; NMConnection *connection; NMConnection *orig_connection; gboolean is_new_connection; gboolean is_changed; NMAccessPoint *ap; GSList *initializing_pages; NMClientPermissionResult can_modify; gboolean title_set; }; G_DEFINE_TYPE (NetConnectionEditor, net_connection_editor, GTK_TYPE_DIALOG) static void page_changed (NetConnectionEditor *self); static void cancel_editing (NetConnectionEditor *self) { g_signal_emit (self, signals[DONE], 0, FALSE); gtk_window_destroy (GTK_WINDOW (self)); } static void close_request_cb (NetConnectionEditor *self) { cancel_editing (self); } static void cancel_clicked_cb (NetConnectionEditor *self) { cancel_editing (self); } static void update_connection (NetConnectionEditor *self) { g_autoptr(GVariant) settings = NULL; settings = nm_connection_to_dbus (self->connection, NM_CONNECTION_SERIALIZE_ALL); nm_connection_replace_settings (self->orig_connection, settings, NULL); } static void update_complete (NetConnectionEditor *self, gboolean success) { gtk_widget_hide (GTK_WIDGET (self)); g_signal_emit (self, signals[DONE], 0, success); } static void updated_connection_cb (GObject *source_object, GAsyncResult *res, gpointer user_data) { NetConnectionEditor *self; g_autoptr(GError) error = NULL; gboolean success = TRUE; if (!nm_remote_connection_commit_changes_finish (NM_REMOTE_CONNECTION (source_object), res, &error)) { g_warning ("Failed to commit changes: %s", error->message); success = FALSE; //return; FIXME return if cancelled } nm_connection_clear_secrets (NM_CONNECTION (source_object)); self = user_data; update_complete (self, success); } static void added_connection_cb (GObject *source_object, GAsyncResult *res, gpointer user_data) { NetConnectionEditor *self; g_autoptr(GError) error = NULL; gboolean success = TRUE; if (!nm_client_add_connection_finish (NM_CLIENT (source_object), res, &error)) { g_warning ("Failed to add connection: %s", error->message); success = FALSE; /* Leave the editor open */ // return; FIXME return if cancelled } self = user_data; update_complete (self, success); } static void apply_clicked_cb (NetConnectionEditor *self) { update_connection (self); 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 net_connection_editor_init (NetConnectionEditor *self) { gtk_widget_init_template (GTK_WIDGET (self)); } static void net_connection_editor_finalize (GObject *object) { NetConnectionEditor *self = NET_CONNECTION_EDITOR (object); g_clear_object (&self->connection); g_clear_object (&self->orig_connection); g_clear_object (&self->device); g_clear_object (&self->client); g_clear_object (&self->ap); G_OBJECT_CLASS (net_connection_editor_parent_class)->finalize (object); } static void net_connection_editor_class_init (NetConnectionEditorClass *class) { GObjectClass *object_class = G_OBJECT_CLASS (class); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class); g_resources_register (net_connection_editor_get_resource ()); object_class->finalize = net_connection_editor_finalize; signals[DONE] = g_signal_new ("done", G_OBJECT_CLASS_TYPE (object_class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_BOOLEAN); gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/network/connection-editor.ui"); gtk_widget_class_bind_template_child (widget_class, NetConnectionEditor, add_connection_box); gtk_widget_class_bind_template_child (widget_class, NetConnectionEditor, add_connection_frame); gtk_widget_class_bind_template_child (widget_class, NetConnectionEditor, apply_button); gtk_widget_class_bind_template_child (widget_class, NetConnectionEditor, cancel_button); gtk_widget_class_bind_template_child (widget_class, NetConnectionEditor, notebook); gtk_widget_class_bind_template_child (widget_class, NetConnectionEditor, toplevel_stack); gtk_widget_class_bind_template_callback (widget_class, cancel_clicked_cb); gtk_widget_class_bind_template_callback (widget_class, close_request_cb); gtk_widget_class_bind_template_callback (widget_class, apply_clicked_cb); } static void net_connection_editor_error_dialog (NetConnectionEditor *self, const char *primary_text, const char *secondary_text) { GtkWidget *dialog; GtkWindow *parent; if (gtk_widget_is_visible (GTK_WIDGET (self))) parent = GTK_WINDOW (self); else parent = gtk_window_get_transient_for (GTK_WINDOW (self)); dialog = gtk_message_dialog_new (parent, GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, "%s", primary_text); if (secondary_text) { gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog), "%s", secondary_text); } gtk_window_present (GTK_WINDOW (dialog)); } static void net_connection_editor_do_fallback (NetConnectionEditor *self, const gchar *type) { g_autofree gchar *cmdline = NULL; g_autoptr(GError) error = NULL; if (self->is_new_connection) { cmdline = g_strdup_printf ("nm-connection-editor --type='%s' --create", type); } else { cmdline = g_strdup_printf ("nm-connection-editor --edit='%s'", nm_connection_get_uuid (self->connection)); } g_spawn_command_line_async (cmdline, &error); if (error) net_connection_editor_error_dialog (self, _("Unable to open connection editor"), error->message); g_signal_emit (self, signals[DONE], 0, FALSE); } static void net_connection_editor_update_title (NetConnectionEditor *self) { g_autofree gchar *id = NULL; if (self->title_set) return; if (self->is_new_connection) { if (self->device) { id = g_strdup (_("New Profile")); } else { /* Leave it set to "Add New Connection" */ return; } } else { NMSettingWireless *sw; sw = nm_connection_get_setting_wireless (self->connection); if (sw) { GBytes *ssid; ssid = nm_setting_wireless_get_ssid (sw); id = nm_utils_ssid_to_utf8 (g_bytes_get_data (ssid, NULL), g_bytes_get_size (ssid)); } else { id = g_strdup (nm_connection_get_id (self->connection)); } } gtk_window_set_title (GTK_WINDOW (self), id); } static gboolean editor_is_initialized (NetConnectionEditor *self) { return self->initializing_pages == NULL; } static void update_sensitivity (NetConnectionEditor *self) { NMSettingConnection *sc; gboolean sensitive; gint i; if (!editor_is_initialized (self)) return; sc = nm_connection_get_setting_connection (self->connection); if (nm_setting_connection_get_read_only (sc)) { sensitive = FALSE; } else { sensitive = self->can_modify; } for (i = 0; i < gtk_notebook_get_n_pages (self->notebook); i++) { GtkWidget *page = gtk_notebook_get_nth_page (self->notebook, i); gtk_widget_set_sensitive (page, sensitive); } } static void validate (NetConnectionEditor *self) { gboolean valid = FALSE; g_autofree gchar *apply_tooltip = NULL; gint i; if (!editor_is_initialized (self)) goto done; valid = TRUE; for (i = 0; i < gtk_notebook_get_n_pages (self->notebook); i++) { CEPage *page = CE_PAGE (gtk_notebook_get_nth_page (self->notebook, i)); g_autoptr(GError) error = NULL; if (!ce_page_validate (page, self->connection, &error)) { valid = FALSE; if (error) { apply_tooltip = g_strdup_printf (_("Invalid setting %s: %s"), ce_page_get_title (page), error->message); g_debug ("%s", apply_tooltip); } else { apply_tooltip = g_strdup_printf (_("Invalid setting %s"), ce_page_get_title (page)); g_debug ("%s", apply_tooltip); } } } update_sensitivity (self); done: if (apply_tooltip != NULL) gtk_widget_set_tooltip_text(GTK_WIDGET (self->apply_button), apply_tooltip); gtk_widget_set_sensitive (GTK_WIDGET (self->apply_button), valid && self->is_changed); } static void page_changed (NetConnectionEditor *self) { if (editor_is_initialized (self)) self->is_changed = TRUE; validate (self); } static gboolean idle_validate (gpointer user_data) { validate (NET_CONNECTION_EDITOR (user_data)); return G_SOURCE_REMOVE; } static void recheck_initialization (NetConnectionEditor *self) { if (!editor_is_initialized (self)) return; gtk_stack_set_visible_child (self->toplevel_stack, GTK_WIDGET (self->notebook)); gtk_notebook_set_current_page (self->notebook, 0); g_idle_add (idle_validate, self); } static void page_initialized (NetConnectionEditor *self, GError *error, CEPage *page) { GtkWidget *label; gint position; gint i; position = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (page), "position")); g_object_set_data (G_OBJECT (page), "position", GINT_TO_POINTER (position)); for (i = 0; i < gtk_notebook_get_n_pages (self->notebook); i++) { GtkWidget *page = gtk_notebook_get_nth_page (self->notebook, i); gint pos = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (page), "position")); if (pos > position) break; } label = gtk_label_new (ce_page_get_title (page)); gtk_notebook_insert_page (self->notebook, GTK_WIDGET (page), label, i); self->initializing_pages = g_slist_remove (self->initializing_pages, page); recheck_initialization (self); } typedef struct { NetConnectionEditor *editor; CEPage *page; const gchar *setting_name; } GetSecretsInfo; static void get_secrets_cb (GObject *source_object, GAsyncResult *res, gpointer user_data) { NMRemoteConnection *connection; g_autofree GetSecretsInfo *info = user_data; g_autoptr(GError) error = NULL; g_autoptr(GVariant) variant = NULL; connection = NM_REMOTE_CONNECTION (source_object); variant = nm_remote_connection_get_secrets_finish (connection, res, &error); if (!variant && g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) return; ce_page_complete_init (info->page, info->editor->connection, info->setting_name, variant, g_steal_pointer (&error)); } static void get_secrets_for_page (NetConnectionEditor *self, CEPage *page, const gchar *setting_name) { GetSecretsInfo *info; info = g_new0 (GetSecretsInfo, 1); info->editor = self; info->page = page; info->setting_name = setting_name; nm_remote_connection_get_secrets_async (NM_REMOTE_CONNECTION (self->orig_connection), setting_name, NULL, //FIXME get_secrets_cb, info); } static void add_page (NetConnectionEditor *self, CEPage *page) { gint position; position = g_slist_length (self->initializing_pages); g_object_set_data (G_OBJECT (page), "position", GINT_TO_POINTER (position)); self->initializing_pages = g_slist_append (self->initializing_pages, page); g_signal_connect_object (page, "changed", G_CALLBACK (page_changed), self, G_CONNECT_SWAPPED); g_signal_connect_object (page, "initialized", G_CALLBACK (page_initialized), self, G_CONNECT_SWAPPED); } static void net_connection_editor_set_connection (NetConnectionEditor *self, NMConnection *connection) { GSList *pages, *l; NMSettingConnection *sc; const gchar *type; gboolean is_wired; gboolean is_wifi; gboolean is_vpn; self->is_new_connection = !nm_client_get_connection_by_uuid (self->client, nm_connection_get_uuid (connection)); if (self->is_new_connection) { gtk_button_set_label (self->apply_button, _("_Add")); self->is_changed = TRUE; } self->connection = nm_simple_connection_new_clone (connection); self->orig_connection = g_object_ref (connection); net_connection_editor_update_title (self); eap_method_ca_cert_ignore_load (self->connection); sc = nm_connection_get_setting_connection (connection); type = nm_setting_connection_get_connection_type (sc); is_wired = g_str_equal (type, NM_SETTING_WIRED_SETTING_NAME); is_wifi = g_str_equal (type, NM_SETTING_WIRELESS_SETTING_NAME); is_vpn = g_str_equal (type, NM_SETTING_VPN_SETTING_NAME); if (!self->is_new_connection) add_page (self, CE_PAGE (ce_page_details_new (self->connection, self->device, self->ap, self))); if (is_wifi) add_page (self, CE_PAGE (ce_page_wifi_new (self->connection, self->client))); else if (is_wired) add_page (self, CE_PAGE (ce_page_ethernet_new (self->connection, self->client))); else if (is_vpn) add_page (self, CE_PAGE (ce_page_vpn_new (self->connection))); else { /* Unsupported type */ net_connection_editor_do_fallback (self, type); return; } add_page (self, CE_PAGE (ce_page_ip4_new (self->connection, self->client))); add_page (self, CE_PAGE (ce_page_ip6_new (self->connection, self->client))); if (is_wifi) add_page (self, CE_PAGE (ce_page_security_new (self->connection))); else if (is_wired) add_page (self, CE_PAGE (ce_page_8021x_security_new (self->connection))); pages = g_slist_copy (self->initializing_pages); for (l = pages; l; l = l->next) { CEPage *page = l->data; const gchar *security_setting; security_setting = ce_page_get_security_setting (page); if (!security_setting || self->is_new_connection) { ce_page_complete_init (page, NULL, NULL, NULL, NULL); } else { get_secrets_for_page (self, page, security_setting); } } g_slist_free (pages); } static NMConnection * complete_vpn_connection (NetConnectionEditor *self, NMConnection *connection) { NMSettingConnection *s_con; NMSetting *s_type; if (!connection) connection = nm_simple_connection_new (); s_con = nm_connection_get_setting_connection (connection); if (!s_con) { s_con = NM_SETTING_CONNECTION (nm_setting_connection_new ()); nm_connection_add_setting (connection, NM_SETTING (s_con)); } if (!nm_setting_connection_get_uuid (s_con)) { g_autofree gchar *uuid = nm_utils_uuid_generate (); g_object_set (s_con, NM_SETTING_CONNECTION_UUID, uuid, NULL); } if (!nm_setting_connection_get_id (s_con)) { const GPtrArray *connections; g_autofree gchar *id = NULL; connections = nm_client_get_connections (self->client); id = ce_page_get_next_available_name (connections, NAME_FORMAT_TYPE, _("VPN")); g_object_set (s_con, NM_SETTING_CONNECTION_ID, id, NULL); } s_type = nm_connection_get_setting (connection, NM_TYPE_SETTING_VPN); if (!s_type) { s_type = g_object_new (NM_TYPE_SETTING_VPN, NULL); nm_connection_add_setting (connection, s_type); } if (!nm_setting_connection_get_connection_type (s_con)) { g_object_set (s_con, NM_SETTING_CONNECTION_TYPE, nm_setting_get_name (s_type), NULL); } return connection; } static void finish_add_connection (NetConnectionEditor *self, NMConnection *connection) { adw_bin_set_child (self->add_connection_frame, NULL); gtk_stack_set_visible_child (self->toplevel_stack, GTK_WIDGET (self->notebook)); gtk_widget_show (GTK_WIDGET (self->apply_button)); if (connection) net_connection_editor_set_connection (self, connection); } static void vpn_import_complete (NMConnection *connection, gpointer user_data) { NetConnectionEditor *self = user_data; if (!connection) { /* The import code shows its own error dialogs. */ g_signal_emit (self, signals[DONE], 0, FALSE); return; } complete_vpn_connection (self, connection); finish_add_connection (self, connection); } static void vpn_type_activated (NetConnectionEditor *self, GtkWidget *row) { const char *service_name = g_object_get_data (G_OBJECT (row), "service_name"); NMConnection *connection; NMSettingVpn *s_vpn; NMSettingConnection *s_con; if (!strcmp (service_name, "import")) { vpn_import (GTK_WINDOW (self), vpn_import_complete, self); return; } connection = complete_vpn_connection (self, NULL); s_vpn = nm_connection_get_setting_vpn (connection); g_object_set (s_vpn, NM_SETTING_VPN_SERVICE_TYPE, service_name, NULL); /* Mark the connection as private to this user, and non-autoconnect */ s_con = nm_connection_get_setting_connection (connection); g_object_set (s_con, NM_SETTING_CONNECTION_AUTOCONNECT, FALSE, NULL); nm_setting_connection_add_permission (s_con, "user", g_get_user_name (), NULL); finish_add_connection (self, connection); } static void select_vpn_type (NetConnectionEditor *self, GtkListBox *list) { GSList *vpn_plugins, *iter; GtkWidget *row, *row_box; GtkWidget *name_label, *desc_label; GtkWidget *child; /* Get the available VPN types */ vpn_plugins = vpn_get_plugins (); /* Remove the previous menu contents */ while ((child = gtk_widget_get_first_child (GTK_WIDGET (list))) != NULL) gtk_list_box_remove (list, child); /* Add the VPN types */ for (iter = vpn_plugins; iter; iter = iter->next) { NMVpnEditorPlugin *plugin = nm_vpn_plugin_info_get_editor_plugin (iter->data); g_autofree gchar *name = NULL; g_autofree gchar *desc = NULL; g_autofree gchar *desc_markup = NULL; g_autofree gchar *service_name = NULL; g_object_get (plugin, NM_VPN_EDITOR_PLUGIN_NAME, &name, NM_VPN_EDITOR_PLUGIN_DESCRIPTION, &desc, NM_VPN_EDITOR_PLUGIN_SERVICE, &service_name, NULL); desc_markup = g_markup_printf_escaped ("%s", desc); row = gtk_list_box_row_new (); row_box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6); gtk_widget_set_margin_start (row_box, 12); gtk_widget_set_margin_end (row_box, 12); gtk_widget_set_margin_top (row_box, 12); gtk_widget_set_margin_bottom (row_box, 12); name_label = gtk_label_new (name); gtk_widget_set_halign (name_label, GTK_ALIGN_START); gtk_box_append (GTK_BOX (row_box), name_label); desc_label = gtk_label_new (NULL); gtk_label_set_markup (GTK_LABEL (desc_label), desc_markup); gtk_label_set_wrap (GTK_LABEL (desc_label), TRUE); gtk_widget_set_halign (desc_label, GTK_ALIGN_START); gtk_widget_add_css_class (desc_label, "dim-label"); gtk_box_append (GTK_BOX (row_box), desc_label); gtk_list_box_row_set_child (GTK_LIST_BOX_ROW (row), row_box); g_object_set_data_full (G_OBJECT (row), "service_name", g_steal_pointer (&service_name), g_free); gtk_list_box_append (list, row); } /* Import */ row = gtk_list_box_row_new (); row_box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6); gtk_widget_set_margin_start (row_box, 12); gtk_widget_set_margin_end (row_box, 12); gtk_widget_set_margin_top (row_box, 12); gtk_widget_set_margin_bottom (row_box, 12); name_label = gtk_label_new (_("Import from fileā€¦")); gtk_widget_set_halign (name_label, GTK_ALIGN_START); gtk_box_append (GTK_BOX (row_box), name_label); gtk_list_box_row_set_child (GTK_LIST_BOX_ROW (row), row_box); g_object_set_data (G_OBJECT (row), "service_name", "import"); gtk_list_box_append (list, row); g_signal_connect_object (list, "row-activated", G_CALLBACK (vpn_type_activated), self, G_CONNECT_SWAPPED); } static void net_connection_editor_add_connection (NetConnectionEditor *self) { GtkListBox *list; list = GTK_LIST_BOX (gtk_list_box_new ()); gtk_list_box_set_selection_mode (list, GTK_SELECTION_NONE); select_vpn_type (self, list); adw_bin_set_child (self->add_connection_frame, GTK_WIDGET (list)); gtk_stack_set_visible_child (self->toplevel_stack, GTK_WIDGET (self->add_connection_box)); gtk_widget_hide (GTK_WIDGET (self->apply_button)); gtk_window_set_title (GTK_WINDOW (self), _("Add VPN")); } static void permission_changed (NetConnectionEditor *self, NMClientPermission permission, NMClientPermissionResult result) { if (permission != NM_CLIENT_PERMISSION_SETTINGS_MODIFY_SYSTEM) return; if (result == NM_CLIENT_PERMISSION_RESULT_YES || result == NM_CLIENT_PERMISSION_RESULT_AUTH) self->can_modify = TRUE; else self->can_modify = FALSE; validate (self); } NetConnectionEditor * net_connection_editor_new (NMConnection *connection, NMDevice *device, NMAccessPoint *ap, NMClient *client) { NetConnectionEditor *self; self = g_object_new (net_connection_editor_get_type (), /* This doesn't seem to work for a template, so it is also hardcoded. */ "use-header-bar", 1, NULL); if (ap) self->ap = g_object_ref (ap); if (device) self->device = g_object_ref (device); self->client = g_object_ref (client); self->can_modify = nm_client_get_permission_result (client, NM_CLIENT_PERMISSION_SETTINGS_MODIFY_SYSTEM); g_signal_connect_object (self->client, "permission-changed", G_CALLBACK (permission_changed), self, G_CONNECT_SWAPPED); if (connection) net_connection_editor_set_connection (self, connection); else net_connection_editor_add_connection (self); return self; } static void forgotten_cb (GObject *source_object, GAsyncResult *res, gpointer user_data) { NMRemoteConnection *connection = NM_REMOTE_CONNECTION (source_object); NetConnectionEditor *self = user_data; g_autoptr(GError) error = NULL; if (!nm_remote_connection_delete_finish (connection, res, &error)) { if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) g_warning ("Failed to delete connection %s: %s", nm_connection_get_id (NM_CONNECTION (connection)), error->message); return; } cancel_editing (self); } void net_connection_editor_forget (NetConnectionEditor *self) { nm_remote_connection_delete_async (NM_REMOTE_CONNECTION (self->orig_connection), NULL, forgotten_cb, self); } void net_connection_editor_set_title (NetConnectionEditor *self, const gchar *title) { gtk_window_set_title (GTK_WINDOW (self), title); self->title_set = TRUE; }