#!/usr/local/cpanel/3rdparty/bin/perl

#                                      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.

package autofixer2::cpanel_csf_install;

use cPstrict;

use Cpanel::Binaries         ();
use Cpanel::Debug            ();
use Cpanel::Notify           ();
use Cpanel::OS               ();
use Cpanel::Plugins          ();
use Cpanel::Version          ();
use Cpanel::Version::Compare ();

use Try::Tiny;

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

our $PLUGIN_NAME = 'cpanel-csf';

our $CSF_PATH         = '/etc/csf';
our $CSF_VERSION_FILE = 'version.txt';
our $CSF_CONFIG_FILE  = 'csf.conf';

sub CSF_VERSION_PATH { return "$CSF_PATH/$CSF_VERSION_FILE" }
sub CSF_CONFIG_PATH  { return "$CSF_PATH/$CSF_CONFIG_FILE" }

my $MINIMUM_CPANEL_VERSION = '11.109.0.0';    # essentially 110, including devel builds

my $MINIMUM_CSF_VERSION = '14.00';
my $MAXIMUM_CSF_VERSION = '14.24';            # last release available through official automatic updates
my $GPL3_CSF_VERSION    = '15.00';            # GPLv3 release; explicitly checked for and allowed

*DIE   = \&Cpanel::Debug::log_die;
*ERROR = \&Cpanel::Debug::log_error;
*WARN  = \&Cpanel::Debug::log_warn_no_backtrace;
*INFO  = \&Cpanel::Debug::log_info;

exit run() unless caller;

sub run {
    my $force = $ENV{CSF_FORCE} ? 1 : 0;

    try {
        my $go = pre_update($force);
        if ($go) {
            do_update();
            do_icontact();
        }
    }
    catch {
        my $ex = $_;
        DIE($ex);
    };

    return 0;
}

sub pre_update {
    my $force = shift // 0;
    my $go    = 1;

    # If the plugin package is already present, there is no need to run this autofixer:
    if ( Cpanel::Plugins::is_plugin_installed($PLUGIN_NAME) ) {
        INFO("cPanel CSF is already installed; there is nothing to be done.");
        return 0;
    }

    # If this isn't a systemd system (i.e., CentOS 6), don't try to upgrade:
    if ( !Cpanel::OS::is_systemd() ) {
        INFO("cPanel CSF will not work properly on a system which does not use systemd. The upgrade will not be performed.");
        return 0;
    }

    # Collect all data we'll need:
    my $csf_version       = get_version( "CSF/LFD", \&csf_version );
    my $autoupdate_status = get_csf_config_option("AUTO_UPDATES");

    # Exit now if CSF doesn't appear to be installed, unless force requested:
    my $is_csf_installed = $csf_version || defined $autoupdate_status ? 1 : 0;
    if ( !$is_csf_installed ) {
        INFO("CSF/LFD does not appear to be installed.");
        return 0 unless $force;
        INFO("Forcing an install of CSF/LFD by request.");
    }

    # Do this second so that the no-CSF/no-force case is handled cleanly, even on ancient systems:
    my $cp_version = get_version( "cPanel", eval { \&Cpanel::Version::get_version_full } );

    # Blow up if we cannot determine cPanel version:
    if ( !$cp_version ) {
        die "autofixer cpanel_csf_install cannot determine the current cPanel version; cPanel will not attempt to update CSF/LFD unless this can be resolved.";
    }

    my @problems;

    # Only work on cPanel 110 and beyond, unless forced:
    if ( Cpanel::Version::Compare::compare( $cp_version, '<', $MINIMUM_CPANEL_VERSION ) ) {
        push @problems, {
            summary  => "cPanel version is too old",
            solution => "Ensure that cPanel has been updated to at least version 110, which is the oldest supported version of cPanel at the time of release of cPanel's CSF/LFD."
        };
    }

    if ($is_csf_installed) {
        if ( !$csf_version ) {
            push @problems, "CSF/LFD version cannot be determined";    # There isn't really a good solution if CSF is installed, but it no longer knows what version it is.
        }
        elsif ( Cpanel::Version::Compare::compare( $csf_version, '<', $MINIMUM_CSF_VERSION ) ) {
            push @problems, "CSF/LFD version is too old";              # Since there isn't an official source outside of the GPLv3 and the Internet Archive copies, there isn't a good solution for this either.
        }
        elsif ( Cpanel::Version::Compare::compare( $csf_version, '>', $MAXIMUM_CSF_VERSION ) && $csf_version ne $GPL3_CSF_VERSION ) {
            push @problems, {
                summary  => "CSF/LFD looks different from versions formerly published and supported by Way to the Web LTD",
                solution => "If you are certain that CSF/LFD does not differ significantly from the original software, and you want cPanel to assume control of CSF/LFD updates, use the override as described below.",
            };
        }

        my $CSF_CONFIG_PATH = CSF_CONFIG_PATH();
        if ( !defined $autoupdate_status ) {
            push @problems, { summary => "CSF/LFD auto-update status cannot be determined", solution => "Ensure that $CSF_CONFIG_PATH exists and can be read by your system without issue." };
        }
        elsif ( !$autoupdate_status ) {
            push @problems, { summary => "CSF/LFD auto-updates are not enabled", solution => <<EOS };
In $CSF_CONFIG_PATH, find the "AUTO_UPDATES" configuration option and enable it, or add the following line to that file if "AUTO_UPDATES" is not present:

        AUTO_UPDATES = "1"
EOS
        }
    }

    if ( scalar @problems > 0 ) {
        my $msg = "The system identified the following issues regarding upgrade to a cPanel-maintained version of CSF/LFD:\n\n";
        for my $problem (@problems) {
            $problem = { summary => $problem } unless $problem && ref $problem;

            if ( ref $problem ne 'HASH' ) {
                die "BUG: Couldn't convert a problem entry into a hashref!";
            }

            $msg .= "* Problem: $problem->{summary}\n";
            $msg .= "  Solution: $problem->{solution}\n" if !$force && $problem->{solution};    # no solutions if force
            $msg .= "\n";
        }

        my $log_cr;
        if ($force) {
            $log_cr = \&WARN;
            $msg .= "You instructed cPanel to ignore these potential issues and to try to take over management of CSF/LFD updates.";
        }
        else {
            $go     = 0;
            $log_cr = \&ERROR;
            $msg .= <<EOS;
If no solution is given for an issue, or if the solution does not resolve the
issue, consider either contacting cPanel Technical Support for assistance or,
AT YOUR OWN RISK, forcing this script to go through with the update, which can
be done by running the following command at the console as the root user:

        CSF_FORCE=1 /usr/local/cpanel/scripts/autorepair cpanel_csf_install
EOS
        }

        $log_cr->($msg);
    }

    return $go;
}

