#!/usr/local/cpanel/3rdparty/bin/perl
#                                      Copyright 2025 WebPros International, LLC
#                                                           All rights reserved.
# copyright@cpanel.net                                         http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited.

package autofixer2::update_outdated_packages;

use strict;
use warnings;

BEGIN { unshift @INC, '/usr/local/cpanel'; }

use Cpanel::OS                        ();
use Cpanel::SafeRun::Simple           ();

# Minimum required versions for telemetry to work correctly
my %MINIMUM_VERSIONS = (
    'ea-nginx'                      => '1.26.3-6',
    'ea-cpanel-tools'               => '1.0-105',
    'cpanel-analytics'              => '1.4.55',
    'ea-apache24-config-runtime'     => '1.0-196',
);

exit run() unless caller;

sub run {
    return 0 unless supported_on_this_major( 110, 132 );    # Set this to the Min/Max we support this autofixer on.

    if ( Cpanel::OS::is_rpm_based() ) {
        my $rpm_fix_result = system('/usr/local/cpanel/scripts/find_and_fix_rpm_issues');
        if ($rpm_fix_result != 0) {
            warn "find_and_fix_rpm_issues returned non-zero exit code, but continuing\n";
        }
    } else {
        print "Skipping RPM database check on non-RPM based system\n";
    }

    my @packages_to_update = _get_packages_needing_update();
    
    if (@packages_to_update) {
        print "Found packages needing update: " . join(', ', @packages_to_update) . "\n";
        return _update_packages(@packages_to_update);
    } else {
        print "All required packages are already at minimum versions or not installed\n";
        return 0;
    }
}

sub _get_packages_needing_update {
    my @packages_to_update;

    foreach my $package (sort keys %MINIMUM_VERSIONS) {
        my $required_version = $MINIMUM_VERSIONS{$package};
        
        my $installed_version = _get_installed_package_version($package);
        if (!$installed_version) {
            if ($package ne 'ea-nginx') {
                print "Package $package is not installed, installing now\n";
                _install_package($package);
                $installed_version = _get_installed_package_version($package);
            } else {
                next;
            }
        }

        # Could use Cpanel::Version::Compare::Package but might not be available on servers with older cPanel versions (used to be Cpanel::Version::Compare::RPM, for example)
        if (version_string_cmp($installed_version, $required_version) < 0) {
            push @packages_to_update, $package;
        }
    }

    return @packages_to_update;
}

sub _get_installed_package_version {
    my ($package) = @_;
    
    my $query_output;
    
    if ( Cpanel::OS::is_rpm_based() ) {
        $query_output = Cpanel::SafeRun::Simple::saferun('rpm', '-q', '--queryformat', '%{VERSION}-%{RELEASE}', $package);
        return unless $query_output && $query_output !~ /not installed/;
    } else {
        $query_output = Cpanel::SafeRun::Simple::saferun('dpkg-query', '-W', '--showformat=${Version}', $package);
        return unless $query_output && $? == 0;
    }

    return $query_output;
}

sub _update_packages {
    my (@packages) = @_;
    
    my $package_manager = _get_package_manager();
    my $ea_nginx_was_updated = 0;

    if ($package_manager eq 'apt') {
        print "Updating package lists\n";
        Cpanel::SafeRun::Simple::saferunallerrors('apt', 'update');
    } elsif ($package_manager eq 'dnf') {
        print "Cleaning and updating DNF metadata\n";
        Cpanel::SafeRun::Simple::saferunallerrors('dnf', 'clean', 'all');
        Cpanel::SafeRun::Simple::saferunallerrors('dnf', 'makecache');
    } elsif ($package_manager eq 'yum') {
        print "Cleaning and updating YUM metadata\n";
        Cpanel::SafeRun::Simple::saferunallerrors('yum', 'clean', 'all');
        Cpanel::SafeRun::Simple::saferunallerrors('yum', 'makecache');
    }
    
    foreach my $package (@packages) {
        print "Updating package: $package\n";
        
        my @cmd_args = _get_command($package_manager, 'update', $package);
        
        print "Running: " . join(' ', @cmd_args) . "\n";
        my $output = Cpanel::SafeRun::Simple::saferunallerrors(@cmd_args);
        
        if ($? != 0) {
            warn "Failed to update $package. Output: $output\n";
        } else {
            print "Successfully updated $package\n";
            if ($package eq 'ea-nginx') {
                $ea_nginx_was_updated = 1;
            }
        }
    }

    # If ea-nginx was updated, also update ea-apache24 to latest to avoid 421 errors (See: CPANEL-49019)
    if ($ea_nginx_was_updated) {
        print "ea-nginx was updated, also updating ea-apache24 and apache modules to latest version\n";
        my @cmd_args = _get_command($package_manager, 'update', 'ea-apache24*');
        print "Running: " . join(' ', @cmd_args) . "\n";
        my $output = Cpanel::SafeRun::Simple::saferunallerrors(@cmd_args);

        if ($? != 0) {
            warn "Failed to update ea-apache24. Output: $output\n";
        } else {
            print "Successfully updated ea-apache24\n";
        }
    }
    
    print "Verifying package updates\n";
    my @still_outdated = _get_packages_needing_update();
    
    if (@still_outdated) {
        warn "Some packages are still outdated after update attempt: " . join(', ', @still_outdated) . "\n";
        return 1;  # Indicate partial failure
    } else {
        print "All required packages are now at minimum versions\n";
        return 0;
    }
}

