From c68d03b2115ec366d2ef96f31686514c3e0074c5 Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Fri, 29 May 2026 21:12:58 +0200 Subject: [PATCH 01/12] Fix stale mailbox entries after deleted or moved Refresh stale Maildir and sorted mailbox indexes when messages disappear, avoid rendering missing messages, and keep IMAP sort indexes in sync with mailbox count changes. --- mailboxes/boxes-lib.pl | 27 +++++++++++++++++++++++---- mailboxes/folders-lib.pl | 26 ++++++++++++++++++++++++-- mailboxes/mailboxes-lib.pl | 1 + 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/mailboxes/boxes-lib.pl b/mailboxes/boxes-lib.pl index ac413b75e..a558db107 100755 --- a/mailboxes/boxes-lib.pl +++ b/mailboxes/boxes-lib.pl @@ -2087,10 +2087,20 @@ foreach $f (@files) { $i++; next; } + local $idx = $i++; local $mail = &read_mail_file($f, $_[3]); - $mail->{'idx'} = $i++; - $mail->{'id'} = $f; # ID is relative path, like cur/4535534 - $mail->{'id'} = substr($mail->{'id'}, length($_[0])+1); + if (!$mail && !$_[4]) { + # The cached Maildir file list can be stale if another client + # deleted or moved a message. Re-read it once before returning + # blank entries to the caller. + &flush_maildir_cachefile($_[0]); + return &list_maildir($_[0], $_[1], $_[2], $_[3], 1); + } + if ($mail) { + $mail->{'idx'} = $idx; + $mail->{'id'} = $f; # ID is relative path, like cur/4535534 + $mail->{'id'} = substr($mail->{'id'}, length($_[0])+1); + } push(@rv, $mail); } return @rv; @@ -2110,9 +2120,11 @@ return map { substr($_, length($file)+1) } &get_maildir_files($file); sub select_maildir { local ($file, $ids, $headersonly) = @_; +local $retried = $_[3]; &mark_read_maildir($file); local @files = &get_maildir_files($file); local @rv; +local $missing; foreach my $i (@$ids) { local $path = "$file/$i"; local $mail = &read_mail_file($path, $headersonly); @@ -2139,8 +2151,15 @@ foreach my $i (@$ids) { # Get index in directory $mail->{'idx'} = &indexof($path, @files); } + else { + $missing = 1; + } push(@rv, $mail); } +if ($missing && !$retried) { + &flush_maildir_cachefile($file); + return &select_maildir($file, $ids, $headersonly, 1); + } return @rv; } @@ -2167,7 +2186,7 @@ else { # Check the on-disk cache file local $cachefile = &get_maildir_cachefile($_[0]); local @cst = $cachefile ? stat($cachefile) : ( ); - if ($cst[9] >= $newest) { + if ($cst[9] > $newest) { # Can read the cache open(CACHE, "<", $cachefile); while() { diff --git a/mailboxes/folders-lib.pl b/mailboxes/folders-lib.pl index a699ce1fa..0e35e2b6e 100755 --- a/mailboxes/folders-lib.pl +++ b/mailboxes/folders-lib.pl @@ -174,6 +174,7 @@ elsif ($_[2]->{'type'} == 4) { local $count = $rv[2]; return () if (!$count); $_[2]->{'lastchange'} = $rv[3] if ($rv[3]); + $_[2]->{'mailcount'} = $count; # Work out what range we want local ($start, $end) = &compute_start_end($_[0], $_[1], $count); @@ -458,6 +459,7 @@ elsif ($folder->{'type'} == 4) { } local $h = $irv[1]; local $count = $irv[2]; + $folder->{'mailcount'} = $count; return () if (!$count); $folder->{'lastchange'} = $irv[3] if ($irv[3]); @@ -637,8 +639,9 @@ elsif ($folder->{'type'} == 4) { } local $h = $rv[1]; local $count = $rv[2]; + $folder->{'mailcount'} = $count; return () if (!$count); - $folder->{'lastchange'} = $irv[3] if ($irv[3]); + $folder->{'lastchange'} = $rv[3] if ($rv[3]); @rv = &imap_command($h, "FETCH 1:$count UID"); foreach my $uid (@{$rv[1]}) { @@ -708,6 +711,8 @@ else { sub mailbox_list_mails_sorted { local ($start, $end, $folder, $headersonly, $error, $field, $dir) = @_; +local ($requested_start, $requested_end) = ($start, $end); +local $retried = $_[7]; print DEBUG "mailbox_list_mails_sorted from $start to $end\n"; if (!$field) { # Default to current ordering @@ -738,11 +743,25 @@ local @rv = map { undef } (0 .. scalar(@sorter)-1); local @wantids = map { $sorter[$_] } ($start .. $end); print DEBUG "wantids = ",scalar(@wantids),"\n"; local @mails = &mailbox_select_mails($folder, \@wantids, $headersonly); +local @missing; for(my $i=0; $i<@mails; $i++) { + if (!$mails[$i]) { + push(@missing, $wantids[$i]); + next; + } $rv[$start+$i] = $mails[$i]; print DEBUG "setting $start+$i to ",$mails[$i]," id ",$wantids[$i],"\n"; $mails[$i]->{'sortidx'} = $start+$i; } +if (@missing && !$retried) { + # A sorted IMAP list can contain UIDs for messages that were + # expunged or moved by another client. Force one rebuild so stale + # entries don't render as blank 1969/no-subject rows. + &force_new_index_recheck($folder); + return &mailbox_list_mails_sorted($requested_start, $requested_end, + $folder, $headersonly, $error, + $field, $dir, 1); + } print DEBUG "rv = ",scalar(@rv),"\n"; return @rv; } @@ -808,7 +827,9 @@ local $ifile = &folder_new_sort_index_file($folder); &open_dbm_db($index, $ifile, 0600); print DEBUG "indexchange=$index->{'lastchange'} folderchange=$folder->{'lastchange'}\n"; if ($index->{'lastchange'} != $folder->{'lastchange'} || - !$folder->{'lastchange'}) { + !$folder->{'lastchange'} || + (defined($folder->{'mailcount'}) && + $index->{'mailcount'} != $folder->{'mailcount'})) { # The mail file has changed .. get IDs and update the index with any # that are missing local @ids = &mailbox_idlist($folder); @@ -823,6 +844,7 @@ if ($index->{'lastchange'} != $folder->{'lastchange'} || local @mails = scalar(@newids) ? &mailbox_select_mails($folder, \@newids, 1) : ( ); foreach my $mail (@mails) { + next if (!$mail || !defined($mail->{'id'})); foreach my $f (@index_fields) { if ($f eq "date") { # Convert date to Unix time diff --git a/mailboxes/mailboxes-lib.pl b/mailboxes/mailboxes-lib.pl index 98cebc09b..8a1f2389e 100755 --- a/mailboxes/mailboxes-lib.pl +++ b/mailboxes/mailboxes-lib.pl @@ -1209,6 +1209,7 @@ print &ui_columns_start(\@hcols, 100, 0, \@tds); # Show rows for actual mail messages my $i = 0; foreach my $mail (@mail) { + next if (!$mail); local $idx = $mail->{'idx'}; local $cols = 0; local @cols; From 034d0a09ce4dc37de6af445a95d2a01c2800b4df Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Sat, 30 May 2026 16:13:55 +0200 Subject: [PATCH 02/12] Fix to skip unusable Maildir entries * Note: Ignore zero-byte or unreadable Maildir files when listing messages, log skipped entries, and treat cached zero-byte reads as missing to avoid blank rows and inflated counts. --- mailboxes/boxes-lib.pl | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/mailboxes/boxes-lib.pl b/mailboxes/boxes-lib.pl index a558db107..6514561f6 100755 --- a/mailboxes/boxes-lib.pl +++ b/mailboxes/boxes-lib.pl @@ -2207,6 +2207,32 @@ else { # Flagged as deleted by IMAP .. skip next; } + # Skip entries that cannot be read as messages. + local $path = "$_[0]/$d/$f"; + local @fst = stat($path); + if (!@fst) { + &error_stderr("Skipping Maildir file ". + "$path : stat failed : ". + "$!"); + next; + } + if (!$fst[7]) { + &error_stderr("Skipping Maildir file ". + "$path : file is zero ". + "bytes"); + next; + } + if (!-r _) { + my $m = sprintf("%04o",$fst[2] & 07777); + my $o = getpwuid($fst[4]) || $fst[4]; + my $g = getgrgid($fst[5]) || $fst[5]; + &error_stderr("Skipping Maildir file ". + "$path : not readable by". + " current user, owner=". + "$o($fst[4]):$g($fst[5])". + " mode=$m"); + next; + } push(@shorts, "$d/$f") } closedir(DIR); @@ -2682,9 +2708,11 @@ my ($file, $headersonly) = @_; # Open and read the mail file &open_as_mail_user(MAIL, $file) || return undef; my $mail = &read_mail_fh(MAIL, 0, $headersonly); -$mail->{'file'} = $file; close(MAIL); + local @st = stat($file); +return undef if (@st && !$st[7]); +$mail->{'file'} = $file; $mail->{'size'} = $st[7]; $mail->{'time'} = $st[9]; $mail->{'ctime'} = $st[10]; From 1eb4eb85a7435b7b6221f36bb010cfd26ffcec4b Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Sat, 30 May 2026 21:22:38 +0200 Subject: [PATCH 03/12] Fix to check empty mail files before opening --- mailboxes/boxes-lib.pl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mailboxes/boxes-lib.pl b/mailboxes/boxes-lib.pl index 6514561f6..5e5b8447e 100755 --- a/mailboxes/boxes-lib.pl +++ b/mailboxes/boxes-lib.pl @@ -2706,12 +2706,12 @@ sub read_mail_file my ($file, $headersonly) = @_; # Open and read the mail file +local @st = stat($file); +return undef if (@st && !$st[7]); &open_as_mail_user(MAIL, $file) || return undef; my $mail = &read_mail_fh(MAIL, 0, $headersonly); close(MAIL); -local @st = stat($file); -return undef if (@st && !$st[7]); $mail->{'file'} = $file; $mail->{'size'} = $st[7]; $mail->{'time'} = $st[9]; From 57b1ae3b1888f8ce8e64abd0328a57ceebd0b9ad Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Sun, 31 May 2026 16:14:38 +0200 Subject: [PATCH 04/12] Fix PHP-FPM monitor for EL /etc/php.ini * Note: Map the shared EL PHP configuration file /etc/php.ini to the php-fpm boot action when that service exists, so the PHP-FPM status monitor can resolve current status on Rocky/RHEL systems. root@rocky9-pro:~# php-fpm -i | grep "Loaded Configuration File" Loaded Configuration File => /etc/php.ini https://github.com/webmin/webmin/issues/2599 --- phpini/phpini-lib.pl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/phpini/phpini-lib.pl b/phpini/phpini-lib.pl index a81dffdf1..ebd7d8276 100755 --- a/phpini/phpini-lib.pl +++ b/phpini/phpini-lib.pl @@ -468,6 +468,14 @@ elsif ($file =~ /\/(php-fpm)\.conf/) { return $init; } } +# Generic /etc/php.ini config shared by EL PHP-FPM packages +elsif ($file eq "/etc/php.ini") { + my $init = "php-fpm"; + my $st = &init::action_status($init); + if ($st) { + return $init; + } + } return undef; } From 435d2db4c608e2f872b834c186c19fe253f7bddb Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Sun, 31 May 2026 20:42:33 +0200 Subject: [PATCH 05/12] Fix to use stronger defaults for generated keys https://github.com/webmin/webmin/issues/2728 --- bind8/conf_zonedef.cgi | 2 +- setup.pl | 2 +- setup.sh | 2 +- sshd/sshd-lib.pl | 2 +- sshd/useradmin_update.pl | 5 +++-- usermin/change_ssl.cgi | 2 +- usermin/usermin-lib.pl | 2 +- webmin/change_ssl.cgi | 2 +- webmin/letsencrypt-lib.pl | 2 +- webmin/webmin-lib.pl | 2 +- 10 files changed, 12 insertions(+), 11 deletions(-) diff --git a/bind8/conf_zonedef.cgi b/bind8/conf_zonedef.cgi index a17937b63..7b00c673a 100755 --- a/bind8/conf_zonedef.cgi +++ b/bind8/conf_zonedef.cgi @@ -92,7 +92,7 @@ if (&supports_dnssec()) { # Default algorithm print &ui_table_row($text{'zonedef_alg'}, - &ui_select("alg", $config{'tmpl_dnssecalg'} || "RSASHA1", + &ui_select("alg", $config{'tmpl_dnssecalg'} || "RSASHA256", [ &list_dnssec_algorithms() ]), 3); # Default size diff --git a/setup.pl b/setup.pl index 6cebaa4e8..274f14840 100755 --- a/setup.pl +++ b/setup.pl @@ -494,7 +494,7 @@ else { $cert = &tempname(); $key = &tempname(); $addtextsup = &get_openssl_version() >= 1.1 ? "-addext subjectAltName=DNS:$host,DNS:localhost -addext extendedKeyUsage=serverAuth" : ""; - open(SSL, "| openssl req -newkey rsa:2048 -x509 -nodes -out $cert -keyout $key -days 1825 -sha256 -subj '/CN=$host/C=US/L=Santa Clara' $addtextsup >/dev/null 2>&1"); + open(SSL, "| openssl req -newkey rsa:4096 -x509 -nodes -out $cert -keyout $key -days 1825 -sha256 -subj '/CN=$host/C=US/L=Santa Clara' $addtextsup >/dev/null 2>&1"); print SSL ".\n"; print SSL ".\n"; print SSL ".\n"; diff --git a/setup.sh b/setup.sh index 2aa919d88..fb4f18b72 100755 --- a/setup.sh +++ b/setup.sh @@ -607,7 +607,7 @@ else addtextsup="" fi # We can generate a new SSL key for this host - openssl req -newkey rsa:2048 -x509 -nodes -out $tempdir/cert -keyout $tempdir/key -days 1825 -sha256 -subj "/CN=$host/C=US/L=Santa Clara" $addtextsup >/dev/null 2>&1 </dev/null 2>&1 <= 6.5) { return "ed25519"; } if ($version{'type'} eq 'openssh' && $version{'number'} >= 3.2) { - return "rsa1"; + return "rsa"; } return undef; } diff --git a/sshd/useradmin_update.pl b/sshd/useradmin_update.pl index fc0fa96e6..4b3660f9d 100755 --- a/sshd/useradmin_update.pl +++ b/sshd/useradmin_update.pl @@ -11,12 +11,13 @@ if ($config{'sync_create'} && &has_command($config{'keygen_path'}) && local $cmd; local $type = $config{'sync_type'} || &get_preferred_key_type(); local $tflag = $type ? "-t $type" : ""; + local $bflag = $type eq "rsa" ? "-b 4096" : ""; if ($config{'sync_pass'} && $uinfo->{'passmode'} == 3) { - $cmd = "$config{'keygen_path'} $tflag -P ". + $cmd = "$config{'keygen_path'} $tflag $bflag -P ". quotemeta($uinfo->{'plainpass'}); } else { - $cmd = "$config{'keygen_path'} $tflag -P \"\""; + $cmd = "$config{'keygen_path'} $tflag $bflag -P \"\""; } &system_logged("echo '' | ".&command_as_user($uinfo->{'user'}, 0, $cmd). " >/dev/null 2>&1"); diff --git a/usermin/change_ssl.cgi b/usermin/change_ssl.cgi index d57b81ef1..9a4b67924 100755 --- a/usermin/change_ssl.cgi +++ b/usermin/change_ssl.cgi @@ -47,7 +47,7 @@ elsif ($in{'cipher_list_def'} == 3) { # Generate file needed for PFS my $out = &backquote_command( "openssl dhparam -out ". - quotemeta($miniserv{'dhparams_file'})." 2048 2>&1"); + quotemeta($miniserv{'dhparams_file'})." 4096 2>&1"); if ($?) { &error(&text('ssl_edhparams', "
".&html_escape($out)."
")); diff --git a/usermin/usermin-lib.pl b/usermin/usermin-lib.pl index d84404266..fc66486dc 100755 --- a/usermin/usermin-lib.pl +++ b/usermin/usermin-lib.pl @@ -41,7 +41,7 @@ $latest_page_url = "$http_proto://$update_host/index6.html"; $latest_rpm = "$http_proto://$update_host/download/usermin-latest.noarch.rpm"; $latest_tgz = "$http_proto://$update_host/download/usermin-latest.tar.gz"; -$default_key_size = 2048; +$default_key_size = 4096; $cron_cmd = "$module_config_directory/update.pl"; diff --git a/webmin/change_ssl.cgi b/webmin/change_ssl.cgi index b3509fb98..16df935df 100755 --- a/webmin/change_ssl.cgi +++ b/webmin/change_ssl.cgi @@ -47,7 +47,7 @@ elsif ($in{'cipher_list_def'} == 3) { # Generate file needed for PFS my $out = &backquote_command( "openssl dhparam -out ". - quotemeta($miniserv{'dhparams_file'})." 2048 2>&1"); + quotemeta($miniserv{'dhparams_file'})." 4096 2>&1"); if ($?) { &error(&text('ssl_edhparams', "
".&html_escape($out)."
")); diff --git a/webmin/letsencrypt-lib.pl b/webmin/letsencrypt-lib.pl index c863744cb..dabf3354e 100755 --- a/webmin/letsencrypt-lib.pl +++ b/webmin/letsencrypt-lib.pl @@ -269,7 +269,7 @@ if ($letsencrypt_cmd) { } } $dir =~ s/\/[^\/]+$//; - $size ||= 2048; + $size ||= 4096; my $out; my $common_flags = " --duplicate". " --force-renewal". diff --git a/webmin/webmin-lib.pl b/webmin/webmin-lib.pl index 86c3c1f64..4f9760d95 100755 --- a/webmin/webmin-lib.pl +++ b/webmin/webmin-lib.pl @@ -64,7 +64,7 @@ our $third_port = $primary_port; our $third_page = "/cgi-bin/third.cgi"; our $third_ssl = $primary_ssl; -our $default_key_size = "2048"; +our $default_key_size = "4096"; our $webmin_yum_repo_file = "/etc/yum.repos.d/webmin.repo"; our $webmin_yum_repo_url = "https://download.webmin.com/download/newkey/yum"; From 8df083b054c1750af79b53f3453641ca15a73d07 Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Mon, 1 Jun 2026 00:58:41 +0200 Subject: [PATCH 06/12] Fix Bell Canada SMS gateway domain #2739 --- status/status-lib.pl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/status/status-lib.pl b/status/status-lib.pl index f0455648a..92bf963ce 100755 --- a/status/status-lib.pl +++ b/status/status-lib.pl @@ -493,7 +493,7 @@ return ( { 'id' => 'tmobile', 'domain' => 't.vodafone.ne.jp' }, { 'id' => 'bellcanada', 'desc' => 'Bell Canada', - 'domain' => 'txt.bellmobility.ca' }, + 'domain' => 'txt.bell.ca' }, { 'id' => 'bellsouth', 'desc' => 'Bell South', 'domain' => 'sms.bellsouth.com' }, From 8ef12b66d75555df3e43b9db7ce0b3acd8cbfdd6 Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Mon, 1 Jun 2026 01:00:11 +0200 Subject: [PATCH 07/12] Add Lucky Mobile SMS carrier #2738 --- status/status-lib.pl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/status/status-lib.pl b/status/status-lib.pl index 92bf963ce..6a11ef597 100755 --- a/status/status-lib.pl +++ b/status/status-lib.pl @@ -494,6 +494,9 @@ return ( { 'id' => 'tmobile', { 'id' => 'bellcanada', 'desc' => 'Bell Canada', 'domain' => 'txt.bell.ca' }, + { 'id' => 'luckymobile', + 'desc' => 'Lucky Mobile', + 'domain' => 'txt.bell.ca' }, { 'id' => 'bellsouth', 'desc' => 'Bell South', 'domain' => 'sms.bellsouth.com' }, From b33b9fb0a0aa90d89aa0273df22986fe1590e476 Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Tue, 2 Jun 2026 01:12:24 +0200 Subject: [PATCH 08/12] Fix NetworkManager IPv6 DNS nameserver saving https://github.com/webmin/webmin/issues/2559 --- net/nm-lib.pl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/net/nm-lib.pl b/net/nm-lib.pl index 7120a789e..5a0624a12 100644 --- a/net/nm-lib.pl +++ b/net/nm-lib.pl @@ -231,7 +231,7 @@ my $method6 = $iface->{'auto6'} ? "auto" : # Update nameservers my @ns = $iface->{'nameserver'} ? @{$iface->{'nameserver'}} : (); my @ns4 = grep { &check_ipaddress($_) } @ns; -my @ns6 = grep { &check_ip6address($ns6) } @ns; +my @ns6 = grep { &check_ip6address($_) } @ns; &save_nm_config($cfg, "ipv4", "dns", @ns4 ? join(" ", @ns4) : undef) if (@ns4); &save_nm_config($cfg, "ipv6", "dns", @ns6 ? join(" ", @ns6) : undef) if (@ns6); my @sr = $iface->{'search'} ? @{$iface->{'search'}} : (); From 6574373761a31b00bbc4ca4064461ca46d391354 Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Tue, 2 Jun 2026 01:33:59 +0200 Subject: [PATCH 09/12] Fix to detect NetworkManager networking on Debian MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⓘ Prefer Netplan when Debian has Netplan YAML config, otherwise select the existing NetworkManager backend for Debian systems with saved NM connection profiles, with regression tests for backend selection. https://github.com/webmin/webmin/issues/2559 --- net/net-detect.pl | 32 +++++++++++++++++++++++ net/net-lib.pl | 19 +++++++------- net/t/run-tests.t | 65 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+), 10 deletions(-) create mode 100644 net/net-detect.pl diff --git a/net/net-detect.pl b/net/net-detect.pl new file mode 100644 index 000000000..c87ded6d6 --- /dev/null +++ b/net/net-detect.pl @@ -0,0 +1,32 @@ +# net-detect.pl +# Helper functions for choosing the network config backend + +sub net_has_network_manager_config +{ +my ($dir) = @_; +$dir ||= "/etc/NetworkManager/system-connections"; +my @files = glob("$dir/*.nmconnection"); +return -d $dir && scalar(@files); +} + +sub net_has_netplan_config +{ +my ($dir) = @_; +$dir ||= "/etc/netplan"; +return &has_command("netplan") && + -d $dir; +} + +sub net_auto_backend +{ +my ($os_type, $netplan_dir, $nm_conn_dir) = @_; +return "netplan" + if ($os_type eq "debian-linux" && + &net_has_netplan_config($netplan_dir)); +return "nm" + if (($os_type eq "redhat-linux" || $os_type eq "debian-linux") && + &net_has_network_manager_config($nm_conn_dir)); +return undef; +} + +1; diff --git a/net/net-lib.pl b/net/net-lib.pl index 9023a2d3d..318dcfd4d 100755 --- a/net/net-lib.pl +++ b/net/net-lib.pl @@ -6,6 +6,9 @@ use WebminCore; &init_config(); %access = &get_module_acl(); $access{'ipnodes'} = $access{'hosts'}; +do "net-detect.pl"; + +$auto_net_mode = &net_auto_backend($gconfig{'os_type'}); if (-r "$module_root_directory/$gconfig{'os_type'}-$gconfig{'os_version'}-lib.pl") { do "$gconfig{'os_type'}-$gconfig{'os_version'}-lib.pl"; @@ -23,20 +26,16 @@ elsif ($gconfig{'os_type'} eq 'slackware-linux' && do "$gconfig{'os_type'}-9.1-ALL-lib.pl"; $net_mode = $gconfig{'os_type'}."/9.1"; } -elsif ($gconfig{'os_type'} eq 'redhat-linux' && - -d "/etc/NetworkManager/system-connections" && - glob("/etc/NetworkManager/system-connections/*.nmconnection")) { - # Special case for systems with network manager - do 'nm-lib.pl'; - $net_mode = "nm"; - } -elsif ($gconfig{'os_type'} eq 'debian-linux' && - &has_command("netplan") && - -d "/etc/netplan") { +elsif ($auto_net_mode eq "netplan") { # Special case for newer Ubuntu versions do "netplan-lib.pl"; $net_mode = "netplan"; } +elsif ($auto_net_mode eq "nm") { + # Special case for systems with network manager + do 'nm-lib.pl'; + $net_mode = "nm"; + } else { do "$gconfig{'os_type'}-lib.pl"; $net_mode = $gconfig{'os_type'}; diff --git a/net/t/run-tests.t b/net/t/run-tests.t index 8012538ac..21f936e3e 100644 --- a/net/t/run-tests.t +++ b/net/t/run-tests.t @@ -4,6 +4,7 @@ use warnings; use Test::More; use Cwd qw(abs_path); use File::Basename qw(dirname); +use File::Path qw(make_path); use File::Temp qw(tempdir); my $root = abs_path(dirname(__FILE__)."/../..") or die "rootdir: $!"; @@ -58,6 +59,7 @@ close($fh) || die "close $file: $!"; sub lock_file { return 1; } sub unlock_file { return 1; } sub error { die join("", @_), "\n"; } +sub unflush_file_lines { delete($file_cache{$_[0]}); } sub has_command { return $_[0] eq "netplan" ? "/usr/sbin/netplan" : undef; } sub execute_command_logged { @@ -68,6 +70,13 @@ $$stdout = $out if (ref($stdout)); $$stderr = $out if (ref($stderr) && $stderr ne $stdout); return $command_status{$cmd} || 0; } +sub backquote_logged +{ +my ($cmd) = @_; +push(@commands, $cmd); +$? = $command_status{$cmd} || 0; +return $command_output{$cmd} || ""; +} sub check_ipaddress { return $_[0] =~ /^\d+\.\d+\.\d+\.\d+$/; } sub check_ip6address { return $_[0] =~ /:/; } sub check_ipaddress_any { return &check_ipaddress($_[0]) || &check_ip6address($_[0]); } @@ -83,6 +92,29 @@ return -1; } unshift(@INC, "$root/net", $root); +do "$root/net/net-detect.pl" || die "net-detect.pl: $@ $!"; + +my $detect_root = tempdir(CLEANUP => 1); +my $detect_netplan = "$detect_root/netplan"; +my $detect_no_netplan = "$detect_root/no-netplan"; +my $detect_nm = "$detect_root/NetworkManager/system-connections"; +my $detect_nm_empty = "$detect_root/NetworkManager-empty/system-connections"; +make_path($detect_netplan, $detect_nm, $detect_nm_empty); +write_text("$detect_nm/eth0.nmconnection", ""); + +is(main::net_auto_backend("debian-linux", $detect_netplan, $detect_nm_empty), + "netplan", "Debian uses Netplan when the config directory exists"); +is(main::net_auto_backend("debian-linux", $detect_no_netplan, $detect_nm), + "nm", "Debian uses NetworkManager when only nmconnection files exist"); +is(main::net_auto_backend("redhat-linux", $detect_no_netplan, $detect_nm), + "nm", "Red Hat still uses NetworkManager when nmconnection files exist"); +write_text("$detect_netplan/50-cloud-init.yaml", ""); +is(main::net_auto_backend("debian-linux", $detect_netplan, $detect_nm), + "netplan", "Debian prefers Netplan over NetworkManager when YAML exists"); +unlink("$detect_netplan/50-cloud-init.yaml"); +is(main::net_auto_backend("debian-linux", $detect_no_netplan, $detect_nm_empty), + undef, "Debian falls back when no Netplan or NetworkManager config exists"); + do "$root/net/netplan-lib.pl" || die "netplan-lib.pl: $@ $!"; { @@ -167,4 +199,37 @@ is_deeply(\@commands, "(cd / && /usr/sbin/netplan apply)" ], "apply_network validates before applying"); +do "$root/net/nm-lib.pl" || die "nm-lib.pl: $@ $!"; +my $nmfile = "$tmp/eth0.nmconnection"; +write_text($nmfile, <<'NM'); +[connection] +id=eth0 +uuid=11111111-2222-3333-4444-555555555555 +type=ethernet +interface-name=eth0 + +[ipv4] +method=auto + +[ipv6] +method=disabled +NM +my $nmcfg = main::read_nm_config($nmfile); +my $nmiface = { + 'name' => 'eth0', + 'fullname' => 'eth0', + 'file' => $nmfile, + 'cfg' => $nmcfg, + 'edit' => 1, + 'up' => 1, + 'dhcp' => 1, + 'address6' => [ ], + 'netmask6' => [ ], + 'nameserver' => [ "2001:4860:4860::8888" ], + }; +@commands = ( ); +main::save_interface($nmiface, [ $nmiface ]); +like(join("\n", @commands), qr/ipv6\.dns/, + "NetworkManager save_interface writes IPv6 nameservers"); + done_testing(); From a11330931015e80c7275aa85a2d71be3ff134260 Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Tue, 2 Jun 2026 13:34:31 +0200 Subject: [PATCH 10/12] Add form grouped buttons API for responsive button --- WebminCore.pm | 2 +- ui-lib.pl | 129 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 128 insertions(+), 3 deletions(-) diff --git a/WebminCore.pm b/WebminCore.pm index e5a2f2a21..9968848c9 100644 --- a/WebminCore.pm +++ b/WebminCore.pm @@ -23,7 +23,7 @@ $main::export_to_caller = 1; # Add functions in web-lib-funcs.pl # Generated with : # grep -h "^sub " web-lib-funcs.pl ui-lib.pl | sed -e 's/sub //' | xargs echo -@EXPORT = qw(read_file read_file_cached read_file_cached_with_stat write_file html_escape html_unescape html_strip quote_escape quote_literal_escape quote_javascript default_webmin_temp_dir tempname_dir tempname_dir_sys tempname transname transname_timestamped trunc indexof indexoflc sysprint check_ipaddress check_ip6address is_non_public_ipaddress generate_icon urlize un_urlize include copydata ReadParseMime ReadParse read_fully read_parse_mime_callback read_parse_mime_javascript PrintHeader header get_html_title get_html_framed_title get_html_status_line popup_header footer popup_footer load_module_preferences load_theme_library redirect kill_byname kill_byname_logged find_byname error error_stderr popup_error register_error_handler call_error_handlers error_setup wait_for fast_wait_for has_command make_date make_date_relative file_chooser_button popup_window_button popup_window_link read_acl acl_filename acl_check get_miniserv_config_file get_miniserv_config put_miniserv_config restart_miniserv reload_miniserv check_os_support http_download complete_http_download http_post ftp_download ftp_upload no_proxy open_socket download_timeout ftp_command to_ipaddress to_ip6address to_hostname icons_table replace_meta replace_file_line read_file_lines flush_file_lines unflush_file_lines unix_user_input unix_group_input hlink user_chooser_button group_chooser_button foreign_check foreign_exists foreign_available foreign_require foreign_call foreign_config foreign_installed foreign_defined get_system_hostname get_webmin_version get_webmin_version_release get_webmin_full_version get_module_acl get_group_module_acl save_module_acl save_group_module_acl init_config load_language_auto load_language text_subs text encode_base64 decode_base64 encode_base32 decode_base32 get_module_info get_all_module_infos list_themes get_theme_info list_locales list_languages safe_language read_env_file write_env_file lock_file unlock_file test_lock unlock_all_files can_lock_file webmin_log additional_log var_dump webmin_debug_log system_logged backquote_logged backquote_with_timeout backquote_command kill_logged rename_logged rename_file symlink_logged symlink_file link_file make_dir make_dir_recursive set_ownership_permissions unlink_logged unlink_file copy_permissions_source_dest copy_source_dest move_source_dest remote_session_name verify_session_id remote_foreign_require remote_foreign_call remote_foreign_check remote_foreign_config remote_eval remote_write remote_read remote_finished remote_error_setup remote_rpc_call remote_multi_callback remote_multi_callback_error serialise_variable unserialise_variable other_groups date_chooser_button help_file read_help_file seed_random disk_usage_kb recursive_disk_usage help_search_link can_use_http_ssl make_http_connection validate_ssl_connection read_http_connection write_http_connection close_http_connection read_http_headers get_http_auth_reason clean_environment reset_environment clean_language progress_callback switch_to_remote_user switch_to_unix_user eval_as_unix_user create_user_config_dirs create_missing_homedir filter_javascript resolve_links simplify_path same_file flush_webmin_caches list_usermods available_usermods get_available_module_infos get_visible_module_infos get_visible_modules_categories is_under_directory parse_http_url check_clicks_function load_entities_map entities_to_ascii get_product_name get_charset get_display_hostname save_module_config save_user_module_config nice_size get_perl_path get_goto_module select_all_link select_invert_link select_rows_link check_pid_file get_mod_lib module_root_directory list_mime_types guess_mime_type open_tempfile close_tempfile print_tempfile is_selinux_enabled get_clear_file_attributes reset_file_attributes cleanup_tempnames open_lock_tempfile END month_to_number number_to_month supports_ipv6 execute_command execute_command_logged open_readfile open_execute_command translate_filename translate_command register_filename_callback register_command_callback capture_function_output capture_function_output_tempfile modules_chooser_button substitute_template substitute_pattern running_in_zone running_in_vserver running_in_xen running_in_openvz list_categories is_readonly_mode command_as_user list_osdn_mirrors convert_osdn_url get_current_dir supports_users supports_symlinks quote_path get_windows_root read_file_contents write_file_contents read_file_contents_limit unix_crypt split_quoted_string write_to_http_cache check_in_http_cache clear_http_cache supports_javascript get_module_name get_module_variable clear_time_locale reset_time_locale callers_package web_libs_package get_userdb_string connect_userdb disconnect_userdb split_userdb_string uniquelc list_combined_webmin_menu list_modules_webmin_menu module_to_menu_item list_combined_system_info shell_is_bash compare_version_numbers convert_to_json convert_from_json print_json get_referer_relative get_webmin_email_url get_webmin_browser_url trim ui_link ui_help ui_img ui_link_button ui_table_start ui_table_end ui_table_row ui_table_hr ui_table_span ui_columns_start ui_columns_row ui_columns_header ui_checked_columns_row ui_radio_columns_row ui_columns_end ui_columns_table ui_form_columns_table ui_form_elements_wrapper ui_form_start ui_form_end ui_form_end_side_by_side ui_textbox ui_filebox ui_bytesbox ui_upload ui_password ui_hidden ui_select ui_multi_select ui_multi_select_javascript ui_radio ui_yesno_radio ui_radio_row ui_checkbox ui_oneradio ui_textarea ui_user_textbox ui_users_textbox ui_group_textbox ui_groups_textbox ui_opt_textbox ui_submit ui_reset ui_button ui_date_input ui_buttons_start ui_buttons_end ui_buttons_row ui_buttons_hr ui_post_header ui_pre_footer ui_print_header ui_print_unbuffered_header ui_print_footer ui_config_link ui_print_endpage ui_subheading ui_links_row ui_hidden_javascript ui_hidden_start ui_hidden_end ui_hidden_table_row_start ui_hidden_table_row_end ui_hidden_table_start ui_hidden_table_end ui_tabs_start ui_tabs_end ui_tabs_start_tab ui_tabs_start_tabletab ui_tabs_end_tab ui_tabs_end_tabletab ui_max_text_width ui_radio_selector ui_radio_selector_javascript ui_switch_theme_javascript ui_grid_table ui_radio_table ui_up_down_arrows ui_hr ui_nav_link ui_confirmation_form ui_text_color ui_alert_box js_disable_inputs ui_page_flipper js_checkbox_disable js_redirect ui_webmin_link ui_line_break_double ui_page_refresh ui_details ui_div_row ui_space ui_newline ui_text_wrap ui_element_inline ui_paginations ui_hide_outside_of_viewport ui_read_file_contents_limit ui_note ui_brh ui_tag_start ui_tag_content ui_tag_end ui_tag ui_alert ui_button_icon ui_link_icon ui_icon ui_br ui_p ui_div ui_text_mask get_python_cmd get_buffer_size get_buffer_size_binary get_webprefix get_sub_ref_name setvar getvar delvar print_call_stack webmin_user_can_rpc webmin_user_login_mode webmin_user_is_admin webmin_user_is get_current_theme_info_cached miniserv_using_default_cert is_int float is_float parse_accepted_language get_default_system_locale get_http_redirect get_http_cookie create_wrapper get_lock_links_dir allocate_miniserv_websocket get_miniserv_websocket_url remove_miniserv_websocket cleanup_miniserv_websockets get_miniserv_websockets_modules get_webmin_base_url encrypt_phrase decrypt_phrase is_encrypt_phrase); +@EXPORT = qw(read_file read_file_cached read_file_cached_with_stat write_file html_escape html_unescape html_strip quote_escape quote_literal_escape quote_javascript default_webmin_temp_dir tempname_dir tempname_dir_sys tempname transname transname_timestamped trunc indexof indexoflc sysprint check_ipaddress check_ip6address is_non_public_ipaddress generate_icon urlize un_urlize include copydata ReadParseMime ReadParse read_fully read_parse_mime_callback read_parse_mime_javascript PrintHeader header get_html_title get_html_framed_title get_html_status_line popup_header footer popup_footer load_module_preferences load_theme_library redirect kill_byname kill_byname_logged find_byname error error_stderr popup_error register_error_handler call_error_handlers error_setup wait_for fast_wait_for has_command make_date make_date_relative file_chooser_button popup_window_button popup_window_link read_acl acl_filename acl_check get_miniserv_config_file get_miniserv_config put_miniserv_config restart_miniserv reload_miniserv check_os_support http_download complete_http_download http_post ftp_download ftp_upload no_proxy open_socket download_timeout ftp_command to_ipaddress to_ip6address to_hostname icons_table replace_meta replace_file_line read_file_lines flush_file_lines unflush_file_lines unix_user_input unix_group_input hlink user_chooser_button group_chooser_button foreign_check foreign_exists foreign_available foreign_require foreign_call foreign_config foreign_installed foreign_defined get_system_hostname get_webmin_version get_webmin_version_release get_webmin_full_version get_module_acl get_group_module_acl save_module_acl save_group_module_acl init_config load_language_auto load_language text_subs text encode_base64 decode_base64 encode_base32 decode_base32 get_module_info get_all_module_infos list_themes get_theme_info list_locales list_languages safe_language read_env_file write_env_file lock_file unlock_file test_lock unlock_all_files can_lock_file webmin_log additional_log var_dump webmin_debug_log system_logged backquote_logged backquote_with_timeout backquote_command kill_logged rename_logged rename_file symlink_logged symlink_file link_file make_dir make_dir_recursive set_ownership_permissions unlink_logged unlink_file copy_permissions_source_dest copy_source_dest move_source_dest remote_session_name verify_session_id remote_foreign_require remote_foreign_call remote_foreign_check remote_foreign_config remote_eval remote_write remote_read remote_finished remote_error_setup remote_rpc_call remote_multi_callback remote_multi_callback_error serialise_variable unserialise_variable other_groups date_chooser_button help_file read_help_file seed_random disk_usage_kb recursive_disk_usage help_search_link can_use_http_ssl make_http_connection validate_ssl_connection read_http_connection write_http_connection close_http_connection read_http_headers get_http_auth_reason clean_environment reset_environment clean_language progress_callback switch_to_remote_user switch_to_unix_user eval_as_unix_user create_user_config_dirs create_missing_homedir filter_javascript resolve_links simplify_path same_file flush_webmin_caches list_usermods available_usermods get_available_module_infos get_visible_module_infos get_visible_modules_categories is_under_directory parse_http_url check_clicks_function load_entities_map entities_to_ascii get_product_name get_charset get_display_hostname save_module_config save_user_module_config nice_size get_perl_path get_goto_module select_all_link select_invert_link select_rows_link check_pid_file get_mod_lib module_root_directory list_mime_types guess_mime_type open_tempfile close_tempfile print_tempfile is_selinux_enabled get_clear_file_attributes reset_file_attributes cleanup_tempnames open_lock_tempfile END month_to_number number_to_month supports_ipv6 execute_command execute_command_logged open_readfile open_execute_command translate_filename translate_command register_filename_callback register_command_callback capture_function_output capture_function_output_tempfile modules_chooser_button substitute_template substitute_pattern running_in_zone running_in_vserver running_in_xen running_in_openvz list_categories is_readonly_mode command_as_user list_osdn_mirrors convert_osdn_url get_current_dir supports_users supports_symlinks quote_path get_windows_root read_file_contents write_file_contents read_file_contents_limit unix_crypt split_quoted_string write_to_http_cache check_in_http_cache clear_http_cache supports_javascript get_module_name get_module_variable clear_time_locale reset_time_locale callers_package web_libs_package get_userdb_string connect_userdb disconnect_userdb split_userdb_string uniquelc list_combined_webmin_menu list_modules_webmin_menu module_to_menu_item list_combined_system_info shell_is_bash compare_version_numbers convert_to_json convert_from_json print_json get_referer_relative get_webmin_email_url get_webmin_browser_url trim ui_link ui_help ui_img ui_link_button ui_table_start ui_table_end ui_table_row ui_table_hr ui_table_span ui_columns_start ui_columns_row ui_columns_header ui_checked_columns_row ui_radio_columns_row ui_columns_end ui_columns_table ui_form_columns_table ui_form_elements_wrapper ui_form_start ui_form_end ui_form_end_side_by_side ui_form_grouped_buttons ui_textbox ui_filebox ui_bytesbox ui_upload ui_password ui_hidden ui_select ui_multi_select ui_multi_select_javascript ui_radio ui_yesno_radio ui_radio_row ui_checkbox ui_oneradio ui_textarea ui_user_textbox ui_users_textbox ui_group_textbox ui_groups_textbox ui_opt_textbox ui_submit ui_reset ui_button ui_date_input ui_buttons_start ui_buttons_end ui_buttons_row ui_buttons_hr ui_post_header ui_pre_footer ui_print_header ui_print_unbuffered_header ui_print_footer ui_config_link ui_print_endpage ui_subheading ui_links_row ui_hidden_javascript ui_hidden_start ui_hidden_end ui_hidden_table_row_start ui_hidden_table_row_end ui_hidden_table_start ui_hidden_table_end ui_tabs_start ui_tabs_end ui_tabs_start_tab ui_tabs_start_tabletab ui_tabs_end_tab ui_tabs_end_tabletab ui_max_text_width ui_radio_selector ui_radio_selector_javascript ui_switch_theme_javascript ui_grid_table ui_radio_table ui_up_down_arrows ui_hr ui_nav_link ui_confirmation_form ui_text_color ui_alert_box js_disable_inputs ui_page_flipper js_checkbox_disable js_redirect ui_webmin_link ui_line_break_double ui_page_refresh ui_details ui_div_row ui_space ui_newline ui_text_wrap ui_element_inline ui_paginations ui_hide_outside_of_viewport ui_read_file_contents_limit ui_note ui_brh ui_tag_start ui_tag_content ui_tag_end ui_tag ui_alert ui_button_icon ui_link_icon ui_icon ui_br ui_p ui_div ui_text_mask get_python_cmd get_buffer_size get_buffer_size_binary get_webprefix get_sub_ref_name setvar getvar delvar print_call_stack webmin_user_can_rpc webmin_user_login_mode webmin_user_is_admin webmin_user_is get_current_theme_info_cached miniserv_using_default_cert is_int float is_float parse_accepted_language get_default_system_locale get_http_redirect get_http_cookie create_wrapper get_lock_links_dir allocate_miniserv_websocket get_miniserv_websocket_url remove_miniserv_websocket cleanup_miniserv_websockets get_miniserv_websockets_modules get_webmin_base_url encrypt_phrase decrypt_phrase is_encrypt_phrase); # Add global variables in web-lib.pl push(@EXPORT, qw(&unique)); diff --git a/ui-lib.pl b/ui-lib.pl index e556f6759..49138bcad 100755 --- a/ui-lib.pl +++ b/ui-lib.pl @@ -1,6 +1,7 @@ use vars qw($theme_no_table $ui_radio_selector_donejs $module_name $ui_multi_select_donejs, $ui_formcount, - $ui_form_end_side_by_side_donecss); + $ui_form_end_side_by_side_donecss, + $ui_form_grouped_buttons_donecss); =head1 ui-lib.pl @@ -817,6 +818,48 @@ $rv .= &_ui_form_end_nojs($nojs); return $rv; } +=head2 ui_form_grouped_buttons(&groups, [width]) + +Returns HTML for a responsive row of submit buttons, without closing the form. +Each button uses the same array format as C. + +Pass an array of button groups. A group can be a normal button list, or a list +of button lists when you want a visible gap between sets of buttons. Groups are +spread across the row when there is room, and wrap on narrow screens. Buttons +inside the same list stay visually joined. + +Example : + + my @save_buttons = ([ undef, $text{'save'} ]); + my @control_buttons = ([ 'start', $text{'start'} ], + [ 'stop', $text{'stop'} ]); + my @delete_buttons = ([ 'delete', $text{'delete'} ]); + + print &ui_form_grouped_buttons([ + [ \@save_buttons, \@control_buttons ], + \@delete_buttons, + ]); + +=cut +sub ui_form_grouped_buttons +{ +return &theme_ui_form_grouped_buttons(@_) if (defined(&theme_ui_form_grouped_buttons)); +my ($groups, $width) = @_; +$groups ||= [ ]; +my @groups = grep { $_ && ref($_) eq 'ARRAY' && @$_ } @$groups; +return "" if (!@groups); + +my %attrs = ( 'class' => 'ui_form_grouped_buttons' ); +$attrs{'style'} = "width:$width" if ($width); +my $rv = &_ui_form_grouped_buttons_css(); +$rv .= &ui_tag_start('div', \%attrs); +foreach my $group (@groups) { + $rv .= &_ui_form_grouped_buttons_group($group); + } +$rv .= &ui_tag_end('div'); +return $rv; +} + sub _ui_form_end_buttons_table { my ($buttons, $width, $formid, $class) = @_; @@ -850,6 +893,54 @@ $rv .= "\n"; return $rv; } +sub _ui_form_grouped_buttons_group +{ +my ($group) = @_; +my @clusters = &_ui_form_grouped_buttons_clusters($group); +return "" if (!@clusters); +my $rv = &ui_tag_start('div', { 'class' => 'ui_form_grouped_group' }); +foreach my $cluster (@clusters) { + $rv .= &_ui_form_grouped_buttons_cluster($cluster); + } +$rv .= &ui_tag_end('div'); +return $rv; +} + +sub _ui_form_grouped_buttons_clusters +{ +my ($group) = @_; +return ( ) if (!$group || ref($group) ne 'ARRAY' || !@$group); +foreach my $item (@$group) { + next if (!defined($item)); + return ref($item) eq 'ARRAY' && + (!@$item || ref($item->[0]) eq 'ARRAY') ? + grep { $_ && ref($_) eq 'ARRAY' && @$_ } @$group : + ( $group ); + } +return ( ); +} + +sub _ui_form_grouped_buttons_cluster +{ +my ($buttons) = @_; +return "" if (!$buttons || !@$buttons); +my $rv = &ui_tag_start('span', { 'class' => 'ui_form_grouped_cluster' }); +foreach my $b (@$buttons) { + next if (!defined($b)); + if (ref($b)) { + my $submit = &ui_submit($b->[1], $b->[0], $b->[3], $b->[4]); + chomp($submit); + $rv .= $submit; + $rv .= $b->[2] ? " ".$b->[2] : ""; + } + elsif ($b) { + $rv .= $b; + } + } +$rv .= &ui_tag_end('span'); +return $rv; +} + sub _ui_form_end_side_by_side_css { return "" if ($ui_form_end_side_by_side_donecss++); @@ -876,6 +967,41 @@ return <<'EOF'; EOF } +sub _ui_form_grouped_buttons_css +{ +return "" if ($ui_form_grouped_buttons_donecss++); +my $css = <<'EOF'; +.ui_form_grouped_buttons { + display: flex; + flex-wrap: wrap; + gap: 0.5em 0.45em; + align-items: flex-start; + justify-content: space-between; + width: 100%; +} +.ui_form_grouped_group { + display: flex; + flex-wrap: wrap; + flex: 0 0 auto; + align-items: center; + width: max-content; + max-width: 100%; + margin: 0 -0.45em -0.45em 0; +} +.ui_form_grouped_cluster { + display: flex; + flex-wrap: wrap; + align-items: center; + max-width: 100%; + margin: 0 0.45em 0 0; +} +.ui_form_grouped_cluster .ui_submit { + margin: 0 0 0.45em 0; +} +EOF +return &ui_tag('style', $css, { 'type' => 'text/css' }); +} + sub _ui_form_end_nojs { my ($nojs) = @_; @@ -3934,4 +4060,3 @@ return $rv; } 1; - From 8dfee315426a44b5a51b8057e75d3cf0ef5d8ade Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Tue, 2 Jun 2026 20:32:26 +0200 Subject: [PATCH 11/12] Fix not to hardcode colors --- gray-theme/unauthenticated/gray-theme.css | 14 ++++++++++---- ui-lib.pl | 13 +++++++------ 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/gray-theme/unauthenticated/gray-theme.css b/gray-theme/unauthenticated/gray-theme.css index abd48242c..91c2f2604 100644 --- a/gray-theme/unauthenticated/gray-theme.css +++ b/gray-theme/unauthenticated/gray-theme.css @@ -1,4 +1,10 @@ @font-face{font-family:FramedTheme;src:url(data:application/font-woff2;charset=utf-8;base64,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) format('woff2');font-weight:400;font-style:normal}.ff{display:inline-block;font:normal normal normal 14px/1 FramedTheme;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.ff-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.ff-2x{font-size:2em}.ff-3x{font-size:3em}.ff-4x{font-size:4em}.ff-5x{font-size:5em}.ff-fw{width:1.28571429em;text-align:center}.ff-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.ff-ul>li{position:relative}.ff-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.ff-li.ff-lg{left:-1.85714286em}.ff-webmin:before{content:'\f000'}.ff-virtualmin:before{content:'\f001'}.ff-virtualmin-tick:before{content:'\f002'}.ff-cloudmin:before{content:'\f003'}.ff-sign-out:before{content:'\f008'}.ff-refresh:before{content:'\f009'}.ff-home:before{content:'\f00b'}.ff-chart:before{content:'\f00c'}.ff-exclamation-triangle:before{content:'\f00d'}.ff-play-circle:before{content:'\f00e'}.ff-mail:before{content:'\f00f'}.ff-address-book:before{content:'\f010'}.ff-signature:before{content:'\f011'}.ff-lock:before{content:'\f012'}.ff-folder-open:before{content:'\f013'}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto} +:root { + --text-color-danger: #bc0303; + --text-color-success: #3c763d; + --text-color-warning: #b58900; + --text-color-info: #108eda; +} body {margin: 8px; color: #212121; line-height:1.5em; text-align:left;} p { margin-top:4px; } table { border-width: 0px; @@ -920,16 +926,16 @@ body > .mode > b[data-mode="server-manager"] > a > .ff-cloudmin { min-width: 15px !important; } .text-danger { - color: #bc0303; + color: var(--text-color-danger); } .text-success { - color: #3c763d; + color: var(--text-color-success); } .text-warning { - color: #b58900; + color: var(--text-color-warning); } .text-info { - color: #108eda; + color: var(--text-color-info); } /* Utility used by forms that need fields to consume the available row width. */ .w-100 { diff --git a/ui-lib.pl b/ui-lib.pl index 49138bcad..92db4a5e4 100755 --- a/ui-lib.pl +++ b/ui-lib.pl @@ -2845,16 +2845,17 @@ Returns HTML for a text string, with its color determined by $type. sub ui_text_color { my ($text, $type) = @_; -my ($color); +my ($color, $class_type); if (defined (&theme_ui_text_color)) { return &theme_ui_text_color(@_); } -if ($type eq "success") { $color = "#3c763d"; } -elsif ($type eq "info") { $color = "#31708f"; } -elsif ($type eq "warn") { $color = "#8a6d3b"; } -elsif ($type eq "danger") { $color = "#a94442"; } -return "$text"; +$class_type = $type eq "warn" ? "warning" : $type; +if ($type eq "success") { $color = "var(--text-color-success, #3c763d)"; } +elsif ($type eq "info") { $color = "var(--text-color-info, #31708f)"; } +elsif ($type eq "warn") { $color = "var(--text-color-warning, #8a6d3b)"; } +elsif ($type eq "danger") { $color = "var(--text-color-danger, #a94442)"; } +return "$text"; } =head2 ui_alert_box(msg, type) From f7b8ef379f7045857b0f9b72755736ef5157ca3c Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Wed, 3 Jun 2026 18:43:53 +0200 Subject: [PATCH 12/12] Fix terminal mode handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⓘ Avoid forcing xterm shell PTYs into IO::Stty raw/noecho mode, which can leave interactive shells with broken echo, line editing, and control-key behavior. Keep the existing stty logic for other PTY callers, but add an opt-out flag so xterm can let the shell manage terminal mode normally. https://github.com/webmin/webmin/issues/2452 --- proc/proc-lib.pl | 26 ++++++++++++-------------- xterm/shellserver.pl | 3 ++- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/proc/proc-lib.pl b/proc/proc-lib.pl index 14be79216..f339c7585 100755 --- a/proc/proc-lib.pl +++ b/proc/proc-lib.pl @@ -249,11 +249,11 @@ do { local $oldexit = $?; } while($xp > 0); } -# pty_process_exec(command, [uid, gid], [force-binary-name]) +# pty_process_exec(command, [uid, gid], [force-binary-name], [skip-stty]) # Starts the given command in a new pty and returns the pty filehandle and PID sub pty_process_exec { -local ($cmd, $uid, $gid, $binary) = @_; +local ($cmd, $uid, $gid, $binary, $skip_stty) = @_; if (&is_readonly_mode()) { # When in readonly mode, don't run the command $cmd = "/bin/true"; @@ -262,6 +262,13 @@ if (&is_readonly_mode()) { if ($gconfig{'debug_what_cmd'}); my ($ptyfh, $ttyfh, $pty, $tty, $TIOCSCTTY); +my $set_pty_raw_noecho = sub { + my ($ttyfh) = @_; + eval "use IO::Stty"; + if (!$@) { + IO::Stty::stty($ttyfh, 'raw', '-echo'); + } + }; eval "use IO::Pty"; if (!$@) { @@ -281,10 +288,7 @@ if (!$@) { $ptyfh->make_slave_controlling_terminal(); # Turn off echoing, if we can - eval "use IO::Stty"; - if (!$@) { - IO::Stty::stty($ttyfh, 'raw', '-echo'); - } + $set_pty_raw_noecho->($ttyfh) if (!$skip_stty); close(STDIN); close(STDOUT); close(STDERR); untie(*STDIN); untie(*STDOUT); untie(*STDERR); @@ -342,10 +346,7 @@ elsif (defined &linux_openpty && }; # Turn off echoing, if we can - eval "use IO::Stty"; - if (!$@) { - IO::Stty::stty($ttyfh, 'raw', '-echo'); - } + $set_pty_raw_noecho->($ttyfh) if (!$skip_stty); close(STDIN); close(STDOUT); close(STDERR); untie(*STDIN); untie(*STDOUT); untie(*STDERR); @@ -397,10 +398,7 @@ else { } # Turn off echoing, if we can - eval "use IO::Stty"; - if (!$@) { - IO::Stty::stty($ttyfh, 'raw', '-echo'); - } + $set_pty_raw_noecho->($ttyfh) if (!$skip_stty); if (defined(&open_controlling_pty)) { &open_controlling_pty($ptyfh, $ttyfh, $pty, $tty); diff --git a/xterm/shellserver.pl b/xterm/shellserver.pl index 1c25dd9d2..513d980ba 100755 --- a/xterm/shellserver.pl +++ b/xterm/shellserver.pl @@ -86,7 +86,8 @@ if ($config{'rcfile'} ne '0') { $shelllogin = undef; } } -my ($shellfh, $pid) = proc::pty_process_exec($shellexec, $uid, $gid, $shelllogin); +my ($shellfh, $pid) = proc::pty_process_exec( + $shellexec, $uid, $gid, $shelllogin, 1); reset_environment(); my $shcmd = "'$shellexec".($shelllogin ? " $shelllogin" : "")."'"; if (!$pid) {