package Cpanel::ClamScanner;

# cpanel - Cpanel/ClamScanner.pm                  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

#-------------------------------------------------------------------------------
# NOTE:
# This module is expected to have 2 identical copies:
#     - $DS/cpanel/trunk/Cpanel/ClamScanner.pm
#           [This is kept as the master copy per Ben]
#     - $FX/addonmodules/clamavconnector/trunk/Cpanel/ClamScanner.pm
#           [Used for publishing to httpupdate]
# Whenever a new change is applied, both copies of this module need to be updated
# with the same change and kept in sync.
#-------------------------------------------------------------------------------

use Cpanel::SafeFile ();
use Cpanel::SafeFind ();
use Cpanel::OSSys    ();
use Cpanel::Branding ();
use Cpanel::Logger   ();

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.2';

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;
my $ClamScanner_homeb;

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

sub ClamScanner_init {
    return 1;
}

sub ClamScanner_disinfectlist {
    if ( open my $ifl_fh, "<", "$Cpanel::homedir/.clamavconnector.scan" ) {
        my $n = 0;
        while ( readline($ifl_fh) ) {
            $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>";
        }
        close $ifl_fh;
    }
}

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 = shift;
    if ( !$clamd ) {
        ($clamd) = find_clamd();    # second value is $cmsg', we don't care about it
    }

    my $dl = `strings $clamd`;
    $dl =~ /^(.*\/clam(?:av|d).conf)/m;
    my $conf = $1;
    if ( $conf eq '' ) { $conf = -e '/etc/clamav.conf' ? '/etc/clamav.conf' : '/etc/clamd.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);
    Cpanel::OSSys::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();
    _jscfileupdate('... connecting to clamd service ...');

    if ( !$INC{'File/Scan/ClamAV.pm'} ) {
        require File::Scan::ClamAV;
    }

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

        Cpanel::SafeFind::finddepth( \&ClamScanner_processfile, $scandir );
        close(VIRII);
        _jscfileupdate("... scan complete $ClamScanner_filecount files scanned.");
        print qq{
<script>
    if (parent.frames.mainfr) {
    parent.frames.mainfr.bars.document.location.href='bars.html?bars=45';
    parent.frames.mainfr.document.viriiform.percent.value = '100';
    }
    </script>
        };

        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 qq{ 
    <script>
        alert('Virus Scan Complete.  No Viruses Found.');
        parent.document.location.href='index.html';
</script>
};
        }

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

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

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

