KGRKJGETMRETU895U-589TY5MIGM5JGB5SDFESFREWTGR54TY
Server : Apache
System : Linux cs317.bluehost.com 4.19.286-203.ELK.el7.x86_64 #1 SMP Wed Jun 14 04:33:55 CDT 2023 x86_64
User : andertr9 ( 1047)
PHP Version : 8.2.18
Disable Function : NONE
Directory :  /cpanel_installer/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Current File : //cpanel_installer/Installer.pm
package Installer;

# cpanel - installd/Installer.pm                   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

use strict;
use warnings;

use Getopt::Long ();
use POSIX        ();
use Errno qw(EAGAIN);

use CpanelLogger;

use Common       ();
use CpanelGPG    ();
use CpanelConfig ();
use OSDetect     ();

use InstallerRhel   ();
use InstallerUbuntu ();

use constant LOCK_FILE                        => '/root/installer.lock';
use constant DEFAULT_MYIP_URL                 => q[https://myip.cpanel.net/v1.0/];
use constant PRODUCT_DNSONLY                  => 64;
use constant MINIMUM_CPANEL_VERSION_SUPPORTED => 93;                                 # TODO Kill UBUNTU_MINIMUM_VERSION and MINIMUM_CUSTOM_OS_VERSION once this LTS bumps.
use constant MINIMUM_CUSTOM_OS_VERSION        => 101;

# TODO: Special ubuntu exception while we roll it out. Can go away once the minimum exceeds UBUNTU_MINIMUM_VERSION
use constant UBUNTU_MINIMUM_VERSION => 101;
use constant ROCKY_MINIMUM_VERSION  => 105; # remove once MINIMUM_CPANEL_VERSION_SUPPORTED >= ROCKY_MINIMUM_VERSION

sub new {
    my $common_class = -e '/etc/debian_version' ? 'InstallerUbuntu' :                #
      -e '/etc/redhat-release' ? 'InstallerRhel' :                                   #
      die("Unknown distro");

    my $self = bless {}, $common_class;

    return $self;
}

sub setup {
    my ( $self, @args ) = @_;

    if ( $] < 5.010 ) {
        print "This installer requires Perl 5.10.0 or better.\n";
        die "Cannot continue.\n";
    }

    $self->{'parent_proc'} = $$;

    $self->set_globals;

    $self->{'install_start'} = CpanelLogger::open_logs();

    $self->parse_argv(@args);

    $self->get_script_lock;
    $self->detect_distro;

    return;
}

sub set_globals {
    $ENV{'CPANEL_BASE_INSTALL'} = 1;
    $ENV{'LANG'}                = 'C';
    $ENV{'LC_ALL'}              = 'C';
    $ENV{'DEBIAN_FRONTEND'}     = 'noninteractive';

    delete $ENV{'LANGUAGE'};

    $| = 1;    ## no critic qw(RequireLocalizedPunctuationVars)
    umask 022;

    return;
}

sub force              { return shift->{'options'}->{'force'} }
sub skip_apache        { return shift->{'options'}->{'skip_apache'} }
sub skip_repo_setup    { return shift->{'options'}->{'skip_repo_setup'} }
sub skip_license_check { return shift->{'options'}->{'skip_license_check'} }
sub skip_cloudlinux    { return shift->{'options'}->{'skip-cloudlinux'} }
sub skip_imunifyav     { return shift->{'options'}->{'skip-imunifyav'} }
sub skip_wptoolkit     { return shift->{'options'}->{'skip-wptoolkit'} }
sub stop_at_update_now { return shift->{'options'}->{'stop_at_update_now'} }
sub install_start      { return shift->{'install_start'} }

sub parse_argv {
    my ( $self, @args ) = @_;

    DEBUG("Parsing command line arguments.");

    # Defaults. Look for touch files.
    $self->{options} = {
        'force'              => 0,
        'skip-cloudlinux'    => 0,
        'skip-imunifyav'     => 0,
        'skip-wptoolkit'     => 0,
        'skip_apache'        => -e '/root/skipapache' ? 1 : 0,
        'skip_repo_setup'    => 0,
        'skip_license_check' => 0,
        'stop_at_update_now' => 0,
        'experimental-os'    => undef,
    };

    # Parse args.
    Getopt::Long::GetOptionsFromArray(
        \@args,
        'force'              => \$self->{options}->{'force'},
        'skip-cloudlinux'    => \$self->{options}->{'skip-cloudlinux'},
        'skip-imunifyav'     => \$self->{options}->{'skip-imunifyav'},
        'skip-wptoolkit'     => \$self->{options}->{'skip-wptoolkit'},
        'skipapache'         => \$self->{options}->{'skip_apache'},
        'skipreposetup'      => \$self->{options}->{'skip_repo_setup'},
        'skiplicensecheck'   => \$self->{options}->{'skip_license_check'},
        'dnsonly'            => \$self->{'dnsonly'},
        'stop_at_update_now' => \$self->{options}->{'stop_at_update_now'},
        'experimental-os=s'  => \$self->{options}->{'experimental-os'},
    );

    # If it's not set to dnsonly, look in the remaining args for a dnsonly string.
    $self->{'dnsonly'} ||= ( grep { $_ eq 'dnsonly' } @args ) ? 1 : 0;

    my $type = $args[0] || 'standard';
    INFO("Install type: $type\n");

    $self->create_touch_files;

    return;
}

