#!/usr/clearos/sandbox/usr/bin/php
<?php

/**
 * Antivirus file scanner.
 *
 * @category   apps
 * @package    file-scan
 * @subpackage scripts
 * @author     ClearFoundation <developer@clearfoundation.com>
 * @copyright  2011 ClearFoundation
 * @license    http://www.gnu.org/copyleft/gpl.html GNU General Public License version 3 or later
 * @link       http://www.clearfoundation.com/docs/developer/apps/file_scan/
 */

///////////////////////////////////////////////////////////////////////////////
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//
///////////////////////////////////////////////////////////////////////////////

///////////////////////////////////////////////////////////////////////////////
// B O O T S T R A P
///////////////////////////////////////////////////////////////////////////////

$bootstrap = getenv('CLEAROS_BOOTSTRAP') ? getenv('CLEAROS_BOOTSTRAP') : '/usr/clearos/framework/shared';
require_once $bootstrap . '/bootstrap.php';

///////////////////////////////////////////////////////////////////////////////
// T R A N S L A T I O N S
///////////////////////////////////////////////////////////////////////////////

clearos_load_language('file_scan');

///////////////////////////////////////////////////////////////////////////////
// D E P E N D E N C I E S
///////////////////////////////////////////////////////////////////////////////

use \clearos\apps\file_scan\File_Scan as File_Scan;
use \clearos\apps\base\Configuration_File as Configuration_File;
use \clearos\apps\base\File as File;
use \clearos\apps\network\Hostname as Hostname;
use \clearos\apps\mail_notification\Mail_Notification as Mail_Notification;

clearos_load_library('file_scan/File_Scan');
clearos_load_library('base/Configuration_File');
clearos_load_library('base/File');
clearos_load_library('network/Hostname');
clearos_load_library('mail_notification/Mail_Notification');


///////////////////////////////////////////////////////////////////////////////
// F U N C T I O N S
///////////////////////////////////////////////////////////////////////////////

// Fatal error
function fatal_error($code, $error, $filename = null, $line = 0)
{
    if ($filename != null)
    printf('%s:%d: ', basename($filename), $line);

    echo("$error\n");
    exit($code);
}

// Shutdown cleanup
function avscan_shutdown()
{
    @unlink(File_Scan::FILE_LOCKFILE);
}

// Lock state file, write serialized state
function serialize_state($fh)
{
    global $av_state;

    $file_scan = new File_Scan();

    if ($file_scan->serialize_state($fh, $av_state) == false)
        fatal_error(1, 'State serialization failure');
}

// Lock state file, read and unserialized status
function unserialize_state($fh)
{
    global $av_state;

    $file_scan = new File_Scan();

    if ($file_scan->unserialize_state($fh, $av_state) === false)
        fatal_error(1, 'State unserialization failure');
}

function send_email_notification($to_address, $body)
{
    $mailer = new Mail_Notification();
    $hostname = new Hostname();
    $mailer->set_message_subject(lang('file_scan_last_scan_result') . ' - ' . $hostname->get());
    $mailer->set_message_body($body);
    $mailer->add_recipient($to_address);
    $sender = $mailer->get_sender();
    try {
        $mailer->send();
    } catch (Exception $e) {
        echo $e->GetMessage();
    }
}

///////////////////////////////////////////////////////////////////////////////
// M A I N
///////////////////////////////////////////////////////////////////////////////

// Register a shutdown function so we can do some clean-up
register_shutdown_function('avscan_shutdown');

// Register custom error handler
set_error_handler('fatal_error');

// Debug mode?
$debug = FALSE;
$ph = popen('/usr/bin/tty', 'r');
list($tty) = chop(fgets($ph, 4096));
pclose($ph);
if ($tty != 'not a tty') $debug = TRUE;

// Must be run as root
if (posix_getuid() != 0) {
    fatal_error(1, 'Must be run as superuser (root)');
}

