4d7d66bc1f
Center the "Password:" label, and add a bit more spacing
409 lines
16 KiB
JavaScript
409 lines
16 KiB
JavaScript
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
|
/*
|
|
* Copyright 2010 Red Hat, Inc
|
|
*
|
|
* 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.
|
|
*
|
|
* Author: David Zeuthen <davidz@redhat.com>
|
|
*/
|
|
|
|
const Lang = imports.lang;
|
|
const Signals = imports.signals;
|
|
const Shell = imports.gi.Shell;
|
|
const AccountsService = imports.gi.AccountsService;
|
|
const Clutter = imports.gi.Clutter;
|
|
const St = imports.gi.St;
|
|
const Pango = imports.gi.Pango;
|
|
const GLib = imports.gi.GLib;
|
|
const Gio = imports.gi.Gio;
|
|
const Mainloop = imports.mainloop;
|
|
const Polkit = imports.gi.Polkit;
|
|
const PolkitAgent = imports.gi.PolkitAgent;
|
|
|
|
const ModalDialog = imports.ui.modalDialog;
|
|
const ShellEntry = imports.ui.shellEntry;
|
|
|
|
const AuthenticationDialog = new Lang.Class({
|
|
Name: 'AuthenticationDialog',
|
|
Extends: ModalDialog.ModalDialog,
|
|
|
|
_init: function(actionId, message, cookie, userNames) {
|
|
this.parent({ styleClass: 'prompt-dialog' });
|
|
|
|
this.actionId = actionId;
|
|
this.message = message;
|
|
this.userNames = userNames;
|
|
this._wasDismissed = false;
|
|
this._completed = false;
|
|
|
|
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 });
|
|
|
|
this._subjectLabel = new St.Label({ style_class: 'prompt-dialog-headline',
|
|
text: _("Authentication Required") });
|
|
|
|
messageBox.add(this._subjectLabel,
|
|
{ y_fill: false,
|
|
y_align: St.Align.START });
|
|
|
|
this._descriptionLabel = new St.Label({ style_class: 'prompt-dialog-description',
|
|
text: message });
|
|
this._descriptionLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
|
|
this._descriptionLabel.clutter_text.line_wrap = true;
|
|
|
|
messageBox.add(this._descriptionLabel,
|
|
{ y_fill: true,
|
|
y_align: St.Align.START });
|
|
|
|
if (userNames.length > 1) {
|
|
log('polkitAuthenticationAgent: Received ' + userNames.length +
|
|
' identities that can be used for authentication. Only ' +
|
|
'considering one.');
|
|
}
|
|
|
|
let userName = GLib.get_user_name();
|
|
if (userNames.indexOf(userName) < 0)
|
|
userName = 'root';
|
|
if (userNames.indexOf(userName) < 0)
|
|
userName = userNames[0];
|
|
|
|
this._user = AccountsService.UserManager.get_default().get_user(userName);
|
|
let userRealName = this._user.get_real_name()
|
|
this._userLoadedId = this._user.connect('notify::is_loaded',
|
|
Lang.bind(this, this._onUserChanged));
|
|
this._userChangedId = this._user.connect('changed',
|
|
Lang.bind(this, this._onUserChanged));
|
|
|
|
// Special case 'root'
|
|
let userIsRoot = false;
|
|
if (userName == 'root') {
|
|
userIsRoot = true;
|
|
userRealName = _("Administrator");
|
|
}
|
|
|
|
if (userIsRoot) {
|
|
let userLabel = new St.Label(({ style_class: 'polkit-dialog-user-root-label',
|
|
text: userRealName }));
|
|
messageBox.add(userLabel);
|
|
} else {
|
|
let userBox = new St.BoxLayout({ style_class: 'polkit-dialog-user-layout',
|
|
vertical: false });
|
|
messageBox.add(userBox);
|
|
this._userIcon = new St.Icon();
|
|
this._userIcon.hide();
|
|
userBox.add(this._userIcon,
|
|
{ x_fill: true,
|
|
y_fill: false,
|
|
x_align: St.Align.END,
|
|
y_align: St.Align.START });
|
|
let userLabel = new St.Label(({ style_class: 'polkit-dialog-user-label',
|
|
text: userRealName }));
|
|
userBox.add(userLabel,
|
|
{ x_fill: true,
|
|
y_fill: false,
|
|
x_align: St.Align.END,
|
|
y_align: St.Align.MIDDLE });
|
|
}
|
|
|
|
this._onUserChanged();
|
|
|
|
this._passwordBox = new St.BoxLayout({ vertical: false, style_class: 'prompt-dialog-password-box' });
|
|
messageBox.add(this._passwordBox);
|
|
this._passwordLabel = new St.Label(({ style_class: 'prompt-dialog-password-label' }));
|
|
this._passwordBox.add(this._passwordLabel, { y_fill: false, y_align: St.Align.MIDDLE });
|
|
this._passwordEntry = new St.Entry({ style_class: 'prompt-dialog-password-entry',
|
|
text: "",
|
|
can_focus: true});
|
|
ShellEntry.addContextMenu(this._passwordEntry, { isPassword: true });
|
|
this._passwordEntry.clutter_text.connect('activate', Lang.bind(this, this._onEntryActivate));
|
|
this._passwordBox.add(this._passwordEntry,
|
|
{ expand: true });
|
|
this.setInitialKeyFocus(this._passwordEntry);
|
|
this._passwordBox.hide();
|
|
|
|
this._errorMessageLabel = new St.Label({ style_class: 'prompt-dialog-error-label' });
|
|
this._errorMessageLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
|
|
this._errorMessageLabel.clutter_text.line_wrap = true;
|
|
messageBox.add(this._errorMessageLabel);
|
|
this._errorMessageLabel.hide();
|
|
|
|
this._infoMessageLabel = new St.Label({ style_class: 'prompt-dialog-info-label' });
|
|
this._infoMessageLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
|
|
this._infoMessageLabel.clutter_text.line_wrap = true;
|
|
messageBox.add(this._infoMessageLabel);
|
|
this._infoMessageLabel.hide();
|
|
|
|
/* text is intentionally non-blank otherwise the height is not the same as for
|
|
* infoMessage and errorMessageLabel - but it is still invisible because
|
|
* gnome-shell.css sets the color to be transparent
|
|
*/
|
|
this._nullMessageLabel = new St.Label({ style_class: 'prompt-dialog-null-label',
|
|
text: 'abc'});
|
|
this._nullMessageLabel.add_style_class_name('hidden');
|
|
this._nullMessageLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
|
|
this._nullMessageLabel.clutter_text.line_wrap = true;
|
|
messageBox.add(this._nullMessageLabel);
|
|
this._nullMessageLabel.show();
|
|
|
|
this.setButtons([{ label: _("Cancel"),
|
|
action: Lang.bind(this, this.cancel),
|
|
key: Clutter.Escape
|
|
},
|
|
{ label: _("Authenticate"),
|
|
action: Lang.bind(this, this._onAuthenticateButtonPressed),
|
|
default: true
|
|
}]);
|
|
|
|
this._doneEmitted = false;
|
|
|
|
this._identityToAuth = Polkit.UnixUser.new_for_name(userName);
|
|
this._cookie = cookie;
|
|
|
|
this._session = new PolkitAgent.Session({ identity: this._identityToAuth,
|
|
cookie: this._cookie });
|
|
this._session.connect('completed', Lang.bind(this, this._onSessionCompleted));
|
|
this._session.connect('request', Lang.bind(this, this._onSessionRequest));
|
|
this._session.connect('show-error', Lang.bind(this, this._onSessionShowError));
|
|
this._session.connect('show-info', Lang.bind(this, this._onSessionShowInfo));
|
|
},
|
|
|
|
startAuthentication: function() {
|
|
this._session.initiate();
|
|
},
|
|
|
|
_ensureOpen: function() {
|
|
// NOTE: ModalDialog.open() is safe to call if the dialog is
|
|
// already open - it just returns true without side-effects
|
|
if (!this.open(global.get_current_time())) {
|
|
// This can fail if e.g. unable to get input grab
|
|
//
|
|
// In an ideal world this wouldn't happen (because the
|
|
// Shell is in complete control of the session) but that's
|
|
// just not how things work right now.
|
|
//
|
|
// One way to make this happen is by running 'sleep 3;
|
|
// pkexec bash' and then opening a popup menu.
|
|
//
|
|
// We could add retrying if this turns out to be a problem
|
|
|
|
log('polkitAuthenticationAgent: Failed to show modal dialog.' +
|
|
' Dismissing authentication request for action-id ' + this.actionId +
|
|
' cookie ' + this._cookie);
|
|
this._emitDone(false, true);
|
|
}
|
|
},
|
|
|
|
_emitDone: function(keepVisible, dismissed) {
|
|
if (!this._doneEmitted) {
|
|
this._doneEmitted = true;
|
|
this.emit('done', keepVisible, dismissed);
|
|
}
|
|
},
|
|
|
|
_onEntryActivate: function() {
|
|
let response = this._passwordEntry.get_text();
|
|
this._session.response(response);
|
|
// When the user responds, dismiss already shown info and
|
|
// error texts (if any)
|
|
this._errorMessageLabel.hide();
|
|
this._infoMessageLabel.hide();
|
|
this._nullMessageLabel.show();
|
|
},
|
|
|
|
_onAuthenticateButtonPressed: function() {
|
|
this._onEntryActivate();
|
|
},
|
|
|
|
_onSessionCompleted: function(session, gainedAuthorization) {
|
|
if (this._completed)
|
|
return;
|
|
|
|
this._completed = true;
|
|
|
|
if (!gainedAuthorization) {
|
|
/* Unless we are showing an existing error message from the PAM
|
|
* module (the PAM module could be reporting the authentication
|
|
* error providing authentication-method specific information),
|
|
* show "Sorry, that didn't work. Please try again."
|
|
*/
|
|
if (!this._errorMessageLabel.visible && !this._wasDismissed) {
|
|
/* Translators: "that didn't work" refers to the fact that the
|
|
* requested authentication was not gained; this can happen
|
|
* because of an authentication error (like invalid password),
|
|
* for instance. */
|
|
this._errorMessageLabel.set_text(_("Sorry, that didn\'t work. Please try again."));
|
|
this._errorMessageLabel.show();
|
|
this._infoMessageLabel.hide();
|
|
this._nullMessageLabel.hide();
|
|
}
|
|
}
|
|
this._emitDone(!gainedAuthorization, false);
|
|
},
|
|
|
|
_onSessionRequest: function(session, request, echo_on) {
|
|
// Cheap localization trick
|
|
if (request == 'Password:' || request == 'Password: ')
|
|
this._passwordLabel.set_text(_("Password:"));
|
|
else
|
|
this._passwordLabel.set_text(request);
|
|
|
|
if (echo_on)
|
|
this._passwordEntry.clutter_text.set_password_char('');
|
|
else
|
|
this._passwordEntry.clutter_text.set_password_char('\u25cf'); // ● U+25CF BLACK CIRCLE
|
|
|
|
this._passwordBox.show();
|
|
this._passwordEntry.set_text('');
|
|
this._passwordEntry.grab_key_focus();
|
|
this._ensureOpen();
|
|
},
|
|
|
|
_onSessionShowError: function(session, text) {
|
|
this._passwordEntry.set_text('');
|
|
this._errorMessageLabel.set_text(text);
|
|
this._errorMessageLabel.show();
|
|
this._infoMessageLabel.hide();
|
|
this._nullMessageLabel.hide();
|
|
this._ensureOpen();
|
|
},
|
|
|
|
_onSessionShowInfo: function(session, text) {
|
|
this._passwordEntry.set_text('');
|
|
this._infoMessageLabel.set_text(text);
|
|
this._infoMessageLabel.show();
|
|
this._errorMessageLabel.hide();
|
|
this._nullMessageLabel.hide();
|
|
this._ensureOpen();
|
|
},
|
|
|
|
destroySession: function() {
|
|
if (this._session) {
|
|
if (!this._completed)
|
|
this._session.cancel();
|
|
this._session = null;
|
|
}
|
|
},
|
|
|
|
_onUserChanged: function() {
|
|
if (this._user.is_loaded) {
|
|
if (this._userIcon) {
|
|
let iconFileName = this._user.get_icon_file();
|
|
let iconFile = Gio.file_new_for_path(iconFileName);
|
|
let icon;
|
|
if (iconFile.query_exists(null)) {
|
|
icon = new Gio.FileIcon({file: iconFile});
|
|
} else {
|
|
icon = new Gio.ThemedIcon({name: 'avatar-default'});
|
|
}
|
|
this._userIcon.set_gicon (icon);
|
|
this._userIcon.show();
|
|
}
|
|
}
|
|
},
|
|
|
|
cancel: function() {
|
|
this._wasDismissed = true;
|
|
this.close(global.get_current_time());
|
|
this._emitDone(false, true);
|
|
},
|
|
});
|
|
Signals.addSignalMethods(AuthenticationDialog.prototype);
|
|
|
|
const AuthenticationAgent = new Lang.Class({
|
|
Name: 'AuthenticationAgent',
|
|
|
|
_init: function() {
|
|
this._native = new Shell.PolkitAuthenticationAgent();
|
|
this._native.connect('initiate', Lang.bind(this, this._onInitiate));
|
|
this._native.connect('cancel', Lang.bind(this, this._onCancel));
|
|
this._currentDialog = null;
|
|
this._isCompleting = false;
|
|
},
|
|
|
|
_onInitiate: function(nativeAgent, actionId, message, iconName, cookie, userNames) {
|
|
this._currentDialog = new AuthenticationDialog(actionId, message, cookie, userNames);
|
|
|
|
// We actually don't want to open the dialog until we know for
|
|
// sure that we're going to interact with the user. For
|
|
// example, if the password for the identity to auth is blank
|
|
// (which it will be on a live CD) then there will be no
|
|
// conversation at all... of course, we don't *know* that
|
|
// until we actually try it.
|
|
//
|
|
// See https://bugzilla.gnome.org/show_bug.cgi?id=643062 for more
|
|
// discussion.
|
|
|
|
this._currentDialog.connect('done', Lang.bind(this, this._onDialogDone));
|
|
this._currentDialog.startAuthentication();
|
|
},
|
|
|
|
_onCancel: function(nativeAgent) {
|
|
this._completeRequest(false, false);
|
|
},
|
|
|
|
_onDialogDone: function(dialog, keepVisible, dismissed) {
|
|
this._completeRequest(keepVisible, dismissed);
|
|
},
|
|
|
|
_reallyCompleteRequest: function(dismissed) {
|
|
this._currentDialog.close();
|
|
this._currentDialog.destroySession();
|
|
this._currentDialog = null;
|
|
this._isCompleting = false;
|
|
|
|
this._native.complete(dismissed)
|
|
},
|
|
|
|
_completeRequest: function(keepVisible, wasDismissed) {
|
|
if (this._isCompleting)
|
|
return;
|
|
|
|
this._isCompleting = true;
|
|
|
|
if (keepVisible) {
|
|
// Give the user 2 seconds to read 'Authentication Failure' before
|
|
// dismissing the dialog
|
|
Mainloop.timeout_add(2000,
|
|
Lang.bind(this,
|
|
function() {
|
|
this._reallyCompleteRequest(wasDismissed);
|
|
return false;
|
|
}));
|
|
} else {
|
|
this._reallyCompleteRequest(wasDismissed);
|
|
}
|
|
}
|
|
});
|
|
|
|
function init() {
|
|
let agent = new AuthenticationAgent();
|
|
}
|