[gnome-shell] Add a system modal dialog for network secrets



commit 2ebdc81c8f68b0e32e71cab5f522c6bd5a83997d
Author: Giovanni Campagna <gcampagna src gnome org>
Date:   Fri Aug 19 20:29:39 2011 +0200

    Add a system modal dialog for network secrets
    
    Using the new ShellNetworkAgent, show a system modal dialog
    (similar to the PolicyKit one) when NetworkManager needs secrets
    for connecting to wireless.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=650244

 data/theme/gnome-shell.css |   15 ++
 js/Makefile.am             |    1 +
 js/ui/main.js              |    3 +
 js/ui/modalDialog.js       |   18 ++-
 js/ui/networkAgent.js      |  400 ++++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 430 insertions(+), 7 deletions(-)
---
diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css
index 4fe68a8..8d112f1 100644
--- a/data/theme/gnome-shell.css
+++ b/data/theme/gnome-shell.css
@@ -1531,6 +1531,10 @@ StTooltip StLabel {
     padding: 4px 32px 5px;
 }
 
+.modal-dialog-button:disabled {
+    color: rgb(60, 60, 60);
+}
+
 .modal-dialog-button:focus {
     padding: 3px 31px 4px;
 }
@@ -1824,6 +1828,17 @@ StTooltip StLabel {
     padding-bottom: 8px;
 }
 
+.network-dialog-show-password-checkbox {
+    padding-top: 5px;
+    padding-bottom: 5px;
+    font-size: 10pt;
+    color: white;
+    spacing: 10px;
+}
+
+.network-dialog-secret-table {
+    spacing-rows: 15px;
+}
 
 /* Magnifier */
 
diff --git a/js/Makefile.am b/js/Makefile.am
index 79a81c7..ee44b47 100644
--- a/js/Makefile.am
+++ b/js/Makefile.am
@@ -38,6 +38,7 @@ nobase_dist_js_DATA = 	\
 	ui/main.js		\
 	ui/messageTray.js	\
 	ui/modalDialog.js	\
+	ui/networkAgent.js	\
 	ui/shellMountOperation.js \
 	ui/notificationDaemon.js \
 	ui/overview.js		\
diff --git a/js/ui/main.js b/js/ui/main.js
index 5b1aef3..8df0592 100644
--- a/js/ui/main.js
+++ b/js/ui/main.js
@@ -26,6 +26,7 @@ const PlaceDisplay = imports.ui.placeDisplay;
 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 Scripting = imports.ui.scripting;
@@ -63,6 +64,7 @@ let magnifier = null;
 let xdndHandler = null;
 let statusIconDispatcher = null;
 let layoutManager = null;
+let networkAgent = null;
 let _errorLogStack = [];
 let _startDate;
 let _defaultCssStylesheet = null;
