diff options
Diffstat (limited to 'js/gdm')
-rw-r--r-- | js/gdm/authPrompt.js | 551 | ||||
-rw-r--r-- | js/gdm/batch.js | 208 | ||||
-rw-r--r-- | js/gdm/credentialManager.js | 24 | ||||
-rw-r--r-- | js/gdm/fingerprint.js | 33 | ||||
-rw-r--r-- | js/gdm/loginDialog.js | 1287 | ||||
-rw-r--r-- | js/gdm/oVirt.js | 51 | ||||
-rw-r--r-- | js/gdm/realmd.js | 108 | ||||
-rw-r--r-- | js/gdm/util.js | 625 | ||||
-rw-r--r-- | js/gdm/vmware.js | 54 |
9 files changed, 2941 insertions, 0 deletions
diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js new file mode 100644 index 0000000..43798b3 --- /dev/null +++ b/js/gdm/authPrompt.js @@ -0,0 +1,551 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- +/* exported AuthPrompt */ + +const { Clutter, GObject, Pango, Shell, St } = imports.gi; + +const Animation = imports.ui.animation; +const Batch = imports.gdm.batch; +const GdmUtil = imports.gdm.util; +const 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, +}; + +var BeginRequestType = { + PROVIDE_USERNAME: 0, + DONT_PROVIDE_USERNAME: 1, +}; + +var AuthPrompt = GObject.registerClass({ + Signals: { + 'cancelled': {}, + 'failed': {}, + 'next': {}, + 'prompted': {}, + 'reset': { param_types: [GObject.TYPE_UINT] }, + }, +}, class AuthPrompt extends St.BoxLayout { + _init(gdmClient, mode) { + super._init({ + style_class: 'login-dialog-prompt-layout', + vertical: true, + x_expand: true, + x_align: Clutter.ActorAlign.CENTER, + }); + + this.verificationStatus = AuthPromptStatus.NOT_VERIFYING; + + this._gdmClient = gdmClient; + this._mode = mode; + this._defaultButtonWellActor = null; + + let reauthenticationOnly; + if (this._mode == AuthPromptMode.UNLOCK_ONLY) + reauthenticationOnly = true; + else if (this._mode == AuthPromptMode.UNLOCK_OR_LOG_IN) + reauthenticationOnly = false; + + this._userVerifier = new GdmUtil.ShellUserVerifier(this._gdmClient, { reauthenticationOnly }); + + this._userVerifier.connect('ask-question', this._onAskQuestion.bind(this)); + this._userVerifier.connect('show-message', this._onShowMessage.bind(this)); + this._userVerifier.connect('verification-failed', this._onVerificationFailed.bind(this)); + this._userVerifier.connect('verification-complete', this._onVerificationComplete.bind(this)); + this._userVerifier.connect('reset', this._onReset.bind(this)); + this._userVerifier.connect('smartcard-status-changed', this._onSmartcardStatusChanged.bind(this)); + this._userVerifier.connect('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._initEntryRow(); + + let capsLockPlaceholder = new St.Label(); + this.add_child(capsLockPlaceholder); + + this._capsLockWarningLabel = new ShellEntry.CapsLockWarning({ + x_expand: true, + x_align: Clutter.ActorAlign.CENTER, + }); + this.add_child(this._capsLockWarningLabel); + + this._capsLockWarningLabel.bind_property('visible', + capsLockPlaceholder, 'visible', + GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.INVERT_BOOLEAN); + + this._message = new St.Label({ + opacity: 0, + styleClass: 'login-dialog-message', + y_expand: true, + x_expand: true, + y_align: Clutter.ActorAlign.START, + x_align: Clutter.ActorAlign.CENTER, + }); + this._message.clutter_text.line_wrap = true; + this._message.clutter_text.ellipsize = Pango.EllipsizeMode.NONE; + this.add_child(this._message); + } + + _onDestroy() { + this._userVerifier.destroy(); + this._userVerifier = null; + } + + vfunc_key_press_event(keyPressEvent) { + if (keyPressEvent.keyval == Clutter.KEY_Escape) + this.cancel(); + return super.vfunc_key_press_event(keyPressEvent); + } + + _initEntryRow() { + this._mainBox = new St.BoxLayout({ + style_class: 'login-dialog-button-box', + vertical: false, + }); + this.add_child(this._mainBox); + + this.cancelButton = new St.Button({ + style_class: 'modal-dialog-button button cancel-button', + accessible_name: _('Cancel'), + button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE, + reactive: this._hasCancelButton, + can_focus: this._hasCancelButton, + x_align: Clutter.ActorAlign.START, + y_align: Clutter.ActorAlign.CENTER, + child: new St.Icon({ icon_name: 'go-previous-symbolic' }), + }); + if (this._hasCancelButton) + this.cancelButton.connect('clicked', () => this.cancel()); + else + this.cancelButton.opacity = 0; + this._mainBox.add_child(this.cancelButton); + + let entryParams = { + style_class: 'login-dialog-prompt-entry', + can_focus: true, + x_expand: true, + }; + + this._entry = null; + + this._textEntry = new St.Entry(entryParams); + ShellEntry.addContextMenu(this._textEntry, { actionMode: Shell.ActionMode.NONE }); + + this._passwordEntry = new St.PasswordEntry(entryParams); + ShellEntry.addContextMenu(this._passwordEntry, { actionMode: Shell.ActionMode.NONE }); + + this._entry = this._passwordEntry; + this._mainBox.add_child(this._entry); + this._entry.grab_key_focus(); + + [this._textEntry, this._passwordEntry].forEach(entry => { + entry.clutter_text.connect('text-changed', () => { + if (!this._userVerifier.hasPendingMessages) + this._fadeOutMessage(); + }); + + entry.clutter_text.connect('activate', () => { + let shouldSpin = entry === this._passwordEntry; + if (entry.reactive) + this._activateNext(shouldSpin); + }); + }); + + this._defaultButtonWell = new St.Widget({ + layout_manager: new Clutter.BinLayout(), + x_align: Clutter.ActorAlign.END, + y_align: Clutter.ActorAlign.CENTER, + }); + this._defaultButtonWell.add_constraint(new Clutter.BindConstraint({ + source: this.cancelButton, + coordinate: Clutter.BindCoordinate.WIDTH, + })); + this._mainBox.add_child(this._defaultButtonWell); + + this._spinner = new Animation.Spinner(DEFAULT_BUTTON_WELL_ICON_SIZE); + this._defaultButtonWell.add_child(this._spinner); + } + + _activateNext(shouldSpin) { + this.updateSensitivity(false); + + if (shouldSpin) + this.startSpinning(); + + if (this._queryingService) + this._userVerifier.answerQuery(this._queryingService, this._entry.text); + else + this._preemptiveAnswer = this._entry.text; + + this.emit('next'); + } + + _updateEntry(secret) { + if (secret && this._entry !== this._passwordEntry) { + this._mainBox.replace_child(this._entry, this._passwordEntry); + this._entry = this._passwordEntry; + } else if (!secret && this._entry !== this._textEntry) { + this._mainBox.replace_child(this._entry, this._textEntry); + this._entry = this._textEntry; + } + this._capsLockWarningLabel.visible = secret; + } + + _onAskQuestion(verifier, serviceName, question, secret) { + if (this._queryingService) + this.clear(); + + this._queryingService = serviceName; + if (this._preemptiveAnswer) { + this._userVerifier.answerQuery(this._queryingService, this._preemptiveAnswer); + this._preemptiveAnswer = null; + return; + } + + this._updateEntry(secret); + + // Hack: The question string comes directly from PAM, if it's "Password:" + // we replace it with our own to allow localization, if it's something + // else we remove the last colon and any trailing or leading spaces. + if (question === 'Password:' || question === 'Password: ') + this.setQuestion(_('Password')); + else + this.setQuestion(question.replace(/: *$/, '').trim()); + + this.updateSensitivity(true); + this.emit('prompted'); + } + + _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, message, type) { + this.setMessage(message, type); + this.emit('prompted'); + } + + _onVerificationFailed(userVerifier, canRetry) { + this._queryingService = null; + this.clear(); + + this.updateSensitivity(canRetry); + this.setActorInDefaultButtonWell(null); + this.verificationStatus = AuthPromptStatus.VERIFICATION_FAILED; + + Util.wiggle(this._entry); + } + + _onVerificationComplete() { + this.setActorInDefaultButtonWell(null); + this.verificationStatus = AuthPromptStatus.VERIFICATION_SUCCEEDED; + this.cancelButton.reactive = false; + this.cancelButton.can_focus = false; + } + + _onReset() { + this.verificationStatus = AuthPromptStatus.NOT_VERIFYING; + this.reset(); + } + + setActorInDefaultButtonWell(actor, animate) { + if (!this._defaultButtonWellActor && + !actor) + return; + + let oldActor = this._defaultButtonWellActor; + + if (oldActor) + oldActor.remove_all_transitions(); + + let wasSpinner; + if (oldActor == this._spinner) + wasSpinner = true; + else + wasSpinner = false; + + let isSpinner; + if (actor == this._spinner) + isSpinner = true; + else + isSpinner = false; + + if (this._defaultButtonWellActor != actor && oldActor) { + if (!animate) { + oldActor.opacity = 0; + + if (wasSpinner) { + if (this._spinner) + this._spinner.stop(); + } + } else { + oldActor.ease({ + opacity: 0, + duration: DEFAULT_BUTTON_WELL_ANIMATION_TIME, + delay: DEFAULT_BUTTON_WELL_ANIMATION_DELAY, + mode: Clutter.AnimationMode.LINEAR, + onComplete: () => { + if (wasSpinner) { + if (this._spinner) + this._spinner.stop(); + } + }, + }); + } + } + + if (actor) { + if (isSpinner) + this._spinner.play(); + + if (!animate) { + actor.opacity = 255; + } else { + actor.ease({ + opacity: 255, + duration: DEFAULT_BUTTON_WELL_ANIMATION_TIME, + delay: DEFAULT_BUTTON_WELL_ANIMATION_DELAY, + mode: Clutter.AnimationMode.LINEAR, + }); + } + } + + this._defaultButtonWellActor = actor; + } + + startSpinning() { + this.setActorInDefaultButtonWell(this._spinner, true); + } + + stopSpinning() { + this.setActorInDefaultButtonWell(null, false); + } + + clear() { + this._entry.text = ''; + this.stopSpinning(); + } + + setQuestion(question) { + this._entry.hint_text = question; + + this._entry.show(); + this._entry.grab_key_focus(); + } + + getAnswer() { + let text; + + if (this._preemptiveAnswer) { + text = this._preemptiveAnswer; + this._preemptiveAnswer = null; + } else { + text = this._entry.get_text(); + } + + return text; + } + + _fadeOutMessage() { + if (this._message.opacity == 0) + return; + this._message.remove_all_transitions(); + this._message.ease({ + opacity: 0, + duration: MESSAGE_FADE_OUT_ANIMATION_TIME, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + }); + } + + setMessage(message, type) { + if (type == GdmUtil.MessageType.ERROR) + this._message.add_style_class_name('login-dialog-message-warning'); + else + this._message.remove_style_class_name('login-dialog-message-warning'); + + if (type == GdmUtil.MessageType.HINT) + this._message.add_style_class_name('login-dialog-message-hint'); + else + this._message.remove_style_class_name('login-dialog-message-hint'); + + if (message) { + this._message.remove_all_transitions(); + this._message.text = message; + this._message.opacity = 255; + } else { + this._message.opacity = 0; + } + } + + updateSensitivity(sensitive) { + if (this._entry.reactive === sensitive) + return; + + this._entry.reactive = sensitive; + + if (sensitive) + this._entry.grab_key_focus(); + else 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 { + // In all other cases, we should get the username up front. + beginRequestType = BeginRequestType.PROVIDE_USERNAME; + } + + this.emit('reset', beginRequestType); + } + + addCharacter(unichar) { + if (!this._entry.visible) + return; + + this._entry.grab_key_focus(); + this._entry.clutter_text.insert_unichar(unichar); + } + + begin(params) { + params = Params.parse(params, { userName: null, + hold: null }); + + this.updateSensitivity(false); + + let hold = params.hold; + if (!hold) + hold = new Batch.Hold(); + + this._userVerifier.begin(params.userName, hold); + this.verificationStatus = AuthPromptStatus.VERIFYING; + } + + finish(onComplete) { + if (!this._userVerifier.hasPendingMessages) { + this._userVerifier.clear(); + onComplete(); + return; + } + + let signalId = this._userVerifier.connect('no-more-messages', () => { + this._userVerifier.disconnect(signalId); + this._userVerifier.clear(); + onComplete(); + }); + } + + cancel() { + if (this.verificationStatus == AuthPromptStatus.VERIFICATION_SUCCEEDED) + return; + + this.verificationStatus = AuthPromptStatus.VERIFICATION_CANCELLED; + this.reset(); + } +}); diff --git a/js/gdm/batch.js b/js/gdm/batch.js new file mode 100644 index 0000000..ca29fc7 --- /dev/null +++ b/js/gdm/batch.js @@ -0,0 +1,208 @@ +// -*- 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. + */ + +const { GObject } = imports.gi; +const Signals = imports.signals; + +var Task = class { + constructor(scope, handler) { + if (scope) + this.scope = scope; + else + this.scope = this; + + this.handler = handler; + } + + run() { + if (this.handler) + return this.handler.call(this.scope); + + return null; + } +}; +Signals.addSignalMethods(Task.prototype); + +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; + } +}; +Signals.addSignalMethods(Hold.prototype); + +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); + } +}; +Signals.addSignalMethods(Batch.prototype); + +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(); + } +}; +Signals.addSignalMethods(ConcurrentBatch.prototype); + +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(); + } + } +}; +Signals.addSignalMethods(ConsecutiveBatch.prototype); diff --git a/js/gdm/credentialManager.js b/js/gdm/credentialManager.js new file mode 100644 index 0000000..5c4bc7e --- /dev/null +++ b/js/gdm/credentialManager.js @@ -0,0 +1,24 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- +/* exported CredentialManager */ + +var CredentialManager = class CredentialManager { + constructor(service) { + this._token = null; + this._service = service; + this._authenticatedSignalId = null; + } + + 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/fingerprint.js b/js/gdm/fingerprint.js new file mode 100644 index 0000000..e23a77b --- /dev/null +++ b/js/gdm/fingerprint.js @@ -0,0 +1,33 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- +/* exported FprintManager */ + +const Gio = imports.gi.Gio; + +const FprintManagerIface = ` +<node> +<interface name="net.reactivated.Fprint.Manager"> +<method name="GetDefaultDevice"> + <arg type="o" direction="out" /> +</method> +</interface> +</node>`; + +const FprintManagerInfo = Gio.DBusInterfaceInfo.new_for_xml(FprintManagerIface); + +function FprintManager() { + var self = new Gio.DBusProxy({ g_connection: Gio.DBus.system, + g_interface_name: FprintManagerInfo.name, + g_interface_info: FprintManagerInfo, + g_name: 'net.reactivated.Fprint', + g_object_path: '/net/reactivated/Fprint/Manager', + g_flags: Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES }); + + try { + self.init(null); + } catch (e) { + log(`Failed to connect to Fprint service: ${e.message}`); + return null; + } + + return self; +} diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js new file mode 100644 index 0000000..ad84c3e --- /dev/null +++ b/js/gdm/loginDialog.js @@ -0,0 +1,1287 @@ +// -*- 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._userChangedId = this.user.connect('changed', + this._onUserChanged.bind(this)); + + this.connect('destroy', this._onDestroy.bind(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'); + } + + _onDestroy() { + this.user.disconnect(this._userChangedId); + } + + 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 gearIcon = new St.Icon({ icon_name: 'emblem-system-symbolic' }); + let button = new St.Button({ + style_class: 'modal-dialog-button button login-dialog-session-list-button', + 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, + child: gearIcon, + }); + + 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(); + + this._settings = new Gio.Settings({ schema_id: GdmUtil.LOGIN_SCREEN_SCHEMA }); + + this._settings.connect('changed::%s'.format(GdmUtil.BANNER_MESSAGE_KEY), + this._updateBanner.bind(this)); + this._settings.connect('changed::%s'.format(GdmUtil.BANNER_MESSAGE_TEXT_KEY), + this._updateBanner.bind(this)); + this._settings.connect('changed::%s'.format(GdmUtil.DISABLE_USER_LIST_KEY), + this._updateDisableUserList.bind(this)); + this._settings.connect('changed::%s'.format(GdmUtil.LOGO_KEY), + this._updateLogo.bind(this)); + + this._textureCache = St.TextureCache.get_default(); + this._updateLogoTextureId = this._textureCache.connect('texture-file-changed', + this._updateLogoTexture.bind(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._realmSignalId = this._realmManager.connect('login-format-changed', + this._showRealmLoginHint.bind(this)); + + LoginManager.getLoginManager().getCurrentSessionProxy(this._gotGreeterSessionProxy.bind(this)); + + // 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 + this._startupCompleteId = Main.layoutManager.connect('startup-complete', + this._updateDisableUserList.bind(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, this._bannerView); + 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._userManagerLoadedId = this._userManager.connect('notify::is-loaded', + () => { + if (this._userManager.is_loaded) { + this._loadUserList(); + this._userManager.disconnect(this._userManagerLoadedId); + this._userManagerLoadedId = 0; + } + }); + } 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(); + } + } + + _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._defaultSessionChangedId = this._greeter.connect('default-session-name-changed', + this._onDefaultSessionChanged.bind(this)); + this._sessionOpenedId = this._greeter.connect('session-opened', + this._onSessionOpened.bind(this)); + this._timedLoginRequestedId = this._greeter.connect('timed-login-requested', + this._onTimedLoginRequested.bind(this)); + } + } + + _onReset(authPrompt, beginRequest) { + this._resetGreeterProxy(); + this._sessionMenuButton.updateSensitivity(true); + + this._user = null; + + if (this._nextSignalId) { + this._authPrompt.disconnect(this._nextSignalId); + this._nextSignalId = 0; + } + + 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(), + }); + } + + _gotGreeterSessionProxy(proxy) { + this._greeterSessionProxy = proxy; + this._greeterSessionProxyChangedId = + proxy.connect('g-properties-changed', () => { + if (proxy.Active) + this._loginScreenSessionActivated(); + }); + } + + _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 = [() => this._waitForItemForUser(userName), + + () => { + loginItem = this._userList.getItemFromUserName(userName); + + // If there is an animation running on the item, reset it. + loginItem.hideTimedLoginIndicator(); + }, + + () => { + // 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 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(); + + let batch = new Batch.ConcurrentBatch(this, [GdmUtil.cloneAndFadeOutActor(this._userSelectionBox), + this._beginVerificationForItem(activatedItem)]); + batch.run(); + } + + _onDestroy() { + if (this._userManagerLoadedId) { + this._userManager.disconnect(this._userManagerLoadedId); + this._userManagerLoadedId = 0; + } + if (this._userAddedId) { + this._userManager.disconnect(this._userAddedId); + this._userAddedId = 0; + } + if (this._userRemovedId) { + this._userManager.disconnect(this._userRemovedId); + this._userRemovedId = 0; + } + if (this._userChangedId) { + this._userManager.disconnect(this._userChangedId); + this._userChangedId = 0; + } + this._textureCache.disconnect(this._updateLogoTextureId); + Main.layoutManager.disconnect(this._startupCompleteId); + if (this._settings) { + this._settings.run_dispose(); + this._settings = null; + } + if (this._greeter) { + this._greeter.disconnect(this._defaultSessionChangedId); + this._greeter.disconnect(this._sessionOpenedId); + this._greeter.disconnect(this._timedLoginRequestedId); + this._greeter = null; + } + if (this._greeterSessionProxy) { + this._greeterSessionProxy.disconnect(this._greeterSessionProxyChangedId); + this._greeterSessionProxy = null; + } + if (this._realmManager) { + this._realmManager.disconnect(this._realmSignalId); + this._realmSignalId = 0; + 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._userAddedId = this._userManager.connect('user-added', + (userManager, user) => { + this._userList.addUser(user); + this._updateDisableUserList(); + }); + + this._userRemovedId = this._userManager.connect('user-removed', + (userManager, user) => { + this._userList.removeUser(user); + this._updateDisableUserList(); + }); + + this._userChangedId = this._userManager.connect('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(); + }); + + 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; + + Main.pushModal(this, { actionMode: Shell.ActionMode.LOGIN_SCREEN }); + + this.ease({ + opacity: 255, + duration: 1000, + mode: Clutter.AnimationMode.EASE_IN_QUAD, + }); + + return true; + } + + close() { + Main.popModal(this); + 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..94a22b8 --- /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 Signals = imports.signals; +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; + }); + } +}; +Signals.addSignalMethods(OVirtCredentialsManager.prototype); + +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..7584968 --- /dev/null +++ b/js/gdm/realmd.js @@ -0,0 +1,108 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +const Gio = imports.gi.Gio; +const Signals = imports.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 { + constructor() { + this._aggregateProvider = Provider(Gio.DBus.system, + 'org.freedesktop.realmd', + '/org/freedesktop/realmd', + this._reloadRealms.bind(this)); + this._realms = {}; + this._loginFormat = null; + + this._signalId = this._aggregateProvider.connect('g-properties-changed', + (proxy, properties) => { + if ('Realms' in properties.deep_unpack()) + this._reloadRealms(); + }); + } + + _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) => { + if ('Configured' in properties.deep_unpack()) + 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.ReleaseRemote()); + this._aggregateProvider.disconnect(this._signalId); + this._realms = { }; + this._updateLoginFormat(); + } +}; +Signals.addSignalMethods(Manager.prototype); diff --git a/js/gdm/util.js b/js/gdm/util.js new file mode 100644 index 0000000..c31ae92 --- /dev/null +++ b/js/gdm/util.js @@ -0,0 +1,625 @@ +// -*- 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 */ + +const { Clutter, Gdm, Gio, GLib } = imports.gi; +const Signals = imports.signals; + +const Batch = imports.gdm.batch; +const Fprint = imports.gdm.fingerprint; +const OVirt = imports.gdm.oVirt; +const Vmware = imports.gdm.vmware; +const Main = imports.ui.main; +const Params = imports.misc.params; +const SmartcardManager = imports.misc.smartcardManager; + +Gio._promisify(Gdm.Client.prototype, + 'open_reauthentication_channel', 'open_reauthentication_channel_finish'); +Gio._promisify(Gdm.Client.prototype, + 'get_user_verifier', 'get_user_verifier_finish'); +Gio._promisify(Gdm.UserVerifierProxy.prototype, + 'call_begin_verification_for_user', 'call_begin_verification_for_user_finish'); +Gio._promisify(Gdm.UserVerifierProxy.prototype, + 'call_begin_verification', 'call_begin_verification_finish'); + +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; + +var MessageType = { + NONE: 0, + ERROR: 1, + INFO: 2, + HINT: 3, +}; + +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(); + + let 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 { + constructor(client, params) { + 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 = Fprint.FprintManager(); + 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._smartcardInsertedId = this._smartcardManager.connect('smartcard-inserted', + this._checkForSmartcard.bind(this)); + this._smartcardRemovedId = this._smartcardManager.connect('smartcard-removed', + this._checkForSmartcard.bind(this)); + + this._messageQueue = []; + this._messageQueueTimeoutId = 0; + this.hasPendingMessages = false; + 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]._authenticatedSignalId = + this._credentialManagers[service].connect('user-authenticated', + this._onCredentialManagerAuthenticated.bind(this)); + } + } + + 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._userVerifier.run_dispose(); + this._userVerifier = 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.disconnect(this._smartcardInsertedId); + this._smartcardManager.disconnect(this._smartcardRemovedId); + this._smartcardManager = null; + + for (let service in this._credentialManagers) { + let credentialManager = this._credentialManagers[service]; + credentialManager.disconnect(credentialManager._authenticatedSignalId); + credentialManager = null; + } + } + + answerQuery(serviceName, answer) { + if (!this.hasPendingMessages) { + this._userVerifier.call_answer_query(serviceName, answer, this._cancellable, null); + } else { + const cancellable = this._cancellable; + let signalId = this.connect('no-more-messages', () => { + this.disconnect(signalId); + if (!cancellable.is_cancelled()) + this._userVerifier.call_answer_query(serviceName, answer, cancellable, null); + }); + } + } + + _getIntervalForMessage(message) { + // We probably could be smarter here + return message.length * USER_READ_TIME; + } + + finishMessageQueue() { + if (!this.hasPendingMessages) + return; + + this._messageQueue = []; + + this.hasPendingMessages = false; + this.emit('no-more-messages'); + } + + _queueMessageTimeout() { + if (this._messageQueue.length == 0) { + this.finishMessageQueue(); + return; + } + + if (this._messageQueueTimeoutId != 0) + return; + + let message = this._messageQueue.shift(); + + this.emit('show-message', message.text, message.type); + + this._messageQueueTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, + message.interval, + () => { + this._messageQueueTimeoutId = 0; + this._queueMessageTimeout(); + return GLib.SOURCE_REMOVE; + }); + GLib.Source.set_name_by_id(this._messageQueueTimeoutId, '[gnome-shell] this._queueMessageTimeout'); + } + + _queueMessage(message, messageType) { + let interval = this._getIntervalForMessage(message); + + this.hasPendingMessages = true; + this._messageQueue.push({ text: message, type: messageType, interval }); + this._queueMessageTimeout(); + } + + _clearMessageQueue() { + this.finishMessageQueue(); + + if (this._messageQueueTimeoutId != 0) { + GLib.source_remove(this._messageQueueTimeoutId); + this._messageQueueTimeoutId = 0; + } + this.emit('show-message', null, MessageType.NONE); + } + + _checkForFingerprintReader() { + this._haveFingerprintReader = false; + + if (!this._settings.get_boolean(FINGERPRINT_AUTHENTICATION_KEY) || + this._fprintManager == null) { + this._updateDefaultService(); + return; + } + + this._fprintManager.GetDefaultDeviceRemote(Gio.DBusCallFlags.NONE, this._cancellable, + (device, error) => { + if (!error && device) { + this._haveFingerprintReader = true; + this._updateDefaultService(); + } + }); + } + + _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) { + logError(error, where); + this._hold.release(); + + this._queueMessage(_("Authentication error"), MessageType.ERROR); + this._verificationFailed(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; + } + + 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; + } + + this._connectSignals(); + this._beginVerification(); + this._hold.release(); + } + + _connectSignals() { + this._userVerifier.connect('info', this._onInfo.bind(this)); + this._userVerifier.connect('problem', this._onProblem.bind(this)); + this._userVerifier.connect('info-query', this._onInfoQuery.bind(this)); + this._userVerifier.connect('secret-info-query', this._onSecretInfoQuery.bind(this)); + this._userVerifier.connect('conversation-stopped', this._onConversationStopped.bind(this)); + this._userVerifier.connect('service-unavailable', this._onServiceUnavailable.bind(this)); + this._userVerifier.connect('reset', this._onReset.bind(this)); + this._userVerifier.connect('verification-complete', this._onVerificationComplete.bind(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 serviceName === FINGERPRINT_SERVICE_NAME && + this._haveFingerprintReader; + } + + _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._haveFingerprintReader) + 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 %s for %s'.format(serviceName, this._userName)); + this._hold.release(); + return; + } + this._reportInitError(this._userName + ? 'Failed to start verification for user' + : 'Failed to start verification', e); + return; + } + this._hold.release(); + } + + _beginVerification() { + this._startService(this._getForegroundService()); + + if (this._userName && this._haveFingerprintReader && !this.serviceIsForeground(FINGERPRINT_SERVICE_NAME)) + this._startService(FINGERPRINT_SERVICE_NAME); + } + + _onInfo(client, serviceName, info) { + if (this.serviceIsForeground(serviceName)) { + this._queueMessage(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. + + // Translators: this message is shown below the password entry field + // to indicate the user can swipe their finger instead + this._queueMessage(_("(or swipe finger)"), MessageType.HINT); + } + } + + _onProblem(client, serviceName, problem) { + const isFingerprint = this.serviceIsFingerprint(serviceName); + + if (!this.serviceIsForeground(serviceName) && !isFingerprint) + return; + + this._queueMessage(problem, MessageType.ERROR); + } + + _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() { + this.cancel(); + this.begin(this._userName, new Batch.Hold()); + } + + _verificationFailed(retry) { + // 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._failCounter++; + let canRetry = retry && this._userName && + (this._reauthOnly || + this._failCounter < this._settings.get_int(ALLOWED_FAILURES_KEY)); + + if (canRetry) { + if (!this.hasPendingMessages) { + this._retry(); + } else { + const cancellable = this._cancellable; + let signalId = this.connect('no-more-messages', () => { + this.disconnect(signalId); + if (!cancellable.is_cancelled()) + this._retry(); + }); + } + } else { + // eslint-disable-next-line no-lonely-if + if (!this.hasPendingMessages) { + this._cancelAndReset(); + } else { + const cancellable = this._cancellable; + let signalId = this.connect('no-more-messages', () => { + this.disconnect(signalId); + if (!cancellable.is_cancelled()) + this._cancelAndReset(); + }); + } + } + + this.emit('verification-failed', canRetry); + } + + _onServiceUnavailable(_client, serviceName, errorMessage) { + this._unavailableServices.add(serviceName); + + if (!errorMessage) + return; + + if (this.serviceIsForeground(serviceName) || this.serviceIsFingerprint(serviceName)) + this._queueMessage(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(false); + return; + } + + 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._verificationFailed(true); + } +}; +Signals.addSignalMethods(ShellUserVerifier.prototype); diff --git a/js/gdm/vmware.js b/js/gdm/vmware.js new file mode 100644 index 0000000..20d57e8 --- /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 Signals = imports.signals; +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; + }); + } +}; +Signals.addSignalMethods(VmwareCredentialsManager.prototype); + +function getVmwareCredentialsManager() { + if (!_vmwareCredentialsManager) + _vmwareCredentialsManager = new VmwareCredentialsManager(); + + return _vmwareCredentialsManager; +} |