diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css index 4fe68a8cb..8d112f1c3 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 79a81c7a0..ee44b4793 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 5b1aef3e5..8df05928f 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 ab637e622..36743604e 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 000000000..c8664cf9b --- /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 + * + * 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