package Cpanel::Easy::Utils;

# cpanel - Cpanel/Easy/Utils.pm                   Copyright(c) 2016 cPanel, Inc.
#                                                           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;
no warnings qw(redefine);
use Cwd                        ();
use Cpanel::SysCmdRC           ();
use Cpanel::SafeRun            ();
use Cpanel::SafeRun::Errors    ();
use Cpanel::DataStore          ();
use Cpanel::SafeDir            ();
use Cpanel::FileUtils          ();
use Cpanel::SafeDir::Read      ();
use Cpanel::Sys::GetOS         ();
use Cpanel::Config::LoadCpConf ();
use Cpanel::FileUtils          ();
use File::Spec                 ();
require lib;

=pod

=head1 Name

Cpanel::Easy::Utils

=head1 Description

Generic utilities that don't fit neatly into a single category.

=head1 API

The following sections contain a description of each API in this module.

=cut

{

    sub copy_file {
        my ( $self, $src, $trg ) = @_;
        Cpanel::FileUtils::safecopy( $src, $trg ) or return ( 0, q{Could not copy '[_1]' to '[_2]': [_3]}, $src, $trg, $! );
        return ( 1, 'ok' );
    }

    sub slurp_file_chomped {
        my ( $self, $file, $args_hr ) = @_;
        $args_hr = {} if ref $args_hr ne 'HASH';

        if ( open my $fh, '<', $file ) {
            my $line = do { local $/; <$fh> };
            close $fh;
            chomp $line if !( exists $args_hr->{'chomp'} && !$args_hr->{'chomp'} );
            return $line if length $line;
        }

        return;
    }

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

        # ulimit -a needs done this way or it exits with -1 and no output via a fork
        $self->output_system_cmd( ['/bin/sh -c "ulimit -a"'] );
    }

    sub _header {
        my ($self) = @_;
        $self->{'ui_obj'}->_header($self) if !$self->{'_'}{'_header'} && $self->{'ui_obj'}->can('_header');
        $self->{'_'}{'_header'}++;
    }

    sub _footer {
        my ($self) = @_;
        $self->{'ui_obj'}->_footer($self) if $self->{'_'}{'_header'} && !$self->{'_'}{'_footer'} && $self->{'ui_obj'}->can('_footer');
        $self->{'_'}{'_footer'}++;
    }

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

        if ( $self->get_param('simulate-failed-memtest') ) {
            $self->_header();
            $self->print_alert_color( 'red', q{Simulating memory test failure as per 'simulate-failed-memtest' flag} );
            $self->_footer();
            exit;
        }

        my ( $vz_rc, @vz_text ) = $self->virtuozzo_memory_check();
        if ( !$vz_rc ) {
            $self->_header();
            $self->print_alert(@vz_text);
            $self->_footer();
            exit;
        }

        if ( eval { require cPanel::MemTest; } ) {
            local $self->{'_'}{'cPanel::MemTest required MB'} = $self->{'_'}{'cPanel::MemTest required MB'};
            if ( $self->get_param('memtest-mb') ) {
                my $memtest = $self->get_param('memtest-mb');
                if ( $memtest !~ m{ \A \d+ \z }xms ) {
                    $self->_header();
                    $self->print_alert( q{Invalid value for '[_1]', using default: '[_2]'}, 'memtest-mb', 'not numeric' );    # $!
                }
                elsif ( $memtest > 1024 || $memtest < 1 ) {
                    $self->_header();
                    $self->print_alert( q{Invalid value for '[_1]', using default: '[_2]'}, 'memtest-mb', 'out of range' );    # $!
                }
                else {
                    $self->_header();
                    $self->print_alert( q{Using '[_1]' as per '[_2]'}, "$memtest MB", 'memtest-mb' );
                    $self->{'_'}{'cPanel::MemTest required MB'} = $memtest;
                }
            }

            my $mb_allocated = cPanel::MemTest::testallocate( $self->{'_'}{'cPanel::MemTest required MB'} );
            if ( $mb_allocated != $self->{'_'}{'cPanel::MemTest required MB'} ) {
                $self->_header();
                $self->print_alert_color(
                    'red',
                    qq{
There was a problem allocating memory!
Could only allocate $mb_allocated Megabytes of memory.
Easyapache will fail to build because the system does not have enough free Memory.
Aborting.
},
                );
                $self->_footer();
                exit;
            }
            elsif ( !$ENV{'SCRIPT_FILENAME'} && !$ENV{'HTTP_HOST'} ) {
                $self->print_alert( q{Pre allocation testing was successfully able to allocate [_1]MB}, $mb_allocated );
            }
        }
        else {
            $self->_header();
            $self->print_alert( q{Skipped testing provided by '[_1]', please install this module}, 'cPanel::MemTest' );
        }
    }

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

        my $vzinfofile = '/proc/vz/veinfo';           # Presence is THE criterion for a virtuozzo system, see cPanelRPM.pm and elsewhere
        my $vzbeanfile = '/proc/user_beancounters';
        if ( -e $vzinfofile && -e $vzbeanfile ) {
            my %mem_p;
            my @tests = (
                {
                    'setting_key' => 'privvmpages',
                    'threshhold'  => 90,
                    'maketext'    => q{Critical Error (VZ): You are only only allowed to use [_1] Megabytes of ram! [_2] Megabytes is required.},
                    'is_fatal'    => 1,
                },
                {
                    'setting_key' => 'privvmpages',
                    'threshhold'  => 512,
                    'maketext'    => q{Critical Warning (VZ): You are only only allowed to use [_1] Megabytes of ram! [_2] Megabytes is recommended.},
                    'is_fatal'    => 0,
                },
                {
                    'setting_key' => 'vmguarpages',
                    'threshhold'  => 512,
                    'maketext'    => q{Warning (VZ): You are only only guaranteed [_1] Megabytes of ram! [_2] Megabytes is recommended.},
                    'is_fatal'    => 0,
                },
                {
                    'setting_key' => 'oomguarpages',
                    'threshhold'  => 512,
                    'maketext'    => q{Warning (VZ): You are only only guaranteed [_1] Megabytes of ram when the system is out of ram! [_2] Megabytes is recommended.},
                    'is_fatal'    => 0,
                },
            );

            my @ctids = $self->get_relevant_ctids($vzinfofile);    # ctid means container id (aka veid)

            my $relevant_ctid;
            my $nline;
            if ( open my $bc_fh, '<', $vzbeanfile ) {
                while ( my $line = readline($bc_fh) ) {
                    ++$nline;
                    next if $line =~ /^\s*Version:/i;                 # skip version info line
                    next if $line =~ /^\s*uid\s+resource\s+held/i;    # skip column header line
                    my @fields = split /\s+/, $line;
                    @fields = grep { defined && length } @fields;
                    if ( 7 == @fields && $fields[0] =~ /(.*):$/ ) {
                        my $ctid = $1;
                        $relevant_ctid = grep { $_ eq $ctid } @ctids;
                    }
                    elsif ( $relevant_ctid && $line =~ m/^\s*(vmguarpages|privvmpages|oomguarpages)\s+(.*)/ ) {
                        my $pname = $1;
                        my $parm  = $2;
                        chomp($parm);
                        my ( $held, $maxheld, $barrier, $limit, $failcnt ) = split( /\s+/, $parm );

                        next unless $barrier;    # the sky is the limit

                        # $barrier is the number of 4KB pages, we want MB
                        $barrier -= $held;       # only work with what is available
                        $mem_p{$pname} += $barrier * 4 / 1024;
                    }
                }
                close($bc_fh);

                for my $key ( keys %mem_p ) {
                    $mem_p{$key} = int $mem_p{$key};
                }

                for my $test (@tests) {
                    if ( $mem_p{ $test->{'setting_key'} } && $mem_p{ $test->{'setting_key'} } < $test->{'threshhold'} ) {
                        if ( $test->{'is_fatal'} ) {
                            return ( 0, $test->{'maketext'}, $mem_p{ $test->{'setting_key'} }, $test->{'threshhold'} );
                        }
                        else {
                            $self->_header();
                            $self->print_alert( $test->{'maketext'}, $mem_p{ $test->{'setting_key'} }, $test->{'threshhold'} );
                        }
                    }
                }
            }
            else {
                return ( 0, q{Could not open '[_1]' for reading: [_2]}, $vzbeanfile, $! );    # we already know it exists but we can't read it = problem
            }
        }

        return ( 1, 'ok' );
    }

    sub get_relevant_ctids {
        my ( $self, $vzinfofile ) = @_;
        my @ctids;
        if ( open my $fh, '<', $vzinfofile ) {
            while (<$fh>) {
                my ($ctid) = m{ ^ \s* (\S+) }x;
                push @ctids, $ctid if defined $ctid;
            }
            close $fh;
        }
        return @ctids;
    }

    sub disk_space_check {
        my $self = shift;

        my $required_space = $self->get_param('disktest-mb') || $self->{'_'}{'cPanel::DiskTest required MB'};

        if ( $required_space !~ /^\d+$/ || $required_space > 4096 || $required_space < 1 ) {
            $required_space = $self->{'_'}{'cPanel::DiskTest required MB'};
            $self->print_alert( q{Invalid value for '[_1]', using default of '[_2]'}, 'disktest-mb', $required_space );
        }

        my $home = $self->{'opt_mod_src_dir'};

        my $passed_check = 1;
        my $free_mb;
        eval {
            require Filesys::Df;
            my $df_ref = Filesys::Df::df($home);
            if ( $df_ref->{'bfree'} < $required_space * 1024 ) {
                $passed_check = 0;
                $free_mb      = int( $df_ref->{'bfree'} / 1024 );
            }
        };

        unless ($passed_check) {
            $self->_header();
            $self->print_alert_color(
                'red',
                qq{
There appears to be insufficient disk space in $home!
${free_mb}MB is available on this file system and ${required_space}MB is needed.
Easyapache will fail to build because there is not enough free disk space.
Aborting.
},
            );
            $self->_footer();
            exit;
        }

        return;
    }

    # basic list of packages required for compiling
    # adding zlib and openssl here since many different things require them
    sub basic_ensurepkgs_list {
        my $self    = shift;
        my @pkglist = qw(
          lsof
          patch
          gmake
          automake
          automake19
          autoconf
          autoconf261
          gettext
          sed
          pam-dev
          pam-devel
          flex
          bison
          glibc-dev
          glibc-devel
          fileutils
          coreutils
          libtool
          libltdl
          libltdl-devel
          libltdl3-devel
          libtool-ltdl
          libtool-ltdl-devel
          libtool-libltdl-devel
          openssl
          openssl-dev
          openssl-devel
          ssl-dev
          libssl-dev
          libopenssl0
          libopenssl0-dev
          libopenssl0-devel
          libopenssl0.9.7-devel
          libopenssl0.9.7-static-devel
          krb5-dev
          krb5-devel
          expat
          expat-dev
          expat-devel
          zlib
          zlib-devel
          zlib1-devel
          libz-devel
        );

        push @pkglist, 'gcc';
        push @pkglist, 'gcc-c++';
        push @pkglist, 'make';
        push @pkglist, 'lex';
        push @pkglist, 'cpp';
        if ( $self->{'cpu_bits'} eq '64' ) {

            # Work around for http://bugzilla.redhat.com/bugzilla/show_bug.cgi?id=215839
            push @pkglist, 'libstdc++.x86_64';
            push @pkglist, 'libstdc++-dev.x86_64';
            push @pkglist, 'libstdc++-devel.x64_64';
        }
        else {
            push @pkglist, 'libstdc++';
            push @pkglist, 'libstdc++-dev';
            push @pkglist, 'libstdc++-devel';
        }

        if ( $self->has_cloudlinux_support() ) {
            push @pkglist, 'liblve-devel';
        }

        return @pkglist;
    }

    sub fix_glibc_dummy {
        my $self = shift;
        return unless ( $self->is_virtuozzo() );
        my $rpmlist = Cpanel::SafeRun::saferunnoerror( 'rpm', '-qa' );
        if ( $rpmlist =~ /^(glibc-dummy-\S+-\S+)$/m ) {
            my $dummy_package = $1;
            $self->print_alert( q{Removing '[_1]' package}, $dummy_package );
            $self->run_system_cmd( [ 'rpm', '-e', '--justdb', $dummy_package ] );
        }
    }

    sub set_virtuozzo_memory_cflags {
        my $self = shift;
        return if ( -e '/var/cpanel/easy_disable_vps_memory_cflags' );
        return unless ( $self->is_virtuozzo() );
        my %test;
        foreach my $flagtype ( 'CFLAGS', 'CXXFLAGS' ) {
            $test{$flagtype} = $ENV{$flagtype} || '';
            unless ( $test{$flagtype} =~ /\s+ggc-min-expand\s*=/ ) {
                $test{$flagtype} .= ' --param ggc-min-expand=1';
            }
            unless ( $test{$flagtype} =~ /\s+ggc-min-heapsize\s*=/ ) {
                $test{$flagtype} .= ' --param ggc-min-heapsize=4096';
            }
        }
        my $test_src = '/cp-ggc-min-heapsize.c';
        my $test_bin = '/cp-ggc-min-heapsize.run';
        if ( -e $test_src ) {
            unlink $test_src;
        }
        if ( -e $test_bin ) {
            unlink $test_bin;
        }
        if ( open my $test_fh, '>', $test_src ) {
            print $test_fh qq[#include <stdio.h>\nint main() {\nprintf("ggc-min-heapsize compiled\\n");\nreturn 0;\n}\n];
            close $test_fh;
            $test{'CFLAGS'} =~ s{^\s+}{};    # trim WS from beginning so that the split() does not end up with an empty first "flag"
            $test{'CFLAGS'} =~ s{\s+$}{};    # trim WS from end for parity with removing from beginning (not likley to be needed but just in case)
            Cpanel::SafeRun::saferunallerrors( 'gcc', split( /\s+/, $test{'CFLAGS'} ), '-o', $test_bin, $test_src );
            if ( -e $test_bin ) {
                $ENV{'CFLAGS'}   = $test{'CFLAGS'};
                $ENV{'CXXFLAGS'} = $test{'CXXFLAGS'};
                unlink $test_bin;
            }

            unlink $test_src;
        }
    }

    sub is_virtuozzo {
        my $self    = shift;
        my $envtype = '';
        if ( open my $et_fh, '<', '/var/cpanel/envtype' ) {
            $envtype = do { local $/; <$et_fh> };
            close $et_fh;
        }
        return $envtype =~ /virtuozzo/i;
    }

    # Returns 1 if /usr/lib is clean of IMAP libraries, 0 otherwise.
    sub imap_rpm_cleanup {
        my $self          = shift;
        my @bad_libraries = (
            '/usr/lib/c-client.a',   '/usr/lib/libc-client.a',   '/usr/lib/c-client.so',   '/usr/lib/c-client.so.1',   '/usr/lib/libc-client.so',   '/usr/lib/libc-client.so.1',
            '/usr/lib64/c-client.a', '/usr/lib64/libc-client.a', '/usr/lib64/c-client.so', '/usr/lib64/c-client.so.1', '/usr/lib64/libc-client.so', '/usr/lib64/libc-client.so.1'
        );
        my $leftover   = 0;
        my %tried_rpms = ();
        foreach my $library (@bad_libraries) {
            next unless ( -e $library || -l $library );
            $self->print_alert( 'Found old IMAP library at [_1]', $library );
            my $package;
            if ( $self->{'cpu_bits'} eq '64' ) {
                $package = Cpanel::SafeRun::saferunallerrors( 'rpm', '-qf', $library, '--queryformat', '%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}\n' );
            }
            else {
                $package = Cpanel::SafeRun::saferunallerrors( 'rpm', '-qf', $library );
            }
            next unless $package;
            $package =~ s/(^\s+|\s+$)//sg;

            if ( $package =~ /^(imap|libc-client)\S*-dev\S*$/i && !$tried_rpms{$package} ) {
                $self->print_alert( 'Attempting to remove [_1]', $package );
                $self->run_system_cmd( [ 'rpm', '-e', $package ] );
                $tried_rpms{$package} = 1;
            }
            $leftover = 1 if ( -e $library || -l $library );
        }
        if ($leftover) {
            $self->print_alert('Could not remove obsolete IMAP libraries.  They will be hidden during the compile instead.');
            return 0;
        }
        return 1;

    }

    sub _move_imap_libraries {
        my $self          = shift;
        my $hide          = shift;
        my @bad_libraries = (
            '/usr/lib/c-client.a',   '/usr/lib/libc-client.a',   '/usr/lib/c-client.so',   '/usr/lib/c-client.so.1',   '/usr/lib/libc-client.so',   '/usr/lib/libc-client.so.1',
            '/usr/lib64/c-client.a', '/usr/lib64/libc-client.a', '/usr/lib64/c-client.so', '/usr/lib64/c-client.so.1', '/usr/lib64/libc-client.so', '/usr/lib64/libc-client.so.1'
        );
        foreach my $library (@bad_libraries) {
            my $src = $library;
            my $dst = $library . '.hidden_from_linker_by_ea3';
            ( $src, $dst ) = ( $dst, $src ) if ( !$hide );
            if ( -e $src || -l $src ) {
                unlink($dst) if ( -e $dst || -l $dst );
                rename( $src, $dst );
            }
        }
    }

    sub hide_imap_libraries {
        my $self = shift;
        return $self->_move_imap_libraries(1);
    }

    sub unhide_imap_libraries {
        my $self = shift;
        return $self->_move_imap_libraries(0);
    }

    # add fetch(), print(), get(), say() as well ??
    sub maketext {
        my ( $self, @args ) = @_;
        return $self->{'lang_obj'}->maketext(@args);
    }

    sub get_param {
        my ( $self, $name ) = @_;
        return $self->{'param_obj'}->param($name);
    }

    sub set_param {
        my ( $self, $name, @vals ) = @_;
        $self->{'param_obj'}->param( $name, @vals );
    }

    sub process_help {
        my ($self) = @_;
        my $help = $self->get_param('help');
        return unless $help;
        if ( $help =~ m{^hooks} ) {
            if ( $self->{'ui_obj'}->can('hook_help') ) {
                $self->{'ui_obj'}->hook_help($self);    # exits
            }
        }

        if ( ref $self->{'help'} ) {
            $self->{'help'}->($self);
        }
    }

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

        return
          if !defined $self->get_param('action')
          || $self->get_param('action') !~ m{ \A \w+ \z }xms;

        my $action = $self->get_param('action');

        ##
        #
        # DETOUR: The below if condition branches out to use new
        # template system to render the pages.
        #
        # Note: Old code is still kept (the code that follows the if condition) for
        # backward compatibility with 11.32.
        #
        ##

        # Check if requested web page is rendered through template
        if ( !$self->{'ui_obj'}->pre_1134_version()
            && ( $action eq '_profile_upload' ) ) {

            # Call the corresponding method's '_tmpl' functions.
            $action = $action . "_tmpl";
        }

        # use obj not returned code ref so the ui_obj is an arg:
        if ( $self->{'ui_obj'}->can($action) ) {
            return 1 if $check;
            $self->{'ui_obj'}->$action($self);
        }
    }

