summaryrefslogtreecommitdiffstats
path: root/js/gdm
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:54:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:54:43 +0000
commite4283f6d48b98e764b988b43bbc86b9d52e6ec94 (patch)
treec8f7f7a6c2f5faa2942d27cefc6fd46cca492656 /js/gdm
parentInitial commit. (diff)
downloadgnome-shell-upstream.tar.xz
gnome-shell-upstream.zip
Adding upstream version 43.9.upstream/43.9upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'js/gdm')
-rw-r--r--js/gdm/authList.js176
-rw-r--r--js/gdm/authPrompt.js707
-rw-r--r--js/gdm/batch.js206
-rw-r--r--js/gdm/credentialManager.js27
-rw-r--r--js/gdm/loginDialog.js1293
-rw-r--r--js/gdm/oVirt.js51
-rw-r--r--js/gdm/realmd.js112
-rw-r--r--js/gdm/util.js785
-rw-r--r--js/gdm/vmware.js54
9 files changed, 3411 insertions, 0 deletions
diff --git a/js/gdm/authList.js b/js/gdm/authList.js
new file mode 100644
index 0000000..fb223a9
--- /dev/null
+++ b/js/gdm/authList.js
@@ -0,0 +1,176 @@
+// -*- 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 <http://www.gnu.org/licenses/>.
+ */
+/* 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({
+ text,
+ style_class: 'login-dialog-auth-list-label',
+ y_align: Clutter.ActorAlign.CENTER,
+ x_expand: false,
+ });
+
+ super._init({
+ style_class: 'login-dialog-auth-list-item',
+ button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE,
+ can_focus: true,
+ child: label,
+ reactive: 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',
+ x_align: Clutter.ActorAlign.START,
+ y_align: Clutter.ActorAlign.CENTER,
+ });
+
+ this.label = new St.Label({ style_class: 'login-dialog-auth-list-title' });
+ this.add_child(this.label);
+
+ this._scrollView = new St.ScrollView({
+ style_class: 'login-dialog-auth-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-auth-list',
+ pseudo_class: 'expanded',
+ });
+
+ this._scrollView.add_actor(this._box);
+ this._items = new Map();
+
+ this.connect('key-focus-in', this._moveFocusToItems.bind(this));
+ }
+
+ _moveFocusToItems() {
+ let hasItems = this.numItems > 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,
+ });
+ }
+
+ addItem(key, text) {
+ this.removeItem(key);
+
+ let item = new AuthListItem(key, text);
+ this._box.add(item);
+
+ this._items.set(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) {
+ if (!this._items.has(key))
+ return;
+
+ let item = this._items.get(key);
+
+ item.destroy();
+
+ this._items.delete(key);
+ }
+
+ get numItems() {
+ return this._items.size;
+ }
+
+ clear() {
+ this.label.text = '';
+ this._box.destroy_all_children();
+ this._items.clear();
+ }
+});
diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js
new file mode 100644
index 0000000..14d09a1
--- /dev/null
+++ b/js/gdm/authPrompt.js
@@ -0,0 +1,707 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported AuthPrompt */
+
+const { Clutter, GLib, GObject, 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;
+const Vmware = imports.gdm.vmware;
+const Params = imports.misc.params;
+const ShellEntry = imports.ui.shellEntry;
+const UserWidget = imports.ui.userWidget;
+const Util = imports.misc.util;
+
+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,
+ VERIFICATION_CANCELLED: 4,
+ VERIFICATION_IN_PROGRESS: 5,
+};
+
+var BeginRequestType = {
+ PROVIDE_USERNAME: 0,
+ DONT_PROVIDE_USERNAME: 1,
+ REUSE_USERNAME: 2,
+};
+
+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,
+ reactive: true,
+ });
+
+ this.verificationStatus = AuthPromptStatus.NOT_VERIFYING;
+
+ this._gdmClient = gdmClient;
+ this._mode = mode;
+ this._defaultButtonWellActor = null;
+ this._cancelledRetries = 0;
+
+ 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('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));
+ this._userVerifier.connect('smartcard-status-changed', this._onSmartcardStatusChanged.bind(this));
+ this._userVerifier.connect('credential-manager-authenticated', this._onCredentialManagerAuthenticated.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._initInputRow();
+
+ 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._inactiveEntry.destroy();
+ this._inactiveEntry = null;
+ 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);
+ }
+
+ _initInputRow() {
+ 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,
+ 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);
+
+ this._authList = new AuthList.AuthList();
+ this._authList.set({
+ 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._mainBox.add_child(this._authList);
+
+ 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._inactiveEntry = this._textEntry;
+
+ this._timedLoginIndicator = new St.Bin({
+ style_class: 'login-dialog-timed-login-indicator',
+ scale_x: 0,
+ });
+
+ this.add_child(this._timedLoginIndicator);
+
+ [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);
+ }
+
+ showTimedLoginIndicator(time) {
+ let hold = new Batch.Hold();
+
+ this.hideTimedLoginIndicator();
+
+ const startTime = GLib.get_monotonic_time();
+
+ this._timedLoginTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 33,
+ () => {
+ const currentTime = GLib.get_monotonic_time();
+ const elapsedTime = (currentTime - startTime) / GLib.USEC_PER_SEC;
+ this._timedLoginIndicator.scale_x = elapsedTime / time;
+ if (elapsedTime >= time) {
+ this._timedLoginTimeoutId = 0;
+ hold.release();
+ return GLib.SOURCE_REMOVE;
+ }
+
+ return GLib.SOURCE_CONTINUE;
+ });
+
+ GLib.Source.set_name_by_id(this._timedLoginTimeoutId, '[gnome-shell] this._timedLoginTimeoutId');
+
+ return hold;
+ }
+
+ hideTimedLoginIndicator() {
+ if (this._timedLoginTimeoutId) {
+ GLib.source_remove(this._timedLoginTimeoutId);
+ this._timedLoginTimeoutId = 0;
+ }
+ this._timedLoginIndicator.scale_x = 0.;
+ }
+
+ _activateNext(shouldSpin) {
+ this.verificationStatus = AuthPromptStatus.VERIFICATION_IN_PROGRESS;
+ 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;
+ this._inactiveEntry = this._textEntry;
+ } else if (!secret && this._entry !== this._textEntry) {
+ this._mainBox.replace_child(this._entry, this._textEntry);
+ this._entry = this._textEntry;
+ this._inactiveEntry = this._passwordEntry;
+ }
+ 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');
+ }
+
+ _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();
+ }
+
+ _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, serviceName, message, type) {
+ let wiggleParameters = {duration: 0};
+
+ if (type === GdmUtil.MessageType.ERROR &&
+ this._userVerifier.serviceIsFingerprint(serviceName)) {
+ // TODO: Use Await for wiggle to be over before unfreezing the user verifier queue
+ wiggleParameters = {
+ duration: 65,
+ wiggleCount: 3,
+ };
+ this._userVerifier.increaseCurrentMessageTimeout(
+ wiggleParameters.duration * (wiggleParameters.wiggleCount + 2));
+ }
+
+ this.setMessage(message, type, wiggleParameters);
+ this.emit('prompted');
+ }
+
+ _onVerificationFailed(userVerifier, serviceName, canRetry) {
+ const wasQueryingService = this._queryingService === serviceName;
+
+ if (wasQueryingService) {
+ this._queryingService = null;
+ this.clear();
+ }
+
+ this.updateSensitivity(canRetry);
+ this.setActorInDefaultButtonWell(null);
+
+ if (!canRetry)
+ this.verificationStatus = AuthPromptStatus.VERIFICATION_FAILED;
+
+ if (wasQueryingService)
+ 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();
+ this._authList.clear();
+ this._authList.hide();
+ }
+
+ setQuestion(question) {
+ 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;
+
+ 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, wiggleParameters = {duration: 0}) {
+ 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');
+
+ this._message.show();
+ if (message) {
+ this._message.remove_all_transitions();
+ this._message.text = message;
+ this._message.opacity = 255;
+ } else {
+ this._message.opacity = 0;
+ }
+
+ Util.wiggle(this._message, wiggleParameters);
+ }
+
+ updateSensitivity(sensitive) {
+ if (this._entry.reactive === sensitive)
+ return;
+
+ this._entry.reactive = sensitive;
+
+ if (sensitive) {
+ this._entry.grab_key_focus();
+ } else {
+ this.grab_key_focus();
+
+ if (this._entry === this._passwordEntry)
+ this._entry.password_visible = false;
+ }
+ }
+
+ 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');
+ else if (oldStatus === AuthPromptStatus.VERIFICATION_CANCELLED)
+ this.emit('cancelled');
+
+ 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
+ if (oldStatus === AuthPromptStatus.VERIFICATION_CANCELLED)
+ return;
+ beginRequestType = BeginRequestType.PROVIDE_USERNAME;
+ } else if (this._userVerifier.serviceIsForeground(OVirt.SERVICE_NAME) ||
+ this._userVerifier.serviceIsForeground(Vmware.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 if (oldStatus === AuthPromptStatus.VERIFICATION_IN_PROGRESS) {
+ // We're going back to retry with current user
+ beginRequestType = BeginRequestType.REUSE_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;
+
+ if (this.verificationStatus === AuthPromptStatus.VERIFICATION_IN_PROGRESS) {
+ this._cancelledRetries++;
+ if (this._cancelledRetries > this._userVerifier.allowedFailures)
+ this.verificationStatus = AuthPromptStatus.VERIFICATION_FAILED;
+ } else {
+ this.verificationStatus = AuthPromptStatus.VERIFICATION_CANCELLED;
+ }
+
+ this.reset();
+ }
+});
diff --git a/js/gdm/batch.js b/js/gdm/batch.js
new file mode 100644
index 0000000..f841f0f
--- /dev/null
+++ b/js/gdm/batch.js
@@ -0,0 +1,206 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/*
+ * Copyright 2011 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 <http://www.gnu.org/licenses/>.
+ */
+
+/*
+ * In order for transformation animations to look good, they need to be
+ * incremental and have some order to them (e.g., fade out hidden items,
+ * then shrink to close the void left over). Chaining animations in this way can
+ * be error-prone and wordy using just ease() callbacks.
+ *
+ * The classes in this file help with this:
+ *
+ * - Task. encapsulates schedulable work to be run in a specific scope.
+ *
+ * - ConsecutiveBatch. runs a series of tasks in order and completes
+ * when the last in the series finishes.
+ *
+ * - ConcurrentBatch. runs a set of tasks at the same time and completes
+ * when the last to finish completes.
+ *
+ * - Hold. prevents a batch from completing the pending task until
+ * the hold is released.
+ *
+ * The tasks associated with a batch are specified in a list at batch
+ * construction time as either task objects or plain functions.
+ * Batches are task objects, themselves, so they can be nested.
+ *
+ * These classes aren't specific to GDM, but were found to be unintuitive and so
+ * are not used elsewhere. These APIs may ultimately get dropped entirely and
+ * replaced by something else.
+ */
+/* exported ConcurrentBatch, ConsecutiveBatch */
+
+const { GObject } = imports.gi;
+const Signals = imports.misc.signals;
+
+var Task = class extends Signals.EventEmitter {
+ constructor(scope, handler) {
+ super();
+
+ if (scope)
+ this.scope = scope;
+ else
+ this.scope = this;
+
+ this.handler = handler;
+ }
+
+ run() {
+ if (this.handler)
+ return this.handler.call(this.scope);
+
+ return null;
+ }
+};
+
+var Hold = class extends Task {
+ constructor() {
+ super(null, () => this);
+
+ this._acquisitions = 1;
+ }
+
+ acquire() {
+ if (this._acquisitions <= 0)
+ throw new Error("Cannot acquire hold after it's been released");
+ this._acquisitions++;
+ }
+
+ acquireUntilAfter(hold) {
+ if (!hold.isAcquired())
+ return;
+
+ this.acquire();
+ let signalId = hold.connect('release', () => {
+ hold.disconnect(signalId);
+ this.release();
+ });
+ }
+
+ release() {
+ this._acquisitions--;
+
+ if (this._acquisitions == 0)
+ this.emit('release');
+ }
+
+ isAcquired() {
+ return this._acquisitions > 0;
+ }
+};
+
+var Batch = class extends Task {
+ constructor(scope, tasks) {
+ super();
+
+ this.tasks = [];
+
+ for (let i = 0; i < tasks.length; i++) {
+ let task;
+
+ if (tasks[i] instanceof Task)
+ task = tasks[i];
+ else if (typeof tasks[i] == 'function')
+ task = new Task(scope, tasks[i]);
+ else
+ throw new Error('Batch tasks must be functions or Task, Hold or Batch objects');
+
+ this.tasks.push(task);
+ }
+ }
+
+ process() {
+ throw new GObject.NotImplementedError(`process in ${this.constructor.name}`);
+ }
+
+ runTask() {
+ if (!(this._currentTaskIndex in this.tasks))
+ return null;
+
+ return this.tasks[this._currentTaskIndex].run();
+ }
+
+ _finish() {
+ this.hold.release();
+ }
+
+ nextTask() {
+ this._currentTaskIndex++;
+
+ // if the entire batch of tasks is finished, release
+ // the hold and notify anyone waiting on the batch
+ if (this._currentTaskIndex >= this.tasks.length) {
+ this._finish();
+ return;
+ }
+
+ this.process();
+ }
+
+ _start() {
+ // acquire a hold to get released when the entire
+ // batch of tasks is finished
+ this.hold = new Hold();
+ this._currentTaskIndex = 0;
+ this.process();
+ }
+
+ run() {
+ this._start();
+
+ // hold may be destroyed at this point
+ // if we're already done running
+ return this.hold;
+ }
+
+ cancel() {
+ this.tasks = this.tasks.splice(0, this._currentTaskIndex + 1);
+ }
+};
+
+var ConcurrentBatch = class extends Batch {
+ process() {
+ let hold = this.runTask();
+
+ if (hold)
+ this.hold.acquireUntilAfter(hold);
+
+ // Regardless of the state of the just run task,
+ // fire off the next one, so all the tasks can run
+ // concurrently.
+ this.nextTask();
+ }
+};
+
+var ConsecutiveBatch = class extends Batch {
+ process() {
+ let hold = this.runTask();
+
+ if (hold && hold.isAcquired()) {
+ // This task is inhibiting the batch. Wait on it
+ // before processing the next one.
+ let signalId = hold.connect('release', () => {
+ hold.disconnect(signalId);
+ this.nextTask();
+ });
+ } else {
+ // This task finished, process the next one
+ this.nextTask();
+ }
+ }
+};
diff --git a/js/gdm/credentialManager.js b/js/gdm/credentialManager.js
new file mode 100644
index 0000000..2ea9f72
--- /dev/null
+++ b/js/gdm/credentialManager.js
@@ -0,0 +1,27 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported CredentialManager */
+
+const Signals = imports.misc.signals;
+
+var CredentialManager = class CredentialManager extends Signals.EventEmitter {
+ constructor(service) {
+ super();
+
+ this._token = null;
+ this._service = service;
+ }
+
+ get token() {
+ return this._token;
+ }
+
+ set token(t) {
+ this._token = t;
+ if (this._token)
+ this.emit('user-authenticated', this._token);
+ }
+
+ get service() {
+ return this._service;
+ }
+};
diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js
new file mode 100644
index 0000000..6071936
--- /dev/null
+++ b/js/gdm/loginDialog.js
@@ -0,0 +1,1293 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported LoginDialog */
+/*
+ * Copyright 2011 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 <http://www.gnu.org/licenses/>.
+ */
+
+const {
+ AccountsService, Atk, Clutter, Gdm, Gio,
+ GLib, GObject, Meta, Pango, Shell, St,
+} = imports.gi;
+
+const AuthPrompt = imports.gdm.authPrompt;
+const Batch = imports.gdm.batch;
+const BoxPointer = imports.ui.boxpointer;
+const CtrlAltTab = imports.ui.ctrlAltTab;
+const GdmUtil = imports.gdm.util;
+const Layout = imports.ui.layout;
+const LoginManager = imports.misc.loginManager;
+const Main = imports.ui.main;
+const PopupMenu = imports.ui.popupMenu;
+const Realmd = imports.gdm.realmd;
+const UserWidget = imports.ui.userWidget;
+
+const _FADE_ANIMATION_TIME = 250;
+const _SCROLL_ANIMATION_TIME = 500;
+const _TIMED_LOGIN_IDLE_THRESHOLD = 5.0;
+
+var UserListItem = GObject.registerClass({
+ Signals: { 'activate': {} },
+}, class UserListItem extends St.Button {
+ _init(user) {
+ let layout = new St.BoxLayout({
+ vertical: true,
+ });
+ super._init({
+ style_class: 'login-dialog-user-list-item',
+ button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE,
+ can_focus: true,
+ x_expand: true,
+ child: layout,
+ reactive: true,
+ });
+
+ this.user = user;
+ this.user.connectObject('changed', this._onUserChanged.bind(this), this);
+
+ this.connect('notify::hover', () => {
+ this._setSelected(this.hover);
+ });
+
+ this._userWidget = new UserWidget.UserWidget(this.user);
+ layout.add(this._userWidget);
+
+ this._userWidget.bind_property('label-actor', this, 'label-actor',
+ GObject.BindingFlags.SYNC_CREATE);
+
+ this._timedLoginIndicator = new St.Bin({
+ style_class: 'login-dialog-timed-login-indicator',
+ scale_x: 0,
+ visible: false,
+ });
+ layout.add(this._timedLoginIndicator);
+
+ this._onUserChanged();
+ }
+
+ vfunc_key_focus_in() {
+ super.vfunc_key_focus_in();
+ this._setSelected(true);
+ }
+
+ vfunc_key_focus_out() {
+ super.vfunc_key_focus_out();
+ this._setSelected(false);
+ }
+
+ _onUserChanged() {
+ this._updateLoggedIn();
+ }
+
+ _updateLoggedIn() {
+ if (this.user.is_logged_in())
+ this.add_style_pseudo_class('logged-in');
+ else
+ this.remove_style_pseudo_class('logged-in');
+ }
+
+ vfunc_clicked() {
+ this.emit('activate');
+ }
+
+ _setSelected(selected) {
+ if (selected) {
+ this.add_style_pseudo_class('selected');
+ this.grab_key_focus();
+ } else {
+ this.remove_style_pseudo_class('selected');
+ }
+ }
+
+ showTimedLoginIndicator(time) {
+ let hold = new Batch.Hold();
+
+ this.hideTimedLoginIndicator();
+
+ this._timedLoginIndicator.visible = true;
+
+ let startTime = GLib.get_monotonic_time();
+
+ this._timedLoginTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 33,
+ () => {
+ let currentTime = GLib.get_monotonic_time();
+ let elapsedTime = (currentTime - startTime) / GLib.USEC_PER_SEC;
+ this._timedLoginIndicator.scale_x = elapsedTime / time;
+ if (elapsedTime >= time) {
+ this._timedLoginTimeoutId = 0;
+ hold.release();
+ return GLib.SOURCE_REMOVE;
+ }
+
+ return GLib.SOURCE_CONTINUE;
+ });
+
+ GLib.Source.set_name_by_id(this._timedLoginTimeoutId, '[gnome-shell] this._timedLoginTimeoutId');
+
+ return hold;
+ }
+
+ hideTimedLoginIndicator() {
+ if (this._timedLoginTimeoutId) {
+ GLib.source_remove(this._timedLoginTimeoutId);
+ this._timedLoginTimeoutId = 0;
+ }
+
+ this._timedLoginIndicator.visible = false;
+ this._timedLoginIndicator.scale_x = 0.;
+ }
+});
+
+var UserList = GObject.registerClass({
+ Signals: {
+ 'activate': { param_types: [UserListItem.$gtype] },
+ 'item-added': { param_types: [UserListItem.$gtype] },
+ },
+}, class UserList extends St.ScrollView {
+ _init() {
+ super._init({
+ style_class: 'login-dialog-user-list-view',
+ x_expand: true,
+ y_expand: true,
+ });
+ this.set_policy(St.PolicyType.NEVER,
+ St.PolicyType.AUTOMATIC);
+
+ this._box = new St.BoxLayout({
+ vertical: true,
+ style_class: 'login-dialog-user-list',
+ pseudo_class: 'expanded',
+ });
+
+ this.add_actor(this._box);
+ this._items = {};
+ }
+
+ vfunc_key_focus_in() {
+ super.vfunc_key_focus_in();
+ this._moveFocusToItems();
+ }
+
+ _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);
+ }
+
+ updateStyle(isExpanded) {
+ if (isExpanded)
+ this._box.add_style_pseudo_class('expanded');
+ else
+ this._box.remove_style_pseudo_class('expanded');
+
+ for (let userName in this._items) {
+ let item = this._items[userName];
+ item.sync_hover();
+ }
+ }
+
+ scrollToItem(item) {
+ let box = item.get_allocation_box();
+
+ let adjustment = this.get_vscroll_bar().get_adjustment();
+
+ let value = (box.y1 + adjustment.step_increment / 2.0) - (adjustment.page_size / 2.0);
+ adjustment.ease(value, {
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ duration: _SCROLL_ANIMATION_TIME,
+ });
+ }
+
+ jumpToItem(item) {
+ let box = item.get_allocation_box();
+
+ let adjustment = this.get_vscroll_bar().get_adjustment();
+
+ let value = (box.y1 + adjustment.step_increment / 2.0) - (adjustment.page_size / 2.0);
+
+ adjustment.set_value(value);
+ }
+
+ getItemFromUserName(userName) {
+ let item = this._items[userName];
+
+ if (!item)
+ return null;
+
+ return item;
+ }
+
+ containsUser(user) {
+ return this._items[user.get_user_name()] != null;
+ }
+
+ addUser(user) {
+ if (!user.is_loaded)
+ return;
+
+ if (user.is_system_account())
+ return;
+
+ if (user.locked)
+ return;
+
+ let userName = user.get_user_name();
+
+ if (!userName)
+ return;
+
+ this.removeUser(user);
+
+ let item = new UserListItem(user);
+ this._box.add_child(item);
+
+ this._items[userName] = 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);
+ }
+
+ removeUser(user) {
+ if (!user.is_loaded)
+ return;
+
+ let userName = user.get_user_name();
+
+ if (!userName)
+ return;
+
+ let item = this._items[userName];
+
+ if (!item)
+ return;
+
+ item.destroy();
+ delete this._items[userName];
+ }
+
+ numItems() {
+ return Object.keys(this._items).length;
+ }
+});
+
+var SessionMenuButton = GObject.registerClass({
+ Signals: { 'session-activated': { param_types: [GObject.TYPE_STRING] } },
+}, class SessionMenuButton extends St.Bin {
+ _init() {
+ let button = new St.Button({
+ style_class: 'modal-dialog-button button login-dialog-session-list-button',
+ icon_name: 'emblem-system-symbolic',
+ reactive: true,
+ track_hover: true,
+ can_focus: true,
+ accessible_name: _("Choose Session"),
+ accessible_role: Atk.Role.MENU,
+ x_align: Clutter.ActorAlign.CENTER,
+ y_align: Clutter.ActorAlign.CENTER,
+ });
+
+ super._init({ child: button });
+ this._button = button;
+
+ this._menu = new PopupMenu.PopupMenu(this._button, 0, St.Side.BOTTOM);
+ Main.uiGroup.add_actor(this._menu.actor);
+ this._menu.actor.hide();
+
+ this._menu.connect('open-state-changed', (menu, isOpen) => {
+ if (isOpen)
+ this._button.add_style_pseudo_class('active');
+ else
+ this._button.remove_style_pseudo_class('active');
+ });
+
+ this._manager = new PopupMenu.PopupMenuManager(this._button,
+ { actionMode: Shell.ActionMode.NONE });
+ this._manager.addMenu(this._menu);
+
+ this._button.connect('clicked', () => this._menu.toggle());
+
+ this._items = {};
+ this._activeSessionId = null;
+ this._populate();
+ }
+
+ updateSensitivity(sensitive) {
+ this._button.reactive = sensitive;
+ this._button.can_focus = sensitive;
+ this.opacity = sensitive ? 255 : 0;
+ this._menu.close(BoxPointer.PopupAnimation.NONE);
+ }
+
+ _updateOrnament() {
+ let itemIds = Object.keys(this._items);
+ for (let i = 0; i < itemIds.length; i++) {
+ if (itemIds[i] == this._activeSessionId)
+ this._items[itemIds[i]].setOrnament(PopupMenu.Ornament.DOT);
+ else
+ this._items[itemIds[i]].setOrnament(PopupMenu.Ornament.NONE);
+ }
+ }
+
+ setActiveSession(sessionId) {
+ if (sessionId == this._activeSessionId)
+ return;
+
+ this._activeSessionId = sessionId;
+ this._updateOrnament();
+ }
+
+ close() {
+ this._menu.close();
+ }
+
+ _populate() {
+ let ids = Gdm.get_session_ids();
+ ids.sort();
+
+ if (ids.length <= 1) {
+ this._button.hide();
+ return;
+ }
+
+ for (let i = 0; i < ids.length; i++) {
+ let [sessionName, sessionDescription_] = Gdm.get_session_name_and_description(ids[i]);
+
+ let id = ids[i];
+ let item = new PopupMenu.PopupMenuItem(sessionName);
+ this._menu.addMenuItem(item);
+ this._items[id] = item;
+
+ item.connect('activate', () => {
+ this.setActiveSession(id);
+ this.emit('session-activated', this._activeSessionId);
+ });
+ }
+ }
+});
+
+var LoginDialog = GObject.registerClass({
+ Signals: {
+ 'failed': {},
+ 'wake-up-screen': {},
+ },
+}, class LoginDialog extends St.Widget {
+ _init(parentActor) {
+ super._init({ style_class: 'login-dialog', visible: false });
+
+ this.get_accessible().set_role(Atk.Role.WINDOW);
+
+ this.add_constraint(new Layout.MonitorConstraint({ primary: true }));
+ this.connect('destroy', this._onDestroy.bind(this));
+ parentActor.add_child(this);
+
+ 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::${GdmUtil.BANNER_MESSAGE_KEY}`,
+ this._updateBanner.bind(this));
+ this._settings.connect(`changed::${GdmUtil.BANNER_MESSAGE_TEXT_KEY}`,
+ this._updateBanner.bind(this));
+ this._settings.connect(`changed::${GdmUtil.DISABLE_USER_LIST_KEY}`,
+ this._updateDisableUserList.bind(this));
+ this._settings.connect(`changed::${GdmUtil.LOGO_KEY}`,
+ this._updateLogo.bind(this));
+
+ this._textureCache = St.TextureCache.get_default();
+ this._textureCache.connectObject('texture-file-changed',
+ this._updateLogoTexture.bind(this), this);
+
+ this._userSelectionBox = new St.BoxLayout({
+ style_class: 'login-dialog-user-selection-box',
+ x_align: Clutter.ActorAlign.CENTER,
+ y_align: Clutter.ActorAlign.CENTER,
+ vertical: true,
+ visible: false,
+ });
+ this.add_child(this._userSelectionBox);
+
+ this._userList = new UserList();
+ this._userSelectionBox.add_child(this._userList);
+
+ this._authPrompt = new AuthPrompt.AuthPrompt(this._gdmClient, AuthPrompt.AuthPromptMode.UNLOCK_OR_LOG_IN);
+ this._authPrompt.connect('prompted', this._onPrompted.bind(this));
+ this._authPrompt.connect('reset', this._onReset.bind(this));
+ this._authPrompt.hide();
+ this.add_child(this._authPrompt);
+
+ // translators: this message is shown below the user list on the
+ // login screen. It can be activated to reveal an entry for
+ // manually entering the username.
+ let notListedLabel = new St.Label({
+ text: _("Not listed?"),
+ style_class: 'login-dialog-not-listed-label',
+ });
+ this._notListedButton = new St.Button({
+ style_class: 'login-dialog-not-listed-button',
+ button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE,
+ can_focus: true,
+ child: notListedLabel,
+ reactive: true,
+ x_align: Clutter.ActorAlign.START,
+ label_actor: notListedLabel,
+ });
+
+ this._notListedButton.connect('clicked', this._hideUserListAskForUsernameAndBeginVerification.bind(this));
+
+ this._notListedButton.hide();
+
+ this._userSelectionBox.add_child(this._notListedButton);
+
+ this._bannerView = new St.ScrollView({
+ style_class: 'login-dialog-banner-view',
+ opacity: 0,
+ vscrollbar_policy: St.PolicyType.AUTOMATIC,
+ hscrollbar_policy: St.PolicyType.NEVER,
+ });
+ this.add_child(this._bannerView);
+
+ let bannerBox = new St.BoxLayout({ vertical: true });
+
+ this._bannerView.add_actor(bannerBox);
+ this._bannerLabel = new St.Label({
+ style_class: 'login-dialog-banner',
+ text: '',
+ });
+ this._bannerLabel.clutter_text.line_wrap = true;
+ this._bannerLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
+ bannerBox.add_child(this._bannerLabel);
+ this._updateBanner();
+
+ this._sessionMenuButton = new SessionMenuButton();
+ this._sessionMenuButton.connect('session-activated',
+ (list, sessionId) => {
+ this._greeter.call_select_session_sync(sessionId, null);
+ });
+ this._sessionMenuButton.opacity = 0;
+ this._sessionMenuButton.show();
+ this.add_child(this._sessionMenuButton);
+
+ this._logoBin = new St.Widget({
+ style_class: 'login-dialog-logo-bin',
+ x_align: Clutter.ActorAlign.CENTER,
+ y_align: Clutter.ActorAlign.END,
+ });
+ this._logoBin.connect('resource-scale-changed', () => {
+ this._updateLogoTexture(this._textureCache, this._logoFile);
+ });
+ this.add_child(this._logoBin);
+ this._updateLogo();
+
+ this._userList.connect('activate', (userList, item) => {
+ this._onUserListActivated(item);
+ });
+
+ this._disableUserList = undefined;
+ this._userListLoaded = false;
+
+ this._realmManager = new Realmd.Manager();
+ this._realmManager.connectObject('login-format-changed',
+ this._showRealmLoginHint.bind(this), this);
+
+ this._getGreeterSessionProxy();
+
+ // If the user list is enabled, it should take key focus; make sure the
+ // screen shield is initialized first to prevent it from stealing the
+ // focus later
+ Main.layoutManager.connectObject('startup-complete',
+ this._updateDisableUserList.bind(this), this);
+ }
+
+ _getBannerAllocation(dialogBox) {
+ let actorBox = new Clutter.ActorBox();
+
+ let [, , natWidth, natHeight] = this._bannerView.get_preferred_size();
+ let centerX = dialogBox.x1 + (dialogBox.x2 - dialogBox.x1) / 2;
+
+ actorBox.x1 = Math.floor(centerX - natWidth / 2);
+ actorBox.y1 = dialogBox.y1 + Main.layoutManager.panelBox.height;
+ actorBox.x2 = actorBox.x1 + natWidth;
+ actorBox.y2 = actorBox.y1 + natHeight;
+
+ return actorBox;
+ }
+
+ _getLogoBinAllocation(dialogBox) {
+ let actorBox = new Clutter.ActorBox();
+
+ let [, , natWidth, natHeight] = this._logoBin.get_preferred_size();
+ let centerX = dialogBox.x1 + (dialogBox.x2 - dialogBox.x1) / 2;
+
+ actorBox.x1 = Math.floor(centerX - natWidth / 2);
+ actorBox.y1 = dialogBox.y2 - natHeight;
+ actorBox.x2 = actorBox.x1 + natWidth;
+ actorBox.y2 = actorBox.y1 + natHeight;
+
+ return actorBox;
+ }
+
+ _getSessionMenuButtonAllocation(dialogBox) {
+ let actorBox = new Clutter.ActorBox();
+
+ let [, , natWidth, natHeight] = this._sessionMenuButton.get_preferred_size();
+
+ if (this.get_text_direction() === Clutter.TextDirection.RTL)
+ actorBox.x1 = dialogBox.x1 + natWidth;
+ else
+ actorBox.x1 = dialogBox.x2 - (natWidth * 2);
+
+ actorBox.y1 = dialogBox.y2 - (natHeight * 2);
+ actorBox.x2 = actorBox.x1 + natWidth;
+ actorBox.y2 = actorBox.y1 + natHeight;
+
+ return actorBox;
+ }
+
+ _getCenterActorAllocation(dialogBox, actor) {
+ let actorBox = new Clutter.ActorBox();
+
+ let [, , natWidth, natHeight] = actor.get_preferred_size();
+ let centerX = dialogBox.x1 + (dialogBox.x2 - dialogBox.x1) / 2;
+ let centerY = dialogBox.y1 + (dialogBox.y2 - dialogBox.y1) / 2;
+
+ natWidth = Math.min(natWidth, dialogBox.x2 - dialogBox.x1);
+ natHeight = Math.min(natHeight, dialogBox.y2 - dialogBox.y1);
+
+ actorBox.x1 = Math.floor(centerX - natWidth / 2);
+ actorBox.y1 = Math.floor(centerY - natHeight / 2);
+ actorBox.x2 = actorBox.x1 + natWidth;
+ actorBox.y2 = actorBox.y1 + natHeight;
+
+ return actorBox;
+ }
+
+ vfunc_allocate(dialogBox) {
+ this.set_allocation(dialogBox);
+
+ let themeNode = this.get_theme_node();
+ dialogBox = themeNode.get_content_box(dialogBox);
+
+ let dialogWidth = dialogBox.x2 - dialogBox.x1;
+ let dialogHeight = dialogBox.y2 - dialogBox.y1;
+
+ // First find out what space the children require
+ let bannerAllocation = null;
+ let bannerHeight = 0;
+ if (this._bannerView.visible) {
+ bannerAllocation = this._getBannerAllocation(dialogBox);
+ bannerHeight = bannerAllocation.y2 - bannerAllocation.y1;
+ }
+
+ let authPromptAllocation = null;
+ let authPromptWidth = 0;
+ if (this._authPrompt.visible) {
+ authPromptAllocation = this._getCenterActorAllocation(dialogBox, this._authPrompt);
+ authPromptWidth = authPromptAllocation.x2 - authPromptAllocation.x1;
+ }
+
+ let userSelectionAllocation = null;
+ let userSelectionHeight = 0;
+ if (this._userSelectionBox.visible) {
+ userSelectionAllocation = this._getCenterActorAllocation(dialogBox, this._userSelectionBox);
+ userSelectionHeight = userSelectionAllocation.y2 - userSelectionAllocation.y1;
+ }
+
+ let logoAllocation = null;
+ let logoHeight = 0;
+ if (this._logoBin.visible) {
+ logoAllocation = this._getLogoBinAllocation(dialogBox);
+ logoHeight = logoAllocation.y2 - logoAllocation.y1;
+ }
+
+ let sessionMenuButtonAllocation = null;
+ if (this._sessionMenuButton.visible)
+ sessionMenuButtonAllocation = this._getSessionMenuButtonAllocation(dialogBox);
+
+ // Then figure out if we're overly constrained and need to
+ // try a different layout, or if we have what extra space we
+ // can hand out
+ if (bannerAllocation) {
+ let bannerSpace;
+
+ if (authPromptAllocation)
+ bannerSpace = authPromptAllocation.y1 - bannerAllocation.y1;
+ else
+ bannerSpace = 0;
+
+ let leftOverYSpace = bannerSpace - bannerHeight;
+
+ if (leftOverYSpace > 0) {
+ // First figure out how much left over space is up top
+ let leftOverTopSpace = leftOverYSpace / 2;
+
+ // Then, shift the banner into the middle of that extra space
+ let yShift = Math.floor(leftOverTopSpace / 2);
+
+ bannerAllocation.y1 += yShift;
+ bannerAllocation.y2 += yShift;
+ } else {
+ // Then figure out how much space there would be if we switched to a
+ // wide layout with banner on one side and authprompt on the other.
+ let leftOverXSpace = dialogWidth - authPromptWidth;
+
+ // In a wide view, half of the available space goes to the banner,
+ // and the other half goes to the margins.
+ let wideBannerWidth = leftOverXSpace / 2;
+ let wideSpacing = leftOverXSpace - wideBannerWidth;
+
+ // If we do go with a wide layout, we need there to be at least enough
+ // space for the banner and the auth prompt to be the same width,
+ // so it doesn't look unbalanced.
+ if (authPromptWidth > 0 && wideBannerWidth > authPromptWidth) {
+ let centerX = dialogBox.x1 + dialogWidth / 2;
+ let centerY = dialogBox.y1 + dialogHeight / 2;
+
+ // A small portion of the spacing goes down the center of the
+ // screen to help delimit the two columns of the wide view
+ let centerGap = wideSpacing / 8;
+
+ // place the banner along the left edge of the center margin
+ bannerAllocation.x2 = Math.floor(centerX - centerGap / 2);
+ bannerAllocation.x1 = Math.floor(bannerAllocation.x2 - wideBannerWidth);
+
+ // figure out how tall it would like to be and try to accommodate
+ // but don't let it get too close to the logo
+ let [, wideBannerHeight] = this._bannerView.get_preferred_height(wideBannerWidth);
+
+ let maxWideHeight = dialogHeight - 3 * logoHeight;
+ wideBannerHeight = Math.min(maxWideHeight, wideBannerHeight);
+ bannerAllocation.y1 = Math.floor(centerY - wideBannerHeight / 2);
+ bannerAllocation.y2 = bannerAllocation.y1 + wideBannerHeight;
+
+ // place the auth prompt along the right edge of the center margin
+ authPromptAllocation.x1 = Math.floor(centerX + centerGap / 2);
+ authPromptAllocation.x2 = authPromptAllocation.x1 + authPromptWidth;
+ } else {
+ // If we aren't going to do a wide view, then we need to limit
+ // the height of the banner so it will present scrollbars
+
+ // First figure out how much space there is without the banner
+ leftOverYSpace += bannerHeight;
+
+ // Then figure out how much of that space is up top
+ let availableTopSpace = Math.floor(leftOverYSpace / 2);
+
+ // Then give all of that space to the banner
+ bannerAllocation.y2 = bannerAllocation.y1 + availableTopSpace;
+ }
+ }
+ } else if (userSelectionAllocation) {
+ // Grow the user list to fill the space
+ let leftOverYSpace = dialogHeight - userSelectionHeight - logoHeight;
+
+ if (leftOverYSpace > 0) {
+ let topExpansion = Math.floor(leftOverYSpace / 2);
+ let bottomExpansion = topExpansion;
+
+ userSelectionAllocation.y1 -= topExpansion;
+ userSelectionAllocation.y2 += bottomExpansion;
+ }
+ }
+
+ // Finally hand out the allocations
+ if (bannerAllocation)
+ this._bannerView.allocate(bannerAllocation);
+
+ if (authPromptAllocation)
+ this._authPrompt.allocate(authPromptAllocation);
+
+ if (userSelectionAllocation)
+ this._userSelectionBox.allocate(userSelectionAllocation);
+
+ if (logoAllocation)
+ this._logoBin.allocate(logoAllocation);
+
+ if (sessionMenuButtonAllocation)
+ this._sessionMenuButton.allocate(sessionMenuButtonAllocation);
+ }
+
+ _ensureUserListLoaded() {
+ if (!this._userManager.is_loaded) {
+ this._userManager.connectObject('notify::is-loaded',
+ () => {
+ if (this._userManager.is_loaded) {
+ this._userManager.disconnectObject(this);
+ this._loadUserList();
+ }
+ });
+ } else {
+ let id = GLib.idle_add(GLib.PRIORITY_DEFAULT, this._loadUserList.bind(this));
+ GLib.Source.set_name_by_id(id, '[gnome-shell] _loadUserList');
+ }
+ }
+
+ _updateDisableUserList() {
+ let disableUserList = this._settings.get_boolean(GdmUtil.DISABLE_USER_LIST_KEY);
+
+ // Disable user list when there are no users.
+ if (this._userListLoaded && this._userList.numItems() == 0)
+ disableUserList = true;
+
+ if (disableUserList != this._disableUserList) {
+ this._disableUserList = disableUserList;
+
+ if (this._authPrompt.verificationStatus == AuthPrompt.AuthPromptStatus.NOT_VERIFYING)
+ this._authPrompt.reset();
+
+ if (this._disableUserList && this._timedLoginUserListHold)
+ this._timedLoginUserListHold.release();
+ }
+ }
+
+ _updateCancelButton() {
+ let cancelVisible;
+
+ // Hide the cancel button if the user list is disabled and we're asking for
+ // a username
+ if (this._authPrompt.verificationStatus == AuthPrompt.AuthPromptStatus.NOT_VERIFYING && this._disableUserList)
+ cancelVisible = false;
+ else
+ cancelVisible = true;
+
+ this._authPrompt.cancelButton.visible = cancelVisible;
+ }
+
+ _updateBanner() {
+ let enabled = this._settings.get_boolean(GdmUtil.BANNER_MESSAGE_KEY);
+ let text = this._settings.get_string(GdmUtil.BANNER_MESSAGE_TEXT_KEY);
+
+ if (enabled && text) {
+ this._bannerLabel.set_text(text);
+ this._bannerLabel.show();
+ } else {
+ this._bannerLabel.hide();
+ }
+ }
+
+ _fadeInBannerView() {
+ this._bannerView.show();
+ this._bannerView.ease({
+ opacity: 255,
+ duration: _FADE_ANIMATION_TIME,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ });
+ }
+
+ _hideBannerView() {
+ this._bannerView.remove_all_transitions();
+ this._bannerView.opacity = 0;
+ this._bannerView.hide();
+ }
+
+ _updateLogoTexture(cache, file) {
+ if (this._logoFile && !this._logoFile.equal(file))
+ return;
+
+ this._logoBin.destroy_all_children();
+ const resourceScale = this._logoBin.get_resource_scale();
+ if (this._logoFile) {
+ let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
+ this._logoBin.add_child(this._textureCache.load_file_async(this._logoFile,
+ -1, -1,
+ scaleFactor,
+ resourceScale));
+ }
+ }
+
+ _updateLogo() {
+ let path = this._settings.get_string(GdmUtil.LOGO_KEY);
+
+ this._logoFile = path ? Gio.file_new_for_path(path) : null;
+ this._updateLogoTexture(this._textureCache, this._logoFile);
+ }
+
+ _onPrompted() {
+ const showSessionMenu = this._shouldShowSessionMenuButton();
+
+ this._sessionMenuButton.updateSensitivity(showSessionMenu);
+ this._sessionMenuButton.visible = showSessionMenu;
+ this._showPrompt();
+ }
+
+ _resetGreeterProxy() {
+ if (GLib.getenv('GDM_GREETER_TEST') != '1') {
+ if (this._greeter)
+ this._greeter.run_dispose();
+
+ this._greeter = this._gdmClient.get_greeter_sync(null);
+
+ this._greeter.connectObject(
+ 'default-session-name-changed', this._onDefaultSessionChanged.bind(this),
+ 'session-opened', this._onSessionOpened.bind(this),
+ 'timed-login-requested', this._onTimedLoginRequested.bind(this), this);
+ }
+ }
+
+ _onReset(authPrompt, beginRequest) {
+ this._resetGreeterProxy();
+ this._sessionMenuButton.updateSensitivity(true);
+
+ const previousUser = this._user;
+ this._user = null;
+
+ if (this._nextSignalId) {
+ this._authPrompt.disconnect(this._nextSignalId);
+ this._nextSignalId = 0;
+ }
+
+ if (previousUser && beginRequest === AuthPrompt.BeginRequestType.REUSE_USERNAME) {
+ this._user = previousUser;
+ this._authPrompt.setUser(this._user);
+ this._authPrompt.begin({ userName: previousUser.get_user_name() });
+ } else if (beginRequest === AuthPrompt.BeginRequestType.PROVIDE_USERNAME) {
+ if (!this._disableUserList)
+ this._showUserList();
+ else
+ this._hideUserListAskForUsernameAndBeginVerification();
+ } else {
+ this._hideUserListAndBeginVerification();
+ }
+ }
+
+ _onDefaultSessionChanged(client, sessionId) {
+ this._sessionMenuButton.setActiveSession(sessionId);
+ }
+
+ _shouldShowSessionMenuButton() {
+ if (this._authPrompt.verificationStatus != AuthPrompt.AuthPromptStatus.VERIFYING &&
+ this._authPrompt.verificationStatus != AuthPrompt.AuthPromptStatus.VERIFICATION_FAILED)
+ return false;
+
+ if (this._user && this._user.is_loaded && this._user.is_logged_in())
+ return false;
+
+ return true;
+ }
+
+ _showPrompt() {
+ if (this._authPrompt.visible)
+ return;
+ this._authPrompt.opacity = 0;
+ this._authPrompt.show();
+ this._authPrompt.ease({
+ opacity: 255,
+ duration: _FADE_ANIMATION_TIME,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ });
+ this._fadeInBannerView();
+ }
+
+ _showRealmLoginHint(realmManager, hint) {
+ if (!hint)
+ return;
+
+ hint = hint.replace(/%U/g, 'user');
+ hint = hint.replace(/%D/g, 'DOMAIN');
+ hint = hint.replace(/%[^UD]/g, '');
+
+ // Translators: this message is shown below the username entry field
+ // to clue the user in on how to login to the local network realm
+ this._authPrompt.setMessage(_("(e.g., user or %s)").format(hint), GdmUtil.MessageType.HINT);
+ }
+
+ _askForUsernameAndBeginVerification() {
+ this._authPrompt.setUser(null);
+ this._authPrompt.setQuestion(_('Username'));
+
+ this._showRealmLoginHint(this._realmManager.loginFormat);
+
+ if (this._nextSignalId)
+ this._authPrompt.disconnect(this._nextSignalId);
+ this._nextSignalId = this._authPrompt.connect('next',
+ () => {
+ this._authPrompt.disconnect(this._nextSignalId);
+ this._nextSignalId = 0;
+ this._authPrompt.updateSensitivity(false);
+ let answer = this._authPrompt.getAnswer();
+ this._user = this._userManager.get_user(answer);
+ this._authPrompt.clear();
+ this._authPrompt.begin({ userName: answer });
+ this._updateCancelButton();
+ });
+ this._updateCancelButton();
+
+ this._sessionMenuButton.updateSensitivity(false);
+ this._authPrompt.updateSensitivity(true);
+ this._showPrompt();
+ }
+
+ _bindOpacity() {
+ this._bindings = Main.layoutManager.uiGroup.get_children()
+ .filter(c => c != Main.layoutManager.screenShieldGroup)
+ .map(c => this.bind_property('opacity', c, 'opacity', 0));
+ }
+
+ _unbindOpacity() {
+ this._bindings.forEach(b => b.unbind());
+ }
+
+ _loginScreenSessionActivated() {
+ if (this.opacity == 255 && this._authPrompt.verificationStatus == AuthPrompt.AuthPromptStatus.NOT_VERIFYING)
+ return;
+
+ if (this._authPrompt.verificationStatus !== AuthPrompt.AuthPromptStatus.NOT_VERIFYING)
+ this._authPrompt.reset();
+
+ this._bindOpacity();
+ this.ease({
+ opacity: 255,
+ duration: _FADE_ANIMATION_TIME,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ onComplete: () => this._unbindOpacity(),
+ });
+ }
+
+ async _getGreeterSessionProxy() {
+ const loginManager = LoginManager.getLoginManager();
+ this._greeterSessionProxy = await loginManager.getCurrentSessionProxy();
+ this._greeterSessionProxy?.connectObject('g-properties-changed', (proxy, properties) => {
+ const activeChanged = !!properties.lookup_value('Active', null);
+ if (activeChanged && proxy.Active)
+ this._loginScreenSessionActivated();
+ }, this);
+ }
+
+ _startSession(serviceName) {
+ this._bindOpacity();
+ this.ease({
+ opacity: 0,
+ duration: _FADE_ANIMATION_TIME,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ onComplete: () => {
+ this._greeter.call_start_session_when_ready_sync(serviceName, true, null);
+ this._unbindOpacity();
+ },
+ });
+ }
+
+ _onSessionOpened(client, serviceName) {
+ this._authPrompt.finish(() => this._startSession(serviceName));
+ }
+
+ _waitForItemForUser(userName) {
+ let item = this._userList.getItemFromUserName(userName);
+
+ if (item)
+ return null;
+
+ let hold = new Batch.Hold();
+ let signalId = this._userList.connect('item-added',
+ () => {
+ item = this._userList.getItemFromUserName(userName);
+
+ if (item)
+ hold.release();
+ });
+
+ hold.connect('release', () => this._userList.disconnect(signalId));
+
+ return hold;
+ }
+
+ _blockTimedLoginUntilIdle() {
+ let hold = new Batch.Hold();
+
+ this._timedLoginIdleTimeOutId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, _TIMED_LOGIN_IDLE_THRESHOLD,
+ () => {
+ this._timedLoginIdleTimeOutId = 0;
+ hold.release();
+ return GLib.SOURCE_REMOVE;
+ });
+ GLib.Source.set_name_by_id(this._timedLoginIdleTimeOutId, '[gnome-shell] this._timedLoginIdleTimeOutId');
+ return hold;
+ }
+
+ _startTimedLogin(userName, delay) {
+ let firstRun = true;
+
+ // Cancel execution of old batch
+ if (this._timedLoginBatch) {
+ this._timedLoginBatch.cancel();
+ this._timedLoginBatch = null;
+ firstRun = false;
+ }
+
+ // Reset previous idle-timeout
+ if (this._timedLoginIdleTimeOutId) {
+ GLib.source_remove(this._timedLoginIdleTimeOutId);
+ this._timedLoginIdleTimeOutId = 0;
+ }
+
+ let loginItem = null;
+ let animationTime;
+
+ let tasks = [
+ () => {
+ if (this._disableUserList)
+ return null;
+
+ this._timedLoginUserListHold = this._waitForItemForUser(userName);
+ return this._timedLoginUserListHold;
+ },
+
+ () => {
+ this._timedLoginUserListHold = null;
+
+ if (this._disableUserList)
+ loginItem = this._authPrompt;
+ else
+ loginItem = this._userList.getItemFromUserName(userName);
+
+ // If there is an animation running on the item, reset it.
+ loginItem.hideTimedLoginIndicator();
+ },
+
+ () => {
+ if (this._disableUserList)
+ return;
+
+ // If we're just starting out, start on the right item.
+ if (!this._userManager.is_loaded)
+ this._userList.jumpToItem(loginItem);
+ },
+
+ () => {
+ // This blocks the timed login animation until a few
+ // seconds after the user stops interacting with the
+ // login screen.
+
+ // We skip this step if the timed login delay is very short.
+ if (delay > _TIMED_LOGIN_IDLE_THRESHOLD) {
+ animationTime = delay - _TIMED_LOGIN_IDLE_THRESHOLD;
+ return this._blockTimedLoginUntilIdle();
+ } else {
+ animationTime = delay;
+ return null;
+ }
+ },
+
+ () => {
+ if (this._disableUserList)
+ return;
+
+ // If idle timeout is done, make sure the timed login indicator is shown
+ if (delay > _TIMED_LOGIN_IDLE_THRESHOLD &&
+ this._authPrompt.visible)
+ this._authPrompt.cancel();
+
+ if (delay > _TIMED_LOGIN_IDLE_THRESHOLD || firstRun) {
+ this._userList.scrollToItem(loginItem);
+ loginItem.grab_key_focus();
+ }
+ },
+
+ () => loginItem.showTimedLoginIndicator(animationTime),
+
+ () => {
+ this._timedLoginBatch = null;
+ this._greeter.call_begin_auto_login_sync(userName, null);
+ },
+ ];
+
+ this._timedLoginBatch = new Batch.ConsecutiveBatch(this, tasks);
+
+ return this._timedLoginBatch.run();
+ }
+
+ _onTimedLoginRequested(client, userName, seconds) {
+ if (this._timedLoginBatch)
+ return;
+
+ this._startTimedLogin(userName, seconds);
+
+ // Restart timed login on user interaction
+ global.stage.connect('captured-event', (actor, event) => {
+ if (event.type() == Clutter.EventType.KEY_PRESS ||
+ event.type() == Clutter.EventType.BUTTON_PRESS)
+ this._startTimedLogin(userName, seconds);
+
+ return Clutter.EVENT_PROPAGATE;
+ });
+ }
+
+ _setUserListExpanded(expanded) {
+ this._userList.updateStyle(expanded);
+ this._userSelectionBox.visible = expanded;
+ }
+
+ _hideUserList() {
+ this._setUserListExpanded(false);
+ if (this._userSelectionBox.visible)
+ GdmUtil.cloneAndFadeOutActor(this._userSelectionBox);
+ }
+
+ _hideUserListAskForUsernameAndBeginVerification() {
+ this._hideUserList();
+ this._askForUsernameAndBeginVerification();
+ }
+
+ _hideUserListAndBeginVerification() {
+ this._hideUserList();
+ this._authPrompt.begin();
+ }
+
+ _showUserList() {
+ this._ensureUserListLoaded();
+ this._authPrompt.hide();
+ this._hideBannerView();
+ this._sessionMenuButton.close();
+ this._sessionMenuButton.hide();
+ this._setUserListExpanded(true);
+ this._notListedButton.show();
+ this._userList.grab_key_focus();
+ }
+
+ _beginVerificationForItem(item) {
+ this._authPrompt.setUser(item.user);
+
+ let userName = item.user.get_user_name();
+ let hold = new Batch.Hold();
+
+ this._authPrompt.begin({ userName, hold });
+ return hold;
+ }
+
+ _onUserListActivated(activatedItem) {
+ this._user = activatedItem.user;
+
+ this._updateCancelButton();
+
+ const batch = new Batch.ConcurrentBatch(this, [
+ GdmUtil.cloneAndFadeOutActor(this._userSelectionBox),
+ this._beginVerificationForItem(activatedItem),
+ ]);
+ batch.run();
+ }
+
+ _onDestroy() {
+ if (this._settings) {
+ this._settings.run_dispose();
+ this._settings = null;
+ }
+ this._greeter = null;
+ this._greeterSessionProxy = null;
+ this._realmManager?.release();
+ this._realmManager = null;
+ }
+
+ _loadUserList() {
+ if (this._userListLoaded)
+ return GLib.SOURCE_REMOVE;
+
+ this._userListLoaded = true;
+
+ let users = this._userManager.list_users();
+
+ for (let i = 0; i < users.length; i++)
+ this._userList.addUser(users[i]);
+
+ this._updateDisableUserList();
+
+ this._userManager.connectObject(
+ 'user-added', (userManager, user) => {
+ this._userList.addUser(user);
+ this._updateDisableUserList();
+ },
+ 'user-removed', (userManager, user) => {
+ this._userList.removeUser(user);
+ this._updateDisableUserList();
+ },
+ 'user-changed', (userManager, user) => {
+ if (this._userList.containsUser(user) && user.locked)
+ this._userList.removeUser(user);
+ else if (!this._userList.containsUser(user) && !user.locked)
+ this._userList.addUser(user);
+ this._updateDisableUserList();
+ }, this);
+
+ return GLib.SOURCE_REMOVE;
+ }
+
+ activate() {
+ this._userList.grab_key_focus();
+ this.show();
+ }
+
+ open() {
+ Main.ctrlAltTabManager.addGroup(this,
+ _("Login Window"),
+ 'dialog-password-symbolic',
+ { sortGroup: CtrlAltTab.SortGroup.MIDDLE });
+ this.activate();
+
+ this.opacity = 0;
+
+ this._grab = Main.pushModal(global.stage, { actionMode: Shell.ActionMode.LOGIN_SCREEN });
+
+ this.ease({
+ opacity: 255,
+ duration: 1000,
+ mode: Clutter.AnimationMode.EASE_IN_QUAD,
+ });
+
+ return true;
+ }
+
+ close() {
+ Main.popModal(this._grab);
+ this._grab = null;
+ Main.ctrlAltTabManager.removeGroup(this);
+ }
+
+ cancel() {
+ this._authPrompt.cancel();
+ }
+
+ addCharacter(_unichar) {
+ // Don't allow type ahead at the login screen
+ }
+
+ finish(onComplete) {
+ this._authPrompt.finish(onComplete);
+ }
+});
diff --git a/js/gdm/oVirt.js b/js/gdm/oVirt.js
new file mode 100644
index 0000000..32e0aa5
--- /dev/null
+++ b/js/gdm/oVirt.js
@@ -0,0 +1,51 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported getOVirtCredentialsManager */
+
+const Gio = imports.gi.Gio;
+const Credential = imports.gdm.credentialManager;
+
+var SERVICE_NAME = 'gdm-ovirtcred';
+
+const OVirtCredentialsIface = `
+<node>
+<interface name="org.ovirt.vdsm.Credentials">
+<signal name="UserAuthenticated">
+ <arg type="s" name="token"/>
+</signal>
+</interface>
+</node>`;
+
+const OVirtCredentialsInfo = Gio.DBusInterfaceInfo.new_for_xml(OVirtCredentialsIface);
+
+let _oVirtCredentialsManager = null;
+
+function OVirtCredentials() {
+ var self = new Gio.DBusProxy({
+ g_connection: Gio.DBus.system,
+ g_interface_name: OVirtCredentialsInfo.name,
+ g_interface_info: OVirtCredentialsInfo,
+ g_name: 'org.ovirt.vdsm.Credentials',
+ g_object_path: '/org/ovirt/vdsm/Credentials',
+ g_flags: Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES,
+ });
+ self.init(null);
+ return self;
+}
+
+var OVirtCredentialsManager = class OVirtCredentialsManager extends Credential.CredentialManager {
+ constructor() {
+ super(SERVICE_NAME);
+ this._credentials = new OVirtCredentials();
+ this._credentials.connectSignal('UserAuthenticated',
+ (proxy, sender, [token]) => {
+ this.token = token;
+ });
+ }
+};
+
+function getOVirtCredentialsManager() {
+ if (!_oVirtCredentialsManager)
+ _oVirtCredentialsManager = new OVirtCredentialsManager();
+
+ return _oVirtCredentialsManager;
+}
diff --git a/js/gdm/realmd.js b/js/gdm/realmd.js
new file mode 100644
index 0000000..52661c1
--- /dev/null
+++ b/js/gdm/realmd.js
@@ -0,0 +1,112 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported Manager */
+
+const Gio = imports.gi.Gio;
+const Signals = imports.misc.signals;
+
+const { loadInterfaceXML } = imports.misc.fileUtils;
+
+const ProviderIface = loadInterfaceXML("org.freedesktop.realmd.Provider");
+const Provider = Gio.DBusProxy.makeProxyWrapper(ProviderIface);
+
+const ServiceIface = loadInterfaceXML("org.freedesktop.realmd.Service");
+const Service = Gio.DBusProxy.makeProxyWrapper(ServiceIface);
+
+const RealmIface = loadInterfaceXML("org.freedesktop.realmd.Realm");
+const Realm = Gio.DBusProxy.makeProxyWrapper(RealmIface);
+
+var Manager = class extends Signals.EventEmitter {
+ constructor() {
+ super();
+
+ this._aggregateProvider = Provider(Gio.DBus.system,
+ 'org.freedesktop.realmd',
+ '/org/freedesktop/realmd',
+ this._reloadRealms.bind(this));
+ this._realms = {};
+ this._loginFormat = null;
+
+ this._aggregateProvider.connectObject('g-properties-changed',
+ (proxy, properties) => {
+ const realmsChanged = !!properties.lookup_value('Realms', null);
+ if (realmsChanged)
+ this._reloadRealms();
+ }, this);
+ }
+
+ _reloadRealms() {
+ let realmPaths = this._aggregateProvider.Realms;
+
+ if (!realmPaths)
+ return;
+
+ for (let i = 0; i < realmPaths.length; i++) {
+ Realm(Gio.DBus.system,
+ 'org.freedesktop.realmd',
+ realmPaths[i],
+ this._onRealmLoaded.bind(this));
+ }
+ }
+
+ _reloadRealm(realm) {
+ if (!realm.Configured) {
+ if (this._realms[realm.get_object_path()])
+ delete this._realms[realm.get_object_path()];
+
+ return;
+ }
+
+ this._realms[realm.get_object_path()] = realm;
+
+ this._updateLoginFormat();
+ }
+
+ _onRealmLoaded(realm, error) {
+ if (error)
+ return;
+
+ this._reloadRealm(realm);
+
+ realm.connect('g-properties-changed', (proxy, properties) => {
+ const configuredChanged = !!properties.lookup_value('Configured', null);
+ if (configuredChanged)
+ this._reloadRealm(realm);
+ });
+ }
+
+ _updateLoginFormat() {
+ let newLoginFormat;
+
+ for (let realmPath in this._realms) {
+ let realm = this._realms[realmPath];
+ if (realm.LoginFormats && realm.LoginFormats.length > 0) {
+ newLoginFormat = realm.LoginFormats[0];
+ break;
+ }
+ }
+
+ if (this._loginFormat != newLoginFormat) {
+ this._loginFormat = newLoginFormat;
+ this.emit('login-format-changed', newLoginFormat);
+ }
+ }
+
+ get loginFormat() {
+ if (this._loginFormat)
+ return this._loginFormat;
+
+ this._updateLoginFormat();
+
+ return this._loginFormat;
+ }
+
+ release() {
+ Service(Gio.DBus.system,
+ 'org.freedesktop.realmd',
+ '/org/freedesktop/realmd',
+ service => service.ReleaseAsync().catch(logError));
+ this._aggregateProvider.disconnectObject(this);
+ this._realms = { };
+ this._updateLoginFormat();
+ }
+};
diff --git a/js/gdm/util.js b/js/gdm/util.js
new file mode 100644
index 0000000..8d09356
--- /dev/null
+++ b/js/gdm/util.js
@@ -0,0 +1,785 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported BANNER_MESSAGE_KEY, BANNER_MESSAGE_TEXT_KEY, LOGO_KEY,
+ DISABLE_USER_LIST_KEY, fadeInActor, fadeOutActor, cloneAndFadeOutActor,
+ ShellUserVerifier */
+
+const { Clutter, Gdm, Gio, GLib } = imports.gi;
+const Signals = imports.misc.signals;
+
+const Batch = imports.gdm.batch;
+const OVirt = imports.gdm.oVirt;
+const Vmware = imports.gdm.vmware;
+const Main = imports.ui.main;
+const { loadInterfaceXML } = imports.misc.fileUtils;
+const Params = imports.misc.params;
+const SmartcardManager = imports.misc.smartcardManager;
+
+const FprintManagerIface = loadInterfaceXML('net.reactivated.Fprint.Manager');
+const FprintManagerProxy = Gio.DBusProxy.makeProxyWrapper(FprintManagerIface);
+const FprintDeviceIface = loadInterfaceXML('net.reactivated.Fprint.Device');
+const FprintDeviceProxy = Gio.DBusProxy.makeProxyWrapper(FprintDeviceIface);
+
+Gio._promisify(Gdm.Client.prototype, 'open_reauthentication_channel');
+Gio._promisify(Gdm.Client.prototype, 'get_user_verifier');
+Gio._promisify(Gdm.UserVerifierProxy.prototype,
+ 'call_begin_verification_for_user');
+Gio._promisify(Gdm.UserVerifierProxy.prototype, 'call_begin_verification');
+
+var PASSWORD_SERVICE_NAME = 'gdm-password';
+var FINGERPRINT_SERVICE_NAME = 'gdm-fingerprint';
+var SMARTCARD_SERVICE_NAME = 'gdm-smartcard';
+var FADE_ANIMATION_TIME = 160;
+var CLONE_FADE_ANIMATION_TIME = 250;
+
+var LOGIN_SCREEN_SCHEMA = 'org.gnome.login-screen';
+var PASSWORD_AUTHENTICATION_KEY = 'enable-password-authentication';
+var FINGERPRINT_AUTHENTICATION_KEY = 'enable-fingerprint-authentication';
+var SMARTCARD_AUTHENTICATION_KEY = 'enable-smartcard-authentication';
+var BANNER_MESSAGE_KEY = 'banner-message-enable';
+var BANNER_MESSAGE_TEXT_KEY = 'banner-message-text';
+var ALLOWED_FAILURES_KEY = 'allowed-failures';
+
+var LOGO_KEY = 'logo';
+var DISABLE_USER_LIST_KEY = 'disable-user-list';
+
+// Give user 48ms to read each character of a PAM message
+var USER_READ_TIME = 48;
+const FINGERPRINT_ERROR_TIMEOUT_WAIT = 15;
+
+var MessageType = {
+ // Keep messages in order by priority
+ NONE: 0,
+ HINT: 1,
+ INFO: 2,
+ ERROR: 3,
+};
+
+const FingerprintReaderType = {
+ NONE: 0,
+ PRESS: 1,
+ SWIPE: 2,
+};
+
+function fadeInActor(actor) {
+ if (actor.opacity == 255 && actor.visible)
+ return null;
+
+ let hold = new Batch.Hold();
+ actor.show();
+ let [, naturalHeight] = actor.get_preferred_height(-1);
+
+ actor.opacity = 0;
+ actor.set_height(0);
+ actor.ease({
+ opacity: 255,
+ height: naturalHeight,
+ duration: FADE_ANIMATION_TIME,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ onComplete: () => {
+ this.set_height(-1);
+ hold.release();
+ },
+ });
+
+ return hold;
+}
+
+function fadeOutActor(actor) {
+ if (!actor.visible || actor.opacity == 0) {
+ actor.opacity = 0;
+ actor.hide();
+ return null;
+ }
+
+ let hold = new Batch.Hold();
+ actor.ease({
+ opacity: 0,
+ height: 0,
+ duration: FADE_ANIMATION_TIME,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ onComplete: () => {
+ this.hide();
+ this.set_height(-1);
+ hold.release();
+ },
+ });
+ return hold;
+}
+
+function cloneAndFadeOutActor(actor) {
+ // Immediately hide actor so its sibling can have its space
+ // and position, but leave a non-reactive clone on-screen,
+ // so from the user's point of view it smoothly fades away
+ // and reveals its sibling.
+ actor.hide();
+
+ const clone = new Clutter.Clone({
+ source: actor,
+ reactive: false,
+ });
+
+ Main.uiGroup.add_child(clone);
+
+ let [x, y] = actor.get_transformed_position();
+ clone.set_position(x, y);
+
+ let hold = new Batch.Hold();
+ clone.ease({
+ opacity: 0,
+ duration: CLONE_FADE_ANIMATION_TIME,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ onComplete: () => {
+ clone.destroy();
+ hold.release();
+ },
+ });
+ return hold;
+}
+
+var ShellUserVerifier = class extends Signals.EventEmitter {
+ constructor(client, params) {
+ super();
+ params = Params.parse(params, { reauthenticationOnly: false });
+ this._reauthOnly = params.reauthenticationOnly;
+
+ this._client = client;
+
+ this._defaultService = null;
+ this._preemptingService = null;
+
+ this._settings = new Gio.Settings({ schema_id: LOGIN_SCREEN_SCHEMA });
+ this._settings.connect('changed',
+ this._updateDefaultService.bind(this));
+ this._updateDefaultService();
+
+ this._fprintManager = new FprintManagerProxy(Gio.DBus.system,
+ 'net.reactivated.Fprint',
+ '/net/reactivated/Fprint/Manager',
+ null,
+ null,
+ Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES);
+ this._smartcardManager = SmartcardManager.getSmartcardManager();
+
+ // We check for smartcards right away, since an inserted smartcard
+ // at startup should result in immediately initiating authentication.
+ // This is different than fingerprint readers, where we only check them
+ // after a user has been picked.
+ this.smartcardDetected = false;
+ this._checkForSmartcard();
+
+ this._smartcardManager.connectObject(
+ 'smartcard-inserted', this._checkForSmartcard.bind(this),
+ 'smartcard-removed', this._checkForSmartcard.bind(this), this);
+
+ this._messageQueue = [];
+ this._messageQueueTimeoutId = 0;
+ this.reauthenticating = false;
+
+ this._failCounter = 0;
+ this._unavailableServices = new Set();
+
+ this._credentialManagers = {};
+ this._credentialManagers[OVirt.SERVICE_NAME] = OVirt.getOVirtCredentialsManager();
+ this._credentialManagers[Vmware.SERVICE_NAME] = Vmware.getVmwareCredentialsManager();
+
+ for (let service in this._credentialManagers) {
+ if (this._credentialManagers[service].token) {
+ this._onCredentialManagerAuthenticated(this._credentialManagers[service],
+ this._credentialManagers[service].token);
+ }
+
+ this._credentialManagers[service].connectObject('user-authenticated',
+ this._onCredentialManagerAuthenticated.bind(this), this);
+ }
+ }
+
+ get hasPendingMessages() {
+ return !!this._messageQueue.length;
+ }
+
+ get allowedFailures() {
+ return this._settings.get_int(ALLOWED_FAILURES_KEY);
+ }
+
+ get currentMessage() {
+ return this._messageQueue ? this._messageQueue[0] : null;
+ }
+
+ begin(userName, hold) {
+ this._cancellable = new Gio.Cancellable();
+ this._hold = hold;
+ this._userName = userName;
+ this.reauthenticating = false;
+
+ this._checkForFingerprintReader();
+
+ // If possible, reauthenticate an already running session,
+ // so any session specific credentials get updated appropriately
+ if (userName)
+ this._openReauthenticationChannel(userName);
+ else
+ this._getUserVerifier();
+ }
+
+ cancel() {
+ if (this._cancellable)
+ this._cancellable.cancel();
+
+ if (this._userVerifier) {
+ this._userVerifier.call_cancel_sync(null);
+ this.clear();
+ }
+ }
+
+ _clearUserVerifier() {
+ if (this._userVerifier) {
+ this._disconnectSignals();
+ this._userVerifier.run_dispose();
+ this._userVerifier = null;
+ if (this._userVerifierChoiceList) {
+ this._userVerifierChoiceList.run_dispose();
+ this._userVerifierChoiceList = null;
+ }
+ }
+ }
+
+ clear() {
+ if (this._cancellable) {
+ this._cancellable.cancel();
+ this._cancellable = null;
+ }
+
+ this._clearUserVerifier();
+ this._clearMessageQueue();
+ }
+
+ destroy() {
+ this.cancel();
+
+ this._settings.run_dispose();
+ this._settings = null;
+
+ this._smartcardManager.disconnectObject(this);
+ this._smartcardManager = null;
+
+ for (let service in this._credentialManagers) {
+ let credentialManager = this._credentialManagers[service];
+ credentialManager.disconnectObject(this);
+ credentialManager = null;
+ }
+ }
+
+ selectChoice(serviceName, key) {
+ this._userVerifierChoiceList.call_select_choice(serviceName, key, this._cancellable, null);
+ }
+
+ async answerQuery(serviceName, answer) {
+ try {
+ await this._handlePendingMessages();
+ this._userVerifier.call_answer_query(serviceName, answer, this._cancellable, null);
+ } catch (e) {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ logError(e);
+ }
+ }
+
+ _getIntervalForMessage(message) {
+ if (!message)
+ return 0;
+
+ // We probably could be smarter here
+ return message.length * USER_READ_TIME;
+ }
+
+ finishMessageQueue() {
+ if (!this.hasPendingMessages)
+ return;
+
+ this._messageQueue = [];
+
+ this.emit('no-more-messages');
+ }
+
+ increaseCurrentMessageTimeout(interval) {
+ if (!this._messageQueueTimeoutId && interval > 0)
+ this._currentMessageExtraInterval = interval;
+ }
+
+ _serviceHasPendingMessages(serviceName) {
+ return this._messageQueue.some(m => m.serviceName === serviceName);
+ }
+
+ _filterServiceMessages(serviceName, messageType) {
+ // This function allows to remove queued messages for the @serviceName
+ // whose type has lower priority than @messageType, replacing them
+ // with a null message that will lead to clearing the prompt once done.
+ if (this._serviceHasPendingMessages(serviceName))
+ this._queuePriorityMessage(serviceName, null, messageType);
+ }
+
+ _queueMessageTimeout() {
+ if (this._messageQueueTimeoutId != 0)
+ return;
+
+ const message = this.currentMessage;
+
+ delete this._currentMessageExtraInterval;
+ this.emit('show-message', message.serviceName, message.text, message.type);
+
+ this._messageQueueTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT,
+ message.interval + (this._currentMessageExtraInterval | 0), () => {
+ this._messageQueueTimeoutId = 0;
+
+ if (this._messageQueue.length > 1) {
+ this._messageQueue.shift();
+ this._queueMessageTimeout();
+ } else {
+ this.finishMessageQueue();
+ }
+
+ return GLib.SOURCE_REMOVE;
+ });
+ GLib.Source.set_name_by_id(this._messageQueueTimeoutId, '[gnome-shell] this._queueMessageTimeout');
+ }
+
+ _queueMessage(serviceName, message, messageType) {
+ let interval = this._getIntervalForMessage(message);
+
+ this._messageQueue.push({ serviceName, text: message, type: messageType, interval });
+ this._queueMessageTimeout();
+ }
+
+ _queuePriorityMessage(serviceName, message, messageType) {
+ const newQueue = this._messageQueue.filter(m => {
+ if (m.serviceName !== serviceName || m.type >= messageType)
+ return m.text !== message;
+ return false;
+ });
+
+ if (!newQueue.includes(this.currentMessage))
+ this._clearMessageQueue();
+
+ this._messageQueue = newQueue;
+ this._queueMessage(serviceName, message, messageType);
+ }
+
+ _clearMessageQueue() {
+ this.finishMessageQueue();
+
+ if (this._messageQueueTimeoutId != 0) {
+ GLib.source_remove(this._messageQueueTimeoutId);
+ this._messageQueueTimeoutId = 0;
+ }
+ this.emit('show-message', null, null, MessageType.NONE);
+ }
+
+ async _checkForFingerprintReader() {
+ this._fingerprintReaderType = FingerprintReaderType.NONE;
+
+ if (!this._settings.get_boolean(FINGERPRINT_AUTHENTICATION_KEY) ||
+ this._fprintManager == null) {
+ this._updateDefaultService();
+ return;
+ }
+
+ try {
+ const [device] = await this._fprintManager.GetDefaultDeviceAsync(
+ Gio.DBusCallFlags.NONE, this._cancellable);
+ const fprintDeviceProxy = new FprintDeviceProxy(Gio.DBus.system,
+ 'net.reactivated.Fprint',
+ device);
+ const fprintDeviceType = fprintDeviceProxy['scan-type'];
+
+ this._fingerprintReaderType = fprintDeviceType === 'swipe'
+ ? FingerprintReaderType.SWIPE
+ : FingerprintReaderType.PRESS;
+ this._updateDefaultService();
+ } catch (e) {}
+ }
+
+ _onCredentialManagerAuthenticated(credentialManager, _token) {
+ this._preemptingService = credentialManager.service;
+ this.emit('credential-manager-authenticated');
+ }
+
+ _checkForSmartcard() {
+ let smartcardDetected;
+
+ if (!this._settings.get_boolean(SMARTCARD_AUTHENTICATION_KEY))
+ smartcardDetected = false;
+ else if (this._reauthOnly)
+ smartcardDetected = this._smartcardManager.hasInsertedLoginToken();
+ else
+ smartcardDetected = this._smartcardManager.hasInsertedTokens();
+
+ if (smartcardDetected != this.smartcardDetected) {
+ this.smartcardDetected = smartcardDetected;
+
+ if (this.smartcardDetected)
+ this._preemptingService = SMARTCARD_SERVICE_NAME;
+ else if (this._preemptingService == SMARTCARD_SERVICE_NAME)
+ this._preemptingService = null;
+
+ this.emit('smartcard-status-changed');
+ }
+ }
+
+ _reportInitError(where, error, serviceName) {
+ logError(error, where);
+ this._hold.release();
+
+ this._queueMessage(serviceName, _('Authentication error'), MessageType.ERROR);
+ this._failCounter++;
+ this._verificationFailed(serviceName, false);
+ }
+
+ async _openReauthenticationChannel(userName) {
+ try {
+ this._clearUserVerifier();
+ this._userVerifier = await this._client.open_reauthentication_channel(
+ userName, this._cancellable);
+ } catch (e) {
+ if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ return;
+ if (e.matches(Gio.DBusError, Gio.DBusError.ACCESS_DENIED) &&
+ !this._reauthOnly) {
+ // Gdm emits org.freedesktop.DBus.Error.AccessDenied when there
+ // is no session to reauthenticate. Fall back to performing
+ // verification from this login session
+ this._getUserVerifier();
+ return;
+ }
+
+ this._reportInitError('Failed to open reauthentication channel', e);
+ 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();
+ this._hold.release();
+ }
+
+ async _getUserVerifier() {
+ try {
+ this._clearUserVerifier();
+ this._userVerifier =
+ await this._client.get_user_verifier(this._cancellable);
+ } catch (e) {
+ if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ return;
+ this._reportInitError('Failed to obtain user verifier', e);
+ 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();
+ }
+
+ _connectSignals() {
+ this._disconnectSignals();
+
+ this._userVerifier.connectObject(
+ 'info', this._onInfo.bind(this),
+ 'problem', this._onProblem.bind(this),
+ 'info-query', this._onInfoQuery.bind(this),
+ 'secret-info-query', this._onSecretInfoQuery.bind(this),
+ 'conversation-stopped', this._onConversationStopped.bind(this),
+ 'service-unavailable', this._onServiceUnavailable.bind(this),
+ 'reset', this._onReset.bind(this),
+ 'verification-complete', this._onVerificationComplete.bind(this),
+ this);
+
+ if (this._userVerifierChoiceList) {
+ this._userVerifierChoiceList.connectObject('choice-query',
+ this._onChoiceListQuery.bind(this), this);
+ }
+ }
+
+ _disconnectSignals() {
+ this._userVerifier?.disconnectObject(this);
+ this._userVerifierChoiceList?.disconnectObject(this);
+ }
+
+ _getForegroundService() {
+ if (this._preemptingService)
+ return this._preemptingService;
+
+ return this._defaultService;
+ }
+
+ serviceIsForeground(serviceName) {
+ return serviceName == this._getForegroundService();
+ }
+
+ serviceIsDefault(serviceName) {
+ return serviceName == this._defaultService;
+ }
+
+ serviceIsFingerprint(serviceName) {
+ return this._fingerprintReaderType !== FingerprintReaderType.NONE &&
+ serviceName === FINGERPRINT_SERVICE_NAME;
+ }
+
+ _updateDefaultService() {
+ if (this._settings.get_boolean(PASSWORD_AUTHENTICATION_KEY))
+ this._defaultService = PASSWORD_SERVICE_NAME;
+ else if (this._settings.get_boolean(SMARTCARD_AUTHENTICATION_KEY))
+ this._defaultService = SMARTCARD_SERVICE_NAME;
+ else if (this._fingerprintReaderType !== FingerprintReaderType.NONE)
+ this._defaultService = FINGERPRINT_SERVICE_NAME;
+
+ if (!this._defaultService) {
+ log("no authentication service is enabled, using password authentication");
+ this._defaultService = PASSWORD_SERVICE_NAME;
+ }
+ }
+
+ async _startService(serviceName) {
+ this._hold.acquire();
+ try {
+ if (this._userName) {
+ await this._userVerifier.call_begin_verification_for_user(
+ serviceName, this._userName, this._cancellable);
+ } else {
+ await this._userVerifier.call_begin_verification(
+ serviceName, this._cancellable);
+ }
+ } catch (e) {
+ if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ return;
+ if (!this.serviceIsForeground(serviceName)) {
+ logError(e,
+ `Failed to start ${serviceName} for ${this._userName}`);
+ this._hold.release();
+ return;
+ }
+ this._reportInitError(
+ this._userName
+ ? `Failed to start ${serviceName} verification for user`
+ : `Failed to start ${serviceName} verification`,
+ e, serviceName);
+ return;
+ }
+ this._hold.release();
+ }
+
+ _beginVerification() {
+ this._startService(this._getForegroundService());
+
+ if (this._userName &&
+ this._fingerprintReaderType !== FingerprintReaderType.NONE &&
+ !this.serviceIsForeground(FINGERPRINT_SERVICE_NAME))
+ this._startService(FINGERPRINT_SERVICE_NAME);
+ }
+
+ _onChoiceListQuery(client, serviceName, promptMessage, list) {
+ if (!this.serviceIsForeground(serviceName))
+ return;
+
+ this.emit('show-choice-list', serviceName, promptMessage, list.deepUnpack());
+ }
+
+ _onInfo(client, serviceName, info) {
+ if (this.serviceIsForeground(serviceName)) {
+ this._queueMessage(serviceName, info, MessageType.INFO);
+ } else if (this.serviceIsFingerprint(serviceName)) {
+ // We don't show fingerprint messages directly since it's
+ // not the main auth service. Instead we use the messages
+ // as a cue to display our own message.
+ if (this._fingerprintReaderType === FingerprintReaderType.SWIPE) {
+ // Translators: this message is shown below the password entry field
+ // to indicate the user can swipe their finger on the fingerprint reader
+ this._queueMessage(serviceName, _('(or swipe finger across reader)'),
+ MessageType.HINT);
+ } else {
+ // Translators: this message is shown below the password entry field
+ // to indicate the user can place their finger on the fingerprint reader instead
+ this._queueMessage(serviceName, _('(or place finger on reader)'),
+ MessageType.HINT);
+ }
+ }
+ }
+
+ _onProblem(client, serviceName, problem) {
+ const isFingerprint = this.serviceIsFingerprint(serviceName);
+
+ if (!this.serviceIsForeground(serviceName) && !isFingerprint)
+ return;
+
+ this._queuePriorityMessage(serviceName, problem, MessageType.ERROR);
+
+ if (isFingerprint) {
+ // pam_fprintd allows the user to retry multiple (maybe even infinite!
+ // times before failing the authentication conversation.
+ // We don't want this behavior to bypass the max-tries setting the user has set,
+ // so we count the problem messages to know how many times the user has failed.
+ // Once we hit the max number of failures we allow, it's time to failure the
+ // conversation from our side. We can't do that right away, however, because
+ // we may drop pending messages coming from pam_fprintd. In order to make sure
+ // the user sees everything, we queue the failure up to get handled in the
+ // near future, after we've finished up the current round of messages.
+ this._failCounter++;
+
+ if (!this._canRetry()) {
+ if (this._fingerprintFailedId)
+ GLib.source_remove(this._fingerprintFailedId);
+
+ const cancellable = this._cancellable;
+ this._fingerprintFailedId = GLib.timeout_add(GLib.PRIORITY_DEFAULT,
+ FINGERPRINT_ERROR_TIMEOUT_WAIT, () => {
+ this._fingerprintFailedId = 0;
+ if (!cancellable.is_cancelled())
+ this._verificationFailed(serviceName, false);
+ return GLib.SOURCE_REMOVE;
+ });
+ }
+ }
+ }
+
+ _onInfoQuery(client, serviceName, question) {
+ if (!this.serviceIsForeground(serviceName))
+ return;
+
+ this.emit('ask-question', serviceName, question, false);
+ }
+
+ _onSecretInfoQuery(client, serviceName, secretQuestion) {
+ if (!this.serviceIsForeground(serviceName))
+ return;
+
+ let token = null;
+ if (this._credentialManagers[serviceName])
+ token = this._credentialManagers[serviceName].token;
+
+ if (token) {
+ this.answerQuery(serviceName, token);
+ return;
+ }
+
+ this.emit('ask-question', serviceName, secretQuestion, true);
+ }
+
+ _onReset() {
+ // Clear previous attempts to authenticate
+ this._failCounter = 0;
+ this._unavailableServices.clear();
+ this._updateDefaultService();
+
+ this.emit('reset');
+ }
+
+ _onVerificationComplete() {
+ this.emit('verification-complete');
+ }
+
+ _cancelAndReset() {
+ this.cancel();
+ this._onReset();
+ }
+
+ _retry(serviceName) {
+ this._hold = new Batch.Hold();
+ this._connectSignals();
+ this._startService(serviceName);
+ }
+
+ _canRetry() {
+ return this._userName &&
+ (this._reauthOnly || this._failCounter < this.allowedFailures);
+ }
+
+ async _verificationFailed(serviceName, shouldRetry) {
+ if (serviceName === FINGERPRINT_SERVICE_NAME) {
+ if (this._fingerprintFailedId)
+ GLib.source_remove(this._fingerprintFailedId);
+ }
+
+ // For Not Listed / enterprise logins, immediately reset
+ // the dialog
+ // Otherwise, when in login mode we allow ALLOWED_FAILURES attempts.
+ // After that, we go back to the welcome screen.
+ this._filterServiceMessages(serviceName, MessageType.ERROR);
+
+ const doneTrying = !shouldRetry || !this._canRetry();
+
+ this.emit('verification-failed', serviceName, !doneTrying);
+ try {
+ if (doneTrying) {
+ this._disconnectSignals();
+ await this._handlePendingMessages();
+ this._cancelAndReset();
+ } else {
+ await this._handlePendingMessages();
+ this._retry(serviceName);
+ }
+ } catch (e) {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ logError(e);
+ }
+ }
+
+ _handlePendingMessages() {
+ if (!this.hasPendingMessage)
+ return Promise.resolve();
+
+ const cancellable = this._cancellable;
+ return new Promise((resolve, reject) => {
+ let signalId = this.connect('no-more-messages', () => {
+ this.disconnect(signalId);
+ if (cancellable.is_cancelled())
+ reject(new GLib.Error(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED, 'Operation was cancelled'));
+ else
+ resolve();
+ });
+ });
+ }
+
+ _onServiceUnavailable(_client, serviceName, errorMessage) {
+ this._unavailableServices.add(serviceName);
+
+ if (!errorMessage)
+ return;
+
+ if (this.serviceIsForeground(serviceName) || this.serviceIsFingerprint(serviceName))
+ this._queueMessage(serviceName, errorMessage, MessageType.ERROR);
+ }
+
+ _onConversationStopped(client, serviceName) {
+ // If the login failed with the preauthenticated oVirt credentials
+ // then discard the credentials and revert to default authentication
+ // mechanism.
+ let foregroundService = Object.keys(this._credentialManagers).find(service =>
+ this.serviceIsForeground(service));
+ if (foregroundService) {
+ this._credentialManagers[foregroundService].token = null;
+ this._preemptingService = null;
+ this._verificationFailed(serviceName, false);
+ return;
+ }
+
+ this._filterServiceMessages(serviceName, MessageType.ERROR);
+
+ if (this._unavailableServices.has(serviceName))
+ return;
+
+ // if the password service fails, then cancel everything.
+ // But if, e.g., fingerprint fails, still give
+ // password authentication a chance to succeed
+ if (this.serviceIsForeground(serviceName))
+ this._failCounter++;
+
+ this._verificationFailed(serviceName, true);
+ }
+};
diff --git a/js/gdm/vmware.js b/js/gdm/vmware.js
new file mode 100644
index 0000000..260b7c8
--- /dev/null
+++ b/js/gdm/vmware.js
@@ -0,0 +1,54 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported getVmwareCredentialsManager */
+
+const Gio = imports.gi.Gio;
+const Credential = imports.gdm.credentialManager;
+
+const dbusPath = '/org/vmware/viewagent/Credentials';
+const dbusInterface = 'org.vmware.viewagent.Credentials';
+
+var SERVICE_NAME = 'gdm-vmwcred';
+
+const VmwareCredentialsIface = `<node>
+<interface name="${dbusInterface}">
+<signal name="UserAuthenticated">
+ <arg type="s" name="token"/>
+</signal>
+</interface>
+</node>`;
+
+
+const VmwareCredentialsInfo = Gio.DBusInterfaceInfo.new_for_xml(VmwareCredentialsIface);
+
+let _vmwareCredentialsManager = null;
+
+function VmwareCredentials() {
+ var self = new Gio.DBusProxy({
+ g_connection: Gio.DBus.session,
+ g_interface_name: VmwareCredentialsInfo.name,
+ g_interface_info: VmwareCredentialsInfo,
+ g_name: dbusInterface,
+ g_object_path: dbusPath,
+ g_flags: Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES,
+ });
+ self.init(null);
+ return self;
+}
+
+var VmwareCredentialsManager = class VmwareCredentialsManager extends Credential.CredentialManager {
+ constructor() {
+ super(SERVICE_NAME);
+ this._credentials = new VmwareCredentials();
+ this._credentials.connectSignal('UserAuthenticated',
+ (proxy, sender, [token]) => {
+ this.token = token;
+ });
+ }
+};
+
+function getVmwareCredentialsManager() {
+ if (!_vmwareCredentialsManager)
+ _vmwareCredentialsManager = new VmwareCredentialsManager();
+
+ return _vmwareCredentialsManager;
+}