From 78496751298a0a31a2735a7b65412ceaeb206279 Mon Sep 17 00:00:00 2001 From: JosephMcc Date: Sun, 20 Oct 2024 14:03:38 -0700 Subject: [PATCH 1/4] Add a new StPasswordEntry widget We are moving to new dialogs in Cinnamon and several contain password entries. This gives a new entry to use for those dialogs that features a a show/hide password feature --- ...actions_scalable_view-conceal-symbolic.svg | 4 + ..._actions_scalable_view-reveal-symbolic.svg | 88 ++++++ data/theme/cinnamon-sass/_common.scss | 5 + src/st/meson.build | 2 + src/st/st-password-entry.c | 286 ++++++++++++++++++ src/st/st-password-entry.h | 67 ++++ 6 files changed, 452 insertions(+) create mode 100644 data/icons/hicolor_actions_scalable_view-conceal-symbolic.svg create mode 100644 data/icons/hicolor_actions_scalable_view-reveal-symbolic.svg create mode 100644 src/st/st-password-entry.c create mode 100644 src/st/st-password-entry.h diff --git a/data/icons/hicolor_actions_scalable_view-conceal-symbolic.svg b/data/icons/hicolor_actions_scalable_view-conceal-symbolic.svg new file mode 100644 index 0000000000..a168c87d8a --- /dev/null +++ b/data/icons/hicolor_actions_scalable_view-conceal-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/data/icons/hicolor_actions_scalable_view-reveal-symbolic.svg b/data/icons/hicolor_actions_scalable_view-reveal-symbolic.svg new file mode 100644 index 0000000000..c9d734be71 --- /dev/null +++ b/data/icons/hicolor_actions_scalable_view-reveal-symbolic.svg @@ -0,0 +1,88 @@ + + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + Gnome Symbolic Icon Theme + + + + + + + + + + + + + + diff --git a/data/theme/cinnamon-sass/_common.scss b/data/theme/cinnamon-sass/_common.scss index 907b779d15..14e7e99c3d 100644 --- a/data/theme/cinnamon-sass/_common.scss +++ b/data/theme/cinnamon-sass/_common.scss @@ -56,6 +56,11 @@ stage { StLabel.hint-text { color: transparentize($fg_color, 0.7); } .capslock-warning { icon-size: $scalable_icon_size; } + + StIcon.peek-password { + icon-size: 16px; + color: $fg_color; + } } // buttons in dialogs diff --git a/src/st/meson.build b/src/st/meson.build index 7c02a9894d..17f1f7f43d 100644 --- a/src/st/meson.build +++ b/src/st/meson.build @@ -15,6 +15,7 @@ st_headers = [ 'st-icon-colors.h', 'st-label.h', 'st-image-content.h', + 'st-password-entry.h', 'st-polygon.h', 'st-private.h', 'st-scrollable.h', @@ -114,6 +115,7 @@ st_gir_sources = [ 'st-icon-colors.c', 'st-image-content.c', 'st-label.c', + 'st-password-entry.c', 'st-polygon.c', 'st-private.c', 'st-scrollable.c', diff --git a/src/st/st-password-entry.c b/src/st/st-password-entry.c new file mode 100644 index 0000000000..5364ed8b4a --- /dev/null +++ b/src/st/st-password-entry.c @@ -0,0 +1,286 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-password-entry.c: Password entry actor based on st-entry + * + * Copyright 2019 Endless Inc. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +#include "st-private.h" +#include "st-password-entry.h" +#include "st-icon.h" + +#define BLACK_CIRCLE 9679 + +enum +{ + PROP_0, + + PROP_PASSWORD_VISIBLE, + PROP_SHOW_PEEK_ICON, +}; + +#define ST_PASSWORD_ENTRY_PRIV(x) ((StPasswordEntry *) x)->priv + +struct _StPasswordEntryPrivate +{ + ClutterActor *peek_password_icon; + gboolean password_visible; + gboolean show_peek_icon; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (StPasswordEntry, st_password_entry, ST_TYPE_ENTRY); + +static void +st_password_entry_secondary_icon_clicked (StEntry *entry) +{ + StPasswordEntry *password_entry = ST_PASSWORD_ENTRY (entry); + StPasswordEntryPrivate *priv = ST_PASSWORD_ENTRY_PRIV (password_entry); + + st_password_entry_set_password_visible (password_entry, !priv->password_visible); +} + +static void +st_password_entry_get_property (GObject *gobject, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + StPasswordEntryPrivate *priv = ST_PASSWORD_ENTRY_PRIV (gobject); + + switch (prop_id) + { + case PROP_PASSWORD_VISIBLE: + g_value_set_boolean (value, priv->password_visible); + break; + + case PROP_SHOW_PEEK_ICON: + g_value_set_boolean (value, priv->show_peek_icon); + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + break; + } +} + +static void +st_password_entry_set_property (GObject *gobject, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + StPasswordEntry *entry = ST_PASSWORD_ENTRY (gobject); + + switch (prop_id) + { + case PROP_PASSWORD_VISIBLE: + st_password_entry_set_password_visible (entry, g_value_get_boolean (value)); + break; + + case PROP_SHOW_PEEK_ICON: + st_password_entry_set_show_peek_icon (entry, g_value_get_boolean (value)); + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + break; + } +} + +static void +st_password_entry_dispose (GObject *gobject) +{ + StPasswordEntryPrivate *priv = ST_PASSWORD_ENTRY_PRIV (gobject); + + g_clear_object (&priv->peek_password_icon); + + G_OBJECT_CLASS(st_password_entry_parent_class)->dispose (gobject); +} + +static void +st_password_entry_class_init (StPasswordEntryClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + StEntryClass *st_entry_class = ST_ENTRY_CLASS (klass); + GParamSpec *pspec; + + st_entry_class->secondary_icon_clicked = st_password_entry_secondary_icon_clicked; + + gobject_class->get_property = st_password_entry_get_property; + gobject_class->set_property = st_password_entry_set_property; + gobject_class->dispose = st_password_entry_dispose; + + pspec = g_param_spec_boolean ("password-visible", + "Password visible", + "Whether the text in the entry is masked or not", + FALSE, + ST_PARAM_READWRITE); + g_object_class_install_property (gobject_class, PROP_PASSWORD_VISIBLE, pspec); + + pspec = g_param_spec_boolean ("show-peek-icon", + "Show peek icon", + "Whether to show the password peek icon", + TRUE, + ST_PARAM_READWRITE); + g_object_class_install_property (gobject_class, PROP_SHOW_PEEK_ICON, pspec); +} + +static void +clutter_text_password_char_cb (GObject *object, + GParamSpec *pspec, + gpointer user_data) +{ + StPasswordEntry *entry = ST_PASSWORD_ENTRY (user_data); + ClutterActor *clutter_text; + + clutter_text = st_entry_get_clutter_text (ST_ENTRY (entry)); + if (clutter_text_get_password_char (CLUTTER_TEXT (clutter_text)) == 0) + st_password_entry_set_password_visible (entry, TRUE); + else + st_password_entry_set_password_visible (entry, FALSE); +} + +static void +st_password_entry_init (StPasswordEntry *entry) +{ + StPasswordEntryPrivate *priv; + ClutterActor *clutter_text; + + priv = entry->priv = st_password_entry_get_instance_private (entry); + + priv->peek_password_icon = g_object_new (ST_TYPE_ICON, + "style-class", "peek-password", + "icon-name", "view-conceal-symbolic", + NULL); + st_entry_set_secondary_icon (ST_ENTRY (entry), priv->peek_password_icon); + + priv->show_peek_icon = TRUE; + + clutter_text = st_entry_get_clutter_text (ST_ENTRY (entry)); + clutter_text_set_password_char (CLUTTER_TEXT (clutter_text), BLACK_CIRCLE); + + g_signal_connect (clutter_text, "notify::password-char", + G_CALLBACK (clutter_text_password_char_cb), entry); +} + +/** + * st_password_entry_new: + * + * Create a new #StPasswordEntry. + * + * Returns: a new #StEntry + */ +StEntry* +st_password_entry_new (void) +{ + return ST_ENTRY (g_object_new (ST_TYPE_PASSWORD_ENTRY, NULL)); +} + +/** + * st_password_entry_set_show_peek_icon: + * @entry: a #StPasswordEntry + * @value: #TRUE to show the peek-icon in the entry, #FALSE otherwise + * + * Sets whether to show or hide the peek-icon in the password entry. + */ +void +st_password_entry_set_show_peek_icon (StPasswordEntry *entry, + gboolean value) +{ + StPasswordEntryPrivate *priv; + + g_return_if_fail (ST_IS_PASSWORD_ENTRY (entry)); + + priv = entry->priv = st_password_entry_get_instance_private (entry); + if (priv->show_peek_icon == value) + return; + + priv->show_peek_icon = value; + if (priv->show_peek_icon) + st_entry_set_secondary_icon (ST_ENTRY (entry), priv->peek_password_icon); + else + st_entry_set_secondary_icon (ST_ENTRY (entry), NULL); + + g_object_notify (G_OBJECT (entry), "show-peek-icon"); +} + +/** + * st_password_entry_get_show_peek_icon: + * @entry: a #StPasswordEntry + * + * Gets whether peek-icon is shown or hidden in the password entry. + */ +gboolean +st_password_entry_get_show_peek_icon (StPasswordEntry *entry) +{ + StPasswordEntryPrivate *priv; + + g_return_val_if_fail (ST_IS_PASSWORD_ENTRY (entry), TRUE); + + priv = ST_PASSWORD_ENTRY_PRIV (entry); + return priv->show_peek_icon; +} + +/** + * st_password_entry_set_password_visible: + * @entry: a #StPasswordEntry + * @value: #TRUE to show the password in the entry, #FALSE otherwise + * + * Sets whether to show or hide text in the password entry. + */ +void +st_password_entry_set_password_visible (StPasswordEntry *entry, + gboolean value) +{ + StPasswordEntryPrivate *priv; + ClutterActor *clutter_text; + + g_return_if_fail (ST_IS_PASSWORD_ENTRY (entry)); + + priv = entry->priv; + if (priv->password_visible == value) + return; + + priv->password_visible = value; + + clutter_text = st_entry_get_clutter_text (ST_ENTRY (entry)); + if (priv->password_visible) + { + clutter_text_set_password_char (CLUTTER_TEXT (clutter_text), 0); + st_icon_set_icon_name (ST_ICON (priv->peek_password_icon), "view-reveal-symbolic"); + } + else + { + clutter_text_set_password_char (CLUTTER_TEXT (clutter_text), BLACK_CIRCLE); + st_icon_set_icon_name (ST_ICON (priv->peek_password_icon), "view-conceal-symbolic"); + } + + g_object_notify (G_OBJECT (entry), "password-visible"); +} + +/** + * st_password_entry_get_password_visible: + * @entry: a #StPasswordEntry + * + * Gets whether the text is masked in the password entry. + */ +gboolean +st_password_entry_get_password_visible (StPasswordEntry *entry) +{ + StPasswordEntryPrivate *priv; + + g_return_val_if_fail (ST_IS_PASSWORD_ENTRY (entry), FALSE); + + priv = entry->priv; + return priv->password_visible; +} diff --git a/src/st/st-password-entry.h b/src/st/st-password-entry.h new file mode 100644 index 0000000000..bb5f9fc94d --- /dev/null +++ b/src/st/st-password-entry.h @@ -0,0 +1,67 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-password-entry.h: Password entry actor based on st-entry + * + * Copyright 2019 Endless Inc. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +#if !defined(ST_H_INSIDE) && !defined(ST_COMPILATION) +#error "Only can be included directly.h" +#endif + +#ifndef __ST_PASSWORD_ENTRY_H__ +#define __ST_PASSWORD_ENTRY_H__ + +G_BEGIN_DECLS + +#include "st-entry.h" + +#define ST_TYPE_PASSWORD_ENTRY (st_password_entry_get_type ()) +#define ST_PASSWORD_ENTRY(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), ST_TYPE_PASSWORD_ENTRY, StPasswordEntry)) +#define ST_IS_PASSWORD_ENTRY(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), ST_TYPE_PASSWORD_ENTRY)) +#define ST_PASSWORD_ENTRY_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), ST_TYPE_PASSWORD_ENTRY, StPasswordEntryClass)) +#define ST_IS_PASSWORD_ENTRY_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), ST_TYPE_PASSWORD_ENTRY)) +#define ST_PASSWORD_ENTRY_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), ST_TYPE_PASSWORD_ENTRY, StPasswordEntryClass)) + +typedef struct _StPasswordEntry StPasswordEntry; +typedef struct _StPasswordEntryPrivate StPasswordEntryPrivate; +typedef struct _StPasswordEntryClass StPasswordEntryClass; + +struct _StPasswordEntry +{ + /*< private >*/ + StEntry parent_instance; + + StPasswordEntryPrivate *priv; +}; + +struct _StPasswordEntryClass +{ + StEntryClass parent_class; +}; + +GType st_password_entry_get_type (void) G_GNUC_CONST; + +StEntry *st_password_entry_new (void); +gboolean st_password_entry_get_password_visible (StPasswordEntry *entry); +void st_password_entry_set_password_visible (StPasswordEntry *entry, + gboolean value); +gboolean st_password_entry_get_show_peek_icon (StPasswordEntry *entry); +void st_password_entry_set_show_peek_icon (StPasswordEntry *entry, + gboolean value); + +G_END_DECLS + +#endif /* __ST_PASSWORD_ENTRY_H__ */ From a933de463b1977c044ae77d02a9499742e4264db Mon Sep 17 00:00:00 2001 From: JosephMcc Date: Sun, 20 Oct 2024 14:11:14 -0700 Subject: [PATCH 2/4] keyringPrompt: Use the StPasswordEntry --- js/ui/keyringPrompt.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/js/ui/keyringPrompt.js b/js/ui/keyringPrompt.js index bb1e565438..14970fa2f7 100644 --- a/js/ui/keyringPrompt.js +++ b/js/ui/keyringPrompt.js @@ -34,25 +34,23 @@ class KeyringDialog extends ModalDialog.ModalDialog { vertical: true, }); - this._passwordEntry = new St.Entry({ + this._passwordEntry = new St.PasswordEntry({ style_class: 'prompt-dialog-password-entry', can_focus: true, x_align: Clutter.ActorAlign.CENTER, }); - this._passwordEntry.clutter_text.set_password_char('\u25cf'); // ● U+25CF BLACK CIRCLE - CinnamonEntry.addContextMenu(this._passwordEntry, { isPassword: true }); + CinnamonEntry.addContextMenu(this._passwordEntry); this._passwordEntry.clutter_text.connect('activate', this._onPasswordActivate.bind(this)); this.prompt.bind_property('password-visible', this._passwordEntry, 'visible', GObject.BindingFlags.SYNC_CREATE); passwordBox.add_child(this._passwordEntry); - this._confirmEntry = new St.Entry({ + this._confirmEntry = new St.PasswordEntry({ style_class: 'prompt-dialog-password-entry', can_focus: true, x_align: Clutter.ActorAlign.CENTER, }); - this._confirmEntry.clutter_text.set_password_char('\u25cf'); // ● U+25CF BLACK CIRCLE - CinnamonEntry.addContextMenu(this._confirmEntry, { isPassword: true }); + CinnamonEntry.addContextMenu(this._confirmEntry); this._confirmEntry.clutter_text.connect('activate', this._onConfirmActivate.bind(this)); this.prompt.bind_property('confirm-visible', this._confirmEntry, 'visible', GObject.BindingFlags.SYNC_CREATE); From 242065a3c5afacdc45499038f9adc4bce0600f14 Mon Sep 17 00:00:00 2001 From: JosephMcc Date: Sun, 20 Oct 2024 14:43:17 -0700 Subject: [PATCH 3/4] polkitAuthenticationDialog: Use the new StPasswordEntry --- js/ui/polkitAuthenticationAgent.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/js/ui/polkitAuthenticationAgent.js b/js/ui/polkitAuthenticationAgent.js index c1c5cf4d63..396c6f96f0 100644 --- a/js/ui/polkitAuthenticationAgent.js +++ b/js/ui/polkitAuthenticationAgent.js @@ -105,14 +105,14 @@ var AuthenticationDialog = GObject.registerClass({ vertical: true, }); - this._passwordEntry = new St.Entry({ + this._passwordEntry = new St.PasswordEntry({ style_class: 'prompt-dialog-password-entry', text: "", can_focus: true, visible: false, x_align: Clutter.ActorAlign.CENTER, }); - CinnamonEntry.addContextMenu(this._passwordEntry, { isPassword: true }); + CinnamonEntry.addContextMenu(this._passwordEntry); this._passwordEntry.clutter_text.connect('activate', this._onEntryActivate.bind(this)); this._passwordEntry.bind_property('reactive', this._passwordEntry.clutter_text, 'editable', @@ -121,6 +121,12 @@ var AuthenticationDialog = GObject.registerClass({ let warningBox = new St.BoxLayout({ vertical: true }); + let capsLockWarning = new CinnamonEntry.CapsLockWarning(); + this._passwordEntry.bind_property('visible', + capsLockWarning, 'visible', + GObject.BindingFlags.SYNC_CREATE); + warningBox.add_child(capsLockWarning); + this._errorMessageLabel = new St.Label({ style_class: 'prompt-dialog-error-label', visible: false, @@ -292,10 +298,7 @@ var AuthenticationDialog = GObject.registerClass({ else this._passwordEntry.hint_text = request; - if (echoOn) - this._passwordEntry.clutter_text.set_password_char(''); - else - this._passwordEntry.clutter_text.set_password_char('\u25cf'); // ● U+25CF BLACK CIRCLE + this._passwordEntry.password_visible = echoOn; this._passwordEntry.show(); this._passwordEntry.set_text(''); From 3d6d71af96a08057683d820588982a85b6454a85 Mon Sep 17 00:00:00 2001 From: JosephMcc Date: Sun, 20 Oct 2024 18:41:53 -0700 Subject: [PATCH 4/4] Convert the network auth dialog to a clutter dialog --- debian/control | 1 + js/misc/config.js.in | 2 + js/ui/main.js | 6 + js/ui/networkAgent.js | 776 ++++++++++++++++++++++++++++++ meson.build | 17 +- src/cinnamon-network-agent.c | 899 +++++++++++++++++++++++++++++++++++ src/cinnamon-network-agent.h | 73 +++ src/cinnamon-util.c | 26 + src/cinnamon-util.h | 3 + src/meson.build | 13 + src/st/st-entry.c | 2 +- 11 files changed, 1810 insertions(+), 8 deletions(-) create mode 100644 js/ui/networkAgent.js create mode 100644 src/cinnamon-network-agent.c create mode 100644 src/cinnamon-network-agent.h diff --git a/debian/control b/debian/control index cb96845286..66b1fe0af1 100644 --- a/debian/control +++ b/debian/control @@ -29,6 +29,7 @@ Build-Depends: libpolkit-agent-1-dev (>= 0.100), libpulse-dev, librsvg2-dev, + libsecret-1-dev, libstartup-notification0-dev (>= 0.11), libxapp-dev (>= 2.6.0), meson, diff --git a/js/misc/config.js.in b/js/misc/config.js.in index 463e4f9d16..dd3c8aba80 100644 --- a/js/misc/config.js.in +++ b/js/misc/config.js.in @@ -4,3 +4,5 @@ var PACKAGE_NAME = '@PACKAGE_NAME@'; /* The version of this package */ var PACKAGE_VERSION = '@PACKAGE_VERSION@'; +/* 1 if networkmanager is available, 0 otherwise */ +var HAVE_NETWORKMANAGER = @HAVE_NETWORKMANAGER@; diff --git a/js/ui/main.js b/js/ui/main.js index 40996653ba..e0ff96f439 100644 --- a/js/ui/main.js +++ b/js/ui/main.js @@ -90,6 +90,7 @@ const PointerTracker = imports.misc.pointerTracker; const SoundManager = imports.ui.soundManager; const BackgroundManager = imports.ui.backgroundManager; +const Config = imports.misc.config; const SlideshowManager = imports.ui.slideshowManager; var AppletManager = imports.ui.appletManager; const SearchProviderManager = imports.ui.searchProviderManager; @@ -107,6 +108,7 @@ const KeyringPrompt = imports.ui.keyringPrompt; const RunDialog = imports.ui.runDialog; const Layout = imports.ui.layout; const LookingGlass = imports.ui.lookingGlass; +const NetworkAgent = imports.ui.networkAgent; const NotificationDaemon = imports.ui.notificationDaemon; const WindowAttentionHandler = imports.ui.windowAttentionHandler; const CinnamonDBus = imports.ui.cinnamonDBus; @@ -163,6 +165,7 @@ var xdndHandler = null; var statusIconDispatcher = null; var virtualKeyboard = null; var layoutManager = null; +var networkAgent = null; var monitorLabeler = null; var themeManager = null; var keybindingManager = null; @@ -419,6 +422,9 @@ function start() { windowAttentionHandler = new WindowAttentionHandler.WindowAttentionHandler(); placesManager = new PlacesManager.PlacesManager(); + if (Config.HAVE_NETWORKMANAGER) + networkAgent = new NetworkAgent.NetworkAgent(); + magnifier = new Magnifier.Magnifier(); locatePointer = new LocatePointer.locatePointer(); diff --git a/js/ui/networkAgent.js b/js/ui/networkAgent.js new file mode 100644 index 0000000000..4496b19419 --- /dev/null +++ b/js/ui/networkAgent.js @@ -0,0 +1,776 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +const Clutter = imports.gi.Clutter; +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; +const NM = imports.gi.NM; +const Pango = imports.gi.Pango; +const Cinnamon = imports.gi.Cinnamon; +const St = imports.gi.St; +const Signals = imports.signals; + +const Dialog = imports.ui.dialog; +const Main = imports.ui.main; +const MessageTray = imports.ui.messageTray; +const ModalDialog = imports.ui.modalDialog; +const CinnamonEntry = imports.ui.cinnamonEntry; + +const VPN_UI_GROUP = 'VPN Plugin UI'; + +var NetworkSecretDialog = GObject.registerClass( +class NetworkSecretDialog extends ModalDialog.ModalDialog { + _init(agent, requestId, connection, settingName, hints, flags, contentOverride) { + super._init({ styleClass: 'prompt-dialog' }); + + this._agent = agent; + this._requestId = requestId; + this._connection = connection; + this._settingName = settingName; + this._hints = hints; + + if (contentOverride) + this._content = contentOverride; + else + this._content = this._getContent(); + + let contentBox = new Dialog.MessageDialogContent({ + title: this._content.title, + description: this._content.message, + }); + + let initialFocusSet = false; + for (let i = 0; i < this._content.secrets.length; i++) { + let secret = this._content.secrets[i]; + let reactive = secret.key != null; + + let entryParams = { + style_class: 'prompt-dialog-password-entry', + hint_text: secret.label, + text: secret.value, + can_focus: reactive, + reactive, + x_align: Clutter.ActorAlign.CENTER, + }; + if (secret.password) + secret.entry = new St.PasswordEntry(entryParams); + else + secret.entry = new St.Entry(entryParams); + CinnamonEntry.addContextMenu(secret.entry); + contentBox.add_child(secret.entry); + + if (secret.validate) + secret.valid = secret.validate(secret); + else // no special validation, just ensure it's not empty + secret.valid = secret.value.length > 0; + + if (reactive) { + if (!initialFocusSet) { + this.setInitialKeyFocus(secret.entry); + initialFocusSet = true; + } + + secret.entry.clutter_text.connect('activate', this._onOk.bind(this)); + secret.entry.clutter_text.connect('text-changed', () => { + secret.value = secret.entry.get_text(); + if (secret.validate) + secret.valid = secret.validate(secret); + else + secret.valid = secret.value.length > 0; + this._updateOkButton(); + }); + } else { + secret.valid = true; + } + } + + if (this._content.secrets.some(s => s.password)) { + let capsLockWarning = new CinnamonEntry.CapsLockWarning(); + contentBox.add_child(capsLockWarning); + } + + if (flags & NM.SecretAgentGetSecretsFlags.WPS_PBC_ACTIVE) { + let descriptionLabel = new St.Label({ + text: _('Alternatively you can connect by pushing the “WPS” button on your router.'), + style_class: 'message-dialog-description', + }); + descriptionLabel.clutter_text.line_wrap = true; + descriptionLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE; + + contentBox.add_child(descriptionLabel); + } + + this.contentLayout.add_child(contentBox); + + this._okButton = { + label: _('Connect'), + action: this._onOk.bind(this), + default: true, + }; + + this.setButtons([{ + label: _('Cancel'), + action: this.cancel.bind(this), + key: Clutter.KEY_Escape, + }, this._okButton]); + + this._updateOkButton(); + } + + _updateOkButton() { + let valid = true; + for (let i = 0; i < this._content.secrets.length; i++) { + let secret = this._content.secrets[i]; + valid = valid && secret.valid; + } + + this._okButton.button.reactive = valid; + this._okButton.button.can_focus = valid; + } + + _onOk() { + let valid = true; + for (let i = 0; i < this._content.secrets.length; i++) { + let secret = this._content.secrets[i]; + valid = valid && secret.valid; + if (secret.key != null) + this._agent.set_password(this._requestId, secret.key, secret.value); + } + + if (valid) { + this._agent.respond(this._requestId, Cinnamon.NetworkAgentResponse.CONFIRMED); + this.close(global.get_current_time()); + } + // do nothing if not valid + } + + cancel() { + this._agent.respond(this._requestId, Cinnamon.NetworkAgentResponse.USER_CANCELED); + this.close(global.get_current_time()); + } + + _validateWpaPsk(secret) { + let value = secret.value; + if (value.length === 64) { + // must be composed of hexadecimal digits only + for (let i = 0; i < 64; i++) { + if (!((value[i] >= 'a' && value[i] <= 'f') || + (value[i] >= 'A' && value[i] <= 'F') || + (value[i] >= '0' && value[i] <= '9'))) + return false; + } + return true; + } + + return value.length >= 8 && value.length <= 63; + } + + _validateStaticWep(secret) { + let value = secret.value; + if (secret.wep_key_type === NM.WepKeyType.KEY) { + if (value.length === 10 || value.length === 26) { + for (let i = 0; i < value.length; i++) { + if (!((value[i] >= 'a' && value[i] <= 'f') || + (value[i] >= 'A' && value[i] <= 'F') || + (value[i] >= '0' && value[i] <= '9'))) + return false; + } + } else if (value.length === 5 || value.length === 13) { + for (let i = 0; i < value.length; i++) { + if (!((value[i] >= 'a' && value[i] <= 'z') || + (value[i] >= 'A' && value[i] <= 'Z'))) + return false; + } + } else { + return false; + } + } else if (secret.wep_key_type === NM.WepKeyType.PASSPHRASE) { + if (value.length < 0 || value.length > 64) + return false; + } + return true; + } + + _getWirelessSecrets(secrets, _wirelessSetting) { + let wirelessSecuritySetting = this._connection.get_setting_wireless_security(); + + if (this._settingName === '802-1x') { + this._get8021xSecrets(secrets); + return; + } + + switch (wirelessSecuritySetting.key_mgmt) { + // First the easy ones + case 'wpa-none': + case 'wpa-psk': + case 'sae': + secrets.push({ label: _('Password'), key: 'psk', + value: wirelessSecuritySetting.psk || '', + validate: this._validateWpaPsk, password: true }); + break; + case 'none': // static WEP + secrets.push({ label: _('Key'), key: `wep-key${wirelessSecuritySetting.wep_tx_keyidx}`, + value: wirelessSecuritySetting.get_wep_key(wirelessSecuritySetting.wep_tx_keyidx) || '', + wep_key_type: wirelessSecuritySetting.wep_key_type, + validate: this._validateStaticWep, password: true }); + break; + case 'ieee8021x': + if (wirelessSecuritySetting.auth_alg === 'leap') { // Cisco LEAP + secrets.push({ label: _('Password'), key: 'leap-password', + value: wirelessSecuritySetting.leap_password || '', password: true }); + } else { // Dynamic (IEEE 802.1x) WEP + this._get8021xSecrets(secrets); + } + break; + case 'wpa-eap': + this._get8021xSecrets(secrets); + break; + default: + log(`Invalid wireless key management: ${wirelessSecuritySetting.key_mgmt}`); + } + } + + _get8021xSecrets(secrets) { + let ieee8021xSetting = this._connection.get_setting_802_1x(); + + /* If hints were given we know exactly what we need to ask */ + if (this._settingName === '802-1x' && this._hints.length) { + if (this._hints.includes('identity')) { + secrets.push({ label: _('Username'), key: 'identity', + value: ieee8021xSetting.identity || '', password: false }); + } + if (this._hints.includes('password')) { + secrets.push({ label: _('Password'), key: 'password', + value: ieee8021xSetting.password || '', password: true }); + } + if (this._hints.includes('private-key-password')) { + secrets.push({ label: _('Private key password'), key: 'private-key-password', + value: ieee8021xSetting.private_key_password || '', password: true }); + } + return; + } + + switch (ieee8021xSetting.get_eap_method(0)) { + case 'md5': + case 'leap': + case 'ttls': + case 'peap': + case 'fast': + // TTLS and PEAP are actually much more complicated, but this complication + // is not visible here since we only care about phase2 authentication + // (and don't even care of which one) + secrets.push({ label: _('Username'), key: null, + value: ieee8021xSetting.identity || '', password: false }); + secrets.push({ label: _('Password'), key: 'password', + value: ieee8021xSetting.password || '', password: true }); + break; + case 'tls': + secrets.push({ label: _('Identity'), key: null, + value: ieee8021xSetting.identity || '', password: false }); + secrets.push({ label: _('Private key password'), key: 'private-key-password', + value: ieee8021xSetting.private_key_password || '', password: true }); + break; + default: + log(`Invalid EAP/IEEE802.1x method: ${ieee8021xSetting.get_eap_method(0)}`); + } + } + + _getPPPoESecrets(secrets) { + let pppoeSetting = this._connection.get_setting_pppoe(); + secrets.push({ label: _('Username'), key: 'username', + value: pppoeSetting.username || '', password: false }); + secrets.push({ label: _('Service'), key: 'service', + value: pppoeSetting.service || '', password: false }); + secrets.push({ label: _('Password'), key: 'password', + value: pppoeSetting.password || '', password: true }); + } + + _getMobileSecrets(secrets, connectionType) { + let setting; + if (connectionType === 'bluetooth') + setting = this._connection.get_setting_cdma() || this._connection.get_setting_gsm(); + else + setting = this._connection.get_setting_by_name(connectionType); + secrets.push({ label: _('Password'), key: 'password', + value: setting.value || '', password: true }); + } + + _getContent() { + let connectionSetting = this._connection.get_setting_connection(); + let connectionType = connectionSetting.get_connection_type(); + let wirelessSetting; + let ssid; + + let content = { }; + content.secrets = []; + + switch (connectionType) { + case '802-11-wireless': + wirelessSetting = this._connection.get_setting_wireless(); + ssid = NM.utils_ssid_to_utf8(wirelessSetting.get_ssid().get_data()); + content.title = _('Authentication required'); + content.message = _('Passwords or encryption keys are required to access the wireless network “%s”.').format(ssid); + this._getWirelessSecrets(content.secrets, wirelessSetting); + break; + case '802-3-ethernet': + content.title = _('Wired 802.1X authentication'); + content.message = null; + content.secrets.push({ label: _('Network name'), key: null, + value: connectionSetting.get_id(), password: false }); + this._get8021xSecrets(content.secrets); + break; + case 'pppoe': + content.title = _('DSL authentication'); + content.message = null; + this._getPPPoESecrets(content.secrets); + break; + case 'gsm': + if (this._hints.includes('pin')) { + let gsmSetting = this._connection.get_setting_gsm(); + content.title = _('PIN code required'); + content.message = _('PIN code is needed for the mobile broadband device'); + content.secrets.push({ label: _('PIN'), key: 'pin', + value: gsmSetting.pin || '', password: true }); + break; + } + // fall through + case 'cdma': + case 'bluetooth': + content.title = _('Authentication required'); + content.message = _('A password is required to connect to “%s”.').format(connectionSetting.get_id()); + this._getMobileSecrets(content.secrets, connectionType); + break; + default: + log(`Invalid connection type: ${connectionType}`); + } + + return content; + } +}); + +var VPNRequestHandler = class { + constructor(agent, requestId, authHelper, serviceType, connection, hints, flags) { + this._agent = agent; + this._requestId = requestId; + this._connection = connection; + this._flags = flags; + this._pluginOutBuffer = []; + this._title = null; + this._description = null; + this._content = []; + this._cinnamonDialog = null; + + let connectionSetting = connection.get_setting_connection(); + + const argv = [ + authHelper.fileName, + '-u', connectionSetting.uuid, + '-n', connectionSetting.id, + '-s', serviceType + ]; + if (authHelper.externalUIMode) + argv.push('--external-ui-mode'); + if (flags & NM.SecretAgentGetSecretsFlags.ALLOW_INTERACTION) + argv.push('-i'); + if (flags & NM.SecretAgentGetSecretsFlags.REQUEST_NEW) + argv.push('-r'); + if (authHelper.supportsHints) { + for (let i = 0; i < hints.length; i++) { + argv.push('-t'); + argv.push(hints[i]); + } + } + + this._newStylePlugin = authHelper.externalUIMode; + + try { + let [success_, pid, stdin, stdout, stderr] = + GLib.spawn_async_with_pipes(null, /* pwd */ + argv, + null, /* envp */ + GLib.SpawnFlags.DO_NOT_REAP_CHILD, + null /* child_setup */); + + this._childPid = pid; + this._stdin = new Gio.UnixOutputStream({ fd: stdin, close_fd: true }); + this._stdout = new Gio.UnixInputStream({ fd: stdout, close_fd: true }); + GLib.close(stderr); + this._dataStdout = new Gio.DataInputStream({ base_stream: this._stdout }); + + if (this._newStylePlugin) + this._readStdoutNewStyle(); + else + this._readStdoutOldStyle(); + + this._childWatch = GLib.child_watch_add(GLib.PRIORITY_DEFAULT, pid, + this._vpnChildFinished.bind(this)); + + this._writeConnection(); + } catch (e) { + logError(e, 'error while spawning VPN auth helper'); + + this._agent.respond(requestId, Cinnamon.NetworkAgentResponse.INTERNAL_ERROR); + } + } + + cancel(respond) { + if (respond) + this._agent.respond(this._requestId, Cinnamon.NetworkAgentResponse.USER_CANCELED); + + if (this._newStylePlugin && this._cinnamonDialog) { + this._cinnamonDialog.close(global.get_current_time()); + this._cinnamonDialog.destroy(); + } else { + try { + this._stdin.write('QUIT\n\n', null); + } catch (e) { /* ignore broken pipe errors */ } + } + + this.destroy(); + } + + destroy() { + if (this._destroyed) + return; + + this.emit('destroy'); + if (this._childWatch) + GLib.source_remove(this._childWatch); + + this._stdin.close(null); + // Stdout is closed when we finish reading from it + + this._destroyed = true; + } + + _vpnChildFinished(pid, status, _requestObj) { + this._childWatch = 0; + if (this._newStylePlugin) { + // For new style plugin, all work is done in the async reading functions + // Just reap the process here + return; + } + + let [exited, exitStatus] = Cinnamon.util_wifexited(status); + + if (exited) { + if (exitStatus !== 0) + this._agent.respond(this._requestId, Cinnamon.NetworkAgentResponse.USER_CANCELED); + else + this._agent.respond(this._requestId, Cinnamon.NetworkAgentResponse.CONFIRMED); + } else { + this._agent.respond(this._requestId, Cinnamon.NetworkAgentResponse.INTERNAL_ERROR); + } + + this.destroy(); + } + + _vpnChildProcessLineOldStyle(line) { + if (this._previousLine !== undefined) { + // Two consecutive newlines mean that the child should be closed + // (the actual newlines are eaten by Gio.DataInputStream) + // Send a termination message + if (line === '' && this._previousLine === '') { + try { + this._stdin.write('QUIT\n\n', null); + } catch (e) { /* ignore broken pipe errors */ } + } else { + this._agent.set_password(this._requestId, this._previousLine, line); + this._previousLine = undefined; + } + } else { + this._previousLine = line; + } + } + + _readStdoutOldStyle() { + this._dataStdout.read_line_async(GLib.PRIORITY_DEFAULT, null, (stream, result) => { + let [line, len_] = this._dataStdout.read_line_finish_utf8(result); + + if (line == null) { + // end of file + this._stdout.close(null); + return; + } + + this._vpnChildProcessLineOldStyle(line); + + // try to read more! + this._readStdoutOldStyle(); + }); + } + + _readStdoutNewStyle() { + this._dataStdout.fill_async(-1, GLib.PRIORITY_DEFAULT, null, (stream, result) => { + let cnt = this._dataStdout.fill_finish(result); + + if (cnt == 0) { + // end of file + this._showNewStyleDialog(); + + this._stdout.close(null); + return; + } + + // Try to read more + this._dataStdout.set_buffer_size(2 * this._dataStdout.get_buffer_size()); + this._readStdoutNewStyle(); + }); + } + + _showNewStyleDialog() { + let keyfile = new GLib.KeyFile(); + let data; + let contentOverride; + + try { + data = this._dataStdout.peek_buffer(); + + if (data instanceof Uint8Array) + data = imports.byteArray.toGBytes(data); + else + data = data.toGBytes(); + + keyfile.load_from_bytes(data, GLib.KeyFileFlags.NONE); + + if (keyfile.get_integer(VPN_UI_GROUP, 'Version') !== 2) + throw new Error('Invalid plugin keyfile version, is %d'); + + contentOverride = { + title: keyfile.get_string(VPN_UI_GROUP, 'Title'), + message: keyfile.get_string(VPN_UI_GROUP, 'Description'), + secrets: [], + }; + + let [groups, len_] = keyfile.get_groups(); + for (let i = 0; i < groups.length; i++) { + if (groups[i] === VPN_UI_GROUP) + continue; + + let value = keyfile.get_string(groups[i], 'Value'); + let shouldAsk = keyfile.get_boolean(groups[i], 'ShouldAsk'); + + if (shouldAsk) { + contentOverride.secrets.push({ + label: keyfile.get_string(groups[i], 'Label'), + key: groups[i], + value, + password: keyfile.get_boolean(groups[i], 'IsSecret'), + }); + } else { + if (!value.length) // Ignore empty secrets + continue; + + this._agent.set_password(this._requestId, groups[i], value); + } + } + } catch (e) { + // No output is a valid case it means "both secrets are stored" + if (data.length > 0) { + logError(e, 'error while reading VPN plugin output keyfile'); + + this._agent.respond(this._requestId, Cinnamon.NetworkAgentResponse.INTERNAL_ERROR); + this.destroy(); + return; + } + } + + if (contentOverride && contentOverride.secrets.length) { + // Only show the dialog if we actually have something to ask + this._cinnamonDialog = new NetworkSecretDialog(this._agent, this._requestId, this._connection, 'vpn', [], this._flags, contentOverride); + this._cinnamonDialog.open(global.get_current_time()); + } else { + this._agent.respond(this._requestId, Cinnamon.NetworkAgentResponse.CONFIRMED); + this.destroy(); + } + } + + _writeConnection() { + let vpnSetting = this._connection.get_setting_vpn(); + + try { + vpnSetting.foreach_data_item((key, value) => { + this._stdin.write(`DATA_KEY=${key}\n`, null); + this._stdin.write(`DATA_VAL=${value || ''}\n\n`, null); + }); + vpnSetting.foreach_secret((key, value) => { + this._stdin.write(`SECRET_KEY=${key}\n`, null); + this._stdin.write(`SECRET_VAL=${value || ''}\n\n`, null); + }); + this._stdin.write('DONE\n\n', null); + } catch (e) { + logError(e, 'internal error while writing connection to helper'); + + this._agent.respond(this._requestId, Cinnamon.NetworkAgentResponse.INTERNAL_ERROR); + this.destroy(); + } + } +}; +Signals.addSignalMethods(VPNRequestHandler.prototype); + +var NetworkAgent = class { + constructor() { + this._native = new Cinnamon.NetworkAgent({ + identifier: 'org.cinnamon.NetworkAgent', + capabilities: NM.SecretAgentCapabilities.VPN_HINTS, + auto_register: true, + }); + + this._dialogs = { }; + this._vpnRequests = { }; + this._notifications = { }; + + this._notificationAnswered = false; + + this._native.connect('new-request', this._newRequest.bind(this)); + this._native.connect('cancel-request', this._cancelRequest.bind(this)); + + this._initialized = false; + this._native.init_async(GLib.PRIORITY_DEFAULT, null, (o, res) => { + try { + this._native.init_finish(res); + this._initialized = true; + } catch (e) { + this._native = null; + logError(e, 'error initializing the NetworkManager Agent'); + } + }); + } + + _showNotification(requestId, connection, settingName, hints, flags) { + let source = new MessageTray.Source(_('Network Manager')) + + let title, body; + + let connectionSetting = connection.get_setting_connection(); + let connectionType = connectionSetting.get_connection_type(); + switch (connectionType) { + case '802-11-wireless': { + let wirelessSetting = connection.get_setting_wireless(); + let ssid = NM.utils_ssid_to_utf8(wirelessSetting.get_ssid().get_data()); + title = _('Authentication required'); + body = _('Passwords or encryption keys are required to access the wireless network “%s”.').format(ssid); + break; + } + case '802-3-ethernet': + title = _('Wired 802.1X authentication'); + body = _('A password is required to connect to “%s”.').format(connection.get_id()); + break; + case 'pppoe': + title = _('DSL authentication'); + body = _('A password is required to connect to “%s”.').format(connection.get_id()); + break; + case 'gsm': + if (hints.includes('pin')) { + title = _('PIN code required'); + body = _('PIN code is needed for the mobile broadband device'); + break; + } + // fall through + case 'cdma': + case 'bluetooth': + title = _('Authentication required'); + body = _('A password is required to connect to “%s”.').format(connectionSetting.get_id()); + break; + case 'vpn': + title = _('VPN password'); + body = _('A password is required to connect to “%s”.').format(connectionSetting.get_id()); + break; + default: + log(`Invalid connection type: ${connectionType}`); + this._native.respond(requestId, Cinnamon.NetworkAgentResponse.INTERNAL_ERROR); + return; + } + + let notification = new MessageTray.Notification(source, title, body); + + notification.connect('clicked', () => { + this._notificationAnswered = true; + this._handleRequest(requestId, connection, settingName, hints, flags); + }); + + this._notifications[requestId] = notification; + notification.connect('destroy', () => { + if (!this._notificationAnswered) + this._native.respond(requestId, Cinnamon.NetworkAgentResponse.USER_CANCELED); + delete this._notifications[requestId]; + }); + + Main.messageTray.add(source); + source.notify(notification); + } + + _newRequest(agent, requestId, connection, settingName, hints, flags) { + if (!(flags & NM.SecretAgentGetSecretsFlags.USER_REQUESTED)) + this._showNotification(requestId, connection, settingName, hints, flags); + else + this._handleRequest(requestId, connection, settingName, hints, flags); + } + + _handleRequest(requestId, connection, settingName, hints, flags) { + if (settingName === 'vpn') { + this._vpnRequest(requestId, connection, hints, flags); + return; + } + + let dialog = new NetworkSecretDialog(this._native, requestId, connection, settingName, hints, flags); + dialog.connect('destroy', () => { + delete this._dialogs[requestId]; + }); + this._dialogs[requestId] = dialog; + dialog.open(global.get_current_time()); + } + + _cancelRequest(agent, requestId) { + if (this._dialogs[requestId]) { + this._dialogs[requestId].close(global.get_current_time()); + this._dialogs[requestId].destroy(); + delete this._dialogs[requestId]; + } else if (this._vpnRequests[requestId]) { + this._vpnRequests[requestId].cancel(false); + delete this._vpnRequests[requestId]; + } + } + + _vpnRequest(requestId, connection, hints, flags) { + let vpnSetting = connection.get_setting_vpn(); + let serviceType = vpnSetting.service_type; + + let binary = this._findAuthBinary(serviceType); + if (!binary) { + log('Invalid VPN service type (cannot find authentication binary)'); + + /* cancel the auth process */ + this._native.respond(requestId, Cinnamon.NetworkAgentResponse.INTERNAL_ERROR); + return; + } + + let vpnRequest = new VPNRequestHandler(this._native, requestId, binary, serviceType, connection, hints, flags); + vpnRequest.connect('destroy', () => { + delete this._vpnRequests[requestId]; + }); + this._vpnRequests[requestId] = vpnRequest; + } + + _findAuthBinary(serviceType) { + const plugin = NM.VpnPluginInfo.new_search_file(null, serviceType); + + if (plugin === null) + return null; + + const fileName = plugin.get_auth_dialog(); + if (!GLib.file_test(fileName, GLib.FileTest.IS_EXECUTABLE)) { + log('VPN plugin at %s is not executable'.format(fileName)); + return null; + } + + const prop = plugin.lookup_property('CINNAMON', 'supports-external-ui-mode'); + const trimmedProp = prop ? prop.trim().toLowerCase() : ''; + + return { + fileName, + supportsHints: plugin.supports_hints(), + externalUIMode: ['true', 'yes', 'on', '1'].includes(trimmedProp), + }; + } +}; diff --git a/meson.build b/meson.build index 818450ecbd..47b46f3ff8 100644 --- a/meson.build +++ b/meson.build @@ -46,10 +46,11 @@ xapp = dependency('xapp', version: '>= 2.6.0') X11 = dependency('x11') xml = dependency('libxml-2.0') -has_nm = not get_option('disable_networkmanager') -if has_nm - # only ever used in *.js via gi import - dependency('libnm') +nm_deps = [] +have_networkmanager = not get_option('disable_networkmanager') +if have_networkmanager + nm_deps += dependency('libnm', version: '>= 1.10.4') + nm_deps += dependency('libsecret-1', version: '>= 0.18') endif if get_option('build_recorder') @@ -71,6 +72,7 @@ python = find_program('python3') cinnamon_conf = configuration_data() cinnamon_conf.set_quoted('VERSION', version) cinnamon_conf.set_quoted('GETTEXT_PACKAGE', meson.project_name().to_lower()) +cinnamon_conf.set('HAVE_NETWORKMANAGER', have_networkmanager) have_mallinfo = cc.has_function('mallinfo', prefix: '#include ') if have_mallinfo @@ -145,6 +147,7 @@ install_subdir( config_js_conf = configuration_data() config_js_conf.set('PACKAGE_NAME', meson.project_name().to_lower()) config_js_conf.set('PACKAGE_VERSION', version) +config_js_conf.set10('HAVE_NETWORKMANAGER', have_networkmanager) configure_file( input: 'js/misc/config.js.in', @@ -160,10 +163,10 @@ install_subdir( ) session_conf = configuration_data() -if has_nm - session_conf.set('REQUIRED', 'nm-applet;') -else +if have_networkmanager session_conf.set('REQUIRED', '') +else + session_conf.set('REQUIRED', 'nm-applet;') endif foreach file : ['cinnamon.session', 'cinnamon2d.session', 'cinnamon-wayland.session'] diff --git a/src/cinnamon-network-agent.c b/src/cinnamon-network-agent.c new file mode 100644 index 0000000000..576affa394 --- /dev/null +++ b/src/cinnamon-network-agent.c @@ -0,0 +1,899 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * Copyright 2011 Red Hat, Inc. + * 2011 Giovanni Campagna + * 2017 Lubomir Rintel + * + * 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 "cinnamon-network-agent.h" + +enum { + SIGNAL_NEW_REQUEST, + SIGNAL_CANCEL_REQUEST, + SIGNAL_LAST +}; + +static gint signals[SIGNAL_LAST]; + +typedef struct { + GCancellable * cancellable; + CinnamonNetworkAgent *self; + + gchar *request_id; + NMConnection *connection; + gchar *setting_name; + gchar **hints; + NMSecretAgentGetSecretsFlags flags; + NMSecretAgentOldGetSecretsFunc callback; + gpointer callback_data; + + GVariantDict *entries; + GVariantBuilder builder_vpn; +} CinnamonAgentRequest; + +struct _CinnamonNetworkAgentPrivate { + /* */ + GHashTable *requests; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (CinnamonNetworkAgent, cinnamon_network_agent, NM_TYPE_SECRET_AGENT_OLD) + +static const SecretSchema network_agent_schema = { + "org.freedesktop.NetworkManager.Connection", + SECRET_SCHEMA_DONT_MATCH_NAME, + { + { CINNAMON_KEYRING_UUID_TAG, SECRET_SCHEMA_ATTRIBUTE_STRING }, + { CINNAMON_KEYRING_SN_TAG, SECRET_SCHEMA_ATTRIBUTE_STRING }, + { CINNAMON_KEYRING_SK_TAG, SECRET_SCHEMA_ATTRIBUTE_STRING }, + { NULL, 0 }, + } +}; + +static void +cinnamon_agent_request_free (gpointer data) +{ + CinnamonAgentRequest *request = data; + + g_cancellable_cancel (request->cancellable); + g_object_unref (request->cancellable); + g_object_unref (request->self); + g_object_unref (request->connection); + g_free (request->setting_name); + g_strfreev (request->hints); + g_clear_pointer (&request->entries, g_variant_dict_unref); + g_variant_builder_clear (&request->builder_vpn); + + g_free (request); +} + +static void +cinnamon_agent_request_cancel (CinnamonAgentRequest *request) +{ + GError *error; + CinnamonNetworkAgent *self; + + self = request->self; + + error = g_error_new (NM_SECRET_AGENT_ERROR, + NM_SECRET_AGENT_ERROR_AGENT_CANCELED, + "Canceled by NetworkManager"); + request->callback (NM_SECRET_AGENT_OLD (self), request->connection, + NULL, error, request->callback_data); + + g_signal_emit (self, signals[SIGNAL_CANCEL_REQUEST], 0, request->request_id); + + g_hash_table_remove (self->priv->requests, request->request_id); + g_error_free (error); +} + +static void +cinnamon_network_agent_init (CinnamonNetworkAgent *agent) +{ + CinnamonNetworkAgentPrivate *priv; + + priv = agent->priv = cinnamon_network_agent_get_instance_private (agent); + priv->requests = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, cinnamon_agent_request_free); +} + +static void +cinnamon_network_agent_finalize (GObject *object) +{ + CinnamonNetworkAgentPrivate *priv = CINNAMON_NETWORK_AGENT (object)->priv; + GError *error; + GHashTableIter iter; + gpointer key; + gpointer value; + + error = g_error_new (NM_SECRET_AGENT_ERROR, + NM_SECRET_AGENT_ERROR_AGENT_CANCELED, + "The secret agent is going away"); + + g_hash_table_iter_init (&iter, priv->requests); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + CinnamonAgentRequest *request = value; + + request->callback (NM_SECRET_AGENT_OLD (object), + request->connection, + NULL, error, + request->callback_data); + } + + g_hash_table_destroy (priv->requests); + g_error_free (error); + + G_OBJECT_CLASS (cinnamon_network_agent_parent_class)->finalize (object); +} + +static void +request_secrets_from_ui (CinnamonAgentRequest *request) +{ + g_signal_emit (request->self, signals[SIGNAL_NEW_REQUEST], 0, + request->request_id, + request->connection, + request->setting_name, + request->hints, + (int)request->flags); +} + +static void +check_always_ask_cb (NMSetting *setting, + const gchar *key, + const GValue *value, + GParamFlags flags, + gpointer user_data) +{ + gboolean *always_ask = user_data; + NMSettingSecretFlags secret_flags = NM_SETTING_SECRET_FLAG_NONE; + + if (flags & NM_SETTING_PARAM_SECRET) + { + if (nm_setting_get_secret_flags (setting, key, &secret_flags, NULL)) + { + if (secret_flags & NM_SETTING_SECRET_FLAG_NOT_SAVED) + *always_ask = TRUE; + } + } +} + +static gboolean +has_always_ask (NMSetting *setting) +{ + gboolean always_ask = FALSE; + + nm_setting_enumerate_values (setting, check_always_ask_cb, &always_ask); + return always_ask; +} + +static gboolean +is_connection_always_ask (NMConnection *connection) +{ + NMSettingConnection *s_con; + const gchar *ctype; + NMSetting *setting; + + /* For the given connection type, check if the secrets for that connection + * are always-ask or not. + */ + s_con = (NMSettingConnection *) nm_connection_get_setting (connection, NM_TYPE_SETTING_CONNECTION); + g_assert (s_con); + ctype = nm_setting_connection_get_connection_type (s_con); + + setting = nm_connection_get_setting_by_name (connection, ctype); + g_return_val_if_fail (setting != NULL, FALSE); + + if (has_always_ask (setting)) + return TRUE; + + /* Try type-specific settings too; be a bit paranoid and only consider + * secrets from settings relevant to the connection type. + */ + if (NM_IS_SETTING_WIRELESS (setting)) + { + setting = nm_connection_get_setting (connection, NM_TYPE_SETTING_WIRELESS_SECURITY); + if (setting && has_always_ask (setting)) + return TRUE; + setting = nm_connection_get_setting (connection, NM_TYPE_SETTING_802_1X); + if (setting && has_always_ask (setting)) + return TRUE; + } + else if (NM_IS_SETTING_WIRED (setting)) + { + setting = nm_connection_get_setting (connection, NM_TYPE_SETTING_PPPOE); + if (setting && has_always_ask (setting)) + return TRUE; + setting = nm_connection_get_setting (connection, NM_TYPE_SETTING_802_1X); + if (setting && has_always_ask (setting)) + return TRUE; + } + + return FALSE; +} + +static void +get_secrets_keyring_cb (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + CinnamonAgentRequest *closure; + CinnamonNetworkAgent *self; + CinnamonNetworkAgentPrivate *priv; + GError *secret_error = NULL; + GError *error = NULL; + GList *items; + GList *l; + gboolean secrets_found = FALSE; + GVariantBuilder builder_setting, builder_connection; + g_autoptr (GVariant) setting = NULL; + + items = secret_service_search_finish (NULL, result, &secret_error); + + if (g_error_matches (secret_error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + { + g_error_free (secret_error); + return; + } + + closure = user_data; + self = closure->self; + priv = self->priv; + + if (secret_error != NULL) + { + g_set_error (&error, + NM_SECRET_AGENT_ERROR, + NM_SECRET_AGENT_ERROR_FAILED, + "Internal error while retrieving secrets from the keyring (%s)", secret_error->message); + g_error_free (secret_error); + closure->callback (NM_SECRET_AGENT_OLD (closure->self), closure->connection, NULL, error, closure->callback_data); + + goto out; + } + + g_variant_builder_init (&builder_setting, NM_VARIANT_TYPE_SETTING); + + for (l = items; l; l = g_list_next (l)) + { + SecretItem *item = l->data; + GHashTable *attributes; + GHashTableIter iter; + const gchar *name, *attribute; + SecretValue *secret = secret_item_get_secret (item); + + /* This can happen if the user denied a request to unlock */ + if (secret == NULL) + continue; + + attributes = secret_item_get_attributes (item); + g_hash_table_iter_init (&iter, attributes); + while (g_hash_table_iter_next (&iter, (gpointer *)&name, (gpointer *)&attribute)) + { + if (g_strcmp0 (name, CINNAMON_KEYRING_SK_TAG) == 0) + { + g_variant_builder_add (&builder_setting, "{sv}", attribute, + g_variant_new_string (secret_value_get (secret, NULL))); + + secrets_found = TRUE; + + break; + } + } + + g_hash_table_unref (attributes); + secret_value_unref (secret); + } + + g_list_free_full (items, g_object_unref); + setting = g_variant_ref_sink (g_variant_builder_end (&builder_setting)); + + /* All VPN requests get sent to the VPN's auth dialog, since it knows better + * than the agent about what secrets are required. Otherwise, if no secrets + * were found and interaction is allowed the ask for some secrets, because + * NetworkManager will fail the connection if not secrets are returned + * instead of asking again with REQUEST_NEW. + */ + if (strcmp(closure->setting_name, NM_SETTING_VPN_SETTING_NAME) == 0 || + (!secrets_found && (closure->flags & NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION))) + { + nm_connection_update_secrets (closure->connection, closure->setting_name, + setting, NULL); + + closure->entries = g_variant_dict_new (setting); + request_secrets_from_ui (closure); + return; + } + + g_variant_builder_init (&builder_connection, NM_VARIANT_TYPE_CONNECTION); + g_variant_builder_add (&builder_connection, "{s@a{sv}}", + closure->setting_name, setting); + + closure->callback (NM_SECRET_AGENT_OLD (closure->self), closure->connection, + g_variant_builder_end (&builder_connection), NULL, + closure->callback_data); + + out: + g_hash_table_remove (priv->requests, closure->request_id); + g_clear_error (&error); +} + +static void +cinnamon_network_agent_get_secrets (NMSecretAgentOld *agent, + NMConnection *connection, + const gchar *connection_path, + const gchar *setting_name, + const gchar **hints, + NMSecretAgentGetSecretsFlags flags, + NMSecretAgentOldGetSecretsFunc callback, + gpointer callback_data) +{ + CinnamonNetworkAgent *self = CINNAMON_NETWORK_AGENT (agent); + CinnamonAgentRequest *request; + GHashTable *attributes; + char *request_id; + + request_id = g_strdup_printf ("%s/%s", connection_path, setting_name); + if ((request = g_hash_table_lookup (self->priv->requests, request_id)) != NULL) + { + /* We already have a request pending for this (connection, setting) + * Cancel it before starting the new one. + * This will also free the request structure and associated resources. + */ + cinnamon_agent_request_cancel (request); + } + + request = g_new0 (CinnamonAgentRequest, 1); + request->self = g_object_ref (self); + request->cancellable = g_cancellable_new (); + request->connection = g_object_ref (connection); + request->setting_name = g_strdup (setting_name); + request->hints = g_strdupv ((gchar **)hints); + request->flags = flags; + request->callback = callback; + request->callback_data = callback_data; + + request->request_id = request_id; + g_hash_table_replace (self->priv->requests, request->request_id, request); + + g_variant_builder_init (&request->builder_vpn, G_VARIANT_TYPE ("a{ss}")); + + if ((flags & NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW) || + ((flags & NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION) + && is_connection_always_ask (request->connection))) + { + request->entries = g_variant_dict_new (NULL); + request_secrets_from_ui (request); + return; + } + + attributes = secret_attributes_build (&network_agent_schema, + CINNAMON_KEYRING_UUID_TAG, nm_connection_get_uuid (connection), + CINNAMON_KEYRING_SN_TAG, setting_name, + NULL); + + secret_service_search (NULL, &network_agent_schema, attributes, + SECRET_SEARCH_ALL | SECRET_SEARCH_UNLOCK | SECRET_SEARCH_LOAD_SECRETS, + request->cancellable, get_secrets_keyring_cb, request); + + g_hash_table_unref (attributes); +} + +void +cinnamon_network_agent_add_vpn_secret (CinnamonNetworkAgent *self, + gchar *request_id, + gchar *setting_key, + gchar *setting_value) +{ + CinnamonNetworkAgentPrivate *priv; + CinnamonAgentRequest *request; + + g_return_if_fail (CINNAMON_IS_NETWORK_AGENT (self)); + + priv = self->priv; + request = g_hash_table_lookup (priv->requests, request_id); + g_return_if_fail (request != NULL); + + g_variant_builder_add (&request->builder_vpn, "{ss}", setting_key, setting_value); +} + +void +cinnamon_network_agent_set_password (CinnamonNetworkAgent *self, + gchar *request_id, + gchar *setting_key, + gchar *setting_value) +{ + CinnamonNetworkAgentPrivate *priv; + CinnamonAgentRequest *request; + + g_return_if_fail (CINNAMON_IS_NETWORK_AGENT (self)); + + priv = self->priv; + request = g_hash_table_lookup (priv->requests, request_id); + g_return_if_fail (request != NULL); + + g_variant_dict_insert (request->entries, setting_key, "s", setting_value); +} + +void +cinnamon_network_agent_respond (CinnamonNetworkAgent *self, + gchar *request_id, + CinnamonNetworkAgentResponse response) +{ + CinnamonNetworkAgentPrivate *priv; + CinnamonAgentRequest *request; + GVariantBuilder builder_connection; + GVariant *vpn_secrets, *setting; + + g_return_if_fail (CINNAMON_IS_NETWORK_AGENT (self)); + + priv = self->priv; + request = g_hash_table_lookup (priv->requests, request_id); + g_return_if_fail (request != NULL); + + if (response == CINNAMON_NETWORK_AGENT_USER_CANCELED) + { + GError *error = g_error_new (NM_SECRET_AGENT_ERROR, + NM_SECRET_AGENT_ERROR_USER_CANCELED, + "Network dialog was canceled by the user"); + + request->callback (NM_SECRET_AGENT_OLD (self), request->connection, NULL, error, request->callback_data); + g_error_free (error); + g_hash_table_remove (priv->requests, request_id); + return; + } + + if (response == CINNAMON_NETWORK_AGENT_INTERNAL_ERROR) + { + GError *error = g_error_new (NM_SECRET_AGENT_ERROR, + NM_SECRET_AGENT_ERROR_FAILED, + "An internal error occurred while processing the request."); + + request->callback (NM_SECRET_AGENT_OLD (self), request->connection, NULL, error, request->callback_data); + g_error_free (error); + g_hash_table_remove (priv->requests, request_id); + return; + } + + /* response == CINNAMON_NETWORK_AGENT_CONFIRMED */ + + /* VPN secrets are stored as a hash of secrets in a single setting */ + vpn_secrets = g_variant_builder_end (&request->builder_vpn); + if (g_variant_n_children (vpn_secrets)) + g_variant_dict_insert_value (request->entries, NM_SETTING_VPN_SECRETS, vpn_secrets); + else + g_variant_unref (vpn_secrets); + + setting = g_variant_dict_end (request->entries); + + /* Save any updated secrets */ + if ((request->flags & NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION) || + (request->flags & NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW)) + { + NMConnection *dup = nm_simple_connection_new_clone (request->connection); + + nm_connection_update_secrets (dup, request->setting_name, setting, NULL); + nm_secret_agent_old_save_secrets (NM_SECRET_AGENT_OLD (self), dup, NULL, NULL); + g_object_unref (dup); + } + + g_variant_builder_init (&builder_connection, NM_VARIANT_TYPE_CONNECTION); + g_variant_builder_add (&builder_connection, "{s@a{sv}}", + request->setting_name, setting); + + request->callback (NM_SECRET_AGENT_OLD (self), request->connection, + g_variant_builder_end (&builder_connection), NULL, + request->callback_data); + + g_hash_table_remove (priv->requests, request_id); +} + +static void +search_vpn_plugin (GTask *task, + gpointer object, + gpointer task_data, + GCancellable *cancellable) +{ + NMVpnPluginInfo *info = NULL; + char *service = task_data; + + info = nm_vpn_plugin_info_new_search_file (NULL, service); + + if (info) + { + g_task_return_pointer (task, info, g_object_unref); + } + else + { + g_task_return_new_error (task, + G_IO_ERROR, G_IO_ERROR_NOT_FOUND, + "No plugin for %s", service); + } +} + +void +cinnamon_network_agent_search_vpn_plugin (CinnamonNetworkAgent *self, + const char *service, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + + g_return_if_fail (CINNAMON_IS_NETWORK_AGENT (self)); + g_return_if_fail (service != NULL); + + task = g_task_new (self, NULL, callback, user_data); + g_task_set_source_tag (task, cinnamon_network_agent_search_vpn_plugin); + g_task_set_task_data (task, g_strdup (service), g_free); + + g_task_run_in_thread (task, search_vpn_plugin); +} + +/** + * cinnamon_network_agent_search_vpn_plugin_finish: + * + * Returns: (nullable) (transfer full): The found plugin or %NULL + */ +NMVpnPluginInfo * +cinnamon_network_agent_search_vpn_plugin_finish (CinnamonNetworkAgent *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CINNAMON_IS_NETWORK_AGENT (self), NULL); + g_return_val_if_fail (G_IS_TASK (result), NULL); + + return g_task_propagate_pointer (G_TASK (result), error); +} + +static void +cinnamon_network_agent_cancel_get_secrets (NMSecretAgentOld *agent, + const gchar *connection_path, + const gchar *setting_name) +{ + CinnamonNetworkAgent *self = CINNAMON_NETWORK_AGENT (agent); + CinnamonNetworkAgentPrivate *priv = self->priv; + gchar *request_id; + CinnamonAgentRequest *request; + + request_id = g_strdup_printf ("%s/%s", connection_path, setting_name); + request = g_hash_table_lookup (priv->requests, request_id); + g_free (request_id); + + if (!request) + { + /* We've already sent the result, but the caller cancelled the + * operation before receiving that result. + */ + return; + } + + cinnamon_agent_request_cancel (request); +} + +/************************* saving of secrets ****************************************/ + +static GHashTable * +create_keyring_add_attr_list (NMConnection *connection, + const gchar *connection_uuid, + const gchar *connection_id, + const gchar *setting_name, + const gchar *setting_key, + gchar **out_display_name) +{ + NMSettingConnection *s_con; + + if (connection) + { + s_con = (NMSettingConnection *) nm_connection_get_setting (connection, NM_TYPE_SETTING_CONNECTION); + g_return_val_if_fail (s_con != NULL, NULL); + connection_uuid = nm_setting_connection_get_uuid (s_con); + connection_id = nm_setting_connection_get_id (s_con); + } + + g_return_val_if_fail (connection_uuid != NULL, NULL); + g_return_val_if_fail (connection_id != NULL, NULL); + g_return_val_if_fail (setting_name != NULL, NULL); + g_return_val_if_fail (setting_key != NULL, NULL); + + if (out_display_name) + { + *out_display_name = g_strdup_printf ("Network secret for %s/%s/%s", + connection_id, + setting_name, + setting_key); + } + + return secret_attributes_build (&network_agent_schema, + CINNAMON_KEYRING_UUID_TAG, connection_uuid, + CINNAMON_KEYRING_SN_TAG, setting_name, + CINNAMON_KEYRING_SK_TAG, setting_key, + NULL); +} + +typedef struct +{ + /* Sort of ref count, indicates the number of secrets we still need to save */ + gint n_secrets; + + NMSecretAgentOld *self; + NMConnection *connection; + gpointer callback; + gpointer callback_data; +} KeyringRequest; + +static void +keyring_request_free (KeyringRequest *r) +{ + g_object_unref (r->self); + g_object_unref (r->connection); + + g_free (r); +} + +static void +save_secret_cb (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + KeyringRequest *call = user_data; + NMSecretAgentOldSaveSecretsFunc callback = call->callback; + + call->n_secrets--; + + if (call->n_secrets == 0) + { + if (callback) + callback (call->self, call->connection, NULL, call->callback_data); + keyring_request_free (call); + } +} + +static void +save_one_secret (KeyringRequest *r, + NMSetting *setting, + const gchar *key, + const gchar *secret, + const gchar *display_name) +{ + GHashTable *attrs; + gchar *alt_display_name = NULL; + const gchar *setting_name; + NMSettingSecretFlags secret_flags = NM_SETTING_SECRET_FLAG_NONE; + + /* Only save agent-owned secrets (not system-owned or always-ask) */ + nm_setting_get_secret_flags (setting, key, &secret_flags, NULL); + if (secret_flags != NM_SETTING_SECRET_FLAG_AGENT_OWNED) + return; + + setting_name = nm_setting_get_name (setting); + g_assert (setting_name); + + attrs = create_keyring_add_attr_list (r->connection, NULL, NULL, + setting_name, + key, + display_name ? NULL : &alt_display_name); + g_assert (attrs); + r->n_secrets++; + secret_password_storev (&network_agent_schema, attrs, SECRET_COLLECTION_DEFAULT, + display_name ? display_name : alt_display_name, + secret, NULL, save_secret_cb, r); + + g_hash_table_unref (attrs); + g_free (alt_display_name); +} + +static void +vpn_secret_iter_cb (const gchar *key, + const gchar *secret, + gpointer user_data) +{ + KeyringRequest *r = user_data; + NMSetting *setting; + const gchar *service_name, *id; + gchar *display_name; + + if (secret && strlen (secret)) + { + setting = nm_connection_get_setting (r->connection, NM_TYPE_SETTING_VPN); + g_assert (setting); + service_name = nm_setting_vpn_get_service_type (NM_SETTING_VPN (setting)); + g_assert (service_name); + id = nm_connection_get_id (r->connection); + g_assert (id); + + display_name = g_strdup_printf ("VPN %s secret for %s/%s/" NM_SETTING_VPN_SETTING_NAME, + key, + id, + service_name); + save_one_secret (r, setting, key, secret, display_name); + g_free (display_name); + } +} + +static void +write_one_secret_to_keyring (NMSetting *setting, + const gchar *key, + const GValue *value, + GParamFlags flags, + gpointer user_data) +{ + KeyringRequest *r = user_data; + const gchar *secret; + + /* Non-secrets obviously don't get saved in the keyring */ + if (!(flags & NM_SETTING_PARAM_SECRET)) + return; + + if (NM_IS_SETTING_VPN (setting) && (g_strcmp0 (key, NM_SETTING_VPN_SECRETS) == 0)) + { + /* Process VPN secrets specially since it's a hash of secrets, not just one */ + nm_setting_vpn_foreach_secret (NM_SETTING_VPN (setting), + vpn_secret_iter_cb, + r); + } + else + { + if (!G_VALUE_HOLDS_STRING (value)) + return; + + secret = g_value_get_string (value); + if (secret && strlen (secret)) + save_one_secret (r, setting, key, secret, NULL); + } +} + +static void +save_delete_cb (NMSecretAgentOld *agent, + NMConnection *connection, + GError *error, + gpointer user_data) +{ + KeyringRequest *r = user_data; + + /* Ignore errors; now save all new secrets */ + nm_connection_for_each_setting_value (connection, write_one_secret_to_keyring, r); + + /* If no secrets actually got saved there may be nothing to do so + * try to complete the request here. If there were secrets to save the + * request will get completed when those keyring calls return (at the next + * mainloop iteration). + */ + if (r->n_secrets == 0) + { + if (r->callback) + ((NMSecretAgentOldSaveSecretsFunc)r->callback) (agent, connection, NULL, r->callback_data); + keyring_request_free (r); + } +} + +static void +cinnamon_network_agent_save_secrets (NMSecretAgentOld *agent, + NMConnection *connection, + const gchar *connection_path, + NMSecretAgentOldSaveSecretsFunc callback, + gpointer callback_data) +{ + KeyringRequest *r; + + r = g_new (KeyringRequest, 1); + r->n_secrets = 0; + r->self = g_object_ref (agent); + r->connection = g_object_ref (connection); + r->callback = callback; + r->callback_data = callback_data; + + /* First delete any existing items in the keyring */ + nm_secret_agent_old_delete_secrets (agent, connection, save_delete_cb, r); +} + +static void +delete_items_cb (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + KeyringRequest *r = user_data; + GError *secret_error = NULL; + GError *error = NULL; + NMSecretAgentOldDeleteSecretsFunc callback = r->callback; + + secret_password_clear_finish (result, &secret_error); + if (secret_error != NULL) + { + error = g_error_new (NM_SECRET_AGENT_ERROR, + NM_SECRET_AGENT_ERROR_FAILED, + "The request could not be completed. Keyring result: %s", + secret_error->message); + g_error_free (secret_error); + } + + callback (r->self, r->connection, error, r->callback_data); + g_clear_error (&error); + keyring_request_free (r); +} + +static void +cinnamon_network_agent_delete_secrets (NMSecretAgentOld *agent, + NMConnection *connection, + const gchar *connection_path, + NMSecretAgentOldDeleteSecretsFunc callback, + gpointer callback_data) +{ + KeyringRequest *r; + NMSettingConnection *s_con; + const gchar *uuid; + + r = g_new (KeyringRequest, 1); + r->n_secrets = 0; /* ignored by delete secrets calls */ + r->self = g_object_ref (agent); + r->connection = g_object_ref (connection); + r->callback = callback; + r->callback_data = callback_data; + + s_con = (NMSettingConnection *) nm_connection_get_setting (connection, NM_TYPE_SETTING_CONNECTION); + g_assert (s_con); + uuid = nm_setting_connection_get_uuid (s_con); + g_assert (uuid); + + secret_password_clear (&network_agent_schema, NULL, delete_items_cb, r, + CINNAMON_KEYRING_UUID_TAG, uuid, + NULL); +} + +void +cinnamon_network_agent_class_init (CinnamonNetworkAgentClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + NMSecretAgentOldClass *agent_class = NM_SECRET_AGENT_OLD_CLASS (klass); + + gobject_class->finalize = cinnamon_network_agent_finalize; + + agent_class->get_secrets = cinnamon_network_agent_get_secrets; + agent_class->cancel_get_secrets = cinnamon_network_agent_cancel_get_secrets; + agent_class->save_secrets = cinnamon_network_agent_save_secrets; + agent_class->delete_secrets = cinnamon_network_agent_delete_secrets; + + signals[SIGNAL_NEW_REQUEST] = g_signal_new ("new-request", + G_TYPE_FROM_CLASS (klass), + 0, /* flags */ + 0, /* class offset */ + NULL, /* accumulator */ + NULL, /* accu_data */ + NULL, /* marshaller */ + G_TYPE_NONE, /* return */ + 5, /* n_params */ + G_TYPE_STRING, + NM_TYPE_CONNECTION, + G_TYPE_STRING, + G_TYPE_STRV, + G_TYPE_INT); + + signals[SIGNAL_CANCEL_REQUEST] = g_signal_new ("cancel-request", + G_TYPE_FROM_CLASS (klass), + 0, /* flags */ + 0, /* class offset */ + NULL, /* accumulator */ + NULL, /* accu_data */ + NULL, /* marshaller */ + G_TYPE_NONE, + 1, /* n_params */ + G_TYPE_STRING); +} diff --git a/src/cinnamon-network-agent.h b/src/cinnamon-network-agent.h new file mode 100644 index 0000000000..fde4cc2cb6 --- /dev/null +++ b/src/cinnamon-network-agent.h @@ -0,0 +1,73 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __CINNAMON_NETWORK_AGENT_H__ +#define __CINNAMON_NETWORK_AGENT_H__ + +#include +#include +#include +#include + +G_BEGIN_DECLS + +typedef enum { + CINNAMON_NETWORK_AGENT_CONFIRMED, + CINNAMON_NETWORK_AGENT_USER_CANCELED, + CINNAMON_NETWORK_AGENT_INTERNAL_ERROR +} CinnamonNetworkAgentResponse; + +typedef struct _CinnamonNetworkAgent CinnamonNetworkAgent; +typedef struct _CinnamonNetworkAgentClass CinnamonNetworkAgentClass; +typedef struct _CinnamonNetworkAgentPrivate CinnamonNetworkAgentPrivate; + +#define CINNAMON_TYPE_NETWORK_AGENT (cinnamon_network_agent_get_type ()) +#define CINNAMON_NETWORK_AGENT(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), CINNAMON_TYPE_NETWORK_AGENT, CinnamonNetworkAgent)) +#define CINNAMON_IS_NETWORK_AGENT(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), CINNAMON_TYPE_NETWORK_AGENT)) +#define CINNAMON_NETWORK_AGENT_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), CINNAMON_TYPE_NETWORK_AGENT, CinnamonNetworkAgentClass)) +#define CINNAMON_IS_NETWORK_AGENT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), CINNAMON_TYPE_NETWORK_AGENT)) +#define CINNAMON_NETWORK_AGENT_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), CINNAMON_TYPE_NETWORK_AGENT, CinnamonNetworkAgentClass)) + +struct _CinnamonNetworkAgent +{ + /*< private >*/ + NMSecretAgentOld parent_instance; + + CinnamonNetworkAgentPrivate *priv; +}; + +struct _CinnamonNetworkAgentClass +{ + /*< private >*/ + NMSecretAgentOldClass parent_class; +}; + +/* used by CINNAMON_TYPE_NETWORK_AGENT */ +GType cinnamon_network_agent_get_type (void); + +void cinnamon_network_agent_add_vpn_secret (CinnamonNetworkAgent *self, + gchar *request_id, + gchar *setting_key, + gchar *setting_value); +void cinnamon_network_agent_set_password (CinnamonNetworkAgent *self, + gchar *request_id, + gchar *setting_key, + gchar *setting_value); +void cinnamon_network_agent_respond (CinnamonNetworkAgent *self, + gchar *request_id, + CinnamonNetworkAgentResponse response); + +void cinnamon_network_agent_search_vpn_plugin (CinnamonNetworkAgent *self, + const char *service, + GAsyncReadyCallback callback, + gpointer user_data); +NMVpnPluginInfo *cinnamon_network_agent_search_vpn_plugin_finish (CinnamonNetworkAgent *self, + GAsyncResult *result, + GError **error); + +/* If these are kept in sync with nm-applet, secrets will be shared */ +#define CINNAMON_KEYRING_UUID_TAG "connection-uuid" +#define CINNAMON_KEYRING_SN_TAG "setting-name" +#define CINNAMON_KEYRING_SK_TAG "setting-key" + +G_END_DECLS + +#endif /* __CINNAMON_NETWORK_AGENT_H__ */ diff --git a/src/cinnamon-util.c b/src/cinnamon-util.c index 6a7912ad28..3dba45b905 100644 --- a/src/cinnamon-util.c +++ b/src/cinnamon-util.c @@ -2,6 +2,9 @@ #include "config.h" +#include +#include + #include "cinnamon-util.h" #include #include @@ -1042,3 +1045,26 @@ cinnamon_shader_effect_set_double_uniform (ClutterShaderEffect *effect, name, &gvalue); } + +/** + * cinnamon_util_wifexited: + * @status: the status returned by wait() or waitpid() + * @exit: (out): the actual exit status of the process + * + * Implements libc standard WIFEXITED, that cannot be used JS + * code. + * Returns: TRUE if the process exited normally, FALSE otherwise + */ +gboolean +cinnamon_util_wifexited (int status, + int *exit) +{ + gboolean ret; + + ret = WIFEXITED(status); + + if (ret) + *exit = WEXITSTATUS(status); + + return ret; +} diff --git a/src/cinnamon-util.h b/src/cinnamon-util.h index c37e835756..edf923f5bf 100644 --- a/src/cinnamon-util.h +++ b/src/cinnamon-util.h @@ -72,6 +72,9 @@ void cinnamon_shader_effect_set_double_uniform (ClutterShaderEffect *effect, const gchar *name, gdouble value); +gboolean cinnamon_util_wifexited (int status, + int *exit); + G_END_DECLS #endif /* __CINNAMON_UTIL_H__ */ diff --git a/src/meson.build b/src/meson.build index 5c05d3a684..75a944766d 100644 --- a/src/meson.build +++ b/src/meson.build @@ -113,6 +113,15 @@ libcinnamon_deps = [ xml, ] +libcinnamon_deps += nm_deps + +if have_networkmanager + cinnamon_sources += [ + 'cinnamon-network-agent.c', + 'cinnamon-network-agent.h', + ] +endif + non_gir = [] if get_option('build_recorder') cinnamon_sources += [ @@ -201,6 +210,10 @@ cinnamon_gir_includes = [ st_gir[0], ] +if have_networkmanager + cinnamon_gir_includes += ['NM-1.0'] +endif + cinnamon_gir = gnome.generate_gir( libcinnamon, namespace: 'Cinnamon', diff --git a/src/st/st-entry.c b/src/st/st-entry.c index 06c37c003c..2cb7171f23 100644 --- a/src/st/st-entry.c +++ b/src/st/st-entry.c @@ -262,7 +262,7 @@ st_entry_update_hint_visibility (StEntry *self) if (priv->hint_actor) g_object_set (priv->hint_actor, "visible", hint_visible, NULL); - if (hint_visible) + if (hint_visible) st_widget_add_style_pseudo_class (ST_WIDGET (self), "indeterminate"); else st_widget_remove_style_pseudo_class (ST_WIDGET (self), "indeterminate");