=head2 I<< Cpanel::Easy::Utils::repair_prefs() >>

Update the preferences file to enable the notify_cpanel option, only
if the $easy->{'report_touchfile'} file does not exist and one or more
of the send_error_reports or send_server_configuration cPanel
configuration settings are enabled.

B<Returns:> 0 on failure, 1 on success

B<Notes:> There are some instances in which errors may be generated
(i.e. writing out the prefs file, and touching the touchfile); we will
endure the errors silently, without reporting them to the user.

If there is no data loaded, or the data is unable to be saved, the
touchfile will not be created.  We'll keep trying on subsequent runs.

=cut

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

        if ( !-e $self->{'report_touchfile'} ) {
            my $prefs_hr = $self->deserialize( $self->{'prefs_file'} );
            my $cpconf   = Cpanel::Config::LoadCpConf::loadcpconf();

            # If we got no data somewhere, don't risk making the
            # problem worse; just bail out
            return 0 if ( !keys( %{$prefs_hr} ) || !keys( %{$cpconf} ) );

            if ( $cpconf->{'send_error_reports'} == 1 || $cpconf->{'send_server_configuration'} == 1 ) {
                $prefs_hr->{'notify_cpanel'} = 1;
            }

            return ( $self->serialize( $self->{'prefs_file'}, $prefs_hr ) && Cpanel::FileUtils::touchfile( $self->{'report_touchfile'} ) );
        }
        return 1;
    }

    sub serialize {
        my ( $self, $file, $outof_hr ) = @_;
        return Cpanel::DataStore::store_ref( $file, $outof_hr );
    }

    sub deserialize {
        my ( $self, $file, $into_hr ) = @_;
        return Cpanel::DataStore::fetch_ref( $file, $into_hr ) || {};
    }

