From 0994cc8fe87cfc9d78221e2e6df074257124a81d Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Tue, 18 Jul 2017 12:58:14 -0400 Subject: [PATCH 1/2] gdm: add AuthList control Ultimately, we want to add support for GDM's new ChoiceList PAM extension. That extension allows PAM modules to present a list of choices to the user. Before we can support that extension, however, we need to have a list control in the login-screen/unlock screen. This commit adds that control. For the most part, it's a copy-and-paste of the gdm userlist, but with less features. It lacks API specific to the users, lacks the built in timed login indicator, etc. It does feature a label heading. --- js/gdm/authList.js | 193 ++++++++++++++++++++++++++++++++++ js/js-resources.gresource.xml | 1 + 2 files changed, 194 insertions(+) create mode 100644 js/gdm/authList.js diff --git a/js/gdm/authList.js b/js/gdm/authList.js new file mode 100644 index 000000000..e4475dc20 --- /dev/null +++ b/js/gdm/authList.js @@ -0,0 +1,193 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- +/* + * Copyright 2017 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, see . + */ +/* exported AuthList */ + +const { Clutter, GObject, Meta, St } = imports.gi; + +const SCROLL_ANIMATION_TIME = 500; + +const AuthListItem = GObject.registerClass({ + Signals: { 'activate': {} }, +}, class AuthListItem extends St.Button { + _init(key, text) { + this.key = key; + const label = new St.Label({ + style_class: 'auth-list-item-label', + y_align: Clutter.ActorAlign.CENTER, + }); + label.text = text; + + super._init({ + style_class: 'login-dialog-user-list-item', + button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE, + can_focus: true, + child: label, + reactive: true, + x_align: St.Align.START, + x_fill: true, + }); + + this.connect('key-focus-in', + () => this._setSelected(true)); + this.connect('key-focus-out', + () => this._setSelected(false)); + this.connect('notify::hover', + () => this._setSelected(this.hover)); + + this.connect('clicked', this._onClicked.bind(this)); + } + + _onClicked() { + this.emit('activate'); + } + + _setSelected(selected) { + if (selected) { + this.add_style_pseudo_class('selected'); + this.grab_key_focus(); + } else { + this.remove_style_pseudo_class('selected'); + } + } +}); + +var AuthList = GObject.registerClass({ + Signals: { + 'activate': { param_types: [GObject.TYPE_STRING] }, + 'item-added': { param_types: [AuthListItem.$gtype] }, + }, +}, class AuthList extends St.BoxLayout { + _init() { + super._init({ + vertical: true, + style_class: 'login-dialog-auth-list-layout', + }); + + this.label = new St.Label({ style_class: 'prompt-dialog-headline' }); + this.add_child(this.label); + + this._scrollView = new St.ScrollView({ + style_class: 'login-dialog-user-list-view', + }); + this._scrollView.set_policy( + St.PolicyType.NEVER, St.PolicyType.AUTOMATIC); + this.add_child(this._scrollView); + + this._box = new St.BoxLayout({ + vertical: true, + style_class: 'login-dialog-user-list', + pseudo_class: 'expanded', + }); + + this._scrollView.add_actor(this._box); + this._items = {}; + + this.connect('key-focus-in', this._moveFocusToItems.bind(this)); + } + + _moveFocusToItems() { + let hasItems = Object.keys(this._items).length > 0; + + if (!hasItems) + return; + + if (global.stage.get_key_focus() !== this) + return; + + let focusSet = this.navigate_focus(null, St.DirectionType.TAB_FORWARD, false); + if (!focusSet) { + Meta.later_add(Meta.LaterType.BEFORE_REDRAW, () => { + this._moveFocusToItems(); + return false; + }); + } + } + + _onItemActivated(activatedItem) { + this.emit('activate', activatedItem.key); + } + + scrollToItem(item) { + let box = item.get_allocation_box(); + + let adjustment = this._scrollView.get_vscroll_bar().get_adjustment(); + + let value = (box.y1 + adjustment.step_increment / 2.0) - (adjustment.page_size / 2.0); + adjustment.ease(value, { + duration: SCROLL_ANIMATION_TIME, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + }); + } + + jumpToItem(item) { + let box = item.get_allocation_box(); + + let adjustment = this._scrollView.get_vscroll_bar().get_adjustment(); + + let value = (box.y1 + adjustment.step_increment / 2.0) - (adjustment.page_size / 2.0); + + adjustment.set_value(value); + } + + getItem(key) { + let item = this._items[key]; + + if (!item) + return null; + + return item; + } + + addItem(key, text) { + this.removeItem(key); + + let item = new AuthListItem(key, text); + this._box.add(item, { x_fill: true }); + + this._items[key] = item; + + item.connect('activate', this._onItemActivated.bind(this)); + + // Try to keep the focused item front-and-center + item.connect('key-focus-in', () => this.scrollToItem(item)); + + this._moveFocusToItems(); + + this.emit('item-added', item); + } + + removeItem(key) { + let item = this._items[key]; + + if (!item) + return; + + item.destroy(); + delete this._items[key]; + } + + numItems() { + return Object.keys(this._items).length; + } + + clear() { + this.label.text = ''; + this._box.destroy_all_children(); + this._items = {}; + } +}); diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml index e65e0e9cf..b2c603a55 100644 --- a/js/js-resources.gresource.xml +++ b/js/js-resources.gresource.xml @@ -1,6 +1,7 @@ + gdm/authList.js gdm/authPrompt.js gdm/batch.js gdm/loginDialog.js -- 2.31.1 From 79c8beffbdabc7d0948bb7335f67ada441e68b4e Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Mon, 17 Jul 2017 16:48:03 -0400 Subject: [PATCH 2/2] gdmUtil: enable support for GDM's ChoiceList PAM extension This commit hooks up support for GDM's ChoiceList PAM extension. --- js/gdm/authPrompt.js | 69 +++++++++++++++++++++++++++++++++++++++++++ js/gdm/loginDialog.js | 5 ++++ js/gdm/util.js | 28 ++++++++++++++++++ js/ui/unlockDialog.js | 7 +++++ 4 files changed, 109 insertions(+) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js index 84c608b2f..b25897ea7 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js @@ -4,6 +4,7 @@ const { Clutter, GLib, GObject, Meta, Pango, Shell, St } = imports.gi; const Animation = imports.ui.animation; +const AuthList = imports.gdm.authList; const Batch = imports.gdm.batch; const GdmUtil = imports.gdm.util; const OVirt = imports.gdm.oVirt; @@ -75,6 +76,7 @@ var AuthPrompt = GObject.registerClass({ this._userVerifier.connect('ask-question', this._onAskQuestion.bind(this)); this._userVerifier.connect('show-message', this._onShowMessage.bind(this)); + this._userVerifier.connect('show-choice-list', this._onShowChoiceList.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)); @@ -107,6 +109,27 @@ var AuthPrompt = GObject.registerClass({ capsLockPlaceholder, 'visible', GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.INVERT_BOOLEAN); + this._authList = new AuthList.AuthList(); + this._authList.set({ + x_expand: true, + x_align: Clutter.ActorAlign.START, + visible: false, + }); + this._authList.connect('activate', (list, key) => { + this._authList.reactive = false; + this._authList.ease({ + opacity: 0, + duration: MESSAGE_FADE_OUT_ANIMATION_TIME, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + onComplete: () => { + this._authList.clear(); + this._authList.hide(); + this._userVerifier.selectChoice(this._queryingService, key); + }, + }); + }); + this.add_child(this._authList); + this._message = new St.Label({ opacity: 0, styleClass: 'login-dialog-message', @@ -303,6 +326,20 @@ var AuthPrompt = GObject.registerClass({ this.emit('prompted'); } + _onShowChoiceList(userVerifier, serviceName, promptMessage, choiceList) { + if (this._queryingService) + this.clear(); + + this._queryingService = serviceName; + + if (this._preemptiveAnswer) + this._preemptiveAnswer = null; + + this.setChoiceList(promptMessage, choiceList); + this.updateSensitivity(true); + this.emit('prompted'); + } + _onCredentialManagerAuthenticated() { if (this.verificationStatus != AuthPromptStatus.VERIFICATION_SUCCEEDED) this.reset(); @@ -438,6 +475,8 @@ var AuthPrompt = GObject.registerClass({ clear() { this._entry.text = ''; this.stopSpinning(); + this._authList.clear(); + this._authList.hide(); } setQuestion(question) { @@ -448,10 +487,39 @@ var AuthPrompt = GObject.registerClass({ this._entry.hint_text = question; + this._authList.hide(); this._entry.show(); this._entry.grab_key_focus(); } + _fadeInChoiceList() { + this._authList.set({ + opacity: 0, + visible: true, + reactive: false, + }); + this._authList.ease({ + opacity: 255, + duration: MESSAGE_FADE_OUT_ANIMATION_TIME, + transition: Clutter.AnimationMode.EASE_OUT_QUAD, + onComplete: () => (this._authList.reactive = true), + }); + } + + setChoiceList(promptMessage, choiceList) { + this._authList.clear(); + this._authList.label.text = promptMessage; + for (let key in choiceList) { + let text = choiceList[key]; + this._authList.addItem(key, text); + } + + this._entry.hide(); + if (this._message.text == '') + this._message.hide(); + this._fadeInChoiceList(); + } + getAnswer() { let text; @@ -487,6 +555,7 @@ var AuthPrompt = GObject.registerClass({ else this._message.remove_style_class_name('login-dialog-message-hint'); + this._message.show(); if (message) { this._message.remove_all_transitions(); this._message.text = message; diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js index d2a82b43d..41dd99646 100644 --- a/js/gdm/loginDialog.js +++ b/js/gdm/loginDialog.js @@ -418,6 +418,11 @@ var LoginDialog = GObject.registerClass({ this._userManager = AccountsService.UserManager.get_default(); this._gdmClient = new Gdm.Client(); + try { + this._gdmClient.set_enabled_extensions([Gdm.UserVerifierChoiceList.interface_info().name]); + } catch (e) { + } + this._settings = new Gio.Settings({ schema_id: GdmUtil.LOGIN_SCREEN_SCHEMA }); this._settings.connect('changed::%s'.format(GdmUtil.BANNER_MESSAGE_KEY), diff --git a/js/gdm/util.js b/js/gdm/util.js index e62114cb1..3f327400f 100644 --- a/js/gdm/util.js +++ b/js/gdm/util.js @@ -238,6 +238,10 @@ var ShellUserVerifier = class { this._disconnectSignals(); this._userVerifier.run_dispose(); this._userVerifier = null; + if (this._userVerifierChoiceList) { + this._userVerifierChoiceList.run_dispose(); + this._userVerifierChoiceList = null; + } } } @@ -268,6 +272,10 @@ var ShellUserVerifier = class { } } + selectChoice(serviceName, key) { + this._userVerifierChoiceList.call_select_choice(serviceName, key, this._cancellable, null); + } + answerQuery(serviceName, answer) { if (!this.hasPendingMessages) { this._userVerifier.call_answer_query(serviceName, answer, this._cancellable, null); @@ -456,6 +464,11 @@ var ShellUserVerifier = class { return; } + if (this._client.get_user_verifier_choice_list) + this._userVerifierChoiceList = this._client.get_user_verifier_choice_list(); + else + this._userVerifierChoiceList = null; + this.reauthenticating = true; this._connectSignals(); this._beginVerification(); @@ -474,6 +487,11 @@ var ShellUserVerifier = class { return; } + if (this._client.get_user_verifier_choice_list) + this._userVerifierChoiceList = this._client.get_user_verifier_choice_list(); + else + this._userVerifierChoiceList = null; + this._connectSignals(); this._beginVerification(); this._hold.release(); @@ -499,6 +517,9 @@ var ShellUserVerifier = class { this._signalIds.push(id); id = this._userVerifier.connect('verification-complete', this._onVerificationComplete.bind(this)); this._signalIds.push(id); + + if (this._userVerifierChoiceList) + this._userVerifierChoiceList.connect('choice-query', this._onChoiceListQuery.bind(this)); } _disconnectSignals() { @@ -581,6 +602,13 @@ var ShellUserVerifier = class { this._startService(FINGERPRINT_SERVICE_NAME); } + _onChoiceListQuery(client, serviceName, promptMessage, list) { + if (!this.serviceIsForeground(serviceName)) + return; + + this.emit('show-choice-list', serviceName, promptMessage, list.deep_unpack()); + } + _onInfo(client, serviceName, info) { if (this.serviceIsForeground(serviceName)) { this._queueMessage(serviceName, info, MessageType.INFO); diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js index 8ddae8b03..33fbbdd8e 100644 --- a/js/ui/unlockDialog.js +++ b/js/ui/unlockDialog.js @@ -484,6 +484,13 @@ var UnlockDialog = GObject.registerClass({ this._gdmClient = new Gdm.Client(); + try { + this._gdmClient.set_enabled_extensions([ + Gdm.UserVerifierChoiceList.interface_info().name, + ]); + } catch (e) { + } + this._adjustment = new St.Adjustment({ actor: this, lower: 0, -- 2.31.1