sub is_dnsonly   { return shift->{'dnsonly'} }
sub distro_type  { die 'unimplemented' }
sub distro_name  { return shift->{'os'}->{'distro'} || die }    # Should always be populated on call.
sub distro_major { return shift->{'os'}->{'major'}  || die }    # Should always be populated on call.
sub distro_minor { return shift->{'os'}->{'minor'}  || die }    # Should always be populated on call.

sub detect_distro {
    my ($self) = @_;
    my @os_info = OSDetect::get_os_info();

    $self->{'os'}->{'kernel'} = shift @os_info;
    $self->{'os'}->{'distro'} = shift @os_info;
    $self->{'os'}->{'major'}  = shift @os_info;
    $self->{'os'}->{'minor'}  = shift @os_info;

    #$self->{'os'}->{'build'} = shift @os_info;

    return;
}

sub cpanel_version {
    my ($self) = @_;

    return $self->{'cpanel_version'} if $self->{'cpanel_version'};

    return $self->{'cpanel_version'} = CpanelConfig::get_cpanel_version();
}

sub lts_version {
    my ($self) = @_;

    return $self->{'lts_version'} if $self->{'lts_version'};

    my $v = $self->cpanel_version;
    return $self->{'lts_version'} = CpanelConfig::get_lts_version($v);
}

sub cpanel_tier {
    my ($self) = @_;
    return $self->{'cpanel_tier'} //= CpanelConfig::get_cpanel_tier();
}

sub create_touch_files {
    my ($self) = @_;

    ensure_var_cpanel();

    if ( $self->is_dnsonly ) {
        INFO("cPanel DNSONLY installation requested.");
        $self->touch('/var/cpanel/dnsonly');
        $self->touch('/var/cpanel/noimunifyav');
        $self->touch('/var/cpanel/nowptoolkit');
    }
    else {
        unlink '/var/cpanel/dnsonly';    # Just in case the customer ran with the wrong args previously.
    }

    if ( $self->skip_cloudlinux ) {
        INFO("Skip cloudlinux installation requested.");
        $self->touch('/var/cpanel/nocloudlinux');
    }

    if ( $self->skip_imunifyav ) {
        INFO("Skip imunifyav installation requested.");
        $self->touch('/var/cpanel/noimunifyav');
    }

    if ( $self->skip_wptoolkit ) {
        INFO("Skip WordPress Toolkit installation requested.");
        $self->touch('/var/cpanel/nowptoolkit');
    }

    # --experimental-os=almalinux-8.4
    $self->setup_experimental_os( $self->{options}->{'experimental-os'} );

    return;
}

# The customer ran latest --experimental-os=centos-7.2
sub setup_experimental_os {
    my ( $self, $settings ) = @_;

    defined $settings or return;    # Didn't pass this option on command line.

    my @os_info = $settings =~ m{^([^-]+)-([0-9]+)\.([0-9]+)}    #
      or die("Unrecognized --experimental-os option. Try: --experimental-os=centos-7.9");
    unshift @os_info, $^O;
    push @os_info, '2020';                                       # An arbitrary build ID we're going to push on the end for consistency.

    my ( $os, $distro, $major, $minor, $build ) = @os_info;

    if ( $distro && $distro eq 'cloudlinux' ) {
        FATAL("Using --experimental-os is not permitted for CloudLinux.");
    }

    mkdir '/var/cpanel/caches', 0711;

    if ( $self->lts_version() < MINIMUM_CUSTOM_OS_VERSION ) {
        my $bad_version = MINIMUM_CUSTOM_OS_VERSION - 1;
        FATAL( 'You cannot use "--experimental-os" argument with versions of cPanel & WHM on or prior to cPanel & WHM version ' . $bad_version . '.' );
    }

    my $cpanel_os_cache_file = "/var/cpanel/caches/Cpanel-OS";
    unlink $cpanel_os_cache_file, "$cpanel_os_cache_file.custom";

    WARN( <<"EOS" );
--experimental-os=$settings was successful.
You are currently installing cPanel & WHM on an unsupported distribution.
We discourage you from using this server for production purposes.
EOS

    # Write out the cache file.
    local $!;
    unlink $cpanel_os_cache_file;
    symlink "$os|$distro|$major|$minor|$build", $cpanel_os_cache_file;

    # Write out the lock file to prevent the cache from being updated going forward.
    unlink "$cpanel_os_cache_file.custom";
    symlink "1", "$cpanel_os_cache_file.custom";

    return;
}

sub get_script_lock {
    my ($self) = @_;

    if ( open my $fh, '<', LOCK_FILE ) {
        print "The system detected an installer lock file: (" . LOCK_FILE . ")\n";
        print "Make certain that an installer is not already running.\n\n";
        print "You can remove this file and re-run the cPanel installation process after you are certain that another installation is not already in progress.\n\n";
        my $pid = <$fh>;
        if ($pid) {
            chomp $pid;
            print `ps auxwww |grep $pid`;    ## no critic(ProhibitQxAndBackticks)
        }
        else {
            print "Warning: The system could not find pid information in the " . LOCK_FILE . " file.\n";
        }
        return 1;
    }

    # Create the lock file.
    if ( open my $fh, '>', LOCK_FILE ) {
        print {$fh} "$$\n";

        close $fh;
    }
    else {
        FATAL( "Unable to write lock file " . LOCK_FILE );

        return 1;
    }

    $self->{'original_pid'} = $$;

    return;
}

