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

package autofixer2::security_upgrade_banner;

#                                      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 File::Path ();

my $SCRIPT    = 'security_upgrade_banner';
my $TAG_BEGIN = '<!-- BEGIN AUTOFIXER:CPANEL-52935 critical-upgrade-banner -->';
my $TAG_END   = '<!-- END AUTOFIXER:CPANEL-52935 critical-upgrade-banner -->';

our $dir = '/var/cpanel/customizations/whm/includes';
our $file = 'global_banner.html.tt';

exit run() unless caller;

# CPANEL-52935
#
# Append a TT/JS banner snippet to the WHM customizations include
# /var/cpanel/customizations/whm/includes/global_banner.html.tt so that root
# admins running an unpatched build see a "CRITICAL SECURITY UPDATE" banner on
# every WHM page until they upgrade.  The version comparison runs in the TT
# snippet at template-render time, so the banner self-hides the moment cP is
# at or above the fixed build for that major.  A leika kill-switch
# (security.disableUpgradeBanner) lets ops silence it.
sub run {
    return 0 unless supported_on_this_major( 32, 136 );

    File::Path::make_path( $dir, { mode => 0755 } ) if ( !-d $dir );

    my $path    = "$dir/$file";
    my $snippet = _snippet();

    # Read existing content (empty string if file does not exist yet).
    my $content = '';
    if ( -e $path ) {
        open( my $rfh, '<', $path ) or die "Can't read '$path': $!";
        local $/;
        $content = <$rfh>;
        close $rfh;
    }

    # For in-place substitution, use only the tag-bounded portion of the
    # snippet (TAG_BEGIN through TAG_END) so we don't accumulate extra
    # whitespace on every run.
    my ($tag_bounded) = $snippet =~ /(\Q$TAG_BEGIN\E.*\Q$TAG_END\E)/s;

    # Try an in-place substitution between the sentinel tags.
    my $replaced = ( $content =~ s/\Q$TAG_BEGIN\E.*?\Q$TAG_END\E/$tag_bounded/s );

    if ( !$replaced ) {
        # Tags not found -- first install or mangled file; append.
        $content .= $snippet;
    }

    open( my $wfh, '>', $path ) or die "Can't write '$path': $!";
    print $wfh $content;
    close $wfh;

    return 0;
}

