From 68c280c3bcf8a67a90eb25c30b2e6c2493d7d4d9 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Wed, 30 Sep 2015 12:47:01 -0400 Subject: [PATCH 1/4] authPrompt: use correct service name when answering preemptively If the user hits enter before a question is asked, we try to answer that question as soon as it comes in. This doesn't work right now because we only answer the question if there's a query service, but there won't ever be one until the question is asked. This commit corrects the code to it's intention, which is to answer the first service that asks a question, as as soon as possible. --- js/gdm/authPrompt.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js index 34ad7fb..816c69c 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js @@ -167,62 +167,61 @@ const AuthPrompt = new Lang.Class({ can_focus: true, label: _("Next") }); this.nextButton.connect('clicked', Lang.bind(this, function() { this.emit('next'); })); this.nextButton.add_style_pseudo_class('default'); this._buttonBox.add(this.nextButton, { expand: false, x_fill: false, y_fill: false, x_align: St.Align.END, y_align: St.Align.END }); this._updateNextButtonSensitivity(this._entry.text.length > 0); this._entry.clutter_text.connect('text-changed', Lang.bind(this, function() { if (!this._userVerifier.hasPendingMessages) this._fadeOutMessage(); this._updateNextButtonSensitivity(this._entry.text.length > 0); })); this._entry.clutter_text.connect('activate', Lang.bind(this, function() { this.emit('next'); })); }, _onAskQuestion: function(verifier, serviceName, question, passwordChar) { if (this._preemptiveAnswer) { - if (this._queryingService) - this._userVerifier.answerQuery(this._queryingService, this._preemptiveAnswer); + this._userVerifier.answerQuery(serviceName, this._preemptiveAnswer); this._preemptiveAnswer = null; return; } if (this._queryingService) this.clear(); this._queryingService = serviceName; this.setPasswordChar(passwordChar); this.setQuestion(question); if (passwordChar) { if (this._userVerifier.reauthenticating) this.nextButton.label = _("Unlock"); else this.nextButton.label = C_("button", "Sign In"); } else { this.nextButton.label = _("Next"); } this.updateSensitivity(true); this.emit('prompted'); }, _onOVirtUserAuthenticated: function() { if (this.verificationStatus != AuthPromptStatus.VERIFICATION_SUCCEEDED) this.reset(); }, _onSmartcardStatusChanged: function() { -- 2.5.0 From 1b7fa7ad6874208873eb8ce1e8b03dc07cb01217 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Wed, 30 Sep 2015 12:51:24 -0400 Subject: [PATCH 2/4] authPrompt: don't fade out auth messages if user types password up front Right now we fade out any stale auth messages as soon as the user starts typing. This behavior doesn't really make sense if the user is typing up front, before a password is asked. --- js/gdm/authPrompt.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js index 816c69c..11b6a23 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js @@ -155,61 +155,61 @@ const AuthPrompt = new Lang.Class({ x_align: St.Align.START, y_align: St.Align.END }); this._buttonBox.add(this._defaultButtonWell, { expand: true, x_fill: false, y_fill: false, x_align: St.Align.END, y_align: St.Align.MIDDLE }); this.nextButton = new St.Button({ style_class: 'modal-dialog-button', button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE, reactive: true, can_focus: true, label: _("Next") }); this.nextButton.connect('clicked', Lang.bind(this, function() { this.emit('next'); })); this.nextButton.add_style_pseudo_class('default'); this._buttonBox.add(this.nextButton, { expand: false, x_fill: false, y_fill: false, x_align: St.Align.END, y_align: St.Align.END }); this._updateNextButtonSensitivity(this._entry.text.length > 0); this._entry.clutter_text.connect('text-changed', Lang.bind(this, function() { - if (!this._userVerifier.hasPendingMessages) + if (!this._userVerifier.hasPendingMessages && this._queryingService && !this._preemptiveAnswer) this._fadeOutMessage(); this._updateNextButtonSensitivity(this._entry.text.length > 0); })); this._entry.clutter_text.connect('activate', Lang.bind(this, function() { this.emit('next'); })); }, _onAskQuestion: function(verifier, serviceName, question, passwordChar) { if (this._preemptiveAnswer) { this._userVerifier.answerQuery(serviceName, this._preemptiveAnswer); this._preemptiveAnswer = null; return; } if (this._queryingService) this.clear(); this._queryingService = serviceName; this.setPasswordChar(passwordChar); this.setQuestion(question); if (passwordChar) { if (this._userVerifier.reauthenticating) this.nextButton.label = _("Unlock"); else this.nextButton.label = C_("button", "Sign In"); } else { this.nextButton.label = _("Next"); -- 2.5.0 From 3b767c0a55b136b53b7c85aff2095969fb230ce4 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Wed, 30 Sep 2015 14:36:33 -0400 Subject: [PATCH 3/4] authPrompt: don't spin unless answering question --- js/gdm/authPrompt.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js index 11b6a23..875f9c5 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js @@ -37,62 +37,62 @@ const BeginRequestType = { }; const AuthPrompt = new Lang.Class({ Name: 'AuthPrompt', _init: function(gdmClient, mode) { this.verificationStatus = AuthPromptStatus.NOT_VERIFYING; this._gdmClient = gdmClient; this._mode = mode; let reauthenticationOnly; if (this._mode == AuthPromptMode.UNLOCK_ONLY) reauthenticationOnly = true; else if (this._mode == AuthPromptMode.UNLOCK_OR_LOG_IN) reauthenticationOnly = false; this._userVerifier = new GdmUtil.ShellUserVerifier(this._gdmClient, { reauthenticationOnly: reauthenticationOnly }); this._userVerifier.connect('ask-question', Lang.bind(this, this._onAskQuestion)); this._userVerifier.connect('show-message', Lang.bind(this, this._onShowMessage)); this._userVerifier.connect('verification-failed', Lang.bind(this, this._onVerificationFailed)); this._userVerifier.connect('verification-complete', Lang.bind(this, this._onVerificationComplete)); this._userVerifier.connect('reset', Lang.bind(this, this._onReset)); this._userVerifier.connect('smartcard-status-changed', Lang.bind(this, this._onSmartcardStatusChanged)); this._userVerifier.connect('ovirt-user-authenticated', Lang.bind(this, this._onOVirtUserAuthenticated)); this.smartcardDetected = this._userVerifier.smartcardDetected; this.connect('next', Lang.bind(this, function() { this.updateSensitivity(false); - this.startSpinning(); if (this._queryingService) { + this.startSpinning(); this._userVerifier.answerQuery(this._queryingService, this._entry.text); } else { this._preemptiveAnswer = this._entry.text; } })); this.actor = new St.BoxLayout({ style_class: 'login-dialog-prompt-layout', vertical: true }); this.actor.connect('destroy', Lang.bind(this, this._onDestroy)); this.actor.connect('key-press-event', Lang.bind(this, function(actor, event) { if (event.get_key_symbol() == Clutter.KEY_Escape) { this.cancel(); } return Clutter.EVENT_PROPAGATE; })); this._userWell = new St.Bin({ x_fill: true, x_align: St.Align.START }); this.actor.add(this._userWell, { x_align: St.Align.START, x_fill: true, y_fill: true, expand: true }); this._label = new St.Label({ style_class: 'login-dialog-prompt-label' }); this.actor.add(this._label, { expand: true, x_fill: false, y_fill: true, -- 2.5.0 From 22d618c293906defbac877925491af13f8def9a3 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Mon, 5 Oct 2015 15:26:18 -0400 Subject: [PATCH 4/4] authPrompt: stop accepting preemptive answer if user stops typing We only want to allow the user to type the preemptive password in one smooth motion. If they start to type, and then stop typing, we should discard their preemptive password as expired. Typing ahead the password is just a convenience for users who don't want to manually lift the shift before typing their passwords, after all. --- js/gdm/authPrompt.js | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js index 875f9c5..fe80519 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js @@ -1,101 +1,109 @@ // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- const Clutter = imports.gi.Clutter; const Lang = imports.lang; const Signals = imports.signals; const St = imports.gi.St; const Animation = imports.ui.animation; const Batch = imports.gdm.batch; const GdmUtil = imports.gdm.util; +const Meta = imports.gi.Meta; const Params = imports.misc.params; const ShellEntry = imports.ui.shellEntry; const Tweener = imports.ui.tweener; const UserWidget = imports.ui.userWidget; const DEFAULT_BUTTON_WELL_ICON_SIZE = 24; const DEFAULT_BUTTON_WELL_ANIMATION_DELAY = 1.0; const DEFAULT_BUTTON_WELL_ANIMATION_TIME = 0.3; const MESSAGE_FADE_OUT_ANIMATION_TIME = 0.5; const AuthPromptMode = { UNLOCK_ONLY: 0, UNLOCK_OR_LOG_IN: 1 }; const AuthPromptStatus = { NOT_VERIFYING: 0, VERIFYING: 1, VERIFICATION_FAILED: 2, VERIFICATION_SUCCEEDED: 3 }; const BeginRequestType = { PROVIDE_USERNAME: 0, DONT_PROVIDE_USERNAME: 1 }; const AuthPrompt = new Lang.Class({ Name: 'AuthPrompt', _init: function(gdmClient, mode) { this.verificationStatus = AuthPromptStatus.NOT_VERIFYING; this._gdmClient = gdmClient; this._mode = mode; + this._idleMonitor = Meta.IdleMonitor.get_core(); + let reauthenticationOnly; if (this._mode == AuthPromptMode.UNLOCK_ONLY) reauthenticationOnly = true; else if (this._mode == AuthPromptMode.UNLOCK_OR_LOG_IN) reauthenticationOnly = false; this._userVerifier = new GdmUtil.ShellUserVerifier(this._gdmClient, { reauthenticationOnly: reauthenticationOnly }); this._userVerifier.connect('ask-question', Lang.bind(this, this._onAskQuestion)); this._userVerifier.connect('show-message', Lang.bind(this, this._onShowMessage)); this._userVerifier.connect('verification-failed', Lang.bind(this, this._onVerificationFailed)); this._userVerifier.connect('verification-complete', Lang.bind(this, this._onVerificationComplete)); this._userVerifier.connect('reset', Lang.bind(this, this._onReset)); this._userVerifier.connect('smartcard-status-changed', Lang.bind(this, this._onSmartcardStatusChanged)); this._userVerifier.connect('ovirt-user-authenticated', Lang.bind(this, this._onOVirtUserAuthenticated)); this.smartcardDetected = this._userVerifier.smartcardDetected; this.connect('next', Lang.bind(this, function() { this.updateSensitivity(false); if (this._queryingService) { this.startSpinning(); this._userVerifier.answerQuery(this._queryingService, this._entry.text); } else { this._preemptiveAnswer = this._entry.text; + + if (this._preemptiveAnswerWatchId) { + this._idleMonitor.remove_watch(this._preemptiveAnswerWatchId); + this._preemptiveAnswerWatchId = 0; + } } })); this.actor = new St.BoxLayout({ style_class: 'login-dialog-prompt-layout', vertical: true }); this.actor.connect('destroy', Lang.bind(this, this._onDestroy)); this.actor.connect('key-press-event', Lang.bind(this, function(actor, event) { if (event.get_key_symbol() == Clutter.KEY_Escape) { this.cancel(); } return Clutter.EVENT_PROPAGATE; })); this._userWell = new St.Bin({ x_fill: true, x_align: St.Align.START }); this.actor.add(this._userWell, { x_align: St.Align.START, x_fill: true, y_fill: true, expand: true }); this._label = new St.Label({ style_class: 'login-dialog-prompt-label' }); this.actor.add(this._label, { expand: true, x_fill: false, y_fill: true, x_align: St.Align.START }); this._entry = new St.Entry({ style_class: 'login-dialog-prompt-entry', can_focus: true }); @@ -107,60 +115,65 @@ const AuthPrompt = new Lang.Class({ y_fill: false, x_align: St.Align.START }); this._entry.grab_key_focus(); this._message = new St.Label({ opacity: 0, styleClass: 'login-dialog-message' }); this._message.clutter_text.line_wrap = true; this.actor.add(this._message, { x_fill: false, x_align: St.Align.START, y_align: St.Align.START }); this._buttonBox = new St.BoxLayout({ style_class: 'login-dialog-button-box', vertical: false }); this.actor.add(this._buttonBox, { expand: true, x_align: St.Align.MIDDLE, y_align: St.Align.END }); this._defaultButtonWell = new St.Widget({ layout_manager: new Clutter.BinLayout() }); this._defaultButtonWellActor = null; this._initButtons(); let spinnerIcon = global.datadir + '/theme/process-working.svg'; this._spinner = new Animation.AnimatedIcon(spinnerIcon, DEFAULT_BUTTON_WELL_ICON_SIZE); this._spinner.actor.opacity = 0; this._spinner.actor.show(); this._defaultButtonWell.add_child(this._spinner.actor); }, _onDestroy: function() { + if (this._preemptiveAnswerWatchId) { + this._idleMonitor.remove_watch(this._preemptiveAnswerWatchId); + this._preemptiveAnswerWatchId = 0; + } + this._userVerifier.destroy(); this._userVerifier = null; }, _initButtons: function() { this.cancelButton = new St.Button({ style_class: 'modal-dialog-button', button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE, reactive: true, can_focus: true, label: _("Cancel") }); this.cancelButton.connect('clicked', Lang.bind(this, function() { this.cancel(); })); this._buttonBox.add(this.cancelButton, { expand: false, x_fill: false, y_fill: false, x_align: St.Align.START, y_align: St.Align.END }); this._buttonBox.add(this._defaultButtonWell, { expand: true, x_fill: false, y_fill: false, x_align: St.Align.END, y_align: St.Align.MIDDLE }); this.nextButton = new St.Button({ style_class: 'modal-dialog-button', button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE, reactive: true, @@ -316,60 +329,65 @@ const AuthPrompt = new Lang.Class({ else Tweener.addTween(actor, { opacity: 255, time: DEFAULT_BUTTON_WELL_ANIMATION_TIME, delay: DEFAULT_BUTTON_WELL_ANIMATION_DELAY, transition: 'linear' }); } this._defaultButtonWellActor = actor; }, startSpinning: function() { this.setActorInDefaultButtonWell(this._spinner.actor, true); }, stopSpinning: function() { this.setActorInDefaultButtonWell(null, false); }, clear: function() { this._entry.text = ''; this.stopSpinning(); }, setPasswordChar: function(passwordChar) { this._entry.clutter_text.set_password_char(passwordChar); this._entry.menu.isPassword = passwordChar != ''; }, setQuestion: function(question) { + if (this._preemptiveAnswerWatchId) { + this._idleMonitor.remove_watch(this._preemptiveAnswerWatchId); + this._preemptiveAnswerWatchId = 0; + } + this._label.set_text(question); this._label.show(); this._entry.show(); this._entry.grab_key_focus(); }, getAnswer: function() { let text; if (this._preemptiveAnswer) { text = this._preemptiveAnswer; this._preemptiveAnswer = null; } else { text = this._entry.get_text(); } return text; }, _fadeOutMessage: function() { if (this._message.opacity == 0) return; Tweener.removeTweens(this._message); Tweener.addTween(this._message, { opacity: 0, time: MESSAGE_FADE_OUT_ANIMATION_TIME, transition: 'easeOutQuad' }); @@ -401,66 +419,86 @@ const AuthPrompt = new Lang.Class({ }, updateSensitivity: function(sensitive) { this._updateNextButtonSensitivity(sensitive && this._entry.text.length > 0); this._entry.reactive = sensitive; this._entry.clutter_text.editable = sensitive; }, hide: function() { this.setActorInDefaultButtonWell(null, true); this.actor.hide(); this._message.opacity = 0; this.setUser(null); this.updateSensitivity(true); this._entry.set_text(''); }, setUser: function(user) { let oldChild = this._userWell.get_child(); if (oldChild) oldChild.destroy(); if (user) { let userWidget = new UserWidget.UserWidget(user); this._userWell.set_child(userWidget.actor); } }, + _onUserStoppedTypePreemptiveAnswer: function() { + if (!this._preemptiveAnswerWatchId || + this._preemptiveAnswer || + this._queryingService) + return; + + this._idleMonitor.remove_watch(this._preemptiveAnswerWatchId); + this._preemptiveAnswerWatchId = 0; + + this._entry.text = ''; + this.updateSensitivity(false); + }, + reset: function() { let oldStatus = this.verificationStatus; this.verificationStatus = AuthPromptStatus.NOT_VERIFYING; this.cancelButton.reactive = true; this.nextButton.label = _("Next"); + if (this._preemptiveAnswerWatchId) { + this._idleMonitor.remove_watch(this._preemptiveAnswerWatchId); + } + this._preemptiveAnswerWatchId = this._idleMonitor.add_idle_watch (500, + Lang.bind(this, + this._onUserStoppedTypePreemptiveAnswer)); + if (this._userVerifier) this._userVerifier.cancel(); this._queryingService = null; this.clear(); this._message.opacity = 0; this.setUser(null); this.stopSpinning(); if (oldStatus == AuthPromptStatus.VERIFICATION_FAILED) this.emit('failed'); let beginRequestType; if (this._mode == AuthPromptMode.UNLOCK_ONLY) { // The user is constant at the unlock screen, so it will immediately // respond to the request with the username beginRequestType = BeginRequestType.PROVIDE_USERNAME; } else if (this._userVerifier.serviceIsForeground(GdmUtil.OVIRT_SERVICE_NAME) || this._userVerifier.serviceIsForeground(GdmUtil.SMARTCARD_SERVICE_NAME)) { // We don't need to know the username if the user preempted the login screen // with a smartcard or with preauthenticated oVirt credentials beginRequestType = BeginRequestType.DONT_PROVIDE_USERNAME; } else { // In all other cases, we should get the username up front. beginRequestType = BeginRequestType.PROVIDE_USERNAME; } this.emit('reset', beginRequestType); }, -- 2.5.0