sub check_system_support {
    my ($self) = @_;

    $self->invalid_system( "Unsupported kernel (" . $self->{'os'}->{'kernel'} . ") for operating system" ) if $self->{'os'}->{'kernel'} ne 'linux';

    my @uname = POSIX::uname();
    if ( $uname[4] ne 'x86_64' ) {
        $self->invalid_system("cPanel & WHM supports 64-bit versions (not $uname[4]) only.");
    }

    INFO("Checking RAM now...");
    my $total_memory = $self->get_total_memory();
    my $minmemory    = $self->distro_major == 6 ? 768 : 1_024;

    if ( $total_memory < $minmemory ) {
        ERROR("cPanel, L.L.C. requires a minimum of $minmemory MB of RAM for your operating system.");
        FATAL("Increase the server's total amount of RAM, and then reinstall cPanel & WHM.");
    }

    return;
}

sub get_total_memory {

    # tests on different architectures show that 15 % is safe
    my $tolerance_factor = 1.15;

    # MemTotal: Total usable ram (i.e. physical ram minus a few reserved
    #          bits and the kernel binary code)
    # note, another option would be to use "dmidecode --type 17", or dmesg
    #   but this will require an additional RPM
    #   we just want to be sure that a customer does not install
    #   with 512 when 700 or more is required
    my $meminfo = q{/proc/meminfo};
    if ( open( my $fh, "<", $meminfo ) ) {
        while ( my $line = readline $fh ) {
            if ( $line =~ m{^MemTotal:\s+([0-9]+)\s*kB}i ) {
                return int( int( $1 / 1_024 ) * $tolerance_factor );
            }
        }
    }

    return 0;    # something is wrong
}

sub invalid_system {
    my ( $self, $message ) = @_;
    $message ||= '';
    chomp $message;

    ERROR($message);
    ERROR('cPanel & WHM does not support the version of the Linux distribution you are running. You will need to install on one of the supported versions of a Linux distribution listed at https://go.cpanel.net/supported-os');
    FATAL('Please reinstall cPanel & WHM from a valid distribution.');
    return;
}

sub clean_install_check {

    INFO('Checking for any control panels...');
    my @server_detected;
    push @server_detected, 'DirectAdmin' if ( -e '/usr/local/directadmin' );
    push @server_detected, 'Plesk'       if ( -e '/etc/psa' );
    push @server_detected, 'Ensim'       if ( -e '/etc/appliance' || -d '/etc/virtualhosting' );

    #push @server_detected, 'Alabanza'    if ( -e '/etc/mail/mailertable' );
    push @server_detected, 'Zervex'              if ( -e '/var/db/dsm' );
    push @server_detected, 'Web Server Director' if ( -e '/bin/rpm' && `/bin/rpm -q ServerDirector` =~ /^ServerDirector/ms );    ## no critic(ProhibitQxAndBackticks)

    # Don't just check for /usr/local/cpanel, as some people will have created
    # that directory as a mount point for the install.
    push @server_detected, 'cPanel & WHM' if -e '/usr/local/cpanel/cpkeyclt';

    return if ( !@server_detected );

    ERROR("The installation process found evidence that the following control panels were installed on this server:");
    ERROR($_) foreach (@server_detected);
    FATAL('You must install cPanel & WHM on a clean server.');
    return;
}

sub check_system_files {
    INFO("Checking for essential system files...");

    unless ( -f '/etc/fstab' ) {
        ERROR("Your system is missing the file /etc/fstab.  This is an");
        ERROR("essential system file that is part of the base system.");
        FATAL("Please ensure the system has been properly installed.");
    }

    setup_empty_directories();

    setup_custom_cpanel_config();

    assure_nobody();

    return;
}

# Place customer provided cpanel.config in place early in case we need to block on any of the settings.
sub setup_custom_cpanel_config {
    my $custom_cpanel_config_file = '/root/cpanel_profile/cpanel.config';
    if ( -e $custom_cpanel_config_file ) {
        INFO("The system is placing the custom cpanel.config file from $custom_cpanel_config_file.");
        unlink '/var/cpanel/cpanel.config';
        system( '/bin/cp', $custom_cpanel_config_file, '/var/cpanel/cpanel.config' );
    }
    return;
}

# mkdir some directories.
sub setup_empty_directories {
    INFO('The installation process will now set up the necessary empty cpanel directories.');

    ensure_var_cpanel();

    foreach my $dir (qw{/usr/local/cpanel /usr/local/cpanel/base /usr/local/cpanel/base/frontend /usr/local/cpanel/logs /var/cpanel/tmp /var/cpanel/version /var/cpanel/perl /var/named}) {
        unlink $dir if ( -f $dir || -l $dir );

        if ( !-d $dir ) {
            DEBUG("mkdir $dir");
            mkdir( $dir, 0755 );
        }
    }

    foreach my $dir (qw{/var/cpanel/logs}) {
        unlink $dir if ( -f $dir || -l $dir );

        if ( !-d $dir ) {
            DEBUG("mkdir $dir");
            mkdir( $dir, 0700 );
        }
    }

    ensure_feature_showcase_dir();

    return;
}

