package Cpanel::ClamScanner;
# cpanel - ClamScanner.pm.pre11                   Copyright(c) 2011 cPanel, Inc.
#                                                           All rights Reserved.
# copyright@cpanel.net                                         http://cpanel.net
# This code is subject to the cpanel license. Unauthorized copying is prohibited

use strict;
use Carp;
use vars qw(@ISA @EXPORT $VERSION);
require Exporter;

@ISA    = qw(Exporter);
@EXPORT = qw( ClamScanner_cleansembox ClamScanner_init ClamScanner_scanhomedir
  ClamScanner_bars ClamScanner_main ClamScanner_disinfectlist
  ClamScanner_disinfect ClamScanner_printScans );

$VERSION = '1.1';

require 5.004;

my $ClamScanner_bytescount    = 0;
my $ClamScanner_pbytescount   = 0;
my $ClamScanner_filecount     = 0;
my $ClamScanner_pfilecount    = 0;
my $ClamScanner_bars          = 0;
my $ClamScanner_infected      = 0;
my $ClamScanner_txtfile       = '';
my $ClamScanner_lastjsupdate  = 0;
my $ClamScanner_lastfjsupdate = 0;
my $ClamScanner_av;

sub ClamScanner_init {
    return (1);
}

sub ClamScanner_disinfectlist {
    open( IFL, "<", "$Cpanel::homedir/.clamavconnector.scan" );
    my $n = 0;
    while (<IFL>) {
        $n++;
        chomp();
        my ( $file, $virus ) = split( /=/, $_ );
        print "<tr>";
        print "<td>$file</td><td>$virus</td>";
        my $dc = 'checked';

        if ( $file =~ /^mail\// ) {
            $dc = '';
            print "<td><input type=radio name=\"action-${n}\" checked value=x></td>";
        }
        else {
            print "<td></td>";
        }
        print "<td><input type=radio name=\"action-${n}\" $dc value=q></td>";
        print "<td><input type=radio name=\"action-${n}\" value=d></td>";
        print "<td><input type=radio name=\"action-${n}\" value=i></td>";
        print "</tr>";

    }
}

sub ClamScanner_main {
    my @perms = _getperms();
    if ( !@perms ) {
        print "<p><b>ClamAV scanner has been disabled for this account.</b></p>\n";
        return ();
    }
    my $now = time();
    if ( ( ( stat("$Cpanel::homedir/.clamavconnector.scan") )[9] + 600 ) <= $now ) {
        unlink("$Cpanel::homedir/.clamavconnector.scan");
    }
    else {
        print "Virus Scan Already In Progress!  Please wait till it completes and then reload this page.\n";
        return ();
    }

    print "<center>Detecting Status....</center>";

    if ( -e "$Cpanel::homedir/.clamavconnector.scan" ) {
        print "<script>document.location.href = 'disinfect.html';</script>\n\n\n\n";
    }
    else {
        print "<script>document.location.href = 'newscan.html';</script>\n\n\n\n";
    }

}

sub ClamScanner_getsocket {
    my ( $clamd, $cmsg ) = find_clamd();
    my $dl = `strings $clamd`;
    $dl =~ /^(.*\/clamav.conf)/m;
    my $conf = $1;
    if ( $conf eq "" ) { $conf = '/etc/clamav.conf'; }
    my $socket = '';
    open( CONF, "<", $conf );
    while (<CONF>) {

        if (/^[\s\t]*LocalSocket[\s\t]*(\S+)/i) {
            $socket = $1;
        }
    }
    close(CONF);
    if ( $socket eq "" ) { $socket = '/var/clamd'; }
    return ($socket);
}

sub find_clamd {
    my $clambin = "";
    my $msg;
    my (@LOC) = (
        "/usr/sbin/clamd", "/usr/local/sbin/clamd",
        "/usr/bin/clamd",  "/usr/local/bin/clamd"
    );

    foreach my $loc (@LOC) {
        if ( -e $loc ) { return ( $loc, undef ); }
        else           { $msg = "Unable to locate clamd\n"; }
    }
    return ( $clambin, $msg );
}