sub _snippet {
    return <<'SNIPPET';

<!-- BEGIN AUTOFIXER:CPANEL-52935 critical-upgrade-banner -->
[%- USE Whostmgr; -%]
[%- # Note: no leika kill-switch -- the plugin is missing on older cP majors
    # and TT raises a non-catchable plugin error during USE on those boxes.
-%]
[%- IF Whostmgr.hasroot();
        # NB: TT2 silently drops assignments to leading-underscore variables, so
        # we use the cp52935_ prefix instead. Also, .split() chained directly off
        # a plugin method does not resolve as a scalar vmethod -- store first.
        cp52935_ver     = Whostmgr.WHM_VERSION;
        cp52935_parts   = cp52935_ver.split('\.');
        cp52935_major   = cp52935_parts.0;
        cp52935_wp2     = cp52935_parts.1;
        cp52935_build   = cp52935_parts.2;
        cp52935_fixed   = { '86' => 41, '110' => 97, '118' => 63, '126' => 54, '130' => 19, '132' => 29, '134' => 20, '136' => 5 };
        IF cp52935_wp2;
            cp52935_fixed.134 = 7;
        END;
        # Map each major to the tier name that should go into CPANEL= in
        # cpupdate.conf.  Using a 4-part build pins the server forever;
        # tier names let it track future updates.
        cp52935_tiers   = { '86' => '11.86', '110' => '11.110', '118' => '11.118', '126' => '11.126', '130' => '11.130', '132' => '11.132', '134' => 'release', '136' => 'current' };
        cp52935_needs   = 0;
        cp52935_unsup   = 0;
        IF cp52935_fixed.exists(cp52935_major);
            IF cp52935_build < cp52935_fixed.$cp52935_major; cp52935_needs = 1; END;
        ELSE;
            cp52935_unsup = 1;
        END;
        # Pick the nearest higher supported major (no downgrades).
        # Use tier names so the server is not permanently pinned.
        cp52935_target = '';
        IF cp52935_unsup;
            IF    cp52935_major < 86;  cp52935_target = '11.86';
            ELSIF cp52935_major < 110; cp52935_target = '11.110';
            ELSIF cp52935_major < 118; cp52935_target = '11.118';
            ELSIF cp52935_major < 126; cp52935_target = '11.126';
            ELSIF cp52935_major < 130; cp52935_target = '11.130';
            ELSIF cp52935_major < 132; cp52935_target = '11.132';
            ELSIF cp52935_major < 134; cp52935_target = 'release';
            ELSIF cp52935_major < 136; cp52935_target = 'current';
            ELSE; cp52935_unsup = 0; END;  # major > 136 -- nothing to do
        ELSIF cp52935_needs;
            cp52935_target = cp52935_tiers.$cp52935_major;
        END;
-%]
[%- IF cp52935_needs || cp52935_unsup -%]
<style>
  .cpanel52935-banner { background:#b30000; color:#fff; padding:14px 22px; font-weight:700;
    font-size:15px; box-shadow:0 2px 6px rgba(0,0,0,0.25); margin:0 0 16px 0; border-radius:3px;
    display:flex; align-items:center; justify-content:space-between; gap:18px; }
  .cpanel52935-banner a.cpanel52935-btn { background:#fff; color:#b30000; padding:8px 14px;
    border-radius:3px; text-decoration:none; font-weight:700; white-space:nowrap; }
  .cpanel52935-banner a.cpanel52935-btn:hover { background:#ffe5e5; }
  .cpanel52935-banner span a { color:#fff; text-decoration:underline; }
  .cpanel52935-banner span a:hover { color:#ffe5e5; }
</style>
<template id="cpanel52935-banner-tpl"><div class="cpanel52935-banner" role="alert">
  <span>
    [% locale.maketext("There is a critical security update to cPanel & WHM.") %]
    [% locale.maketext("We strongly recommend that you update to the [output,url,_1,latest supported version,target,_blank] immediately.", "https://docs.cpanel.net/knowledge-base/cpanel-product/product-versions-and-the-release-process/#releases") %]
    [% locale.maketext("If you have specified a custom version in your update settings, this will automatically reconfigure those settings.") %]
  </span>
  <a class="cpanel52935-btn" href="#" data-target-version="[% cp52935_target | html %]">[% locale.maketext("Update now") %]</a>
</div></template>
<script>
(function(){
  function inject(){
    // Don't nag the admin while they're literally watching the upgrade run.
    if (window.location.pathname.indexOf('/scripts/upcp2') !== -1) return;
    var tpl = document.getElementById('cpanel52935-banner-tpl');
    var contentContainer = document.querySelector('#contentContainer');
    if (!tpl || !contentContainer || !contentContainer.parentNode) return;
    var node = tpl.content.firstElementChild.cloneNode(true);
    contentContainer.parentNode.insertBefore(node, contentContainer);
    var token = '[% cp_security_token %]';
    node.querySelector('a.cpanel52935-btn').addEventListener('click', function(ev){
      ev.preventDefault();
      var target = this.getAttribute('data-target-version');
      // Pin the configured tier to the fixed build, then start a forced upcp.
      fetch(token + '/json-api/update_updateconf?api.version=1&CPANEL=' + encodeURIComponent(target),
            { credentials:'same-origin' })
        .then(function(){ window.location.href = token + '/scripts/upcp2?force=1'; })
        .catch(function(){ window.location.href = token + '/scripts2/upcpform'; });
    });
  }
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', inject);
  } else { inject(); }
})();
</script>
[%- END -%]
[%- END -%]
<!-- END AUTOFIXER:CPANEL-52935 critical-upgrade-banner -->


SNIPPET
}

# ---- Standard autofixer helpers (from template_autofixer) ----

sub supported_on_this_major {
    my ( $min, $max ) = @_;

    my $major = get_major_version();
    return 0 if !$major;
    return 0 if $major < $min || $major > $max;
    return 1;
}

sub get_major_version {
    open( my $fh, '<', '/usr/local/cpanel/version' ) or return 0;
    my $version = readline($fh);
    close $fh;
    chomp $version if $version;

    my ($major) = $version =~ m/^11\.(\d+)\./a;
    return $major || 0;
}

1;
