364 lines
13 KiB
JavaScript
364 lines
13 KiB
JavaScript
|
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
||
|
|
||
|
const AccountsService = imports.gi.AccountsService;
|
||
|
const Clutter = imports.gi.Clutter;
|
||
|
const GdmGreeter = imports.gi.GdmGreeter;
|
||
|
const Gio = imports.gi.Gio;
|
||
|
const GLib = imports.gi.GLib;
|
||
|
const Gtk = imports.gi.Gtk;
|
||
|
const Lang = imports.lang;
|
||
|
const Mainloop = imports.mainloop;
|
||
|
const Signals = imports.signals;
|
||
|
const Shell = imports.gi.Shell;
|
||
|
const St = imports.gi.St;
|
||
|
|
||
|
const ModalDialog = imports.ui.modalDialog;
|
||
|
|
||
|
const Fprint = imports.gdm.fingerprint;
|
||
|
const GdmLoginDialog = imports.gdm.loginDialog;
|
||
|
|
||
|
function _fadeInActor(actor) {
|
||
|
if (actor.opacity == 255 && actor.visible)
|
||
|
return;
|
||
|
|
||
|
actor.show();
|
||
|
let [minHeight, naturalHeight] = actor.get_preferred_height(-1);
|
||
|
|
||
|
actor.opacity = 0;
|
||
|
actor.set_height(0);
|
||
|
Tweener.addTween(actor,
|
||
|
{ opacity: 255,
|
||
|
height: naturalHeight,
|
||
|
time: _FADE_ANIMATION_TIME,
|
||
|
transition: 'easeOutQuad',
|
||
|
onComplete: function() {
|
||
|
this.set_height(-1);
|
||
|
},
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function _fadeOutActor(actor) {
|
||
|
if (!actor.visible || actor.opacity == 0) {
|
||
|
actor.opacity = 0;
|
||
|
actor.hide();
|
||
|
}
|
||
|
|
||
|
Tweener.addTween(actor,
|
||
|
{ opacity: 0,
|
||
|
height: 0,
|
||
|
time: _FADE_ANIMATION_TIME,
|
||
|
transition: 'easeOutQuad',
|
||
|
onComplete: function() {
|
||
|
this.hide();
|
||
|
this.set_height(-1);
|
||
|
},
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// A widget showing the user avatar and name
|
||
|
const UserWidget = new Lang.Class({
|
||
|
Name: 'UserWidget',
|
||
|
|
||
|
_init: function(user) {
|
||
|
this._user = user;
|
||
|
|
||
|
this.actor = new St.BoxLayout({ style_class: 'status-chooser',
|
||
|
vertical: false,
|
||
|
reactive: false
|
||
|
});
|
||
|
|
||
|
this._iconBin = new St.Bin({ style_class: 'status-chooser-user-icon' });
|
||
|
this.actor.add(this._iconBin,
|
||
|
{ x_fill: true,
|
||
|
y_fill: true });
|
||
|
|
||
|
this._label = new St.Label({ style_class: 'login-dialog-prompt-label',
|
||
|
// FIXME:
|
||
|
style: 'text-align: right' });
|
||
|
this.actor.add(this._label,
|
||
|
{ expand: true,
|
||
|
x_fill: true,
|
||
|
y_fill: true
|
||
|
});
|
||
|
|
||
|
this._userLoadedId = this._user.connect('notify::is-loaded',
|
||
|
Lang.bind(this,
|
||
|
this._updateUser));
|
||
|
this._userChangedId = this._user.connect('changed',
|
||
|
Lang.bind(this,
|
||
|
this._updateUser));
|
||
|
this.actor.connect('notify::mapped', Lang.bind(this, function() {
|
||
|
if (this.actor.mapped)
|
||
|
this._updateUser();
|
||
|
}));
|
||
|
},
|
||
|
|
||
|
destroy: function() {
|
||
|
// clean up signal handlers
|
||
|
if (this._userLoadedId != 0) {
|
||
|
this._user.disconnect(this._userLoadedId);
|
||
|
this._userLoadedId = 0;
|
||
|
}
|
||
|
|
||
|
if (this._userChangedId != 0) {
|
||
|
this._user.disconnect(this._userChangedId);
|
||
|
this._userChangedId = 0;
|
||
|
}
|
||
|
|
||
|
this.actor.destroy();
|
||
|
},
|
||
|
|
||
|
_updateUser: function() {
|
||
|
let iconFile = null;
|
||
|
if (this._user.is_loaded) {
|
||
|
this._label.text = this._user.get_real_name();
|
||
|
iconFile = this._user.get_icon_file();
|
||
|
if (!GLib.file_test(iconFile, GLib.FileTest.EXISTS))
|
||
|
iconFile = null;
|
||
|
} else {
|
||
|
this._label.text = "";
|
||
|
}
|
||
|
|
||
|
if (iconFile)
|
||
|
this._setIconFromFile(iconFile);
|
||
|
else
|
||
|
this._setIconFromName('avatar-default');
|
||
|
},
|
||
|
|
||
|
// XXX: a GFileIcon instead?
|
||
|
_setIconFromFile: function(iconFile) {
|
||
|
this._iconBin.set_style('background-image: url("' + iconFile + '");' +
|
||
|
'background-size: contain;');
|
||
|
this._iconBin.child = null;
|
||
|
},
|
||
|
|
||
|
_setIconFromName: function(iconName) {
|
||
|
this._iconBin.set_style(null);
|
||
|
|
||
|
if (iconName != null) {
|
||
|
let icon = new St.Icon({ icon_name: iconName,
|
||
|
icon_type: St.IconType.SYMBOLIC,
|
||
|
icon_size: DIALOG_ICON_SIZE });
|
||
|
this._iconBin.child = icon;
|
||
|
this._iconBin.show();
|
||
|
} else {
|
||
|
this._iconBin.child = null;
|
||
|
this._iconBin.hide();
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
const UnlockDialog = new Lang.Class({
|
||
|
Name: 'UnlockDialog',
|
||
|
Extends: ModalDialog.ModalDialog,
|
||
|
|
||
|
_init: function() {
|
||
|
this.parent({ shellReactive: true,
|
||
|
styleClass: 'login-dialog' });
|
||
|
|
||
|
this._userManager = AccountsService.UserManager.get_default();
|
||
|
this._userName = GLib.get_user_name();
|
||
|
this._user = this._userManager.get_user(this._userName);
|
||
|
|
||
|
this._greeterClient = GdmGreeter.Server.new_for_display_sync(null, null);
|
||
|
|
||
|
this._greeterClient.call_start_conversation_sync(GdmLoginDialog.PASSWORD_SERVICE_NAME, null);
|
||
|
|
||
|
this._greeterClient.connect('reset',
|
||
|
Lang.bind(this, this._onReset));
|
||
|
this._greeterClient.connect('ready',
|
||
|
Lang.bind(this, this._onReady));
|
||
|
this._greeterClient.connect('info',
|
||
|
Lang.bind(this, this._onInfo));
|
||
|
this._greeterClient.connect('problem',
|
||
|
Lang.bind(this, this._onProblem));
|
||
|
this._greeterClient.connect('info-query',
|
||
|
Lang.bind(this, this._onInfoQuery));
|
||
|
this._greeterClient.connect('secret-info-query',
|
||
|
Lang.bind(this, this._onSecretInfoQuery));
|
||
|
this._greeterClient.connect('session-opened',
|
||
|
Lang.bind(this, this._onSessionOpened));
|
||
|
this._greeterClient.connect('conversation-stopped',
|
||
|
Lang.bind(this, this._onConversationStopped));
|
||
|
|
||
|
this._fprintManager = new Fprint.FprintManager();
|
||
|
this._startFingerprintConversationIfNeeded();
|
||
|
|
||
|
this._userWidget = new UserWidget(this._user);
|
||
|
this.contentLayout.add_actor(this._userWidget.actor);
|
||
|
|
||
|
this._promptLayout = new St.BoxLayout({ style_class: 'login-dialog-prompt-layout',
|
||
|
vertical: false
|
||
|
});
|
||
|
|
||
|
this._promptLabel = new St.Label({ style_class: 'login-dialog-prompt-label' });
|
||
|
this._promptLayout.add(this._promptLabel,
|
||
|
{ expand: false,
|
||
|
x_fill: true,
|
||
|
y_fill: true,
|
||
|
x_align: St.Align.START });
|
||
|
|
||
|
this._promptEntry = new St.Entry({ style_class: 'login-dialog-prompt-entry',
|
||
|
can_focus: true });
|
||
|
this._promptLayout.add(this._promptEntry,
|
||
|
{ expand: true,
|
||
|
x_fill: true,
|
||
|
y_fill: false,
|
||
|
x_align: St.Align.START });
|
||
|
|
||
|
this.contentLayout.add_actor(this._promptLayout);
|
||
|
|
||
|
// Translators: this message is shown below the password entry field
|
||
|
// to indicate the user can swipe their finger instead
|
||
|
this._promptFingerprintMessage = new St.Label({ text: _("(or swipe finger)"),
|
||
|
style_class: 'login-dialog-prompt-fingerprint-message' });
|
||
|
this._promptFingerprintMessage.hide();
|
||
|
this.contentLayout.add_actor(this._promptFingerprintMessage);
|
||
|
|
||
|
this._okButton = { label: _("Unlock"),
|
||
|
action: Lang.bind(this, this._doUnlock),
|
||
|
key: Clutter.KEY_Return,
|
||
|
};
|
||
|
this.setButtons([this._okButton]);
|
||
|
|
||
|
this._updateOkButton(true);
|
||
|
},
|
||
|
|
||
|
_updateOkButton: function(sensitive) {
|
||
|
this._okButton.button.reactive = sensitive;
|
||
|
this._okButton.button.can_focus = sensitive;
|
||
|
if (sensitive)
|
||
|
this._okButton.button.remove_style_pseudo_class('disabled');
|
||
|
else
|
||
|
this._okButton.button.add_style_pseudo_class('disabled');
|
||
|
},
|
||
|
|
||
|
_onReset: function() {
|
||
|
// I'm not sure this is emitted for external greeters...
|
||
|
|
||
|
this._greeterClient.call_start_conversation_sync(GdmLoginDialog.PASSWORD_SERVICE_NAME, null);
|
||
|
this._startFingerprintConversationIfNeeded();
|
||
|
},
|
||
|
|
||
|
_startFingerprintConversationIfNeeded: function() {
|
||
|
this._haveFingerprintReader = false;
|
||
|
|
||
|
// FIXME: the greeter has a GSettings key for disabling fingerprint auth
|
||
|
|
||
|
this._fprintManager.GetDefaultDeviceRemote(Lang.bind(this,
|
||
|
function(device, error) {
|
||
|
if (!error && device)
|
||
|
this._haveFingerprintReader = true;
|
||
|
|
||
|
if (this._haveFingerprintReader)
|
||
|
this._greeterClient.call_start_conversation_sync(GdmLoginDialog.FINGERPRINT_SERVICE_NAME, null);
|
||
|
}));
|
||
|
},
|
||
|
|
||
|
_onReady: function(greeter, serviceName) {
|
||
|
greeter.call_begin_verification_for_user_sync(serviceName, this._userName, null);
|
||
|
},
|
||
|
|
||
|
_onInfo: function(greeter, serviceName, info) {
|
||
|
// We don't display fingerprint messages, because they
|
||
|
// have words like UPEK in them. Instead we use the messages
|
||
|
// as a cue to display our own message.
|
||
|
if (serviceName == GdmLoginDialog.FINGERPRINT_SERVICE_NAME &&
|
||
|
this._haveFingerprintReader &&
|
||
|
(!this._promptFingerprintMessage.visible ||
|
||
|
this._promptFingerprintMessage.opacity != 255)) {
|
||
|
|
||
|
_fadeInActor(this._promptFingerprintMessage);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (serviceName != GdmLoginDialog.PASSWORD_SERVICE_NAME)
|
||
|
return;
|
||
|
Main.notify(info);
|
||
|
},
|
||
|
|
||
|
_onProblem: function(client, serviceName, problem) {
|
||
|
// we don't want to show auth failed messages to
|
||
|
// users who haven't enrolled their fingerprint.
|
||
|
if (serviceName != GdmLoginDialog.PASSWORD_SERVICE_NAME)
|
||
|
return;
|
||
|
Main.notifyError(problem);
|
||
|
},
|
||
|
|
||
|
_onInfoQuery: function(client, serviceName, question) {
|
||
|
// We only expect questions to come from the main auth service
|
||
|
if (serviceName != GdmLoginDialog.PASSWORD_SERVICE_NAME)
|
||
|
return;
|
||
|
|
||
|
this._promptLabel.text = question;
|
||
|
this._promptEntry.text = '';
|
||
|
this._promptEntry.clutter_text.set_password_char('');
|
||
|
|
||
|
this._currentQuery = serviceName;
|
||
|
this._updateOkButton(true);
|
||
|
},
|
||
|
|
||
|
_onSecretInfoQuery: function(client, serviceName, secretQuestion) {
|
||
|
// We only expect secret requests to come from the main auth service
|
||
|
if (serviceName != GdmLoginDialog.PASSWORD_SERVICE_NAME)
|
||
|
return;
|
||
|
|
||
|
this._promptLabel.text = secretQuestion;
|
||
|
this._promptEntry.text = '';
|
||
|
this._promptEntry.clutter_text.set_password_char('\u25cf');
|
||
|
|
||
|
this._currentQuery = serviceName;
|
||
|
this._updateOkButton(true);
|
||
|
},
|
||
|
|
||
|
_doUnlock: function() {
|
||
|
if (!this._currentQuery)
|
||
|
return;
|
||
|
|
||
|
let query = this._currentQuery;
|
||
|
this._currentQuery = null;
|
||
|
|
||
|
this._updateOkButton(false);
|
||
|
|
||
|
this._greeterClient.call_answer_query_sync(query, this._promptEntry.text, null);
|
||
|
},
|
||
|
|
||
|
_onConversationStopped: function(client, serviceName) {
|
||
|
// if the password service fails, then cancel everything.
|
||
|
// But if, e.g., fingerprint fails, still give
|
||
|
// password authentication a chance to succeed
|
||
|
if (serviceName == GdmLoginDialog.PASSWORD_SERVICE_NAME) {
|
||
|
this._greeterClient.call_cancel_sync(null);
|
||
|
this.emit('failed');
|
||
|
} else if (serviceName == GdmLoginDialog.FINGERPRINT_SERVICE_NAME) {
|
||
|
_fadeOutActor(this._promptFingerprintMessage);
|
||
|
}
|
||
|
},
|
||
|
|
||
|
_onSessionOpened: function(client, serviceName) {
|
||
|
// For external greeters, SessionOpened means we succeded
|
||
|
// in the authentication process
|
||
|
|
||
|
// Close the greeter proxy
|
||
|
this._greeterClient.run_dispose();
|
||
|
this._greeterClient = null;
|
||
|
|
||
|
this.emit('unlocked');
|
||
|
},
|
||
|
|
||
|
destroy: function() {
|
||
|
if (this._greeterClient) {
|
||
|
this._greeterClient.run_dispose();
|
||
|
this._greeterClient = null;
|
||
|
}
|
||
|
|
||
|
this.parent();
|
||
|
},
|
||
|
|
||
|
cancel: function() {
|
||
|
this._greeterClient.call_cancel_sync(null);
|
||
|
|
||
|
this.destroy();
|
||
|
},
|
||
|
});
|