sub ClamScanner_scanhomedir {
    alarm(7200);
    POSIX::nice(19);    #do not steal all the cpu

    my ($scanarg) = @_;

    $| = 1;

    my $scandir = $Cpanel::homedir;
    if ( $scanarg eq "pubftp" )  { $scandir .= "/public_ftp"; }
    if ( $scanarg eq "pubhtml" ) { $scandir .= "/public_html"; }
    if ( $scanarg eq "mail" )    { $scandir .= "/mail"; }

    if ( -e "$Cpanel::homedir/.clamavconnector.scan" ) {
        print "<script>alert('Virus Scan Already In Progress!  Please wait till it completes before scanning again.');</script>\n\n\n";
        print "<script>parent.document.location.href = 'index.html';</script>\n\n\n\n";
    }

    my $port = ClamScanner_getsocket();
    print "<script>parent.frames.mainfr.document.viriiform.cfile.value = '... connecting to clamd service ...';</script>\n\n\n\n";
    $ClamScanner_av = new File::Scan::ClamAV( find_all => 1, port => $port );
    if ( $ClamScanner_av->ping ) {
        print "<script>parent.frames.mainfr.document.viriiform.cfile.value = '... connected to clamd service ...';</script>\n\n\n\n";
        open( VIRII, ">", "$Cpanel::homedir/.clamavconnector.scan" );
        File::Find::find( \&ClamScanner_filecount, $scandir );
        my $tbytes = int( $ClamScanner_bytescount / 1024 / 1024 );
        print "<script>parent.frames.mainfr.document.viriiform.tfilen.value = '$ClamScanner_filecount';</script>\n\n\n\n";
        print "<script>parent.frames.mainfr.document.viriiform.tbytes.value = '$tbytes';</script>\n\n\n\n";

        File::Find::find( \&ClamScanner_processfile, $scandir );
        close(VIRII);
        print "<script>parent.frames.mainfr.document.viriiform.cfile.value = '... scan complete $ClamScanner_filecount files scanned.';</script>\n\n\n\n";
        print "<script>parent.frames.mainfr.bars.document.location.href='bars.html?bars=45';</script>\n";
        print "<script>parent.frames.mainfr.document.viriiform.percent.value = '100';</script>\n\n\n\n";
        if ($ClamScanner_infected) {
            print "<script>alert('Virus Scan Complete.  $ClamScanner_infected infected files found.');</script>\n\n";
            print "<script>parent.document.location.href='disinfect.html';</script>\n\n";
        }
        else {
            unlink("$Cpanel::homedir/.clamavconnector.scan");
            print "<script>alert('Virus Scan Complete.  No Viruses Found.');</script>\n\n";
            print "<script>parent.document.location.href='index.html';</script>\n\n";
        }

    }
    else {
        print "Could not connect to clamd\n";
    }
}

