<> or do {
die "Worker type names always begin with a capital! (given: “$worker_type”)";
};
return "WORKER_NODE-$worker_type";
}
sub _parse {
my ($str) = @_;
return $str ? [ split m<:>, $str, 2 ] : undef;
}
1;
} # --- END Cpanel/LinkedNode/Worker/Storage.pm
{ # --- BEGIN Cpanel/SafeFile/Replace.pm
package Cpanel::SafeFile::Replace;
use strict;
use warnings;
# use Cpanel::Fcntl::Constants ();
# use Cpanel::FileUtils::Open ();
use File::Basename ();
use constant {
WRONLY_CREAT_EXCL => $Cpanel::Fcntl::Constants::O_WRONLY | $Cpanel::Fcntl::Constants::O_CREAT | $Cpanel::Fcntl::Constants::O_EXCL,
_EEXIST => 17
};
sub safe_replace_content {
my ( $fh, $safelock, @content ) = @_;
return locked_atomic_replace_contents(
$fh,
$safelock,
sub {
local $!;
@content = @{ $content[0] } if scalar @content == 1 && ref $content[0] eq 'ARRAY';
print { $_[0] } @content;
if ($!) {
my $length = 0;
$length += length for @content;
my $err = $!;
require Cpanel::Exception;
die Cpanel::Exception::create( 'IO::WriteError', [ length => $length, error => $err ] );
}
return 1;
}
);
}
my $_lock_ex_nb;
sub locked_atomic_replace_contents {
my ( $fh, $safelock, $coderef ) = @_;
$_lock_ex_nb //= $Cpanel::Fcntl::Constants::LOCK_EX | $Cpanel::Fcntl::Constants::LOCK_NB;
if ( !flock $fh, $_lock_ex_nb ) {
my $err = $!;
require Cpanel::Exception;
die Cpanel::Exception::create_raw( 'IOError', "locked_atomic_replace_contents could not lock the file handle because of an error: $err" );
}
if ( !ref $safelock ) {
local $@;
if ( !eval { $safelock->isa('Cpanel::SafeFileLock') } ) {
die "locked_atomic_replace_contents requires a Cpanel::SafeFileLock object";
}
}
my $locked_path = $safelock->get_path_to_file_being_locked();
die "locked_path must be valid" if !length $locked_path;
my ( $temp_file, $temp_fh, $created_temp_file, $attempts );
my $current_perms = ( stat($fh) )[2] & 07777;
while ( !$created_temp_file && ++$attempts < 100 ) {
$temp_file = sprintf(
'%s-%x-%x-%x',
$locked_path,
substr( rand, 2 ),
scalar( reverse time ),
scalar( reverse $$ ),
);
my ( $basename, $dirname );
$basename = File::Basename::basename($temp_file);
if ( length $basename >= 255 ) {
$basename = substr( $basename, 255 );
$dirname = File::Basename::dirname($temp_file);
$temp_file = "$dirname/$basename";
}
$created_temp_file = Cpanel::FileUtils::Open::sysopen_with_real_perms( $temp_fh, $temp_file, WRONLY_CREAT_EXCL, $current_perms ) or do {
last if $! != _EEXIST;
};
}
if ( !$created_temp_file ) {
my $lasterr = $!;
die Cpanel::Exception::create( 'TempFileCreateError', [ path => $temp_file, error => $lasterr ] );
}
if ( !flock $temp_fh, $Cpanel::Fcntl::Constants::LOCK_EX ) {
my $err = $!;
require Cpanel::Exception;
die Cpanel::Exception::create( 'IO::FlockError', [ path => $temp_file, error => $err, operation => $Cpanel::Fcntl::Constants::LOCK_EX ] );
}
select( ( select($temp_fh), $| = 1 )[0] ); ##no critic qw(ProhibitOneArgSelect Variables::RequireLocalizedPunctuationVars) #aka $fd->autoflush(1);
if ( $coderef->( $temp_fh, $temp_file, $current_perms ) ) {
rename( $temp_file, $locked_path );
return $temp_fh;
}
local $!;
close $temp_fh;
unlink $temp_file;
die "locked_atomic_replace_contents coderef returns false";
}
1;
} # --- END Cpanel/SafeFile/Replace.pm
{ # --- BEGIN Cpanel/Config/CpUserGuard.pm
package Cpanel::Config::CpUserGuard;
use strict;
use warnings;
# use Cpanel::Destruct ();
# use Cpanel::Config::CpUser ();
# use Cpanel::Config::CpUser::Write ();
# use Cpanel::Config::LoadCpUserFile ();
# use Cpanel::Debug ();
sub new {
my ( $class, $user ) = @_;
my ( $data, $file, $lock, $is_locked ) = ( undef, undef, undef, 0 );
my $cpuser = Cpanel::Config::LoadCpUserFile::_load_locked($user);
if ( $cpuser && ref $cpuser eq 'HASH' ) {
$data = $cpuser->{'data'};
$file = $cpuser->{'file'};
$lock = $cpuser->{'lock'};
$is_locked = defined $lock;
}
else {
Cpanel::Debug::log_warn("Failed to load user file for '$user': $!");
return;
}
my $path = "$Cpanel::Config::CpUser::cpuser_dir/$user";
return bless {
user => $user,
data => $data,
path => $path,
_file => $file,
_lock => $lock,
_pid => $$,
is_locked => $is_locked,
};
}
sub set_worker_node {
my ( $self, $worker_type, $worker_alias, $token ) = @_;
require Cpanel::LinkedNode::Worker::Storage;
Cpanel::LinkedNode::Worker::Storage::set( $self->{'data'}, $worker_type, $worker_alias, $token );
return $self;
}
sub unset_worker_node {
my ( $self, $worker_type ) = @_;
require Cpanel::LinkedNode::Worker::Storage;
return Cpanel::LinkedNode::Worker::Storage::unset( $self->{'data'}, $worker_type );
}
sub save {
my ($self) = @_;
my $user = $self->{'user'};
my $data = $self->{'data'};
if ( $self->{'_pid'} != $$ ) {
Cpanel::Debug::log_die('Locked in parent, cannot save');
return;
}
if ( !UNIVERSAL::isa( $data, 'HASH' ) ) {
Cpanel::Debug::log_die( __PACKAGE__ . ': hash reference required' );
return;
}
my $clean_data = Cpanel::Config::CpUser::clean_cpuser_hash( $self->{'data'}, $user );
if ( !$clean_data ) {
Cpanel::Debug::log_warn("Data for user '$user' was not saved.");
return;
}
if ( !$self->{'_file'} || !$self->{'_lock'} ) {
Cpanel::Debug::log_warn("Unable to save user file for '$user': file not open and locked for writing");
return;
}
require Cpanel::SafeFile::Replace;
require Cpanel::Autodie;
my $newfh = Cpanel::SafeFile::Replace::locked_atomic_replace_contents(
$self->{'_file'}, $self->{'_lock'},
sub {
my ($fh) = @_;
chmod( 0640, $fh ) or do {
warn sprintf( "Failed to set permissions on “%s” to 0%o: %s", $self->{'path'}, 0640, $! );
};
return Cpanel::Autodie::syswrite_sigguard(
$fh,
Cpanel::Config::CpUser::Write::serialize($clean_data),
);
}
)
or do {
Cpanel::Debug::log_warn("Failed to save user file for “$user”: $!");
};
$self->{'_file'} = $newfh;
my $cpgid = Cpanel::Config::CpUser::get_cpgid($user);
if ($cpgid) {
chown 0, $cpgid, $self->{'path'} or do {
Cpanel::Debug::log_warn("Failed to chown( 0, $cpgid, $self->{'path'}): $!");
};
}
if ( $INC{'Cpanel/Locale/Utils/User.pm'} ) {
Cpanel::Locale::Utils::User::clear_user_cache($user);
}
Cpanel::Config::CpUser::recache( $data, $user, $cpgid );
require Cpanel::SafeFile;
Cpanel::SafeFile::safeclose( $self->{'_file'}, $self->{'_lock'} ) or do {
Cpanel::Debug::log_warn("Failed to safeclose $self->{'path'}: $!");
};
$self->{'_file'} = $self->{'_lock'} = undef;
$self->{'is_locked'} = 0;
return 1;
}
sub abort {
my ($self) = @_;
my $user = $self->{'user'};
my $data = $self->{'data'};
if ( $self->{'_pid'} != $$ ) {
Cpanel::Debug::log_die('Locked in parent, cannot save');
return;
}
require Cpanel::SafeFile;
Cpanel::SafeFile::safeclose( $self->{'_file'}, $self->{'_lock'} );
$self->{'_file'} = $self->{'_lock'} = undef;
$self->{'is_locked'} = 0;
return 1;
}
sub DESTROY {
my ($self) = @_;
return unless $self->{'is_locked'};
return if Cpanel::Destruct::in_dangerous_global_destruction();
return unless $self->{'_pid'} == $$;
Cpanel::SafeFile::safeclose( $self->{'_file'}, $self->{'_lock'} );
$self->{'is_locked'} = 0;
return;
}
1;
} # --- END Cpanel/Config/CpUserGuard.pm
{ # --- BEGIN Cpanel/Locale/Utils/User/Modify.pm
package Cpanel::Locale::Utils::User::Modify;
use strict;
use warnings;
# use Cpanel::PwCache ();
sub save_user_locale {
my ( $locale, undef, $user ) = @_;
$locale ||= 'en';
$user ||= $Cpanel::user || $ENV{'REMOTE_USER'} || ( $> == 0 ? 'root' : ( Cpanel::PwCache::getpwuid_noshadow($>) )[0] );
if ( $user eq 'root' ) {
require Cpanel::LoadModule;
Cpanel::LoadModule::load_perl_module('Cpanel::DataStore');
my $root_conf_yaml = Cpanel::PwCache::gethomedir('root') . '/.cpanel_config';
my $hr = Cpanel::DataStore::fetch_ref($root_conf_yaml);
return 2 if exists $hr->{'locale'} && $hr->{'locale'} eq $locale;
$hr->{'locale'} = $locale;
return 1 if Cpanel::DataStore::store_ref( $root_conf_yaml, $hr );
return;
}
elsif ( $> == 0 ) {
require Cpanel::Config::CpUserGuard;
my $cpuser_guard = Cpanel::Config::CpUserGuard->new($user) or return;
$cpuser_guard->{'data'}->{'LOCALE'} = $locale;
delete $cpuser_guard->{'data'}->{'LANG'};
delete $cpuser_guard->{'data'}{'__LOCALE_MISSING'};
return $cpuser_guard->save();
}
else {
require Cpanel::LoadModule;
Cpanel::LoadModule::load_perl_module('Cpanel::AdminBin');
return Cpanel::AdminBin::run_adminbin_with_status( 'lang', 'SAVEUSERSETTINGS', $locale, 0, $user )->{'status'};
}
return 1;
}
1;
} # --- END Cpanel/Locale/Utils/User/Modify.pm
{ # --- BEGIN Cpanel/Version/Tiny.pm
package Cpanel::Version::Tiny;
use strict;
our $VERSION = '11.110.0';
our $VERSION_BUILD = '11.110.0.59';
our $VERSION_TEXT = '110.0 (build 59)';
our $VERSION_DISPLAY = '110.0.59';
our $parent_version = 11;
our $major_version = 110;
our $minor_version = 0;
our $build_number = 59;
our $build_time_text = 'Wed Apr 30 12:57:28 2025';
our $buildtime = 1746035848;
1;
} # --- END Cpanel/Version/Tiny.pm
{ # --- BEGIN Cpanel/Version/Full.pm
package Cpanel::Version::Full;
use strict;
my $full_version;
our $VERSION_FILE = '/usr/local/cpanel/version';
sub getversion {
if ( !$full_version ) {
if ( open my $ver_fh, '<', $VERSION_FILE ) {
if ( read $ver_fh, $full_version, 32 ) {
chomp($full_version);
}
elsif ($!) {
warn "read($VERSION_FILE): $!";
}
}
else {
warn "open($VERSION_FILE): $!";
}
if ( !$full_version || $full_version =~ tr{.}{} < 3 ) {
require Cpanel::Version::Tiny;
$full_version = $Cpanel::Version::Tiny::VERSION_BUILD;
}
}
return $full_version;
}
sub _clear_cache {
undef $full_version;
return;
}
1;
} # --- END Cpanel/Version/Full.pm
{ # --- BEGIN Cpanel/Version/Compare.pm
package Cpanel::Version::Compare;
use cPstrict;
my %modes = (
'>' => sub ( $check, $against ) {
return if $check eq $against; # no need to continue if they are the same
return ( cmp_versions( $check, $against ) > 0 );
},
'<' => sub ( $check, $against ) {
return if $check eq $against; # no need to continue if they are the same
return ( cmp_versions( $check, $against ) < 0 );
},
'==' => sub ( $check, $against ) {
return ( $check eq $against || cmp_versions( $check, $against ) == 0 );
},
'!=' => sub ( $check, $against ) {
return ( $check ne $against && cmp_versions( $check, $against ) != 0 );
},
'>=' => sub ( $check, $against ) {
return 1 if $check eq $against; # no need to continue if they are the same
return ( cmp_versions( $check, $against ) >= 0 );
},
'<=' => sub ( $check, $against ) {
return 1 if $check eq $against; # no need to continue if they are the same
return ( cmp_versions( $check, $against ) <= 0 );
},
'<=>' => sub ( $check, $against ) {
return cmp_versions( $check, $against );
},
);
sub compare ( $check, $mode, $against ) {
if ( !defined $mode || !exists $modes{$mode} ) {
return;
}
foreach my $ver ( $check, $against ) {
$ver //= '';
if ( $ver !~ m{ ^((?:\d+[._]){0,}\d+[a-z]?).*?$ }axms ) {
return;
}
$ver = $1;
}
$check =~ s/_/\./g;
$against =~ s/_/\./g;
$check =~ s/([a-z])$/'.' . ord($1)/e;
$against =~ s/([a-z])$/'.' . ord($1)/e;
my @check_len = split( /[_\.]/, $check );
my @against_len = split( /[_\.]/, $against );
if ( @check_len > 4 ) {
return;
}
elsif ( @check_len < 4 ) {
for ( 1 .. 4 - @check_len ) {
$check .= '.0';
}
}
if ( @against_len > 4 ) {
return;
}
elsif ( @against_len < 4 ) {
for ( 1 .. 4 - @against_len ) {
$against .= '.0';
}
}
return if $check !~ m { \A \d+\.\d+\.\d+\.\d+ \z }axms;
return if $against !~ m { \A \d+\.\d+\.\d+\.\d+ \z }axms;
return $modes{$mode}->( $check, $against );
}
sub cmp_versions ( $left, $right ) {
my ( $maj, $min, $rev, $sup ) = split /[\._]/, $left;
my ( $mj, $mn, $rv, $sp ) = split /[\._]/, $right;
return $maj <=> $mj || $min <=> $mn || $rev <=> $rv || $sup <=> $sp;
}
sub get_major_release ( $version = '' ) {
$version =~ s/\s*//g;
my ( $major, $minor );
if ( $version =~ m/^([0-9]+)\.([0-9]+)/ ) {
$major = int $1;
$minor = int $2;
}
else {
return;
}
$minor++ if $minor % 2;
return "$major.$minor";
}
sub compare_major_release ( $check, $mode, $against ) {
return unless defined $check && defined $mode && defined $against;
my $maj1 = get_major_release($check);
return unless defined $maj1;
my $maj2 = get_major_release($against);
return unless defined $maj2;
return $modes{$mode}->( $maj1, $maj2 );
}
1;
} # --- END Cpanel/Version/Compare.pm
{ # --- BEGIN Cpanel/Version.pm
package Cpanel::Version;
use strict;
use warnings;
# use Cpanel::Version::Full ();
our ( $VERSION, $MAJORVERSION, $LTS ) = ( '4.0', '11.110', '11.110' );
sub get_version_text {
return sprintf( "%d.%d (build %d)", ( split( m{\.}, Cpanel::Version::Full::getversion() ) )[ 1, 2, 3 ] );
}
sub get_version_parent {
return _ver_key('parent_version');
}
sub get_version_display {
return sprintf( "%d.%d.%d", ( split( m{\.}, Cpanel::Version::Full::getversion() ) )[ 1, 2, 3 ] );
}
{
no warnings 'once'; # for updatenow
*get_version_full = *Cpanel::Version::Full::getversion;
}
sub getversionnumber {
return sprintf( "%d.%d.%d", ( split( m{\.}, Cpanel::Version::Full::getversion() ) )[ 0, 1, 2 ] );
}
sub get_lts {
return $LTS;
}
sub get_short_release_number {
my $current_ver = ( split( m{\.}, Cpanel::Version::Full::getversion() ) )[1];
if ( $current_ver % 2 == 0 ) {
return $current_ver;
}
return $current_ver + 1;
}
sub _ver_key {
require Cpanel::Version::Tiny if !$INC{'Cpanel/Version/Tiny.pm'};
return ${ $Cpanel::Version::Tiny::{ $_[0] } };
}
sub compare {
require Cpanel::Version::Compare;
goto &Cpanel::Version::Compare::compare;
}
sub is_major_version {
my ( $ver, $major ) = @_;
require Cpanel::Version::Compare;
return ( $ver eq $major || Cpanel::Version::Compare::get_major_release($ver) eq $major ) ? 1 : 0;
}
sub is_development_version {
return substr( $MAJORVERSION, -1 ) % 2 ? 1 : 0;
}
sub display_version {
my ($ver) = @_;
if ( defined $ver && $ver =~ tr{\.}{} >= 2 ) {
my @v = split( m{\.}, $ver );
if ( $v[0] == 11 && $v[1] >= 54 ) {
return join( '.', (@v)[ 1, 2, 3 ] );
}
return $ver;
}
return;
}
1;
} # --- END Cpanel/Version.pm
{ # --- BEGIN Cpanel/Locale.pm
package Cpanel::Locale;
use strict;
BEGIN {
$ENV{'IGNORE_WIN32_LOCALE'} = 1;
}
# use Cpanel::CPAN::Locale::Maketext::Utils();
our @ISA;
BEGIN { push @ISA, qw(Cpanel::CPAN::Locale::Maketext::Utils); }
# use Cpanel::Locale::Utils (); # Individual Locale modules depend on this being brought in here, if it is removed they will all need updated. Same for cpanel.pl
# use Cpanel::Locale::Utils::Paths ();
# use Cpanel::CPAN::Locale::Maketext ();
# use Cpanel::Exception ();
use constant _ENOENT => 2;
BEGIN {
local $^H = 0; # cheap no warnings without importing it
local $^W = 0;
*Cpanel::CPAN::Locale::Maketext::Utils::remove_key_from_lexicons = sub { }; # PPI NO PARSE - loaded above - disabled
}
our $SERVER_LOCALE_FILE = '/var/cpanel/server_locale';
our $LTR = 1;
our $RTL = 2;
our %known_locales_character_orientation = (
ar => $RTL,
bn => $LTR,
bg => $LTR,
cs => $LTR,
da => $LTR,
de => $LTR,
el => $LTR,
en => $LTR,
en_US => $LTR,
en_GB => $LTR,
es_419 => $LTR,
es => $LTR,
es_es => $LTR,
fi => $LTR,
fil => $LTR,
fr => $LTR,
he => $RTL,
hi => $LTR,
hu => $LTR,
i_cpanel_snowmen => $LTR,
i_cp_qa => $LTR,
id => $LTR,
it => $LTR,
ja => $LTR,
ko => $LTR,
ms => $LTR,
nb => $LTR,
nl => $LTR,
no => $LTR,
pl => $LTR,
pt_br => $LTR,
pt => $LTR,
ro => $LTR,
ru => $LTR,
sl => $LTR,
sv => $LTR,
th => $LTR,
tr => $LTR,
uk => $LTR,
vi => $LTR,
zh => $LTR,
zh_tw => $LTR,
zh_cn => $LTR,
);
my $logger;
sub _logger {
require Cpanel::Logger;
return ( $logger ||= Cpanel::Logger->new() );
}
*get_lookup_hash_of_mutli_epoch_datetime = *get_lookup_hash_of_multi_epoch_datetime;
sub preinit {
if ( exists $INC{'Cpanel.pm'} && !$Cpanel::CPDATA{'LOCALE'} ) {
require Cpanel::Locale::Utils::User if !exists $INC{'Cpanel/Locale/Utils/User.pm'};
Cpanel::Locale::Utils::User::init_cpdata_keys();
}
if ( $ENV{'HTTP_COOKIE'} ) {
require Cpanel::Cookies unless $INC{'Cpanel/Cookies.pm'};
if ( !keys %Cpanel::Cookies ) {
%Cpanel::Cookies = %{ Cpanel::Cookies::get_cookie_hashref() };
}
}
%Cpanel::Grapheme = %{ Cpanel::Locale->get_grapheme_helper_hashref() };
return 1;
}
sub makevar {
return $_[0]->maketext( ref $_[1] ? @{ $_[1] } : @_[ 1 .. $#_ ] ); ## no extract maketext
}
*maketext = *Cpanel::CPAN::Locale::Maketext::maketext; ## no extract maketext
my %singleton_stash = ();
BEGIN {
no warnings; ## no critic(ProhibitNoWarnings)
CHECK {
if ( ( $INC{'O.pm'} || $INC{'Cpanel/BinCheck.pm'} || $INC{'Cpanel/BinCheck/Lite.pm'} ) && %singleton_stash ) {
die("If you use a locale at begin time, you are responsible for deleting it too. Try calling _reset_singleton_stash\n");
}
}
}
sub _reset_singleton_stash {
foreach my $class ( keys %singleton_stash ) {
foreach my $args_sig ( keys %{ $singleton_stash{$class} } ) {
$singleton_stash{$class}{$args_sig}->cpanel_detach_lexicon();
}
}
%singleton_stash = ();
return 1;
}
sub get_handle {
preinit();
no warnings 'redefine';
*get_handle = *_real_get_handle;
goto &_real_get_handle;
}
sub _map_any_old_style_to_new_style {
my (@locales) = @_;
if ( grep { !$known_locales_character_orientation{$_} && index( $_, 'i_' ) != 0 } @locales ) {
require Cpanel::Locale::Utils::Legacy;
goto \&Cpanel::Locale::Utils::Legacy::map_any_old_style_to_new_style;
}
return @locales;
}
our $IN_REAL_GET_HANDLE = 0;
sub _setup_for_real_get_handle { ## no critic qw(RequireArgUnpacking)
if ($IN_REAL_GET_HANDLE) {
_load_carp();
if ( $IN_REAL_GET_HANDLE > 1 ) {
die 'Cpanel::Carp'->can('safe_longmess')->("Attempted to call _setup_for_real_get_handle from _setup_for_real_get_handle");
}
warn 'Cpanel::Carp'->can('safe_longmess')->("Attempted to call _setup_for_real_get_handle from _setup_for_real_get_handle");
if ($Cpanel::Exception::IN_EXCEPTION_CREATION) { # PPI NO PARSE - Only care about this check if the module is loaded
$Cpanel::Exception::LOCALIZE_STRINGS = 0; # PPI NO PARSE - Only care about this check if the module is loaded
}
}
local $IN_REAL_GET_HANDLE = $IN_REAL_GET_HANDLE + 1;
if ( defined $Cpanel::App::appname && defined $ENV{'REMOTE_USER'} ) { # PPI NO PARSE - Only care about this check if the module is loaded
if (
$Cpanel::App::appname eq 'whostmgr' # PPI NO PARSE - Only care about this check if the module is loaded
&& $ENV{'REMOTE_USER'} ne 'root'
) {
require Cpanel::Config::HasCpUserFile;
if ( Cpanel::Config::HasCpUserFile::has_readable_cpuser_file( $ENV{'REMOTE_USER'} ) ) {
require Cpanel::Config::LoadCpUserFile::CurrentUser;
my $cpdata_ref = Cpanel::Config::LoadCpUserFile::CurrentUser::load( $ENV{'REMOTE_USER'} );
if ( scalar keys %{$cpdata_ref} ) {
*Cpanel::CPDATA = $cpdata_ref;
}
}
}
}
my ( $class, @langtags ) = (
$_[0],
(
defined $_[1] ? _map_any_old_style_to_new_style( (@_)[ 1 .. $#_ ] )
: exists $Cpanel::Cookies{'session_locale'} && $Cpanel::Cookies{'session_locale'} ? _map_any_old_style_to_new_style( $Cpanel::Cookies{'session_locale'} )
: ( exists $Cpanel::CPDATA{'LOCALE'} && $Cpanel::CPDATA{'LOCALE'} ) ? ( $Cpanel::CPDATA{'LOCALE'} )
: ( exists $Cpanel::CPDATA{'LANG'} && $Cpanel::CPDATA{'LANG'} ) ? ( _map_any_old_style_to_new_style( $Cpanel::CPDATA{'LANG'} ) )
: ( get_server_locale() )
)
);
if ( !$Cpanel::Locale::CDB_File_Path ) {
$Cpanel::Locale::CDB_File_Path = Cpanel::Locale::Utils::init_lexicon( 'en', \%Cpanel::Locale::Lexicon, \$Cpanel::Locale::VERSION, \$Cpanel::Locale::Encoding );
}
_make_alias_if_needed( @langtags ? @langtags : 'en_us' );
return @langtags;
}
my %_made_aliases;
sub _make_alias_if_needed {
foreach my $tag ( grep { ( $_ eq 'en' || $_ eq 'i_default' || $_ eq 'en_us' ) && !$_made_aliases{$_} } ( 'en', @_ ) ) {
Cpanel::Locale->make_alias( [$tag], 1 );
$_made_aliases{$tag} = 1;
}
return 0;
}
sub _real_get_handle {
my ( $class, @arg_langtags ) = @_;
my @langtags = _setup_for_real_get_handle( $class, @arg_langtags );
@langtags = map { my $l = $_; $l = 'en' if ( $l eq 'en_us' || $l eq 'i_default' ); $l } grep { $class->cpanel_is_valid_locale($_) } @langtags;
@langtags = ('en') unless scalar @langtags;
my $args_sig = join( ',', @langtags ) || 'no_args';
return (
( defined $singleton_stash{$class}{$args_sig} && ++$singleton_stash{$class}{$args_sig}->{'_singleton_reused'} )
? $singleton_stash{$class}{$args_sig}
: ( $singleton_stash{$class}{$args_sig} = Cpanel::CPAN::Locale::Maketext::get_handle( $class, @langtags ) )
);
}
sub get_non_singleton_handle {
my ( $class, @arg_langtags ) = @_;
my @langtags = _setup_for_real_get_handle( $class, @arg_langtags );
return Cpanel::CPAN::Locale::Maketext::get_handle( $class, @langtags );
}
sub init {
my ($lh) = @_;
$lh->SUPER::init();
$lh->_initialize_unknown_phrase_logging();
$lh->_initialize_bracket_notation_whitelist();
return $lh;
}
sub _initialize_unknown_phrase_logging {
my $lh = shift;
if ( defined $Cpanel::Locale::Context::DEFAULT_OUTPUT_CONTEXT ) { # PPI NO PARSE - Only needed if loaded
my $setter_cr = $lh->can("set_context_${Cpanel::Locale::Context::DEFAULT_OUTPUT_CONTEXT}") or do { # PPI NO PARSE - Only needed if loaded
die "Invalid \$Cpanel::Locale::Context::DEFAULT_OUTPUT_CONTEXT: “$Cpanel::Locale::Context::DEFAULT_OUTPUT_CONTEXT”!"; # PPI NO PARSE - Only needed if loaded
};
$setter_cr->($lh);
}
elsif ( defined $Cpanel::Carp::OUTPUT_FORMAT ) { # issafe
if ( $Cpanel::Carp::OUTPUT_FORMAT eq 'xml' ) { # issafe
$lh->set_context_plain(); # no HTML markup or ANSI escape sequences
}
elsif ( $Cpanel::Carp::OUTPUT_FORMAT eq 'html' ) { # issafe
$lh->set_context_html(); # HTML
}
}
$lh->{'use_external_lex_cache'} = 1;
if ( exists $Cpanel::CPDATA{'LOCALE_LOG_MISSING'} && $Cpanel::CPDATA{'LOCALE_LOG_MISSING'} ) {
$lh->{'_log_phantom_key'} = sub {
my ( $lh, $key ) = @_;
my $chain = '';
my $base_class = $lh->get_base_class();
foreach my $class ( $lh->get_language_class, $base_class ) {
my $lex_path = $lh->get_cdb_file_path( $class eq $base_class ? 1 : 0 );
next if !$lex_path;
$chain .= "\tLOCALE: $class\n\tPATH: $lex_path\n";
last if $class eq 'Cpanel::Locale::en' || $class eq 'Cpanel::Locale::en_us' || $class eq 'Cpanel::Locale::i_default';
}
my $pkg = $lh->get_language_tag();
_logger->info( ( $Cpanel::Parser::Vars::file ? "$Cpanel::Parser::Vars::file ::" : '' ) . qq{ Could not find key via '$pkg' locale:\n\tKEY: '$key'\n$chain} ); # PPI NO PARSE -- module will already be there is we care about it
};
}
return $lh;
}
our @DEFAULT_WHITELIST = qw(quant asis output current_year list_and list_or comment boolean datetime local_datetime format_bytes get_locale_name get_user_locale_name is_defined is_future join list_and_quoted list_or_quoted numerate numf);
sub _initialize_bracket_notation_whitelist {
my $lh = shift;
my @whitelist = @DEFAULT_WHITELIST;
my $custom_whitelist_file = Cpanel::Locale::Utils::Paths::get_custom_whitelist_path();
if ( open( my $fh, '<', $custom_whitelist_file ) ) {
while ( my $ln = readline($fh) ) {
chomp $ln;
push @whitelist, $ln if length($ln);
}
close $fh;
}
$lh->whitelist(@whitelist);
return $lh;
}
sub output_cpanel_error {
my ( $lh, $position ) = @_;
if ( $lh->context_is_ansi() ) {
return "\e[1;31m" if $position eq 'begin';
return "\e[0m" if $position eq 'end';
return '';
}
elsif ( $lh->context_is_html() ) {
return qq{} if $position eq 'begin';
return '
' if $position eq 'end';
return '';
}
else {
return ''; # e.g. $lh->context_is_plain()
}
}
sub cpanel_get_3rdparty_lang {
my ( $lh, $_3rdparty ) = @_;
require Cpanel::Locale::Utils::3rdparty;
return Cpanel::Locale::Utils::3rdparty::get_app_setting( $lh, $_3rdparty ) || Cpanel::Locale::Utils::3rdparty::get_3rdparty_lang( $lh, $_3rdparty ) || $lh->get_language_tag() || 'en';
}
sub cpanel_is_valid_locale {
my ( $lh, $locale ) = @_;
my %valid_locales = map { $_ => 1 } ( qw(en en_us i_default), $lh->list_available_locales );
return $valid_locales{$locale} ? 1 : 0;
}
sub cpanel_get_3rdparty_list {
my ($lh) = @_;
require Cpanel::Locale::Utils::3rdparty;
return Cpanel::Locale::Utils::3rdparty::get_3rdparty_list($lh);
}
sub cpanel_get_lex_path {
my ( $lh, $path, $rv ) = @_;
return if !defined $path || $path eq '' || substr( $path, -3 ) ne '.js';
require Cpanel::JS::Variations;
my $query = $path;
$query = Cpanel::JS::Variations::get_base_file( $query, '-%s.js' );
if ( defined $rv && index( $rv, '%s' ) == -1 ) {
substr( $rv, -3, 3, '-%s.js' );
}
my $asset_path = $lh->get_asset_file( $query, $rv );
return $asset_path if $asset_path && substr( $asset_path, -3 ) eq '.js' && index( $asset_path, '-' ) > -1; # Only return a value if there is a localized js file here
return;
}
sub tag_is_default_locale {
my $tag = $_[1] || $_[0]->get_language_tag();
return 1 if $tag eq 'en' || $tag eq 'en_us' || $tag eq 'i_default';
return;
}
sub get_cdb_file_path {
my ( $lh, $core ) = @_;
my $class = $core ? $lh->get_base_class() : $lh->get_language_class();
no strict 'refs';
return
$class eq 'Cpanel::Locale::en'
|| $class eq 'Cpanel::Locale::en_us'
|| $class eq 'Cpanel::Locale::i_default' ? $Cpanel::Locale::CDB_File_Path : ${ $class . '::CDB_File_Path' };
}
sub _slurp_small_file_if_exists_no_exception {
my ($path) = @_;
local ( $!, $^E );
open my $rfh, '<', $path or do {
if ( $! != _ENOENT() ) {
warn "open($path): $!";
}
return undef;
};
read $rfh, my $buf, 8192 or do {
warn "read($path): $!";
};
return $buf;
}
my $_server_locale_file_contents;
sub get_server_locale {
if ( exists $ENV{'CPANEL_SERVER_LOCALE'} ) {
return $ENV{'CPANEL_SERVER_LOCALE'} if $ENV{'CPANEL_SERVER_LOCALE'} !~ tr{A-Za-z0-9_-}{}c;
return undef;
}
if (%main::CPCONF) {
return $main::CPCONF{'server_locale'} if exists $main::CPCONF{'server_locale'};
}
return ( $_server_locale_file_contents //= ( _slurp_small_file_if_exists_no_exception($SERVER_LOCALE_FILE) || '' ) );
}
sub _clear_cache {
$_server_locale_file_contents = undef;
return;
}
sub get_locale_for_user_cpanel {
if (%main::CPCONF) {
return $main::CPCONF{'cpanel_locale'} if exists $main::CPCONF{'cpanel_locale'};
return $main::CPCONF{'server_locale'} if exists $main::CPCONF{'server_locale'};
}
require Cpanel::Config::LoadCpConf;
my $cpconf = Cpanel::Config::LoadCpConf::loadcpconf_not_copy(); # safe since we do not modify cpconf
return $cpconf->{'cpanel_locale'} if $cpconf->{'cpanel_locale'}; # will not be autovivified, 0 and "" are invalid, if the value is invalid they will get 'en'
return $cpconf->{'server_locale'} if $cpconf->{'server_locale'}; # will not be autovivified, 0 and "" are invalid, if the value is invalid they will get 'en'
return;
}
sub cpanel_reinit_lexicon {
my ($lh) = @_;
$lh->cpanel_detach_lexicon();
$lh->cpanel_attach_lexicon();
}
my $detach_locale_lex;
sub cpanel_detach_lexicon {
my ($lh) = @_;
my $locale = $lh->get_language_tag();
no strict 'refs';
undef $Cpanel::Locale::CDB_File_Path;
if ( $locale ne 'en' && $locale ne 'en_us' && $locale ne 'i_default' ) {
$detach_locale_lex = ${ 'Cpanel::Locale::' . $locale . '::CDB_File_Path' };
undef ${ 'Cpanel::Locale::' . $locale . '::CDB_File_Path' };
}
untie( %{ 'Cpanel::Locale::' . $locale . '::Lexicon' } );
untie %Cpanel::Locale::Lexicon;
}
sub cpanel_attach_lexicon {
my ($lh) = @_;
my $locale = $lh->get_language_tag();
$Cpanel::Locale::CDB_File_Path = Cpanel::Locale::Utils::init_lexicon( 'en', \%Cpanel::Locale::Lexicon, \$Cpanel::Locale::VERSION, \$Cpanel::Locale::Encoding );
_make_alias_if_needed($locale);
no strict 'refs';
if ( defined $detach_locale_lex ) {
${ 'Cpanel::Locale::' . $locale . '::CDB_File_Path' } = $detach_locale_lex;
}
else {
${ 'Cpanel::Locale::' . $locale . '::CDB_File_Path' } = $Cpanel::Locale::CDB_File_Path;
}
my $file_path = $lh->get_cdb_file_path();
return if !$file_path;
return Cpanel::Locale::Utils::get_readonly_tie( $lh->get_cdb_file_path(), \%{ 'Cpanel::Locale::' . $locale . '::Lexicon' } );
}
sub is_rtl {
my ($lh) = @_;
return 'right-to-left' eq $lh->get_language_tag_character_orientation() ? 1 : 0;
}
sub get_language_tag_character_orientation {
if ( my $direction = $known_locales_character_orientation{ $_[1] || $_[0]->{'fallback_locale'} || $_[0]->get_language_tag() } ) {
return 'right-to-left' if $direction == $RTL;
return 'left-to-right';
}
$_[0]->SUPER::get_language_tag_character_orientation( @_[ 1 .. $#_ ] );
}
my $menu_ar;
sub get_locale_menu_arrayref {
return $menu_ar if $menu_ar;
require Cpanel::Locale::Utils::Display;
$menu_ar = [ Cpanel::Locale::Utils::Display::get_locale_menu_hashref(@_) ]; # always array context to get all structs, properly uses other args besides object
return $menu_ar;
}
my $non_existent;
sub get_non_existent_locale_menu_arrayref {
return $non_existent if $non_existent;
require Cpanel::Locale::Utils::Display;
$non_existent = [ Cpanel::Locale::Utils::Display::get_non_existent_locale_menu_hashref(@_) ]; # always array context to get all structs, properly uses other args besides object
return $non_existent;
}
sub _api1_maketext {
require Cpanel::Locale::Utils::Api1;
goto \&Cpanel::Locale::Utils::Api1::_api1_maketext; ## no extract maketext
}
our $api1 = {
'maketext' => { ## no extract maketext
'function' => \&_api1_maketext, ## no extract maketext
'internal' => 1,
'legacy_function' => 2,
'modify' => 'inherit',
},
};
sub current_year {
return (localtime)[5] + 1900; # we override datetime() so we can't use the internal current_year()
}
sub local_datetime {
my ( $lh, $epoch, $format ) = @_;
my $timezone = $ENV{'TZ'} // do {
require Cpanel::Timezones;
Cpanel::Timezones::calculate_TZ_env();
};
return $lh->datetime( $epoch, $format, $timezone );
}
sub datetime {
my ( $lh, $epoch, $format, $timezone ) = @_;
require Cpanel::Locale::Utils::DateTime;
if ( $epoch && $epoch =~ tr<0-9><>c ) {
require # do not include it in updatenow.static
Cpanel::Validate::Time;
Cpanel::Validate::Time::iso_or_die($epoch);
require Cpanel::Time::ISO;
$epoch = Cpanel::Time::ISO::iso2unix($epoch);
}
return Cpanel::Locale::Utils::DateTime::datetime( $lh, $epoch, $format, $timezone );
}
sub get_lookup_hash_of_multi_epoch_datetime {
my ( $lh, $epochs_ar, $format, $timezone ) = @_;
require Cpanel::Locale::Utils::DateTime;
return Cpanel::Locale::Utils::DateTime::get_lookup_hash_of_multi_epoch_datetime( $lh, $epochs_ar, $format, $timezone );
}
sub get_locale_name_or_nothing {
my ( $locale, $name, $in_locale_tongue ) = @_;
$name ||= $locale->get_language_tag();
if ( index( $name, 'i_' ) == 0 ) {
require Cpanel::DataStore;
my $i_locales_path = Cpanel::Locale::Utils::Paths::get_i_locales_config_path();
my $i_conf = Cpanel::DataStore::fetch_ref("$i_locales_path/$name.yaml");
return $i_conf->{'display_name'} if $i_conf->{'display_name'};
}
else {
my $real = $locale->get_language_tag_name( $name, $in_locale_tongue );
return $real if $real;
}
return;
}
sub get_locale_name_or_tag {
return $_[0]->get_locale_name_or_nothing( $_[1], $_[2] ) || $_[1] || $_[0]->get_language_tag();
}
*get_locale_name = *get_locale_name_or_tag; # for shorter BN
sub get_user_locale {
return $Cpanel::CPDATA{'LOCALE'} if $Cpanel::CPDATA{'LOCALE'};
require Cpanel::Locale::Utils::User; # probably a no-op but just in case since its loading is conditional
return Cpanel::Locale::Utils::User::get_user_locale();
}
sub get_user_locale_name {
require Cpanel::Locale::Utils::User; # probably a no-op but just in case since its loading is conditional
return $_[0]->get_locale_name_or_tag( Cpanel::Locale::Utils::User::get_user_locale( $_[1] ) );
}
sub set_user_locale {
my ( $locale, $country_code ) = @_;
if ($country_code) {
my $language_name = $locale->lang_names_hashref();
if ( exists $language_name->{$country_code} ) {
require Cpanel::Locale::Utils::Legacy;
require Cpanel::Locale::Utils::User::Modify;
my $language = Cpanel::Locale::Utils::Legacy::get_best_guess_of_legacy_from_locale($country_code);
if ( Cpanel::Locale::Utils::User::Modify::save_user_locale( $country_code, $language, $Cpanel::user ) ) {
return 1;
}
}
}
die Cpanel::Exception::create_raw( "Empty", $locale->maketext("Unable to set locale, please specify a valid country code.") );
}
sub get_locales {
my $locale = shift;
my @listing;
my ( $names, $local_names ) = $locale->lang_names_hashref();
foreach ( keys %{$names} ) {
push @listing, {
locale => $_,
name => $names->{$_},
local_name => $local_names->{$_},
direction => ( !defined $known_locales_character_orientation{$_} || $known_locales_character_orientation{$_} == $LTR ) ? 'ltr' : 'rtl'
};
}
return \@listing;
}
my $api2_lh;
sub api2_get_user_locale {
$api2_lh ||= Cpanel::Locale->get_handle();
return ( { 'locale' => $api2_lh->get_user_locale() } );
}
sub api2_get_user_locale_name {
$api2_lh ||= Cpanel::Locale->get_handle();
return ( { 'name' => $api2_lh->get_user_locale_name() } );
}
sub api2_get_locale_name {
$api2_lh ||= Cpanel::Locale->get_handle();
my $tag = ( scalar @_ > 2 ) ? {@_}->{'locale'} : $_[1];
return ( { 'name' => $api2_lh->get_locale_name_or_tag($tag) } );
}
sub api2_get_encoding {
$api2_lh ||= Cpanel::Locale->get_handle();
return ( { 'encoding' => $api2_lh->encoding() } );
}
sub api2_numf {
my %args = @_;
$api2_lh ||= Cpanel::Locale->get_handle();
return ( { 'numf' => $api2_lh->numf( $args{number}, $args{max_decimal_places} ) } );
}
sub api2_get_html_dir_attr {
$api2_lh ||= Cpanel::Locale->get_handle();
return ( { 'dir' => $api2_lh->get_html_dir_attr() } );
}
my $allow_demo = { allow_demo => 1 };
our %API = (
get_locale_name => $allow_demo,
get_encoding => $allow_demo,
get_html_dir_attr => $allow_demo,
get_user_locale => $allow_demo,
get_user_locale_name => $allow_demo,
numf => $allow_demo,
);
sub api2 {
my ($func) = @_;
return { %{ $API{$func} } } if $API{$func};
return;
}
my $global_lh;
sub lh {
return ( $global_lh ||= Cpanel::Locale->get_handle() );
}
sub import {
my ( $package, @args ) = @_;
my ($namespace) = caller;
if ( @args == 1 && $args[0] eq 'lh' ) {
no strict 'refs'; ## no critic(ProhibitNoStrict)
my $exported_name = "${namespace}::lh";
*$exported_name = \*lh;
}
}
sub _load_carp {
if ( !$INC{'Cpanel/Carp.pm'} ) {
local $@;
eval 'require Cpanel::Carp; 1;' or die $@; # hide from perlcc
}
return;
}
sub user_feedback_text_for_more_locales {
require Cpanel::Version;
my $locale = Cpanel::Locale->get_handle();
my $version = Cpanel::Version::get_version_full();
my $survey_url = 'https://surveys.webpros.com/to/xGTiJG4X?utm_source=cpanel-changelanguage&cpanel_productversion=' . $version;
return $locale->maketext( "Don’t see your language of choice? Take our [output,url,_1,Language Support Feedback Survey,class,externalLink,target,Language Survey] to let us know your preferences.", $survey_url );
}
1;
} # --- END Cpanel/Locale.pm
{ # --- BEGIN Cpanel/Sys/Uname.pm
package Cpanel::Sys::Uname;
use strict;
our $SYS_UNAME = 63;
our $UNAME_ELEMENTS = 6;
our $_UTSNAME_LENGTH = 65;
my $UNAME_PACK_TEMPLATE = ( 'c' . $_UTSNAME_LENGTH ) x $UNAME_ELEMENTS;
my $UNAME_UNPACK_TEMPLATE = ( 'Z' . $_UTSNAME_LENGTH ) x $UNAME_ELEMENTS;
my @uname_cache;
sub get_uname_cached {
return ( @uname_cache ? @uname_cache : ( @uname_cache = syscall_uname() ) );
}
sub clearcache {
@uname_cache = ();
return;
}
sub syscall_uname {
my $uname;
if ( syscall( $SYS_UNAME, $uname = pack( $UNAME_PACK_TEMPLATE, () ) ) == 0 ) {
return unpack( $UNAME_UNPACK_TEMPLATE, $uname );
}
else {
die "The uname() system call failed because of an error: $!";
}
return;
}
1;
} # --- END Cpanel/Sys/Uname.pm
{ # --- BEGIN Cpanel/Sys/Hostname/Fallback.pm
package Cpanel::Sys::Hostname::Fallback;
use strict;
use warnings;
use Socket ();
# use Cpanel::Sys::Uname ();
sub get_canonical_hostname {
my @uname = Cpanel::Sys::Uname::get_uname_cached();
my ( $err, @results ) = Socket::getaddrinfo( $uname[1], 0, { flags => Socket::AI_CANONNAME() } );
if ( @results && $results[0]->{'canonname'} ) {
return $results[0]->{'canonname'};
}
return undef;
}
1;
} # --- END Cpanel/Sys/Hostname/Fallback.pm
{ # --- BEGIN Cpanel/Sys/Hostname.pm
package Cpanel::Sys::Hostname;
use strict;
use warnings;
our $VERSION = 2.0;
# use Cpanel::Sys::Uname ();
our $cachedhostname = '';
sub gethostname {
my $nocache = shift || 0;
if ( !$nocache && length $cachedhostname ) { return $cachedhostname }
my $hostname = _gethostname($nocache);
if ( length $hostname ) {
$hostname =~ tr{A-Z}{a-z}; # hostnames must be lowercase (see Cpanel::Sys::Hostname::Modify::make_hostname_lowercase_fqdn)
$cachedhostname = $hostname;
}
return $hostname;
}
sub _gethostname {
my $nocache = shift || 0;
my $hostname;
Cpanel::Sys::Uname::clearcache() if $nocache;
my @uname = Cpanel::Sys::Uname::get_uname_cached();
if ( $uname[1] && index( $uname[1], '.' ) > -1 ) {
$hostname = $uname[1];
$hostname =~ tr{A-Z}{a-z}; # hostnames must be lowercase (see Cpanel::Sys::Hostname::Modify::make_hostname_lowercase_fqdn)
return $hostname;
}
eval {
require Cpanel::Sys::Hostname::Fallback;
$hostname = Cpanel::Sys::Hostname::Fallback::get_canonical_hostname();
};
if ($hostname) {
$hostname =~ tr{A-Z}{a-z}; # hostnames must be lowercase (see Cpanel::Sys::Hostname::Modify::make_hostname_lowercase_fqdn)
return $hostname;
}
require Cpanel::LoadFile;
chomp( $hostname = Cpanel::LoadFile::loadfile( '/proc/sys/kernel/hostname', { 'skip_exists_check' => 1 } ) );
if ($hostname) {
$hostname =~ tr{A-Z}{a-z}; # hostnames must be lowercase (see Cpanel::Sys::Hostname::Modify::make_hostname_lowercase_fqdn)
$hostname =~ tr{\r\n}{}d; # chomp is not enough (not sure if this is required, however we cannot test all kernels so its safer to leave it in)
return $hostname;
}
require Cpanel::Debug;
Cpanel::Debug::log_warn('Unable to determine correct hostname');
return;
}
sub shorthostname {
my $hostname = gethostname();
return $hostname if index( $hostname, '.' ) == -1; # Hostname is not a FQDN (this should never happen)
return substr( $hostname, 0, index( $hostname, '.' ) );
}
1;
} # --- END Cpanel/Sys/Hostname.pm
{ # --- BEGIN Cpanel/Hostname.pm
package Cpanel::Hostname;
use strict;
use warnings;
# use Cpanel::Sys::Hostname ();
our $VERSION = 2.0;
{
no warnings 'once';
*gethostname = *Cpanel::Sys::Hostname::gethostname;
*shorthostname = *Cpanel::Sys::Hostname::shorthostname;
}
1;
} # --- END Cpanel/Hostname.pm
{ # --- BEGIN Cpanel/Config/CpConfGuard/CORE.pm
package Cpanel::Config::CpConfGuard::CORE;
use strict;
use warnings;
# use Cpanel::ConfigFiles ();
# use Cpanel::Debug ();
# use Cpanel::FileUtils::Write::JSON::Lazy ();
# use Cpanel::LoadModule ();
# use Cpanel::Config::CpConfGuard ();
our $SENDING_MISSING_FILE_NOTICE = 0;
my $FILESYS_PERMS = 0644;
sub find_missing_keys {
my ($self) = @_;
_verify_called_as_object_method($self);
Cpanel::LoadModule::load_perl_module('Cpanel::Config::CpConfGuard::Default');
my $default = 'Cpanel::Config::CpConfGuard::Default'->new(
current_config => $self->{data},
current_changes => $self->{changes},
);
if ( $self->{'is_missing'} ) {
if ( UNIVERSAL::isa( $self->{'cache'}, 'HASH' ) && %{ $self->{'cache'} } ) {
$self->{'data'} = {};
%{ $self->{'data'} } = %{ $self->{'cache'} };
my $config = $self->{'data'};
foreach my $key ( $default->get_keys() ) {
next if exists $config->{$key};
$config->{$key} = $default->get_default_for($key);
}
}
else {
$self->{'data'} = $default->get_all_defaults();
}
$self->{'modified'} = 1; # Mark as save needed.
return;
}
my $cache = $self->{'cache'};
undef( $self->{'cache'} ); # we do not need the cache after the first pass
my $config = $self->{'data'};
my $changes = $self->{'changes'}; # used for notifications
$config->{'tweak_unset_vars'} ||= '';
foreach my $key ( $default->get_keys() ) {
next if exists $config->{$key};
$self->{'modified'} = 1; # Mark as save needed.
if ( exists $cache->{$key} ) {
$config->{$key} = $cache->{$key};
$changes->{'from_cache'} ||= [];
push @{ $changes->{'from_cache'} }, $key;
$changes->{'changed_keys'} ||= {};
$changes->{'changed_keys'}{$key} = 'from_cache';
next;
}
my $changes_type = $default->is_dynamic($key) ? 'from_dynamic' : 'from_default';
$changes->{'changed_keys'} ||= {};
$changes->{'changed_keys'}{$key} = $changes_type;
$changes->{$changes_type} ||= [];
push @{ $changes->{$changes_type} }, $key;
$config->{$key} = $default->get_default_for($key);
}
foreach my $key ( @{ $default->dead_variables() } ) {
next unless exists $config->{$key};
$self->{'modified'} = 1; # Mark as save needed.
delete( $config->{$key} );
$changes->{'dead_variable'} ||= [];
push @{ $changes->{'dead_variable'} }, $key;
}
return;
}
sub validate_keys {
my ($self) = @_;
_verify_called_as_object_method($self);
Cpanel::LoadModule::load_perl_module('Cpanel::Config::CpConfGuard::Validate');
my $invalid = 'Cpanel::Config::CpConfGuard::Validate'->can('patch_cfg')->( $self->{'data'} );
if (%$invalid) {
$self->{modified} = 1;
$self->{'changes'}->{'invalid'} = $invalid;
}
return;
}
sub notify_and_save_if_changed {
my ($self) = @_;
_verify_called_as_object_method($self);
return if !$self->{'use_lock'};
return if !$self->{'modified'};
my $config = $self->{'data'};
if ( $ENV{'CPANEL_BASE_INSTALL'} ) {
; # Do nothing for notification.
}
elsif ( $self->{'is_missing'} ) {
$config->{'tweak_unset_vars'} = '';
Cpanel::Debug::log_warn("Missing cpanel.config regenerating …");
$self->notify_missing_file;
}
elsif ( %{ $self->{'changes'} } ) {
my $changes = $self->{'changes'};
my %uniq = map { $_ => 1 } @{ $changes->{'from_default'} || [] }, @{ $changes->{'from_dynamic'} || [] }, split( /\s*,\s*/, $config->{'tweak_unset_vars'} );
$config->{'tweak_unset_vars'} = join ",", sort keys %uniq;
$self->log_missing_values();
}
return $self->save( keep_lock => 1 );
}
sub _server_locale {
my ($self) = @_;
_verify_called_as_object_method($self);
my $locale_name = $self->{'data'}->{'server_locale'} || 'en';
require Cpanel::Locale;
return Cpanel::Locale->_real_get_handle($locale_name);
}
sub _longest {
my @array = @_;
return length( ( sort { length $b <=> length $a } @array )[0] );
}
sub _stringify_undef {
my $value = shift;
return defined $value ? $value : '';
}
sub log_missing_values {
my ($self) = @_;
require Cpanel::Hostname;
my $changes = $self->{'changes'};
my $locale = $self->_server_locale();
my $hostname = Cpanel::Hostname::gethostname();
my $prev = $locale->set_context_plain();
my $message = '';
$message .= $locale->maketext( 'One or more key settings for “[_1]” were either not found in [asis,cPanel amp() WHM]’s server configuration file ([_2]), or were present but did not pass validation.', $hostname, $self->{'path'} ) . "\n";
if ( $changes->{'from_dynamic'} ) {
$message .= $locale->maketext('The following settings were absent and have been selected based on the current state of your installation.');
$message .= "\n";
my @keys = @{ $changes->{'from_dynamic'} };
my $max_len = _longest(@keys) + 2;
foreach my $key (@keys) {
$message .= sprintf( " %-${max_len}s= %s\n", $key, _stringify_undef( $self->{'data'}->{$key} ) );
}
$message .= "\n";
}
if ( $changes->{'from_cache'} ) {
$message .= $locale->maketext('The following settings were absent, but were restored from your [asis,cpanel.config.cache] file:');
$message .= "\n";
my @keys = @{ $changes->{'from_cache'} };
my $max_len = _longest(@keys) + 2;
foreach my $key (@keys) {
$message .= sprintf( " %-${max_len}s= %s\n", $key, _stringify_undef( $self->{'data'}->{$key} ) );
}
$message .= "\n";
}
if ( $changes->{'from_default'} or $changes->{'invalid'} ) {
$message .= $locale->maketext('The following settings were absent or invalid. Your server has copied the defaults for them from the configuration defaults file ([asis,/usr/local/cpanel/etc/cpanel.config]).');
$message .= "\n";
if ( $changes->{'from_default'} ) {
my @keys = @{ $changes->{'from_default'} };
my $max_len = _longest(@keys) + 2;
foreach my $key (@keys) {
$message .= sprintf( " %-${max_len}s= %s\n", $key, _stringify_undef( $self->{'data'}->{$key} ) );
}
}
if ( $changes->{'invalid'} ) {
my $invalid = $changes->{'invalid'};
my @keys = keys %$invalid;
my $max_len = _longest(@keys) + 2;
foreach my $key (@keys) {
$message .= sprintf( " %-${max_len}s= %s (Previously set to '%s')\n", $key, _stringify_undef( $invalid->{$key}->{'to'} ), _stringify_undef( $invalid->{$key}->{'from'} ) );
}
}
$message .= "\n";
}
if ( $changes->{'dead_variable'} ) {
$message .= $locale->maketext('The following settings are obsolete and have been removed from the server configuration file:');
$message .= "\n";
$message .= ' ' . join( ', ', @{ $changes->{'dead_variable'} } );
$message .= "\n\n";
}
$message .= $locale->maketext( 'Read the [asis,cpanel.config] file [output,url,_1,documentation] for important information about this file.', 'https://go.cpanel.net/cpconfig' );
$message .= "\n\n";
Cpanel::Debug::logger(); # initialize the logger
local $Cpanel::Logger::ENABLE_BACKTRACE = 0;
foreach my $chunk ( split( /\n+/, $message ) ) {
Cpanel::Debug::log_warn($chunk);
}
$locale->set_context($prev);
return;
}
sub notify_missing_file {
my ($self) = @_;
if ($SENDING_MISSING_FILE_NOTICE) {
return; #Already sending notification, don't double up
}
require Cpanel::Hostname;
local $SENDING_MISSING_FILE_NOTICE = 1;
my $locale = $self->_server_locale();
my $prev = $locale->set_context_plain();
my @to_log;
my %critical_values;
my $hostname = Cpanel::Hostname::gethostname();
push @to_log, $locale->maketext('Your server has copied the defaults from your cache and the configuration defaults file ([asis,/usr/local/cpanel/etc/cpanel.config]) to [asis,/var/cpanel/cpanel.config], and it has generated the following critical values:');
Cpanel::LoadModule::load_perl_module('Cpanel::Config::CpConfGuard::Default');
my $critical = Cpanel::Config::CpConfGuard::Default::critical_values();
my $max_len = _longest(@$critical) + 2;
my $critical_value;
foreach my $key ( sort @$critical ) {
$critical_value = _stringify_undef( $self->{'data'}->{$key} );
$critical_values{$key} = $critical_value;
push @to_log, sprintf( " %-${max_len}s= %s\n", $key, $critical_value );
}
push @to_log, $locale->maketext( 'Read the [asis,cpanel.config] file [output,url,_1,documentation] for more information about this file.', 'https://go.cpanel.net/cpconfig' ) . ' ';
Cpanel::Debug::logger(); # initialize the logger
local $Cpanel::Logger::ENABLE_BACKTRACE = 0;
foreach my $chunk (@to_log) {
chomp $chunk;
Cpanel::Debug::log_warn($chunk);
}
_icontact( \%critical_values );
$locale->set_context($prev);
return;
}
sub _icontact {
my $critical_values = shift;
Cpanel::LoadModule::load_perl_module("Cpanel::iContact::Class::Config::CpConfGuard");
Cpanel::LoadModule::load_perl_module('Cpanel::Notify');
'Cpanel::Notify'->can('notification_class')->(
'class' => 'Config::CpConfGuard',
'application' => 'Config::CpConfGuard',
'constructor_args' => [
'origin' => 'cpanel.config',
'critical_values' => $critical_values,
]
);
return;
}
sub save {
my ( $self, %opts ) = @_;
_verify_called_as_object_method($self);
return unless ( $self->{'use_lock'} );
return if ( $] > 5.007 && $] < 5.014 );
return 1 if $Cpanel::Config::CpConfGuard::memory_only;
if ( !$self->{'rw'} ) {
Cpanel::LoadModule::load_perl_module('Cpanel::SafeFile');
$self->{'fh'} = 'Cpanel::SafeFile'->can('safereopen')->( $self->{'fh'}, '+>', $Cpanel::ConfigFiles::cpanel_config_file );
return $self->abort('Cannot reopen file for rw') unless $self->{'fh'};
$self->{'rw'} = 1;
}
return $self->abort('Locked in parent, cannot save') if $self->{'pid'} != $$;
return $self->abort('hash reference required') if !UNIVERSAL::isa( $self->{'data'}, 'HASH' );
Cpanel::LoadModule::load_perl_module('Cpanel::Config::FlushConfig');
Cpanel::LoadModule::load_perl_module('Cpanel::Config::SaveCpConf');
'Cpanel::Config::FlushConfig'->can('flushConfig')->(
$self->{'fh'},
$self->{'data'},
'=',
'Cpanel::Config::SaveCpConf'->can('header_message')->(),
{
sort => 1,
perms => $FILESYS_PERMS,
},
);
%{$Cpanel::Config::CpConfGuard::MEM_CACHE} = %{ $self->{'data'} };
return 1 if $opts{keep_lock};
$self->release_lock;
return 1;
}
sub _update_cache {
my ($self) = @_;
_verify_called_as_object_method($self);
return 0 if Cpanel::Config::CpConfGuard::_cache_is_valid() && $self->{'cache_is_valid'}; # Don't re-write the file if it looks correct.
$Cpanel::Config::CpConfGuard::MEM_CACHE_CPANEL_CONFIG_MTIME = ( stat($Cpanel::ConfigFiles::cpanel_config_file) )[9] || 0;
return unless $self->{'use_lock'}; # never update the cache when not root
local $@;
my $ok = eval { Cpanel::FileUtils::Write::JSON::Lazy::write_file( $Cpanel::ConfigFiles::cpanel_config_cache_file, $Cpanel::Config::CpConfGuard::MEM_CACHE, $FILESYS_PERMS ) || 0 };
if ( !$ok ) {
if ( !defined $ok ) {
Cpanel::Debug::log_warn("Cannot update cache file: $Cpanel::ConfigFiles::cpanel_config_cache_file $@");
unlink $Cpanel::ConfigFiles::cpanel_config_cache_file;
return -1;
}
return;
}
my $past = ( stat($Cpanel::ConfigFiles::cpanel_config_cache_file) )[9] - 1;
return _adjust_timestamp_for( $Cpanel::ConfigFiles::cpanel_config_file => $past );
}
sub _adjust_timestamp_for {
my ( $f, $time ) = @_;
return unless defined $f && defined $time;
return 1 if utime( $time, $time, $f );
my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) = localtime($time);
my $stamp = sprintf( "%04d%02d%02d%02d%02d.%02d", $year + 1900, $mon + 1, $mday, $hour, $min, $sec );
unless ( _touch( $f => $stamp ) ) {
Cpanel::Debug::log_warn("Cannot update mtime on $f: $@");
return;
}
return 1;
}
sub _touch { # mainly created to easily mock that part during the tests
my ( $f, $stamp ) = @_;
return system( 'touch', '-t', $stamp, $f ) == 0 ? 1 : 0;
}
sub _verify_called_as_object_method {
if ( ref( $_[0] ) ne "Cpanel::Config::CpConfGuard" ) {
die '' . ( caller(0) )[3] . " was not called as an object method [" . ref( $_[0] ) . "]\n";
}
return;
}
sub abort {
my ( $self, $msg ) = @_;
_verify_called_as_object_method($self);
if ( $self->{'pid'} != $$ ) {
Cpanel::Debug::log_die('Locked in parent, cannot release lock');
return;
}
$self->release_lock();
Cpanel::Debug::log_die($msg) if $msg;
return 1;
}
sub set {
my ( $self, $k, $v ) = @_;
_verify_called_as_object_method($self);
return unless defined $k;
my $config = $self->{'data'};
$config->{$k} = $v;
if ( $config->{'tweak_unset_vars'} && index( $config->{'tweak_unset_vars'}, $k ) > -1 ) {
my %unset = map { ( $_ => 1 ) } split( /\s*,\s*/, $config->{'tweak_unset_vars'} );
delete( $unset{$k} );
$config->{'tweak_unset_vars'} = join( ',', sort keys %unset );
}
return 1;
}
1;
} # --- END Cpanel/Config/CpConfGuard/CORE.pm
{ # --- BEGIN Cpanel/Config/CpConfGuard.pm
package Cpanel::Config::CpConfGuard;
use strict;
use warnings;
# use Cpanel::JSON::FailOK ();
# use Cpanel::ConfigFiles ();
# use Cpanel::Debug ();
# use Cpanel::Destruct ();
use constant {
_ENOENT => 2,
};
our $IN_LOAD = 0;
our $SENDING_MISSING_FILE_NOTICE = 0;
my $FILESYS_PERMS = 0644;
my $is_daemon;
BEGIN {
$is_daemon = 0; # initialize the value in the begin block
if ( index( $0, 'updatenow' ) > -1
|| index( $0, 'cpsrvd' ) > -1
|| index( $0, 'cpdavd' ) > -1
|| index( $0, 'queueprocd' ) > -1
|| index( $0, 'tailwatchd' ) > -1
|| index( $0, 'cpanellogd' ) > -1
|| ( length $0 > 7 && substr( $0, -7 ) eq '.static' ) ) {
$is_daemon = 1;
}
}
my $module_file;
our ( $MEM_CACHE_CPANEL_CONFIG_MTIME, $MEM_CACHE ) = ( 0, undef );
our $memory_only;
sub _is_daemon { $is_daemon }; # for testing
sub clearcache {
$MEM_CACHE_CPANEL_CONFIG_MTIME = 0;
$MEM_CACHE = undef;
return;
}
sub new {
my ( $class, %opts ) = @_;
Cpanel::JSON::FailOK::LoadJSONModule() if !$is_daemon && !$INC{'Cpanel/JSON.pm'};
my $self = bless {
%opts, # to be improved
'path' => $Cpanel::ConfigFiles::cpanel_config_file,
'pid' => $$,
'modified' => 0,
'changes' => {},
}, $class;
$self->{'use_lock'} //= ( $> == 0 ) ? 1 : 0;
if ($memory_only) {
$self->{'data'} = ref($memory_only) eq 'HASH' ? $memory_only : {};
return $self;
}
( $self->{'cache'}, $self->{'cache_is_valid'} ) = get_cache();
return $self if $self->{'loadcpconf'} && $self->{'cache_is_valid'};
$self->load_cpconf_file();
return $self if $is_daemon || $opts{'no_validate'} || !$self->{'use_lock'};
$self->find_missing_keys();
$self->validate_keys();
$self->notify_and_save_if_changed();
return $self;
}
sub set {
require Cpanel::Config::CpConfGuard::CORE;
goto \&Cpanel::Config::CpConfGuard::CORE::set;
}
sub config_copy {
my ($self) = @_;
_verify_called_as_object_method($self);
my $config = $self->{'data'} || $self->{'cache'} || {};
return {%$config};
}
sub find_missing_keys {
require Cpanel::Config::CpConfGuard::CORE;
goto \&Cpanel::Config::CpConfGuard::CORE::find_missing_keys;
}
sub validate_keys {
require Cpanel::Config::CpConfGuard::CORE;
goto \&Cpanel::Config::CpConfGuard::CORE::validate_keys;
}
sub notify_and_save_if_changed {
require Cpanel::Config::CpConfGuard::CORE;
goto \&Cpanel::Config::CpConfGuard::CORE::notify_and_save_if_changed;
}
sub log_missing_values {
require Cpanel::Config::CpConfGuard::CORE;
goto \&Cpanel::Config::CpConfGuard::CORE::log_missing_values;
}
sub notify_missing_file {
require Cpanel::Config::CpConfGuard::CORE;
goto \&Cpanel::Config::CpConfGuard::CORE::notify_missing_file;
}
sub save {
require Cpanel::Config::CpConfGuard::CORE;
goto \&Cpanel::Config::CpConfGuard::CORE::save;
}
sub release_lock {
my ($self) = @_;
_verify_called_as_object_method($self);
return unless $self->{'use_lock'} && defined $self->{'pid'} && $self->{'pid'} eq $$ && $self->{'lock'};
require Cpanel::SafeFile;
Cpanel::SafeFile::safeclose( $self->{'fh'}, $self->{'lock'}, sub { return $self->_update_cache() } );
$self->{'fh'} = $self->{'lock'} = undef;
$self->{'is_locked'} = 0;
return;
}
sub abort {
require Cpanel::Config::CpConfGuard::CORE;
goto \&Cpanel::Config::CpConfGuard::CORE::abort;
}
sub _update_cache {
require Cpanel::Config::CpConfGuard::CORE;
goto \&Cpanel::Config::CpConfGuard::CORE::_update_cache;
}
sub _server_locale {
require Cpanel::Config::CpConfGuard::CORE;
goto \&Cpanel::Config::CpConfGuard::CORE::_server_locale;
}
sub get_cache {
my $cpanel_config_mtime = ( stat($Cpanel::ConfigFiles::cpanel_config_file) )[9] || 0;
my $verbose = ( defined $Cpanel::Debug::level ? $Cpanel::Debug::level : 0 ) >= 5;
if ( $MEM_CACHE && ref($MEM_CACHE) eq 'HASH' && $cpanel_config_mtime && $cpanel_config_mtime == $MEM_CACHE_CPANEL_CONFIG_MTIME ) {
Cpanel::Debug::log_info("loadcpconf memory cache hit") if $verbose;
return ( $MEM_CACHE, 1 );
}
clearcache(); # Invalidate the memory cache.
Cpanel::Debug::log_info("loadcpconf memory cache miss") if $verbose;
my $mtime_before_read;
if ( !$INC{'Cpanel/JSON.pm'} ) {
Cpanel::Debug::log_info("Cpanel::JSON not loaded. Skipping cache load.") if $verbose;
return ( undef, 0 );
}
elsif ( -e $Cpanel::ConfigFiles::cpanel_config_cache_file ) { # No need to do -r (costs 5 additional syscalls) since we write this 0644
$mtime_before_read = ( stat _ )[9] || 0;
}
else {
Cpanel::Debug::log_info("The cache file “$Cpanel::ConfigFiles::cpanel_config_cache_file” could not be read. Skipping cache load.") if $verbose;
return ( undef, 0 );
}
my ( $mtime_after_read, $cpconf_ref ) = (0);
my $loop_count = 0;
while ( $mtime_after_read != $mtime_before_read && $loop_count++ < 10 ) {
sleep 1 if ( $mtime_after_read == time ); # If it was just written to, give it a second in case it's being written to.
Cpanel::Debug::log_info( "loadcpconf cache_filesys_mtime = $mtime_before_read , filesys_mtime: $cpanel_config_mtime , memory_mtime: $MEM_CACHE_CPANEL_CONFIG_MTIME , now: " . time ) if $verbose;
$cpconf_ref = Cpanel::JSON::FailOK::LoadFile($Cpanel::ConfigFiles::cpanel_config_cache_file);
$mtime_after_read = ( stat($Cpanel::ConfigFiles::cpanel_config_cache_file) )[9] || 0;
sleep 1 if ( $mtime_after_read != $mtime_before_read );
}
if ( $cpconf_ref && scalar keys %{$cpconf_ref} ) {
if ( _cache_is_valid( $cpanel_config_mtime, $mtime_after_read ) ) {
Cpanel::Debug::log_info("loadcpconf file system cache hit") if $verbose;
( $MEM_CACHE, $MEM_CACHE_CPANEL_CONFIG_MTIME ) = ( $cpconf_ref, $cpanel_config_mtime );
return ( $cpconf_ref, 1 );
}
Cpanel::Debug::log_info("loadcpconf cpanel.config.cache miss.") if $verbose;
return ( $cpconf_ref, 0 );
}
Cpanel::Debug::log_info("loadcpconf cpanel.config.cache miss.") if $verbose;
return ( undef, 0 );
}
sub _cache_is_valid {
my ( $config_mtime, $cache_mtime ) = @_;
$cache_mtime ||= ( stat($Cpanel::ConfigFiles::cpanel_config_cache_file) )[9] || 0;
return 0 unless $cache_mtime;
$config_mtime ||= ( stat($Cpanel::ConfigFiles::cpanel_config_file) )[9] || 0;
return 0 unless $config_mtime;
return ( $config_mtime + 1 == $cache_mtime ) ? 1 : 0;
}
sub load_cpconf_file {
my ($self) = @_;
if ($IN_LOAD) {
require Cpanel::Carp;
die Cpanel::Carp::safe_longmess("Load loop detected");
}
local $IN_LOAD = 1;
_verify_called_as_object_method($self);
my $config = {};
my $config_file = $Cpanel::ConfigFiles::cpanel_config_file;
$self->{'is_missing'} = ( -e $config_file ) ? 0 : 1;
return if ( !$self->{'use_lock'} && $self->{'is_missing'} ); # We can't do anything if the file is missing and we're not root. ABORT!
if ( $self->{'use_lock'} && $self->{'is_missing'} ) {
if ( open( my $touch_fh, '>>', $config_file ) ) {
print {$touch_fh} '';
close $touch_fh;
chown 0, 0, $config_file; # avoid pulling in Cpanel::PwCache for memory reasons
chmod 0644, $config_file;
}
}
$self->{'rw'} = 0;
$self->{'rw'} = 1 if ( $self->{'use_lock'} && !$self->{'cache_is_valid'} );
require Cpanel::Config::LoadConfig;
my ( $ref, $fh, $conflock, $err ) = Cpanel::Config::LoadConfig::loadConfig(
$Cpanel::ConfigFiles::cpanel_config_file,
$config,
(undef) x 4,
{
'keep_locked_open' => !!$self->{'use_lock'},
'nocache' => 1,
'rw' => $self->{'rw'},
'allow_undef_values' => 1,
},
);
if ( !$ref && !$fh && $! != _ENOENT() ) {
$err ||= '(unknown error)';
require Cpanel::Carp;
die Cpanel::Carp::safe_longmess("Can’t read “$Cpanel::ConfigFiles::cpanel_config_file” ($err)");
}
$self->{'fh'} = $fh;
$self->{'lock'} = $conflock;
$self->{'data'} = $config;
if ( $self->{'use_lock'} ) {
Cpanel::Debug::log_warn("Failed to establish lock on $Cpanel::ConfigFiles::cpanel_config_file") unless $self->{'lock'};
Cpanel::Debug::log_warn("Failed to get file handle for $Cpanel::ConfigFiles::cpanel_config_file") unless $self->{'fh'};
}
$self->{'is_locked'} = defined $self->{'lock'} ? 1 : 0; # alias for external usage
if ( !$MEM_CACHE ) {
$MEM_CACHE = {};
%$MEM_CACHE = %$config;
}
return;
}
sub _verify_called_as_object_method {
if ( ref( $_[0] ) ne __PACKAGE__ ) {
die '' . ( caller(0) )[3] . " was not called as an object method [" . ref( $_[0] ) . "]\n";
}
return;
}
sub DESTROY { ## no critic(RequireArgUnpacking)
return 1 if ( $> || $memory_only ); # Special modes we don't or won't write to cpanel.config files.
return 2 if ( !$_[0] || !keys %{ $_[0] } ); # Nothing to cleanup if we're just a blessed empty hash.
return if !$_[0]->{'lock'};
return if Cpanel::Destruct::in_dangerous_global_destruction();
$_[0]->release_lock(); # Close the file so we can update the cache properly.
return;
}
1;
} # --- END Cpanel/Config/CpConfGuard.pm
{ # --- BEGIN Cpanel/Config/LoadCpConf.pm
package Cpanel::Config::LoadCpConf;
use strict;
use warnings;
# use Cpanel::Config::CpConfGuard ();
sub loadcpconf {
my $cpconf = Cpanel::Config::CpConfGuard->new( 'loadcpconf' => 1 )->config_copy;
return wantarray ? %$cpconf : $cpconf;
}
sub loadcpconf_not_copy {
if ( !defined $Cpanel::Config::CpConfGuard::memory_only && $Cpanel::Config::CpConfGuard::MEM_CACHE_CPANEL_CONFIG_MTIME ) {
my ( $cache, $cache_is_valid ) = Cpanel::Config::CpConfGuard::get_cache();
if ($cache_is_valid) {
return wantarray ? %$cache : $cache;
}
}
my $cpconf_obj = Cpanel::Config::CpConfGuard->new( 'loadcpconf' => 1 );
my $cpconf = $cpconf_obj->{'data'} || $cpconf_obj->{'cache'} || {};
return wantarray ? %$cpconf : $cpconf;
}
sub clearcache;
*clearcache = *Cpanel::Config::CpConfGuard::clearcache;
1;
} # --- END Cpanel/Config/LoadCpConf.pm
{ # --- BEGIN Cpanel/Maxmem.pm
package Cpanel::Maxmem;
use strict;
use warnings;
# use Cpanel::Config::LoadUserDomains::Count ();
use constant _INITIAL_DEFAULT => 4096;
sub _count_domains {
return eval { Cpanel::Config::LoadUserDomains::Count::countuserdomains() } // 1;
}
sub minimum {
return _INITIAL_DEFAULT() * ( 1 + int( _count_domains() / 10_000 ) );
}
*default = *minimum;
1;
} # --- END Cpanel/Maxmem.pm
{ # --- BEGIN Cpanel/OSSys/Bits.pm
package Cpanel::OSSys::Bits;
use strict;
use warnings;
our $MAX_32_BIT_SIGNED;
our $MAX_32_BIT_UNSIGNED;
our $MAX_64_BIT_SIGNED;
our $MAX_64_BIT_UNSIGNED;
our $MAX_NATIVE_SIGNED;
our $MAX_NATIVE_UNSIGNED;
sub getbits {
return length( pack( 'l!', 1000 ) ) * 8;
}
BEGIN {
$MAX_32_BIT_UNSIGNED = ( 1 << 32 ) - 1;
$MAX_32_BIT_SIGNED = ( 1 << 31 ) - 1;
$MAX_64_BIT_UNSIGNED = ~0; #true on both 32- and 64-bit systems
$MAX_64_BIT_SIGNED = -1 >> 1; #true on both 32- and 64-bit systems
if ( getbits() == 32 ) {
$MAX_NATIVE_SIGNED = $MAX_32_BIT_SIGNED;
$MAX_NATIVE_UNSIGNED = $MAX_32_BIT_UNSIGNED;
}
else {
$MAX_NATIVE_SIGNED = $MAX_64_BIT_SIGNED;
$MAX_NATIVE_UNSIGNED = $MAX_64_BIT_UNSIGNED;
}
}
1;
} # --- END Cpanel/OSSys/Bits.pm
{ # --- BEGIN Cpanel/Sys/Rlimit.pm
package Cpanel::Sys::Rlimit;
use strict;
use warnings;
# use Cpanel::OSSys::Bits ();
# use Cpanel::Pack ();
# use Cpanel::Syscall ();
my $SYS_getrlimit;
my $SYS_setrlimit;
our $RLIM_INFINITY; # denotes no limit on a resource
our %RLIMITS = (
'CPU' => 0, # CPU time limit in seconds.
'DATA' => 2, # The maximum size of the process's data segment
'CORE' => 4, # Maximum size of a core file
'RSS' => 5, # Specifies the limit (in pages) of the process's resident set
'NPROC' => 6, # The maximum number of processes
'NOFILE' => 7, # The maximum number of file descriptors
'AS' => 9, # The maximum size of the process's virtual memory
'FSIZE' => 1,
'STACK' => 3,
'MEMLOCK' => 8,
'LOCKS' => 10,
'SIGPENDING' => 11,
'MSGQUEUE' => 12,
'NICE' => 13,
'RTPRIO' => 14,
'RTTIME' => 15,
);
BEGIN {
$RLIM_INFINITY = $Cpanel::OSSys::Bits::MAX_NATIVE_UNSIGNED;
}
our $PACK_TEMPLATE = 'L!L!';
our @TEMPLATE = (
rlim_cur => 'L!', # unsigned long
rlim_max => 'L!', # unsigned long
);
sub getrlimit {
my ($rlimit) = @_;
local $!;
die "getrlimit requires an rlimit constant" if !defined $rlimit;
my $buffer = pack( $PACK_TEMPLATE, 0 );
my $rlimit_num = _rlimit_to_num($rlimit);
Cpanel::Syscall::syscall( 'getrlimit', $rlimit_num, $buffer );
my $getrlimit_hr = Cpanel::Pack->new( \@TEMPLATE )->unpack_to_hashref($buffer);
return ( $getrlimit_hr->{'rlim_cur'}, $getrlimit_hr->{'rlim_max'} );
}
sub setrlimit {
my ( $rlimit, $soft, $hard ) = @_;
local $!;
die "setrlimit requires an rlimit constant" if !defined $rlimit;
die "setrlimit requires a soft limit" if !defined $soft;
die "setrlimit requires a hard limit" if !defined $hard;
my $buffer = pack( $PACK_TEMPLATE, $soft, $hard );
my $rlimit_num = _rlimit_to_num($rlimit);
Cpanel::Syscall::syscall( 'setrlimit', $rlimit_num, $buffer );
return 1;
}
sub _rlimit_to_num {
my ($rlimit) = @_;
if ( length($rlimit) && $rlimit !~ tr<0-9><>c ) {
return $rlimit;
}
elsif ( exists $RLIMITS{$rlimit} ) {
return $RLIMITS{$rlimit};
}
die "Unknown RLIMIT: $rlimit";
}
1;
} # --- END Cpanel/Sys/Rlimit.pm
{ # --- BEGIN Cpanel/Rlimit.pm
package Cpanel::Rlimit;
use strict;
# use Cpanel::Config::LoadCpConf ();
# use Cpanel::Maxmem ();
# use Cpanel::Sys::Rlimit ();
sub set_rlimit {
my ( $limit, $limit_names ) = @_;
my ( $default_rlimit, $coredump_are_enabled ) = _get_server_setting_or_default();
$limit ||= $default_rlimit || $Cpanel::Sys::Rlimit::RLIM_INFINITY;
$limit_names ||= [qw/RSS AS/];
my $core_limit = $coredump_are_enabled ? $limit : 0;
if ( $limit > $Cpanel::Sys::Rlimit::RLIM_INFINITY ) {
require Cpanel::Logger;
Cpanel::Logger->new->warn("set_rlimit adjusted the requested limit of “$limit” to infinity because it exceeded the maximum allowed value.");
$limit = $Cpanel::Sys::Rlimit::RLIM_INFINITY;
}
my $error = '';
foreach my $lim (@$limit_names) {
local $@;
eval { Cpanel::Sys::Rlimit::setrlimit( $lim, $limit, $limit ) } or do {
my $limit_human_value = ( $limit == $Cpanel::Sys::Rlimit::RLIM_INFINITY ? 'INFINITY' : $limit );
$error .= "$$: Unable to set RLIMIT_$lim to $limit_human_value: $@\n";
}
}
local $@;
eval { Cpanel::Sys::Rlimit::setrlimit( 'CORE', $core_limit, $core_limit ) }
or $error .= "$$: Unable to set RLIMIT_CORE to $core_limit: $@\n";
if ($error) {
$error =~ s/\n$//;
require Cpanel::Logger;
Cpanel::Logger->new->warn($error);
return 0;
}
return 1;
}
sub set_min_rlimit {
my ($min) = @_;
my $error = '';
foreach my $lim (qw(RSS AS)) {
my ( $current_soft, $current_hard ) = Cpanel::Sys::Rlimit::getrlimit($lim);
if ( $current_soft < $min || $current_hard < $min ) {
local $@;
eval { Cpanel::Sys::Rlimit::setrlimit( $lim, $min, $min ) } or $error .= "$$: Unable to set RLIMIT_$lim to $min: $@\n";
}
}
if ($error) {
$error =~ s/\n$//;
require Cpanel::Logger;
Cpanel::Logger->new->warn($error);
return 0;
}
return 1;
}
sub get_current_rlimits {
return { map { $_ => [ Cpanel::Sys::Rlimit::getrlimit($_) ] } (qw(RSS AS CORE)) };
}
sub restore_rlimits {
my $limit_hr = shift;
my $error = '';
if ( ref $limit_hr eq 'HASH' ) {
foreach my $resource_name ( keys %{$limit_hr} ) {
my $values = $limit_hr->{$resource_name};
if ( ref $values ne 'ARRAY' || scalar @{$values} != 2 ) {
$error .= "Invalid limit arguments, could not restore resource limit for $resource_name.\n";
next;
}
local $@;
eval { Cpanel::Sys::Rlimit::setrlimit( $resource_name, $values->[0], $values->[1] ) }
or $error .= "$$: Unable to set $resource_name to $values->[0] and $values->[1]: $@\n";
}
}
else {
$error .= "Invalid arguments, could not restore resource limits.\n";
}
if ($error) {
$error =~ s/\n$//;
require Cpanel::Logger;
Cpanel::Logger->new->warn($error);
return 0;
}
return 1;
}
sub set_rlimit_to_infinity {
return set_rlimit($Cpanel::Sys::Rlimit::RLIM_INFINITY);
}
sub set_open_files_to_maximum {
my $limit = 1048576;
if ( open( my $fh, '<', '/proc/sys/fs/nr_open' ) ) {
$limit = <$fh>;
chomp($limit);
close($fh);
}
return set_rlimit( $limit, [qw/NOFILE/] );
}
sub _get_server_setting_or_default {
my $cpconf = Cpanel::Config::LoadCpConf::loadcpconf_not_copy();
my $default_maxmem = Cpanel::Maxmem::default();
my $core_dumps_enabled = $cpconf->{'coredump'};
my $configured_maxmem = exists $cpconf->{'maxmem'} ? int( $cpconf->{'maxmem'} || 0 ) : $default_maxmem;
if ( $configured_maxmem && $configured_maxmem < $default_maxmem ) {
return ( _mebibytes_to_bytes($default_maxmem), $core_dumps_enabled );
}
elsif ( $configured_maxmem == 0 ) {
return ( $Cpanel::Sys::Rlimit::RLIM_INFINITY, $core_dumps_enabled );
}
else {
return ( _mebibytes_to_bytes($configured_maxmem), $core_dumps_enabled );
}
}
sub _mebibytes_to_bytes {
my $mebibytes = shift;
return ( $mebibytes * 1024**2 );
}
1;
} # --- END Cpanel/Rlimit.pm
package main;
# cpanel - scripts/upcp Copyright 2022 cPanel, L.L.C.
# All rights reserved.
# copyright@cpanel.net http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited
package scripts::upcp;
BEGIN {
unshift @INC, q{/usr/local/cpanel};
# if we are being called with a compile check flag ( perl -c ), skip the begin block
# so we don't actually call upcp.static when just checking syntax and such is OK
return if $^C;
# static never gets --use-checked and should pass all the begin block checks
return if $0 =~ /\.static$/;
# let the '--use-check' instance compiled
if ( grep { $_ eq '--use-check' } @ARGV ) {
no warnings;
# dynamic definition of the INIT block
eval "INIT { exit(0); }";
return;
}
system("$0 --use-check >/dev/null 2>&1");
# compilation is ok with '--use-check', we will continue the non static version
return if $? == 0;
my $static = $0 . ".static";
if ( -f $static ) {
print STDERR "We determined that $0 had compilation issues..\n";
print STDERR "Trying to exec $static " . join( ' ', @ARGV ) . "\n";
exec( $^X, $static, @ARGV );
}
}
use cPstrict;
no warnings; ## no critic qw(ProhibitNoWarnings)
use Try::Tiny;
# use Cpanel::OS::All (); # PPI USE OK -- make sure Cpanel::OS is embedded
# use Cpanel::HiRes ( preload => 'perl' );
# use Cpanel::Env ();
# use Cpanel::Update::IsCron ();
# use Cpanel::Update::Logger ();
# use Cpanel::FileUtils::TouchFile ();
# use Cpanel::LoadFile ();
# use Cpanel::LoadModule ();
# use Cpanel::Usage ();
# use Cpanel::UPID ();
use IO::Handle ();
use POSIX ();
# use Cpanel::Unix::PID::Tiny ();
my $pidfile = '/var/run/upcp.pid';
my $lastlog = '/var/cpanel/updatelogs/last';
my $upcp_disallowed_path = '/root/.upcp_controlc_disallowed';
my $version_upgrade_file = '/usr/local/cpanel/upgrade_in_progress.txt';
our $logger; # Global for logger object.
our $logfile_path;
my $now;
my $forced = 0;
my $fromself = 0;
my $sync_requested = 0;
my $bg = 0;
my $from_version;
my $pbar_starting_point;
exit( upcp() || 0 ) unless caller();
sub usage {
print <&STDOUT" ) or die $!;
local $| = 1;
umask(0022);
$now = time();
setupenv();
unset_rlimits();
#############################################################################
# Record the arguments used when started, check for certain flags
my $update_is_available_exit_code = 42;
my @retain_argv = @ARGV;
foreach my $arg (@ARGV) {
if ( $arg =~ m/^--log=(.*)/ ) {
$logfile_path = $1;
}
elsif ( $arg eq '--fromself' ) {
$fromself = 1;
}
elsif ( $arg eq '--force' ) {
$forced = 1;
$ENV{'FORCEDCPUPDATE'} = 1;
}
elsif ( $arg eq '--sync' ) {
$sync_requested = 1;
}
elsif ( $arg eq '--bg' ) {
$bg = 1;
}
}
if ( $sync_requested && $forced ) {
print "FATAL: --force and --sync are mutually exclusive commands.\n";
print " Force is designed to update your installed version, regardless of whether it's already up to date.\n";
print " Sync is designed to update the version already installed, regardless of what is available.\n";
return 1;
}
if ( $> != 0 ) {
die "upcp must be run as root";
}
#############################################################################
# Make sure easyapache isn't already running
my $upid = Cpanel::Unix::PID::Tiny->new();
if ( $upid->is_pidfile_running('/var/run/easyapache.pid') ) {
print "EasyApache is currently running. Please wait for EasyApache to complete before running cPanel Update (upcp).\n";
return 1;
}
#############################################################################
# Make sure we aren't already running && make sure everyone knows we are running
my $curpid = $upid->get_pid_from_pidfile($pidfile) || 0;
if ( $curpid && $curpid != $$ && !$fromself && -e '/var/cpanel/upcpcheck' ) {
my $pidfile_mtime = ( stat($pidfile) )[9];
my $pidfile_age = ( time - $pidfile_mtime );
if ( $pidfile_age > 21600 ) { # Running for > 6 hours
_logger()->warning("previous PID ($curpid) has been running more than 6 hours. Killing processes.");
kill_upcp($curpid); # the pid_file_no_cleanup() will exit if it is still stuck after this
sleep 1; # Give the process group time to die.
}
elsif ( my $logpath = _determine_logfile_path_if_running($curpid) ) {
print _message_about_already_running( $curpid, $logpath ) . "\n";
return 1;
}
}
if ( $curpid && $curpid != $$ && !$upid->is_pidfile_running($pidfile) ) {
print "Stale PID file '$pidfile' (pid=$curpid)\n";
}
if ( !$fromself && !$upid->pid_file_no_cleanup($pidfile) ) {
print "process is already running\n";
return 1;
}
# to indicate re-entry into upcp
$pbar_starting_point = $fromself ? 17 : 0;
# record current version
$from_version = fetch_cpanel_version();
#############################################################################
# Set up the upcp log directory and files
setup_updatelogs();
#############################################################################
# Fork a child to the background. The child does all the heavy lifting and
# logs to a file; the parent just watches, reads, and parses the log file,
# displaying what it gets.
#
# Note that the parent reads the log in proper line-oriented -- and buffered!
# -- fashion. An earlier version of this script did raw sysread() calls here,
# and had to deal with all the mess that that entailed. The current approach
# reaps all the benefits of Perl's and Linux's significant file read
# optimizations without needing to re-invent any of them. The parent loop
# below becomes lean, mean, and even elegant.
#
# Note in particular that we do not need to explicitly deal with an
# end-of-file condition (other than avoiding using undefined data). For
# exiting the read loop we merely need to test that the child has expired,
# which in any case is the only situation that can cause an eof condition for
# us on the file the child is writing.
#
# Note, too, that the open() needs to be inside this loop, in case the child
# has not yet created the file.
if ( !$fromself ) {
# we need to be sure that log an pid are the current one when giving back the end
unlink $lastlog if $bg;
if ( my $updatepid = fork() ) {
$logfile_path ||= _determine_logfile_path_if_running($updatepid);
if ($logger) { # Close if logged about killing stale process.
$logger->{'brief'} = 1; # Don't be chatty about closing
$logger->close_log;
}
if ($bg) {
print _message_about_newly_started( $updatepid, $logfile_path ) . "\n";
my $progress;
select undef, undef, undef, .10;
while ( !-e $lastlog ) {
print '.';
select undef, undef, undef, .25;
$progress = 1;
}
print "\n" if $progress;
}
else {
monitor_upcp($updatepid);
}
return;
}
else {
$logfile_path ||= _determine_logfile_path_if_running($$);
}
}
local $0 = 'cPanel Update (upcp) - Slave';
open( my $RNULL, '<', '/dev/null' ) or die "Cannot open /dev/null: $!";
chdir '/';
_logger(); # Open the log file.
#############################################################################
# Set CPANEL_IS_CRON env var based on detection algorithm
my $cron_reason = set_cron_env();
$logger->info("Detected cron=$ENV{'CPANEL_IS_CRON'} ($cron_reason)");
my $set_cron_method = $ENV{'CPANEL_IS_CRON'} ? 'set_on' : 'set_off';
Cpanel::Update::IsCron->$set_cron_method();
my $openmax = POSIX::sysconf( POSIX::_SC_OPEN_MAX() );
if ( !$openmax ) { $openmax = 64; }
foreach my $i ( 0 .. $openmax ) { POSIX::close($i) unless $i == fileno( $logger->{'fh'} ); }
POSIX::setsid();
open( STDOUT, '>', '/dev/null' ) or warn $!;
open( STDERR, '>', '/dev/null' ) or warn $!;
$logger->update_pbar($pbar_starting_point);
##############################################################################
# Symlink /var/cpanel/updatelogs/last to the current log file
unlink $lastlog;
symlink( $logfile_path, $lastlog ) or $logger->error("Could not symlink $lastlog: $!");
#############################################################################
# now that we have sporked: update our pidfile and ensure it is removed
unlink $pidfile; # so that pid_file() won't see it as running.
if ( !$upid->pid_file($pidfile) ) { # re-verifies (i.e. upcp was not also started after the unlink() and here) and sets up cleanup of $pidfile for sporked proc
$logger->error("Could not update pidfile “$pidfile” with BG process: $!\n");
return 1;
}
# Assuming we didn't get re-executed from a upcp change after updatenow (!$fromself).
# If the file is still there from a failed run, remove it.
unlink($upcp_disallowed_path) if !$fromself && -f $upcp_disallowed_path;
# make sure that the pid file is going to be removed when killed by a signal
$SIG{INT} = $SIG{HUP} = $SIG{TERM} = sub { ## no critic qw(Variables::RequireLocalizedPunctuationVars)
unlink $pidfile;
if ($logger) {
$logger->close_log;
$logger->open_log;
$logger->error("User hit ^C or killed the process ( pid file '$pidfile' removed ).");
$logger->close_log;
}
return;
};
#############################################################################
# Get variables needed for update
my $gotSigALRM = 0;
my $connecttimeout = 30;
my $liveconnect = 0;
my $connectedhost = q{};
my @HOST_IPs = ();
## Case 46528: license checks moved to updatenow and Cpanel::Update::Blocker
$logger->debug("Done getting update config variables..");
$logger->increment_pbar;
#############################################################################
# Run the preupcp hook
if ( -x '/usr/local/cpanel/scripts/preupcp' ) {
$logger->info("Running /usr/local/cpanel/scripts/preupcp");
system '/usr/local/cpanel/scripts/preupcp';
}
if ( -x '/usr/local/cpanel/scripts/hook' ) {
$logger->info("Running Standardized hooks");
system '/usr/local/cpanel/scripts/hook', '--category=System', '--event=upcp', '--stage=pre';
}
$logger->increment_pbar();
#############################################################################
# Check mtime on ourselves before sync
# This is the target for a goto in the case that the remote TIERS file is
# changed sometime during the execution of this upcp run. It prevents the
# need for a new script argument and re-exec.
STARTOVER:
my $mtime = ( stat('/usr/local/cpanel/scripts/upcp') )[9];
$logger->info( "mtime on upcp is $mtime (" . scalar( localtime($mtime) ) . ")" );
# * If no fromself arg is passed, it's either the first run from crontab or called manually.
# * --force is passed to updatenow, has no bearing on upcp itself.
# * Even if upcp is changed 3 times in a row during an update (fastest builds ever?), we
# would never actually update more than once unless the new upcp script changed the logic below
if ( !$fromself ) {
# run updatenow to sync everything
# updatenow expects --upcp to be passed or will error out
my @updatenow_args = ( '/usr/local/cpanel/scripts/updatenow', '--upcp', "--log=$logfile_path" );
# if --forced was received, pass it on to updatenow
if ($forced) { push( @updatenow_args, '--force' ); }
# if --sync was received, pass it on to updatenow. --force makes --sync meaningless.
if ( !$forced && $sync_requested ) { push( @updatenow_args, '--sync' ); }
# This is the point of no return, we are upgrading
# and its no longer abortable.
# set flag to disallow ^C during updatenow
Cpanel::FileUtils::TouchFile::touchfile($upcp_disallowed_path) or $logger->warn("Failed to create: $upcp_disallowed_path: $!");
# call updatenow, if we get a non-zero status, die.
my $exit_code = system(@updatenow_args);
$logger->increment_pbar(15);
if ( $exit_code != 0 ) {
my $signal = $exit_code % 256;
$exit_code = $exit_code >> 8;
analyze_and_report_error(
#success_msg => undef,
error_msg => "Running `@updatenow_args` failed, exited with code $exit_code (signal = $signal)",
type => 'upcp::UpdateNowFailed',
exit_status => $exit_code,
extra => [
'signal' => $signal,
'updatenow_args' => \@updatenow_args,
],
);
return ($exit_code);
}
# get the new mtime and compare it, if upcp changed, let's run ourselves again.
# this should be a fairly rare occasion.
my $newmtime = ( stat('/usr/local/cpanel/scripts/upcp') )[9];
if ( $newmtime ne $mtime ) {
#----> Run our new self (and never come back).
$logger->info("New upcp detected, restarting ourself");
$logger->close_log();
exec '/usr/local/cpanel/scripts/upcp', @retain_argv, '--fromself', "--log=$logfile_path";
}
}
#############################################################################
# Run the maintenance script
my $last_logfile_position;
my $save_last_logfile_position = sub {
$last_logfile_position = int( qx{wc -l $logfile_path 2>/dev/null} || 0 );
};
$logger->close_log(); # Allow maintenance to write to the log
$save_last_logfile_position->(); # remember how many lines has the logfile before starting the maintenance script
my $exit_status;
my $version_change_happened = -e $version_upgrade_file;
if ($version_change_happened) {
$exit_status = system( '/usr/local/cpanel/scripts/maintenance', '--pre', '--log=' . $logfile_path, '--pbar-start=20', '--pbar-stop=30' );
}
else {
$exit_status = system( '/usr/local/cpanel/scripts/maintenance', '--log=' . $logfile_path, '--pbar-start=20', '--pbar-stop=95' );
}
$logger->open_log(); # Re-open the log now maintenance is done.
analyze_and_report_error(
success_msg => "Pre Maintenance completed successfully",
error_msg => "Pre Maintenance ended, however it did not exit cleanly ($exit_status). Please check the logs for an indication of what happened",
type => 'upcp::MaintenanceFailed',
exit_status => $exit_status,
logfile => $logfile_path,
last_logfile_position => $last_logfile_position,
);
# Run post-sync cleanup only if updatenow did a sync
# Formerly run after layer2 did a sync.
if ($version_change_happened) {
# post_sync pbar range: 30%-55%
$logger->close_log(); # Yield the log to post_sync_cleanup
$save_last_logfile_position->(); # remember how many lines has the logfile before starting the post_sync_cleanup script
my $post_exit_status = system( '/usr/local/cpanel/scripts/post_sync_cleanup', '--log=' . $logfile_path, '--pbar-start=30', '--pbar-stop=55' );
$logger->open_log; # reopen the log to continue writing messages
analyze_and_report_error(
success_msg => "Post-sync cleanup completed successfully",
error_msg => "Post-sync cleanup has ended, however it did not exit cleanly. Please check the logs for an indication of what happened",
type => 'upcp::PostSyncCleanupFailed',
exit_status => $post_exit_status,
logfile => $logfile_path,
last_logfile_position => $last_logfile_position,
);
unlink $version_upgrade_file;
unlink($upcp_disallowed_path) if -f ($upcp_disallowed_path);
# Maintenance pbar range: 55-95%
$logger->close_log(); # Allow maintenance to write to the log
$save_last_logfile_position->(); # remember how many lines has the logfile before starting the maintenance --post
$exit_status = system( '/usr/local/cpanel/scripts/maintenance', '--post', '--log=' . $logfile_path, '--pbar-start=55', '--pbar-stop=95' );
$logger->open_log(); # Re-open the log now maintenance is done.
analyze_and_report_error(
success_msg => "Post Maintenance completed successfully",
error_msg => "Post Maintenance ended, however it did not exit cleanly ($exit_status). Please check the logs for an indication of what happened",
type => 'upcp::MaintenanceFailed',
exit_status => $exit_status,
logfile => $logfile_path,
last_logfile_position => $last_logfile_position,
);
# Check for new version... used when updating to next LTS version
$logger->info("Polling updatenow to see if a newer version is available for upgrade");
$logger->close_log(); # Yield the log to updatenow
my $update_available = system( '/usr/local/cpanel/scripts/updatenow', "--log=$logfile_path", '--checkremoteversion' );
$logger->open_log; # reopen the log to continue writing messages
if ( !$sync_requested && $update_available && ( $update_available >> 8 ) == $update_is_available_exit_code ) {
$logger->info("\n\n/!\\ - Next LTS version available, restarting upcp and updating system. /!\\\n\n");
$fromself = 0;
goto STARTOVER;
}
}
else {
unlink($upcp_disallowed_path) if -f ($upcp_disallowed_path);
}
#############################################################################
# Run the post upcp hook
$logger->update_pbar(95);
if ( -x '/usr/local/cpanel/scripts/postupcp' ) {
$logger->info("Running /usr/local/cpanel/scripts/postupcp");
system '/usr/local/cpanel/scripts/postupcp';
}
if ( -x '/usr/local/cpanel/scripts/hook' ) {
$logger->info("Running Standardized hooks");
system '/usr/local/cpanel/scripts/hook', '--category=System', '--event=upcp', '--stage=post';
}
close($RNULL);
#############################################################################
# All done.
#############################################################################
$logger->update_pbar(100);
$logger->info( "\n\n\tcPanel update completed\n\n", 1 );
$logger->info("A log of this update is available at $logfile_path\n\n");
# this happens on exit so it shouldn't be necessary
$logger->info("Removing upcp pidfile");
unlink $pidfile if -f $pidfile || $logger->warn("Could not delete pidfile $pidfile : $!");
my $update_blocks_fname = '/var/cpanel/update_blocks.config';
if ( -s $update_blocks_fname ) {
$logger->warning("NOTE: A system upgrade was not possible due to the following blockers:\n");
if ( open( my $blocks_fh, '<', $update_blocks_fname ) ) {
while ( my $line = readline $blocks_fh ) {
my ( $level, $message ) = split /,/, $line, 2;
# Not using the level in the log, cause the logger can emit additional messages
# on some of the levels used (fatal emits an 'email message', etc)
# Remove URL from log output. Make sure message is defined.
if ($message) {
$message =~ s///ig;
$message =~ s{}{}ig;
}
$logger->warning( uc("[$level]") . " - $message" );
}
}
else {
$logger->warning("Unable to open blocks file! Please review '/var/cpanel/update_blocks.config' manually.");
}
}
else {
$logger->info("\n\nCompleted all updates\n\n");
}
$logger->close_log();
return 0;
}
#############################################################################
######[ Subroutines ]########################################################
#############################################################################
sub analyze_and_report_error {
my %info = @_;
my $type = $info{type} or die;
my $exit_status = $info{exit_status};
if ( $exit_status == 0 ) {
if ( defined $info{success_msg} ) {
$logger->info( $info{success_msg} );
}
return;
}
my $msg = $info{error_msg} or die;
my @extra;
if ( ref $info{extra} ) {
@extra = @{ $info{extra} };
}
my $logfile_content = Cpanel::LoadFile::loadfile_r($logfile_path);
# add events to the end of the error log
if ( try( sub { Cpanel::LoadModule::load_perl_module("Cpanel::Logs::ErrorEvents") } ) ) {
my ($events) = Cpanel::Logs::ErrorEvents::extract_events_from_log( log => $logfile_content, after_line => $info{last_logfile_position} );
if ( $events && ref $events && scalar @$events ) {
my $events_str = join ', ', map { qq["$_"] } @$events;
$events_str = qq[The following events were logged: ${events_str}.];
$msg =~ s{(Please check)}{${events_str} $1} or $msg .= ' ' . $events_str;
}
}
$logger->error( $msg, 1 );
if ( try( sub { Cpanel::LoadModule::load_perl_module("Cpanel::iContact::Class::$type") } ) ) {
require Cpanel::Notify;
Cpanel::Notify::notification_class(
'class' => $type,
'application' => $type,
'constructor_args' => [
'exit_code' => $exit_status,
'events_after_line' => $info{last_logfile_position},
@extra,
'attach_files' => [ { 'name' => 'update_log.txt', 'content' => $logfile_content, 'number_of_preview_lines' => 25 } ]
]
);
}
elsif (
!try(
sub {
Cpanel::LoadModule::load_perl_module("Cpanel::iContact");
Cpanel::iContact::icontact(
'application' => 'upcp',
'subject' => 'cPanel & WHM update failure (upcp)',
'message' => $msg,
);
}
)
) {
$logger->error('Failed to send contact message');
}
return 1;
}
#############################################################################
sub kill_upcp {
my $pid = shift or die;
my $status = shift || 'hanging';
my $msg = shift || "/usr/local/cpanel/scripts/upcp was running as pid '$pid' for longer than 6 hours. cPanel will kill this process and run a new upcp in its place.";
# Attempt to notify admin of the kill.
if ( try( sub { Cpanel::LoadModule::load_perl_module("Cpanel::iContact::Class::upcp::Killed") } ) ) {
require Cpanel::Notify;
Cpanel::Notify::notification_class(
'class' => 'upcp::Killed',
'application' => 'upcp::Killed',
'constructor_args' => [
'upcp_path' => '/usr/local/cpanel/scripts/upcp',
'pid' => $pid,
'status' => $status,
'attach_files' => [ { 'name' => 'update_log.txt', 'content' => Cpanel::LoadFile::loadfile_r($logfile_path), 'number_of_preview_lines' => 25 } ]
]
);
}
else {
try(
sub {
Cpanel::LoadModule::load_perl_module("Cpanel::iContact");
Cpanel::iContact::icontact(
'application' => 'upcp',
'subject' => "cPanel update $status",
'message' => $msg,
);
}
);
}
print "Sending kill signal to process group for $pid\n";
kill -1, $pid; # Kill the process group
for ( 1 .. 60 ) {
print "Waiting for processes to die\n";
waitpid( $pid, POSIX::WNOHANG() );
last if ( !kill( 0, $pid ) );
sleep 1;
}
if ( kill( 0, $pid ) ) {
print "Could not kill upcp nicely. Doing kill -9 $pid\n";
kill 9, $pid;
}
else {
print "Done!\n";
}
return;
}
#############################################################################
sub setupenv {
Cpanel::Env::clean_env();
delete $ENV{'DOCUMENT_ROOT'};
delete $ENV{'SERVER_SOFTWARE'};
if ( $ENV{'WHM50'} ) {
$ENV{'GATEWAY_INTERFACE'} = 'CGI/1.1';
}
( $ENV{'USER'}, $ENV{'HOME'} ) = ( getpwuid($>) )[ 0, 7 ];
$ENV{'PATH'} .= ':/sbin:/usr/sbin:/usr/bin:/bin:/usr/local/bin';
$ENV{'LANG'} = 'C';
$ENV{'LC_ALL'} = 'C';
}
sub unset_rlimits {
# This is required if upcp started running from a pre-1132
eval {
local $SIG{__DIE__};
require Cpanel::Rlimit;
Cpanel::Rlimit::set_rlimit_to_infinity();
};
}
#############################################################################
sub setup_updatelogs {
return if ( -d '/var/cpanel/updatelogs' );
unlink('/var/cpanel/updatelogs');
mkdir( '/var/cpanel/updatelogs', 0700 );
}
sub set_cron_env {
# Do not override the env var if set.
return 'env var CPANEL_IS_CRON was present before this process started.' if ( defined $ENV{'CPANEL_IS_CRON'} );
if ( grep { $_ eq '--cron' } @ARGV ) {
$ENV{'CPANEL_IS_CRON'} = 1;
return 'cron mode set from command line';
}
if ( $ARGV[0] eq 'manual' ) {
$ENV{'CPANEL_IS_CRON'} = 0;
return 'manual flag passed on command line';
}
if ($forced) {
$ENV{'CPANEL_IS_CRON'} = 0;
return '--force passed on command line';
}
if ( -t STDOUT ) {
$ENV{'CPANEL_IS_CRON'} = 0;
return 'Terminal detected';
}
if ( $ENV{'SSH_CLIENT'} ) {
$ENV{'CPANEL_IS_CRON'} = 0;
return 'SSH connection detected';
}
# cron sets TERM=dumb
if ( $ENV{'TERM'} eq 'dumb' ) {
$ENV{'CPANEL_IS_CRON'} = 1;
return 'TERM detected as set to dumb';
}
# Check if parent is whostmgr
if ( readlink( '/proc/' . getppid() . '/exe' ) =~ m/whostmgrd/ ) {
$ENV{'CPANEL_IS_CRON'} = 0;
return 'parent process is whostmgrd';
}
# Default to cron enabled.
$ENV{'CPANEL_IS_CRON'} = 1;
return 'default';
}
#############################################################################
sub fetch_cpanel_version {
my $version;
my $version_file = '/usr/local/cpanel/version';
return if !-f $version_file;
my $fh;
local $/ = undef;
return if !open $fh, '<', $version_file;
$version = <$fh>;
close $fh;
$version =~ s/^\s+|\s+$//gs;
return $version;
}
#############################################################################
sub monitor_upcp {
my $updatepid = shift or die;
$0 = 'cPanel Update (upcp) - Master';
$SIG{INT} = $SIG{TERM} = sub {
print "User hit ^C\n";
if ( -f $upcp_disallowed_path ) {
print "Not allowing upcp slave to be killed during updatenow, just killing monitoring process.\n";
exit;
}
print "killing upcp\n";
kill_upcp( $updatepid, "aborted", "/usr/local/cpanel/scripts/upcp was aborted by the user hitting Ctrl-C." );
exit;
};
$SIG{HUP} = sub {
print "SIGHUP detected; closing monitoring process.\n";
print "The upcp slave has not been affected\n";
exit;
};
# Wait till the file shows up.
until ( -e $logfile_path ) {
select undef, undef, undef, .25; # sleep just a bit
}
# Wait till we're allowed to open it.
my $fh;
until ( defined $fh && fileno $fh ) {
$fh = IO::Handle->new();
if ( !open $fh, '<', $logfile_path ) {
undef $fh;
select undef, undef, undef, .25; # sleep just a bit
next;
}
}
# Read the file until the pid dies.
my $child_done = 0;
while (1) {
# Read all the available lines.
while (1) {
my $line = <$fh>;
last if ( !defined $line || $line eq '' );
print $line;
}
# Once the child is history, we need to do yet one more final read,
# on the off chance (however remote) that she has written one last
# hurrah after we last checked. Hence the following.
last if $child_done; # from prev. pass
$child_done = 1 if -1 == waitpid( $updatepid, 1 ); # and loop back for one more read
select undef, undef, undef, .25; # Yield idle time to the cpu
}
close $fh if $fh;
exit;
}
sub _logger {
return $logger if $logger;
$logger = Cpanel::Update::Logger->new( { 'logfile' => $logfile_path, 'stdout' => 1, 'log_level' => 'info' } );
# do not set the pbar in the constructor to do not display the 0 % in bg mode
$logger->{pbar} = $pbar_starting_point;
return $logger;
}
sub _determine_logfile_path_if_running ($pid) {
my $upid = Cpanel::UPID::get($pid);
return $upid ? "/var/cpanel/updatelogs/update.$upid.log" : undef;
}
#----------------------------------------------------------------------
# HANDLE WITH CARE!! This string is parsed
# in at least one place. (cf. Cpanel::Update::Start)
sub _message_about_newly_started ( $updatepid, $logfile_path ) {
return "upcp is going into background mode (PID $updatepid). You can follow “$logfile_path” to watch its progress.";
}
#----------------------------------------------------------------------
# HANDLE WITH CARE!! This string is parsed
# in at least one place. (cf. Cpanel::Update::Start)
sub _message_about_already_running ( $curpid, $logpath ) {
return "cPanel Update (upcp) is already running. Please wait for the previous upcp (PID $curpid, log file “$logpath”) to complete, then try again. You can use the command 'ps --pid $curpid' to check if the process is running. You may wish to use '--force'.";
}
1;