From 04f1426f3316b974070c0d9a5a5ca76c22d7989a Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Sat, 28 Feb 2026 18:23:13 +0200 Subject: [PATCH 1/6] Add ability to get module edition --- bin/webmin | 99 +++++++++++++++++++++++++----------------------------- 1 file changed, 45 insertions(+), 54 deletions(-) diff --git a/bin/webmin b/bin/webmin index db7f6ea18..799e677bf 100755 --- a/bin/webmin +++ b/bin/webmin @@ -5,7 +5,7 @@ use strict; use warnings; BEGIN { $Pod::Usage::Formatter = 'Pod::Text::Color'; } use 5.010; # Version in CentOS 6 -use Getopt::Long qw(:config permute pass_through); +use Getopt::Long qw(:config no_ignore_case permute pass_through); use Term::ANSIColor qw(:constants); use Pod::Usage; @@ -27,7 +27,7 @@ sub main { 'describe|d' => \$opt{'describe'}, 'man|m' => \$opt{'man'}, 'version|v' => \$opt{'version'}, - 'versions' => \$opt{'versions'}, + 'versions|V' => \$opt{'versions'}, '<>' => sub { # Handle unrecognized options, inc. subcommands. my($arg) = @_; @@ -56,61 +56,52 @@ sub main { exit 0; } elsif ($opt{'version'} || $opt{'versions'}) { # Load libs - my $ver_checked = sub { - my ($ver_remote, $ver_curr) = @_; - if ($ver_remote && $ver_curr && - compare_version_numbers($ver_remote, $ver_curr) > 0 ) { - return (BRIGHT_RED, $ver_curr, RESET, DARK, " (" . RESET, BRIGHT_GREEN, $ver_remote, RESET, DARK . " is available)", RESET); - } else { - return GREEN, $ver_curr, RESET; - } - }; my $print_mod_vers = sub { - my ($module_type, $modules_list, $prod_root, $prod_ver, $versions_remote_local) = @_; - my @minfo; - if (ref($modules_list)) { - my $head; - my @modules_list = sort(@{$modules_list}); - foreach my $mod (@modules_list) { - my %mod_info; - read_file($mod, \%mod_info); - my $mod_ver = $mod_info{'version_actual'} || $mod_info{'version'}; - my $mod_desc = $mod_info{'desc'}; - if ($mod_ver && $prod_ver && $mod_desc && $prod_ver !~ /^$mod_ver/) { - say CYAN, " $module_type: ", RESET if (!$head++); - my ($mod_dir) = $mod =~ m/$prod_root\/(.*?)\//; - push(@minfo, {'desc' => $mod_desc, 'ver' => $mod_ver, 'dir' => $mod_dir}); - } - } - @minfo = sort { $a->{'desc'} cmp $b->{'desc'} } @minfo; - foreach my $mod (@minfo) { - say " $mod->{'desc'}: " , &$ver_checked($versions_remote_local->{$mod->{'dir'}}, $mod->{'ver'}), DARK " [$mod->{'dir'}]", RESET; + my ($module_type, $modules_list, $prod_root, $prod_ver) = @_; + return if (!ref($modules_list)); + # Gather module info + my @mods; + foreach my $mod (@{$modules_list}) { + my %mi; + read_file($mod, \%mi); + my $ver = $mi{'version_actual'} || $mi{'version'}; + my ($dir) = $mod =~ m/$prod_root\/(.*?)\//; + next if (!$ver || !$mi{'desc'} || !$dir); + next if ($prod_ver =~ /^\Q$ver\E/); + push(@mods, { desc => $mi{'desc'}, ver => $ver, dir => $dir }); + } + # Print sorted by description + my $head; + foreach my $m (sort { $a->{'desc'} cmp $b->{'desc'} } @mods) { + my $mod_ver = $m->{'ver'}; + if (-r "$prod_root/$m->{'dir'}/module.info") { + eval { no warnings 'once'; + local $main::error_must_die = 1; + &foreign_require($m->{'dir'}) }; + # Get module edition if available + my $ed; + $ed = eval { + &foreign_call($m->{'dir'}, "get_module_edition") } + if (&foreign_defined($m->{'dir'}, + "get_module_edition")); + $mod_ver .= " $ed" if ($ed); } + say CYAN, " $module_type: ", RESET if (!$head++); + say " $m->{'desc'}: ", GREEN, $mod_ver, RESET, + DARK " [$m->{'dir'}]", RESET; } }; my $root = root($opt{'config'}); if ($root && -d $root) { + $ENV{'WEBMIN_CONFIG'} = $opt{'config'}; + no warnings 'once'; + @main::root_directories = ($root); + $main::root_directory = $root; + *unique = sub { my %seen; grep { !$seen{$_}++ } @_ } if (!defined(&unique)); + use warnings; require("$root/web-lib-funcs.pl"); - # Try to get remote versions first - my %versions_remote; - if ($opt{'versions'}) { - my ($latest_known_versions_remote, $latest_known_versions_remote_error); - http_download("virtualmin.com", 443, '/software-latest', - \$latest_known_versions_remote, \$latest_known_versions_remote_error, - undef, 1, undef, undef, 5); - if ($latest_known_versions_remote && - !$latest_known_versions_remote_error) { - %versions_remote = map { - my ($k, $v) = split(/=/, $_, 2); - defined($v) ? ($k => $v) : (); - } split(/\n/, $latest_known_versions_remote); - } elsif ($latest_known_versions_remote_error) { - say BRIGHT_YELLOW, "Warning: ", RESET, "Cannot fetch remote packages versions list - $latest_known_versions_remote_error"; - } - } - # Get Webmin version installed my $ver1 = "$root/version"; my $ver2 = "$opt{'config'}/version"; @@ -126,7 +117,7 @@ sub main { say "$ver$verrel"; exit 0; } else { - say CYAN, "Webmin: ", RESET, &$ver_checked($versions_remote{'webmin'}, "$ver$verrel"), DARK " [$root]", RESET; + say CYAN, "Webmin: ", RESET, GREEN, "$ver$verrel", RESET, DARK " [$root]", RESET; } } else { say RED, "Error: ", RESET, "Cannot determine Webmin version"; @@ -147,8 +138,8 @@ sub main { } } closedir($dir); - &$print_mod_vers('Themes', \@themes, $root, $ver, \%versions_remote); - &$print_mod_vers('Modules', \@mods, $root, $ver, \%versions_remote); + &$print_mod_vers('Themes', \@themes, $root, $ver); + &$print_mod_vers('Modules', \@mods, $root, $ver); # Check for Usermin my $wmumconfig = "$opt{'config'}/usermin/config"; @@ -175,7 +166,7 @@ sub main { } $uver = trim($uver) . $uverrel; if ($uver) { - say CYAN, "Usermin: ", RESET, &$ver_checked($versions_remote{'usermin'}, $uver), DARK " [$uroot]", RESET; + say CYAN, "Usermin: ", RESET, GREEN, $uver, RESET, DARK " [$uroot]", RESET; my ($udir, @uthemes, @umods); if (opendir($udir, "$uroot")) { while (my $file = readdir($udir)) { @@ -190,8 +181,8 @@ sub main { } } closedir($udir); - &$print_mod_vers('Themes', \@uthemes, $uroot, $uver, \%versions_remote); - &$print_mod_vers('Modules', \@umods, $uroot, $uver, \%versions_remote); + &$print_mod_vers('Themes', \@uthemes, $uroot, $uver); + &$print_mod_vers('Modules', \@umods, $uroot, $uver); } } } From f204480957696d792bcd8eed38b308883724f96c Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Sat, 28 Feb 2026 18:49:31 +0200 Subject: [PATCH 2/6] Fix formatting --- bin/webmin | 716 ++++++++++++++++++++++++++++------------------------- 1 file changed, 376 insertions(+), 340 deletions(-) diff --git a/bin/webmin b/bin/webmin index 799e677bf..4e21aca2c 100755 --- a/bin/webmin +++ b/bin/webmin @@ -1,399 +1,435 @@ #!/usr/bin/env perl # Webmin CLI - Allows performing a variety of common Webmin-related # functions on the command line. + use strict; use warnings; BEGIN { $Pod::Usage::Formatter = 'Pod::Text::Color'; } -use 5.010; # Version in CentOS 6 +use 5.010; use Getopt::Long qw(:config no_ignore_case permute pass_through); use Term::ANSIColor qw(:constants); use Pod::Usage; # Check if root if ($> != 0) { - die BRIGHT_RED, "Error: ", RESET, BRIGHT_YELLOW,"webmin", RESET, - " command must be run as root\n"; - exit 1; - } + die BRIGHT_RED, + "Error: ", RESET, BRIGHT_YELLOW,"webmin", RESET, + " command must be run as root\n"; + exit 1; + } my $a0 = $ARGV[0]; -sub main { - my ( %opt, $subcmd ); - GetOptions( - 'help|h' => \$opt{'help'}, - 'config|c=s' => \$opt{'config'}, - 'list-commands|l' => \$opt{'list'}, - 'describe|d' => \$opt{'describe'}, - 'man|m' => \$opt{'man'}, - 'version|v' => \$opt{'version'}, - 'versions|V' => \$opt{'versions'}, - '<>' => sub { - # Handle unrecognized options, inc. subcommands. - my($arg) = @_; - if ($arg =~ m{^-}) { - say "Usage error: Unknown option $arg."; - pod2usage(0); - } else { - # It must be a subcommand. - $subcmd = $arg; - die "!FINISH"; - } - } - ); +sub main +{ +my ( %opt, $subcmd ); +GetOptions( + 'help|h' => \$opt{'help'}, + 'config|c=s' => \$opt{'config'}, + 'list-commands|l' => \$opt{'list'}, + 'describe|d' => \$opt{'describe'}, + 'man|m' => \$opt{'man'}, + 'version|v' => \$opt{'version'}, + 'versions|V' => \$opt{'versions'}, + '<>' => sub { + # Handle unrecognized options, inc. subcommands. + my($arg) = @_; + if ($arg =~ m{^-}) { + say "Usage error: Unknown option $arg."; + pod2usage(0); + } + else { + # It must be a subcommand. + $subcmd = $arg; + die "!FINISH"; + } + } +); - # Set defaults - $opt{'config'} ||= "/etc/webmin"; - $opt{'commands'} = $a0; - - # Load libs - loadlibs(\%opt); +# Set defaults +$opt{'config'} ||= "/etc/webmin"; +$opt{'commands'} = $a0; - my @remain = @ARGV; - # List commands? - if ($opt{'list'}) { - list_commands(\%opt); - exit 0; - } elsif ($opt{'version'} || $opt{'versions'}) { - # Load libs - my $print_mod_vers = sub { - my ($module_type, $modules_list, $prod_root, $prod_ver) = @_; - return if (!ref($modules_list)); - # Gather module info - my @mods; - foreach my $mod (@{$modules_list}) { - my %mi; - read_file($mod, \%mi); - my $ver = $mi{'version_actual'} || $mi{'version'}; - my ($dir) = $mod =~ m/$prod_root\/(.*?)\//; - next if (!$ver || !$mi{'desc'} || !$dir); - next if ($prod_ver =~ /^\Q$ver\E/); - push(@mods, { desc => $mi{'desc'}, ver => $ver, dir => $dir }); - } - # Print sorted by description - my $head; - foreach my $m (sort { $a->{'desc'} cmp $b->{'desc'} } @mods) { - my $mod_ver = $m->{'ver'}; - if (-r "$prod_root/$m->{'dir'}/module.info") { - eval { no warnings 'once'; - local $main::error_must_die = 1; - &foreign_require($m->{'dir'}) }; - # Get module edition if available - my $ed; - $ed = eval { - &foreign_call($m->{'dir'}, "get_module_edition") } - if (&foreign_defined($m->{'dir'}, - "get_module_edition")); - $mod_ver .= " $ed" if ($ed); - } - say CYAN, " $module_type: ", RESET if (!$head++); - say " $m->{'desc'}: ", GREEN, $mod_ver, RESET, - DARK " [$m->{'dir'}]", RESET; - } - }; +# Load libs +loadlibs(\%opt); - my $root = root($opt{'config'}); - if ($root && -d $root) { - $ENV{'WEBMIN_CONFIG'} = $opt{'config'}; - no warnings 'once'; - @main::root_directories = ($root); - $main::root_directory = $root; - *unique = sub { my %seen; grep { !$seen{$_}++ } @_ } if (!defined(&unique)); - use warnings; - require("$root/web-lib-funcs.pl"); - - # Get Webmin version installed - my $ver1 = "$root/version"; - my $ver2 = "$opt{'config'}/version"; - my $ver = read_file_contents($ver1) || read_file_contents($ver2); - my $verrel_file = "$root/release"; - my $verrel = -r $verrel_file ? read_file_contents($verrel_file) : ""; - if ($verrel) { - $verrel = ":@{[trim($verrel)]}"; - } - $ver = trim($ver); - if ($ver) { - if ($opt{'version'}) { - say "$ver$verrel"; - exit 0; - } else { - say CYAN, "Webmin: ", RESET, GREEN, "$ver$verrel", RESET, DARK " [$root]", RESET; - } - } else { - say RED, "Error: ", RESET, "Cannot determine Webmin version"; - exit 1; - } - - # Get other Webmin themes/modules versions if available - my ($dir, @themes, @mods); - if (opendir($dir, $root)) { - while (my $file = readdir($dir)) { - my $theme_info_file = "$root/$file/theme.info"; - push(@themes, $theme_info_file) - if (-r $theme_info_file); +my @remain = @ARGV; +# List commands? +if ($opt{'list'}) { + list_commands(\%opt); + exit 0; + } +elsif ($opt{'version'} || $opt{'versions'}) { + # Load libs + my $print_mod_vers = sub { + my ($module_type, $modules_list, $prod_root, $prod_ver) = @_; + return if (!ref($modules_list)); + # Gather module info + my @mods; + foreach my $mod (@{$modules_list}) { + my %mi; + read_file($mod, \%mi); + my $ver = $mi{'version'}; + my ($dir) = $mod =~ m/$prod_root\/(.*?)\//; + next if (!$ver || !$mi{'desc'} || !$dir); + next if ($prod_ver =~ /^\Q$ver\E/); + push(@mods, { desc => $mi{'desc'}, ver => $ver, + dir => $dir }); + } + # Print sorted by description + my $head; + foreach my $m (sort { $a->{'desc'} cmp $b->{'desc'} } @mods) { + my $mod_ver = $m->{'ver'}; + if (-r "$prod_root/$m->{'dir'}/module.info") { + eval { no warnings 'once'; + local $main::error_must_die = 1; + &foreign_require($m->{'dir'}) }; + # Get module edition if available + my $ed; + $ed = eval { &foreign_call( + $m->{'dir'}, + "get_module_edition") + } if (&foreign_defined($m->{'dir'}, + "get_module_edition")); + $mod_ver .= " $ed" if ($ed); + } + say CYAN, " $module_type: ", RESET if (!$head++); + say " $m->{'desc'}: ", GREEN, $mod_ver, RESET, + DARK " [$m->{'dir'}]", RESET; + } + }; - my $mod_info_file = "$root/$file/module.info"; - push(@mods, $mod_info_file) - if (-r $mod_info_file); - } - } - closedir($dir); - &$print_mod_vers('Themes', \@themes, $root, $ver); - &$print_mod_vers('Modules', \@mods, $root, $ver); + my $root = root($opt{'config'}); + if ($root && -d $root) { + $ENV{'WEBMIN_CONFIG'} = $opt{'config'}; + no warnings 'once'; + @main::root_directories = ($root); + $main::root_directory = $root; + *unique = sub { my %seen; grep { !$seen{$_}++ } @_ } + if (!defined(&unique)); + use warnings; + require("$root/web-lib-funcs.pl"); + + # Get Webmin version installed + my $ver1 = "$root/version"; + my $ver2 = "$opt{'config'}/version"; + my $ver = read_file_contents($ver1) || + read_file_contents($ver2); + my $verrel_file = "$root/release"; + my $verrel = -r $verrel_file + ? read_file_contents($verrel_file) : ""; + if ($verrel) { + $verrel = ":@{[trim($verrel)]}"; + } + $ver = trim($ver); + if ($ver) { + if ($opt{'version'}) { + say "$ver$verrel"; + exit 0; + } + else { + say CYAN, "Webmin: ", RESET, GREEN, + "$ver$verrel", RESET, + DARK " [$root]", RESET; + } + } + else { + say RED, "Error: ", RESET, + "Cannot determine Webmin version"; + exit 1; + } + + # Get other Webmin themes/modules versions if available + my ($dir, @themes, @mods); + if (opendir($dir, $root)) { + while (my $file = readdir($dir)) { + my $theme_info_file = + "$root/$file/theme.info"; + push(@themes, $theme_info_file) + if (-r $theme_info_file); + my $mod_info_file = "$root/$file/module.info"; + push(@mods, $mod_info_file) + if (-r $mod_info_file); + } + } + closedir($dir); + &$print_mod_vers('Themes', \@themes, $root, $ver); + &$print_mod_vers('Modules', \@mods, $root, $ver); - # Check for Usermin - my $wmumconfig = "$opt{'config'}/usermin/config"; - if (-r $wmumconfig) { - my %wmumconfig; - read_file($wmumconfig, \%wmumconfig); + # Check for Usermin + my $wmumconfig = "$opt{'config'}/usermin/config"; + if (-r $wmumconfig) { + my %wmumconfig; + read_file($wmumconfig, \%wmumconfig); - # Usermin config dir - $wmumconfig = $wmumconfig{'usermin_dir'}; - if ($wmumconfig) { - my %uminiserv; - read_file("$wmumconfig/miniserv.conf", \%uminiserv); - my $uroot = $uminiserv{'root'}; + # Usermin config dir + $wmumconfig = $wmumconfig{'usermin_dir'}; + if ($wmumconfig) { + my %uminiserv; + read_file("$wmumconfig/miniserv.conf", + \%uminiserv); + my $uroot = $uminiserv{'root'}; - # Get Usermin version installed - if ($uroot && -d $uroot) { - my $uver1 = "$uroot/version"; - my $uver2 = "$wmumconfig/version"; - my $uver = read_file_contents($uver1) || read_file_contents($uver2); - my $uverrel_file = "$uroot/release"; - my $uverrel = -r $uverrel_file ? read_file_contents($uverrel_file) : ""; - if ($uverrel) { - $uverrel = ":@{[trim($uverrel)]}"; - } - $uver = trim($uver) . $uverrel; - if ($uver) { - say CYAN, "Usermin: ", RESET, GREEN, $uver, RESET, DARK " [$uroot]", RESET; - my ($udir, @uthemes, @umods); - if (opendir($udir, "$uroot")) { - while (my $file = readdir($udir)) { - my $theme_info_file = "$uroot/$file/theme.info"; - push(@uthemes, $theme_info_file) - if (-r $theme_info_file); + # Get Usermin version installed + if ($uroot && -d $uroot) { + my $uver1 = "$uroot/version"; + my $uver2 = "$wmumconfig/version"; + my $uver = read_file_contents($uver1) || + read_file_contents($uver2); + my $uverrel_file = "$uroot/release"; + my $uverrel = -r $uverrel_file + ? read_file_contents($uverrel_file) : ""; + $uverrel = ":@{[trim($uverrel)]}" if ($uverrel); + $uver = trim($uver) . $uverrel; + if ($uver) { + say CYAN, "Usermin: ", RESET, GREEN, $uver, RESET, DARK " [$uroot]", RESET; + my ($udir, @uthemes, @umods); + if (opendir($udir, "$uroot")) { + while (my $file = readdir($udir)) { + my $theme_info_file = "$uroot/$file/theme.info"; + push(@uthemes, $theme_info_file) + if (-r $theme_info_file); - my $mod_info_file = "$uroot/$file/module.info"; - push(@umods, $mod_info_file) - if (-r $mod_info_file); + my $mod_info_file = "$uroot/$file/module.info"; + push(@umods, $mod_info_file) + if (-r $mod_info_file); - } - } - closedir($udir); - &$print_mod_vers('Themes', \@uthemes, $uroot, $uver); - &$print_mod_vers('Modules', \@umods, $uroot, $uver); - } - } - } - } - } + } + } + closedir($udir); + &$print_mod_vers('Themes', \@uthemes, $uroot, $uver); + &$print_mod_vers('Modules', \@umods, $uroot, $uver); + } + } + } + } + } + exit 0; + } +elsif ($opt{'man'} || $opt{'help'} || !defined($remain[0])) { + # Show the full manual page + man_command(\%opt, $subcmd); + exit 0; + } +elsif ($subcmd) { + run_command( \%opt, $subcmd, \@remain ); + } - exit 0; - } elsif ($opt{'man'} || $opt{'help'} || !defined($remain[0])) { - # Show the full manual page - man_command(\%opt, $subcmd); - exit 0; - } elsif ($subcmd) { - run_command( \%opt, $subcmd, \@remain ); - } - - exit 0; +exit 0; } exit main( \@ARGV ) if !caller(0); # run_command - Run a subcommand # $optref is a reference to an options object passed down from global options # like --help or a --config path. -sub run_command { - my ( $optref, $subcmd, $remainref ) = @_; +sub run_command +{ +my ( $optref, $subcmd, $remainref ) = @_; - # Load libs - loadlibs($optref); +# Load libs +loadlibs($optref); - # Figure out the Webmin root directory - my $root = root($optref->{'config'}); +# Figure out the Webmin root directory +my $root = root($optref->{'config'}); - my (@commands) = list_commands($optref); - if (! grep( /^$subcmd$/, @commands ) ) { - say RED, "Error: ", RESET, "Command \`$subcmd\` doesn't exist", RESET; - exit 1; - } +my (@commands) = list_commands($optref); +if (! grep( /^$subcmd$/, @commands ) ) { + say RED, "Error: ", RESET, "Command \`$subcmd\` doesn't exist", RESET; + exit 1; + } - my $command_path = get_command_path($root, $subcmd, $optref); - - # Merge the options - # Only handling config, right now... - # XXX Should we do this with libraries instead of commands? - # Maybe detect .pm for that possibility. - my @allopts = ("--config", "$optref->{'config'}", @$remainref); - # Run that binch - system($command_path, @allopts); - # Try to exit with the passed through exit code (rarely used, but - # why not?) - if ($? == -1) { - say RED, "Error: ", RESET, "Failed to execute \`$command_path\`: $!"; - exit 1; - } else { - exit $? >> 8; - } +my $command_path = get_command_path($root, $subcmd, $optref); + +# Merge the options +my @allopts = ("--config", "$optref->{'config'}", @$remainref); +# Run +system($command_path, @allopts); +# Try to exit with the passed through exit code (rarely used, but why not?) +if ($? == -1) { + say RED, "Error: ", RESET, "Failed to execute \`$command_path\`: $!"; + exit 1; + } +else { + exit $? >> 8; + } } -sub get_command_path { - my ($root, $subcmd, $optref) = @_; +sub get_command_path +{ +my ($root, $subcmd, $optref) = @_; - # Load libs - loadlibs($optref); +# Load libs +loadlibs($optref); - # Check for a root-level command (in "$root/bin") - my $command_path; - if ($subcmd) { - $command_path = File::Spec->catfile($root, 'bin', $subcmd); - } else { - $command_path = File::Spec->catfile($root, 'bin', 'webmin'); - } - my $module_name; - my $command; - if ( -x $command_path) { - $command = $command_path; - } else { - # Try to extract a module name from the command - # Get list of directories - opendir (my $DIR, $root); - my @module_dirs = grep { -d "$root/$_" } readdir($DIR); - # See if any of them are a substring of $subcmd - for my $dir (@module_dirs) { - if (index($subcmd, $dir) == 0) { - $module_name = $dir; - my $barecmd = substr($subcmd, -(length($subcmd)-length($module_name)-1)); - $command = File::Spec->catfile($root, $dir, 'bin', $barecmd); - # Could be .pl or no extension - if ( -x $command ) { - last; - } elsif ( -x $command . ".pl" ) { - $command = $command . ".pl"; - last; - } - } - } - } - if ($optref->{'commands'} && - $optref->{'commands'} =~ /^(stats|status|start|stop|restart|reload|force-restart|force-reload|kill)$/) { - exit system("$0 server $optref->{'commands'}"); - } elsif ($command) { - return $command; - } else { - die RED, "Unrecognized subcommand: $subcmd", RESET , "\n"; - } +# Check for a root-level command (in "$root/bin") +my $command_path; +if ($subcmd) { + $command_path = File::Spec->catfile($root, 'bin', $subcmd); + } +else { + $command_path = File::Spec->catfile($root, 'bin', 'webmin'); + } + +my $module_name; +my $command; +if ( -x $command_path) { + $command = $command_path; + } +else { + # Try to extract a module name from the command + # Get list of directories + opendir (my $DIR, $root); + my @module_dirs = grep { -d "$root/$_" } readdir($DIR); + closedir($DIR); + # See if any of them are a substring of $subcmd + for my $dir (@module_dirs) { + if (index($subcmd, $dir) == 0) { + $module_name = $dir; + my $barecmd = substr($subcmd, -(length($subcmd)-length($module_name)-1)); + $command = File::Spec->catfile($root, $dir, 'bin', $barecmd); + # Could be .pl or no extension + if ( -x $command ) { + last; + } + elsif ( -x $command . ".pl" ) { + $command = $command . ".pl"; + last; + } + } + } + } +if ($optref->{'commands'} && + $optref->{'commands'} =~ /^(stats|status|start|stop|restart|reload|force-restart|force-reload|kill)$/) { + exit system("$0 server $optref->{'commands'}"); + } +elsif ($command) { + return $command; + } +else { + die RED, "Unrecognized subcommand: $subcmd", RESET , "\n"; + } } -sub list_commands { - my ($optref) = @_; +sub list_commands +{ +my ($optref) = @_; - my $root = root($optref->{'config'}); - my @commands; +my $root = root($optref->{'config'}); +my @commands; - # Find and list global commands - for my $command (glob ("$root/bin/*")) { - my ($bin, $path) = fileparse($command); - if ($bin =~ "webmin") { - next; - } - if ($optref->{'describe'}) { - # Display name and description - say YELLOW, "$bin", RESET; - pod2usage( -verbose => 99, - -sections => [ qw(DESCRIPTION) ], - -input => $command, - -exitval => "NOEXIT"); - } else { - if (wantarray) { - push(@commands, $bin); - } else { - # Just list the names - say "$bin"; - } - } - } +# Find and list global commands +for my $command (glob ("$root/bin/*")) { + my ($bin, $path) = fileparse($command); + if ($bin =~ "webmin") { + next; + } + if ($optref->{'describe'}) { + # Display name and description + say YELLOW, "$bin", RESET; + pod2usage( -verbose => 99, + -sections => [ qw(DESCRIPTION) ], + -input => $command, + -exitval => "NOEXIT"); + } + else { + if (wantarray) { + push(@commands, $bin); + } + else { + # Just list the names + say "$bin"; + } + } + } - my @modules; - # Find all module directories with something in bin - for my $command (glob ("$root/*/bin/*")) { - my ($bin, $path) = fileparse($command); - my $module = (split /\//, $path)[-2]; - if ($optref->{'describe'}) { - # Display name and description - say YELLOW, "$module-$bin", RESET; - pod2usage( -verbose => 99, - -sections => [ qw(DESCRIPTION) ], - -input => $command, - -exitval => "NOEXIT"); - } else { - if (wantarray) { - push(@modules, "$module-$bin"); - } else { - # Just list the names - say "$module-$bin"; - } - } - } +my @modules; +# Find all module directories with something in bin +for my $command (glob ("$root/*/bin/*")) { + my ($bin, $path) = fileparse($command); + my $module = (split /\//, $path)[-2]; + if ($optref->{'describe'}) { + # Display name and description + say YELLOW, "$module-$bin", RESET; + pod2usage( -verbose => 99, + -sections => [ qw(DESCRIPTION) ], + -input => $command, + -exitval => "NOEXIT"); + } + else { + if (wantarray) { + push(@modules, "$module-$bin"); + } + else { + # Just list the names + say "$module-$bin"; + } + } + } - if (wantarray) { - return (@commands, @modules); - } +if (wantarray) { + return (@commands, @modules); + } } # Display either a short usage message (--help) or a full manual (--man) -sub man_command { - my ($optref, $subcmd) = @_; +sub man_command +{ +my ($optref, $subcmd) = @_; - my $root = root($optref->{'config'}); - my $command_path = get_command_path($root, $subcmd, $optref); +my $root = root($optref->{'config'}); +my $command_path = get_command_path($root, $subcmd, $optref); - $ENV{'PAGER'} ||= "more"; - open(my $PAGER, "|-", "$ENV{'PAGER'}"); - if ($optref->{'help'}) { - pod2usage( -input => $command_path ); - } else { - pod2usage( -verbose => 99, - -input => $command_path, - -output => $PAGER); - } +$ENV{'PAGER'} ||= "more"; +open(my $PAGER, "|-", "$ENV{'PAGER'}"); +if ($optref->{'help'}) { + pod2usage( -input => $command_path ); + } +else { + pod2usage( -verbose => 99, + -input => $command_path, + -output => $PAGER); + } +close($PAGER); } -sub root { - my ($config) = @_; - open(my $CONF, "<", "$config/miniserv.conf") || die RED, - "Failed to open $config/miniserv.conf", RESET , "\n"; - my $root; - while (<$CONF>) { - if (/^root=(.*)/) { - $root = $1; - } - } - close($CONF); - # Does the Webmin root exist? - if ( $root ) { - die "$root is not a directory. Is --config correct?\n" unless (-d $root); - } else { - die "Unable to determine Webmin installation directory from $ENV{'WEBMIN_CONFIG'}\n"; - } +sub root +{ +my ($config) = @_; +open(my $CONF, "<", "$config/miniserv.conf") || + die RED, "Failed to open $config/miniserv.conf", RESET , "\n"; +my $root; +while (<$CONF>) { + if (/^root=(.*)/) { + $root = $1; + } + } +close($CONF); - return $root; +# Does the Webmin root exist? +if ( $root ) { + die "$root is not a directory. Is --config correct?\n" unless (-d $root); + } +else { + die "Unable to determine Webmin installation directory ". + "from $ENV{'WEBMIN_CONFIG'}\n"; + } + +return $root; } # loadlibs - Load libraries from the Webmin vendor dir # as those may not be installed as dependency, because # Webmin already provides them from package manager # perspective. -sub loadlibs { - my ($optref) = @_; - $optref->{'config'} ||= "/etc/webmin"; - my $root = root($optref->{'config'}); - my $libroot = "$root/vendor_perl"; - eval "use lib '$libroot'"; - eval "use File::Basename"; - eval "use File::Spec"; +sub loadlibs +{ +my ($optref) = @_; +$optref->{'config'} ||= "/etc/webmin"; +my $root = root($optref->{'config'}); +my $libroot = "$root/vendor_perl"; +eval "use lib '$libroot'"; +eval "use File::Basename"; +eval "use File::Spec"; } 1; @@ -453,7 +489,7 @@ Returns Webmin and other modules and themes versions installed (only those for w =head1 LICENSE AND COPYRIGHT - Copyright 2018 Jamie Cameron - Joe Cooper - Ilia Ross + Copyright 2018 Jamie Cameron + Joe Cooper + Ilia Ross From d7434c61a2eee6ec26a0d2c731db71cb37aced10 Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Sat, 28 Feb 2026 22:17:11 +0200 Subject: [PATCH 3/6] Fix to show post-save message about 2FA https://forum.virtualmin.com/t/no-qr-code-displayed-when-selectinc-totp/136703/5 --- webmin/change_twofactor.cgi | 3 ++- webmin/clear_blocked.cgi | 5 ++++- webmin/index.cgi | 4 ++++ webmin/lang/en | 4 ++-- webmin/webmin-lib.pl | 6 ++++-- 5 files changed, 16 insertions(+), 6 deletions(-) diff --git a/webmin/change_twofactor.cgi b/webmin/change_twofactor.cgi index ffc0e6192..7e9092135 100755 --- a/webmin/change_twofactor.cgi +++ b/webmin/change_twofactor.cgi @@ -32,7 +32,8 @@ $miniserv{'twofactor_provider'} = $in{'twofactor_provider'}; &put_miniserv_config(\%miniserv); &unlock_file($ENV{'MINISERV_CONFIG'}); -$msg = $text{'restart_done'}."

