#!/usr/local/cpanel/3rdparty/bin/perl
package autofixer2::insecure_cpanel_version_icontact_notification;

#                                      Copyright 2026 WebPros International, LLC
#                                                           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;

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

use Template;
use Template::Plugin;
use Template::Plugins;

use Cpanel::Debug    ();
use Cpanel::Notify   ();
use Cpanel::LoadFile ();
use Cpanel::FileType ();

my $cached_version;
my $notification_state_file = '/var/cpanel/autofixer2_insecure_version_notification.last_sent';

exit run() if !caller();

{

    package Cpanel::iContact::Class::autofixer2::insecure_cpanel_version_icontact_notification;

    use parent qw{Cpanel::iContact::Class};

    sub _get_subject {
        return 'Your server is running an insecure and out of date version of cPanel';
    }

    sub _get_parsed_template {
        my ($self) = @_;
        my $tmpl = <<"HERE";
[%
PROCESS "include/colors.tmpl";
PROCESS "include/styles.tmpl";
PROCESS "include/code_block.tmpl";

WRAPPER 'wrapper/main.html.tmpl';
%]
<h5>Dear WHM administrator,</h5>
<p>There is a critical security update to cPanel & WHM. We strongly recommend that you update to the <a href="https://go.cpanel.net/cpreleases">latest supported version</a> immediately.</p>
<p>Best regards,</p>
<i>The cPanel Team</i>
[% END %]
HERE

        my @html_related;
        $self->{'_html_related'} = \@html_related;
        my $template_root = '/usr/local/cpanel/etc/icontact_templates/';
        my $out           = '';
        my $obj           = Template->new(
            ( map { $_ => 0 } qw{ABSOLUTE PRE_CHOMP} ),
            ( map { $_ => 1 } qw{RELATIVE TRIM POST_CHOMP} ),
            INCLUDE_PATH => $template_root,
            LOAD_PLUGINS => [
                Template::Plugins->new( { PLUGINS => { CPANEL => 'Cpanel::iContact::Class::Plugin::CPANEL' } } ),
            ]
        );

        # Set up the CPANEL plugin with html_related tracking
        $obj->context()->stash()->set(
            CPANEL => $obj->context()->plugin(
                'CPANEL',
                [
                    template_root   => $template_root,
                    locale_obj      => $self->_find_or_return_locale_handle_from_input( $self->{'_locale'} ),
                    html_related    => \@html_related,
                    icontact_object => $self,
                ]
            ),
        );

        my $ok = $obj->process(
            \$tmpl,
            {
                NOTICE => {
                    notification_cannot_be_disabled => 0,
                },
            },
            \$out,
        );
        die "Template failed to process: " . $obj->error() if !$ok;

        # Post-process html_related entries to load content and detect MIME types
        for my $html_rel (@html_related) {
            my $full_path = "$template_root/$html_rel->{'path'}";
            $html_rel->{'content'} = Cpanel::LoadFile::load_r($full_path);

            if ( !$html_rel->{'content_type'} ) {
                $html_rel->{'content_type'} = Cpanel::FileType::determine_mime_type($full_path) || 'application/octet-stream';
            }
        }

        return $out;
    }
    sub _should_not_be_disabled { return 0 }
    1;
}

sub run {
    return 0 if -e '/var/cpanel/insecure_cpanel_version_notification.disable';
    return 0 unless supported_on_this_major( 32, 136 );
    return 0 unless is_insecure_version();
    return 0 unless should_send_notification_today();

    my $class = __PACKAGE__;

    # Fool the notifier into not loading what's already in memory.
    $INC{'Cpanel/iContact/Class/autofixer2/insecure_cpanel_version_icontact_notification.pm'} = 1;
    $INC{'Cpanel/iContact/Class/Plugin/CPANEL.pm'}                                            = 1;

    my $notify = Cpanel::Notify::notification_class(
        'constructor_args' => [],
        map { $_ => $class } qw{class application},
    );
    waitpid( $notify->{'_icontact_pid'}, 0 ) if $notify->{_icontact_pid};

    update_notification_timestamp();
    return 0;
}

sub should_send_notification_today {
    return 1 unless -e $notification_state_file;

    my $mtime = ( stat($notification_state_file) )[9];
    return 1 unless defined $mtime;

    my $time_since_last_sent = time() - $mtime;

    # 84600 seconds = 23.5 hours. We use a slight buffer instead of 24 hours
    # to ensure that a cron firing right at the 24 hour mark will send.
    return $time_since_last_sent >= 84600;
}

sub update_notification_timestamp {
    open( my $fh, '>', $notification_state_file ) or return;
    print {$fh} time() . "\n";
    close($fh);
    return 1;
}

sub _read_cpanel_version {
    return $cached_version if defined $cached_version;

    open( my $fh, '<', '/usr/local/cpanel/version' ) or do {
        $cached_version = '';
        return '';
    };
    my $version = <$fh>;
    close($fh);

    if ( defined $version && length $version ) {
        chomp $version;
        $cached_version = $version;
    }
    else {
        $cached_version = '';
    }

    return $cached_version;
}

# Do we run this code?
sub supported_on_this_major {
    my ( $min_ver, $max_ver ) = @_;
    my $major = get_major_version();
    return $major >= $min_ver && $major <= $max_ver;
}

sub is_insecure_version {
    my $version = get_full_version();
    return 0 unless $version;

    my ($major) = $version =~ /^[0-9]+\.([0-9]+)/;
    return 1 if $major < 86;

    my %fixed_versions = (
        86  => '11.86.0.41',
        110 => '11.110.0.97',
        118 => '11.118.0.63',
        126 => '11.126.0.54',
        130 => '11.130.0.19',
        132 => '11.132.0.29',
        134 => '11.134.0.20',
        136 => is_wp2() ? '11.136.1.7' : '11.136.0.5',
    );

    my @insecure_ranges = (
        { min => 88,  max => 109 },
        { min => 112, max => 117 },
        { min => 120, max => 125 },
    );

    return 1 if _is_in_insecure_range( $major, \@insecure_ranges );
    return _is_below_fixed_version( $version, $major, \%fixed_versions );
}

sub _is_in_insecure_range {
    my ( $major, $ranges ) = @_;

    for my $range (@$ranges) {
        return 1 if $major >= $range->{min} && $major <= $range->{max};
    }
    return 0;
}

sub _is_below_fixed_version {
    my ( $version, $major, $fixed_versions ) = @_;

    return 0 unless exists $fixed_versions->{$major};
    return version_compare( $version, $fixed_versions->{$major} ) < 0;
}

sub is_wp2 {
    my $type = readlink("/usr/local/cpanel/server.type") // "";
    return $type eq "wp2";
}

sub version_compare {
    my ( $ver1, $ver2 ) = @_;

    my @v1 = split( /\./, $ver1 );
    my @v2 = split( /\./, $ver2 );

    for ( my $i = 0; $i < 4; $i++ ) {
        my $n1 = $v1[$i] || 0;
        my $n2 = $v2[$i] || 0;
        return -1 if $n1 < $n2;
        return 1  if $n1 > $n2;
    }
    return 0;
}

sub get_full_version {
    return _read_cpanel_version();
}

sub get_major_version {
    my $version = _read_cpanel_version();
    return 30 unless $version;

    my ($major) = $version =~ /^[0-9]+\.([0-9]+)/;
    return $major || 30;
}

1;