=pod

=head2 I<< Cpanel::Easy::Utils::add_command() >>

On success or failure, EasyApache has to issue a variety of cleanup
routines.  This API allows you to add commands to be executed during
1 of the 3 cleanup stages:

=head3 pre-confgen-commands

The first stage of routines that occurs during the "cleanup" phase
that occurs near the end of an EasyApache run, but just before it
generates new Apache httpd.conf.

=head3 post-confgen-pre-restart-commands

The second stage of routines that occurs just after Apache httpd.conf
regeneration, but before the Apache web server is restarted and tested.
This stage can be executed in one of two ways:

  1. If a successful build has occurred
  2. A failed build has occurred, and a restore was not requested.

=head3 post-confgen-commands

The third (and last) stage of routines that occurs after the Apache
httpd.conf has been regenerated and it has been restarted.  Assuming
a successful EasyApache build, it runs just before the exection of
the optional hook script, /usr/local/cpanel/scripts/posteasyapache.

  Input
    Scalar -- One of the above mentioned key names
    Array -- One or more CODE or ARRAY references suitable for
      run_command() execution.

  Output
    N/A

  Note
    If no key or values are supplied, this acts as a NOOP

=cut

    sub add_command {
        my $self = shift;
        my $key  = shift;
        push @{ $self->{'_'}{'commands'}{$key} }, @_ if defined $key && @_;
    }

=pod

=head2 I<< Cpanel::Easy::Utils::run_commands() >>

The primary API to execute a requested stages of cleanup in
EasyApache.

  Input
    Scalar -- key name representing a stage of clean up.
       The valid key names are defined in add_command().

  Output
    N/A

  Note
    Any key is accepted and no validation is performed.