sub ensure_var_cpanel {

    my $dir   = '/var/cpanel';
    my $perms = 0711;

    if ( -f $dir || -l $dir ) {
        unlink $dir or FATAL("Fail to remove existing file $dir (should be a directory) $!");
    }

    if ( !-d $dir ) {
        mkdir $dir, $perms;
    }
    else {
        chown 0, 0, $dir;
        chmod $perms, $dir;
    }

    return;
}

sub ensure_feature_showcase_dir {

    foreach my $dir (qw{ /var/cpanel/activate /var/cpanel/activate/features }) {
        my $perms = 0700;

        if ( -f $dir || -l $dir ) {
            unlink $dir or FATAL("Fail to remove existing file $dir (should be a directory) $!");
        }

        if ( !-d $dir ) {
            mkdir $dir, $perms;
        }

        Common::ssystem( 'chown', '-R', 'root:root', $dir );
        Common::ssystem( 'chmod', '-R', '0700',      $dir );
    }

    return;
}

sub disable_systemd_resolved_if_enabled { return }    # Only run on ubuntu systems.

sub setup_and_check_resolv_conf {

    # Remote resolvers are required, since we remove local BIND during installation.
    open my $resolv_conf_fh, '<', '/etc/resolv.conf' or FATAL("Could not open /etc/resolv.conf: $!");

    if ( !grep { m/^\s*nameserver\s+/ && !m/\s+127.0.0.1$/ } <$resolv_conf_fh> ) {
        FATAL("/etc/resolv.conf must be configured with non-local resolvers for installations to complete.");
    }

    INFO("Validating whether the system can look up domains...");
    my @domains = qw(
      httpupdate.cpanel.net
      securedownloads.cpanel.net
    );

    foreach my $domain (@domains) {
        DEBUG("Testing $domain...");
        next if ( gethostbyname($domain) );
        ERROR( '!' x 105 . "\n" );
        ERROR("The system cannot resolve the $domain domain. Check the /etc/resolv.conf file. The system has terminated the installation process.\n");
        FATAL( '!' x 105 . "\n" );
    }
    return;
}

sub ensure_pkgs_installed {
    my ($self) = @_;

    my $pid = $self->run_in_background( sub { $self->install_basic_precursor_packages } );

    # While the ensure is running in the background
    # we show the message warning that they need a clean
    # server
    local $SIG{'INT'} = sub {
        kill( 'TERM', $pid );
        WARN("Install terminated by user input");
        exit(0);    ## no critic qw(NoExitsFromSubroutines)
    };

    $self->warn_clean_server_needed;
    local $?;
    waitpid( $pid, 0 );
    if ( $? != 0 ) {
        FATAL("ensure_pkgs_install failed: $?");
    }

    return;
}

sub warn_clean_server_needed {
    my ($self) = @_;
    INFO("cPanel Layer 1 Installer Starting...");
    INFO("Warning !!! Warning !!! WARNING !!! Warning !!! Warning");
    INFO("-------------------------------------------------------");
    INFO("cPanel requires a fresh, clean server!");
    INFO("If you serve websites from this server, this installer");
    INFO("will overwrite all of your configuration files.");
    INFO("Hit Ctrl+C NOW!");
    INFO("If this is a new server, please ignore this message.");
    INFO("-------------------------------------------------------");
    INFO("Warning !!! Warning !!! WARNING !!! Warning !!! Warning");
    INFO("Waiting 5 seconds...");
    INFO("");
    INFO("");
    $self->five_second_pause();
    return;
}

sub five_second_pause {
    for ( 1 .. 5 ) { print '.'; sleep(1); }
    print "\n";
    return;
}

sub run_in_background {
    my ( $self, $sub ) = @_;

  FORK: {
        my $pid = fork;
        return $pid if $pid;    # Parent.

        if ( !defined $pid ) {  # Still the parent but fork didn't work!
            if ( $! == EAGAIN ) {

                # EAGAIN is the supposedly recoverable fork error
                WARN("Fork failed! Trying again.");
                sleep 5;
                redo FORK;
            }
            else {
                # weird fork error
                FATAL("Can't fork: $!\n");
            }
        }
    }

    begin_collect_output();

    local $@;
    eval { $sub->(); };

    emit_collected_output();
    die if $@;

    exit(0);
}