sub _install_package {
    my ($package) = @_;
    
    my $package_manager = _get_package_manager();
    my @cmd_args = _get_command($package_manager, 'install', $package);
    
    print "Installing package: $package\n";
    print "Running: " . join(' ', @cmd_args) . "\n";
    my $output = Cpanel::SafeRun::Simple::saferunallerrors(@cmd_args);
    
    if ($? != 0) {
        warn "Failed to install $package. Output: $output\n";
    } else {
        print "Successfully installed $package\n";
    }
}

sub _get_package_manager {
    # Could use a Cpanel library but might not be available on servers with older cPanel versions
    if ( Cpanel::OS::is_rpm_based() ) {
        if (-x '/usr/bin/dnf') {
            return 'dnf';
        } else {
            return 'yum';
        }
    } else {
        return 'apt';
    }
}

sub _get_command {
    my ($package_manager, $action, $package) = @_;
    
    if ($package_manager eq 'dnf') {
        return ('dnf', $action, '-y', '--disableexcludes=all', $package);
    } elsif ($package_manager eq 'yum') {
        return ('yum', $action, '-y', '--disableexcludes=all', $package);
    } elsif ($package_manager eq 'apt') {
        if ($action eq 'update') {
            return ('apt', 'install', '--only-upgrade', '-y', $package);
        } elsif ($action eq 'install') {
            return ('apt', 'install', '-y', $package);
        }
    }
}

# Direct copy from Cpanel::Version::Compare::Package module
sub version_string_cmp {
    my ($ver1, $ver2) = @_;

    # Split the version string into its constituent parts (Epoch, Version, &
    # Release) and then compare each in turn.
    my ( $e1, $v1, $r1 ) = $ver1 =~ m/(?:(\d+):)?([^:-]+)(?:-([^:-]+))?/;
    my ( $e2, $v2, $r2 ) = $ver2 =~ m/(?:(\d+):)?([^:-]+)(?:-([^:-]+))?/;

    # First compare epochs, which must be an integer.  They are 0 if not
    # specified.
    my $cmp = ( $e1 || 0 ) <=> ( $e2 || 0 );
    return $cmp if $cmp;

    # Compare the version and then the release fields using the RPM sorting
    # algorithm.
    return version_cmp( $v1, $v2 ) || version_cmp( $r1 || '', $r2 || '' );
}

# This algorithm is taken from http://stackoverflow.com/a/3206477 and is a direct copy from the Cpanel::Version::Compare::Package module
sub version_cmp {
    my ($ver1, $ver2) = @_;

    # Split into alpha xor numeric groups, which define the individual sections
    # that will be compared.  Discard all other characters.
    my @v1 = ( $ver1 =~ m/([a-zA-Z]+|[0-9]+)/g );
    my @v2 = ( $ver2 =~ m/([a-zA-Z]+|[0-9]+)/g );

    # Compare each section in succession until the values no longer match.
    while ( @v1 && @v2 ) {
        my $s1 = shift @v1;
        my $s2 = shift @v2;

        # Numeric sections are compared numerically (<=>); alphabetic sections
        # are compared lexicographically (cmp).  If one of the versions has a
        # numeric section and the other alphabetic, the numeric version is
        # newer.
        if ( $s1 =~ m/\d/ ) {
            return 1 if $s2 =~ m/\D/;    # Handle numeric/alphabetic mismatch.
            my $cmp = $s1 <=> $s2;
            return $cmp if $cmp;
        }
        else {
            return -1 if $s2 =~ m/\d/;    # Handle numeric/alphabetic mismatch.
            my $cmp = $s1 cmp $s2;
            return $cmp if $cmp;
        }
    }

    # If one of the versions runs out of parts, the one with more parts is
    # considered newer.
    return @v1 <=> @v2;
}

# Do we run this code?
sub supported_on_this_major {
    my ( $min_ver, $max_ver ) = @_;

    my $major = get_major_version();

    return 0 if $major < $min_ver;
    return 0 if $major > $max_ver;

    return 1;
}

sub get_major_version {
    my $major_version;

    if ( open( my $fh, '<', '/usr/local/cpanel/version' ) ) {
        my $full_version = <$fh>;
        close($fh);
        if ( length $full_version ) {
            chomp $full_version;
            ($major_version) = $full_version =~ /^[0-9]+\.([0-9]+)/;
        }
    }

    # Safe default.
    return $major_version || 30;
}