From 5b4afbc956af99b11eb5be6301ed024899d2cb76 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Mon, 27 Sep 2021 16:27:09 -0400 Subject: [PATCH] Allow extensions at the login screen Related: #2006985 --- gnome-shell.spec | 7 +- login-screen-extensions.patch | 484 ++++++++++++++++++++++++++++++++++ 2 files changed, 490 insertions(+), 1 deletion(-) create mode 100644 login-screen-extensions.patch diff --git a/gnome-shell.spec b/gnome-shell.spec index 2c8c878..e579043 100644 --- a/gnome-shell.spec +++ b/gnome-shell.spec @@ -2,7 +2,7 @@ Name: gnome-shell Version: 40.4 -Release: 2%{?dist} +Release: 3%{?dist} Summary: Window management and application launching for GNOME License: GPLv2+ @@ -21,6 +21,7 @@ Patch12: disable-unlock-entry-until-question.patch Patch13: 0001-loginDialog-make-info-messages-themed.patch Patch14: support-choicelist-extension.patch Patch15: gdm-networking.patch +Patch16: login-screen-extensions.patch # Misc. Patch30: 0001-panel-add-an-icon-to-the-ActivitiesButton.patch @@ -251,6 +252,10 @@ desktop-file-validate %{buildroot}%{_datadir}/applications/evolution-calendar.de %{_mandir}/man1/gnome-shell.1* %changelog +* Mon Sep 27 2021 Ray Strode - 40.4-3 +- Allow extensions at the login screen + Related: #2006985 + * Thu Aug 19 2021 Florian Müllner - 40.4-2 - Use wwan setting panel for GSM/LTE modems Resolves: #1995560 diff --git a/login-screen-extensions.patch b/login-screen-extensions.patch new file mode 100644 index 0000000..5c42b45 --- /dev/null +++ b/login-screen-extensions.patch @@ -0,0 +1,484 @@ +From 335b88a5508ae97c8da491e33fb07e823f405844 Mon Sep 17 00:00:00 2001 +From: Ray Strode +Date: Sat, 28 Aug 2021 13:54:39 -0400 +Subject: [PATCH 1/2] extensionSystem: Allow extensions to run on the login + screen + +At the moment it's not realy possible to extend the login screen to do +things it doesn't have built-in support for. This means in order +to support niche use cases, those cases have to change the main +code base. For instance, oVirt and Vmware deployments want to be able +to automaticaly log in guest VMs when a user pre-authenticates through a +console on a management host. To support those use cases, we added +code to the login screen directly, even though most machines will never +be associated with oVirt or Vmware management hosts. + +We also get requests from e.g. government users that need certain features +at the login screen that wouldn't get used much outside of government +deployments. For instance, we've gotten requests that a machine contains +prominently displays that it has "Top Secret" information. + +All of these use cases seem like they would better handled via +extensions that could be installed in the specific deployments. The +problem is extensions only run in the user session, and get +disabled at the login screen automatically. + +This commit changes that. Now extensions can specify in their metadata +via a new sessionModes property, which modes that want to run in. For +backward compatibility, if an extension doesn't specify which session +modes it works in, its assumed the extension only works in the user +session. +--- + js/ui/extensionSystem.js | 33 +++++++++++++++++++++++++++++---- + 1 file changed, 29 insertions(+), 4 deletions(-) + +diff --git a/js/ui/extensionSystem.js b/js/ui/extensionSystem.js +index 6b624fca0..278d31f08 100644 +--- a/js/ui/extensionSystem.js ++++ b/js/ui/extensionSystem.js +@@ -48,120 +48,142 @@ var ExtensionManager = class { + } + + GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 60, () => { + disableFile.delete(null); + return GLib.SOURCE_REMOVE; + }); + + this._installExtensionUpdates(); + this._sessionUpdated(); + + GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, UPDATE_CHECK_TIMEOUT, () => { + ExtensionDownloader.checkForUpdates(); + return GLib.SOURCE_CONTINUE; + }); + ExtensionDownloader.checkForUpdates(); + } + + get updatesSupported() { + const appSys = Shell.AppSystem.get_default(); + return appSys.lookup_app('org.gnome.Extensions.desktop') !== null; + } + + lookup(uuid) { + return this._extensions.get(uuid); + } + + getUuids() { + return [...this._extensions.keys()]; + } + ++ _extensionSupportsSessionMode(uuid) { ++ const extension = this.lookup(uuid); ++ if (!extension) ++ return false; ++ ++ if (extension.sessionModes.includes(Main.sessionMode.currentMode)) ++ return true; ++ if (extension.sessionModes.includes(Main.sessionMode.parentMode)) ++ return true; ++ return false; ++ } ++ ++ _sessionModeCanUseExtension(uuid) { ++ if (!Main.sessionMode.allowExtensions) ++ return false; ++ ++ if (!this._extensionSupportsSessionMode(uuid)) ++ return false; ++ ++ return true; ++ } ++ + _callExtensionDisable(uuid) { + let extension = this.lookup(uuid); + if (!extension) + return; + + if (extension.state != ExtensionState.ENABLED) + return; + + // "Rebase" the extension order by disabling and then enabling extensions + // in order to help prevent conflicts. + + // Example: + // order = [A, B, C, D, E] + // user disables C + // this should: disable E, disable D, disable C, enable D, enable E + + let orderIdx = this._extensionOrder.indexOf(uuid); + let order = this._extensionOrder.slice(orderIdx + 1); + let orderReversed = order.slice().reverse(); + + for (let i = 0; i < orderReversed.length; i++) { + let otherUuid = orderReversed[i]; + try { + this.lookup(otherUuid).stateObj.disable(); + } catch (e) { + this.logExtensionError(otherUuid, e); + } + } + + try { + extension.stateObj.disable(); + } catch (e) { + this.logExtensionError(uuid, e); + } + + if (extension.stylesheet) { + let theme = St.ThemeContext.get_for_stage(global.stage).get_theme(); + theme.unload_stylesheet(extension.stylesheet); + delete extension.stylesheet; + } + + for (let i = 0; i < order.length; i++) { + let otherUuid = order[i]; + try { + this.lookup(otherUuid).stateObj.enable(); + } catch (e) { + this.logExtensionError(otherUuid, e); + } + } + + this._extensionOrder.splice(orderIdx, 1); + + if (extension.state != ExtensionState.ERROR) { + extension.state = ExtensionState.DISABLED; + this.emit('extension-state-changed', extension); + } + } + + _callExtensionEnable(uuid) { +- if (!Main.sessionMode.allowExtensions) ++ if (!this._sessionModeCanUseExtension(uuid)) + return; + + let extension = this.lookup(uuid); + if (!extension) + return; + + if (extension.state == ExtensionState.INITIALIZED) + this._callExtensionInit(uuid); + + if (extension.state != ExtensionState.DISABLED) + return; + + let stylesheetNames = ['%s.css'.format(global.session_mode), 'stylesheet.css']; + let theme = St.ThemeContext.get_for_stage(global.stage).get_theme(); + for (let i = 0; i < stylesheetNames.length; i++) { + try { + let stylesheetFile = extension.dir.get_child(stylesheetNames[i]); + theme.load_stylesheet(stylesheetFile); + extension.stylesheet = stylesheetFile; + break; + } catch (e) { + if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND)) + continue; // not an error + this.logExtensionError(uuid, e); + return; + } + } + + try { + extension.stateObj.enable(); +@@ -289,60 +311,61 @@ var ExtensionManager = class { + } catch (e) { + throw new Error('Failed to load metadata.json: %s'.format(e.toString())); + } + let meta; + try { + meta = JSON.parse(metadataContents); + } catch (e) { + throw new Error('Failed to parse metadata.json: %s'.format(e.toString())); + } + + let requiredProperties = ['uuid', 'name', 'description', 'shell-version']; + for (let i = 0; i < requiredProperties.length; i++) { + let prop = requiredProperties[i]; + if (!meta[prop]) + throw new Error('missing "%s" property in metadata.json'.format(prop)); + } + + if (uuid != meta.uuid) + throw new Error('uuid "%s" from metadata.json does not match directory name "%s"'.format(meta.uuid, uuid)); + + let extension = { + metadata: meta, + uuid: meta.uuid, + type, + dir, + path: dir.get_path(), + error: '', + hasPrefs: dir.get_child('prefs.js').query_exists(null), + hasUpdate: false, + canChange: false, ++ sessionModes: meta['session-modes'] ? meta['session-modes'] : [ 'user' ], + }; + this._extensions.set(uuid, extension); + + return extension; + } + + _canLoad(extension) { + if (!this._unloadedExtensions.has(extension.uuid)) + return true; + + const version = this._unloadedExtensions.get(extension.uuid); + return extension.metadata.version === version; + } + + loadExtension(extension) { + // Default to error, we set success as the last step + extension.state = ExtensionState.ERROR; + + let checkVersion = !global.settings.get_boolean(EXTENSION_DISABLE_VERSION_CHECK_KEY); + + if (checkVersion && ExtensionUtils.isOutOfDate(extension)) { + extension.state = ExtensionState.OUT_OF_DATE; + } else if (!this._canLoad(extension)) { + this.logExtensionError(extension.uuid, new Error( + 'A different version was loaded previously. You need to log out for changes to take effect.')); + } else { + let enabled = this._enabledExtensions.includes(extension.uuid); + if (enabled) { + if (!this._callExtensionInit(extension.uuid)) + return; +@@ -373,61 +396,61 @@ var ExtensionManager = class { + // If we did install an importer, it is now cached and it's + // impossible to load a different version + if (type === ExtensionType.PER_USER && extension.imports) + this._unloadedExtensions.set(uuid, extension.metadata.version); + + this._extensions.delete(uuid); + return true; + } + + reloadExtension(oldExtension) { + // Grab the things we'll need to pass to createExtensionObject + // to reload it. + let { uuid, dir, type } = oldExtension; + + // Then unload the old extension. + this.unloadExtension(oldExtension); + + // Now, recreate the extension and load it. + let newExtension; + try { + newExtension = this.createExtensionObject(uuid, dir, type); + } catch (e) { + this.logExtensionError(uuid, e); + return; + } + + this.loadExtension(newExtension); + } + + _callExtensionInit(uuid) { +- if (!Main.sessionMode.allowExtensions) ++ if (!this._sessionModeCanUseExtension(uuid)) + return false; + + let extension = this.lookup(uuid); + if (!extension) + throw new Error("Extension was not properly created. Call createExtensionObject first"); + + let dir = extension.dir; + let extensionJs = dir.get_child('extension.js'); + if (!extensionJs.query_exists(null)) { + this.logExtensionError(uuid, new Error('Missing extension.js')); + return false; + } + + let extensionModule; + let extensionState = null; + + ExtensionUtils.installImporter(extension); + try { + extensionModule = extension.imports.extension; + } catch (e) { + this.logExtensionError(uuid, e); + return false; + } + + if (extensionModule.init) { + try { + extensionState = extensionModule.init(extension); + } catch (e) { + this.logExtensionError(uuid, e); + return false; +@@ -462,67 +485,69 @@ var ExtensionManager = class { + : ENABLED_EXTENSIONS_KEY; + + extension.canChange = + !hasError && + global.settings.is_writable(changeKey) && + (isMode || !modeOnly); + } + + _getEnabledExtensions() { + let extensions = this._getModeExtensions(); + + if (!global.settings.get_boolean(DISABLE_USER_EXTENSIONS_KEY)) + extensions = extensions.concat(global.settings.get_strv(ENABLED_EXTENSIONS_KEY)); + + // filter out 'disabled-extensions' which takes precedence + let disabledExtensions = global.settings.get_strv(DISABLED_EXTENSIONS_KEY); + return extensions.filter(item => !disabledExtensions.includes(item)); + } + + _onUserExtensionsEnabledChanged() { + this._onEnabledExtensionsChanged(); + this._onSettingsWritableChanged(); + } + + _onEnabledExtensionsChanged() { + let newEnabledExtensions = this._getEnabledExtensions(); + + // Find and enable all the newly enabled extensions: UUIDs found in the + // new setting, but not in the old one. + newEnabledExtensions +- .filter(uuid => !this._enabledExtensions.includes(uuid)) ++ .filter(uuid => !this._enabledExtensions.includes(uuid) && ++ this._extensionSupportsSessionMode(uuid)) + .forEach(uuid => this._callExtensionEnable(uuid)); + + // Find and disable all the newly disabled extensions: UUIDs found in the + // old setting, but not in the new one. + this._extensionOrder +- .filter(uuid => !newEnabledExtensions.includes(uuid)) ++ .filter(uuid => !newEnabledExtensions.includes(uuid) || ++ !this._extensionSupportsSessionMode(uuid)) + .reverse().forEach(uuid => this._callExtensionDisable(uuid)); + + this._enabledExtensions = newEnabledExtensions; + } + + _onSettingsWritableChanged() { + for (let extension of this._extensions.values()) { + this._updateCanChange(extension); + this.emit('extension-state-changed', extension); + } + } + + _onVersionValidationChanged() { + // Disabling extensions modifies the order array, so use a copy + let extensionOrder = this._extensionOrder.slice(); + + // Disable enabled extensions in the reverse order first to avoid + // the "rebasing" done in _callExtensionDisable... + extensionOrder.slice().reverse().forEach(uuid => { + this._callExtensionDisable(uuid); + }); + + // ...and then reload and enable extensions in the correct order again. + [...this._extensions.values()].sort((a, b) => { + return extensionOrder.indexOf(a.uuid) - extensionOrder.indexOf(b.uuid); + }).forEach(extension => this.reloadExtension(extension)); + } + + _installExtensionUpdates() { + if (!this.updatesSupported) +-- +2.32.0 + +From 8153a4fd6c0526946afaf531a6e6f14e2da1ef1b Mon Sep 17 00:00:00 2001 +From: Ray Strode +Date: Tue, 10 Aug 2021 15:31:00 -0400 +Subject: [PATCH 2/2] sessionMode: Allow extensions at the login and unlock + screens + +Now extensions can specify which session modes they work in, +but specifying the login screen or unlock screen session modes in +an extensions metadata still won't work, because those session +modes disallow extensions. + +This commit fixes that. +--- + js/ui/sessionMode.js | 2 ++ + 1 file changed, 2 insertions(+) + +diff --git a/js/ui/sessionMode.js b/js/ui/sessionMode.js +index 4d4fb2444..0534fd1d4 100644 +--- a/js/ui/sessionMode.js ++++ b/js/ui/sessionMode.js +@@ -16,76 +16,78 @@ const _modes = { + 'restrictive': { + parentMode: null, + stylesheetName: 'gnome-shell.css', + themeResourceName: 'gnome-shell-theme.gresource', + hasOverview: false, + showCalendarEvents: false, + showWelcomeDialog: false, + allowSettings: false, + allowExtensions: false, + allowScreencast: false, + enabledExtensions: [], + hasRunDialog: false, + hasWorkspaces: false, + hasWindows: false, + hasNotifications: false, + hasWmMenus: false, + isLocked: false, + isGreeter: false, + isPrimary: false, + unlockDialog: null, + components: [], + panel: { + left: [], + center: [], + right: [], + }, + panelStyle: null, + }, + + 'gdm': { ++ allowExtensions: true, + hasNotifications: true, + isGreeter: true, + isPrimary: true, + unlockDialog: imports.gdm.loginDialog.LoginDialog, + components: Config.HAVE_NETWORKMANAGER + ? ['networkAgent', 'polkitAgent'] + : ['polkitAgent'], + panel: { + left: [], + center: ['dateMenu'], + right: ['dwellClick', 'a11y', 'keyboard', 'aggregateMenu'], + }, + panelStyle: 'login-screen', + }, + + 'unlock-dialog': { ++ allowExtensions: true, + isLocked: true, + unlockDialog: undefined, + components: ['polkitAgent', 'telepathyClient'], + panel: { + left: [], + center: [], + right: ['dwellClick', 'a11y', 'keyboard', 'aggregateMenu'], + }, + panelStyle: 'unlock-screen', + }, + + 'user': { + hasOverview: true, + showCalendarEvents: true, + showWelcomeDialog: true, + allowSettings: true, + allowExtensions: true, + allowScreencast: true, + hasRunDialog: true, + hasWorkspaces: true, + hasWindows: true, + hasWmMenus: true, + hasNotifications: true, + isLocked: false, + isPrimary: true, + unlockDialog: imports.ui.unlockDialog.UnlockDialog, + components: Config.HAVE_NETWORKMANAGER + ? ['networkAgent', 'polkitAgent', 'telepathyClient', + 'keyring', 'autorunManager', 'automountManager'] + : ['polkitAgent', 'telepathyClient', +-- +2.32.0 +