Mini Shell
Direktori : /opt/sharedrads/ |
|
Current File : //opt/sharedrads/lil-cpanel |
#!/usr/local/cpanel/3rdparty/bin/perl
# encoding: utf-8
#
# author: Kyle Yetter
#
BEGIN { unshift @INC, '/usr/local/cpanel'; }
# since the local SSL certificate's probably self-signed, allow the query
# to skip certificate verification
$ENV{'PERL_LWP_SSL_VERIFY_HOSTNAME'} = 0;
our $VERSION = 1.4;
use strict;
use LWP::UserAgent;
use URI;
use File::Basename;
use File::Slurp qw( slurp read_dir );
use File::Spec::Functions;
use Getopt::Long;
use JSON::Syck;
use Data::Dumper;
use Term::ANSIColor;
use Pod::Usage;
use Text::Wrap;
use Cpanel::Config::LoadCpUserFile ();
sub tidy($) {
my ( $str ) = @_;
$str =~ s(^\s+\| ?)()g;
$str =~ s(\A\s+|\s+\z)()g;
return $str;
}
sub p {
print( Dumper( @_ ), "\n" );
return @_;
}
sub get_prefix($) {
my ( $name ) = @_;
my $pre = substr($name, 0, 8);
return $pre;
}
# this module's missing sometimes, so check if it's available.
# If not, the script will fail in a hard-to-figure-out way.
eval {
require LWP::Protocol::https;
} or do {
die tidy q(
| LWP::Protocol::https is not installed on this system.
| It can be installed via:
| /scripts/perlinstaller LWP::Protocol::https
);
};
####################################################################################################
##################################### Global Options and Values ####################################
####################################################################################################
# display extra debugging info if true
our $debug = 0;
# if true, don't colorize output with ANSI escape sequences
our $bleach = 0;
# set to 0 when a WHM api call fails
our $api_success = 1;
# the cPanel user name for the account we're messing with
our $user = undef;
# the basic cPanel user information for the user
our $prefix = undef;
# the basic cPanel user information for the user
our $user_data = undef;
# stores the cPanel API access key, which will be read from disk on demand
our $whm_access_key = undef;
# set to 1 if this script had to create a cPanel access hash
our $created_access_hash = 0;
our $mysql_dbs = undef;
our $mysql_users = undef;
our $addon_domains = undef;
our $parked_domains = undef;
our $subdomains = undef;
our $ftp_users = undef;
our $do_help = 0;
our $program_name = basename( $0 );
our $print_metrics = {
indent_size => 4,
right_margin => 4,
center_margin => 4,
win_width => undef,
column_width => undef,
desc_width => undef
};
END {
#
# If we had to create a temporary access hash to make this work,
# make sure it gets trashed
#
if ( -f '/root/.accesshash' && $created_access_hash ) {
unlink( '/root/.accesshash' ) or die( "failed to remove /root/.accesshash" );
}
}
####################################################################################################
##################################### Global Utility Functions #####################################
####################################################################################################
sub c($$) {
my ( $str, $style ) = @_;
$str = colored( $str, $style ) unless $bleach;
return $str;
}
sub d {
if ( $debug ) {
my ( $fmt, @params ) = @_;
local $\ = "\n";
my $message = c( sprintf( $fmt, @params ), 'cyan' );
print STDERR "DEBUG: $message";
}
}
sub fail {
my $message = sprintf( @_ );
$message = c( $message, 'red' );
print STDERR $message;
exit( 1 );
}
sub read_delimited($$) {
my $path = shift;
my $delim = shift;
my %data;
foreach ( slurp( $path ) ) {
chomp;
s(^\s+|\s+$)()g; # strip the line
next if /^\s*(?:#.*)?$/; # skip over blank and comment lines
my ( $key, $value ) = split( /$delim/, $_, 2 );
$data{$key} = $value;
}
return %data;
}
sub window_size {
my @size = ( $ENV{'COLUMNS'} || 80, $ENV{'LINES'} || 22 );
my $tiocgwinsz = 0x5413;
eval {
my $data = '';
if ( ioctl( STDERR, $tiocgwinsz, $data ) >= 0 ) {
my ( $height, $width ) = unpack( "SSSS", $data );
$size[ 1 ] = $height if $height >= 0;
$size[ 0 ] = $width if $width >= 0;
}
};
return @size;
}
sub load_json($) {
my ( $source ) = @_;
my $data;
eval { $data = JSON::Syck::Load( $source ); };
if ( $@ ) {
die( $@ . "\nJSON SOURCE:\n" . $source );
}
return $data;
}
sub quick_api($%) {
my $function = shift;
my %params = @_;
my $uri = URI->new( "https://127.0.0.1:2087/json-api/$function" );
$uri->query_form( %params );
d "query URI: %s", $uri;
my $auth = "WHM root:" . whm_access_key();
my $ua = LWP::UserAgent->new;
my $request =
HTTP::Request->new( GET => "$uri" );
$request->header( Authorization => $auth );
my $response = $ua->request( $request );
my $data = load_json( $response->content );
d "json data:\n%s", Dumper( $data ) if $debug;
# ^-- keeps the Dumper call from executing unless totally necessary
return $data;
}
sub modular_api_v1($$@) {
my $module = shift;
my $func = shift;
my @args = @_;
my %opts = (
cpanel_jsonapi_user => $user,
cpanel_jsonapi_module => $module,
cpanel_jsonapi_func => $func,
cpanel_jsonapi_apiversion => 1
);
for my $i ( 0 .. $#args ) {
$opts{ "arg-$i" } = $args[ $i ];
}
my $result = quick_api( 'cpanel', %opts );
}
sub modular_api_v2($$%) {
my $module = shift;
my $func = shift;
my %opts = (
cpanel_jsonapi_user => $user,
cpanel_jsonapi_module => $module,
cpanel_jsonapi_func => $func,
cpanel_jsonapi_apiversion => 2,
@_
);
my $result = quick_api( 'cpanel', %opts );
return $result->{ cpanelresult };
}
sub whm_access_key {
if ( $whm_access_key ) { return $whm_access_key; }
unless ( -f '/root/.accesshash' ) {
$ENV{ REMOTE_USER } = 'root';
`/usr/local/cpanel/bin/mkaccesshash`;
delete $ENV{ REMOTE_USER };
$created_access_hash = 1;
}
if ( -f '/root/.accesshash' ) {
$whm_access_key = slurp( "/root/.accesshash" ) or die "could not read /root/.accesshash: $!";
$whm_access_key =~ s/\n//g;
} else {
die "could not read /root/.accesshash: $!";
}
return $whm_access_key;
}
our $domain_rx = qr((?:(?:[a-z0-9](?:[a-z0-9\-]*[a-z0-9])?)\.)*(?:[a-z0-9](?:[a-z0-9\-]*[a-z0-9])?)(?:\.(?:m(?:[acdeghkmnpqrstvwxyz]|u(?:seum)?|o(?:bi)?|i?l)|a(?:[cdfgilmnoqtuwxz]|e(?:ro)?|r(?:pa)?|s(?:ia)?)|c(?:[cdfghiklmnruvxyz]|o(?:op|m)?|at?)|t(?:[cdfghjkmnoptvwz]|r(?:avel)?|e?l)|n(?:[cfgilopruz]|a(?:me)?|et?)|b(?:[abdefghjmnorstvwyz]|iz?)|g(?:[abdefghilmnpqrstuwy]|ov)|i(?:[delmoqrst]|n(?:fo|t)?)|p(?:[aefghklmnstwy]|ro?)|s[abcdeghijklmnortuvyz]|j(?:[emp]|o(?:bs)?)|e(?:[cegrst]|d?u)|k[eghimnprwyz]|l[abcikrstuvy]|v[aceginu]|d[ejkmoz]|f[ijkmor]|h[kmnrtu]|o(?:rg|m)|u[agksyz]|r[eosuw]|z[amw]|w[fs]|y[et]|qa))+)i;
our $ip_rx = qr(^(?:(?:2(?:[0-4]\d|5[0-5])|1\d{2}|\d{1,2})\.){3}(?:2(?:[0-4][0-9]|5[0-5])|1\d{2}|\d{1,2})$);
sub who_owns( $ ) {
my $domain = quotemeta( shift );
my $user = undef;
open( USERDOMAINS, "/etc/userdomains" ) or die "could not open /etc/userdomains: $!";
SEARCH:
while( <USERDOMAINS> ) {
if ( /^$domain: (\S+)/i ) {
$user = $1;
$prefix = get_prefix($user);
last SEARCH;
}
}
close( USERDOMAINS );
return $user;
}
sub complete_word($$) {
my $fragment = shift;
my $data = shift;
my $type = ref( $data );
my @words;
if ( $type eq 'HASH' ) {
@words = keys %$data;
} elsif ( $type eq 'ARRAY' ) {
@words = @$data;
}
my $search = quotemeta( $fragment );
my @matches = grep /^$search/i, @words;
my $found = scalar( @matches );
# ambiguous completion
if ( $found > 1 ) {
$search = lc( $fragment );
foreach ( @matches ) { $search eq lc( $_ ) and return $_; }
my @pretty = map { "`$_'" } @matches;
my $tail = pop @pretty;
my $head = join( ', ', @pretty );
my $display = $head ? "$head and $tail" : $tail;
$@ = "ambiguous completion for `$fragment': matches $display";
return;
} elsif ( $found == 1 ) {
return $matches[ 0 ];
} else {
$@ = "unknown command `$fragment'";
return;
}
}
sub set_user($) {
( $user ) = @_;
$user_data = Cpanel::Config::LoadCpUserFile::loadcpuserfile( $user );
}
sub load_mysql_data(;$) {
my ( $force_update ) = @_;
unless ( $mysql_dbs && !$force_update ) {
my %databases;
my $data = modular_api_v2( 'MysqlFE', 'listdbs' );
for my $db_info ( @{ $data->{ data } } ) {
$databases{ $db_info->{ db } } = $db_info;
}
$mysql_dbs = \%databases;
}
unless ( $mysql_users && !$force_update ) {
my %users;
my $data = modular_api_v2( 'MysqlFE', 'listusers' );
for my $user_info ( @{ $data->{ data } } ) {
$users{ $user_info->{ user } } = $user_info;
}
$mysql_users = \%users;
}
}
sub load_addon_domains(;$) {
my ( $force_update ) = @_;
unless ( $addon_domains && !$force_update ) {
my %domains;
my $data = modular_api_v2( 'AddonDomain', 'listaddondomains' );
for my $dom_info ( @{ $data->{ data } } ) {
$domains{ $dom_info->{ domain } } = $dom_info;
}
$addon_domains = \%domains;
}
}
sub load_parked_domains(;$) {
my ( $force_update ) = @_;
unless ( $parked_domains && !$force_update ) {
my %domains;
my $data = modular_api_v2( 'Park', 'listparkeddomains' );
for my $dom_info ( @{ $data->{ data } } ) {
$domains{ $dom_info->{ domain } } = $dom_info;
}
$parked_domains = \%domains;
}
}
sub load_subdomains(;$) {
my ( $force_update ) = @_;
unless ( $subdomains && !$force_update ) {
my %domains;
my $data = modular_api_v2( 'SubDomain', 'listsubdomains' );
for my $dom_info ( @{ $data->{ data } } ) {
$domains{ $dom_info->{ domain } } = $dom_info;
}
$subdomains = \%domains;
}
}
sub load_ftp_users(;$) {
my ( $force_update ) = @_;
unless ( $ftp_users && !$force_update ) {
my %users;
my $data = modular_api_v2( 'Ftp', 'listftp' );
for my $ftp_info ( @{ $data->{ data } } ) {
$users{ $ftp_info->{ user } } = $ftp_info;
}
$ftp_users = \%users;
}
}
sub is_user($) {
my ( $name ) = @_;
return( -f "/var/cpanel/users/$name" );
}
sub is_mysql_db($) {
my ( $name ) = @_;
load_mysql_data;
return $mysql_dbs->{ $name };
}
sub is_mysql_user($) {
my ( $name ) = @_;
load_mysql_data;
return $mysql_users->{ $name };
}
sub check_mysql_db_name($) {
$_ = $_[0];
my $limit = 64 - length( $prefix ) - 1;
unless ( /^[a-z0-9]+$/i ) {
die "the given database name `$_' contains invalid characters (should only be letters and numbers)";
}
if ( length > $limit ) {
die "the given database name `$_' is too long -- must be less than $limit characters in length";
}
}
sub check_mysql_user_name($) {
$_ = $_[0];
my $limit = 16 - length( $prefix ) - 1;
unless ( /^[a-z0-9]+$/i ) {
die "the given MySQL user name `$_' contains invalid characters (should only be letters and numbers)";
}
if ( length > $limit ) {
die "the given MySQL user name `$_' is too long -- must be less than $limit characters in length";
}
}
####################################################################################################
########################################### class Command ##########################################
####################################################################################################
{
package Command;
use Class::Struct;
struct(
'Command', [
'group' => '$',
'name' => '$',
'desc' => '$',
'arguments' => '%',
'api_version' => '$',
'api_module' => '$',
'api_func' => '$',
'execute' => '$',
'reporter' => '$',
'argument_handler' => '$',
'depth' => '$',
'parent' => '$',
'options' => '%',
'result' => '$',
'success' => '$',
'message' => '$'
]
);
*window_size = *main::window_size;
*c = *main::c;
*load_json = *main::load_json;
*is_user = *main::is_user;
*whm_access_key = *main::whm_access_key;
sub bind($@) {
my $self = shift;
$self->clear;
my @argv = $self->prepare_arguments( @_ );
# since values may get bound by hook methods and options,
# focus only on unbound arguments
my @params = grep { not $_->bound } values %{ $self->arguments };
@params = sort { $a->position <=> $b->position } @params;
my $required_arity = 0;
for ( @params ) { $required_arity += 1 if $_->required; }
if ( @argv < $required_arity ) {
$self->help( "too few arguments given" );
} elsif ( @argv > @params ) {
$self->help( "too many arguments given" );
}
my $optional_slots = @argv - $required_arity;
if ( @argv ) {
BINDING:
for my $i ( 0 .. $#argv ) {
my $param = $params[ $i ];
my $value = $argv[ $i ];
unless ( $param->required ) {
if ( $optional_slots > 0 ) {
$optional_slots -= 1;
} else {
next BINDING;
}
}
$param->bind( $value ) if defined( $value );
}
}
return $self;
}
sub prepare_arguments($@) {
my $self = shift;
my @argv = @_;
my $handler = $self->argument_handler;
return $handler ? $handler->( $self, @argv ) : @argv;
}
sub clear($) {
my $self = shift;
$_->clear for values %{ $self->arguments };
return $self;
}
sub uri($) {
my ( $self ) = @_;
my $v = $self->api_version;
my $uri = URI->new( "https://127.0.0.1:2087/json-api/cpanel" );
my %params = (
cpanel_jsonapi_user => $user,
cpanel_jsonapi_module => $self->api_module,
cpanel_jsonapi_func => $self->api_func,
cpanel_jsonapi_apiversion => $self->api_version
);
my @arg_spec = values %{ $self->arguments };
foreach my $spec ( @arg_spec ) {
my $v = $spec->value;
if ( defined $v ) {
$params{ $spec->query_key } = $v;
}
}
$uri->query_form( %params );
return $uri;
}
sub run {
my $self = shift;
my @argv = @_;
if ( $argv[ 0 ] eq 'help' ) {
$do_help = 1;
shift( @argv );
}
if ( $do_help ) {
$self->help( 0 );
}
unless ( $user ) {
if ( is_user( $argv[ 0 ] ) ) {
set_user( shift( @argv ) );
} else {
die "no valid user name provided for this command";
}
}
my $execute = $self->execute;
if ( $execute ) {
return $execute->( $self, @argv );
} else {
my $success = $self->api_call( @argv );
$self->report;
return $success;
}
}
sub report($) {
my ( $self ) = @_;
my $reporter = $self->reporter;
$reporter ? $reporter->( $self ) : $self->default_report;
}
sub default_report($$) {
my ( $self, $result ) = @_;
print c( $self->message, $self->success ? 'green' : 'red' );
}
sub api_call($@) {
my $self = shift;
my @argv = @_;
$self->bind( @argv );
my $uri = $self->uri;
my $auth = "WHM root:" . whm_access_key();
my $ua = LWP::UserAgent->new;
my $request =
HTTP::Request->new( GET => "$uri" );
$request->header( Authorization => $auth );
my $response = $ua->request( $request );
my $data = load_json( $response->content );
my $success = 1;
my $message = 'OK';
if ( $self->api_version == 2 ) {
if ( exists $data->{ cpanelresult } ) {
$data = $data->{ cpanelresult };
}
}
if ( exists $data->{ error } ) {
$success = 0;
$message = $data->{ error };
} elsif ( exists $data->{ data } ) {
my $content = $data->{ data };
if ( ref( $content ) eq 'ARRAY' ) {
$content = $content->[ 0 ];
}
if ( ref( $content ) eq 'HASH' ) {
if ( exists $content->{ reason } ) {
$success = $content->{ result };
$message = $content->{ reason };
} elsif ( exists $content->{ result } ) {
$message = $content->{ result };
}
}
}
$self->success( $success );
$self->message( $message );
$self->result( $data );
return $success;
}
sub argument($$$%) {
my $self = shift;
my $name = shift;
my $desc = shift;
my %options = @_;
my $pos = scalar keys %{ $self->arguments };
my $validate = $options{ validate };
my $cast = $options{ cast };
my $default = $options{ default };
my $required = !exists $options{ default };
my $key = $options{ key } || $name;
if ( $self->api_version == 1 ) {
$key = "arg-$pos";
}
if ( exists $options{ required } ) {
$required = $options{ required };
}
my $arg = Command::Argument->new();
$arg->command( $self );
$arg->name( $name );
$arg->desc( $desc );
$arg->position( $pos );
$arg->required( $required );
$arg->default_value( $default );
$arg->validate( $validate );
$arg->cast( $cast );
$arg->query_key( $key );
$self->arguments( $name, $arg );
return $arg;
}
sub option($@) {
my ( $self, @option_params ) = @_;
my $option = Command::Option->new();
$option->configure( @option_params );
$self->options( $option->name, $option );
return $option;
}
sub argument_list($) {
my ( $self ) = @_;
return( sort { $a->position <=> $b->position } values %{ $self->arguments } );
}
sub help($;$) {
my $self = shift;
my $parent = $self->parent;
my $error_message = shift;
my $exit_status = 0;
if ( $error_message ) {
print c( "ERROR: $error_message", 'red' ), "\n";
$exit_status = 1;
}
$self->compute_print_metrics;
print "USAGE: ", $self->usage_summary, "\n\n";
my $win_width = $print_metrics->{ win_width };
my $indent_size = $print_metrics->{ indent_size };
my $right_margin = $print_metrics->{ right_margin };
my $indent = ' ' x $indent_size;
my @desc_lines = wrap( $self->desc, $win_width - $indent_size - $right_margin );
print( $indent, $_, "\n" ) for @desc_lines;
print "\n";
if ( %{ $self->arguments } ) {
print "ARGUMENTS:\n";
$self->print_arguments;
print "\n";
} else {
print "ARGUMENTS: (none)\n\n";
}
my $takes_options = 0;
OPT_SEARCH:
for ( my $node = $self; $node; $node = $node->parent ) {
if ( %{ $node->options } ) {
$takes_options = 1;
last OPT_SEARCH;
}
}
if ( $takes_options ) {
print "OPTIONS:\n";
$self->print_options;
}
exit( $exit_status );
}
sub print_arguments($) {
my $self = shift;
my $column_width = $print_metrics->{ column_width };
my $desc_width = $print_metrics->{ desc_width };
my $right_margin = $print_metrics->{ right_margin };
my $center_margin = $print_metrics->{ center_margin };
my $indent_size = $print_metrics->{ indent_size };
my $arg_width = $column_width - $indent_size;
my $mask = "%-${arg_width}s";
my $indent = ' ' x $indent_size;
my $fill = ' ' x $arg_width;
my $center_fill = ' ' x $center_margin;
for my $arg ( $self->argument_list ) {
my $aname = sprintf( $mask, $arg->name );
my ( $first, @lines ) = wrap( $arg->desc, $desc_width );
print( $indent, c( $aname, 'magenta' ), $center_fill, $first, "\n" );
print( $indent, $fill, $center_fill, $_, "\n" ) for @lines;
}
}
sub print_options($) {
my ( $self ) = @_;
my @opts = values %{ $self->options };
if ( @opts ) {
my $depth = $self->depth;
my $name = ucfirst( $self->name || "global" );
my $column_width = $print_metrics->{ column_width };
my $desc_width = $print_metrics->{ desc_width };
my $right_margin = $print_metrics->{ right_margin };
my $center_margin = $print_metrics->{ center_margin };
my $indent_size = $print_metrics->{ indent_size };
my $option_width = $column_width - ( 2 * $indent_size );
my $indent_space = ' ' x $indent_size;
my $center_fill = ' ' x $center_margin;
my $filler = ( ' ' x $option_width );
print( $indent_space, "$name Options:\n" );
my $name_mask = "%-${option_width}s";
@opts = sort { $a->name cmp $b-> name } @opts;
foreach my $o ( @opts ) {
my $oname = sprintf( $name_mask, $o->summary );
my ( $first, @lines ) = wrap( $o->desc, $desc_width );
print( $indent_space, $indent_space, $oname, $center_fill, $first, "\n" );
print( $indent_space, $indent_space, $filler, $center_fill, $_, "\n" ) for @lines;
}
}
my $parent = $self->parent;
$parent->print_options() if $parent;
return $self;
}
sub get_value($$) {
my ( $self, $arg_name ) = @_;
my $arg = $self->arguments( $arg_name ) or die "invalid argument name `$arg_name'";
return $arg->value;
}
sub set_value($$$) {
my ( $self, $arg_name, $value ) = @_;
my $arg = $self->arguments( $arg_name ) or die "invalid argument name `$arg_name'";
$arg->bind( $value );
return $arg;
}
sub parse_api_spec($$) {
my ( $self, $spec ) = @_;
$_ = $spec;
if ( /^\s*v?([12])\s*(\S+)::(\S+)$/ ) {
my ( $v, $m, $f ) = ( $1, $2, $3 );
$self->api_version( $v + 0 );
$self->api_module( $m );
$self->api_func( $f );
} else {
die "bad api spec given: `$_'";
}
return $self;
}
sub usage_summary($) {
my ( $self ) = @_;
my $action = $self;
my @trail;
do {
unshift( @trail, $action->name || $program_name );
} while ( $action = $action->parent );
for my $arg ( $self->argument_list ) {
my $arg_name = c( $arg->name, 'underline' );
push( @trail, $arg->required ? $arg_name : "[$arg_name]" );
}
return join( ' ', @trail );
}
sub command_width($) {
my ( $self ) = @_;
my $depth = $self->depth;
return( ( $depth * $print_metrics->{ indent_size } ) + length( $self->name ) );
}
sub argument_width($) {
my ( $self ) = @_;
my @args = $self->argument_list;
my $width = 0;
my $indent_size = $print_metrics->{ indent_size };
for my $arg ( @args ) {
my $arg_width = length( $arg->name ) + $indent_size;
$width = $arg_width if $arg_width > $width;
}
return $width;
}
sub option_width($) {
my ( $self ) = @_;
my $parent = $self->parent;
my $width = $parent ? $parent->option_width : 0;
my @opts = values %{ $self->options };
for my $opt ( @opts ) {
my $margin = $print_metrics->{ indent_size } * 2;
my $summary = $opt->summary;
my $w = $margin + length( $summary );
$width = $w if $w > $width;
}
return $width;
}
sub wrap($$) {
my ( $text, $width ) = @_;
$width < 15 and $width = 15;
$Text::Wrap::columns = $width;
$text = Text::Wrap::wrap( '', '', $text );
return( split( /\r?\n/, $text ) );
}
sub compute_print_metrics($) {
my ( $self ) = @_;
my ( $win_width, undef ) = window_size();
my $right_margin = $print_metrics->{ right_margin };
my $center_margin = $print_metrics->{ center_margin };
my $arg_width = $self->argument_width;
my $option_width = $self->option_width;
my $column_width = $arg_width > $option_width ? $arg_width : $option_width;
my $desc_width = $win_width - $right_margin - $center_margin - $column_width;
$desc_width < 15 and $desc_width = 15;
$print_metrics->{ column_width } = $column_width;
$print_metrics->{ desc_width } = $desc_width;
$print_metrics->{ win_width } = $win_width;
return $print_metrics;
}
####################################################################################################
###################################### class Command::Argument #####################################
####################################################################################################
package Command::Argument;
use Class::Struct;
struct(
'Command::Argument', [
'command' => '$',
'position' => '$',
'name' => '$',
'required' => '$',
'desc' => '$',
'default_value' => '$',
'validate' => '$',
'cast' => '$',
'bound_value' => '$',
'bound' => '$',
'query_key' => '$'
]
);
sub bind($$) {
my ( $self, $given_value ) = @_;
my $value = $self->cast ? $self->cast->( $given_value ) : $given_value;
$self->validate and $self->validate->( $value );
$self->bound_value( $value );
$self->bound( 1 );
return $self;
}
sub clear($) {
my ( $self ) = @_;
$self->bound_value( '' );
$self->bound( 0 );
return $self;
}
sub value($) {
my ( $self ) = @_;
my ( $default, $name );
return $self->bound_value if $self->bound;
if ( $self->required ) {
$name = $self->name;
die "no value given for required argument `$name'";
}
$default = $self->default_value;
if ( ref( $default ) eq 'CODE' ) {
my $cmd = $self->command;
return $default->( $cmd, $self );
}
return $default;
}
####################################################################################################
#################################### class Command::Dispatcher #####################################
####################################################################################################
package Command::Dispatcher;
use Class::Struct;
struct(
'Command::Dispatcher', [
'name' => '$',
'actions' => '%',
'depth' => '$',
'parent' => '$',
'options' => '%'
]
);
*complete_word = *main::complete_word;
*window_size = *main::window_size;
*c = *main::c;
sub command($$$$%) {
my $self = shift;
my $name = shift;
my $desc = shift;
my $spec = shift;
my %options = @_;
my $execute = $options{ execute };
my $command = $self->add_child( $name, Command->new() );
$command->desc( $desc );
$command->parse_api_spec( $spec );
if ( %options ) {
for my $prop ( qw( execute reporter argument_handler ) ) {
$command->$prop( $options{ $prop } ) if exists $options{ $prop };
}
}
return $command
}
sub group($$%) {
my $self = shift;
my $name = shift;
my $group = Command::Dispatcher->new;
return $self->add_child( $name, $group );
}
sub option($@) {
my ( $self, @option_params ) = @_;
my $option = Command::Option->new();
$option->configure( @option_params );
$self->options( $option->name, $option );
return $option;
}
sub add_child($$$) {
my ( $self, $name, $child ) = @_;
my $depth;
unless ( $depth = $self->depth ) {
$self->depth( 0 );
$depth = 0;
}
$child->name( $name );
$child->depth( $depth + 1 );
$child->parent( $self );
$self->actions( $name, $child );
return $child;
}
sub run {
my $self = shift;
my @argv = @_;
@argv = $self->parse_options( @argv );
my $name = shift( @argv );
if ( $name eq 'help' ) {
$do_help = 1;
$name = shift( @argv );
}
my $action;
unless ( $name ) {
if ( $do_help ) {
$self->help( 0 );
} else {
$self->help( "no command name given" );
}
}
unless ( $action = $self->actions( $name ) ) {
my $match = complete_word( $name, $self->actions ) or
$self->help( $@ );
$action = $self->actions( $match );
}
return $action->run( @argv );
}
sub parse_options($@) {
my ( $self, @argv ) = @_;
my @specs = values %{ $self->options };
my %option_spec;
foreach my $o ( @specs ) {
$option_spec{ $o->to_spec } = $o->target;
}
if ( %option_spec ) {
Getopt::Long::ConfigDefaults();
Getopt::Long::Configure( 'bundling', 'pass_through' );
Getopt::Long::GetOptionsFromArray( \@argv, %option_spec );
}
return @argv;
}
sub help($;$) {
my $self = shift;
my $error_message = shift;
my $exit_status = 0;
if ( $error_message ) {
print c( "ERROR: $error_message", 'red' ), "\n";
$exit_status = 1;
}
$self->compute_print_metrics;
print "USAGE: ", $self->usage_summary, "\n\n";
print "ACTIONS:\n";
$self->print_commands();
my $takes_options = 0;
OPT_SEARCH:
for ( my $node = $self; $node; $node = $node->parent ) {
if ( %{ $node->options } ) {
$takes_options = 1;
last OPT_SEARCH;
}
}
if ( $takes_options ) {
print "OPTIONS:\n";
$self->print_options;
}
exit( $exit_status );
}
sub compute_print_metrics($) {
my ( $self ) = @_;
my ( $win_width, undef ) = window_size();
my $right_margin = $print_metrics->{ right_margin };
my $center_margin = $print_metrics->{ center_margin };
my $command_width = $self->command_width;
my $option_width = $self->option_width;
my $column_width = $command_width > $option_width ? $command_width : $option_width;
my $desc_width = $win_width - $right_margin - $center_margin - $column_width;
$desc_width < 15 and $desc_width = 15;
$print_metrics->{ column_width } = $column_width;
$print_metrics->{ desc_width } = $desc_width;
$print_metrics->{ win_width } = $win_width;
return $print_metrics;
}
sub print_commands($) {
my $self = shift;
my $depth = $self->depth;
my $name = $self->name;
my $column_width = $print_metrics->{ column_width };
my $desc_width = $print_metrics->{ desc_width };
my $right_margin = $print_metrics->{ right_margin };
my $center_margin = $print_metrics->{ center_margin };
my $indent_size = $print_metrics->{ indent_size };
my $command_width = $column_width - ( ( $depth + 1 ) * $indent_size );
my @children = values %{ $self->actions };
my @groups;
my @commands;
for my $child ( @children ) {
if ( ref( $child ) eq 'Command' ) {
push( @commands, $child );
} else {
push( @groups, $child );
}
}
@groups = sort { $a->name cmp $b->name } @groups;
@commands = sort { $a->name cmp $b->name } @commands;
for my $group ( @groups ) {
my $group_name = $group->name;
print( ( ' ' x ( ( $depth + 1 ) * $indent_size ) ) . c( $group_name, 'yellow' ) . "\n" );
$group->print_commands();
}
my $indent_space = ( ' ' x ( ( $depth + 1 ) * $indent_size ) );
my $center_space = ( ' ' x $center_margin );
my $filler = ( ' ' x $command_width );
$Text::Wrap::columns = $desc_width;
for my $command ( @commands ) {
my $cname = $command->name;
my $cdesc = $command->desc;
$cname = sprintf( "%-${command_width}s", $cname );
$cdesc = Text::Wrap::wrap( '', '', $cdesc );
my ( $first, @lines ) = split( /\r?\n/, $cdesc );
print( $indent_space, c( $cname, 'cyan' ), $center_space, $first, "\n" );
print( $indent_space, $filler, $center_space, $_, "\n" ) for @lines;
}
}
sub print_options($) {
my ( $self ) = @_;
my @opts = values %{ $self->options };
if ( @opts ) {
my $depth = $self->depth;
my $name = ucfirst( $self->name || "global" );
my $column_width = $print_metrics->{ column_width };
my $desc_width = $print_metrics->{ desc_width };
my $right_margin = $print_metrics->{ right_margin };
my $center_margin = $print_metrics->{ center_margin };
my $indent_size = $print_metrics->{ indent_size };
my $option_width = $column_width - ( 2 * $indent_size );
my $indent_space = ' ' x $indent_size;
my $center_fill = ' ' x $center_margin;
my $filler = ( ' ' x $option_width );
print( $indent_space, "$name Options:\n" );
$Text::Wrap::columns = $desc_width;
my $name_mask = "%-${option_width}s";
@opts = sort { $a->name cmp $b-> name } @opts;
foreach my $o ( @opts ) {
my $oname = sprintf( $name_mask, $o->summary );
my $odesc = Text::Wrap::wrap( '', '', $o->desc );
my ( $first, @lines ) = split( /\r?\n/, $odesc );
print( $indent_space, $indent_space, $oname, $center_fill, $first, "\n" );
print( $indent_space, $indent_space, $filler, $center_fill, $_, "\n" ) for @lines;
}
}
my $parent = $self->parent;
$parent->print_options() if $parent;
return $self;
}
sub command_width($) {
my ( $self ) = @_;
my @children = values %{ $self->actions };
my $width = 0;
for my $child ( @children ) {
my $child_width = $child->command_width();
$width = $child_width if $child_width > $width;
}
return $width;
}
sub option_width($) {
my ( $self ) = @_;
my $parent = $self->parent;
my $width = $parent ? $parent->option_width : 0;
my @opts = values %{ $self->options };
for my $opt ( @opts ) {
my $margin = $print_metrics->{ indent_size } * 2;
my $summary = $opt->summary;
my $w = $margin + length( $summary );
$width = $w if $w > $width;
}
return $width;
}
sub usage_summary($) {
my ( $self ) = @_;
my $action = $self;
my @trail;
do {
unshift( @trail, $action->name || "$program_name USER" );
} while ( $action = $action->parent );
return join( ' ', @trail ) . ' ACTION *ARGS*';
}
####################################################################################################
##################################### class Command::Option ########################################
####################################################################################################
package Command::Option;
use Class::Struct;
struct(
'Command::Option', [
'name' => '$',
'short' => '$',
'desc' => '$',
'type' => '$',
'arg_name' => '$',
'target' => '$'
]
);
sub configure($@) {
my ( $self, @argv ) = @_;
my $target = pop( @argv );
$self->target( $target );
for ( @argv ) {
if ( /^[a-z\-]$/i ) {
$self->short( $_ );
} elsif ( /^[a-z\-]+$/i ) {
$self->name( $_ );
} elsif ( /^([=:][a-z])\s+(\S+)/i ) {
my ( $type, $arg_name ) = ( $1, $2 );
$self->type( $type );
$self->arg_name( $arg_name );
} elsif ( /\s/ ) {
$self->desc( $_ );
} else {
die "don't know how to handle option spec argument: `$_'";
}
}
}
sub summary($) {
my ( $self ) = @_;
my @parts;
my $short = $self->short();
my $long = $self->name();
my $type = $self->type();
my $arg_name = $self->arg_name();
push( @parts, "-$short" ) if $short;
if ( $type ) {
if ( $type =~ /^=/ ) {
push( @parts, "--$long=$arg_name" );
} else {
push( @parts, "--${long}[=$arg_name]" );
}
} else {
push( @parts, "--$long" );
}
return join( ', ', @parts );
}
sub to_spec($) {
my ( $self ) = @_;
my @parts = ();
my $short = $self->short();
my $long = $self->name();
my $type = $self->type();
push( @parts, $long ) if $long;
push( @parts, $short ) if $short;
my $spec = join( '|', @parts );
$spec .= $type if $type;
return $spec;
}
}
our $main = Command::Dispatcher->new;
#####################################################################################################
########################################### MySQL Functions ########################################
####################################################################################################
sub strip_user($) {
my ( $name ) = @_;
$name =~ s(^\Q${user}_\E)();
return $name;
}
sub prefix_user($) {
my ( $name ) = @_;
$name =~ s(^\Q${prefix}_\E)();
return $name;
}
##################
my $cmd;
my $mysql = $main->group( 'mysql' );
$cmd =
$mysql->command(
'create',
'create a new MySQL database on the user\'s account',
'v1 Mysql::adddb'
);
$cmd->argument(
'db_name',
"the name of the database (without the `userna5_' prefix)",
cast => \&strip_user,
validate => sub {
my ( $db_name ) = @_;
my $full_name = "${prefix}_$db_name";
check_mysql_db_name( $db_name );
if ( is_mysql_db( $full_name ) ) {
die "a database named `$full_name' already exists";
}
}
);
##################
$cmd =
$mysql->command(
'create-user',
'create a new MySQL user on the user\'s account',
'v1 Mysql::adduser'
);
$cmd->argument(
'mysql_user',
"the new MySQL user name (without the `userna5_' prefix)",
cast => \&strip_user,
validate => sub {
my ( $user_name ) = @_;
my $full_name = "${prefix}_$user_name";
check_mysql_user_name( $user_name );
if ( is_mysql_user( $full_name ) ) { die "a MySQL user named `$full_name' already exists"; }
}
);
$cmd->argument( 'password', "the new user's password" );
##################
$cmd =
$mysql->command(
'delete',
'remove a database defined on the user\'s account',
'v1 Mysql::deldb'
);
$cmd->argument(
'db_name',
"the name of the database (without the `userna5_' prefix)",
cast => \&prefix_user,
validate => sub {
my ( $db_name ) = @_;
unless ( is_mysql_db( $db_name ) ) {
die "no database named `$db_name' appears to exist";
}
}
);
##################
$cmd =
$mysql->command(
'delete-user',
'remove a database user defined on the user\'s account',
'v1 Mysql::deluser'
);
$cmd->argument(
'mysql_user',
"the name of the MySQL user (without the `userna5_' prefix)",
cast => \&prefix_user
);
##################
$cmd =
$mysql->command(
'associate',
'add an existing mysql user to a given database',
'v1 Mysql::adduserdb'
);
$cmd->argument(
'db_name',
"the name of the database (without the `userna5_' prefix)",
cast => \&prefix_user
);
$cmd->argument(
'mysql_user',
"the name of the MySQL user (without the `userna5_' prefix)",
cast => \&prefix_user
);
$cmd->argument(
'permissions',
"a space-separated list of permissions to grant (`all' if none given)",
default => 'all'
);
###############
my $mlist = $mysql->group( 'list' );
$mlist->command(
'dbs',
"list the databases owned by the user",
"v2 MysqlFE::listdbs",
execute => sub {
load_mysql_data;
my @names = sort keys %{ $mysql_dbs };
print "$_\n" for @names;
exit( 0 );
}
);
################
$mlist->command(
'users',
"list the MySQL users associated with the user",
"v2 MysqlFE::listusers",
execute => sub {
load_mysql_data;
my @names = sort keys %{ $mysql_users };
print "$_\n" for @names;
exit( 0 );
}
);
####################################################################################################
######################################### E-Mail Functions #########################################
####################################################################################################
sub process_mail_arguments {
my ( $cmd, @args ) = @_;
if ( $args[ 0 ] =~ /^([^@\s]+)@([^@\s]+)$/ ) {
my $email_user = $1;
my $domain = $2;
shift( @args );
$cmd->set_value( 'email', $email_user );
$cmd->set_value( 'domain', $domain );
}
return @args;
}
my $mail = $main->group( 'mail' );
$cmd =
$mail->command(
"create",
"create a new e-mail address",
"v2 Email::addpop",
argument_handler => \&process_mail_arguments
);
$cmd->argument(
"email",
"the user name part of the e-mail address"
);
$cmd->argument(
"password",
"the password for the e-mail address"
);
$cmd->argument(
"quota",
"the address' disk quota in MB (0 for unlimited)",
default => 0
);
$cmd->argument(
"domain",
"the domain name of the e-mail address"
);
#################################################
$cmd =
$mail->command(
"delete",
"delete an existing e-mail address",
"v2 Email::delpop",
argument_handler => \&process_mail_arguments
);
$cmd->argument(
"email",
"the user name part of the e-mail address"
);
$cmd->argument(
"domain",
"the domain name of the e-mail address"
);
#######################
$cmd =
$mail->command(
"quota",
"modify an existing e-mail's quota settings",
"v2 Email::editquota",
argument_handler => \&process_mail_arguments
);
$cmd->argument(
"email",
"the user name part of the e-mail address"
);
$cmd->argument(
"domain",
"the domain name of the e-mail address"
);
$cmd->argument(
"quota",
"the address' disk quota in MB (0 for unlimited)"
);
#######################
$cmd =
$mail->command(
"password",
"change an e-mail account's password",
"v2 Email::passwdpop",
argument_handler => \&process_mail_arguments
);
$cmd->argument(
"email",
"the user name part of the e-mail address"
);
$cmd->argument(
"domain",
"the domain name of the e-mail address"
);
$cmd->argument(
"password",
"the new password for the account"
);
###
my $elist = $mail->group( "list" );
$elist->command(
"domains",
"list domains that send and receive mail",
"v2 Email::listmaildomains",
reporter => sub {
my ( $cmd ) = @_;
my $data = $cmd->result->{ data };
for my $dom_info ( @$data ) {
my $dom = $dom_info->{ domain };
print "$dom\n";
}
}
);
$elist->command(
"accounts",
"list e-mail addresses established across the user's domains",
"v2 Email::listpopssingle",
reporter => sub {
my ( $cmd ) = @_;
my $data = $cmd->result->{ data };
for my $mail_info ( @$data ) {
my $mail = $mail_info->{ email };
print "$mail\n";
}
}
);
$cmd =
$elist->command(
"forwarders",
"list e-mail forwarders set up by the user",
"v2 Email::listforwards",
reporter => sub {
my ( $cmd ) = @_;
my $data = $cmd->result->{ data };
for my $finfo ( @$data ) {
my $from = $finfo->{ forward };
my $to = $finfo->{ dest };
print "$from => $to\n";
}
}
);
$cmd->argument(
'domain',
"only list forwarders for this domain",
default => undef
);
####################################################################################################
############################################### Spam ###############################################
####################################################################################################
my $spam = $main->group( 'spam' );
$spam->command(
'enable',
'turn on Spam Assassin',
'v1 Email::addspam'
);
################
$spam->command(
'disable',
'turn off Spam Assassin',
'v1 Email::delspam'
);
################
$cmd =
$spam->command(
'set-score',
'set the threshold score for Spam Assassin',
'v1 Email::addspamfilter'
);
$cmd->argument(
'score',
"the new score value"
);
####################################################################################################
########################################## Add-On Domains ##########################################
####################################################################################################
my $addon = $main->group( 'addon' );
$cmd =
$addon->command(
'create',
"create an add-on domain on the user's account",
'v2 AddonDomain::addaddondomain'
);
$cmd->argument(
'domain',
"the domain to set up",
key => 'newdomain',
validate => sub {
my ( $domain ) = @_;
unless ( $domain =~ /$domain_rx/i ) {
die "`$domain' does not look like a valid domain name";
}
my $owner = who_owns( $domain );
if ( $owner ) {
die "`$domain' is already owned by $owner";
}
}
);
$cmd->argument(
'sub_name',
"the name to use as the subdomain / ftp user",
key => 'subdomain',
default => sub {
my ( $c ) = @_;
my $dom = $c->get_value( 'domain' );
my ( $sub, undef ) = split( /\./, $dom, 2 );
return $sub;
}
);
$cmd->argument(
'doc_root',
"the document root for the domain",
key => 'dir',
default => sub {
my ( $c ) = @_;
my $dom = $c->get_value( 'domain' );
return "public_html/$dom";
}
);
################
$cmd =
$addon->command(
'delete',
"remove an add-on domain configured on the user's account",
'v2 AddonDomain::deladdondomain'
);
$cmd->argument(
'domain',
"the name of the add-on domain"
);
$cmd->argument(
'domain_key',
"a value containing the user name and main domain name (you should probably let me figure it out for you)",
key => 'subdomain',
default => sub {
my ( $c ) = @_;
my $dom = $c->get_value( 'domain' );
load_addon_domains;
return $addon_domains->{ $dom }->{ domainkey };
}
);
$cmd =
$addon->command(
'list',
"list all add-on domains belonging to a suer",
'v2 AddonDomain::listaddondomains',
execute => sub {
load_addon_domains;
my @names = sort keys %{ $addon_domains };
print "$_\n" for @names;
exit( 0 );
}
);
####################################################################################################
########################################## Parked Domains ##########################################
####################################################################################################
my $park = $main->group( 'park' );
$cmd =
$park->command(
'create',
"park a domain against the user's primary domain",
'v2 Park::park'
);
$cmd->argument(
'domain',
"the domain name to park"
);
$cmd =
$park->command(
'delete',
"remove a parked domain from the user's account",
'v2 Park::unpark'
);
$cmd->argument(
'domain',
"the domain name to remove"
);
$cmd =
$park->command(
'list',
"list a user's parked domains",
'v2 Park::listparkeddomains',
execute => sub {
load_parked_domains;
my @names = sort keys %{ $parked_domains };
print "$_\n" for @names;
exit( 0 );
}
);
####################################################################################################
############################################ Subdomains ############################################
####################################################################################################
sub process_subdomain_arguments {
my ( $cmd, @args ) = @_;
if ( $args[ 0 ] =~ /^([^\.]+)\.(.*)$/ ) {
my $sub_name = $1;
my $base_domain = $2;
shift( @args );
$cmd->set_value( 'domain', $sub_name );
$cmd->set_value( 'base_domain', $base_domain );
}
return @args;
}
my $subdomain = $main->group( 'subdomain' );
$cmd =
$subdomain->command(
'list',
"list subdomains owned by the user",
'v2 SubDomain::listsubdomains',
execute => sub {
load_subdomains;
my @names = sort keys %{ $subdomains };
print "$_\n" for @names;
exit( 0 );
}
);
################
$cmd =
$subdomain->command(
'create',
"add a new subdomain to the user's account",
'v2 SubDomain::addsubdomain',
argument_handler => \&process_subdomain_arguments
);
$cmd->argument(
'domain',
"the domain name to park"
);
$cmd->argument(
'document_root',
"the path of the directory to serve as the document root of the domain",
key => 'dir',
default => sub {
my ( $c ) = @_;
return( catfile( 'public_html', $c->get_value( 'domain' ) ) );
}
);
$cmd->argument(
'no_dot',
"automatically strip dots from the `domain' parameter; should be `0' or `1'",
default => '0'
);
$cmd->argument(
'base_domain',
"the domain on which the subdomain is based",
key => 'rootdomain',
default => sub {
return $user_data->{ DOMAIN };
},
validate => sub {
my ( $domain ) = @_;
if ( $domain eq $user_data->{ DOMAIN } ) { return; }
for ( @{ $user_data->{ DOMAINS } } ) {
if ( $_ eq $domain ) { return; }
}
die "`$domain' is not a domain belonging to `$user'";
}
);
################
$cmd =
$subdomain->command(
'delete',
"remove an existing subdomain from the user's account",
'v2 SubDomain::delsubdomain'
);
$cmd->argument(
'domain',
'the name of the subdomain to remove'
);
####################################################################################################
################################################ FTP ###############################################
####################################################################################################
my $ftp = $main->group( 'ftp' );
$cmd =
$ftp->command(
'create',
"set up a new FTP account for the user",
'v2 Ftp::addftp'
);
$cmd->argument(
'user_name',
"the new user name (without the \@domain.com part)",
key => 'user'
);
$cmd->argument(
'password',
"the password for the new account",
key => 'pass'
);
$cmd->argument(
'root_directory',
"the base directory of the new FTP account -- must be relative to public_html",
default => '.',
key => 'homedir'
);
$cmd->argument(
'quota',
"an optional quota number to place on the account (0 for none)",
default => '0'
);
########
$cmd =
$ftp->command(
'delete',
"remove an existing FTP account",
'v2 Ftp::delftp'
);
$cmd->argument(
'user_name',
"the FTP user to remove (without the \@domain.com)",
key => 'user'
);
$cmd->argument(
'destroy',
"Boolean, 1=True, 0=False, if true, destroy the base directory of the account that's being deleted"
);
########
$cmd =
$ftp->command(
'password',
"change an existing FTP account's password",
'v2 Ftp::passwd'
);
$cmd->argument(
'user_name',
"the FTP user to update (without the \@domain.com)",
key => 'user'
);
$cmd->argument(
'password',
"the new password for the account",
key => 'pass'
);
########
$cmd =
$ftp->command(
'quota',
"change an account's quota setting",
'v2 Ftp::setquota'
);
$cmd->argument(
'user_name',
"the FTP user to update (without the \@domain.com)",
key => 'user'
);
$cmd->argument(
'quota',
"the quota value to place on the account (0 for none)",
default => '0'
);
################
$cmd =
$ftp->command(
'list',
"list the ftp accounts associated with the user",
'v2 Ftp::listftps',
execute => sub {
load_ftp_users;
my @names = sort keys %{ $ftp_users };
print "$_\n" for @names;
exit( 0 );
}
);
####################################################################################################
####################################### Command Line Parsing #######################################
####################################################################################################
$main->option(
'bleach', 'b',
"Do not use color escapes in the output",
\$bleach
);
$main->option(
'debug', 'D',
"Print the WHM API URI sent to the server and dump the resulting data structure",
\$debug
);
$main->option(
'version', 'v',
"Print the script version and exit",
sub {
print "$VERSION\n";
exit( 0 );
}
);
$main->option(
'help', 'h',
"Print help information for the current command and exit",
sub { $do_help = 1; }
);
$main->option(
'user', 'u', '=s userna5',
"Specify the user name to operate upon",
sub {
my ( $opt, $value ) = @_;
$value =~ s/^s+|\s+$//g;
if ( is_user( $value ) ) {
set_user( $value );
} else {
fail( "`$value' is not an existing cPanel user name" );
}
}
);
@ARGV = $main->parse_options( @ARGV );
unless ( $user ) {
if ( is_user( $ARGV[ 0 ] ) ) {
set_user( shift( @ARGV ) );
}
}
my $success = $main->run( @ARGV );
exit( ! $success );
Zerion Mini Shell 1.0