\n"; +$msg = ""; +$msg .= $text{'restart_done'}."

\n" if ($gconfig{'restart_async'}); if ($in{'twofactor_provider'}) { $msg .= &text('twofactor_enrolllink', "../acl/twofactor_form.cgi")."

\n"; diff --git a/webmin/clear_blocked.cgi b/webmin/clear_blocked.cgi index 081b77689..ac2d18477 100755 --- a/webmin/clear_blocked.cgi +++ b/webmin/clear_blocked.cgi @@ -3,6 +3,9 @@ require './webmin-lib.pl'; -&show_restart_page($text{'blocked_title'}, $text{'blocked_restarting'}); +&show_restart_page($text{'blocked_title'}, + $gconfig{'restart_async'} + ? $text{'blocked_restarting'} + : undef); diff --git a/webmin/index.cgi b/webmin/index.cgi index 4cea9cef0..6db8c571d 100755 --- a/webmin/index.cgi +++ b/webmin/index.cgi @@ -82,6 +82,10 @@ for(my $i=0; $i<@wlinks; $i++) { $i--; } } + +print &ui_alert_box(&filter_javascript($in{'message'}), 'success', undef, 1, + &html_escape($in{'title'})) if ($in{'message'}); + &icons_table(\@wlinks, \@wtitles, \@wicons); print &ui_hr(); diff --git a/webmin/lang/en b/webmin/lang/en index d673d88ce..84b139160 100644 --- a/webmin/lang/en +++ b/webmin/lang/en @@ -994,7 +994,7 @@ cache_enone=None selected cache_efile=Invalid filename restart_title=Restarting Webmin -restart_done=The Webmin server process is now restarting - please wait for a few seconds before continuing. +restart_done=The Webmin server process is restarting. Please wait a few seconds before continuing. mobile_title=Mobile Device Options mobile_header=Options for mobile browsers @@ -1015,7 +1015,7 @@ blocked_user=Webmin user blocked_host=Client host blocked_clear=Clear All Blocks blocked_cleardesc=Click this button to clear all current host and user blocks, by restarting the Webmin server process. -blocked_restarting=The Webmin server process is now restarting to clear blocked hosts and users - please wait for a few seconds before continuing. +blocked_restarting=The Webmin server process is restarting to clear blocked hosts and users. Please wait a few seconds before continuing. refreshmods_title=Refresh Modules refreshmods_installed=Checking for usable Webmin modules .. diff --git a/webmin/webmin-lib.pl b/webmin/webmin-lib.pl index f2db0fb8f..8660bb1a8 100755 --- a/webmin/webmin-lib.pl +++ b/webmin/webmin-lib.pl @@ -1954,12 +1954,14 @@ Output a page with header and footer about Webmin needing to restart. =cut sub show_restart_page { +my ($title, $msg) = @_; if (!$gconfig{'restart_async'}) { &restart_miniserv(); - &redirect(""); + my $msg_redir = ""; + $msg_redir = "?title=".&urlize($title)."&message=".&urlize($msg) if $msg; + &redirect($msg_redir); return; } -my ($title, $msg) = @_; $title ||= $text{'restart_title'}; $msg ||= $text{'restart_done'}; &ui_print_header(undef, $title, ""); From b4984e495d2300e60cddfb5eae80d777b2ea6234 Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Sun, 1 Mar 2026 15:15:17 +0200 Subject: [PATCH 4/6] Fix to ensure /tmp on tmpfs is displayed as separate mount --- mount/mount-lib.pl | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mount/mount-lib.pl b/mount/mount-lib.pl index 05acef62d..6a3d74a95 100755 --- a/mount/mount-lib.pl +++ b/mount/mount-lib.pl @@ -318,11 +318,12 @@ if (&has_command("zpool")) { # Add up all local filesystems foreach my $m (@mounted) { + my $tmp_is_tmpfs = $m->[0] eq "/tmp" && $m->[2] eq "tmpfs"; if ($m->[2] =~ /^ext/ || $m->[2] eq "reiserfs" || $m->[2] eq "ufs" || $m->[2] eq "f2fs" || $m->[2] eq "zfs" || $m->[2] eq "simfs" || $m->[2] eq "vzfs" || $m->[2] eq "xfs" || $m->[2] eq "jfs" || $m->[2] eq "btrfs" || - $m->[2] eq "apfs" || $m->[2] eq "fuseblk" || + $m->[2] eq "apfs" || $m->[2] eq "fuseblk" || $tmp_is_tmpfs || $m->[1] =~ /^\/dev\// || &indexof($m->[1], @$always) >= 0) { my $zp; @@ -333,13 +334,13 @@ foreach my $m (@mounted) { $zp = $zpools{$1}; } if ($donedevice{$m->[0]}++ || - $donedevice{$m->[1]}++) { + !$tmp_is_tmpfs && $donedevice{$m->[1]}++) { # Don't double-count mounts from the same device, or # on the same directory. next; } my @st = stat($m->[0]); - if (@st && $donedevno{$st[0]}++) { + if (!$tmp_is_tmpfs && @st && $donedevno{$st[0]}++) { # Don't double-count same filesystem by device number next; } From 2e4ec0367036127f661da0507b521910aeb76e77 Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Sun, 1 Mar 2026 15:27:48 +0200 Subject: [PATCH 5/6] Fix language files --- mount/lang/en | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/mount/lang/en b/mount/lang/en index f3c0adbc0..c492d84d9 100644 --- a/mount/lang/en +++ b/mount/lang/en @@ -504,11 +504,7 @@ acl_sysinfo=Show available disk data on Dashboard? sysinfo_total=Total sysinfo_dev=Device ID -<<<<<<< HEAD -sysinfo_smalltmp=Warning! The filesystem $2 which contains the Webmin temp files directory $1 has a size of only $4, which is less than the recommended minimum of $3 for temporary and backup files. Consider switching the temp files location to a different directory in the Webmin Configuration module. -sysinfo_ramtmp=Warning! The filesystem $2 which contains the Webmin temp files directory $1 is mounted from a tmpfs RAM disk, which may be too small for temporary and backup files. Consider switching the temp files location to a different directory in the Webmin Configuration module. -======= sysinfo_smalltmp=The filesystem $2 which contains the Webmin temp files directory $1 has a size of only $4, which is less than the recommended minimum of $3 for temporary and backup files. Consider switching the temp files location to a different directory in the Webmin Configuration module. ->>>>>>> 3354a0cc2fa31610b76563f3baa444766d86b7e3 +sysinfo_ramtmp=The filesystem $2 which contains the Webmin temp files directory $1 is mounted from a tmpfs RAM disk, which may be too small for temporary and backup files. Consider switching the temp files location to a different directory in the Webmin Configuration module. __norefs=1 From 5d860a6728d65c454be148bef0314060b7957b63 Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Sun, 1 Mar 2026 15:44:09 +0200 Subject: [PATCH 6/6] Fix to avoid showing a message if the user explicitly prefers tempdir --- mount/system_info.pl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mount/system_info.pl b/mount/system_info.pl index 2107be4d8..b8f63a59f 100644 --- a/mount/system_info.pl +++ b/mount/system_info.pl @@ -84,8 +84,8 @@ my @rv = ({ 'type' => 'html', }); # Check if the filesystem the Webmin temp dir is on is too small -if (&foreign_available("webmin")) { - my $tmp = $gconfig{'tempdir'} || &default_webmin_temp_dir(); +if (!$gconfig{'tempdir'} && &foreign_available("webmin")) { + my $tmp = &default_webmin_temp_dir(); my $small = 10*1024*1024; # 10 MB my $url = &get_webprefix()."/webmin/edit_advanced.cgi"; foreach my $disk (sort { length($b->{'dir'}) <=>