sub update_system_clock {
    my ($self) = @_;

    my @date_cmd;
    my $binary_name;
    if ( $self->distro_type eq 'rhel' && $self->distro_major >= 8 ) {    # only rhel 8 doesn't provide rdate!
        $binary_name = 'chronyc';
        @date_cmd    = -x '/bin/chronyc' ? ( '/bin/chronyc', 'makestep' ) : ();
    }
    else {
        $binary_name = 'rdate';
        foreach my $bin (qw { /usr/sbin/rdate /usr/bin/rdate /bin/rdate }) {
            next unless -x $bin;
            @date_cmd = ( $bin, '-s', 'rdate.cpanel.net' );
            last;
        }
    }

    # Complain if we don't have an rdate binary.
    if ( !@date_cmd ) {
        ERROR("The system could not set the system clock because the $binary_name binary is missing.");
        return;
    }

    # Set the clock
    my $was = time();
    Common::ssystem(@date_cmd);
    my $now = time();
    INFO( "The system set the clock to: " . localtime($now) );
    my $change = $now - $was;

    # Adjust the start time if it shifted more than 10 seconds.
    if ( abs($change) > 10 ) {
        WARN("The system changed the clock by $change seconds.");
        $self->{'install_start'} = $self->install_start + $change;
        WARN( "The system adjusted the starting time to " . localtime( $self->install_start ) . "." );
    }
    else {
        INFO("The system changed the clock by $change seconds.");
    }

    return 1;
}

