#!/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 Getopt::Long qw(:config no_ignore_case permute pass_through);
use Term::ANSIColor qw(color);
use Pod::Usage;

# %ansi_specs
# Map logical color names to preferred Term::ANSIColor specs and
# compatibility fallbacks for older versions.
my %ansi_specs = (
	reset         => [ 'reset' ],
	red           => [ 'red' ],
	green         => [ 'green' ],
	yellow        => [ 'yellow' ],
	cyan          => [ 'cyan' ],
	dark          => [ 'dark', 'faint' ],
	bright_red    => [ 'bright_red', 'bold red', 'red' ],
	bright_green  => [ 'bright_green', 'bold green', 'green' ],
	bright_yellow => [ 'bright_yellow', 'bold yellow', 'yellow' ],
);
my %ansi_cache;

# ansi(name)
# Resolve a named color to an ANSI escape sequence, with fallbacks
# for older Term::ANSIColor versions that may not support newer names.
sub ansi
{
my ($name) = @_;
return $ansi_cache{$name} if (exists $ansi_cache{$name});
foreach my $spec (@{ $ansi_specs{$name} || [] }) {
	my $value = eval { color($spec) };
	return $ansi_cache{$name} = $value if (defined($value));
	}
return $ansi_cache{$name} = "";
}

# print_line(line)
# Print a full line without relying on Perl's say syntax,
# which is rejected by some older Perl parsers in this script.
sub print_line
{
print @_;
print "\n";
}

# Check if root
if ($> != 0) {
	die join("",
		ansi('bright_red'), "Error: ", ansi('reset'),
		ansi('bright_yellow'), "webmin", ansi('reset'),
		" command must be run as root\n");
	exit 1;
	}

my $a0 = $ARGV[0];

# main()
# Parse global options and dispatch the requested Webmin CLI action.
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{^-}) {
			print_line("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);

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);
				}
			print_line(ansi('cyan'), "  $module_type: ",
				   ansi('reset')) if (!$head++);
			print_line("    $m->{'desc'}: ", ansi('green'),
				   $mod_ver, ansi('reset'),
				   ansi('dark'), " [$m->{'dir'}]",
				   ansi('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");
		
		# 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'}) {
				print_line("$ver$verrel");
				exit 0;
				}
			else {
				print_line(ansi('cyan'), "Webmin: ",
					   ansi('reset'),
					   ansi('green'),
					   "$ver$verrel", ansi('reset'),
					   ansi('dark'), " [$root]",
					   ansi('reset'));
				}
			}
		else {
			print_line(ansi('red'), "Error: ",
				   ansi('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);

			# 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) : "";
					$uverrel = ":@{[trim($uverrel)]}" if ($uverrel);
					$uver = trim($uver) . $uverrel;
					if ($uver) {
						print_line(ansi('cyan'),
							   "Usermin: ",
							   ansi('reset'),
							   ansi('green'),
							   $uver, ansi('reset'),
							   ansi('dark'),
							   " [$uroot]",
							   ansi('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);

								}
							}
						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;
}
exit main( \@ARGV ) if !caller(0);

# run_command(optref, subcmd, remainref)
# Run a subcommand with the resolved Webmin config and pass through
# its exit status.
sub run_command
{
my ( $optref, $subcmd, $remainref ) = @_;

# Load libs
loadlibs($optref);

# Figure out the Webmin root directory
my $root = root($optref->{'config'});

my (@commands) = list_commands($optref);
if (! grep( /^$subcmd$/, @commands ) ) {
	print_line(ansi('red'), "Error: ", ansi('reset'),
		   "Command \`$subcmd\` doesn't exist",
		   ansi('reset'));
	exit 1;
	}

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) {
	print_line(ansi('red'), "Error: ", ansi('reset'),
		   "Failed to execute \`$command_path\`: $!");
	exit 1;
	}
else {
	exit $? >> 8;
	}
}

# get_command_path(root, subcmd, optref)
# Resolve a CLI command name to an executable path under Webmin's
# global or module-specific bin directories.
sub get_command_path
{
my ($root, $subcmd, $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);
	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 join("", ansi('red'),
		 "Unrecognized subcommand: $subcmd",
		 ansi('reset'), "\n");
	}
}

# list_commands(optref)
# Enumerate available global and module CLI commands, optionally
# rendering each command's DESCRIPTION section.
sub list_commands
{
my ($optref) = @_;

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
		print_line(ansi('yellow'), "$bin", ansi('reset'));
		pod2usage( -verbose => 99,
				-sections => [ qw(DESCRIPTION) ],
				-input => $command,
				-exitval => "NOEXIT");
		}
	else {
		if (wantarray) {
			push(@commands, $bin);
			}
		else {
			# Just list the names
			print_line("$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
		print_line(ansi('yellow'), "$module-$bin",
			   ansi('reset'));
		pod2usage( -verbose => 99,
				-sections => [ qw(DESCRIPTION) ],
				-input => $command,
				-exitval => "NOEXIT");
		}
	else {
		if (wantarray) {
			push(@modules, "$module-$bin");
			}
		else {
			# Just list the names
			print_line("$module-$bin");
			}
		}
	}

if (wantarray) {
	return (@commands, @modules);
	}
}


# man_command(optref, subcmd)
# Display usage message.
sub man_command
{
my ($optref, $subcmd) = @_;

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);
	}
close($PAGER);
}

# root(config)
# Read the configured Webmin installation root from miniserv.conf.
sub root
{
my ($config) = @_;
open(my $CONF, "<", "$config/miniserv.conf") ||
	die join("", ansi('red'),
		 "Failed to open $config/miniserv.conf",
		 ansi('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";
	}

return $root;
}

# loadlibs(optref)
# Load bundled libraries from the Webmin vendor dir when they are not
# available from system package dependencies.
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;

=pod

=head1 NAME

webmin

=head1 DESCRIPTION

Webmin CLI command to perform many common Webmin tasks from the command line or from scripts.

=head1 SYNOPSIS

webmin [options] subcommand [subcommand options]

=head1 OPTIONS

=over

=item --help, -h

Print this usage summary and exit. Subcommands may also have a usage summary.

=item --config <path>, -c <path>

Specify the full path to the Webmin configuration directory. Defaults to
C</etc/webmin>.

=item --list-commands, -l

List available subcommands.

=item --describe, -d

When listing commands, briefly describe what they do.

=item --man <command>, -m <command>

Display the manual page for the given subcommand.

=item --version, -v

Returns current Webmin version installed

=item --versions

Returns Webmin and other modules and themes versions installed (only those for which version is available)

=back

=head1 EXIT CODES

0 on success ; non-0 on error

=head1 LICENSE AND COPYRIGHT

 Copyright 2018 Jamie Cameron <jamie@webmin.com>
		Joe Cooper <joe@virtualmin.com>
		Ilia Ross <ilia@virtualmin.com>

