// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- /* exported AuthPrompt */ const { Clutter, GObject, Pango, Shell, St } = imports.gi; const Animation = imports.ui.animation; const Batch = imports.gdm.batch; const GdmUtil = imports.gdm.util; const Util = imports.misc.util; const Params = imports.misc.params; const ShellEntry = imports.ui.shellEntry; const UserWidget = imports.ui.userWidget; var DEFAULT_BUTTON_WELL_ICON_SIZE = 16; var DEFAULT_BUTTON_WELL_ANIMATION_DELAY = 1000; var DEFAULT_BUTTON_WELL_ANIMATION_TIME = 300; var MESSAGE_FADE_OUT_ANIMATION_TIME = 500; var AuthPromptMode = { UNLOCK_ONLY: 0, UNLOCK_OR_LOG_IN: 1, }; var AuthPromptStatus = { NOT_VERIFYING: 0, VERIFYING: 1, VERIFICATION_FAILED: 2, VERIFICATION_SUCCEEDED: 3, }; var BeginRequestType = { PROVIDE_USERNAME: 0, DONT_PROVIDE_USERNAME: 1, }; var AuthPrompt = GObject.registerClass({ Signals: { 'cancelled': {}, 'failed': {}, 'next': {}, 'prompted': {}, 'reset': { param_types: [GObject.TYPE_UINT] }, }, }, class AuthPrompt extends St.BoxLayout { _init(gdmClient, mode) { super._init({ style_class: 'login-dialog-prompt-layout', vertical: true, x_expand: true, x_align: Clutter.ActorAlign.CENTER, }); this.verificationStatus = AuthPromptStatus.NOT_VERIFYING; this._gdmClient = gdmClient; this._mode = mode; this._defaultButtonWellActor = null; 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 }); this._userVerifier.connect('ask-question', this._onAskQuestion.bind(this)); this._userVerifier.connect('show-message', this._onShowMessage.bind(this)); this._userVerifier.connect('verification-failed', this._onVerificationFailed.bind(this)); this._userVerifier.connect('verification-complete', this._onVerificationComplete.bind(this)); this._userVerifier.connect('reset', this._onReset.bind(this)); this._userVerifier.connect('smartcard-status-changed', this._onSmartcardStatusChanged.bind(this)); this._userVerifier.connect('ovirt-user-authenticated', this._onOVirtUserAuthenticated.bind(this)); this.smartcardDetected = this._userVerifier.smartcardDetected; this.connect('destroy', this._onDestroy.bind(this)); this._userWell = new St.Bin({ x_expand: true, y_expand: true, }); this.add_child(this._userWell); this._hasCancelButton = this._mode === AuthPromptMode.UNLOCK_OR_LOG_IN; this._initEntryRow(); let capsLockPlaceholder = new St.Label(); this.add_child(capsLockPlaceholder); this._capsLockWarningLabel = new ShellEntry.CapsLockWarning({ x_expand: true, x_align: Clutter.ActorAlign.CENTER, }); this.add_child(this._capsLockWarningLabel); this._capsLockWarningLabel.bind_property('visible', capsLockPlaceholder, 'visible', GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.INVERT_BOOLEAN); this._message = new St.Label({ opacity: 0, styleClass: 'login-dialog-message', y_expand: true, x_expand: true, y_align: Clutter.ActorAlign.START, x_align: Clutter.ActorAlign.CENTER, }); this._message.clutter_text.line_wrap = true; this._message.clutter_text.ellipsize = Pango.EllipsizeMode.NONE; this.add_child(this._message); } _onDestroy() { this._userVerifier.destroy(); this._userVerifier = null; } vfunc_key_press_event(keyPressEvent) { if (keyPressEvent.keyval == Clutter.KEY_Escape) this.cancel(); return super.vfunc_key_press_event(keyPressEvent); } _initEntryRow() { this._mainBox = new St.BoxLayout({ style_class: 'login-dialog-button-box', vertical: false, }); this.add_child(this._mainBox); this.cancelButton = new St.Button({ style_class: 'modal-dialog-button button cancel-button', accessible_name: _('Cancel'), button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE, reactive: this._hasCancelButton, can_focus: this._hasCancelButton, x_align: Clutter.ActorAlign.START, y_align: Clutter.ActorAlign.CENTER, child: new St.Icon({ icon_name: 'go-previous-symbolic' }), }); if (this._hasCancelButton) this.cancelButton.connect('clicked', () => this.cancel()); else this.cancelButton.opacity = 0; this._mainBox.add_child(this.cancelButton); let entryParams = { style_class: 'login-dialog-prompt-entry', can_focus: true, x_expand: true, }; this._entry = null; this._textEntry = new St.Entry(entryParams); ShellEntry.addContextMenu(this._textEntry, { actionMode: Shell.ActionMode.NONE }); this._passwordEntry = new St.PasswordEntry(entryParams); ShellEntry.addContextMenu(this._passwordEntry, { actionMode: Shell.ActionMode.NONE }); this._entry = this._passwordEntry; this._mainBox.add_child(this._entry); this._entry.grab_key_focus(); [this._textEntry, this._passwordEntry].forEach(entry => { entry.clutter_text.connect('text-changed', () => { if (!this._userVerifier.hasPendingMessages) this._fadeOutMessage(); }); entry.clutter_text.connect('activate', () => { let shouldSpin = entry === this._passwordEntry; if (entry.reactive) this._activateNext(shouldSpin); }); }); this._defaultButtonWell = new St.Widget({ layout_manager: new Clutter.BinLayout(), x_align: Clutter.ActorAlign.END, y_align: Clutter.ActorAlign.CENTER, }); this._defaultButtonWell.add_constraint(new Clutter.BindConstraint({ source: this.cancelButton, coordinate: Clutter.BindCoordinate.WIDTH, })); this._mainBox.add_child(this._defaultButtonWell); this._spinner = new Animation.Spinner(DEFAULT_BUTTON_WELL_ICON_SIZE); this._defaultButtonWell.add_child(this._spinner); } _activateNext(shouldSpin) { this.updateSensitivity(false); if (shouldSpin) this.startSpinning(); if (this._queryingService) this._userVerifier.answerQuery(this._queryingService, this._entry.text); else this._preemptiveAnswer = this._entry.text; this.emit('next'); } _updateEntry(secret) { if (secret && this._entry !== this._passwordEntry) { this._mainBox.replace_child(this._entry, this._passwordEntry); this._entry = this._passwordEntry; } else if (!secret && this._entry !== this._textEntry) { this._mainBox.replace_child(this._entry, this._textEntry); this._entry = this._textEntry; } this._capsLockWarningLabel.visible = secret; } _onAskQuestion(verifier, serviceName, question, secret) { if (this._queryingService) this.clear(); this._queryingService = serviceName; if (this._preemptiveAnswer) { this._userVerifier.answerQuery(this._queryingService, this._preemptiveAnswer); this._preemptiveAnswer = null; return; } this._updateEntry(secret); // Hack: The question string comes directly from PAM, if it's "Password:" // we replace it with our own to allow localization, if it's something // else we remove the last colon and any trailing or leading spaces. if (question === 'Password:' || question === 'Password: ') this.setQuestion(_('Password')); else this.setQuestion(question.replace(/: *$/, '').trim()); this.updateSensitivity(true); this.emit('prompted'); } _onOVirtUserAuthenticated() { if (this.verificationStatus != AuthPromptStatus.VERIFICATION_SUCCEEDED) this.reset(); } _onSmartcardStatusChanged() { this.smartcardDetected = this._userVerifier.smartcardDetected; // Most of the time we want to reset if the user inserts or removes // a smartcard. Smartcard insertion "preempts" what the user was // doing, and smartcard removal aborts the preemption. // The exceptions are: 1) Don't reset on smartcard insertion if we're already verifying // with a smartcard // 2) Don't reset if we've already succeeded at verification and // the user is getting logged in. if (this._userVerifier.serviceIsDefault(GdmUtil.SMARTCARD_SERVICE_NAME) && this.verificationStatus == AuthPromptStatus.VERIFYING && this.smartcardDetected) return; if (this.verificationStatus != AuthPromptStatus.VERIFICATION_SUCCEEDED) this.reset(); } _onShowMessage(userVerifier, message, type) { this.setMessage(message, type); this.emit('prompted'); } _onVerificationFailed(userVerifier, canRetry) { this._queryingService = null; this.clear(); this.updateSensitivity(canRetry); this.setActorInDefaultButtonWell(null); this.verificationStatus = AuthPromptStatus.VERIFICATION_FAILED; Util.wiggle(this._entry); } _onVerificationComplete() { this.setActorInDefaultButtonWell(null); this.verificationStatus = AuthPromptStatus.VERIFICATION_SUCCEEDED; this.cancelButton.reactive = false; this.cancelButton.can_focus = false; } _onReset() { this.verificationStatus = AuthPromptStatus.NOT_VERIFYING; this.reset(); } setActorInDefaultButtonWell(actor, animate) { if (!this._defaultButtonWellActor && !actor) return; let oldActor = this._defaultButtonWellActor; if (oldActor) oldActor.remove_all_transitions(); let wasSpinner; if (oldActor == this._spinner) wasSpinner = true; else wasSpinner = false; let isSpinner; if (actor == this._spinner) isSpinner = true; else isSpinner = false; if (this._defaultButtonWellActor != actor && oldActor) { if (!animate) { oldActor.opacity = 0; if (wasSpinner) { if (this._spinner) this._spinner.stop(); } } else { oldActor.ease({ opacity: 0, duration: DEFAULT_BUTTON_WELL_ANIMATION_TIME, delay: DEFAULT_BUTTON_WELL_ANIMATION_DELAY, mode: Clutter.AnimationMode.LINEAR, onComplete: () => { if (wasSpinner) { if (this._spinner) this._spinner.stop(); } }, }); } } if (actor) { if (isSpinner) this._spinner.play(); if (!animate) { actor.opacity = 255; } else { actor.ease({ opacity: 255, duration: DEFAULT_BUTTON_WELL_ANIMATION_TIME, delay: DEFAULT_BUTTON_WELL_ANIMATION_DELAY, mode: Clutter.AnimationMode.LINEAR, }); } } this._defaultButtonWellActor = actor; } startSpinning() { this.setActorInDefaultButtonWell(this._spinner, true); } stopSpinning() { this.setActorInDefaultButtonWell(null, false); } clear() { this._entry.text = ''; this.stopSpinning(); } setQuestion(question) { this._entry.hint_text = question; this._entry.show(); this._entry.grab_key_focus(); } getAnswer() { let text; if (this._preemptiveAnswer) { text = this._preemptiveAnswer; this._preemptiveAnswer = null; } else { text = this._entry.get_text(); } return text; } _fadeOutMessage() { if (this._message.opacity == 0) return; this._message.remove_all_transitions(); this._message.ease({ opacity: 0, duration: MESSAGE_FADE_OUT_ANIMATION_TIME, mode: Clutter.AnimationMode.EASE_OUT_QUAD, }); } setMessage(message, type) { if (type == GdmUtil.MessageType.ERROR) this._message.add_style_class_name('login-dialog-message-warning'); else this._message.remove_style_class_name('login-dialog-message-warning'); if (type == GdmUtil.MessageType.HINT) this._message.add_style_class_name('login-dialog-message-hint'); else this._message.remove_style_class_name('login-dialog-message-hint'); if (message) { this._message.remove_all_transitions(); this._message.text = message; this._message.opacity = 255; } else { this._message.opacity = 0; } } updateSensitivity(sensitive) { this._entry.reactive = sensitive; } vfunc_hide() { this.setActorInDefaultButtonWell(null, true); super.vfunc_hide(); this._message.opacity = 0; this.setUser(null); this.updateSensitivity(true); this._entry.set_text(''); } setUser(user) { let oldChild = this._userWell.get_child(); if (oldChild) oldChild.destroy(); let userWidget = new UserWidget.UserWidget(user, Clutter.Orientation.VERTICAL); this._userWell.set_child(userWidget); if (!user) this._updateEntry(false); } reset() { let oldStatus = this.verificationStatus; this.verificationStatus = AuthPromptStatus.NOT_VERIFYING; this.cancelButton.reactive = this._hasCancelButton; this.cancelButton.can_focus = this._hasCancelButton; this._preemptiveAnswer = null; if (this._userVerifier) this._userVerifier.cancel(); this._queryingService = null; this.clear(); this._message.opacity = 0; this.setUser(null); this._updateEntry(true); 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); } addCharacter(unichar) { if (!this._entry.visible) return; this._entry.grab_key_focus(); this._entry.clutter_text.insert_unichar(unichar); } begin(params) { params = Params.parse(params, { userName: null, hold: null }); this.updateSensitivity(false); let hold = params.hold; if (!hold) hold = new Batch.Hold(); this._userVerifier.begin(params.userName, hold); this.verificationStatus = AuthPromptStatus.VERIFYING; } finish(onComplete) { if (!this._userVerifier.hasPendingMessages) { this._userVerifier.clear(); onComplete(); return; } let signalId = this._userVerifier.connect('no-more-messages', () => { this._userVerifier.disconnect(signalId); this._userVerifier.clear(); onComplete(); }); } cancel() { if (this.verificationStatus == AuthPromptStatus.VERIFICATION_SUCCEEDED) return; this.reset(); this.emit('cancelled'); } });