sub ClamScanner_processfile {
    my $file = $File::Find::name;
    return if ( -l $file || -d $file || $file =~ m/quarantine_clamavconnector\// );
    $ClamScanner_pbytescount += ( stat(_) )[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; }

    if ( !$ClamScanner_homeb ) {
        $ClamScanner_homeb = Cpanel::Branding::Branding_image( 'homeb', 1 );
    }
    my $txtfile = substr( $file, length($Cpanel::homedir) + 1 );

    my $textarea_txtfile;
    if ( $INC{'Cpanel/Encoder/Tiny.pm'} ) {
        $textarea_txtfile = Cpanel::Encoder::Tiny::safe_html_encode_str($txtfile);
    }
    else {
        Cpanel::Encoder::html_encode_str($txtfile);
    }

    if ( $ClamScanner_homeb eq '' ) {
        $txtfile = 'home:' . $txtfile;
    }
    else {
        $txtfile = qq{<img src="$ClamScanner_homeb" align="absmiddle">/} . $txtfile;
    }
    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 qq{<script>
    if (parent.frames.mainfr) {
        parent.frames.mainfr.document.viriiform.status.options[parent.frames.mainfr.document.viriiform.status.options.length] = new Option('$textarea_txtfile: $virus','$txtfile');
    }
        </script>
        };
    }

    if ( $ClamScanner_bars != $bars ) {
        $ClamScanner_bars = $bars;
        print qq{<script>
        if (parent.frames.mainfr) {
                parent.frames.mainfr.bars.document.location.href='bars.html?bars=$ClamScanner_bars';\n
         }
        </script>
        };
    }
    _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( my $ifl_fh, "<", "$Cpanel::homedir/.clamavconnector.scan" );
    while ( readline($ifl_fh) ) {
        $n++;
        chomp();

        # Filename may contain "=" and the "=" separator should be the last occurrence
        # (Usage of greedy pattern is needed.)
        my ( $file, $virus ) = /(.*)=([^=]+)/;

        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_fh);
    unlink("$Cpanel::homedir/.clamavconnector.scan");
}

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

    my $port = ClamScanner_getsocket();

    if ( !$INC{'File/Scan/ClamAV.pm'} ) {
        require File::Scan::ClamAV;
    }

    $ClamScanner_av = new File::Scan::ClamAV(
        'find_all' => 1,
        'port'     => $port
    );
    if ( $ClamScanner_av->ping ) {

        my $message = 0;

        my $mbox_fh;
        my $clean_mbox_fh;

        # Processing input $mbox
        if ( !open( $mbox_fh, "<", $mbox ) ) {
            $logger->info("Encountered open file error ... Unable to disinfect mail file \"$mbox\": $!");
            return;
        }

        # tidyoff
        # Using output file $mbox.clamavconnector_clean as holder
        if ( !sysopen( $clean_mbox_fh, "${mbox}.clamavconnector_clean",
                &Fcntl::O_WRONLY | &Fcntl::O_TRUNC | &Fcntl::O_CREAT | &Fcntl::O_NOFOLLOW, 0600
            )
        ) {
            $logger->warn("Could not open file \"${mbox}.clamavconnector_clean\"");
        }

        # Using output file ${mbox}.clamscan.tmp.${message} for each mail message
        my $st_fh;
        if (
            !sysopen( 
                $st_fh, "${mbox}.clamscan.tmp.${message}",
                &Fcntl::O_WRONLY | &Fcntl::O_TRUNC | &Fcntl::O_CREAT | &Fcntl::O_NOFOLLOW, 0600
            )
        ) {
            $logger->warn("Could not open file \"${mbox}.clamscan.tmp.${message}\"");
        }
        # tidyon
        my $virus;
        my $line;
        while ( $line = readline($mbox_fh) ) {
            if ( $line =~ /^From\s+\S+\s+\S+\s+\S+\s+\d+\s+\d+:\d+:\d+\s+\d+/ ) {

                close($st_fh);
                $virus = _checkvirus( $mbox, $message );
                _recvirus( $mbox, $virus, $message, $clean_mbox_fh );
                _cleanvtmp( $mbox, $message );

                # Process next message.
                $message++;
                # tidyoff
                if (
                    !sysopen(
                        $st_fh, "${mbox}.clamscan.tmp.${message}",
                        &Fcntl::O_WRONLY | &Fcntl::O_TRUNC | &Fcntl::O_CREAT | &Fcntl::O_NOFOLLOW, 0600
                    )
                ) {
                    $logger->warn("Could not open file \"${mbox}.clamscan.tmp.${message}\"");
                }
                # tidyon
            }

            if ( !print {$st_fh} $line ) {
                $logger->warn("Could not print line to file : $!");
            }
        }

        close($mbox_fh);

        # Process remaining data
        close($st_fh);
        $virus = _checkvirus( $mbox, $message );
        _recvirus( $mbox, $virus, $message, $clean_mbox_fh );
        _cleanvtmp( $mbox, $message );

        close($clean_mbox_fh);

        # Update mbox using the clean one

        if ( !sysopen( $clean_mbox_fh, "${mbox}.clamavconnector_clean", &Fcntl::O_RDONLY | &Fcntl::O_NOFOLLOW ) ) {
            $logger->warn("Could not open file \"${mbox}.clamavconnector_clean\"");
        }

        my $out_mbox_fh = IO::Handle->new();
        my $mboxlock = Cpanel::SafeFile::safeopen( $out_mbox_fh, ">", "$mbox" );
        if ( !$mboxlock ) {
            $logger->warn("Could not write to mbox file \"$mbox\"");
            return;
        }

        # Update mbox with the clean data from clean_mbox_fh
        while ( $line = readline($clean_mbox_fh) ) {
            print {$out_mbox_fh} $line;
        }
        Cpanel::SafeFile::safeclose( $out_mbox_fh, $mboxlock );
        close($clean_mbox_fh);

        unlink("${mbox}.clamavconnector_clean");
    }
}