sub do_update {

    # The logic of taking over from an existing CSF now lives in the cpanel-csf package itself.
    Cpanel::Plugins::install_plugins($PLUGIN_NAME);

    INFO("CSF has been updated to use cPanel as a source of upgrades.");

    return;
}

sub do_icontact {

    # Fool the notifier into not loading what's already in memory.
    $INC{'Cpanel/iContact/Class/autofixer2/cpanel_csf_install.pm'} = 1;    ## no critic qw(RequireLocalizedPunctuationVars)
    $INC{'Cpanel/iContact/Class/Plugin/CPANEL.pm'}                 = 1;    ## no critic qw(RequireLocalizedPunctuationVars)

    my $notify = Cpanel::Notify::notification_class(
        constructor_args => [],
        class            => __PACKAGE__,
        application      => __PACKAGE__,
    );

    waitpid( $notify->{'_icontact_pid'}, 0 ) if $notify->{_icontact_pid};

    return;
}

sub get_version {
    my ( $software, $version_cr ) = @_;
    die "BUG: get_version() received bag args" unless $software && !ref $software && defined $version_cr && ref $version_cr eq 'CODE';

    my $version = try {
        my $result = $version_cr->();
        INFO("$software version: $result") if defined $result;
        $result;
    };
    return $version;
}

sub csf_version {
    my $filename = shift || CSF_VERSION_PATH();

    open( my $fh, '<', $filename ) or return;
    my $version = <$fh>;
    chomp $version;
    close $fh;

    return $version;
}

# Modified version of the code from the analytics change, which in turn parses like CSF itself:
sub get_csf_config_option {

    my ( $option, $path ) = @_;

    die "BUG: no option name given!" if !$option;
    $path ||= CSF_CONFIG_PATH();

    open my $fh, '<', $path or return;
    while ( my $line = <$fh> ) {
        next if $line =~ m/^\s*(?:#|$)/;    # Skip blank and comment lines
        next if $line !~ m/=/;              # Skip lines that don't look valid
        $line =~ s/^\s*//;
        $line =~ s/\s*$//;
        my ( $key, $value ) = split /\s*=\s*/, $line, 2;

        # Keep going if we haven't found what we want:
        next if $key ne $option;

        # Otherwise we found our option, so we're done:
        close $fh;

        $value =~ s/^\"(.*)\"$/$1/;
        return $value;
    }
    close $fh;

    # We didn't find anything, so return an empty string so that we're false but defined:
    return '';
}

{

    package Cpanel::iContact::Class::autofixer2::cpanel_csf_install;

    use Cpanel::FileType      ();
    use Cpanel::LoadFile      ();
    use Cpanel::Sys::Hostname ();

    use Template          ();
    use Template::Plugin  ();
    use Template::Plugins ();

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

    sub _get_subject {
        return 'Important update regarding ConfigServer Security & Firewall (CSF)';
    }

    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>Following our earlier announcement, the ConfigServer Security & Firewall (CSF) configuration on “[% NOTICE.hostname %]” has been modified to receive updates from the CSF fork maintained by cPanel. We have taken over security maintenance of this plugin to ensure that your servers continue receiving critical security and stability fixes. No further action is required by you at this time, and your firewall rules and settings remain unchanged.</p>

<p>If you wish to change CSF on your system to a different provider of the software, now that this upgrade has occurred, you will need to uninstall cPanel's instance of CSF before switching. For more information about the process of uninstalling cPanel's CSF, <a href="https://go.cpanel.net/csf">see our documentation on this topic</a>.</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 => 1,
                    hostname                        => Cpanel::Sys::Hostname::gethostname(),
                },
            },
            \$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 1 }

    1;
}

1;
