From 4a03da36817c8d22a32a63d5c115efcf49ce85f5 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 | 152 ++++++++++++++++++++++------------------ 1 file changed, 84 insertions(+), 68 deletions(-) diff --git a/js/ui/status/network.js b/js/ui/status/network.js index b407d8e78d..6d070f6d88 100644 --- a/js/ui/status/network.js +++ b/js/ui/status/network.js @@ -51,7 +51,7 @@ var PortalHelperResult = { }; const PortalHelperIface = loadInterfaceXML('org.gnome.Shell.PortalHelper'); -const PortalHelperProxy = Gio.DBusProxy.makeProxyWrapper(PortalHelperIface); +const PortalHelperInfo = Gio.DBusInterfaceInfo.new_for_xml(PortalHelperIface); function signalToIcon(value) { if (value < 20) @@ -1707,6 +1707,77 @@ var DeviceCategory = class extends PopupMenu.PopupMenuSection { } }; +class CaptivePortalHandler { + constructor(checkUri) { + 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?.CloseRemote(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?.AuthenticateRemote(path, this._checkUri, timestamp); + this._connectivityQueue.add(path); + } + + clear() { + for (const item of this._connectivityQueue) + this._portalHelperProxy?.CloseRemote(item); + this._connectivityQueue.clear(); + } +} +Signals.addSignalMethods(CaptivePortalHandler.prototype); + var NMApplet = GObject.registerClass( class Indicator extends PanelMenu.SystemIndicator { _init() { @@ -1763,6 +1834,16 @@ class Indicator extends PanelMenu.SystemIndicator { this._vpnSection.connect('icon-changed', this._updateIcon.bind(this)); this.menu.addMenuItem(this._vpnSection.item); + 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._readConnections(); this._readDevices(); this._syncNMState(); @@ -2074,51 +2155,10 @@ class Indicator extends PanelMenu.SystemIndicator { this._syncConnectivity(); } - _flushConnectivityQueue() { - if (this._portalHelperProxy) { - for (let item of this._connectivityQueue) - this._portalHelperProxy.CloseRemote(item); - } - - this._connectivityQueue = []; - } - - _closeConnectivityCheck(path) { - let index = this._connectivityQueue.indexOf(path); - - if (index >= 0) { - if (this._portalHelperProxy) - this._portalHelperProxy.CloseRemote(path); - - this._connectivityQueue.splice(index, 1); - } - } - - async _portalHelperDone(proxy, emitter, 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 he chooses 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: %s'.format(result)); - } - } - _syncConnectivity() { if (this._mainConnection == null || this._mainConnection.state != NM.ActiveConnectionState.ACTIVATED) { - this._flushConnectivityQueue(); + this._portalHandler.clear(); return; } @@ -2133,31 +2173,7 @@ class Indicator extends PanelMenu.SystemIndicator { if (!isPortal || Main.sessionMode.isGreeter) return; - let path = this._mainConnection.get_path(); - for (let item of this._connectivityQueue) { - if (item == path) - return; - } - - let timestamp = global.get_current_time(); - if (this._portalHelperProxy) { - this._portalHelperProxy.AuthenticateRemote(path, '', timestamp); - } else { - new PortalHelperProxy(Gio.DBus.session, 'org.gnome.Shell.PortalHelper', - '/org/gnome/Shell/PortalHelper', (proxy, error) => { - if (error) { - log('Error launching the portal helper: %s'.format(error)); - return; - } - - this._portalHelperProxy = proxy; - proxy.connectSignal('Done', this._portalHelperDone.bind(this)); - - proxy.AuthenticateRemote(path, '', timestamp); - }); - } - - this._connectivityQueue.push(path); + this._portalHandler.addConnection(this._mainConnection.get_path()); } _updateIcon() { -- 2.45.2 From 7d1a2442c957df3a31ab544da9b4e1365b8c2348 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 6d070f6d88..5913467454 100644 --- a/js/ui/status/network.js +++ b/js/ui/status/network.js @@ -1711,19 +1711,43 @@ class CaptivePortalHandler { constructor(checkUri) { 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.Source( + _('System'), + 'emblem-system-symbolic'); + Main.messageTray.add(source); + + const notification = new MessageTray.Notification( + source, _('Sign Into Wi–Fi Network'), name); + notification.connect('activated', + () => this._onNotificationActivated(path)); + notification.connect('destroy', + () => this._notifications.delete(path)); + this._notifications.set(path, notification); + source.showNotification(notification); } + removeConnection(path) { if (this._connectivityQueue.delete(path)) this._portalHelperProxy?.CloseRemote(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) { @@ -1774,6 +1798,10 @@ class CaptivePortalHandler { for (const item of this._connectivityQueue) this._portalHelperProxy?.CloseRemote(item); this._connectivityQueue.clear(); + + for (const n of this._notifications.values()) + n.destroy(MessageTray.NotificationDestroyedReason.SOURCE_CLOSED); + this._notifications.clear(); } } Signals.addSignalMethods(CaptivePortalHandler.prototype); @@ -2173,7 +2201,9 @@ class Indicator extends PanelMenu.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 cda2810d13bb9b5294759a8268cfbb27d83190f1 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 | 13 ++++++++++--- meson.build | 5 +++++ meson_options.txt | 6 ++++++ src/meson.build | 2 +- 9 files changed, 43 insertions(+), 12 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 4a1e16d467..01cf828310 100644 --- a/data/meson.build +++ b/data/meson.build @@ -4,7 +4,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 e54e280441..0882af6d01 100644 --- a/js/misc/config.js.in +++ b/js/misc/config.js.in @@ -8,6 +8,8 @@ var PACKAGE_VERSION = '@PACKAGE_VERSION@'; var HAVE_BLUETOOTH = @HAVE_BLUETOOTH@; /* 1 if networkmanager is available, 0 otherwise */ var HAVE_NETWORKMANAGER = @HAVE_NETWORKMANAGER@; +/* 1 if portal helper is enabled, 0 otherwise */ +var HAVE_PORTAL_HELPER = @HAVE_PORTAL_HELPER@; /* gettext package */ var GETTEXT_PACKAGE = '@GETTEXT_PACKAGE@'; /* locale dir */ diff --git a/js/misc/meson.build b/js/misc/meson.build index 2702c3dbc9..5f5f6c390f 100644 --- a/js/misc/meson.build +++ b/js/misc/meson.build @@ -5,6 +5,7 @@ jsconf.set('GETTEXT_PACKAGE', meson.project_name()) jsconf.set('LIBMUTTER_API_VERSION', mutter_api_version) jsconf.set10('HAVE_BLUETOOTH', bt_dep.found()) 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 5913467454..27a5eeb823 100644 --- a/js/ui/status/network.js +++ b/js/ui/status/network.js @@ -4,6 +4,7 @@ const { Clutter, Gio, GLib, GObject, Meta, NM, Polkit, St } = imports.gi; const Signals = imports.signals; const Animation = imports.ui.animation; +const Config = imports.misc.config; const Main = imports.ui.main; const PanelMenu = imports.ui.panelMenu; const PopupMenu = imports.ui.popupMenu; @@ -1744,7 +1745,13 @@ class CaptivePortalHandler { } _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(); @@ -1767,8 +1774,7 @@ class CaptivePortalHandler { } } - 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, @@ -1790,6 +1796,7 @@ class CaptivePortalHandler { } } + const {timestamp} = context; this._portalHelperProxy?.AuthenticateRemote(path, this._checkUri, timestamp); this._connectivityQueue.add(path); } diff --git a/meson.build b/meson.build index ff841dccf2..8feac29224 100644 --- a/meson.build +++ b/meson.build @@ -116,6 +116,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('systemd') libsystemd_dep = dependency('libsystemd') systemd_dep = dependency('systemd') diff --git a/meson_options.txt b/meson_options.txt index ef76b73c34..7666fc5421 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -28,6 +28,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 d235c37438..3e1accf2e7 100644 --- a/src/meson.build +++ b/src/meson.build @@ -250,7 +250,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