################################################################################
# ClamScanner_printScans
################################################################################
sub ClamScanner_printScans {

    my $locale;
    my $lang;
    if ( $INC{'Cpanel/Locale.pm'} ) {
        $locale = Cpanel::Locale->get_handle();
    }
    else {
        $lang = $Cpanel::CPDATA{'LANG'};
    }

    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 = defined $locale ? $locale->maketext('Clam-ScanMail') : $Cpanel::Lang::LANG{$lang}{'Clam-ScanMail'};
            $checked = " CHECKED";
        }
        elsif ( $perm eq "homedir" ) {
            $perm    = "home";
            $message = defined $locale ? $locale->maketext('Clam-ScanHome') : $Cpanel::Lang::LANG{$lang}{'Clam-ScanHome'};
            $checked = "";
        }
        elsif ( $perm eq "pubhtml" ) {
            $perm    = "pubhtml";
            $message = defined $locale ? $locale->maketext('Clam-ScanPublicWeb') : $Cpanel::Lang::LANG{$lang}{'Clam-ScanPublicWeb'};
            $checked = "";
        }
        elsif ( $perm eq "pubftp" ) {
            $perm    = "pubftp";
            $message = defined $locale ? $locale->maketext('Clam-ScanPublicFtp') : $Cpanel::Lang::LANG{$lang}{'Clam-ScanPublicFtp'};
            $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";
    my $scan_now = defined $locale ? $locale->maketext('Clam-ScanNow') : $Cpanel::Lang::LANG{$lang}{'Clam-ScanNow'};
    print "<tr><td><input class=\"input-button\" 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( my $mbox_fh, "<", $mbox );
    my $line;

    my $st_fh;
    # tidyoff
    if ( 
        !sysopen( 
            $st_fh, "${mbox}.clamscan.tmp.${message}",
            &Fcntl::O_WRONLY | &Fcntl::O_TRUNC | &Fcntl::O_CREAT | &Fcntl::O_NOFOLLOW, 0600
        )
    ) {
        $logger->warn("Could not open file \"${mbox}.clamscan.tmp.${message}\"");
    }
    # tidyon

    while ( $line = readline($mbox_fh) ) {
        if ( $line =~ /^From\s+\S+\s+\S+\s+\S+\s+\d+\s+\d+:\d+:\d+\s+\d+/ ) {

            # Process the current tmp file
            close($st_fh);
            my $virus = _checkvirus( $mbox, $message );
            _cleanvtmp( $mbox, $message );    # Clean up the st_fh temp file after virus check
            if ( $virus ne "" ) { return ($virus); }

            # Moving to the next one
            $message++;
            # tidyoff
            if ( 
                !sysopen( 
                    $st_fh, "${mbox}.clamscan.tmp.${message}",
                    &Fcntl::O_WRONLY | &Fcntl::O_TRUNC | &Fcntl::O_CREAT | &Fcntl::O_NOFOLLOW, 0600 
                )
            ) {
                $logger->warn("Could not open file \"${mbox}.clamscan.tmp.${message}\"");
            }
            # tidyon
        }

        if ( !print {$st_fh} $line ) {
            $logger->warn("Could not print line to file : $!");
        }
    }
    close($mbox_fh);

    # Process remaining data
    close($st_fh);
    my $virus = _checkvirus( $mbox, $message );
    _cleanvtmp( $mbox, $message );    # Clean up the st_fh temp file after virus check
    if ( $virus ne "" ) { return ($virus); }

    return ("");
}

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

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

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

    if ( !-e "${file}.clamscan.tmp.${message}" ) {
        $logger->info("No such file ${file}.clamscan.tmp.${message} to scan!");
        return;
    }
    return if -z _;

    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 ( !defined $size || $size > 10 || $ClamScanner_lastjsupdate < $now ) {
        print qq{
    <script>
       
    if (parent.frames.mainfr) {
        if (parent.frames.mainfr.document.viriiform.cfile) {
            parent.frames.mainfr.document.viriiform.cfile.value = '$jsfile';
        }
        if (parent.frames.mainfr.document.getElementById('viriifile')) {
            parent.frames.mainfr.document.getElementById('viriifile').innerHTML = '$jsfile';
        }
    }
        </script>};
        $ClamScanner_lastjsupdate = $now;
    }
}

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

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

        $ClamScanner_lastfjsupdate = $now;
    }
}

1;