=cut

    sub run_commands {
        my ( $self, $key ) = @_;
        if ( exists $self->{'_'}{'commands'}{$key} ) {
            if ( ref $self->{'_'}{'commands'}{$key} eq 'ARRAY' ) {
                for my $cmd_ref ( @{ $self->{'_'}{'commands'}{$key} } ) {
                    if ( ref($cmd_ref) eq 'CODE' ) {
                        $cmd_ref->($self);
                    }
                    else {
                        $self->run_one_cmd($cmd_ref);
                    }
                }
            }
        }
    }

    sub run_one_cmd {
        my ( $self, $cmd_ref ) = @_;
        $self->print_alert( q{Executing '[_1]'}, join( ' ', @{$cmd_ref} ) );
        my ( $rc, @err ) = $self->run_system_cmd_returnable( $cmd_ref, 1, 1 );
        if ( !$rc ) {
            $self->print_alert(@err);
            return;    # just in case we want it
        }
        return 1;      # just in case we want it
    }

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

        if ( !$self->is_namespace($ns) ) {
            return $self->context_return(q{invalid name space string});
        }

        if ( !eval "require $ns" ) {
            return $self->context_return( q{require() failed for '[_1]': [_2]}, $ns, $@ );
        }
        else {
            my $get_ref = '';
            no strict 'refs';

            if ( my $path_inst_cr = $ns->can('path_installed') ) {
                $get_ref = $path_inst_cr->( $self, @args );
            }
            else {
                return $self->context_return( q{'[_1]' does not have '[_2]'}, $ns, 'path_installed()' );
            }

            if ( ref $get_ref ne 'HASH' ) {
                return $self->context_return( q{'[_1]' did not return a '[_2]' reference}, $ns, 'HASH' );
            }

            if ( -d $get_ref->{'install_path'} && $get_ref->{'itis_up2date'}->() ) {
                my $cur_md5 = $self->get_current_path_md5( $get_ref->{'install_path'} ) || -1;
                my $lst_md5 = $self->get_saved_path_md5( $get_ref->{'install_path'} )   || -2;
                if ( $cur_md5 ne $lst_md5 ) {
                    my $md5_note = <<"END_WARN";

        '[_1]' is up to date but looks like it has local modifications.
        If you experience any trouble remove '[_1]' so that it will be rebuilt fresh.
END_WARN

                    $self->{'ui_obj'}->alert_over_last_twiddle( $self, $md5_note, $get_ref->{'install_path'}, );
                    $self->{'_'}{'post_failure_fyi_info'} .= "\n!!\n" . $self->maketext( $md5_note, $get_ref->{'install_path'} ) . "\n!!\n";
                }
                return wantarray ? ( $get_ref->{'install_path'}, 'ok' ) : $get_ref->{'install_path'};
            }

            my $start = Cwd::cwd();

            if ( !chdir $self->{'opt_mod_src_dir'} ) {
                return $self->context_return( q{Could not chdir into '[_1]': [_2]}, $self->{'opt_mod_src_dir'}, $! );
            }

            my $targz         = $self->get_file_from_namespace($ns) . '.tar.gz';
            my $md5_before    = $self->get_current_path_md5($targz);
            my $targz_md5sums = $self->get_targz_md5sums_hashref();

            my ( $targz_rc, @text ) = $self->fetch_tarball_if_needed( $targz, $targz_md5sums, $ns );

            if ( !$targz_rc ) {
                chdir $start or $self->print_alert( q{Could not chdir back into '[_1]': [_2]}, $start, $! );
                return $self->context_return(@text) if !$targz_rc;
            }

            # If we just downloaded the tarball it may or may not match $ns
            my $md5_current = $self->get_current_path_md5($targz);
            if ( $self->get_param('simulate-optlib-targz-check-loop') || !$md5_before || $md5_before ne $md5_current || !$md5_current ) {

                # This loop can only result in a return; or a retry so the first ting we do is
                # go back to where we started (we are not operating in . at this point)
                chdir $start or $self->print_alert( q{Could not chdir back into '[_1]': [_2]}, $start, $! );

                $self->{'cache'}{'get_path_installed-targz-loop-check'}{$ns}++;

                # If a new version was published underneath us then the tarball will
                # not match the opt mod and it will fail to build at some point.
                #
                # We can't grab the old tarball, and we may not have had the version we needed
                # before we fetch_tarball_if_needed() so the best option is to grab the latest .pm
                # and re-try this function with the new .pm's code

                if ( $self->{'cache'}{'get_path_installed-targz-loop-check'}{$ns} > 3 ) {
                    return $self->context_return(
                        "Attempted to sync \xe2\x80\x9c[_1]\xe2\x80\x9d too many times ([numf,_2]).",
                        $targz,
                        ( $self->{'cache'}{'get_path_installed-targz-loop-check'}{$ns} - 1 ),
                    );
                }

                $self->print_alert(
                    "Reloading \xe2\x80\x9c[_1]\xe2\x80\x9d since \xe2\x80\x9c[_2]\xe2\x80\x9d was updated.",
                    $ns,
                    $targz,
                );

                $self->print_alert(
                    "The md5 sum for \xe2\x80\x9c[_1]\xe2\x80\x9d was previously \xe2\x80\x9c[_2]\xe2\x80\x9d and is currently \xe2\x80\x9c[_3]\xe2\x80\x9d.",
                    $targz,
                    ( $md5_before  || '' ),
                    ( $md5_current || '' ),
                );

                # download latest version of optlib opt mod:
                $self->fetch_from_httpupdate(
                    'host'     => $self->get_easources_host(),
                    'url'      => "$self->{'httpupdate_uri'}/" . $self->get_file_from_namespace( $ns, 1 ),
                    'destfile' => $self->get_file_from_namespace($ns),
                );

                # ensure that the new .pm's code is pulled in the next time get_path_installed() eval()s in $ns
                $self->unload_ns($ns);

                # now that our tarball and .pm have both been downloaded and the old .pm's code removed:
                #    have get_path_installed() start over fresh by calling itself
                goto &get_path_installed;    # (infinite loop protection is handled via counter kept in the object)
            }

            my ( $untar_rc, @untar_text ) = $self->untar_targz($targz);

            if ( !$untar_rc ) {
                chdir $start or $self->print_alert( q{Could not chdir back into '[_1]': [_2]}, $start, $! );
                return $self->context_return(@untar_text);
            }

            if ( !-d $get_ref->{'install_path'} ) {
                unlink( $get_ref->{'install_path'} ) if ( -e $get_ref->{'install_path'} || -l $get_ref->{'install_path'} );
                if ( !File::Copy::Recursive::pathmk( $get_ref->{'install_path'} ) ) {
                    chdir $start or $self->print_alert( q{Could not chdir back into '[_1]': [_2]}, $start, $! );
                    return $self->context_return( q{ pathhmk() failed for '[_1]': [_2]}, $get_ref->{'install_path'}, $! );
                }
            }

            if ( chdir $get_ref->{'working_path'} ) {

                $self->{'ui_obj'}->alert_over_last_twiddle(
                    $self,
                    q{Installing or updating '[_1]', this } . q{will take a while and shouldn't have to be done again } . 'until a new version is released by the vendor.',
                    $get_ref->{'name'},
                );

              CMD_LIST:
                for my $cmd_ar ( @{ $get_ref->{'command_list'} } ) {

                    if ( ref $cmd_ar eq 'CODE' ) {
                        my ( $rc, @text ) = $cmd_ar->( $self, @args );
                        if ( !$rc ) {
                            chdir $start or $self->print_alert( q{Could not chdir back into '[_1]': [_2]}, $start, $! );
                            return ( $rc, @text );
                        }
                    }
                    else {
                        my $optional = 0;
                        if ( $cmd_ar->[0] eq ':optional:' ) {
                            $optional = 1;
                            shift @{$cmd_ar};
                            $self->print_alert( q{Begin optional command: '[_1]'}, join( ' ', @{$cmd_ar} ) );
                        }

                        my ( $rc, $exit ) = $self->run_system_cmd( $cmd_ar, 1 );

                        if ( !$rc ) {
                            my $failed_in_dir = $self->cwd();
                            if ($optional) {
                                $self->print_alert( q{Command failed in '[_1]': '[_2]'}, $failed_in_dir, join( ' ', @{$cmd_ar} ) );
                            }
                            else {
                                chdir $start or $self->print_alert( q{Could not chdir back into '[_1]': [_2]}, $start, $! );
                                return $self->context_return( q{Command failed in '[_1]': '[_2]'}, $failed_in_dir, join( ' ', @{$cmd_ar} ) );
                            }
                        }
                        elsif ($optional) {
                            $self->print_alert( q{End optional command: '[_1]'}, join( ' ', @{$cmd_ar} ) );
                        }
                    }
                }

                if ( $self->{'cpu_bits'} eq '64' ) {
                    my $lib   = File::Spec->catfile( $get_ref->{'install_path'}, 'lib' );
                    my $lib64 = File::Spec->catfile( $get_ref->{'install_path'}, 'lib64' );
                    if ( -e $lib && !-e $lib64 ) {
                        symlink $lib, $lib64;
                    }
                }

                $self->set_path_md5( $get_ref->{'install_path'} );

                if ( chdir $start ) {
                    return wantarray ? ( $get_ref->{'install_path'}, 'ok' ) : $get_ref->{'install_path'};
                }
                else {
                    return $self->context_return( q{Could not chdir back into '[_1]': [_2]}, $start, $! );
                }
            }
            else {
                return $self->context_return( q{Could not chdir into '[_1]': [_2]}, $get_ref->{'working_path'}, $! );
            }
        }
    }

    sub output_system_cmd {
        my ( $self, $cmd_ar, $report_only ) = @_;
        my ( $rc, @err ) = $self->run_system_cmd_returnable( $cmd_ar, 0, 1 );
        if ($rc) {
            $self->{'last_run_system_cmd'} =~ s{RC\s+-\d+-\d+[.]\d+-\s+}{};
            my $string = $self->maketext( q{Output from '[_1]': [_2]}, join( ' ', @{$cmd_ar} ), "\n$self->{'last_run_system_cmd'}" );
            $self->_append_to_report( $string, 'System Command' );
            $self->print_alert("\n$string") if !$report_only;
        }
        else {
            $self->_append_to_report( $self->maketext(@err), 'System Command Error' );
            $self->print_alert(@err) if !$report_only;
        }
    }

    sub hook_script {
        my ( $self, $script ) = @_;
        return ( 1, 'skip-hook flag' ) if $self->get_param('skip-hooks');

        if ( !exists $self->{'cache'}{'hook_script_args'} ) {
            $self->{'cache'}{'hook_script_args'} = [
                $self->{'version'},     # 3.5.0
                $self->{'revision'},    # 4128 or 4128 (test branch)
                                        # apache version
                '',                     # don't know it yet apparently

                # CSV php versions
                '',                                   # don't know it yet apparently
                ( $self->get_param('hook-args') ),    # specifically in array context
            ];
        }

        if ( -x $script ) {
            {

                # YAML::Syck is brought in by Cpanel::DataStore already but just in case:
                require YAML::Syck;
                local $ENV{'ea3_params_yaml'} = YAML::Syck::Dump( $self->{'ui_obj'}->get_param_hashref($self) );
                $self->print_alert_color( 'blue', q{Executing custom hook '[_1]'}, $script );
                my ( $rc, @msg ) = $self->run_system_cmd_returnable( [ $script, @{ $self->{'cache'}{'hook_script_args'} } ] );
                $self->print_alert_color( 'blue', q{Done executing '[_1]'}, $script );

                if ( !$rc ) {
                    $self->print_alert_color( 'red', @msg );
                    return;
                }
            }
            return 1;
        }

        return ( 1, q{'[_1]' does not exist}, $script );    # not used but its there if we want it
    }

    sub report_system_cmd {
        my ( $self, $cmd_ar ) = @_;
        $self->output_system_cmd( $cmd_ar, 1 );
    }

    sub ls_ld_each_part_of {
        my ( $self, $path, $report_only, $skip_root ) = @_;

        my @progressive;

        my ( undef, $dir, $file ) = File::Spec->splitpath($path);
        my @parts     = File::Spec->splitdir($dir);
        my $pathsofar = File::Spec->catdir( shift @parts );
        push @progressive, $pathsofar if @parts;

        pop @parts if @parts || $pathsofar =~ m{^/};

        for my $section (@parts) {
            $pathsofar = File::Spec->catdir( $pathsofar, $section );
            push @progressive, $pathsofar;
        }

        push @progressive, $path if $path ne $pathsofar;

        if ( $skip_root && $progressive[0] eq File::Spec->catdir('') && @progressive > 1 ) {
            shift @progressive;
        }

        for my $part ( reverse @progressive ) {
            my $cmd_ar = [ 'ls', '-ld', $part ];
            $report_only ? $self->report_system_cmd($cmd_ar) : $self->output_system_cmd($cmd_ar);
        }
    }

    sub tail_last_lines_of {
        my ( $self, $file, $lines, $report_only ) = @_;

        if ( !-f $file ) {
            $self->print_alert( q{Invalid file type for '[_1]'}, 'tail_last_lines_of()' );
            return;
        }

        $lines = int($lines) || 20;

        my $cmd_ar = [ 'tail', '-n', $lines, $file ];
        $report_only ? $self->report_system_cmd($cmd_ar) : $self->output_system_cmd($cmd_ar);
    }

    sub _append_to_report {
        my ( $self, $text, $title ) = @_;

        $title ||= 'No title given: ' . join( ' : ', caller(1) );
        $self->{'_'}{'debug_info_for_report'} .= "\n!!\n[ $title ]\n$text\n!!\n";
    }

    sub run_system_cmd_stack {
        my ( $self, $command_ref, $skip_stash, $noout ) = @_;

        for my $cmd ( @{$command_ref} ) {
            $self->print_alert( q{Executing '[_1]'}, join( ' ', @{$cmd} ) );
            my ( $rc, @msg ) = $self->run_system_cmd_returnable( $cmd, $skip_stash, $noout );
            $self->print_alert( q{Done executing '[_1]'}, join( ' ', @{$cmd} ) );
            return ( $rc, @msg ) if !$rc;
        }

        return ( 1, 'ok' );
    }

    sub run_system_cmd_returnable {
        my ( $self, $command_ref, $skip_stash, $noout ) = @_;
        my ( $rc, $exit ) = $self->run_system_cmd( $command_ref, $skip_stash, $noout );

        return ( 1, 'ok' ) if $rc;
        if ( $exit eq '-1' ) {
            return ( 0, q{'[_1]' failed with exit code '[_2]' (IE: command not found)}, join( ' ', @{$command_ref} ), $exit );
        }
        return ( 0, q{'[_1]' failed with exit code '[_2]'}, join( ' ', @{$command_ref} ), $exit );
    }

    sub run_system_cmd_errors {
        my $self   = shift;
        my $output = Cpanel::SafeRun::Errors::saferunallerrors(@_);
        return ( $? >> 8, $output );
    }

    sub run_system_cmd {
        my ( $self, $command_ref, $skip_stash, $noout ) = @_;

        require IPC::Open3;
        my $rc = 0;
        my $exit;
        $self->{'last_run_system_cmd'} = '';

        my $uniq = time() . '.' . $$;
        my ( $write_fh, $read_fh );

        # my $error_fh = Symbol::gensym();

        # make read/err the same handle so we don't have to IO::Select them and so we don't miss any lines
        my $child_pid = IPC::Open3::open3( $write_fh, $read_fh, $read_fh, Cpanel::SysCmdRC::get_path(), $uniq, @{$command_ref} );

        local *_;

        # to avoid "Modification of readonly value attempted" errors with @_
        # You ask, "Do you mean the _open3()'s or while()'s @_? " and the answer is: "exactly!" ;p

      CMD_OUTPUT:
        while ( my $cur_line = readline($read_fh) ) {
            $self->{'last_run_system_cmd'} .= $cur_line if !$skip_stash;
            $self->perl_profile_stat( join( ' ', '[SYSTEM]', @{$command_ref} ), $child_pid );

            if ( Cpanel::SysCmdRC::string_is_rc_line($cur_line) ) {
                my ( $_exit, $_uniq ) = Cpanel::SysCmdRC::parse_rc_line($cur_line);
                if ( $_uniq ne $uniq ) {
                    $self->print_to_log_and_screen( $self->maketext( q{rc_line formatted line '[_1]' does not match '[_2]': ([_3]), continuing on...}, $_uniq, $uniq, $_exit ) );
                }
                else {
                    $rc   = Cpanel::SysCmdRC::get_return_value_from_rc($_exit);
                    $exit = $_exit;
                    last CMD_OUTPUT;
                }
            }

            $self->print_verbose_aware($cur_line) if !$noout;
        }

        waitpid $child_pid, 0;    # before or after close()s best ???
        close $write_fh;
        close $read_fh;

        return wantarray ? ( $rc, $exit ) : $rc;
    }

    # Find out which patch level this uses.  This will try up to 7 levels trying to find
    # something that will work.  Why?  Because of the extra paths applied via git patches.
    #
    # This returns 2 values:
    #  patch level: 0-7, or -1 on error
    #  maketext status array msg: list
    sub test_patch {
        my ( $self, $patch, $dir, $min ) = @_;
        my $minlevel = defined $min ? $min : 0;
        my $maxlevel = 7;
        my $level;
        my $start = $self->cwd();

        if ($dir) {
            chdir $dir or return ( -1, q{Could not chdir into '[_1]': [_2]}, $dir, $! );
        }

        if ( !-e $patch ) {
            return ( -1, q{Patch missing; [_1]}, $patch );
        }

        # try each level until we find one that works
        for ( $level = $minlevel; $level <= $maxlevel; $level++ ) {
            my $test_rc = $self->run_system_cmd( [qq{patch -p$level -F99 -s --dry-run <$patch &>/dev/null}] );
            last if $test_rc;
        }

        chdir $start;

        return ( $level > 7 ? ( -1, q{Patch test '[_1]' failed}, $patch ) : ( $level, q{Patch tested at -p[_1]; [_2]}, $level, $patch ) );
    }

    sub apply_patch {
        my ( $self, $patch, $dir, $min ) = @_;

        # test patch
        my ( $p, @msg ) = $self->test_patch( $patch, $dir, $min );

        $self->print_verbose_aware( $self->maketext(@msg) );

        if ( $p < 0 ) {
            return ( 0, q{Patch test failed; '[_1]'}, $patch );
        }

        # apply patch
        my $start = $self->cwd();

        if ($dir) {
            chdir $dir or return ( 0, q{Could not chdir into '[_1]': [_2]}, $dir, $! );
        }

        $self->print_verbose_aware( $self->maketext( q{Applying patch; [_1]}, $patch ) );
        my $rc = $self->run_system_cmd( ["patch -p$p -F99 <$patch"] );
        @msg = $rc ? ( 1, 'Ok' ) : ( 0, q{Patch apply failed; [_1]}, $patch );

        if ($dir) {
            chdir $start or return ( 0, q{Could not chdir back into '[_1]': [_2]}, $start, $! );
        }

        return @msg;
    }

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

        my @range;
        for my $step_nm ( sort { $a <=> $b } keys %{ $args_hr->{'easyconfig_hr'}{'step'} } ) {
            if ( $args_hr->{'exclusive'} ) {
                push @range, $step_nm if $step_nm > $args_hr->{'start'} && $step_nm < $args_hr->{'stop'};
            }
            else {
                push @range, $step_nm if $step_nm >= $args_hr->{'start'} && $step_nm <= $args_hr->{'stop'};
            }
        }

        return @range;
    }

    sub cwd {
        return Cwd::cwd();
    }

    sub use_lib_cwd {
        my ($self) = @_;
        lib->import( $self->cwd() );
    }

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

        if (wantarray) {
            return ( 0, @maketext );
        }
        else {
            $self->log_warn( \@maketext );
            return;
        }
    }

    sub centos5_openssl_fixup {
        my $self = shift;
        if ( $self->{'getos'} eq 'centos' && $self->{'getos_releaseversion'} =~ /^5/ && !-e '/var/cpanel/version/centos5_openssl_fix' ) {
            $self->print_alert(qq{Performing one-time fixup for http://bugs.centos.org/view.php?id=4680});
            $self->run_system_cmd( [qw(rpm -e --justdb --nodeps openssl.i386)] );
            unless ( -d '/var/cpanel/version' ) {
                Cpanel::SafeDir::safemkdir('/var/cpanel/version');
            }
            Cpanel::FileUtils::touchfile('/var/cpanel/version/centos5_openssl_fix');
        }
    }

    # Returns the hex version number converted to decimal or undef
    sub get_openssl_version_number {
        my $self = shift;
        my $prefix = shift || $self->get_openssl_prefix( { 'skip_mach' => 1 } );

        my $version_number;
        foreach my $path (qw(/include/openssl/opensslv.h /include/opensslv.h /openssl/opensslv.h)) {
            next unless ( -e $prefix . $path );
            open my $ver_fh, '<', $prefix . $path || next;
            local $/ = undef;
            my $file_contents = readline($ver_fh);
            close $ver_fh;
            if ( $file_contents =~ m/^#\s*define\s+OPENSSL_VERSION_NUMBER\s+(0x[0-9a-fA-F]+)/m ) {
                $version_number = hex($1);
                last;
            }
        }

        return $version_number;
    }

    sub get_openssl_prefix {
        my ( $self, $args_ref ) = @_;
        $args_ref->{'skip_mach'} ||= 0;

        my $openssl_path =
            -d '/opt/openssl'                ? '/opt/openssl'
          : -d '/usr/local/include/openssl/' ? '/usr/local'
          : -d '/usr/include/openssl/'       ? '/usr'
          :                                    '';

        if ( $openssl_path eq '/usr' && ( -e '/bin/rpm' || -e '/usr/bin/rpm' ) ) {

            # TODO: change this legacy statement to not do backticks
            if ( `rpm -V openssl-devel 2>/dev/null | grep -v '^\\.\\.\\.\\.\\.\\.\\.T'` ne '' ) {    # probaby don't need "ne ''"...
                $self->print_alert( q{Warning: '[_1]' has been modified, reinstalling...}, 'openssl-devel' );
                $self->run_system_cmd( [qw(rpm -e --nodeps openssl-devel)] );
                $self->run_system_cmd( [qw(/usr/local/cpanel/scripts/ensurerpm openssl-devel)] );
                $self->print_alert( q{Done reinstalling '[_1]'}, 'openssl-devel' );
            }
        }

        # need to comment why this is needed if it still is or remove it
        if ( $openssl_path ne '/opt/openssl' && !$args_ref->{'skip_mach'} && $self->{'POSIX::uname'}->[4] =~ m{x86_64} ) {
            $openssl_path = 'SYSTEM';
        }

        # PHP may look for lib64
        if ( $openssl_path eq '/opt/openssl' && -e '/opt/openssl/lib' && !-e '/opt/openssl/lib64' ) {
            $self->run_system_cmd( [qw(ln -sf /opt/openssl/lib /opt/openssl/lib64)] );
        }

        return $openssl_path;
    }

    # There's a few different variants of RedHat systems, so this returns a boolean
    # value if it's one of them
    sub is_redhat {
        my $self = shift;
        my $os;

        if ( ref($self) eq '' ) {

            # Due to a bug, going to use Cpanel code to get around the fact
            # that Cpanel::Easy::Utils::NameSpace::get_std_variation_of_ns_list is
            # not passing an object to versions().  Instead, it's passing a scalar
            # string.
            $os = Cpanel::Sys::GetOS::getos();
        }
        else {
            # For routines that properly pass an object
            $os = $self->{'getos'};
        }

        ( defined $os && $os =~ /^(?:redhat|rhel|cloudlinux|centos)$/i ) ? 1 : 0;
    }

    sub openssl_bug_check {
        my ($self) = @_;
        return;    # disable it now that the problem is fixed (sort of)
        return 1 if $self->{'getos'} eq 'centos' && $self->{'getos_releaseversion'} =~ /^4/;
        return 1 if $self->{'getos'} eq 'redhat' && $self->{'getos_releaseversion'} =~ /^4/;    # rhel

        return;
    }

    sub merge_two_hr_right_prec {
        my ( $self, $hr_a, $hr_b ) = @_;
        require Cpanel::CPAN::Hash::Merge;

        # no clone support, implies Hash::Merge::set_clone_behavior( 0 );
        Cpanel::CPAN::Hash::Merge::set_behavior('RIGHT_PRECEDENT');
        return Cpanel::CPAN::Hash::Merge::merge( $hr_a, $hr_b );
    }

    sub merge_easyconfig_hrs_into_one {
        goto &merge_two_hr_right_prec;
    }

    sub debug {
        my ( $self, $debug_hr ) = @_;
        return if !$self->get_param('debug');

        my @caller = caller(1);
        if ( !$debug_hr->{'skip_caller_info'} ) {

            my $args =
              ref $debug_hr->{'args'} ne 'ARRAY'
              ? '"argument list" N/A for this debug()'
              : '( ' . join( ', ', @{ $debug_hr->{'args'} } ) . ' )';

            my @curval = $self->get_param('debug');
            $self->set_param( 'debug', 0 );
            $self->print_alert( q{DEBUG: Package '[_1]' calling '[_2]()' in file '[_3]' at line } . q{'[_4]' at mark '[_5]' with argument list of: [_6]}, @caller[ 0, 3, 1, 2 ], time(), "\n\t$args", );
            $self->set_param( 'debug', @curval );
        }

        if ( $debug_hr->{'message'} ) {
            my @alert = ref $debug_hr->{'message'} eq 'ARRAY' ? @{ $debug_hr->{'message'} } : ( $debug_hr->{'message'} );
            $self->print_alert(@alert);
        }

        if ( $debug_hr->{'dumpobj'} ) {
            my $safe_copy = $self->get_obj_hr_without_coderefs();

            sleep 1;    # to ensure time() has been used yet...
            my $dump_file = $self->{'log_file'} . "DEBUG.$caller[3]" . time();

            $self->serialize( $dump_file, $safe_copy, );

            $self->print_alert( q{Object state at this point dumped to '[_1]'}, $dump_file );
        }
    }

    sub get_obj_hr_without_coderefs {
        my ( $self, $hr ) = @_;
        return if ref $self ne 'HASH';

        my %copy = %{ ref $hr ? $hr : $self };    # yes deref it so that we don't change self

        # recursively remove CODE refs
      KEY:
        for my $key ( keys %copy ) {
            my $ref = ref $copy{$key};
            next if !$ref;
            if ( $ref eq 'CODE' ) {
                delete $copy{$key};
            }
            elsif ( $ref eq 'ARRAY' ) {
                my @new;
              ITEM:
                for my $item ( @{ $copy{$key} } ) {
                    next ITEM if ref $item eq 'CODE';
                    my $iref = ref $item;
                    if ( !$iref ) {
                        push @new, $iref;
                    }
                    elsif ( $iref eq 'ARRAY' ) {
                        my $new = $self->get_obj_hr_without_coderefs( { 'item' => $iref } );
                        push @new, $new->{'item'};
                    }
                    else {
                        push @new, $self->get_obj_hr_without_coderefs($item);
                    }
                }
                $copy{$key} = \@new;
            }
            else {
                $copy{$key} = $self->get_obj_hr_without_coderefs( $copy{$key} );
            }
        }

        return \%copy;
    }

    sub add_to_modify_later_queue {
        my ( $self, $ns, $easyconfig ) = @_;

        my @caller = caller(1);

        return ( 0, q{Invalid argument to '[_2]()' from package '[_1]' in file '[_3]' at line '[_4]'}, @caller[ 0, 3, 1, 2 ] ) if !$self->is_namespace($ns) || ref $easyconfig ne 'HASH';

        if ( exists $self->{'modify_later_easyconfig_hr'}{$ns} ) {
            if ( ref $self->{'modify_later_easyconfig_hr'}{$ns} eq 'HASH' ) {

                # TODO: warn when dryrun|step|??? entries already exist and will be overwritten

                $self->{'modify_later_easyconfig_hr'}{$ns} = $self->merge_easyconfig_hrs_into_one( $self->{'modify_later_easyconfig_hr'}{$ns}, $easyconfig );
            }
            else {
                $self->{'modify_later_easyconfig_hr'}{$ns} = $easyconfig;
            }
        }
        else {
            $self->{'modify_later_easyconfig_hr'}{$ns} = $easyconfig;
        }
        return ( 1, 'Ok' );
    }

    # takes care of dealing with skip and reverse notation
    # it essentially converts the 'ns' value to ON=1 or OFF=0
    # so we can compare apples to apples.  Remember to switch
    # this back if it is reversed
    sub get_ns_value {
        my $self    = shift;
        my $ns      = shift;
        my $ns_ref  = shift;
        my $profile = shift;
        my $value;

        if ($ns) {
            $value = $self->get_ns_value_from_profile( $ns, $profile ) ? 1 : 0;
            $value = 0 if $ns_ref->{'skip'};
        }
        else {
            $value = 0;
        }

        return $value;
    }

    # retrieves the value that an optmod implies ONLY FOR THE PURPOSES of
    # the implies one pass logic.
    sub get_imply_value {
        my $self = shift;
        my $ns   = shift;
        my $ref  = shift;
        my $value;

        if ( $ref && defined $ref->{'implies'} && defined $ref->{'implies'}->{$ns} ) {
            $value = $ref->{'implies'}->{$ns};
        }
        else {
            $value = 0;
        }

        return $value;
    }

    # Determines if an OptMod creates a loop
    sub is_imply_loop {
        my $self    = shift;
        my $a_ns    = shift;
        my $a_ref   = shift;
        my $c_ns    = shift;
        my $c_ref   = shift;
        my $cache   = shift;
        my $profile = shift;

        my $b_ns = $cache->{$c_ns} || '';
        my $b_ref = $self->get_easyconfig_hr_from_ns_variations($b_ns) if $b_ns;

        my $A = $self->get_ns_value( $a_ns, $a_ref, $profile );
        my $B = $self->get_ns_value( $b_ns, $b_ref, $profile );
        my $C = $self->get_ns_value( $c_ns, $c_ref, $profile );

        my $Ca = $self->get_imply_value( $c_ns, $a_ref );
        my $Cb = $self->get_imply_value( $c_ns, $b_ref );

        #print "a_ns:$a_ns, b_ns:$b_ns, c_ns:$c_ns, A:$A, B:$B. C:$C, Ca:$Ca, Cb:$Cb\n";

        return ( $A && $B && ( $Ca != $Cb ) );
    }

    # Determines what value an OptMod will become given one or more parent
    # imply relationships
    sub get_imply_result {
        my $self    = shift;
        my $a_ns    = shift;
        my $a_ref   = shift;
        my $c_ns    = shift;
        my $c_ref   = shift;
        my $cache   = shift;
        my $profile = shift;
        my $visible = shift;
        my $value;

        my $b_ns = $cache->{$c_ns} || '';
        my $b_ref = $self->get_easyconfig_hr_from_ns_variations($b_ns) if $b_ns;

        my $A = $self->get_ns_value( $a_ns, $a_ref, $profile );
        my $B = $self->get_ns_value( $b_ns, $b_ref, $profile );
        my $C = $self->get_ns_value( $c_ns, $c_ref, $profile );

        my $Ca = $self->get_imply_value( $c_ns, $a_ref );
        my $Cb = $self->get_imply_value( $c_ns, $b_ref );

        if ($A) {
            $value = $Ca;
        }
        else {
            if ($B) {
                $value = $Cb;
            }
            else {
                $value = $visible ? $C : 0;
            }
        }

        #print "a_ns:$a_ns, b_ns:$b_ns, c_ns:$c_ns, A:$A, B:$B. C:$C, Ca:$Ca, Cb:$Cb, Res:$value\n";

        return $value;
    }

    # Determines in what cirmcumstances an optmod is turned on or off, as
    # well as determining when two optmods disagree (e.g. a loop)
    sub apply_one_pass_implies_logic {
        my $self    = shift;
        my $profile = shift;
        my ( %changed, %loop );
        my %setting;

      NSI:
        for my $ns ( @{ $self->{'state'}{'order'} } ) {
            my $ns_ref = $self->get_easyconfig_hr_from_ns_variations($ns);

            if ( exists $ns_ref->{'implies'} ) {
              IMP:

                # examine each implies for changes
                for my $imp_ns ( sort keys %{ $ns_ref->{'implies'} } ) {
                    my $imp_ref = $self->get_easyconfig_hr_from_ns_variations($imp_ns);

                    # determine if this imply will cause a loop
                    if ( $self->is_imply_loop( $ns, $ns_ref, $imp_ns, $imp_ref, \%setting, $profile ) ) {
                        $loop{$ns}{$imp_ns} = $self->get_imply_value( $imp_ns, $ns_ref );
                        last NSI;
                    }

                    # determine the value of the implied optmod
                    my $visible = $imp_ref->{'display_hide'} ? 0 : 1;
                    my $new_value = $self->get_imply_result( $ns, $ns_ref, $imp_ns, $imp_ref, \%setting, $profile, $visible );
                    my $cur_value = $self->get_ns_value( $imp_ns, $imp_ref, $profile );

                    # ns wants to change the value of the implied optmod
                    if ( $cur_value != $new_value ) {
                        $changed{$imp_ns}{'changed_by'} = $ns;
                        $changed{$imp_ns}{'changed_to'} = $new_value;

                        $self->set_ns_value_in_profile( $imp_ns, $profile, $new_value );
                        $self->set_ns_value_in_profile( $imp_ns, $self->{'working_profile'}, $new_value );
                    }

                    $setting{$imp_ns} = $ns;
                }
            }
        }

        return ( \%changed, \%loop );
    }

    sub handle_one_pass_hashes {
        my ( $easy, $changed_hr, $loop_hr, $change_cr, $loop_fyi_cr, $loop_cr ) = @_;

        if ( keys %{$changed_hr} ) {

            # list changes
            for my $chg ( sort keys %{$changed_hr} ) {
                my $ec = $easy->get_easyconfig_hr_from_ns_variations($chg);
                my $ch = $easy->get_easyconfig_hr_from_ns_variations( $changed_hr->{$chg}{'changed_by'} );

                my $status = $changed_hr->{$chg}{'changed_to'} ? 'enabled' : 'disabled';
                if ( $ec->{'reverse'} ) {
                    $status = $status eq 'enabled' ? 'disabled' : 'enabled';    # they won't see anything in output about since it was 'enabled' by turning the opt mod to 0
                }

                # lh: q{'[_1]' was [truefalse,_2,enabled,disabled] by '[_3]'}
                $change_cr->(qq('$ec->{'name'}' was $status by '$ch->{'name'}'));
            }
        }

        if ( keys %{$loop_hr} ) {

            # show problems
            for my $lup ( sort keys %{$loop_hr} ) {
                my $ec = $easy->get_easyconfig_hr_from_ns_variations($lup);

                for my $con ( sort keys %{ $loop_hr->{$lup} } ) {
                    my $ch = $easy->get_easyconfig_hr_from_ns_variations($con);

                    my $changed_to = '';
                    if ( exists $changed_hr->{$con}{'changed_to'} ) {
                        $changed_to = "as '$changed_hr->{$con}{'changed_to'}' ";
                    }

                    my $changed_by = 'by another module';
                    if ( exists $changed_hr->{$con}{'changed_by'} ) {
                        my $al = $easy->get_easyconfig_hr_from_ns_variations( $changed_hr->{$con}{'changed_by'} );
                        $changed_by = "via '" . $al->{'name'} . "'";
                    }

                    $loop_fyi_cr->( qq(Could not 'imply' '$ch->{'name'}' value to '$loop_hr->{ $lup }{ $con }' for '$ec->{'name'}'.) . qq( It was already implied ${changed_to}${changed_by}.) );
                }
            }

            $loop_cr->("'implies' loop detected: could not save profile, stopping...!");
        }
    }

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

        # Example:
        # package Cpanel::Easy::ABC;
        #
        # sub get_extension_status_coderef {
        #     return sub {
        #         my ($self) = @_;
        #
        #         return if ref $self->{'_'}{'php_ini'}{'extension'} ne 'ARRAY';
        #         if ( grep / [/] abc [.] so \z/xms, @{ $self->{'_'}{'php_ini'}{'extension'} } ) {
        #            return 1;
        #         }
        #         return;
        #     }
        # }

        $self->set_phpini_object_key_from('/usr/local/lib/php.ini');

        for my $ns ( @{ $self->{'state_config'}{'include'} } ) {

            my $value = $self->get_ns_value_from_profile( $ns, $profile_hr ) || 0;

            # if it's on leave it on
            if ( !$value ) {
                eval qq{require $ns;};    # prolly not needed
                if ( $ns->can('get_extension_status_coderef') ) {
                    if ( $ns->get_extension_status_coderef->($self) ) {
                        $self->set_ns_value_in_profile( $ns, $profile_hr, 1 );
                        $self->set_param( $ns, 1 );
                    }
                }
            }
        }
    }

    sub create_note_and_verify_on_from_license_url {
        my ( $easy, $self_hr, $profile_hr ) = @_;
        return if $self_hr->{'license_url'} !~ m{^\S+\:\/\/};
        my $is_html = ref( $easy->{'ui_obj'} ) =~ 'HTML' ? 1 : 0;
        if ($is_html) {
            require Cpanel::Encoder;
            $self_hr->{'license_url'} = Cpanel::Encoder::safe_html_encode_str( $self_hr->{'license_url'} );
        }
        my $newline = $is_html ? '<br /><br />' : "\n\n";
        my $agree =
          $is_html
          ? "By choosing this option you are signifying agreement and compliance with the license at <a href=\"$self_hr->{'license_url'}\" target=\"_blank\">$self_hr->{'license_url'}</a>. If you do not agree to those terms you must not choose this option."
          : "By choosing this option you are signifying agreement and compliance with the license at $self_hr->{'license_url'}.  If you do not agree to those terms you must not choose this option.";

        $self_hr->{'note'}      = $self_hr->{'note'}      ? $self_hr->{'note'} . "$newline $agree"      : $agree;
        $self_hr->{'verify_on'} = $self_hr->{'verify_on'} ? $self_hr->{'verify_on'} . "$newline $agree" : $agree;
    }

    sub create_note_and_verify_on_from_implies {
        my ( $easy, $self_hr, $profile_hr ) = @_;

        my $is_html = ref( $easy->{'ui_obj'} ) =~ 'HTML' ? 1 : 0;
        my $note    = '';
        my $ons     = [];
        my $offs    = [];

        my $verify = '';

        for my $ns ( sort keys %{ $self_hr->{'implies'} } ) {
            my $en = $self_hr->{'implies'}{$ns} ? 1 : 0;
            my $hr = $easy->get_easyconfig_hr_from_ns_variations($ns);

            # don't warn the user with hidden optmods
            next if $hr->{'display_hide'};

            my $text = "  $hr->{'name'}\n";
            my $whm  = " &nbsp;  $hr->{'name'}<br />\n";
            if ( $hr->{'reverse'} ) {
                if ($en) {
                    push @{$offs}, $hr->{'name'};
                }
                else {
                    push @{$ons}, $hr->{'name'};
                }
            }
            else {
                if ($en) {
                    push @{$ons}, $hr->{'name'};
                }
                else {
                    push @{$offs}, $hr->{'name'};
                }
            }
        }

        # we have a chance to bail out due to hidden optmods.
        # this check ensures if these arrays are emptys (thus no visible implies),
        # then we don't present the user with an empty accept prompt.
        return ( 1, 'Ok' ) if $#$ons < 0 && $#$offs < 0;

        if ( @{$ons} ) {
            $note =
              $is_html
              ? qq{<p>Enables:<br />\n} . join( '', map { " \&nbsp; $_<br />\n" } @{$ons} ) . "</p>\n"
              : qq{Enables: } . join( ', ', @{$ons} );
            $verify =
              $is_html
              ? q{<br />Enables: } . join( ', ', @{$ons} ) . '.<br />'
              : qq{\n\nEnables:\n} . join( '', map { "  $_\n" } @{$ons} ) . "\n";
        }

        if ( @{$offs} ) {
            if ( @{$ons} ) {
                $note .= $is_html ? "\n" : ( length $note ? ', ' : '' );
                $verify .= $is_html ? '' : "\n";
            }

            $note .=
              $is_html
              ? qq{<p>Disables:<br />} . join( '', map { qq{ \&nbsp; <span class="errors">$_</span><br />} } @{$offs} ) . "</p>\n"
              : qq{Disables: } . join( ', ', @{$offs} );
            $verify .=
              $is_html
              ? q{<br />Disables: } . join( ', ', @{$offs} ) . '.<br />'
              : qq{Disables:\n} . join( '', map { "  $_\n" } @{$offs} ) . "\n";
        }

        my $are_you_sure =
          $self_hr->{'verify_show_mod_name'}
          ? 'Are you sure you want to install ' . $self_hr->{'name'} . '?'
          : 'Are you sure you want to do this?';

        my $new_note      = "This option will make the following changes to your profile prior to the build: $note";
        my $new_verify_on = "This option will make the following changes to your profile prior to the build: $verify $are_you_sure";

        $self_hr->{'note'}      = $self_hr->{'note'}      ? $self_hr->{'note'} . " $new_note"           : $new_note;
        $self_hr->{'verify_on'} = $self_hr->{'verify_on'} ? $self_hr->{'verify_on'} . " $new_verify_on" : $new_verify_on;
    }

    sub cleanup_log_dir {
        my ($easy) = @_;

        return if ( !-d '/usr/local/cpanel/logs/easy/apache/' );
        chmod 0700, '/usr/local/cpanel/logs/easy/apache/';

        opendir my $log_dh, '/usr/local/cpanel/logs/easy/apache/' || return;
        my @logfiles = readdir($log_dh);
        close $log_dh;
        foreach my $file (@logfiles) {
            next if $file =~ /^\./;
            $file = '/usr/local/cpanel/logs/easy/apache/' . $file;
            next if -l $file || !-f _;
            my $mode = ( stat($file) )[2] && 022;

            if ($mode) {
                chmod 0600, $file;
            }
        }
    }

    sub optlib_3109_check {
        my ($easy) = @_;
        my $flag_file = '/var/cpanel/easy/did_optlib_3109_check_2';

        if ( !-f $flag_file ) {
            for my $opt ( Cpanel::SafeDir::Read::read_dir("/opt") ) {
                next if !-d "/opt/$opt";

                # We can't `next if !-f "…/OptLib/$opt.pm";` because not every /opt/$opt is an OptLib/$opt.pm so lets just do (almost) all directories, if we don't use them no big deal
                next if $opt eq "rh" || $opt eq "cpanel";

                $easy->print_to_log_and_screen("Doing one time sum regen of /opt/$opt\n");
                $easy->set_path_md5("/opt/$opt");
            }
            Cpanel::FileUtils::touchfile($flag_file) or $easy->print_to_log_and_screen("Could not create “$flag_file”: $!\n");
        }
    }

    sub do_env_details {
        my ( $easy, $title, $logonly ) = @_;

        my $envout = "\nEnvironment State Snapshot ($title)\n\n";

        local $ENV{CMDLINE} = qq{$0 @ARGV};

        for my $key ( sort keys %ENV ) {
            if ( $key =~ /remote_password/i ) {
                delete $ENV{$key};
                next;
            }

            my $value = defined $ENV{$key} ? $ENV{$key} : 'undef';    # suppress warnings

            # if it was Data::Dumper-ish this will show us what it was executed with, probably very useful
            # next if $key eq 'Cpanel::Form::Param::last_new';
            $envout .= "[$key]\n\t$value\n\n";
        }

        $logonly ? $easy->print_to_log( '!!' . $envout . "!!\n" ) : $easy->print_alert($envout);

        if ( open my $env_fh, '>>', $easy->{'log_file'} . '.env' ) {
            chmod 0600, $easy->{'log_file'} . '.env';
            print {$env_fh} $envout;
            close $env_fh;
            push @{ $easy->{'attach_files_to_report'} }, $easy->{'log_file'} . '.env';
        }
    }

    sub archive_last_success_profile {
        my ($self) = @_;
        return if $self->get_param('makecpphp') || $self->{'_'}{'restore_backup'};
        my @copy = ( $self->{'profile_main'}, $self->{'profile_last_success'} );
        my ( $rc, @msg ) = $self->copy_file(@copy);
        $self->log_warn(@msg) if !$rc;
    }

    sub profile_manual_edit_fixup {
        my ( $self, $profile_hr, $force_working_profile ) = @_;

        for my $ns ( @{ $self->{'state'}{'order'} } ) {
            my $value = $self->get_ns_value_from_profile( $ns, $profile_hr ) || '';

            next if $value =~ m{ \A \d+ \z }xms || $value eq '';    # empty is false which is alright

            $self->debug( { 'message' => [ q{value before '[_1]'}, $value ] } );

            if ( $value =~ /(1|2)/ ) {
                $value = $1;
            }
            else {
                $value = 0;
            }

            $self->print_alert_color( 'blue', q{The profile item '[_1]' has invalid data. This is usually the result of a manually edited profile. EasyApache changed the value of '[_1]' to '[_2]' (typically off).}, $ns, $value );
            $self->debug( { 'message' => [ q{value after '[_1]'}, $value ] } );

            $self->set_ns_value_in_profile( $ns, $profile_hr, $value );
        }

        $self->include_with_versions_manual_edit_profile_fixup( $profile_hr, $force_working_profile );

        return $profile_hr;    # not used but it is there if we want it
    }

    sub include_with_versions_manual_edit_profile_fixup {
        my ( $self, $profile_hr, $force_working_profile ) = @_;

        for my $ns ( @{ $self->{'state_config'}{'include'} } ) {
            if ( $self->ns_is_include_that_has_versions($ns) ) {
                next if !$self->get_ns_value_from_profile( $ns, $profile_hr );
                my $versions_found = 0;
                my %cur_ver_lookup;
                for my $ver ( $ns->versions() ) {
                    $cur_ver_lookup{$ver}++;
                    $versions_found++ if $self->get_ns_value_from_profile( $ns . '::' . $ver, $profile_hr );
                }

                if ( !$versions_found ) {
                    my $latest = $ns->can('latest_version') ? $ns->latest_version($profile_hr) : ( $ns->versions() )[-1];
                    my $forced_value = 0;    # calculate the version to turn on based on $force_working_profile, if none do the newest

                    for my $v ( $ns->versions() ) {
                        if ( exists $force_working_profile->{ $ns . '::' . $v } ) {
                            $forced_value = $v if $force_working_profile->{ $ns . '::' . $v };
                        }
                    }

                    if ($forced_value) {
                        $latest = $forced_value;
                    }

                    $self->print_alert_color( 'blue', q{The profile item '[_1]' is set to an out-of-date or unavailable version. EasyApache set the version to the latest version, '[_2]', by default.}, $ns, $latest );
                    if ( defined $force_working_profile && ref $force_working_profile eq 'HASH' ) {
                        for my $v ( $ns->versions() ) {
                            delete $force_working_profile->{ $ns . '::' . $v };
                        }
                    }

                    $self->set_ns_value_in_profile( $ns . '::' . $latest, $profile_hr, 1 );
                }

                if ( $versions_found > 1 ) {
                    my $latest       = 0;
                    my $forced_value = 0;    # calculate the version to turn on based on $force_working_profile, if none do the newest

                    # set all to zero, tracking the latest one of the ones checked...
                    for my $v ( $ns->versions() ) {
                        $latest = $v;

                        if ( exists $force_working_profile->{ $ns . '::' . $latest } ) {
                            $forced_value = $latest if $force_working_profile->{ $ns . '::' . $latest };
                        }

                        $self->set_ns_value_in_profile( $ns . '::' . $latest, $profile_hr, 0 );
                    }

                    if ($forced_value) {
                        $latest = $forced_value;
                    }

                    # how could this happen? just in case:
                    if ( !$latest ) {
                        ($latest) = reverse( $ns->versions() );
                    }

                    $self->print_alert_color( 'blue', q{The profile item '[_1]' has multiple versions selected. This is usually the result of a manually edited profile. EasyApache set the default to the most recent version that you selected, '[_2]'.}, $ns, $latest );
                    $self->set_ns_value_in_profile( $ns . '::' . $latest, $profile_hr, 1 );
                }

                # remove any obsolete version PMs that were left stale from interrupted cpanelsync
                if ( my $chk = $ns->can('is_top_ns_version') ) {
                    for my $full_ns ( grep /^\Q$ns\E\:\:/, keys %{$profile_hr} ) {
                        my ($top_ns) = reverse( split( '::', $full_ns ) );
                        if ( $chk->( $self, $top_ns ) && !exists $cur_ver_lookup{$top_ns} ) {
                            my $ns_pm = $self->get_file_from_namespace( $full_ns, 1 );
                            delete $profile_hr->{$full_ns} if !-e $self->{'opt_mod_custom_dir'} . '/' . $ns_pm;    # remove it from profile unless its a custom opt modm leaving it on
                            my $abs_path = '/var/cpanel/perl/easy/' . $ns_pm;
                            if ( -e $abs_path ) {
                                unlink $abs_path or $self->print_alert_color( 'blue', q{Could not unlink '[_1]': [_2]}, $abs_path, $! );
                            }

                            # ? do same thing with get_dir_from_namespace ?
                        }
                    }
                }
            }
        }

        return $profile_hr;    # not used but it is there if we want it
    }

    sub raise_fd_setsize_in_system_includes {
        my $self = shift;
        return if ( -e '/var/cpanel/disable_patchfdsetsize' );
        if ( -x '/usr/local/cpanel/scripts/patchfdsetsize' ) {
            $self->run_system_cmd( ['/usr/local/cpanel/scripts/patchfdsetsize'] );
        }
        else {
            $self->run_system_cmd( ['/usr/local/cpanel/scripts/patchtypes'] )      if ( -x '/usr/local/cpanel/scripts/patchtypes' );
            $self->run_system_cmd( ['/usr/local/cpanel/scripts/patchposixtypes'] ) if ( -x '/usr/local/cpanel/scripts/patchposixtypes' );
            $self->run_system_cmd( ['/usr/local/cpanel/scripts/patchtypesizes'] )  if ( -x '/usr/local/cpanel/scripts/patchtypesizes' );
        }
    }

    sub get_cpu_info {    # essentially borrowed from Cpanel::Status
        my $self = shift;

        if ( !$self->{'cache'}{'cpu_info'} ) {
            my ( $numcpus, $cpuname );

            my $file = '/proc/cpuinfo';
            if ( -e $file ) {
                if ( open my $fh, $file ) {
                    while (<$fh>) {
                        $numcpus = 1 + $1 if /^processor\s*:\s*(\d+)/;
                    }
                    close $fh;
                }
            }

            if ( !$numcpus ) {
                $numcpus = qx( /usr/local/cpanel/bin/ncpus );
                chomp $numcpus;
            }

            $self->{'cache'}{'cpu_info'} = { numcpus => $numcpus, cpuname => $cpuname };
        }

        return wantarray
          ? @{ $self->{'cache'}{'cpu_info'} }{qw/numcpus cpuname/}
          : $self->{'cache'}{'cpu_info'}->{'numcpus'};
    }

    sub insert_make_options {
        my ( $self, $aref ) = @_;
        warn $self->maketext( "The “[_1]” method is deprecated, please use “[_2]” instead.", 'insert_make_options()', 'get_make_options()' );
        my $numcpus = $self->get_cpu_info();
        splice @{$aref}, 1, 0, '-j2'    # make -j2 is for multi-core (i.e., multiple CPU) machines
          if 'make' eq $aref->[0] && $numcpus > 1;
    }

    # Allow customer to add makefile flags using the rawopts mechanism (e.g. -j2)
    sub get_make_options {
        my $self    = shift;
        my @opts    = @{ $self->get_raw_opts_as_array('make') };
        my $numcpus = $self->get_cpu_info();
        push @opts, '-j2' if ( !@opts && $numcpus > 1 );    # backwards-compatibility for people not specifying -j2
        return \@opts;
    }

    sub get_easources_host {
        my $self = shift;

        # First look for EA-specific, else fall back on the generic httpupdate host
        for my $name (qw[ EASOURCES HTTPUPDATE ]) {
            my $host = $self->{'_'}{'cpsources'}{$name};
            return $host if $host;
        }

        # We'll never arrive here;  Cpanel::Config::Sources always sets a suitable default
    }
}

1;

__END__