@@ -145,6 +147,7 @@ function start() {
     telepathyClient = new TelepathyClient.Client();
     automountManager = new AutomountManager.AutomountManager();
     autorunManager = new AutorunManager.AutorunManager();
+    networkAgent = new NetworkAgent.NetworkAgent();
 
     layoutManager.init();
     overview.init();
diff --git a/js/ui/modalDialog.js b/js/ui/modalDialog.js
index ab637e6..3674360 100644
--- a/js/ui/modalDialog.js
+++ b/js/ui/modalDialog.js
@@ -93,6 +93,10 @@ ModalDialog.prototype = {
         this._savedKeyFocus = null;
     },
 
+    destroy: function() {
+        this._group.destroy();
+    },
+
     setButtons: function(buttons) {
         this._buttonLayout.destroy_children();
         this._actionKeys = {};
@@ -104,10 +108,10 @@ ModalDialog.prototype = {
             let action = buttonInfo['action'];
             let key = buttonInfo['key'];
 
-            let button = new St.Button({ style_class: 'modal-dialog-button',
-                                         reactive:    true,
-                                         can_focus:   true,
-                                         label:       label });
+            buttonInfo.button = new St.Button({ style_class: 'modal-dialog-button',
+                                                reactive:    true,
+                                                can_focus:   true,
+                                                label:       label });
 
             let x_alignment;
             if (buttons.length == 1)
@@ -119,15 +123,15 @@ ModalDialog.prototype = {
             else
                 x_alignment = St.Align.MIDDLE;
 
-            this._initialKeyFocus = button;
-            this._buttonLayout.add(button,
+            this._initialKeyFocus = buttonInfo.button;
+            this._buttonLayout.add(buttonInfo.button,
                                    { expand: true,
                                      x_fill: false,
                                      y_fill: false,
                                      x_align: x_alignment,
                                      y_align: St.Align.MIDDLE });
 
-            button.connect('clicked', action);
+            buttonInfo.button.connect('clicked', action);
 
             if (key)
                 this._actionKeys[key] = action;
diff --git a/js/ui/networkAgent.js b/js/ui/networkAgent.js
new file mode 100644
index 0000000..c8664cf
--- /dev/null
+++ b/js/ui/networkAgent.js
@@ -0,0 +1,400 @@
+/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*-
+ *
+ * Copyright 2011 Giovanni Campagna <scampa giovanni gmail com>
+ *
+ * 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, 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., 59 Temple Place - Suite 330, Boston, MA
+ * 02111-1307, USA.
+ *
+ */
+
+const Clutter = imports.gi.Clutter;
+const Gio = imports.gi.Gio;
+const Lang = imports.lang;
+const NetworkManager = imports.gi.NetworkManager;
+const NMClient = imports.gi.NMClient;
+const Pango = imports.gi.Pango;
+const Shell = imports.gi.Shell;
+const St = imports.gi.St;
+
+const ModalDialog = imports.ui.modalDialog;
+const PopupMenu = imports.ui.popupMenu;
+
+function NetworkSecretDialog() {
+    this._init.apply(this, arguments);
+}
+
+NetworkSecretDialog.prototype = {
+    __proto__: ModalDialog.ModalDialog.prototype,
+
+    _init: function(agent, requestId, connection, settingName, hints) {
+        ModalDialog.ModalDialog.prototype._init.call(this, { styleClass: 'polkit-dialog' });
+
+        this._agent = agent;
+        this._requestId = requestId;
+        this._connection = connection;
+        this._settingName = settingName;
+        this._hints = hints;
+
+        this._content = this._getContent();
+
+        let mainContentBox = new St.BoxLayout({ style_class: 'polkit-dialog-main-layout',
+                                                vertical: false });
+        this.contentLayout.add(mainContentBox,
+                               { x_fill: true,
+                                 y_fill: true });
+
+        let icon = new St.Icon({ icon_name: 'dialog-password-symbolic' });
+        mainContentBox.add(icon,
+                           { x_fill:  true,
+                             y_fill:  false,
+                             x_align: St.Align.END,
+                             y_align: St.Align.START });
+
+        let messageBox = new St.BoxLayout({ style_class: 'polkit-dialog-message-layout',
+                                            vertical: true });
+        mainContentBox.add(messageBox,
+                           { y_align: St.Align.START });
+
+        let subjectLabel = new St.Label({ style_class: 'polkit-dialog-headline',
+                                            text: this._content.title });
+        messageBox.add(subjectLabel,
+                       { y_fill:  false,
+                         y_align: St.Align.START });
+
+        if (this._content.message != null) {
+            let descriptionLabel = new St.Label({ style_class: 'polkit-dialog-description',
+                                                  text: this._content.message,
+                                                  // HACK: for reasons unknown to me, the label
+                                                  // is not asked the correct height for width,
+                                                  // and thus is underallocated
+                                                  // place a fixed height to avoid overflowing
+                                                  style: 'height: 3em'
+                                                });
+            descriptionLabel.clutter_text.line_wrap = true;
+
+            messageBox.add(descriptionLabel,
+                           { y_fill:  true,
+                             y_align: St.Align.START,
+                             expand: true });
+        }
+
+        let secretTable = new St.Table({ style_class: 'network-dialog-secret-table' });
+        let pos = 0;
+        for (let i = 0; i < this._content.secrets.length; i++) {
+            let secret = this._content.secrets[i];
+            let label = new St.Label({ style_class: 'polkit-dialog-password-label',
+                                       text: secret.label });
+
+            let reactive = secret.key != null;
+
+            secret.entry = new St.Entry({ style_class: 'polkit-dialog-password-entry',
+                                          text: secret.value, can_focus: reactive,
+                                          reactive: reactive });
+
+            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) {
+                secret.entry.clutter_text.connect('text-changed', Lang.bind(this, function() {
+                    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;
+
+            secretTable.add(label, { row: pos, col: 0, x_align: St.Align.START, y_align: St.Align.START });
+            secretTable.add(secret.entry, { row: pos, col: 1, x_expand: true, x_fill: true, y_align: St.Align.END });
+            pos++;
+
+            if (secret.password) {
+                secret.entry.clutter_text.set_password_char('\u25cf');
+
+                // FIXME: need a real checkbox here
+                let button = new St.Button({ button_mask: St.ButtonMask.ONE,
+                                             can_focus: true });
+                let checkbox = new St.BoxLayout({ vertical: false,
+                                                  style_class: 'network-dialog-show-password-checkbox' 
+                                                });
+                let _switch = new PopupMenu.Switch(false);
+                checkbox.add(_switch.actor);
+                checkbox.add(new St.Label({ text: _("Show password") }), { expand: true });
+                button.connect('clicked', function() {
+                    _switch.toggle();
+                    if (_switch.state)
+                        secret.entry.clutter_text.set_password_char('');
+                    else
+                        secret.entry.clutter_text.set_password_char('\u25cf');
+                });
+                button.child = checkbox;
+                secretTable.add(button, { row: pos, col: 1, x_expand: true, x_fill: true, y_fill: true })
+                pos++;
+            }
+        }
+        messageBox.add(secretTable);
+
+        this._okButton = { label:  _("Connect"),
+                           action: Lang.bind(this, this._onOk),
+                           key:    Clutter.KEY_Return,
+                         };
+
+        this.setButtons([{ label: _("Cancel"),
+                           action: Lang.bind(this, this.cancel),
+                           key:    Clutter.KEY_Escape,
+                         },
+                         this._okButton]);
+    },
+
+    _updateOkButton: function() {
+        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;
+        if (valid)
+            this._okButton.button.remove_style_pseudo_class('disabled');
+        else
+            this._okButton.button.add_style_pseudo_class('disabled');
+    },
+
+    _onOk: function() {
+        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, false);
+            this.close(global.get_current_time());
+        }
+        // do nothing if not valid
+    },
+
+    cancel: function() {
+        this._agent.respond(this._requestId, true);
+        this.close(global.get_current_time());
+    },
+
+    _validateWpaPsk: function(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: function(secret) {
+        let value = secret.value;
+        if (secret.wep_key_type == NetworkManager.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 == NetworkManager.WepKeyType.PASSPHRASE) {
+	    if (value.length < 0 || value.length > 64)
+	        return false;
+	}
+        return true;
+    },
+
+    _getWirelessSecrets: function(secrets, wirelessSetting) {
+        let wirelessSecuritySetting = this._connection.get_setting_wireless_security();
+        switch (wirelessSecuritySetting.key_mgmt) {
+        // First the easy ones
+        case 'wpa-none':
+        case 'wpa-psk':
+            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: function(secrets) {
+        let ieee8021xSetting = this._connection.get_setting_802_1x();
+        let phase2method;
+
+        switch (ieee8021xSetting.get_eap_method(0)) {
+        case 'md5':
+        case 'leap':
+        case 'ttls':
+        case 'peap':
+            // 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: function(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: function(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: function() {
+        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 = NetworkManager.utils_ssid_to_utf8(wirelessSetting.get_ssid());
+            content.title = _("Authentication required by wireless network");
+            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.indexOf('pin') != -1) {
+                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 });
+            }
+            // fall through
+        case 'cdma':
+        case 'bluetooth':
+            content.title = _("Mobile broadband network password");
+            content.message = _("A password is required to connect to '%s'.").format(connectionSetting.get_id());
+            this._getMobileSecrets(content.secrets);
+            break;
+        default:
+            log('Invalid connection type: ' + connectionType);
+        };
+
+        return content;
+    }
+};
+
+function NetworkAgent() {
+    this._init.apply(this, arguments);
+}
+
+NetworkAgent.prototype = {
+    _init: function() {
+        this._native = new Shell.NetworkAgent({ auto_register: true,
+                                                identifier: 'org.gnome.Shell.NetworkAgent' });
+
+        this._dialogs = { };
+        this._native.connect('new-request', Lang.bind(this, this._newRequest));
+        this._native.connect('cancel-request', Lang.bind(this, this._cancelRequest));
+    },
+
+    _newRequest:  function(agent, requestId, connection, settingName, hints) {
+        let dialog = new NetworkSecretDialog(agent, requestId, connection, settingName, hints);
+        dialog.connect('destroy', Lang.bind(this, function() {
+            delete this._dialogs[requestId];
+        }));
+        this._dialogs[requestId] = dialog;
+        dialog.open(global.get_current_time());
+    },
+
+    _cancelRequest: function(agent, requestId) {
+        this._dialogs[requestId].close(global.get_current_time());
+        this._dialogs[requestId].destroy();
+    }
+};
\ No newline at end of file



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]