sub ClamScanner_filecount {
    my $file = $File::Find::name;
    return if ( -d $file || -l $file || $file =~ /quarantine_clamavconnector\// );

    $ClamScanner_bytescount += ( stat($file) )[7];
    $ClamScanner_filecount++;

}

sub ClamScanner_processfile {
    my $file = $File::Find::name;
    return if ( -d $file || -l $file || $file =~ /quarantine_clamavconnector\// );
    $ClamScanner_pbytescount += ( stat($file) )[7];
    $ClamScanner_pfilecount++;

    my $cbytes = int( $ClamScanner_pbytescount / 1024 / 1024 );
    my ($virus);

    my $percent    = ( $ClamScanner_pbytescount / $ClamScanner_bytescount );
    my $bars       = int( 45 * $percent );
    my $txtpercent = int( $percent * 100 );
    if ( $bars == 0 ) { $bars = 1; }

    my $txtfile = "home:" . substr( $file, length($Cpanel::homedir) + 1 );
    my $rlfile = substr( $file, length($Cpanel::homedir) + 1 );

    $txtfile =~ s/\'/\\\\'/g;

    _jscfileupdate( $txtfile, $cbytes );

    if ( $rlfile =~ /^mail\// ) {
        $ClamScanner_txtfile = $txtfile;
        $virus               = _scanmbox($file);
    }
    else {
        $ClamScanner_txtfile = '';
        ( undef, $virus ) = $ClamScanner_av->scan($file);
    }

    if ( $virus ne "" ) {
        $ClamScanner_infected++;
        print VIRII "${rlfile}=${virus}\n";
        $virus =~ s/\'/\\\\'/g;
        print "<script>parent.frames.mainfr.document.viriiform.status.options[parent.frames.mainfr.document.viriiform.status.options.length] = new Option('$txtfile: $virus','$txtfile');</script>\n\n";
    }

    if ( $ClamScanner_bars != $bars ) {
        $ClamScanner_bars = $bars;
        print "<script>\n";
        print "parent.frames.mainfr.bars.document.location.href='bars.html?bars=$ClamScanner_bars';\n";
        print "</script>\n";
    }
    _jscupdate( $ClamScanner_pfilecount, $cbytes, $txtpercent );

}

sub ClamScanner_bars {
    my ($bars) = @_;
    for ( my $i = 1; $i <= $bars; $i++ ) {
        print "<img src=blank.gif width=2 height=31>";
        print "<img src=bar.gif width=12 height=31>";
    }
}

sub ClamScanner_disinfect {
    $| = 1;

    my ($form) = @_;
    my (%FORM) = %{$form};

    my $n = 0;
    open( IFL, "<", "$Cpanel::homedir/.clamavconnector.scan" );
    while (<IFL>) {
        $n++;
        chomp();
        my ( $file, $virus ) = split( /=/, $_ );
        print "<b>${file}</b>: ";
        if ( $FORM{"action-${n}"} eq "d" ) {
            print "destroying...";
            unlink("${Cpanel::homedir}/${file}");
            print "...done";
        }
        elsif ( $FORM{"action-${n}"} eq "q" ) {
            my $safefile = $file;
            $safefile =~ s/\//_/g;
            print "quarantining...";
            if ( !-e "${Cpanel::homedir}/quarantine_clamavconnector" ) {
                mkdir( "${Cpanel::homedir}/quarantine_clamavconnector", 0700 );
            }
            rename( "${Cpanel::homedir}/${file}", "${Cpanel::homedir}/quarantine_clamavconnector/$safefile" );
            print "...done";
        }
        elsif ( $FORM{"action-${n}"} eq "x" ) {
            print "disinfecting...";
            ClamScanner_cleansembox( ${Cpanel::homedir} . "/" . $file );
            print "...done";
        }
        else {
            print "ignored";
        }
        print "<br>";
    }
    close(IFL);
    unlink("$Cpanel::homedir/.clamavconnector.scan");
}

sub ClamScanner_cleansembox {
    my ($mbox) = @_;

    my $port = ClamScanner_getsocket();

    $ClamScanner_av = new File::Scan::ClamAV(
        find_all => 1,
        port     => $port
    );
    if ( $ClamScanner_av->ping ) {
        my $message = 0;
        unlink("${mbox}.clamscan.tmp");
        my $sl = SafeFile::safeopen( \*MXCLEAN, "<", "${mbox}.clamscan.tmpsafe" );
        open( CMBOX, ">", "$mbox.clamavconnector_clean" );
        chmod( 0600, "$mbox.clamavconnector_clean" );
        open( MBOX, "<", $mbox );
        my $line;
        while ( $line = <MBOX> ) {

            if ( $line =~ /^From\s+\S+\s+\S+\s+\S+\s+\d+\s+\d+:\d+:\d+\s+\d+/ ) {
                my $virus = _checkvirus( $mbox, $message );
                _recvirus( $mbox, $virus, $message );
                _cleanvtmp( $mbox, $message );
                $message++;
                open( ST, ">", "${mbox}.clamscan.tmp.${message}" );
                chmod( 0600, "${mbox}.clamscan.tmp.${message}" );
            }
            print ST $line;
        }
        my $virus = _checkvirus( $mbox, $message );
        _recvirus( $mbox, $virus, $message );
        _cleanvtmp( $mbox, $message );
        close(CMBOX);
        open( CMBOX, "<", "${mbox}.clamavconnector_clean" );
        my $mboxlock = SafeFile::safeopen( \*MBOX, ">", "$mbox" );
        while (<CMBOX>) {
            print MBOX;
        }
        SafeFile::safeclose( \*MBOX, $mboxlock );
        close(CMBOX);
        unlink("${mbox}.clamavconnector_clean");
        SafeFile::safeclose( \*MXCLEAN, $sl );
    }
}

################################################################################
# ClamScanner_printScans
################################################################################
sub ClamScanner_printScans {
    my @perms = _getperms();
    if ( !@perms ) {
        print "<p><b>ClamAV Scanner has been disabled for this account.</b></p>\n";
        return ();
    }

    print "<table align=\"center\" cellspacing=\"0\" cellpadding=\"2\" border=\"0\">";
    print "<form action=\"scanner.html\">\n";

    my $message = "";
    my $checked = "";
    foreach my $perm (@perms) {
        if ( $perm eq "mail" ) {
            $perm    = "mail";
            $message = "Scan Mail";
            $checked = " CHECKED";
        }
        elsif ( $perm eq "homedir" ) {
            $perm    = "home";
            $message = "Scan Entire Home Directory";
            $checked = "";
        }
        elsif ( $perm eq "pubhtml" ) {
            $perm    = "pubhtml";
            $message = "Scan Public Web Space";
            $checked = "";
        }
        elsif ( $perm eq "pubftp" ) {
            $perm    = "pubftp";
            $message = "Scan Public FTP Space";
            $checked = "";
        }
        print "<tr>\n<td>";
        print "<input type=\"radio\" name=\"scanpath\" value=\"$perm\"${checked}>${message}</input>\n";
        print "</td></tr>\n";
    }
    print "<tr><td>&nbsp;</td></tr>\n";
    print "<tr><td><input type=\"submit\" value=\"Scan Now\"></input></td></tr>\n";
    print "</form></table>\n";
}

################################################################################
# _getperms
################################################################################

sub _getperms {
    my %conf;
    my @perms = qw/ mail homedir pubhtml pubftp /;

    if ( -e "/etc/cpclamav.conf" ) {
        open( CONF, "<", "/etc/cpclamav.conf" );
        while (<CONF>) {
            chomp();
            my ( $var, $val ) = split( /=/, $_ );
            $conf{$var} = $val;
        }
        close(CONF);
    }
    else {
        return (@perms);
    }

    if ( defined( $conf{'CLAMAVOVERRIDEUSERS'} ) ) {
        my @overrideusers = split( /,/, $conf{'CLAMAVOVERRIDEUSERS'} );
        foreach my $user (@overrideusers) {
            if ( $user eq $Cpanel::user ) {
                if ( -e "/var/cpanel/users/" . $user ) {
                    open( CONF, "/var/cpanel/users/" . $user ) or return undef;
                    my @u_conf = <CONF>;
                    close(CONF);
                    @u_conf = map { chomp; $_ } @u_conf;
                    my %conf;
                    foreach my $line (@u_conf) {
                        next if ( $line =~ /^[\s\t]*$/ );
                        my ( $var, $val ) = split( /=/, $line );
                        $conf{$var} = $val;
                    }
                    if ( defined( $conf{'CLAMAVSCANS'} ) ) {
                        @perms = split( /,/, $conf{'CLAMAVSCANS'} );
                        return (@perms);
                    }
                }
                last;
            }
        }
        return (@perms);
    }

    if ( defined( $conf{'DEFAULTSCANS'} ) ) {
        @perms = split( /,/, $conf{'DEFAULTSCANS'} );
        return (@perms);
    }
    else {
        return (@perms);
    }
}

sub _scanmbox {
    my ($mbox) = @_;

    my $message = 0;
    open( MBOX, "<", $mbox );
    my $line;
    while ( $line = <MBOX> ) {
        if ( $line =~ /^From\s+\S+\s+\S+\s+\S+\s+\d+\s+\d+:\d+:\d+\s+\d+/ ) {
            my $virus = _checkvirus( $mbox, $message );
            _cleanvtmp( $mbox, $message );
            if ( $virus ne "" ) { return ($virus); }

            $message++;
            open( ST, ">", "${mbox}.clamscan.tmp.${message}" );
            chmod( 0600, "${mbox}.clamscan.tmp.${message}" );
        }
        print ST $line;
    }
    my $virus = _checkvirus( $mbox, $message );
    _cleanvtmp( $mbox, $message );
    if ( $virus ne "" ) { return ($virus); }

    return ("");
}

sub _recvirus {
    my ( $file, $virus, $message ) = @_;
    if ( $virus ne "" ) {
        print "..purged message $message (infected with $virus)..\n";
    }
    else {
        open( ST, "<", "${file}.clamscan.tmp.$message" );
        while (<ST>) { print CMBOX; }
        close(ST);
        print ".\n";
    }
}

sub _cleanvtmp {
    my ( $file, $message ) = @_;
    unlink("${file}.clamscan.tmp.${message}");
}

sub _checkvirus {
    my ( $file, $message ) = @_;

    return if ( !-e "${file}.clamscan.tmp.${message}" );
    close(ST);

    if ( $ClamScanner_txtfile ne "" ) {
        _jscfileupdate("$ClamScanner_txtfile (Message ${message})");
    }

    my ( $result, $virus ) = $ClamScanner_av->scan("${file}.clamscan.tmp.${message}");

    return ($virus);
}

sub _jscfileupdate {
    my ( $jsfile, $size ) = @_;
    my $now = time();

    if ( $size > 10 || $ClamScanner_lastjsupdate < $now ) {
        print "<script>parent.frames.mainfr.document.viriiform.cfile.value = '$jsfile';</script>\n\n\n\n";
        $ClamScanner_lastjsupdate = $now;
    }
}

sub _jscupdate {
    my ( $cfilen, $cbytes, $percent ) = @_;
    my $now = time();

    if ( $ClamScanner_lastfjsupdate < $now ) {
        print "<script>parent.frames.mainfr.document.viriiform.cfilen.value = '$cfilen';</script>\n\n\n\n";
        print "<script>parent.frames.mainfr.document.viriiform.cbytes.value = '$cbytes';</script>\n\n\n\n";
        print "<script>parent.frames.mainfr.document.viriiform.percent.value = '$percent';</script>\n\n\n\n";
        $ClamScanner_lastfjsupdate = $now;
    }
}

1;
