From 9e43e0f7172b448d449764ab20dbe0540316f388 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 44242a1a79a4d9b0311b735d484c996584bee0e2 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..39140d0ce4 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 = new 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 b25485ef256068b53a852070bcb5fcf4c4a62875 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 0307613e70..8b376a6d6b 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 39140d0ce4..5832d96dc6 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 923a5228ec..d554045c5b 100644 --- a/meson.build +++ b/meson.build @@ -101,6 +101,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 c81bd1a42a..d4445743bd 100644 --- a/src/meson.build +++ b/src/meson.build @@ -258,7 +258,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