From d77ed9ad5fdadcdefcc41fecf8200bd2bf47282d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Wed, 12 Jun 2024 13:13:41 +0200 Subject: [PATCH 1/3] network: Split out CaptivePortalHandler class The handling of captive portals is going to be extended a bit, so split out a proper class instead of mixing it in with the indicator code. --- js/ui/status/network.js | 151 +++++++++++++++++++++++----------------- 1 file changed, 87 insertions(+), 64 deletions(-) diff --git a/js/ui/status/network.js b/js/ui/status/network.js index 404733b74c..d34e947073 100644 --- a/js/ui/status/network.js +++ b/js/ui/status/network.js @@ -14,6 +14,7 @@ import * as Main from '../main.js'; import * as PopupMenu from '../popupMenu.js'; import * as MessageTray from '../messageTray.js'; import * as ModemManager from '../../misc/modemManager.js'; +import * as Signals from '../../misc/signals.js'; import * as Util from '../../misc/util.js'; import {Spinner} from '../animation.js'; @@ -1944,12 +1945,84 @@ class NMModemToggle extends NMDeviceToggle { } }); +class CaptivePortalHandler extends Signals.EventEmitter { + constructor(checkUri) { + super(); + + this._checkUri = checkUri; + this._connectivityQueue = new Set(); + this._portalHelperProxy = null; + } + + addConnection(path) { + if (this._connectivityQueue.has(path)) + return; + + this._launchPortalHelper(path).catch(logError); + } + + removeConnection(path) { + if (this._connectivityQueue.delete(path)) + this._portalHelperProxy?.CloseAsync(path); + } + + _portalHelperDone(parameters) { + const [path, result] = parameters; + + if (result === PortalHelperResult.CANCELLED) { + // Keep the connection in the queue, so the user is not + // spammed with more logins until we next flush the queue, + // which will happen once they choose a better connection + // or we get to full connectivity through other means + } else if (result === PortalHelperResult.COMPLETED) { + this.removeConnection(path); + } else if (result === PortalHelperResult.RECHECK) { + this.emit('recheck', path); + } else { + log(`Invalid result from portal helper: ${result}`); + } + } + + async _launchPortalHelper(path) { + const timestamp = global.get_current_time(); + if (!this._portalHelperProxy) { + this._portalHelperProxy = new Gio.DBusProxy({ + g_connection: Gio.DBus.session, + g_name: 'org.gnome.Shell.PortalHelper', + g_object_path: '/org/gnome/Shell/PortalHelper', + g_interface_name: PortalHelperInfo.name, + g_interface_info: PortalHelperInfo, + }); + this._portalHelperProxy.connectSignal('Done', + (proxy, emitter, params) => { + this._portalHelperDone(params); + }); + + try { + await this._portalHelperProxy.init_async( + GLib.PRIORITY_DEFAULT, null); + } catch (e) { + console.error(`Error launching the portal helper: ${e.message}`); + } + } + + this._portalHelperProxy?.AuthenticateAsync(path, this._checkUri, timestamp).catch(logError); + this._connectivityQueue.add(path); + } + + clear() { + for (const item of this._connectivityQueue) + this._portalHelperProxy?.CloseAsync(item); + this._connectivityQueue.clear(); + } +} + export const Indicator = GObject.registerClass( class Indicator extends SystemIndicator { _init() { super._init(); - this._connectivityQueue = new Set(); + this._portalHandler = null; this._mainConnection = null; @@ -2004,6 +2077,16 @@ class Indicator extends SystemIndicator { this, 'visible', GObject.BindingFlags.SYNC_CREATE); + const {connectivityCheckUri} = this._client; + this._portalHandler = new CaptivePortalHandler(connectivityCheckUri); + this._portalHandler.connect('recheck', async (o, path) => { + try { + const state = await this._client.check_connectivity_async(null); + if (state >= NM.ConnectivityState.FULL) + this._portalHandler.removeConnection(path); + } catch (e) { } + }); + this._client.connectObject( 'notify::primary-connection', () => this._syncMainConnection(), 'notify::activating-connection', () => this._syncMainConnection(), @@ -2066,42 +2149,10 @@ class Indicator extends SystemIndicator { this._notification?.destroy(); } - _flushConnectivityQueue() { - for (let item of this._connectivityQueue) - this._portalHelperProxy?.CloseAsync(item); - this._connectivityQueue.clear(); - } - - _closeConnectivityCheck(path) { - if (this._connectivityQueue.delete(path)) - this._portalHelperProxy?.CloseAsync(path); - } - - async _portalHelperDone(parameters) { - let [path, result] = parameters; - - if (result === PortalHelperResult.CANCELLED) { - // Keep the connection in the queue, so the user is not - // spammed with more logins until we next flush the queue, - // which will happen once they choose a better connection - // or we get to full connectivity through other means - } else if (result === PortalHelperResult.COMPLETED) { - this._closeConnectivityCheck(path); - } else if (result === PortalHelperResult.RECHECK) { - try { - const state = await this._client.check_connectivity_async(null); - if (state >= NM.ConnectivityState.FULL) - this._closeConnectivityCheck(path); - } catch (e) { } - } else { - log(`Invalid result from portal helper: ${result}`); - } - } - - async _syncConnectivity() { + _syncConnectivity() { if (this._mainConnection == null || this._mainConnection.state !== NM.ActiveConnectionState.ACTIVATED) { - this._flushConnectivityQueue(); + this._portalHandler.clear(); return; } @@ -2116,35 +2167,7 @@ class Indicator extends SystemIndicator { if (!isPortal || Main.sessionMode.isGreeter) return; - let path = this._mainConnection.get_path(); - if (this._connectivityQueue.has(path)) - return; - - let timestamp = global.get_current_time(); - if (!this._portalHelperProxy) { - this._portalHelperProxy = new Gio.DBusProxy({ - g_connection: Gio.DBus.session, - g_name: 'org.gnome.Shell.PortalHelper', - g_object_path: '/org/gnome/Shell/PortalHelper', - g_interface_name: PortalHelperInfo.name, - g_interface_info: PortalHelperInfo, - }); - this._portalHelperProxy.connectSignal('Done', - (proxy, emitter, params) => { - this._portalHelperDone(params).catch(logError); - }); - - try { - await this._portalHelperProxy.init_async( - GLib.PRIORITY_DEFAULT, null); - } catch (e) { - console.error(`Error launching the portal helper: ${e.message}`); - } - } - - this._portalHelperProxy?.AuthenticateAsync(path, this._client.connectivity_check_uri, timestamp).catch(logError); - - this._connectivityQueue.add(path); + this._portalHandler.addConnection(this._mainConnection.get_path()); } _updateIcon() { -- 2.45.2 From cd489e54948e3f2900a1f4e354fd4bb43c62db6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Wed, 12 Jun 2024 13:13:41 +0200 Subject: [PATCH 2/3] status/network: Show notification when detecting captive portal When NetworkManager detects limited connectivity, we currently pop up the portal helper window immediately. This can both be disruptive when it happens unexpectedly, and unnoticeable when it happens during screen lock. In any case, it seems better to not pop up a window without explicit user action, so instead show a notification that launches the portal window when activated. Closes: https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/7688 --- js/ui/status/network.js | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/js/ui/status/network.js b/js/ui/status/network.js index d34e947073..5d1e168fb5 100644 --- a/js/ui/status/network.js +++ b/js/ui/status/network.js @@ -1951,19 +1951,43 @@ class CaptivePortalHandler extends Signals.EventEmitter { this._checkUri = checkUri; this._connectivityQueue = new Set(); + this._notifications = new Map(); this._portalHelperProxy = null; } - addConnection(path) { - if (this._connectivityQueue.has(path)) + addConnection(name, path) { + if (this._connectivityQueue.has(path) || this._notifications.has(path)) return; - this._launchPortalHelper(path).catch(logError); + const source = MessageTray.getSystemSource(); + + const notification = new MessageTray.Notification({ + title: _('Sign Into Wi–Fi Network'), + body: name, + source, + }); + notification.connect('activated', + () => this._onNotificationActivated(path).catch(logError)); + notification.connect('destroy', + () => this._notifications.delete(path)); + this._notifications.set(path, notification); + source.addNotification(notification); } + removeConnection(path) { if (this._connectivityQueue.delete(path)) this._portalHelperProxy?.CloseAsync(path); + this._notifications.get(path)?.destroy( + MessageTray.NotificationDestroyedReason.SOURCE_CLOSED); + this._notifications.delete(path); + } + + _onNotificationActivated(path) { + this._launchPortalHelper(path).catch(logError); + + Main.overview.hide(); + Main.panel.closeCalendar(); } _portalHelperDone(parameters) { @@ -2014,6 +2038,10 @@ class CaptivePortalHandler extends Signals.EventEmitter { for (const item of this._connectivityQueue) this._portalHelperProxy?.CloseAsync(item); this._connectivityQueue.clear(); + + for (const n of this._notifications.values()) + n.destroy(MessageTray.NotificationDestroyedReason.SOURCE_CLOSED); + this._notifications.clear(); } } @@ -2167,7 +2195,9 @@ class Indicator extends SystemIndicator { if (!isPortal || Main.sessionMode.isGreeter) return; - this._portalHandler.addConnection(this._mainConnection.get_path()); + this._portalHandler.addConnection( + this._mainConnection.get_id(), + this._mainConnection.get_path()); } _updateIcon() { -- 2.45.2 From 52b8a150dd96086a48ba2ac7668b22e6429767fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Wed, 12 Jun 2024 13:13:41 +0200 Subject: [PATCH 3/3] build: Add option to disable portal-helper The portal login window uses WebKit, which is a security-sensitive component that not all vendors want to support. Support that case with a build option, and update the captive portal handler to use the user's default browser if the portal-helper is disabled. --- data/icons/meson.build | 10 +++++++++- data/meson.build | 2 +- js/meson.build | 14 ++++++++------ js/misc/config.js.in | 2 ++ js/misc/meson.build | 1 + js/ui/status/network.js | 15 +++++++++++---- meson.build | 5 +++++ meson_options.txt | 6 ++++++ src/meson.build | 2 +- 9 files changed, 44 insertions(+), 13 deletions(-) diff --git a/data/icons/meson.build b/data/icons/meson.build index eff6e4b530..277df017b2 100644 --- a/data/icons/meson.build +++ b/data/icons/meson.build @@ -1 +1,9 @@ -install_subdir('hicolor', install_dir: icondir) +excluded_icons=[] +if not have_portal_helper + excluded_icons += [ + 'scalable/apps/org.gnome.Shell.CaptivePortal.svg', + 'symbolic/apps/org.gnome.Shell.CaptivePortal-symbolic.svg', + ] +endif +install_subdir('hicolor', + install_dir: icondir, exclude_files: excluded_icons) diff --git a/data/meson.build b/data/meson.build index 8654dfeca7..ed13b6baea 100644 --- a/data/meson.build +++ b/data/meson.build @@ -6,7 +6,7 @@ desktop_files = [ ] service_files = [] -if have_networkmanager +if have_portal_helper desktop_files += 'org.gnome.Shell.PortalHelper.desktop' service_files += 'org.gnome.Shell.PortalHelper.service' endif diff --git a/js/meson.build b/js/meson.build index 4809f82b83..e594e23627 100644 --- a/js/meson.build +++ b/js/meson.build @@ -8,9 +8,11 @@ js_resources = gnome.compile_resources( dependencies: [config_js] ) -portal_resources = gnome.compile_resources( - 'portal-resources', 'portal-resources.gresource.xml', - source_dir: ['.', meson.current_build_dir()], - c_name: 'portal_js_resources', - dependencies: [config_js] -) +if have_portal_helper + portal_resources = gnome.compile_resources( + 'portal-resources', 'portal-resources.gresource.xml', + source_dir: ['.', meson.current_build_dir()], + c_name: 'portal_js_resources', + dependencies: [config_js] + ) +endif diff --git a/js/misc/config.js.in b/js/misc/config.js.in index ad8d46d841..ce319e0f84 100644 --- a/js/misc/config.js.in +++ b/js/misc/config.js.in @@ -7,6 +7,8 @@ export const PACKAGE_NAME = '@PACKAGE_NAME@'; export const PACKAGE_VERSION = '@PACKAGE_VERSION@'; /* 1 if networkmanager is available, 0 otherwise */ export const HAVE_NETWORKMANAGER = @HAVE_NETWORKMANAGER@; +/* 1 if portal helper is enabled, 0 otherwise */ +export const HAVE_PORTAL_HELPER = @HAVE_PORTAL_HELPER@; /* gettext package */ export const GETTEXT_PACKAGE = '@GETTEXT_PACKAGE@'; /* locale dir */ diff --git a/js/misc/meson.build b/js/misc/meson.build index 5aceefac42..5fc8ca433f 100644 --- a/js/misc/meson.build +++ b/js/misc/meson.build @@ -4,6 +4,7 @@ jsconf.set('PACKAGE_VERSION', meson.project_version()) jsconf.set('GETTEXT_PACKAGE', meson.project_name()) jsconf.set('LIBMUTTER_API_VERSION', mutter_api_version) jsconf.set10('HAVE_NETWORKMANAGER', have_networkmanager) +jsconf.set10('HAVE_PORTAL_HELPER', have_portal_helper) jsconf.set('datadir', datadir) jsconf.set('libexecdir', libexecdir) diff --git a/js/ui/status/network.js b/js/ui/status/network.js index 5d1e168fb5..cdc9ba8928 100644 --- a/js/ui/status/network.js +++ b/js/ui/status/network.js @@ -10,6 +10,7 @@ import Polkit from 'gi://Polkit'; import Shell from 'gi://Shell'; import St from 'gi://St'; +import * as Config from '../../misc/config.js'; import * as Main from '../main.js'; import * as PopupMenu from '../popupMenu.js'; import * as MessageTray from '../messageTray.js'; @@ -1967,7 +1968,7 @@ class CaptivePortalHandler extends Signals.EventEmitter { source, }); notification.connect('activated', - () => this._onNotificationActivated(path).catch(logError)); + () => this._onNotificationActivated(path)); notification.connect('destroy', () => this._notifications.delete(path)); this._notifications.set(path, notification); @@ -1984,7 +1985,13 @@ class CaptivePortalHandler extends Signals.EventEmitter { } _onNotificationActivated(path) { - this._launchPortalHelper(path).catch(logError); + const context = global.create_app_launch_context( + global.get_current_time(), -1); + + if (Config.HAVE_PORTAL_HELPER) + this._launchPortalHelper(path, context).catch(logError); + else + Gio.AppInfo.launch_default_for_uri(this._checkUri, context); Main.overview.hide(); Main.panel.closeCalendar(); @@ -2007,8 +2014,7 @@ class CaptivePortalHandler extends Signals.EventEmitter { } } - async _launchPortalHelper(path) { - const timestamp = global.get_current_time(); + async _launchPortalHelper(path, context) { if (!this._portalHelperProxy) { this._portalHelperProxy = new Gio.DBusProxy({ g_connection: Gio.DBus.session, @@ -2030,6 +2036,7 @@ class CaptivePortalHandler extends Signals.EventEmitter { } } + const {timestamp} = context; this._portalHelperProxy?.AuthenticateAsync(path, this._checkUri, timestamp).catch(logError); this._connectivityQueue.add(path); } diff --git a/meson.build b/meson.build index 035f54732a..4721bd6017 100644 --- a/meson.build +++ b/meson.build @@ -107,6 +107,11 @@ else have_networkmanager = false endif +have_portal_helper = get_option('portal_helper') +if have_portal_helper and not have_networkmanager + error('Portal helper requires networkmanager support') +endif + if get_option('camera_monitor') libpipewire_dep = dependency('libpipewire-0.3', version: pipewire_req) have_pipewire = true diff --git a/meson_options.txt b/meson_options.txt index 6e83d92f2e..01e0d5803b 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -40,6 +40,12 @@ option('networkmanager', description: 'Enable NetworkManager support' ) +option('portal_helper', + type: 'boolean', + value: true, + description: 'Enable build-in network portal login' +) + option('systemd', type: 'boolean', value: true, diff --git a/src/meson.build b/src/meson.build index 752104e120..a3bf542480 100644 --- a/src/meson.build +++ b/src/meson.build @@ -286,7 +286,7 @@ executable('gnome-shell', 'main.c', install: true ) -if have_networkmanager +if have_portal_helper executable('gnome-shell-portal-helper', 'gnome-shell-portal-helper.c', portal_resources, c_args: tools_cflags, -- 2.45.2