GIF89a=( õ' 7IAXKgNgYvYx\%wh…hŽth%ˆs%—x¨}9®Œ©€&©‰%¶†(¹–.¹5·œD¹&Çš)ÇŸ5ǘ;Í£*È¡&Õ²)ׯ7×µ<Ñ»4ï°3ø‘HÖ§KͯT÷¨Yÿšqÿ»qÿÔFØ !ù ' !ÿ NETSCAPE2.0 , =( þÀ“pH,È¤rÉl:ŸÐ¨tJ­Z¯Ø¬vËíz¿à°xL.›Ïè´zÍn»ßð¸|N¯Ûïø¼~Ïïûÿ€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ¡¢£¤¥¦§gª«ªE¯°¨¬ª±²Œ¹º¹E¾­”´ÂB¶¯ §Åȸ»ÑD¾¿Á•ÄÅ®° ÝH¾ÒLÀÆDÙ«D¶BÝïðÀ¾DÑÑÔTÌÍíH òGö¨A RÎڐ |¥ ٭&ºìE8œ¹kGÔAÞpx­a¶­ã R2XB®åE8I€Õ6Xî:vT)äžþÀq¦è³¥ì仕F~%xñ  4#ZÔ‰O|-4Bs‘X:= QÉ œš lºÒyXJŠGȦ|s hÏíK–3l7·B|¥$'7Jީܪ‰‡àá”Dæn=Pƒ ¤Òëí‰`䌨ljóá¯Éüv>á–Á¼5 ½.69ûϸd«­ºÀûnlv©‹ªîf{¬ÜãPbŸ  l5‘ޝpß ´ ˜3aÅùäI«O’ý·‘áÞ‡˜¾Æ‚ÙÏiÇÿ‹Àƒ #öó)pâš Þ½ ‘Ý{ó)vmÞü%D~ 6f s}ŃƒDØW Eþ`‡þ À…L8xá†ç˜{)x`X/> Ì}mø‚–RØ‘*|`D=‚Ø_ ^ð5 !_…'aä“OÚ—7âcð`D”Cx`ÝÂ¥ä‹éY¹—F¼¤¥Š?¡Õ™ n@`} lď’ÄÉ@4>ñd œ à‘vÒxNÃ×™@žd=ˆgsžG±æ ´²æud &p8Qñ)ˆ«lXD©øÜéAžHìySun jª×k*D¤LH] †¦§C™Jä–´Xb~ʪwStŽ6K,°£qÁœ:9ت:¨þªl¨@¡`‚ûÚ ».Û¬¯t‹ÆSÉ[:°=Š‹„‘Nåû”Ìî{¿ÂA ‡Rà›ÀÙ6úë°Ÿð0Ä_ ½;ÃϱîÉì^ÇÛÇ#Ëë¼ôº!±Ä˜íUîÅÇ;0L1óÁµö«p% AÀºU̬ݵ¼á%霼€‡¯Á~`ÏG¯»À× ­²± =4ªnpð3¾¤³¯­ü¾¦îuÙuµÙ®|%2ÊIÿür¦#0·ÔJ``8È@S@5ê¢ ö×Þ^`8EÜ]ý.뜃Âç 7 ú ȉÞj œ½Dç zý¸iþœÑÙûÄë!ˆÞÀl§Ïw‹*DçI€nEX¯¬¼ &A¬Go¼QföõFç°¯;é¦÷îŽêJ°îúôF5¡ÌQ|îúöXªæ»TÁÏyñêï]ê² o óÎC=öõ›ÒÓPB@ D×½œä(>èCÂxŽ`±«Ÿ–JЀ»Û á¤±p+eE0`ëŽ`A Ú/NE€Ø†À9‚@¤à H½7”à‡%B‰`Àl*ƒó‘–‡8 2ñ%¸ —€:Ù1Á‰E¸àux%nP1ð!‘ðC)¾P81lÑɸF#ˆ€{´âé°ÈB„0>±û °b¡Š´±O‚3È–Ù()yRpbµ¨E.Z‘D8ÊH@% òŒx+%Ù˜Æcü »¸˜fõ¬b·d`Fê™8èXH"ÉÈ-±|1Ô6iI, 2““¬$+](A*jÐ QTÂo‰.ÛU슬Œã„Ž`¯SN¡–¶Äåyše¯ª’­¬‚´b¦Éož œ)åyâ@Ì®3 ÎtT̉°&Ø+žLÀf"Ø-|žçÔ>‡Ðv¦Ðžì\‚ Q1)Ž@Žh#aP72”ˆ™¨$‚ !ù " , =( …7IAXG]KgNgYvYxR"k\%w]'}hŽth%ˆg+ˆs%—r.—m3šx3˜x¨}9®€&©€+¨‡7§‰%¶†(¹–.¹œD¹&ǘ;Í•&ײ)×»4ïÌ6ò§KÍ þ@‘pH,È¤rÉl:ŸÐ¨tJ­Z¯Ø¬vËíz¿à°xL.›Ïè´zÍn»ßð¸|N¯Ûïø¼~Ïïûÿ€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ¡¢£¤¥¦§g «¬ E ±± ¨­¶°ººE Á´”·®C¬²§Ç¶Œ»ÓDÃÕƷ¯Ê±H½ºM×ÁGÚ¬D¶BËÁ½î½DÓôTÏÛßîG»ôõC×CÌ l&âž:'òtU³6ɹ#·Ø)€'Ü.6±&ëÍÈ» K(8p0N?!æ2"ÛˆNIJX>R¼ÐO‚M '¡¨2¸*Ÿþ>#n↠å@‚<[:¡Iïf’ ¤TÚ˘CdbÜÙ“[«ŽEú5MBo¤×@€`@„€Êt W-3 ¶Ÿ¡BíêäjIÝ…Eò9[T…$íêﯧ„…•s»Óȳ¹€ÅÚdc®UUρ#±Ùïldj?´í¼²`\ŽÁðÞu|3'ÖŒ]ë6 ¶S#²‡˜FKLÈ *N E´‘áäŠ$˜›eÄYD„ºq«.è촁ƒs \-ÔjA 9²õ÷å- üúM[Âx(ís÷ì®x€|í¡Ù’p¦‚ ŽkÛTÇDpE@WÜ ²Ç]kŠ1¨ þ€·Yb ÓÁ‰l°*n0 ç™—žzBdОu¾7ĉBl€â‰-ºx~|UåU‰  h*Hœ|e"#"?vpÄiŠe6^ˆ„+qâŠm8 #VÇá ‘å–ÄV„œ|Аè•m"сœn|@›U¶ÆÎž—Špb¥G¨ED”€±Úê2FÌIç? >Éxå Œ± ¡¤„%‘žjŸ‘ꄯ<Ìaà9ijÐ2˜D¦È&›†Z`‚å]wþ¼Â:ç6àB¤7eFJ|õÒ§Õ,¨äàFÇ®cS·Ê¶+B°,‘Þ˜ºNûãØ>PADÌHD¹æž«ÄÀnÌ¥}­#Ë’ë QÀÉSÌÂÇ2ÌXÀ{æk²lQÁ2«ÊðÀ¯w|2Í h‹ÄÂG€,m¾¶ë3ÐÙ6-´ÅE¬L°ÆIij*K½ÀÇqï`DwVÍQXœÚÔpeœ±¬Ñ q˜§Tœ½µƒ°Œìu Â<¶aØ*At¯lmEØ ü ôÛN[P1ÔÛ¦­±$ÜÆ@`ùåDpy¶yXvCAyåB`ŽD¶ 0QwG#¯ æš[^Äþ $ÀÓÝǦ{„L™[±úKÄgÌ;ï£S~¹ìGX.ôgoT.»åˆ°ùŸûù¡?1zö¦Ÿž:ÅgÁ|ìL¹ „®£œŠ‚à0œ]PÁ^p F<"•ç?!,ñ‡N4—…PÄ Á„ö¨Û:Tè@hÀ‹%táÿ:ø-žI<`þ‹p I….)^ 40D#p@ƒj4–؀:²‰1Øâr˜¼F2oW¼#Z†;$Q q” ‘ ÂK¦ñNl#29 !’F@¥Bh·ᏀL!—XFóLH‘Kh¤.«hE&JòG¨¥<™WN!€ÑÙÚˆY„@†>Œž19J" 2,/ &.GXB%ÌRÈ9B6¹W]’î×ÔW¥’IÎ$ ñ‹ÓŒE8YÆ ¼³™ñA5“à®Q.aŸB€&Ø©³ JÁ—! ¦t)K%tœ-¦JF bòNMxLôþ)ÐR¸Ð™‘ èÝ6‘O!THÌ„HÛ ‰ !ù ) , =( …AXKgNgYvYxR"k\%wh…hŽh%ˆg+ˆs%—r.—x3˜x¨}9®€&©€+¨Œ,©‡7§‰%¶†(¹–.¹5·&Çš)ǘ;Í•&×£*Ȳ)ׯ7×»4ï°3øÌ6ò‘HÖ§KÍ»Hó¯T÷¨Yÿ»qÿÇhÿ þÀ”pH,È¤rÉl:ŸÐ¨tJ­Z¯Ø¬vËíz¿à°xL.›Ïè´zÍn»ßð¸|N¯Ûïø¼~Ïïûÿ€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ¡¢£¤¥¦§g ª« E$±²¨ª­ · °²½$E$ÂÕ««D· Í ¿¦Ç¶¸ÌŒ¾³CÃÅÆ E ééH½MÛÂGâªD­ çBêêϾD²ÒaÀà€Š1r­ðÓ¤ ÔožzU!L˜C'¾yW½UGtäÇïÙllê0×àÂuGþ)AÀs[þ·xì ÁxO%ƒûX2ó—  P£n›R/¡ÑšHše+êDm?# —‘Ç£6¡8íJ¡ŸâDiäªM¥Ö„ôj“¬¹£5oQ7°- <‡ *´lãÓŒ2r/a!l)dÈ A™ÈE¢ôÔ͆…ð ;Ö˜c ¡%ß‚’Ùˆâ¸b½—pe~C"BíëÚHïeF2§æŠ8qb t_`urŠeü wÅu3êæPv§h•"ß`íÍxçLĹÜÖ3á  ~Öº“®›¸ÏMDfJÙ °„ÛµáWõ%§œ‚à©–‚X ÓØ)@®Ñ›Eþ´wëuÅSxb8y\mÖzœ¥§ZbºE—ÂLªÌw!y(>¡™wú=Ç|ÅÝs¢d €CÁW)HÜcC$€L Ä7„r.á\{)@ð` @ äXÈ$PD” `šaG:§æˆOˆ72EÐamn]ù"ŒcÊxÑŒ° &dR8`g«iÙŸLR!¦P …d’ä¡“¦ðÎTƒ¦ià|À _ ¥ Qi#¦Šg›Æ ›noMµ ›V ã£)p ç£ÎW…š=Âeªk§†j„ ´®1ß²sÉxéW«jšl|0¯B0Û, \jÛ´›6±¬¶C ÛíWþï|ëÙ‹¸ñzĸV {ì;Ýñn¼òVˆm³I¼³.Ðã¤PN¥ ²µ¼„µCã+¹ÍByî£Ñ¾HŸ›ëê 7ìYÆFTk¨SaoaY$Dµœìï¿Ã29RÈkt Çïfñ ÇÒ:ÀÐSp¹3ÇI¨â¥DZÄ ü9Ïýögñ½­uÔ*3)O‘˜Ö[_hv ,àî×Et Ÿé¶BH€ Õ[ü±64M@ÔSÌM7dÐl5-ÄÙU܍´©zߌ3Ô€3ž„ „ ¶ÛPô½5×g› êÚ˜kN„Ý…0Îj4€Ìë°“#{þÕ3S2çKÜ'ợlø¼Ú2K{° {Û¶?žm𸧠ËI¼nEò='êüóºè^üæÃ_Û=°óž‚ì#Oý¿Í'¡½áo..ÏYìnüñCœO±Áa¿¢Kô½o,üÄËbö²çºíï{ËC Ú— "”Ï{ËK ÍÒw„õ±Oz dÕ¨à:$ ƒô—«v»] A#ð «€¿šéz)Rx׿ˆ¥‚d``èw-îyÏf×K!ð€þ­Ð|ìPľ„=Ì`ý(f” 'Pa ¥ÐBJa%Ðâf§„%Š¡}FàáÝ×6>ÉäŠG"éŽè=ø!oа^FP¼Ø©Q„ÀCÙÁ`(Ž\ÄÝ® ©Â$<n@dÄ E#ììUÒI! ‚#lù‹`k¦ÐÇ'Rró’ZýNBÈMF Í[¤+‹ðɈ-áwj¨¥þ8¾rá ,VÂh„"|½œ=×G_¦Ñ™EØ 0i*%̲˜Æda0mV‚k¾)›;„&6 p>ÓjK “¦Ç# âDÂ:ûc?:R Ó¬fÞéI-Ì“•Ã<ä=™Ï7˜3œ¨˜c2ŒW ,ˆ”8(T™P‰F¡Jhç"‚ ; 403WebShell
403Webshell
Server IP : 192.254.185.125  /  Your IP : 216.73.216.196
Web Server : Apache
System : Linux gator3151.hostgator.com 5.14.0-687.42.1.el9_8.x86_64 #1 SMP PREEMPT_DYNAMIC Thu Aug 27 11:55:12 EDT 2026 x86_64
User : diablo ( 32101)
PHP Version : 7.4.33
Disable Function : NONE
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : OFF  |  Sudo : ON  |  Pkexec : ON
Directory :  /scripts/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /scripts//log_retention
#!/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 scripts::log_retention;

use cPstrict;

use Getopt::Long ();
use Try::Tiny;
use File::Find;
use File::Path qw(remove_tree);
use File::Basename;
use Time::HiRes;

use Cpanel::AccessIds::ReducedPrivileges ();
use Cpanel::Config::LoadCpConf           ();
use Cpanel::Config::Users                ();
use Cpanel::Hooks                        ();
use Cpanel::Logger                       ();
use Cpanel::LogManager                   ();
use Cpanel::SafeRun::Object              ();
use Cpanel::YAML                         ();

use constant {
    PLUGIN_DIR => '/var/cpanel/log_retention/plugins',
};

# Built-in log types
my %BUILTIN_LOG_TYPES = (
    web => {
        get_default_retention => \&_get_web_default_retention,
        get_user_retention    => \&_get_web_user_retention,
        sys_paths             => \&_get_web_sys_paths,
        user_paths            => \&_get_web_user_paths,
        description           => 'Web server logs (Apache/NGINX access and error logs)',
    },
);

# Combined log types (built-in + plugins)
my %LOG_TYPES;

sub script {
    my (@args) = @_;

    local $| = 1;

    # Load built-in types first, then plugins
    %LOG_TYPES = %BUILTIN_LOG_TYPES;
    _load_plugin_log_types();

    my %opts = (
        help    => 0,
        run     => 0,
        type    => [],
        verbose => 0,
    );

    my $getopt_result = Getopt::Long::GetOptionsFromArray(
        \@args,
        'help|h'    => \$opts{help},
        'run'       => \$opts{run},
        'type=s@'   => $opts{type},
        'verbose|v' => \$opts{verbose},
    );

    if ( !$getopt_result || $opts{help} ) {
        _usage();
        return 0;
    }

    my $logger = Cpanel::Logger->new();

    if ( $opts{run} ) {
        return _run_retention_cleanup( $opts{type}, $opts{verbose}, $logger );
    }
    else {
        return _show_log_types( $opts{type}, $opts{verbose}, $logger );
    }
}

sub _usage {
    print <<"EOF";
Usage: $0 [options]

Log retention management utility for cPanel & WHM

Options:
    --help, -h          Show this help message
    --run               Execute log retention cleanup
    --type TYPE         Process specific log type(s) only (can be repeated)
    --verbose, -v       Enable verbose output

Examples:
    $0                          # Show all log types and their settings
    $0 --run                    # Process all log types
    $0 --run --type web         # Process only web logs
    $0 --run --type web --type custom  # Process web and custom log types
    $0 --verbose               # Show detailed information

Built-in log types:
EOF

    for my $type ( sort keys %BUILTIN_LOG_TYPES ) {
        printf "    %-10s %s\n", $type, $BUILTIN_LOG_TYPES{$type}{description};
    }

    # Show plugin log types if any
    my @plugin_types = grep { !exists $BUILTIN_LOG_TYPES{$_} } keys %LOG_TYPES;
    if (@plugin_types) {
        print "\nPlugin log types:\n";
        for my $type ( sort @plugin_types ) {
            printf "    %-10s %s\n", $type, $LOG_TYPES{$type}{description};
        }
    }

    print "\nPlugin directory: " . PLUGIN_DIR . "\n";
    print "\n";
    return;
}

sub _show_log_types {
    my ( $requested_types, $verbose, $logger ) = @_;

    my @types_to_show = @{$requested_types} ? @{$requested_types} : sort keys %LOG_TYPES;

    print "Log Retention Configuration:\n";
    print "=" x 50 . "\n";

    for my $type (@types_to_show) {
        if ( !exists $LOG_TYPES{$type} ) {
            $logger->warn("Unknown log type: $type");
            next;
        }

        my $config            = $LOG_TYPES{$type};
        my $default_retention = $config->{get_default_retention}->();

        print "\nType: $type\n";
        print "Description: $config->{description}\n";
        print "Configured system-wide retention: ";
        if ( $default_retention == 0 ) {
            print "Never delete\n";
        }
        else {
            print "$default_retention days\n";
        }

        if ($verbose) {
            print "System paths:\n";
            my @sys_paths = $config->{sys_paths}->();
            for my $path (@sys_paths) {
                print "  - $path\n";
            }

            print "User retention setting: ~<user>/.cpanel-logs (web-log-retention-days)\n";
        }
    }

    print "\n";
    return 0;
}

sub _run_retention_cleanup {
    my ( $requested_types, $verbose, $logger ) = @_;

    my @types_to_process = @{$requested_types} ? @{$requested_types} : sort keys %LOG_TYPES;

    $logger->info("Starting log retention cleanup process");

    for my $type (@types_to_process) {
        if ( !exists $LOG_TYPES{$type} ) {
            $logger->warn("Unknown log type: $type - skipping");
            next;
        }

        $logger->info("Processing log type: $type");

        my $start_time = Time::HiRes::time();

        try {
            _process_log_type( $type, $verbose, $logger );
        }
        catch {
            $logger->error("Failed to process log type '$type': $_");
        };

        my $duration = sprintf( "%.2f", Time::HiRes::time() - $start_time );
        $logger->info("Completed processing '$type' in ${duration}s");
    }

    $logger->info("Log retention cleanup process completed");
    return 0;
}

sub _process_log_type {
    my ( $type, $verbose, $logger ) = @_;

    my $config            = $LOG_TYPES{$type};
    my $default_retention = $config->{get_default_retention}->();

    my $files_processed = 0;
    my $process_success = 1;

    # Check if running as non-root user
    my $is_root      = ( $> == 0 );
    my $running_user = $is_root ? undef : getpwuid($>);

    eval {
        # Process system paths
        # Skip if default retention is 0 = never delete
        # Also skip system paths if running as non-root user
        if ( !$is_root ) {
            $logger->info("Running as non-root user - skipping system paths for type: $type") if $verbose;
        }
        elsif ( $default_retention == 0 ) {
            $logger->info("System retention set to 'never delete' - skipping system paths for type: $type") if $verbose;
        }
        else {
            $logger->info("Processing system logs for type: $type");
            my @sys_paths = $config->{sys_paths}->();
            for my $path (@sys_paths) {
                $files_processed += _cleanup_logs_in_path(
                    {
                        base_path      => $path,
                        retention_days => $default_retention,
                        log_type       => $type,
                        user           => undef,
                        verbose        => $verbose,
                        logger         => $logger,
                    }
                );
            }
        }

        # Process user paths
        # If running as non-root, only process the current user's logs
        $logger->info("Processing user logs for type: $type");
        my @users = $is_root ? Cpanel::Config::Users::getcpusers() : ($running_user);

        for my $user (@users) {

            my $user_retention = $config->{get_user_retention}->($user) // $default_retention;

            if ( $user_retention == 0 ) {
                $logger->info("User '$user' has retention set to 'never delete' - skipping") if $verbose;
                next;
            }

            my @user_paths = $config->{user_paths}->($user);
            for my $path (@user_paths) {
                $files_processed += _cleanup_logs_in_path(
                    {
                        base_path      => $path,
                        retention_days => $user_retention,
                        log_type       => $type,
                        user           => $user,
                        verbose        => $verbose,
                        logger         => $logger,
                    }
                );
            }
        }

        1;
    } or do {
        $process_success = 0;
        $logger->error("Processing failed: $@");
    };

    $logger->info( "Processed type '$type': $files_processed files deleted, status: " . ( $process_success ? 'success' : 'failed' ) );

    return;
}

sub _find_user_log_files ($cutoff_time) {
    my $archives = Cpanel::LogManager::list_logs();

    my @files;
    for my $archive ( @{$archives} ) {
        if ( $archive->{mtime} && $archive->{mtime} < $cutoff_time ) {
            push @files, $archive->{path};
        }
    }

    return @files;
}

sub _find_system_log_files ( $base_path, $cutoff_time ) {
    my @files;

    my $wanted = sub {
        return unless -f $_;
        return if -l $_;                     # Skip symlinks to prevent symlink attack vulnerabilities
        return if $_ eq '.' or $_ eq '..';

        # Skip current/active log files
        return if $_ =~ /\.log$/ && $_ !~ /\.(gz|bz2|\d+)$/;

        # Look for rotated logs: .log.1, .log.gz, .log.20241201, etc.
        return unless $_ =~ /\.log\.(?:\d+(?:\.gz|\.bz2)?|gz|bz2|\d{8}(?:\.gz|\.bz2)?)$/;

        my $mtime = ( stat($_) )[9];
        if ( $mtime && $mtime < $cutoff_time ) {
            push @files, $File::Find::name;
        }
    };

    find( { wanted => $wanted, no_chdir => 1 }, $base_path );

    return @files;
}

sub _cleanup_logs_in_path {
    my ($args)         = @_;
    my $base_path      = $args->{base_path};
    my $retention_days = $args->{retention_days};
    my $log_type       = $args->{log_type};
    my $user           = $args->{user};
    my $verbose        = $args->{verbose};
    my $logger         = $args->{logger};

    return 0 unless -d $base_path;

    my $cutoff_time = time() - ( $retention_days * 24 * 60 * 60 );
    my @files_to_delete;

    # Define the file finding operation
    my $find_files_coderef = sub {
        @files_to_delete =
          $user
          ? _find_user_log_files($cutoff_time)
          : _find_system_log_files( $base_path, $cutoff_time );
        return;
    };

    # Find rotated log files - drop privileges for user paths to prevent
    # symlink attacks and unauthorized access (TOCTOU mitigation)
    if ( $user && $> == 0 && $user ne 'root' ) {
        Cpanel::AccessIds::ReducedPrivileges::call_as_user( $find_files_coderef, $user );
    }
    else {
        $find_files_coderef->();
    }

    if ( !@files_to_delete ) {
        return 0;
    }

    $logger->info(
        sprintf(
            "Found %d log files to delete in %s (retention: %d days%s)",
            scalar(@files_to_delete),
            $base_path,
            $retention_days,
            $user ? " for user: $user" : ""
        )
    ) if $verbose || @files_to_delete > 10;

    # Execute pre-deletion hook
    try {
        Cpanel::Hooks::hook(
            {
                category => 'Log::Retention',
                event    => 'pre_deletion',
                stage    => 'pre',
            },
            {
                log_type        => $log_type,
                user            => $user,
                base_path       => $base_path,
                files_to_delete => \@files_to_delete,
                retention_days  => $retention_days,
            }
        );
    }
    catch {
        $logger->warn("Pre-deletion hook failed: $_");
    };

    # Define the deletion operation
    my $deleted_count        = 0;
    my $delete_files_coderef = sub {
        for my $file (@files_to_delete) {
            try {
                unlink($file) or die "Failed to delete $file: $!";
                $deleted_count++;
                $logger->info("Deleted: $file") if $verbose;
            }
            catch {
                $logger->warn("Failed to delete $file: $_");
            };
        }

        return;
    };

    # Delete files - drop privileges for user paths to ensure we can only
    # delete files the user owns (prevents privilege escalation)
    if ( $user && $> == 0 && $user ne 'root' ) {
        Cpanel::AccessIds::ReducedPrivileges::call_as_user( $delete_files_coderef, $user );
    }
    else {
        $delete_files_coderef->();
    }

    $logger->info( "Deleted $deleted_count files from $base_path" . ( $user ? " (user: $user)" : "" ) );

    # Execute post-deletion hook
    try {
        Cpanel::Hooks::hook(
            {
                category => 'Log::Retention',
                event    => 'post_deletion',
                stage    => 'post',
            },
            {
                log_type       => $log_type,
                user           => $user,
                base_path      => $base_path,
                deleted_count  => $deleted_count,
                retention_days => $retention_days,
            }
        );
    }
    catch {
        $logger->warn("Post-deletion hook failed: $_");
    };

    return $deleted_count;
}

sub _get_web_default_retention {
    my $cpconf = Cpanel::Config::LoadCpConf::loadcpconf();

    return $cpconf->{'web_log_retention_days'} // 0;
}

sub _get_web_user_retention {
    my ($user) = @_;

    my $user_home = _get_user_home($user);
    return unless $user_home;

    my $config_path = "$user_home/.cpanel-logs";
    return unless -e $config_path;

    require Cpanel::Config::LoadConfig;

    my @pwent = getpwnam($user);
    return unless @pwent;

    my $conf_ref;
    if ( $> == 0 && $user ne 'root' ) {
        $conf_ref = Cpanel::AccessIds::ReducedPrivileges::call_as_user(
            sub { Cpanel::Config::LoadConfig::loadConfig($config_path) },
            $pwent[2], $pwent[3]
        );
    }
    else {
        $conf_ref = Cpanel::Config::LoadConfig::loadConfig($config_path);
    }

    my $value = $conf_ref->{'web-log-retention-days'};
    return unless defined $value && $value =~ /^[0-9]+$/;

    return int($value);
}

sub _get_web_sys_paths {
    my @paths;

    # Apache system logs
    push @paths, '/usr/local/apache/logs' if -d '/usr/local/apache/logs';
    push @paths, '/var/log/apache2'       if -d '/var/log/apache2';
    push @paths, '/var/log/httpd'         if -d '/var/log/httpd';

    # NGINX system logs
    push @paths, '/var/log/nginx' if -d '/var/log/nginx';

    # cPanel domlogs
    push @paths, '/usr/local/apache/domlogs' if -d '/usr/local/apache/domlogs';

    return @paths;
}

sub _get_web_user_paths {
    my ($user) = @_;

    my $user_home = _get_user_home($user);
    return () unless $user_home;

    my @paths;

    # web specific logs in user's ~/logs directory ???
    my $logs_dir = "$user_home/logs";
    push @paths, $logs_dir if -d $logs_dir;

    return @paths;
}

sub _get_user_home {
    my ($user) = @_;

    my @pwent = getpwnam($user);
    return @pwent ? $pwent[7] : undef;
}

#
# Plugin System
#
# Third-party plugins can be installed by dropping a YAML file in PLUGIN_DIR.
# Each plugin YAML file should define handlers that point to executable scripts.
#
# Example plugin YAML structure:
#
#   name: myapp
#   description: "MyApp application logs"
#   handlers:
#     get_default_retention: /opt/cpanel/myapp/bin/log_retention_default
#     get_user_retention: /opt/cpanel/myapp/bin/log_retention_user
#     sys_paths: /opt/cpanel/myapp/bin/log_retention_sys_paths
#     user_paths: /opt/cpanel/myapp/bin/log_retention_user_paths
#
# Handler scripts should:
#   - get_default_retention: Print the default retention days (integer) to STDOUT
#   - get_user_retention: Accept username as $1, print retention days to STDOUT (or nothing for default)
#   - sys_paths: Print one path per line to STDOUT
#   - user_paths: Accept username as $1, print one path per line to STDOUT
#

sub _load_plugin_log_types {
    my $plugin_dir = PLUGIN_DIR;
    return unless -d $plugin_dir;

    opendir my $dh, $plugin_dir or return;
    while ( my $file = readdir($dh) ) {
        next unless $file =~ /^([a-z][a-z0-9_-]*)\.(?:yaml|yml)$/i;
        my $potential_name = $1;
        next unless -f "$plugin_dir/$file";

        try {
            my $config = Cpanel::YAML::LoadFile("$plugin_dir/$file");
            _register_plugin_log_type( $config, "$plugin_dir/$file" );
        }
        catch {
            warn "Failed to load plugin from $file: $_\n";
        };
    }
    closedir $dh;

    return;
}

sub _register_plugin_log_type {
    my ( $config, $config_path ) = @_;

    # Validate required fields
    my $name = $config->{name};
    unless ( $name && $name =~ /^[a-z][a-z0-9_-]*$/i ) {
        warn "Plugin config $config_path: 'name' is required and must be alphanumeric\n";
        return;
    }

    # Don't allow overriding built-in types
    if ( exists $BUILTIN_LOG_TYPES{$name} ) {
        warn "Plugin config $config_path: Cannot override built-in log type '$name'\n";
        return;
    }

    my $handlers = $config->{handlers};
    unless ( $handlers && ref($handlers) eq 'HASH' ) {
        warn "Plugin config $config_path: 'handlers' section is required\n";
        return;
    }

    # Validate required handlers exist and are executable
    for my $required (qw(get_default_retention sys_paths)) {
        my $handler = $handlers->{$required};
        unless ( $handler && -f $handler && -x $handler ) {
            warn "Plugin config $config_path: Handler '$required' ($handler) must exist and be executable\n";
            return;
        }
    }

    # Register the plugin log type with wrapper functions
    $LOG_TYPES{$name} = {
        description           => $config->{description} || "Plugin: $name",
        get_default_retention => sub { _call_plugin_handler( $handlers->{get_default_retention}, 'scalar' ) },
        get_user_retention    => sub { _call_plugin_handler( $handlers->{get_user_retention},    'scalar', @_ ) },
        sys_paths             => sub { _call_plugin_handler( $handlers->{sys_paths},             'list' ) },
        user_paths            => sub { _call_plugin_handler( $handlers->{user_paths},            'list', @_ ) },
        _plugin_config        => $config,
    };

    return 1;
}

sub _call_plugin_handler {
    my ( $handler_path, $return_type, @args ) = @_;

    # If no handler defined (optional handlers like get_user_retention)
    return $return_type eq 'list' ? () : undef unless $handler_path;
    return $return_type eq 'list' ? () : undef unless -f $handler_path && -x $handler_path;

    my $result = Cpanel::SafeRun::Object->new(
        program => $handler_path,
        args    => \@args,
        timeout => 30,
    );

    if ( $result->CHILD_ERROR() ) {
        warn "Plugin handler $handler_path failed: " . ( $result->stderr() || 'unknown error' ) . "\n";
        return $return_type eq 'list' ? () : undef;
    }

    my $output = $result->stdout() // '';
    chomp $output;

    if ( $return_type eq 'scalar' ) {

        # Return first line as scalar (e.g., retention days)
        my ($value) = split /\n/, $output, 2;
        return $value;
    }
    else {
        # Return all lines as list (e.g., paths)
        return grep { length $_ } split /\n/, $output;
    }
}

exit( __PACKAGE__->script(@ARGV) || 0 ) if !caller();

1;

__END__

=head1 NAME

scripts::log_retention - Log retention management utility for cPanel & WHM

=head1 SYNOPSIS

    /usr/local/cpanel/scripts/log_retention [options]

    Options:
        --help, -h          Show help message
        --run               Execute log retention cleanup
        --type TYPE         Process specific log type(s) only
        --verbose, -v       Enable verbose output

=head1 DESCRIPTION

This script manages log file retention across different log types in cPanel & WHM.
It supports:

=over 4

=item * System-wide default retention policies

=item * User-specific retention overrides

=item * Multiple log types (web, and extensible for others)

=item * Hook points for custom pre/post deletion actions

=item * Resource-aware processing

=back

=head1 LOG TYPES

=head2 web

Manages web server logs including:

=over 4

=item * Apache access and error logs

=item * NGINX access and error logs

=item * Domain-specific logs in user directories

=item * System-wide web server logs

=back

=head1 CONFIGURATION

=head2 System Configuration

Default web log retention is configured via WHM Tweak Settings
(web_log_retention_days in /var/cpanel/cpanel.config):

    web_log_retention_days=30

Set to 0 to never delete (this is the default).

=head2 User Configuration

Users can override the system default via the .cpanel-logs file
in the user's home directory (~user/.cpanel-logs):

    web-log-retention-days=60

Set to 0 to never delete user logs.
If not set or invalid, the system default is used.

=head1 HOOKS

The following hooks are available for custom integration, such as archiving
logs to external storage before deletion:

=head2 Log::Retention::pre_deletion

Called before deleting files. This is the recommended integration point for
archiving solutions - copy or upload the files before they are deleted.

Receives:

=over 4

=item * log_type - The type of logs being processed

=item * user - Username (if processing user logs, undef for system logs)

=item * base_path - Base directory being processed

=item * files_to_delete - Array reference of files to be deleted

=item * retention_days - Retention period in days

=back

=head2 Log::Retention::post_deletion

Called after deleting files. Receives:

=over 4

=item * log_type - The type of logs processed

=item * user - Username (if processing user logs, undef for system logs)

=item * base_path - Base directory processed

=item * deleted_count - Number of files actually deleted

=item * retention_days - Retention period in days

=back

=cut

Youez - 2016 - github.com/yon3zu
LinuXploit