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
This commit is contained in:
parent
2af5e851b3
commit
2ebdc81c8f
@ -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 */
|
||||
|
||||
|
@ -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 \
|
||||
|
@ -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();
|
||||
|
@ -93,6 +93,10 @@ ModalDialog.prototype = {
|
||||
this._savedKeyFocus = null;
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
this._group.destroy();
|
||||
},
|
||||
|
||||
setButtons: function(buttons) {
|
||||
this._buttonLayout.destroy_children();
|
||||
this._actionKeys = {};
|
||||
@ -104,7 +108,7 @@ ModalDialog.prototype = {
|
||||
let action = buttonInfo['action'];
|
||||
let key = buttonInfo['key'];
|
||||
|
||||
let button = new St.Button({ style_class: 'modal-dialog-button',
|
||||
buttonInfo.button = new St.Button({ style_class: 'modal-dialog-button',
|
||||
reactive: true,
|
||||
can_focus: true,
|
||||
label: label });
|
||||
@ -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;
|
||||
|
400
js/ui/networkAgent.js
Normal file
400
js/ui/networkAgent.js
Normal file
@ -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();
|
||||
}
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user