From f9d480cfe50ca1d7a0f0b5a2b8bb9932962bfbe7 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sat, 27 Apr 2024 17:07:22 +0200 Subject: Adding upstream version 3.38.6. Signed-off-by: Daniel Baumann --- js/ui/components/polkitAgent.js | 477 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 477 insertions(+) create mode 100644 js/ui/components/polkitAgent.js (limited to 'js/ui/components/polkitAgent.js') diff --git a/js/ui/components/polkitAgent.js b/js/ui/components/polkitAgent.js new file mode 100644 index 0000000..27b705e --- /dev/null +++ b/js/ui/components/polkitAgent.js @@ -0,0 +1,477 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- +/* exported Component */ + +const { AccountsService, Clutter, GLib, + GObject, Pango, PolkitAgent, Polkit, Shell, St } = imports.gi; + +const Dialog = imports.ui.dialog; +const Main = imports.ui.main; +const ModalDialog = imports.ui.modalDialog; +const ShellEntry = imports.ui.shellEntry; +const UserWidget = imports.ui.userWidget; +const Util = imports.misc.util; + +const DialogMode = { + AUTH: 0, + CONFIRM: 1, +}; + +const DIALOG_ICON_SIZE = 64; + +const DELAYED_RESET_TIMEOUT = 200; + +var AuthenticationDialog = GObject.registerClass({ + Signals: { 'done': { param_types: [GObject.TYPE_BOOLEAN] } }, +}, class AuthenticationDialog extends ModalDialog.ModalDialog { + _init(actionId, description, cookie, userNames) { + super._init({ styleClass: 'prompt-dialog' }); + + this.actionId = actionId; + this.message = description; + this.userNames = userNames; + + this._sessionUpdatedId = Main.sessionMode.connect('updated', () => { + this.visible = !Main.sessionMode.isLocked; + }); + + this.connect('closed', this._onDialogClosed.bind(this)); + + let title = _("Authentication Required"); + + let headerContent = new Dialog.MessageDialogContent({ title, description }); + this.contentLayout.add_child(headerContent); + + let bodyContent = new Dialog.MessageDialogContent(); + + if (userNames.length > 1) { + log('polkitAuthenticationAgent: Received %d '.format(userNames.length) + + 'identities that can be used for authentication. Only ' + + 'considering one.'); + } + + let userName = GLib.get_user_name(); + if (!userNames.includes(userName)) + userName = 'root'; + if (!userNames.includes(userName)) + userName = userNames[0]; + + this._user = AccountsService.UserManager.get_default().get_user(userName); + + let userBox = new St.BoxLayout({ + style_class: 'polkit-dialog-user-layout', + vertical: true, + }); + bodyContent.add_child(userBox); + + this._userAvatar = new UserWidget.Avatar(this._user, { + iconSize: DIALOG_ICON_SIZE, + }); + this._userAvatar.x_align = Clutter.ActorAlign.CENTER; + userBox.add_child(this._userAvatar); + + this._userLabel = new St.Label({ + style_class: userName === 'root' + ? 'polkit-dialog-user-root-label' + : 'polkit-dialog-user-label', + }); + + if (userName === 'root') + this._userLabel.text = _('Administrator'); + + userBox.add_child(this._userLabel); + + let passwordBox = new St.BoxLayout({ + style_class: 'prompt-dialog-password-layout', + vertical: true, + }); + + this._passwordEntry = new St.PasswordEntry({ + style_class: 'prompt-dialog-password-entry', + text: "", + can_focus: true, + visible: false, + x_align: Clutter.ActorAlign.CENTER, + }); + ShellEntry.addContextMenu(this._passwordEntry); + this._passwordEntry.clutter_text.connect('activate', this._onEntryActivate.bind(this)); + this._passwordEntry.bind_property('reactive', + this._passwordEntry.clutter_text, 'editable', + GObject.BindingFlags.SYNC_CREATE); + passwordBox.add_child(this._passwordEntry); + + let warningBox = new St.BoxLayout({ vertical: true }); + + let capsLockWarning = new ShellEntry.CapsLockWarning(); + this._passwordEntry.bind_property('visible', + capsLockWarning, 'visible', + GObject.BindingFlags.SYNC_CREATE); + warningBox.add_child(capsLockWarning); + + this._errorMessageLabel = new St.Label({ + style_class: 'prompt-dialog-error-label', + visible: false, + }); + this._errorMessageLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE; + this._errorMessageLabel.clutter_text.line_wrap = true; + warningBox.add_child(this._errorMessageLabel); + + this._infoMessageLabel = new St.Label({ + style_class: 'prompt-dialog-info-label', + visible: false, + }); + this._infoMessageLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE; + this._infoMessageLabel.clutter_text.line_wrap = true; + warningBox.add_child(this._infoMessageLabel); + + /* text is intentionally non-blank otherwise the height is not the same as for + * infoMessage and errorMessageLabel - but it is still invisible because + * gnome-shell.css sets the color to be transparent + */ + this._nullMessageLabel = new St.Label({ style_class: 'prompt-dialog-null-label' }); + this._nullMessageLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE; + this._nullMessageLabel.clutter_text.line_wrap = true; + warningBox.add_child(this._nullMessageLabel); + + passwordBox.add_child(warningBox); + bodyContent.add_child(passwordBox); + + this._cancelButton = this.addButton({ label: _("Cancel"), + action: this.cancel.bind(this), + key: Clutter.KEY_Escape }); + this._okButton = this.addButton({ label: _("Authenticate"), + action: this._onAuthenticateButtonPressed.bind(this), + reactive: false }); + this._okButton.bind_property('reactive', + this._okButton, 'can-focus', + GObject.BindingFlags.SYNC_CREATE); + + this._passwordEntry.clutter_text.connect('text-changed', text => { + this._okButton.reactive = text.get_text().length > 0; + }); + + this.contentLayout.add_child(bodyContent); + + this._doneEmitted = false; + + this._mode = -1; + + this._identityToAuth = Polkit.UnixUser.new_for_name(userName); + this._cookie = cookie; + + this._userLoadedId = this._user.connect('notify::is-loaded', + this._onUserChanged.bind(this)); + this._userChangedId = this._user.connect('changed', + this._onUserChanged.bind(this)); + this._onUserChanged(); + } + + _initiateSession() { + this._destroySession(DELAYED_RESET_TIMEOUT); + + this._session = new PolkitAgent.Session({ identity: this._identityToAuth, + cookie: this._cookie }); + this._sessionCompletedId = this._session.connect('completed', this._onSessionCompleted.bind(this)); + this._sessionRequestId = this._session.connect('request', this._onSessionRequest.bind(this)); + this._sessionShowErrorId = this._session.connect('show-error', this._onSessionShowError.bind(this)); + this._sessionShowInfoId = this._session.connect('show-info', this._onSessionShowInfo.bind(this)); + this._session.initiate(); + } + + _ensureOpen() { + // NOTE: ModalDialog.open() is safe to call if the dialog is + // already open - it just returns true without side-effects + if (!this.open(global.get_current_time())) { + // This can fail if e.g. unable to get input grab + // + // In an ideal world this wouldn't happen (because the + // Shell is in complete control of the session) but that's + // just not how things work right now. + // + // One way to make this happen is by running 'sleep 3; + // pkexec bash' and then opening a popup menu. + // + // We could add retrying if this turns out to be a problem + + log('polkitAuthenticationAgent: Failed to show modal dialog. ' + + 'Dismissing authentication request for action-id %s '.format(this.actionId) + + 'cookie %s'.format(this._cookie)); + this._emitDone(true); + } + } + + _emitDone(dismissed) { + if (!this._doneEmitted) { + this._doneEmitted = true; + this.emit('done', dismissed); + } + } + + _onEntryActivate() { + let response = this._passwordEntry.get_text(); + if (response.length === 0) + return; + + this._passwordEntry.reactive = false; + this._okButton.reactive = false; + + this._session.response(response); + // When the user responds, dismiss already shown info and + // error texts (if any) + this._errorMessageLabel.hide(); + this._infoMessageLabel.hide(); + this._nullMessageLabel.show(); + } + + _onAuthenticateButtonPressed() { + if (this._mode === DialogMode.CONFIRM) + this._initiateSession(); + else + this._onEntryActivate(); + } + + _onSessionCompleted(session, gainedAuthorization) { + if (this._completed || this._doneEmitted) + return; + + this._completed = true; + + /* Yay, all done */ + if (gainedAuthorization) { + this._emitDone(false); + + } else { + /* Unless we are showing an existing error message from the PAM + * module (the PAM module could be reporting the authentication + * error providing authentication-method specific information), + * show "Sorry, that didn't work. Please try again." + */ + if (!this._errorMessageLabel.visible) { + /* Translators: "that didn't work" refers to the fact that the + * requested authentication was not gained; this can happen + * because of an authentication error (like invalid password), + * for instance. */ + this._errorMessageLabel.set_text(_("Sorry, that didn’t work. Please try again.")); + this._errorMessageLabel.show(); + this._infoMessageLabel.hide(); + this._nullMessageLabel.hide(); + + Util.wiggle(this._passwordEntry); + } + + /* Try and authenticate again */ + this._initiateSession(); + } + } + + _onSessionRequest(session, request, echoOn) { + if (this._sessionRequestTimeoutId) { + GLib.source_remove(this._sessionRequestTimeoutId); + this._sessionRequestTimeoutId = 0; + } + + // Hack: The request 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 (request === 'Password:' || request === 'Password: ') + this._passwordEntry.hint_text = _('Password'); + else + this._passwordEntry.hint_text = request.replace(/: *$/, '').trim(); + + this._passwordEntry.password_visible = echoOn; + + this._passwordEntry.show(); + this._passwordEntry.set_text(''); + this._passwordEntry.reactive = true; + this._okButton.reactive = false; + + this._ensureOpen(); + this._passwordEntry.grab_key_focus(); + } + + _onSessionShowError(session, text) { + this._passwordEntry.set_text(''); + this._errorMessageLabel.set_text(text); + this._errorMessageLabel.show(); + this._infoMessageLabel.hide(); + this._nullMessageLabel.hide(); + this._ensureOpen(); + } + + _onSessionShowInfo(session, text) { + this._passwordEntry.set_text(''); + this._infoMessageLabel.set_text(text); + this._infoMessageLabel.show(); + this._errorMessageLabel.hide(); + this._nullMessageLabel.hide(); + this._ensureOpen(); + } + + _destroySession(delay = 0) { + if (this._session) { + this._session.disconnect(this._sessionCompletedId); + this._session.disconnect(this._sessionRequestId); + this._session.disconnect(this._sessionShowErrorId); + this._session.disconnect(this._sessionShowInfoId); + + if (!this._completed) + this._session.cancel(); + + this._completed = false; + this._session = null; + } + + if (this._sessionRequestTimeoutId) { + GLib.source_remove(this._sessionRequestTimeoutId); + this._sessionRequestTimeoutId = 0; + } + + let resetDialog = () => { + this._sessionRequestTimeoutId = 0; + + if (this.state != ModalDialog.State.OPENED) + return GLib.SOURCE_REMOVE; + + this._passwordEntry.hide(); + this._cancelButton.grab_key_focus(); + this._okButton.reactive = false; + + return GLib.SOURCE_REMOVE; + }; + + if (delay) { + this._sessionRequestTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, delay, resetDialog); + GLib.Source.set_name_by_id(this._sessionRequestTimeoutId, '[gnome-shell] this._sessionRequestTimeoutId'); + } else { + resetDialog(); + } + } + + _onUserChanged() { + if (!this._user.is_loaded) + return; + + let userName = this._user.get_user_name(); + let realName = this._user.get_real_name(); + + if (userName !== 'root') + this._userLabel.set_text(realName); + + this._userAvatar.update(); + + if (this._user.get_password_mode() === AccountsService.UserPasswordMode.NONE) { + if (this._mode === DialogMode.CONFIRM) + return; + + this._mode = DialogMode.CONFIRM; + this._destroySession(); + + this._okButton.reactive = true; + + /* We normally open the dialog when we get a "request" signal, but + * since in this case initiating a session would perform the + * authentication, only open the dialog and initiate the session + * when the user confirmed. */ + this._ensureOpen(); + } else { + if (this._mode === DialogMode.AUTH) + return; + + this._mode = DialogMode.AUTH; + this._initiateSession(); + } + } + + close(timestamp) { + // Ensure cleanup if the dialog was never shown + if (this.state === ModalDialog.State.CLOSED) + this._onDialogClosed(); + super.close(timestamp); + } + + cancel() { + this.close(global.get_current_time()); + this._emitDone(true); + } + + _onDialogClosed() { + if (this._sessionUpdatedId) + Main.sessionMode.disconnect(this._sessionUpdatedId); + + if (this._sessionRequestTimeoutId) + GLib.source_remove(this._sessionRequestTimeoutId); + this._sessionRequestTimeoutId = 0; + + if (this._user) { + this._user.disconnect(this._userLoadedId); + this._user.disconnect(this._userChangedId); + this._user = null; + } + + this._destroySession(); + } +}); + +var AuthenticationAgent = GObject.registerClass( +class AuthenticationAgent extends Shell.PolkitAuthenticationAgent { + _init() { + super._init(); + + this._currentDialog = null; + this.connect('initiate', this._onInitiate.bind(this)); + this.connect('cancel', this._onCancel.bind(this)); + this._sessionUpdatedId = 0; + } + + enable() { + try { + this.register(); + } catch (e) { + log('Failed to register AuthenticationAgent'); + } + } + + disable() { + try { + this.unregister(); + } catch (e) { + log('Failed to unregister AuthenticationAgent'); + } + } + + _onInitiate(nativeAgent, actionId, message, iconName, cookie, userNames) { + // Don't pop up a dialog while locked + if (Main.sessionMode.isLocked) { + this._sessionUpdatedId = Main.sessionMode.connect('updated', () => { + Main.sessionMode.disconnect(this._sessionUpdatedId); + this._sessionUpdatedId = 0; + + this._onInitiate(nativeAgent, actionId, message, iconName, cookie, userNames); + }); + return; + } + + this._currentDialog = new AuthenticationDialog(actionId, message, cookie, userNames); + this._currentDialog.connect('done', this._onDialogDone.bind(this)); + } + + _onCancel(_nativeAgent) { + this._completeRequest(false); + } + + _onDialogDone(_dialog, dismissed) { + this._completeRequest(dismissed); + } + + _completeRequest(dismissed) { + this._currentDialog.close(); + this._currentDialog = null; + + if (this._sessionUpdatedId) + Main.sessionMode.disconnect(this._sessionUpdatedId); + this._sessionUpdatedId = 0; + + this.complete(dismissed); + } +}); + +var Component = AuthenticationAgent; -- cgit v1.2.3