// Ensure we are the only instance running
if (file_exists(File_Scan::FILE_LOCKFILE)) {
    $fh = @fopen(File_Scan::FILE_LOCKFILE, 'r');
    list($pid) = fscanf($fh, '%d');

    // Perhaps this is a stale lock file?
    if (!file_exists("/proc/$pid")) {
        // Yes, the process 'appears' to no longer be running...
        @unlink(File_Scan::FILE_LOCKFILE);
    } else {
        // Only one instance can run at a time
        fatal_error(1, 'A scan is already in progress');
    }

    fclose($fh);
} else {
    // Grab the lock ASAP...
    touch(File_Scan::FILE_LOCKFILE);
}


// Save our PID to the lock file
$fh = @fopen(File_Scan::FILE_LOCKFILE, 'w');

fprintf($fh, "%d\n", posix_getpid());
fclose($fh);

// Open configuration (list of directories to recursively scan).  Read it
// into an array quicky in case webconfig writes over it (no file locking)
$dirs = array();
$fh = @fopen(File_Scan::FILE_SCAN_FOLDERS, 'r');

while (!feof($fh)) {
    $dir = chop(fgets($fh, 4096));
    if (strlen($dir) && file_exists($dir)) $dirs[] = $dir;
}

fclose($fh);
sort($dirs);

// Open state file.  This is where we dump scanner status
$fh = @fopen(File_Scan::FILE_STATE, 'a+');
chown(File_Scan::FILE_STATE, 'webconfig');
chgrp(File_Scan::FILE_STATE, 'webconfig');
chmod(File_Scan::FILE_STATE, 0750);
unserialize_state($fh);
$av_state['timestamp'] = time();
serialize_state($fh);

// Override configuration parameters of clam scan
$configfile = new Configuration_File(File_Scan::FILE_CONFIG);

// Defaults
$clam_scan_options = '';
$notify_email_address = NULL;
$quarantine = FALSE;

if ($configfile->exists()) {
    try {
        $options = $configfile->load();
        foreach ($options as $param => $value) {
            if ($param == 'max-filesize')
                $clam_scan_options .= " --$param=$value";
            elseif ($param == 'max-scansize')
                $clam_scan_options .= " --$param=$value";
            elseif ($param == 'notify-email')
                $notify_email_address = $value;
            elseif ($param == 'quarantine')
                $quarantine = (bool)$value;
        }
    } catch (Exception $e) {
        // Use safe defaults
    }
}