sub do_initial_clock_update {
    my ($self) = @_;

    # Sync the clock.
    if ( !$self->update_system_clock ) {
        WARN( "The current system time is set to: " . `date` );    ## no critic(ProhibitQxAndBackticks)
        WARN("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
        WARN("The installation process could not verify the system time. The utility to set time from a remote host, rdate or chrony, is not installed.");
        WARN("If your system time is incorrect by more than a few hours, source compilations will subtly fail.");
        WARN("This issue may result in an overall installation failure.");
        WARN("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
    }
    return;
}

# Start nscd if its not running since it will improve rpm install time
sub start_nscd {
    my ($self) = @_;

    return Common::ssystem("ps -U nscd -h 2>/dev/null || /sbin/service nscd start");
}

sub DESTROY {
    my ($self) = @_;

    return if $INC{'Test/More.pm'};             # Required for testing.
    return unless $self;
    return unless $self->{'original_pid'};
    $self->{'original_pid'} == $$ or return;    # this is a child process so no need to cleanup the lock.
    unlink LOCK_FILE;

    CpanelGPG::cleanup_gpg_homedir();
    return;
}

sub remove_distro_software { die };             # Needs to be implemented in child classes.

# This code is somewhat of a duplication of the code for updatenow that blocks updates based on configuration
# settings. It needs to be here also because of the bootstrap level nature for when this needs to run.
sub check_for_install_version_blockers {
    my ($self) = @_;

    my $lts_version = $self->lts_version();
    my $tier        = $self->cpanel_tier();

    if ( $lts_version < MINIMUM_CPANEL_VERSION_SUPPORTED ) {
        FATAL( 'You cannot install versions of cPanel & WHM prior to cPanel & WHM version ' . MINIMUM_CPANEL_VERSION_SUPPORTED . '.' );
    }

    if ( $self->distro_type eq 'debian' && $lts_version < UBUNTU_MINIMUM_VERSION ) {
        FATAL("cPanel & WHM only supports Ubuntu on version 102 or greater. Please refer to our additional installation instructions at https://go.cpanel.net/ubuntu-install-procedure");
    }

    if ( $self->distro_name eq 'rocky' && $lts_version < ROCKY_MINIMUM_VERSION ) {
        FATAL("cPanel & WHM only supports Rocky Linux on version 106 or greater. Please refer to our additional installation instructions at https://go.cpanel.net/ubuntu-install-procedure");
    }

    my $staging_dir = CpanelConfig::get_staging_dir();
    if ( $staging_dir ne '' && $staging_dir ne '/usr/local/cpanel' ) {
        FATAL("STAGING_DIR must be set to /usr/local/cpanel during installs.");
    }

    # pull in cpanel.config settings or return if the file's not there (defaults will assert)
    my $cpanel_config = CpanelConfig::read_config('/var/cpanel/cpanel.config');

    # This is distro specific.
    $self->verify_mysql_version($cpanel_config);

    if ( defined $cpanel_config->{'mailserver'} && $cpanel_config->{'mailserver'} !~ m/^(dovecot|disabled)$/i ) {
        FATAL("You must use 'dovecot' or 'disabled' for the mailserver in the /var/cpanel/cpanel.config file for cPanel & WHM version $lts_version.");
    }

    if ( defined $cpanel_config->{'local_nameserver_type'} && $cpanel_config->{'local_nameserver_type'} !~ m/^(powerdns|bind|disabled)$/i ) {
        FATAL("You must use 'powerdns', 'bind' or 'disabled' for the local_nameserver_type in the /var/cpanel/cpanel.config file. For more information, see: https://docs.cpanel.net/whm/service-configuration/nameserver-selection/");
    }

    return;

}

sub bootstrap_cpanel_perl {
    my ($self) = @_;

    # Make sure the cPanel key is in place.
    CpanelGPG::fetch_gpg_key_once();

    my $cpanel_version = $self->cpanel_version;

    # Install cPanel files.
    INFO("Installing bootstrap cPanel Perl");

    # Download the tar.gz files and extract them instead.
    my $script = 'fix-cpanel-perl';
    my $source = "/cpanelsync/$cpanel_version/cpanel/scripts/$script.xz";

    unlink $script;

    DEBUG("Retrieving the $script file from $source if available...");

    # download file in current directory (inside the self extracted tarball)
    Common::cpfetch($source);

    chmod 0700, $script;

    INFO("Running script $script to bootstrap cPanel Perl.");

    my $exit;

    # Retry a few times if one of the http request failed
    my $max = 3;
    foreach my $iter ( 1 .. $max ) {
        $exit = Common::ssystem("./$script");
        if ( $exit == 0 ) {
            INFO("Successfully installed cPanel Perl minimal version.");
            return;
        }

        WARN("Run #$iter/$max failed to run script $script");
        last if $iter == $max;
        sleep 5;
    }

    my $signal = $? % 256;

    # This isn't going to return actually. It's going to die.
    ERROR("Failed to run script $script to bootstrap cPanel Perl.");
    return FATAL("The script $script terminated with the following exit code: $exit ($signal); The cPanel & WHM installation process cannot proceed.");
}

sub pre_checks_while_waiting_for_fix_cpanel_perl {
    my ($self) = @_;

    # Make sure the OS is relatively clean.
    $self->check_no_mysql();

    # Check that we're in runlevel 3.
    $self->check_for_multiuser;

    # Assure dnsonly/standard matches chosen installer.
    $self->check_license_conflict();

    # TODO: Get rid of these files and replace them with /var/cpanel/dnsonly
    # Disable services by touching files.
    if ( $self->is_dnsonly() ) {
        my @dnsonlydisable = qw( cpdavd );
        foreach my $dis_service (@dnsonlydisable) {
            $self->touch( '/etc/' . $dis_service . 'disable' );
        }
    }

    create_slash_scripts_symlink();

    # Some checks may be done in the child class.
    return;
}

sub create_slash_scripts_symlink {
    if ( -e '/scripts' && !-l '/scripts' ) {
        if ( !-d '/scripts' ) {
            WARN("The system detected /scripts as a file. Moving it to a new location...");
            Common::ssystem( qw{/bin/mv /scripts}, "/scripts.o.$$" );
        }
        else {
            WARN("The system detected the /scripts directory. Moving its contents to the /usr/local/cpanel/scripts directory...");
            Common::ssystem(qw{mkdir -p /usr/local/cpanel/scripts});
            Common::ssystem('cd / && tar -cf - scripts | (cd /usr/local/cpanel && tar -xvf -)');
            Common::ssystem(qw{/bin/rm -rf /scripts});
        }
    }
    unlink qw{/scripts};

    # This symlink *must* be relative in order to allow future in-place OS upgrades.
    symlink(qw{usr/local/cpanel/scripts /scripts}) unless -e '/scripts';

    if ( !-l '/scripts' ) {
        WARN("The /scripts directory must be a symlink to the /usr/local/cpanel/scripts directory. cPanel & WHM does not use the /scripts directory.");
    }
    else {
        DEBUG('/scripts symlink is set to point to /usr/local/cpanel/scripts');
    }

    return;
}

sub check_no_mysql {

    # This can cause failures if the database is newer than the version we're
    # going to install.
    INFO('Checking for an existing MySQL or MariaDB instance...');

    my $mysql_dir = '/var/lib/mysql';
    return unless -d $mysql_dir;
    my $nitems = 0;
    if ( opendir( my $dh, $mysql_dir ) ) {
        $nitems = scalar grep { !/\A(?:\.{1,2}|lost\+found)\z/ } readdir $dh;
        closedir($dh);
    }
    return unless $nitems;

    ERROR("The installation process found evidence that MySQL or MariaDB was installed on this server:");
    ERROR("The $mysql_dir directory is present and not completely empty.");
    FATAL('You must install cPanel & WHM on a clean server.');
    return;
}

sub check_for_multiuser {
    my ($self) = @_;

    # If we can detect multiuser, then we're good. Do no more checks.
    return if $self->check_systemd_multiuser_target;
    return $self->check_runlevel;    # Will fail if it doesn't succeed.
}

# This code is probably dead for all systemd systems. It's not clear when a system would be valid with multi-user being inactive but runlevel being 3.
# This code can probably be removed when we drop RHEL 6 support.
sub check_runlevel {
    my ($self) = @_;

    # From `man runlevel` :
    #       Table 1. Mapping between runlevels and systemd targets
    #       ┌─────────┬───────────────────┐
    #       │Runlevel │ Target            │
    #       ├─────────┼───────────────────┤
    #       │0        │ poweroff.target   │
    #       ├─────────┼───────────────────┤
    #       │1        │ rescue.target     │
    #       ├─────────┼───────────────────┤
    #       │2, 3, 4  │ multi-user.target │
    #       ├─────────┼───────────────────┤
    #       │5        │ graphical.target  │
    #       ├─────────┼───────────────────┤
    #       │6        │ reboot.target     │
    #       └─────────┴───────────────────┘

    my $runlevel = `runlevel`;    ## no critic(ProhibitQxAndBackticks)
    chomp $runlevel;
    my ( $prev, $curr ) = split /\s+/, $runlevel;

    my $message;

    # currently we allow runlevel 3 or 5, as 5 is the default even on Ubuntu Server, just with no X installed or running
    # runlevel can also return unknown
    if    ( !defined $curr )           { $message = "The installation process could not determine the server's current runlevel."; }
    elsif ( $curr != 3 && $curr != 5 ) { $message = "The installation process detected that the server was in runlevel $curr."; }
    else                               { return; }

    # the system claims to be in an unsupported runlevel.
    if ( $self->force ) {
        WARN($message);
        WARN('The server must be in runlevel 3 or 5. Proceeding anyway because --force was specified!');
        return;
    }

    ERROR("The installation process detected that the server was in runlevel $curr.");
    FATAL('The server must be in runlevel 3 or 5 before the installation can continue.');

    return die "unreachable code";
}

sub check_systemd_multiuser_target {
    my ($self) = @_;

    return if $self->distro_major == 6;    # Cloudlinux 6 is the only thing we support that's not systemd.

    local $?;
    `systemctl is-active multi-user.target >/dev/null 2>&1`;    ## no critic(ProhibitQxAndBackticks)
    return 1 if $? == 0;                                        # We're in multiuser state.
    if ( $self->force ) {
        WARN('The installation process detected that the multi-user.target is not active (boot is probably not finished).');
        WARN('The multi-user.target must be active. Proceeding anyway because --force was specified!');
    }
    else {
        ERROR('The installation process detected that the multi-user.target is not active (boot is probably not finished).');
        FATAL('The multi-user.target must be active before the installation can continue.');
    }

    return;
}

sub verify_url {
    my ( $self, $ip ) = @_;
    $ip ||= '';

    return qq[https://verify.cpanel.net/xml/verifyfeed?ip=$ip];
}

#
#   block cPanel&WHM install when a DNSONLY license is valid for the server
#   block DNSONLY license when a cPanel license is valid for the server
#
sub check_license_conflict {
    my ($self) = @_;

    return if $self->skip_license_check;

    my $ip = guess_ip();

    # skip check and continue install if we cannot guess up
    return unless defined $ip;

    INFO("Checking for existing active license linked to IP '$ip'.");

    my $verify_license_xml = q[verify.license.xml];

    my $url = $self->verify_url($ip);

    # check verify.cpanel.net - the xml one...
    Common::fetch_url_to_file( $url, $verify_license_xml );

    my $active_basepkg = 0;
    my $package        = "";
    {
        open( my $fh, '<', $verify_license_xml ) or FATAL("Cannot read file $verify_license_xml.");
        while ( my $line = <$fh> ) {
            next unless $line =~ m/status="1"/;     # package is active
            next unless $line =~ m/basepkg="1"/;    # package is a base package (skipping packages like kernelcare, cloudlinux & co)
            if ( $line =~ m/producttype="([0-9]+)"/ ) {
                $active_basepkg = $1;

                $line =~ m/package="([^"]+)"/;
                $package = $1;

                last;
            }
        }
    }

    return unless $active_basepkg;

    if ( $self->is_dnsonly ) {

        # we cannot install dnsonly if a cPanel license exists
        if ( $active_basepkg != PRODUCT_DNSONLY ) {
            unlink '/var/cpanel/dnsonly';
            ERROR("Unexpected license type found for your IP: https://verify.cpanel.net/app/verify?ip=$ip");
            ERROR("Current active package is $package");
            FATAL("Installation aborted. Perhaps you meant to install latest instead of latest-dnsonly? If not please cancel your cPanel license before installing a cPanel DNSONLY server.");
        }
    }
    else {
        # we cannot install cPanel if a dnsonly license exists
        if ( $active_basepkg & PRODUCT_DNSONLY ) {
            ERROR("Unexpected license type found for your IP: https://verify.cpanel.net/app/verify?ip=$ip");
            FATAL("Installation aborted. Perhaps you meant to install latest-dnsonly instead of latest? If not please cancel your DNSONLY license before installing a cPanel & WHM server.");
        }
    }

    # everything is fine at this point
    return;
}

sub get_myip_url {
    my $conf     = CpanelConfig::read_config('/etc/cpsources.conf');
    my $myip_url = $conf->{'MYIP'} || DEFAULT_MYIP_URL;

    DEBUG("Using MyIp URL to detect server IP '$myip_url'.");
    return $myip_url;
}

sub guess_ip {
    my $url = get_myip_url();

    my $file = q[guess.my.ip];

    my $max = 3;
    foreach my $iter ( 1 .. $max ) {
        unlink $file;
        Common::fetch_url_to_file( $url, $file );
        last if $? == 0;
        if ( $iter == $max ) {
            FATAL("Failed to call URL $url to detect your IP.");
        }
        WARN("Call to $url fails, giving it another try [$iter/$max]");
        sleep 3;
    }

    my $ip;

    {
        open( my $fh, '<', $file ) or FATAL("Cannot read file $file.");
        $ip = readline($fh);
        close($fh);
    }

    chomp($ip) if defined $ip;

    if ( !defined $ip || !length $ip ) {

        # could also use FATAL - be relax for now to avoid false positives
        WARN("Fail to guess your IP using URL $url.");
        return;
    }

    # sanitize the IP - Ipv4 or Ipv6 character set only
    if ( $ip !~ qr{^[0-9a-f\.:]+$}i ) {

        # could also use FATAL - be relax for now to avoid false positives
        WARN("Invalid IP address '$ip' returned by $url");
        return;
    }

    return $ip;
}

sub background_download_packages_used_during_initial_install { die 'unimplemented' }

# NOTE, this is as "concise" as I could make this, though I'm
# basically copying what Cpanel::FileUtils::TouchFile does.
my @touch_meths = (
    sub { return 1 if utime( undef, undef, $_[0] ) },
    sub { return !Common::ssystem( '/bin/touch', $_[0] ) },
);

sub touch {
    my ( $self, $file ) = @_;

    foreach (@touch_meths) {
        return if $_->($file);
    }

    die "Can't touch file $file";
}

sub updatenow {
    my ($self) = @_;

    INFO("Downloading updatenow.static");

    # Download the tar.gz files and extract them instead.

    my $install_version = $self->cpanel_version();
    my $source          = "/cpanelsync/$install_version/cpanel/scripts/updatenow.static.bz2";

    DEBUG("Retrieving the updatenow.static file from $source...");

    # download file in current directory (inside the self extracted tarball)
    unlink 'updatenow.static';
    Common::cpfetch($source);
    chmod 0755, 'updatenow.static';
    my $exit;

    my @flags;
    push @flags, '--skipapache'    => $self->skip_apache;
    push @flags, '--skipreposetup' => $self->skip_repo_setup;

    for ( 1 .. 5 ) {    # Re-try updatenow if it fails.
        INFO("Closing the installation log and passing output control to the updatenow.static file...");

        # close the log file so it can be re-opened by updatenow.
        CpanelLogger::close_log_file();

        my $log_file = CpanelLogger::LOG_FILE();
        $exit = system( './updatenow.static', '--upcp', '--force', "--log=$log_file", @flags );

        # Re-open file regardless of updatenow success.
        CpanelLogger::open_log_for_append();

        return if ( !$exit );

        DEBUG("The installation process detected a failed synchronization. The system will reattempt the synchronization with the updatenow.static file...");
    }

    my $signal = $exit % 256;
    $exit = $exit >> 8;

    FATAL("The installation process was unable to synchronize cPanel & WHM. Verify that your network can connect to httpupdate.cpanel.net and rerun the installer.");
    FATAL("The updatenow.static process terminated with the following exit code: $exit ($signal); The cPanel & WHM installation process cannot proceed.");
    return;
}

sub assure_nobody {

    my $systemd_nobody_file = '/etc/systemd/dont-synthesize-nobody';
    if ( -d "/etc/systemd" && !-e $systemd_nobody_file ) {
        if ( open( my $fh, '>', $systemd_nobody_file ) ) {
            print $fh '';
            close $fh;
        }
        INFO("Touch $systemd_nobody_file");
    }

    my $uid = getpwnam("nobody");
    my $gid = getgrnam("nobody");
    my @ids = ( 65534, 99 );

    my $user_needs_informed = 0;

    if ( !defined $gid ) {
        $user_needs_informed = 1;

        my $addgroup = -x '/usr/sbin/groupadd' ? "groupadd" : "addgroup";
        for my $id (@ids) {
            Common::ssystem( $addgroup, '--system', '--gid', $id, 'nobody', { 'ignore_errors' => 1 } );
            $gid = getgrnam("nobody");
            last if defined $gid;
        }

        if ( !defined $gid ) {
            Common::ssystem( $addgroup, qw/--system nobody/, { 'ignore_errors' => 1 } );
        }

        $gid = getgrnam("nobody");
        FATAL("Could not ensure `nobody` group") if !defined $gid;
    }

    if ( !defined $uid ) {
        $user_needs_informed = 1;

        my $flags = -x '/usr/sbin/groupadd' ? "" : "--disabled-password --disabled-login";
        for my $id (@ids) {
            Common::ssystem( "adduser --system --uid $id --gid $gid --home / --no-create-home --shell /sbin/nologin $flags nobody", { 'ignore_errors' => 1 } );
            $uid = getpwnam("nobody");
            last if defined $uid;
        }

        if ( !defined $uid ) {
            Common::ssystem( "adduser --system --gid $gid --home / --no-create-home --shell /sbin/nologin $flags nobody", { 'ignore_errors' => 1 } );
        }

        $uid = getpwnam("nobody");
        FATAL("Could not ensure `nobody` user") if !defined $uid;
    }

    # if already done, its a noop. adduser’s --gid does not make this happen
    Common::ssystem( 'usermod', '-g', $gid, 'nobody', { 'ignore_errors' => 1 } );

    my $home = ( getpwnam("nobody") )[7];
    if ( !-d $home ) {
        mkdir $home, 0755;
        chown $uid, $gid, $home;
    }

    if ( !-d $home ) {
        WARN('Detected non-existent home directory for `nobody`.');
        WARN('');
        WARN('This situation can result in some harmless STDERR going to your web server’s error log as errors.');
        WARN('');
        WARN('If you experience this your options are:');
        WARN('');
        WARN('    1. Ignore the log entries');
        WARN('    2. Create the directory “$home” if it is safe to do so.');
        WARN('    3. Change the `nobody` user’s home directory to one that exists. e.g. `usermod --home / nobody`');
        WARN('');
    }

    INFO("'nobody' user created with UID $uid and GID $gid") if $user_needs_informed;

    return;
}

1;

Anon7 - 2021