gnome-shell/js/ui/networkAgent.js
Giovanni Campagna a29507e452 St: add :disabled pseudo class when a button is not reactive
The :reactive property is used on StButton to like the :sensitive
property on GtkWidgets, that is, to indicate that the user is not
(yet) expected to click the button, and therefore should affect
styling too.
This allows to remove some code at the JS layer.

https://bugzilla.gnome.org/show_bug.cgi?id=619955
2012-07-21 15:40:28 +02:00

702 lines
28 KiB
JavaScript

// -*- mode: js; js-indent-level: 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 GLib = imports.gi.GLib;
const GObject = imports.gi.GObject;
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 Config = imports.misc.config;
const ModalDialog = imports.ui.modalDialog;
const PopupMenu = imports.ui.popupMenu;
const ShellEntry = imports.ui.shellEntry;
const VPN_UI_GROUP = 'VPN Plugin UI';
const NetworkSecretDialog = new Lang.Class({
Name: 'NetworkSecretDialog',
Extends: ModalDialog.ModalDialog,
_init: function(agent, requestId, connection, settingName, hints, contentOverride) {
this.parent({ 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 mainContentBox = new St.BoxLayout({ style_class: 'prompt-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: 'prompt-dialog-message-layout',
vertical: true });
mainContentBox.add(messageBox,
{ y_align: St.Align.START });
let subjectLabel = new St.Label({ style_class: 'prompt-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: 'prompt-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 initialFocusSet = false;
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: 'prompt-dialog-password-label',
text: secret.label });
let reactive = secret.key != null;
secret.entry = new St.Entry({ style_class: 'prompt-dialog-password-entry',
text: secret.value, can_focus: reactive,
reactive: reactive });
ShellEntry.addContextMenu(secret.entry,
{ isPassword: secret.password });
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', Lang.bind(this, this._onOk));
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_expand: false, x_fill: true, 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');
}
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;
},
_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, Shell.NetworkAgentResponse.CONFIRMED);
this.close(global.get_current_time());
}
// do nothing if not valid
},
cancel: function() {
this._agent.respond(this._requestId, Shell.NetworkAgentResponse.USER_CANCELED);
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, connectionType);
break;
default:
log('Invalid connection type: ' + connectionType);
};
return content;
}
});
const VPNRequestHandler = new Lang.Class({
Name: 'VPNRequestHandler',
_init: function(agent, requestId, authHelper, serviceType, connection, hints, flags) {
this._agent = agent;
this._requestId = requestId;
this._connection = connection;
this._pluginOutBuffer = [];
this._title = null;
this._description = null;
this._content = [ ];
this._shellDialog = null;
let connectionSetting = connection.get_setting_connection();
let argv = [ authHelper.fileName,
'-u', connectionSetting.uuid,
'-n', connectionSetting.id,
'-s', serviceType
];
if (authHelper.externalUIMode)
argv.push('--external-ui-mode');
if (flags & NMClient.SecretAgentGetSecretsFlags.ALLOW_INTERACTION)
argv.push('-i');
if (flags & NMClient.SecretAgentGetSecretsFlags.REQUEST_NEW)
argv.push('-r');
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 });
// We need this one too, even if don't actually care of what the process
// has to say on stderr, because otherwise the fd opened by g_spawn_async_with_pipes
// is kept open indefinitely
let stderrStream = new Gio.UnixInputStream({ fd: stderr, close_fd: true });
stderrStream.close(null);
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,
Lang.bind(this, this._vpnChildFinished));
this._writeConnection();
} catch(e) {
logError(e, 'error while spawning VPN auth helper');
this._agent.respond(requestId, Shell.NetworkAgentResponse.INTERNAL_ERROR);
}
},
cancel: function() {
if (this._newStylePlugin && this._shellDialog) {
this._shellDialog.close(global.get_current_time());
this._shellDialog.destroy();
} else {
try {
this._stdin.write('QUIT\n\n', null);
} catch(e) { /* ignore broken pipe errors */ }
}
this.destroy();
},
destroy: function() {
if (this._destroyed)
return;
GLib.source_remove(this._childWatch);
this._stdin.close(null);
// Stdout is closed when we finish reading from it
this._destroyed = true;
},
_vpnChildFinished: function(pid, status, requestObj) {
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] = Shell.util_wifexited(status);
if (exited) {
if (exitStatus != 0)
this._agent.respond(this._requestId, Shell.NetworkAgentResponse.USER_CANCELED);
else
this._agent.respond(this._requestId, Shell.NetworkAgentResponse.CONFIRMED);
} else
this._agent.respond(this._requestId, Shell.NetworkAgentResponse.INTERNAL_ERROR);
this.destroy();
},
_vpnChildProcessLineOldStyle: function(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: function() {
this._dataStdout.read_line_async(GLib.PRIORITY_DEFAULT, null, Lang.bind(this, function(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: function() {
this._dataStdout.fill_async(-1, GLib.PRIORITY_DEFAULT, null, Lang.bind(this, function(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: function() {
let keyfile = new GLib.KeyFile();
let contentOverride;
try {
let data = this._dataStdout.peek_buffer();
keyfile.load_from_data(data.toString(), data.length,
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: 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) {
logError(e, 'error while reading VPN plugin output keyfile');
this._agent.respond(this._requestId, Shell.NetworkAgentResponse.INTERNAL_ERROR);
return;
}
if (contentOverride.secrets.length) {
// Only show the dialog if we actually have something to ask
this._shellDialog = new NetworkSecretDialog(this._agent, this._requestId, this._connection, 'vpn', [], contentOverride);
this._shellDialog.open(global.get_current_time());
} else {
this._agent.respond(this._requestId, Shell.NetworkAgentResponse.CONFIRMED);
}
},
_writeConnection: function() {
let vpnSetting = this._connection.get_setting_vpn();
try {
vpnSetting.foreach_data_item(Lang.bind(this, function(key, value) {
this._stdin.write('DATA_KEY=' + key + '\n', null);
this._stdin.write('DATA_VAL=' + (value || '') + '\n\n', null);
}));
vpnSetting.foreach_secret(Lang.bind(this, function(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, Shell.NetworkAgentResponse.INTERNAL_ERROR);
}
},
});
const NetworkAgent = new Lang.Class({
Name: 'NetworkAgent',
_init: function() {
this._native = new Shell.NetworkAgent({ auto_register: true,
identifier: 'org.gnome.Shell.NetworkAgent' });
this._dialogs = { };
this._vpnRequests = { };
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, flags) {
if (settingName == 'vpn') {
this._vpnRequest(requestId, connection, hints, flags);
return;
}
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) {
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();
delete this._vpnRequests[requestId];
}
},
_vpnRequest: function(requestId, connection, hints, flags) {
let vpnSetting = connection.get_setting_vpn();
let serviceType = vpnSetting.service_type;
this._buildVPNServiceCache();
let binary = this._vpnBinaries[serviceType];
if (!binary) {
log('Invalid VPN service type (cannot find authentication binary)');
/* cancel the auth process */
this._native.respond(requestId, Shell.NetworkAgentResponse.INTERNAL_ERROR);
return;
}
this._vpnRequests[requestId] = new VPNRequestHandler(this._native, requestId, binary, serviceType, connection, hints, flags);
},
_buildVPNServiceCache: function() {
if (this._vpnCacheBuilt)
return;
this._vpnCacheBuilt = true;
this._vpnBinaries = { };
let dir = Gio.file_new_for_path(GLib.build_filenamev([Config.SYSCONFDIR, 'NetworkManager/VPN']));
try {
let fileEnum = dir.enumerate_children('standard::name', Gio.FileQueryInfoFlags.NONE, null);
let info;
while ((info = fileEnum.next_file(null))) {
let name = info.get_name();
if (name.substr(-5) != '.name')
continue;
try {
let keyfile = new GLib.KeyFile();
keyfile.load_from_file(dir.get_child(name).get_path(), GLib.KeyFileFlags.NONE);
let service = keyfile.get_string('VPN Connection', 'service');
let binary = keyfile.get_string('GNOME', 'auth-dialog');
let externalUIMode = false;
try {
externalUIMode = keyfile.get_boolean('GNOME', 'supports-external-ui-mode');
} catch(e) { } // ignore errors if key does not exist
let path = binary;
if (!GLib.path_is_absolute(path)) {
path = GLib.build_filenamev([Config.LIBEXECDIR, path]);
}
if (GLib.file_test(path, GLib.FileTest.IS_EXECUTABLE))
this._vpnBinaries[service] = { fileName: path, externalUIMode: externalUIMode };
else
throw new Error('VPN plugin at %s is not executable'.format(path));
} catch(e) {
log('Error \'%s\' while processing VPN keyfile \'%s\''.
format(e.message, dir.get_child(name).get_path()));
continue;
}
}
} catch(e) {
logError(e, 'error while enumerating VPN auth helpers');
}
}
});