// Scan each directory for: THE VIRUSES!
foreach ($dirs as $dir) {
    $safe_dir = escapeshellarg($dir);

    $av_state['dir'] = $dir;
    $av_state['filename'] = '';
    $av_state['filename_with_path'] = '';
    $av_state['count'] = 0;

    serialize_state($fh);

    // First, find out how many files we'll be scanning so we can have a
    // nifty progress bar in webconfig
    if ($debug) echo("Counting files: $safe_dir\n");
    $ph = popen("find $safe_dir -type f | wc -l", 'r');
    list($av_state['total']) = fscanf($ph, '%d');
    if (pclose($ph)) fatal_error(1, "Unable to determine file count in: $safe_dir");

    serialize_state($fh);

    // Run scanner...
    if ($debug) echo("Scanning directory: $safe_dir\n");

    putenv('LANG=en_US');
    $file_scan = new File_Scan();
    $whitelist = $file_scan->get_whitelist();
    $whitelist_options = '';
    if ($whitelist !== FALSE && !empty($whitelist)) {
        foreach ($whitelist as $whitelisted_filename) {
	    if (trim($whitelisted_filename) != '')
		    $whitelist_options .= ' --exclude ' . $whitelisted_filename;
	}
    }
    $ph = popen(File_Scan::FILE_CLAMSCAN . " --stdout $clam_scan_options $whitelist_options -r $safe_dir 2>/dev/null", 'r');

    while (!feof($ph)) {
        $buffer = fgets($ph, 4096);
        // Is this a good idea?
        if (($pos = strrpos($buffer, ':')) === FALSE)
            continue;

        $match = NULL;
        if (preg_match('/^Known viruses:\s*(\d+)$/', $buffer, $match))
            $av_state['stats']['known_viruses'] = $match[1]; 
        else if (preg_match('/^Engine version:\s*(.*)$/', $buffer, $match))
            $av_state['stats']['engine_version'] = $match[1]; 
        else if (preg_match('/^Scanned directories:\s*(\d+)$/', $buffer, $match))
            $av_state['stats']['scanned_dirs'] = $match[1]; 
        else if (preg_match('/^Scanned files:\s*(\d+)$/', $buffer, $match))
            $av_state['stats']['scanned_files'] = $match[1]; 
        else if (preg_match('/^Infected files:\s*(\d+)$/', $buffer, $match))
            $av_state['stats']['infected_files'] = $match[1]; 
        else if (preg_match('/^Data scanned:\s*(.*)$/', $buffer, $match))
            $av_state['stats']['data_scanned'] = $match[1]; 
        else if (preg_match('/^Data read:\s*(.*)\s+\(ratio.*\)$/', $buffer, $match))
            $av_state['stats']['data_read'] = $match[1]; 
        else if (preg_match('/^Time:\s*(.*)\s+sec\s+\(.*\)$/', $buffer, $match))
            $av_state['stats']['time'] = $match[1]; 

        // Sync our state file, may have been modified externally...
        unserialize_state($fh);

        if ($match != NULL) {
            // No need to go further..not a file reference
            serialize_state($fh);
            continue;
        }

        $av_state['count']++;

        // Extract filename and scan result
        $av_state['filename'] = substr($buffer, 0, $pos);
        $av_state['result'] = substr(chop($buffer), $pos + 2);

        $hash = md5($av_state['filename']);

        // Evaluate result
        switch ($av_state['result']) {
            case 'OK':
            case 'Empty file':
            case 'Symbolic link':
                break;
            case 'Excluded':
                break;
            case 'ERROR':
                // Remember files with errors...
                $av_state['error'][$hash] = array(
                    'filename' => $av_state['filename'],
                    'error' => $av_state['result'], 
                    'timestamp' => time()
                    );
                break;
            default:
                // Virus found...
                $av_state['virus'][$hash] = array(
                    'filename' => $av_state['filename'],
                    'filename_without_path' => basename($av_state['filename']),
                    'path' => dirname($av_state['filename']),
                    'virus' => str_replace(' FOUND', '', $av_state['result']),
                    'quarantined' => $quarantine,
                    'dir' => $dir, 'timestamp' => time()
                );
                break;
        }

        serialize_state($fh);

	if ($quarantine && isset($av_state['virus'][$hash]))
	    $file_scan->quarantine_virus($hash);

        if ($debug)
            printf('%.02f: %s', $av_state['count'] * 100 / $av_state['total'], $buffer);
    }

    $av_state['rc'] = pclose($ph);

    serialize_state($fh);
}

fclose($fh);

if ($notify_email_address != NULL && (isset($av_state['error']) || isset($av_state['virus']))) {
    $dump = '';
    if (isset($av_state['virus'])) {
        if ($quarantine)
            $dump .= "The following infected files were found and moved to quaranatine (" . File_Scan::PATH_QUARANTINE . "):\n\n";
        else
            $dump .= "The following infected files were found:\n\n";
        foreach ($av_state['virus'] as $hash => $info)
            $dump .= $info['filename'] . ":  " . $info['virus'] . "\n";
    }
    if (isset($av_state['error'])) {
        $dump .= "The following errors occurred:\n";
        foreach ($viruses as $hash => $info)
            $dump .= $info['filename'] . ":  " . $info['error'] . "\n";
    }
    send_email_notification($notify_email_address, $dump);
}
// vim: syntax=php
