#!/usr/bin/perl
# fedora-extras version

use strict;

# apparently you aren't supposed to leave warnings
# on when you distribute programs...
# use warnings;

use Cwd;
use locale;
use POSIX qw/locale_h strftime/;
use File::Basename;
use File::Find::Rule;
use File::Copy qw/move/;
use File::Path qw/mkpath/;
use File::Temp qw/tempdir/;
use LWP::Simple qw/$ua get/;
use Date::Calc qw/Delta_Days/;

use Gtk2;
use Gtk2::SimpleList;
use Gtk2::SimpleMenu;
use Glib qw/TRUE FALSE/;
Gtk2->init;

use constant COLUMN_FILE   => 0;
use constant COLUMN_TYPE   => 1;
use constant COLUMN_SIZE   => 2;
use constant COLUMN_STATUS => 3;
use constant NUM_COLUMNS   => 4;

my $VERSION   = '2.06';
my $virus_log = '';
my ( $key, $value );
my $ren = "None";
my ( $save_log, $hidden, $showall ) = (0) x 3;
my $count = 0;
my %found;
my @virus;
my $start_time;
my ( $q_state, $l_state );
my ( @files,   @scanthese );
my $directory = $ENV{HOME} || getcwd();
my $c_dir     = "$directory/.clamtk";
my $v_dir     = "$c_dir/viruses";
my $l_dir     = "$c_dir/history";
my ( $a_tooltip, $authenticate );
my %dirs_scanned;

# maintenance subroutine variables below
my $new_slist;
my @q_files = ();
my $q_label;

BEGIN {
    my $encoding = setlocale(LC_CTYPE);
    $ENV{LC_ALL} = $encoding;
}

if ( $> == 0 ) {
    $authenticate = 'gtk-yes';
    $a_tooltip    = "Check for signature updates";
} else {
    $authenticate = 'gtk-no';
    $a_tooltip    = "You must be root to install updates";
}

my $FRESHPATH =
    ( -e '/usr/bin/freshclam' ) ? '/usr/bin/freshclam'
  : ( -e '/usr/local/bin/freshclam' ) ? '/usr/local/bin/freshclam'
  : die "freshclam not found!\n";
my $SIGPATH =
    ( -e '/usr/bin/sigtool' ) ? '/usr/bin/sigtool'
  : ( -e '/usr/local/bin/sigtool' ) ? '/usr/local/bin/sigtool'
  : die "sigtool not found!\n";
my $CLAMPATH =
    ( -e '/usr/bin/clamscan' ) ? '/usr/bin/clamscan'
  : ( -e '/usr/local/bin/clamscan' ) ? '/usr/local/bin/clamscan'
  : die "clamscan not found!\n";
my $RARPATH =
    ( -e '/usr/bin/unrar' ) ? '/usr/bin/unrar'
  : ( -e '/usr/local/bin/unrar' ) ? '/usr/local/bin/unrar'
  : '';
my $ZIPPATH =
    ( -e '/usr/bin/unzip' ) ? '/usr/bin/unzip'
  : ( -e '/usr/local/bin/unzip' ) ? '/usr/local/bin/unzip'
  : '';
my $CABPATH =
    ( -e '/usr/bin/cabextract' ) ? '/usr/bin/cabextract'
  : ( -e '/usr/local/bin/cabextract' ) ? '/usr/local/bin/cabextract'
  : '';
my $FILE =
    ( -e '/usr/bin/file' ) ? '/usr/bin/file'
  : ( -e '/usr/local/bin/file' ) ? '/usr/local/bin/file'
  : die "\"file\" command not found!\n";

# virus definitions path
my $DEFPATH =
    ( -e '/var/lib/clamav/daily.cvd' ) ? '/var/lib/clamav/daily.cvd'
  : ( -e '/var/clamav/daily.cvd' ) ? '/var/clamav/daily.cvd'
  : '';

if ( $RARPATH eq '' ) {
    $RARPATH =
        ( -e '/usr/bin/rar' ) ? '/usr/bin/rar'
      : ( -e '/usr/local/bin/rar' ) ? '/usr/local/bin/rar'
      : '';
}

if ( !-d $v_dir ) {
    eval { mkpath( $v_dir, 0, 0777 ); };
    if ($@) {
        $q_state = "disabled";
    } else {
        $q_state = "normal";
    }
} else {
    $q_state = "normal";
}

if ( !-d $l_dir ) {
    eval { mkpath( $l_dir, 0, 0777 ); };
    if ($@) {
        $l_state = "disabled";
    } else {
        $l_state = "normal";
    }
} else {
    $l_state = "normal";
}

my $window = Gtk2::Window->new();
$window->signal_connect( destroy => sub { Gtk2->main_quit; } );
$window->set_default_size( 680, 325 );
$window->set_title("ClamTk Virus Scanner");
$window->set_border_width(0);

my $main_vbox = Gtk2::VBox->new( FALSE, 0 );
$window->add($main_vbox);
$main_vbox->show;

my $menu_tree = create_menu();

my $menu = Gtk2::SimpleMenu->new(
    menu_tree        => $menu_tree,
    default_callback => \&return,
    user_data        => 'user data',
);

$window->add_accel_group( $menu->{accel_group} );
my $menubar = $menu->{widget};
$main_vbox->pack_start( $menubar, FALSE, FALSE, 1 );

# This are the GUI's for scanning and exiting;
# hopefully they don't make the toolbar look too busy
my $toolbar = Gtk2::Toolbar->new;
$toolbar->set_style('icons');

# We can set the size of the toolbar stuff here, but
# for now, I like the default
# $toolbar->set_icon_size('menu');
$toolbar->insert_stock( 'gtk-new', "Scan a file", undef,
    sub { getfile('file') },
    undef, -1 );
$toolbar->insert_stock(
    'gtk-open', "Scan a directory",
    undef, sub { getfile('dir') },
    undef, -1
);
$toolbar->insert_stock(
    'gtk-home', "Scan your home directory",
    undef, sub { getfile('home') },
    undef, -1
);
$toolbar->append_space;
$toolbar->insert_stock( 'gtk-clear', "Clear output", undef, \&clear_output,
    undef, -1 );
$toolbar->insert_stock(
    'gtk-stop', "Stop scanning now",
    undef, sub { @files = (); @scanthese = (); },
    undef, -1
);
$toolbar->insert_stock( 'gtk-quit', "Exit", undef, sub { Gtk2->main_quit },
    undef, -1 );
$main_vbox->pack_start( $toolbar, FALSE, FALSE, 1 );

# This is the top label where
# scanning messages are displayed
my $top_label = Gtk2::Label->new();
$main_vbox->pack_start( $top_label, FALSE, FALSE, 3 );

# This scrolled window holds the slist
my $scrolled_win = Gtk2::ScrolledWindow->new;
$scrolled_win->set_shadow_type('etched_in');
$scrolled_win->set_policy( 'automatic', 'automatic' );
$main_vbox->pack_start( $scrolled_win, TRUE, TRUE, 0 );

my $slist = create_list();
$scrolled_win->add($slist);
$slist->get_selection->set_mode('single');
$slist->set_rules_hint(TRUE);

# can't be reorderable - messes up the \&row_clicked function
$slist->set_reorderable(FALSE);
map { $_->set_sizing('autosize') } $slist->get_columns;
$slist->signal_connect( row_activated => \&row_clicked );

# tooltips - they don't show until the scanning is done;
# that's in sub cleanup
my $tooltips = Gtk2::Tooltips->new;
$tooltips->set_tip( $slist, "Double-click items for information" );
$tooltips->disable;

# bottom_box keeps track of # scanned, # of viruses, and time
my $bottom_box = Gtk2::HBox->new( FALSE, 0 );
$main_vbox->pack_start( $bottom_box, FALSE, FALSE, 1 );

my $left_status = Gtk2::Label->new("Files Scanned: ");
$left_status->set_alignment( 0.0, 0.5 );
$bottom_box->pack_start( $left_status, TRUE, TRUE, 0 );

my $mid_status = Gtk2::Label->new("Viruses Found: ");
$mid_status->set_alignment( 0.0, 0.5 );
$bottom_box->pack_start( $mid_status, TRUE, TRUE, 0 );

my $right_status = Gtk2::Label->new("Ready");
$right_status->set_alignment( 0.0, 0.5 );
$bottom_box->pack_start( $right_status, TRUE, TRUE, 0 );

$window->show_all();

# This is to combine any and all startup checks
# it will be used later to read from a pref file
startup_prefs();

Gtk2->main;

sub create_list {
    my $list = Gtk2::SimpleList->new(
        'File'   => 'text',
        'Type'   => 'text',
        'Size'   => 'text',
        'Status' => 'text',
    );

    return $list;
}

sub getfile {
    my $option = shift;

    # $option will be either "home", "file", "dir", or "recur"
    $window->queue_draw;
    $tooltips->disable;
    clear_output();
    chdir($directory) or chdir("/tmp");

    my ( $filename, $dir, $dialog );
    if ( $option eq "home" ) {
        my $home = $ENV{HOME};
        @files = File::Find::Rule->file()->readable->maxdepth(1)->in($home);
    } elsif ( $option eq "file" ) {
        $dialog = Gtk2::FileChooserDialog->new(
            'Select a File', undef, 'open',
            'gtk-cancel' => 'cancel',
            'gtk-ok'     => 'ok',
        );

        if ( "ok" eq $dialog->run ) {
            $filename = $dialog->get_filename;
            $dialog->destroy;
            push( @files, $filename );
        } else {
            $dialog->destroy;
            return;
        }
    } elsif ( $option eq "dir" ) {
        $dialog = Gtk2::FileChooserDialog->new(
            'Select a Directory (directory scan)', undef, 'select-folder',
            'gtk-cancel' => 'cancel',
            'gtk-ok'     => 'ok',
        );

        if ( "ok" eq $dialog->run ) {
            $dir = $dialog->get_filename;
            $dialog->destroy;
            @files = File::Find::Rule->file()->readable->maxdepth(1)->in($dir);
        } else {
            $dialog->destroy;
            return;
        }
    } elsif ( $option eq "recur" ) {
        $dialog = Gtk2::FileChooserDialog->new(
            'Select a Directory (recursive scan)', undef, 'select-folder',
            'gtk-cancel' => 'cancel',
            'gtk-ok'     => 'ok'
        );

        if ( "ok" eq $dialog->run ) {
            $dir = $dialog->get_filename;
            $dialog->destroy;
            @files = File::Find::Rule->file()->readable->in($dir);
        } else {
            $dialog->destroy;
            return;
        }
    } else {
        $dialog->destroy;
        return;
    }

    # start the timer - replaces the "Ready"
    $start_time = time;
    $right_status->set_text("Elapsed Time: ");

    foreach my $scanthis (@files) {
        $dirs_scanned{ dirname($scanthis) } = 1;
        next if ( basename($scanthis) =~ /^\./ ) && !$hidden;

        # next line is just a safeguard. we probably don't need it.
        # it's just easier to avoid links and sockets
        next if ( -l $scanthis || -S $scanthis );
        my $tmp  = quotemeta($scanthis);
        my $type = `$FILE -b $tmp`;
        chomp($type);
        $virus[$count]{type} = $type;
        $virus[$count]{full} = $scanthis;
        $virus[$count]{base} = basename($scanthis);
        Gtk2->main_iteration while Gtk2->events_pending;

        if ( $virus[$count]{type} =~ /RAR archive data/i ) {
            unrar($scanthis);
        } elsif ( $virus[$count]{type} =~ /Zip archive data/ ) {
            uzip($scanthis);
        } elsif ( $virus[$count]{type} =~ /Microsoft Cabinet file/ && $CABPATH )
        {
            uncab($scanthis);
        } else {
            scan();
        }
    }

    clean_up();
}

# gotta be an easier way than the next three subroutines...
sub unrar {
    my $parent = shift;
    timer();

## Check for encrypted rars ##
## this seems pointless, scanning it twice - but
## it's much easier than coming up with my own ways to
## check for encrypted and/or corrupt rars
    my $scan_obj;
    $scan_obj = quotemeta( $virus[$count]{full} );
    my $ret = `clamscan --block-encrypted --no-summary $scan_obj`;
    chomp($ret);

    if ( $ret =~ /.*?rarpwd.*?FOUND$/ || $ret =~ /.*?Encrypted.RAR FOUND$/ ) {
        $top_label->set_text("Scanning $virus[$count]{base}...");
        $left_status->set_text("Files Scanned: $count");

        $virus[$count]{size}   = size( $virus[$count]{full} );
        $virus[$count]{type}   = "RAR Encrypted File";
        $virus[$count]{status} = "Unreadable";
        display();
        return ();
    }

    if ( $RARPATH eq '' ) {
        $top_label->set_text("Scanning $virus[$count]{base}...");
        $left_status->set_text("Files Scanned: $count");

        $virus[$count]{type}   = "RAR Compressed File";
        $virus[$count]{size}   = size( $virus[$count]{full} );
        $virus[$count]{status} = "Unrar Not Found";
        display();
        return;
    }
    my $tmpdir = tempdir( UNLINK => 1, CLEANUP => 1 );
    my @args = ( $RARPATH, "e", "-y", "-inul", $virus[$count]{full}, $tmpdir );
    system(@args);
    @scanthese = glob "$tmpdir/*";

    foreach (@scanthese) {
        Gtk2->main_iteration while Gtk2->events_pending;
        if ( -d $_ ) {
            my @recur = File::Find::Rule->file()->readable->in($_);
            @recur = grep { -f } @recur;
            push( @scanthese, @recur );
            next;
        }
        $virus[$count]{full} = $_;

        # yet another file test, this time to see if it's a rar
        # this only affects the "children"
        # have to quotemeta it, due to spaces & special characters
        my $tmp_obj = quotemeta($_);
        my $tmp     = `$FILE -b $tmp_obj`;
        chomp($tmp);

        # currently not descending into other RARs...
        # we'll just tweak the status display and move on
        if ( $tmp =~ /RAR archive data/ && $parent ) {
            $virus[$count]{base} = basename( $virus[$count]{full} );
            $top_label->set_text("Scanning $virus[$count]{base}...");
            $left_status->set_text("Files Scanned: $count");

            $virus[$count]{type}   = $tmp;
            $virus[$count]{status} = "RAR within RAR";
            $virus[$count]{size}   = size( $virus[$count]{full} );
            $virus[$count]{real}   = $virus[$count]{full};
            $virus[$count]{full}   = $virus[$count]{base};
            $virus[$count]{full} .= " (in $parent)";
            display();
            next;
        }

        scan($parent);
    }
}

sub uzip {
    timer();
    my $parent = shift;

    my $scan_obj;
    $scan_obj = quotemeta( $virus[$count]{full} );
    my $ret = `clamscan --block-encrypted --no-summary $scan_obj`;
    chomp($ret);

    if ( $ret =~ /.*?zippwd.*?FOUND$/ || $ret =~ /.*?Encrypted.Zip FOUND$/i ) {
        $top_label->set_text("Scanning $virus[$count]{base}...");
        $left_status->set_text("Files Scanned: $count");

        $virus[$count]{type}   = "Zip Encrypted File";
        $virus[$count]{status} = "Unreadable";
        $virus[$count]{size}   = size( $virus[$count]{full} );
        display();
        return;
    }
    if ( $ZIPPATH eq '' ) {
        $virus[$count]{type}   = "Zip Compressed File";
        $virus[$count]{status} = "Unzip Not Found";
        $virus[$count]{size}   = size( $virus[$count]{full} );
        $top_label->set_text("Scanning $virus[$count]{base}...");
        $left_status->set_text("Files Scanned: $count");

        display();
        return;
    }
    my $tmpdir = tempdir( UNLINK => 1, CLEANUP => 1 );
    my @args = ( $ZIPPATH, "-qq", $virus[$count]{full}, "-d", $tmpdir );
    system(@args);
    @scanthese = <$tmpdir/*>;
    foreach (@scanthese) {
        Gtk2->main_iteration while Gtk2->events_pending;
        if ( -d $_ ) {
            my @recur = File::Find::Rule->file()->readable->in($_);
            push( @scanthese, @recur );
            next;
        }
        $virus[$count]{full} = $_;

        # have to quotemeta it, due to spaces & special characters
        my $tmp_obj = quotemeta($_);
        my $tmp     = `$FILE -b $tmp_obj`;
        chomp($tmp);

        # currently not descending into other RARs...
        # we'll just tweak the status display and move on
        if ( $tmp =~ /RAR archive data/ && $parent ) {
            $top_label->set_text("Scanning $virus[$count]{base}...");
            $left_status->set_text("Files Scanned: $count");

            $virus[$count]{type}   = $tmp;
            $virus[$count]{status} = "RAR within RAR";
            $virus[$count]{size}   = size( $virus[$count]{full} );
            $virus[$count]{real}   = $virus[$count]{full};
            $virus[$count]{base}   = basename( $virus[$count]{full} );
            $virus[$count]{full}   = $virus[$count]{base};
            $virus[$count]{full} .= " (in $parent)";
            display();
            next;
        }
        scan($parent);
    }
}

sub uncab {
    my $parent = shift;
    timer();
    my @scanthese;
    my $tmpdir = tempdir( UNLINK => 1, CLEANUP => 1 );
    my @args = ( $CABPATH, "-q", $virus[$count]{full}, "-d", $tmpdir );
    system(@args);
    @scanthese = glob "$tmpdir/*";

    foreach (@scanthese) {
        Gtk2->main_iteration while Gtk2->events_pending;
        if ( -d $_ ) {
            my @recur = File::Find::Rule->file()->readable->in($_);
            @recur = grep { -f } @recur;
            push( @scanthese, @recur );
            next;
        }
        $virus[$count]{full} = $_;

        # yet another file test, this time to see if it's a rar
        # this only affects the "children"
        # have to quotemeta it, due to spaces & special characters
        my $tmp_obj = quotemeta($_);
        my $tmp     = `$FILE -b $tmp_obj`;
        chomp($tmp);

        # currently not descending into other RARs...
        # we'll just tweak the status display and move on
        if ( $tmp =~ /RAR archive data/ && $parent ) {
            $virus[$count]{base} = basename( $virus[$count]{full} );
            $top_label->set_text("Scanning $virus[$count]{base}...");
            $left_status->set_text("Files Scanned: $count");

            $virus[$count]{type}   = $tmp;
            $virus[$count]{status} = "RAR within CAB";
            $virus[$count]{size}   = size( $virus[$count]{full} );
            $virus[$count]{real}   = $virus[$count]{full};
            $virus[$count]{full}   = $virus[$count]{base};
            $virus[$count]{full} .= " (in $parent)";
            display();
            next;
        }

        scan($parent);
    }
}

sub scan {
    my $parent = shift;
    timer();
    my $scan_obj;
    $scan_obj            = quotemeta( $virus[$count]{full} );
    $virus[$count]{real} = $virus[$count]{full};
    $virus[$count]{base} = basename( $virus[$count]{full} );

    my $tmp = `$FILE -b $scan_obj`;
    chomp($tmp);

    # this is an attempt to shorten some file descriptions
    $tmp =~ s/(.*?),.*$/$1/;    # remove 1st comma and on
    $tmp =~ s/(MS Windows) PE.*?(\w+ executable).*?/$1 $2/i;   # these are long!
    $tmp =~ s/(.*?executable).*$/$1/i;    # remove after word 'executable'
    $tmp =~ s/(.*? text).*$/$1/i;         # good for URL descriptors
    $tmp =~ s/Intel 80386\s//g;
    $virus[$count]{type} = $tmp;
    $virus[$count]{size} = size( $virus[$count]{full} );
    $virus[$count]{real} = $virus[$count]{full};
    $virus[$count]{full} =~ s/\s*//g;

    if ($parent) {
        $virus[$count]{full} =~ s/.*\///;
        if ( length( $virus[$count]{full} ) > 17 ) {
            $virus[$count]{full} = substr( $virus[$count]{full}, 0, 17 );
            $virus[$count]{full} .= "(...)";
        }
        $virus[$count]{full} .= " (in $parent)";
    }

    if ( length( $virus[$count]{base} ) > 20 ) {
        $virus[$count]{base} = substr( $virus[$count]{base}, 0, 20 );
        $virus[$count]{base} .= "(...)";
    }

    if ( $virus[$count]{size} =~ /(\d{1,3}).*?MB/ ) {
        if ( $1 > 15 ) {
            $virus[$count]{status} = "**Large File Size**";
            display();
            return;
        }
    }

    $top_label->set_text("Scanning $virus[$count]{base}...");
    $left_status->set_text("Files Scanned: $count");
    timer();
    my @words = ( "OK", "Empty file", "Error: Not supported file type" );
    my $clean = join( '|', @words );

    # finally, the actual scan.
    my $scan = `$CLAMPATH --no-summary $scan_obj`;
    chomp($scan);
    my $status;
    if ( $scan =~ /$clean$/i ) {
        $status = "Clean";
    } else {
        ( $status = $scan ) =~ s/.*: (.*?)\s+FOUND$/$1/;
    }
    $virus[$count]{status} = $status;

    if ( $status !~ /^(Clean|Error|Unreadable)/ ) {
        if ($parent) {
            $found{$parent} = $status;
        } else {
            $found{ $virus[$count]{full} } = $status;
        }
        if ( $ren eq "Quarantine" ) {
            if ($parent) {
                move( $parent, $v_dir ) or system( "mv", $parent, $v_dir );
            } else {
                move( $virus[$count]{real}, $v_dir )
                  or system( "mv", $virus[$count]{real}, $v_dir );
            }
        } elsif ( $ren eq "Delete" ) {
            if ($parent) {
                unlink($parent);
            } else {
                unlink( $virus[$count]{real} );
            }
        }
        my $num_so_far = scalar keys(%found);
        $left_status->set_text("Files Scanned: $count");
        $mid_status->set_markup("<b>Viruses Found: $num_so_far</b>");
    }
    display();

}

sub display {
    use encoding 'utf8';
    $virus[$count]{type}   =~ s/(.*)\s+$/$1/;
    $virus[$count]{status} =~ s/(.*)\s+$/$1/;
    if ( $virus[$count]{status} ne "Clean" || $showall ) {
        push @{ $slist->{data} },
          [
            $virus[$count]{base}, $virus[$count]{type},
            $virus[$count]{size}, $virus[$count]{status}
          ];
    }
    $count++;
    timer();
}

sub timer {
    my $now     = time;
    my $seconds = $now - $start_time;
    my $s       = sprintf "%02d", ( $seconds % 60 );
    my $m       = sprintf "%02d", ( $seconds - $s ) / 60;
    $right_status->set_text("Elapsed time: $m:$s");
    $window->queue_draw;
}

sub size {
    my $file = shift;
    my $size = -s $file;
    if ( $size == 0 ) {
        $size = "empty";
    } elsif ( $size < 1024 ) {
        $size = "< 1 KB";
    } elsif ( $size < 1000 * 1000 ) {
        $size = $size / 1000;
        $size = sprintf "%.2f", ($size);
        $size .= " KB";
    } else {
        my $kb = $size / 1000;
        my $mb = $kb / 1000;
        $size = sprintf "%.2f", ($mb);
        $size .= " MB";
    }
    return $size;
}

sub clean_up {
    $count ||= 0;
    $tooltips->enable;

    my $num_found = scalar keys(%found);
    my $db_total  = num_of_sigs();
    if ($save_log) {
        my ( $mon, $day, $year ) = split / /, strftime( '%b %d %Y', localtime );
        $virus_log = "$mon-$day-$year" . ".log";

        # sort the directories scanned for display
        my @sorted = sort { length $a <=> length $b } keys %dirs_scanned;
        if ( open REPORT, ">>$l_dir/$virus_log" ) {
            print REPORT "\nClamTk, v$VERSION\n", scalar localtime, "\n";
            print REPORT "Current ClamAV Signatures: $db_total\n";
            print REPORT "Directories Scanned:\n";
            for my $list (@sorted) {
                print REPORT "$list\n";
            }
            print REPORT
              "\nFound $num_found possible viruses ($count files scanned).\n\n";
        } else {
            $top_label->set_text(
                "Could not write to logfile. Check permissions.");
            $save_log = 0;
        }

    }
    $db_total =~ s/(\w+)\s+$/$1/;
    $top_label->set_text("Scanning complete ($db_total signatures)");
    $left_status->set_text("Files Scanned: $count");
    if ( $num_found == 0 ) {
        $mid_status->set_text("Viruses Found: $num_found");
    }
    $right_status->set_text("Ready");
    $window->queue_draw;

    if ( $num_found == 0 ) {
        print REPORT "No viruses found.\n" if ($save_log);
    } else {
        if ($save_log) {
            while ( ( $key, $value ) = each %found ) {
                write(REPORT);
            }
        }
    }
    if ($save_log) {
        print REPORT "-" x 76, "\n";
        close(REPORT);
    }

    #change the quarantined files
    my @defang = glob "$v_dir/*";
    if (@defang) {
        foreach my $mod (@defang) {
            next if ( $mod =~ /\.VIRUS$/ );
            chmod 0600, $mod;
            rename( $mod, "$mod.VIRUS" ) or system( "mv", $mod, "$mod.VIRUS" );
        }
    }
    $count = 0;
    %found = ();
    @files = ();
}

format REPORT =
@<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<@>>>>>>>>>>>>>>>>>>>>>>>>>>>>
$key,                                                                 $value 
.

sub clear_output {
    return if ( scalar(@files) > 0 );
    @{ $slist->{data} } = ();
    $window->resize( 680, 325 );
    $window->queue_draw;
    $left_status->set_text("Files Scanned: ");
    $mid_status->set_text("Viruses Found: ");
    $right_status->set_text("Ready");
    $top_label->set_text("");
    $tooltips->disable;
}

sub row_clicked {
    my @sel   = $slist->get_selected_indices;
    my $deref = $sel[0];
    $top_label->set_text(
        "File: $virus[$deref]{full} Status: $virus[$deref]{status}");
}

sub about {
    my $message =
"ClamTk Virus Scanner\nVersion $VERSION\nhttp://freshmeat.net/projects/clamtk/\nhttp://clamtk.sourceforge.net\n\nCopyright (c) 2004-2005 Dave M\n(dave.nerd AT gmail DOT com)\n\nThis program is free software.\nYou may modify it and/or redistribute it\nunder the same terms as Perl itself.\n\nPlease read the DISCLAIMER, LICENSE,\nand README for more information.\n\nClamAV is available at http://www.clamav.net.\t\n";
    my $dialog =
      Gtk2::MessageDialog->new_with_markup( $window,
        [qw(modal destroy-with-parent)],
        'info', "ok", $message );

    if ( "ok" eq $dialog->run ) {
        $dialog->destroy;
        return;
    }

}

sub update {
    if ( $> != 0 ) {
        $top_label->set_text("You must be root to install updates.");
        return;
    }

    # test for connection
    require IO::Socket;
    my $socket = IO::Socket::INET->new(
        PeerAddr => 'www.google.com',
        PeerPort => 'http(80)',
        Proto    => 'tcp',
        Timeout  => 1,
    );
    unless ($socket) {
        $top_label->set_text("Update failed. Check internet connection.");
        return;
    }

    $top_label->set_text("Please wait, checking for updates...");
    Gtk2->main_iteration while Gtk2->events_pending;
    $main_vbox->queue_draw;
    $window->queue_draw;
    my @result = `$FRESHPATH --stdout`;
    my $showthis;
    if ( !@result ) {
        $top_label->set_text("Result:\n\nError!");
    } else {
        foreach my $line (@result) {
            if ( $line =~ /Database updated .(\d+) signatures/ ) {
                $showthis =
                  "Your virus signatures have been updated ($1 signatures).";
                $top_label->set_text($showthis);
                last;
            }
        }
        $top_label->set_text("Your virus signatures are up-to-date.")
          if ( !$showthis );
    }
    $window->queue_draw;
}

sub quar_check {
    return if ( !-d $v_dir );
    my @trash = glob "$v_dir/*";
    my $del   = scalar(@trash);
    if ( !$del ) {
        $top_label->set_text("No items currently quarantined.");
    } else {
        $top_label->set_text("$del item(s) currently quarantined.");
    }
}

sub del_quar {
    if ( !-d $v_dir ) {
        $top_label->set_text("No quarantine directory to empty.");
    } else {
        my @trash = glob "$v_dir/*";
        if ( scalar(@trash) == 0 ) {
            $top_label->set_text("No items to empty.");
        } else {
            my $del = 0;
            foreach (@trash) {
                unlink and $del++;
            }
            $top_label->set_text("Removed $del item(s).");
        }
    }
}

sub history {
    my $choice = shift;
    my @trash  = ();
    unless ( -e $l_dir ) {
        $top_label->set_text("History directory not found.");
        return;
    }

    my ( $filename, $dialog );
    if ( $choice eq "view" ) {
        @trash = glob "$l_dir/*.log";
        if ( scalar(@trash) == 0 ) {
            $top_label->set_text("No history logs to view.");
            return;
        }
        chdir($l_dir);
        $window->iconify;
        $dialog = Gtk2::FileChooserDialog->new(
            'Select a History File', undef, 'open',
            'gtk-cancel' => 'cancel',
            'gtk-ok'     => 'ok'
        );

        if ( "ok" eq $dialog->run ) {
            $filename = $dialog->get_filename;
            $dialog->destroy;
        } else {
            $window->deiconify;
            $dialog->destroy;
            return;
        }
        my $base = basename($filename);

        my $new_win = Gtk2::Dialog->new( "Viewing $base",
            undef, [], 'gtk-close' => 'close' );
        $new_win->set_default_response('close');
        $new_win->signal_connect(
            response => sub { $window->deiconify; $new_win->destroy } );
        $new_win->set_default_size( 650, 350 );

        my $textview = Gtk2::TextView->new;
        $textview->set( editable => FALSE );

        open( FILE, "<", $filename )
          or $top_label->set_text("Problems opening $filename...")
          and return;
        my $text;
        {
            local $/ = undef;
            $text = <FILE>;
        }
        close(FILE);

        my $textbuffer = $textview->get_buffer;
        $textbuffer->create_tag( 'mono', family => 'Monospace' );
        $textbuffer->insert_with_tags_by_name( $textbuffer->get_start_iter,
            $text, 'mono' );

        my $scroll_win = Gtk2::ScrolledWindow->new;
        $scroll_win->set_border_width(5);
        $scroll_win->set_shadow_type('etched-in');
        $scroll_win->set_policy( 'automatic', 'automatic' );

        $new_win->vbox->pack_start( $scroll_win, TRUE, TRUE, 0 );

        $scroll_win->show();
        $scroll_win->add($textview);
        $textview->show();
        $new_win->show();

    } elsif ( $choice eq "delete" ) {
        @trash = <$l_dir/*.log>;
        if ( scalar(@trash) == 0 ) {
            $top_label->set_text("No history logs to delete.");
            return;
        }
        my $confirm_message = "Really delete all history logs?";
        my $confirm         =
          Gtk2::MessageDialog->new_with_markup( $window,
            [qw(modal destroy-with-parent)],
            'question', 'ok-cancel', $confirm_message );

        if ( "cancel" eq $confirm->run ) {
            $confirm->destroy;
            return;
        }
        $confirm->destroy;
        my @not_del;
        foreach (@trash) {
            unlink($_) or push( @not_del, $_ );
        }
        if ( scalar(@not_del) >= 1 ) {
            $top_label->set_text("Could not delete files: @not_del");
        } else {
            $top_label->set_text("Successfully removed history logs.");
        }
    }

}

sub startup_prefs {

    # this is an effort to combine any
    # and all startup things, such as the upcoming prefs file

    # theoretically, this next line shouldn't be necessary
    if ( $q_state eq "disabled" || $l_state eq "disabled" ) {
        $top_label->set_text(
            "Unable to create ~/.clamtk directory - check permissions.");
    }

    date_diff();
    first_run();    # necessarily for fedora_extras rpms only
}

sub sys_info {
    my ( $info, $return, $version, $number );

    # Signature date info
    if ($DEFPATH) {
        $return = `$SIGPATH --info $DEFPATH 2>&1`;
    } else {
        $return = "(No definitions found)";
    }

    if ( $return =~ /^Build time: (\d+\s\w+\s\d{4})/ ) {
        $info = $1;
    } else {
        $info = $return;
    }

    # ClamAV version
    my $ver_info = `$CLAMPATH -V`;
    chomp($ver_info);
    ( $version = $ver_info ) =~ s/^(\w+\s+0\.\d+(?:\.\d+)).*$/$1/;

    # Number of signatures
    $number = num_of_sigs();

    my $total =
        "\nBuild: $version\t\n\n"
      . "Signatures: $number\t\n"
      . "($info)\n\n"
      . "GUI Version: $VERSION\n";
    my $dialog =
      Gtk2::MessageDialog->new_with_markup( $window,
        [qw(modal destroy-with-parent)],
        'info', "ok", $total );

    if ( "ok" eq $dialog->run ) {
        $dialog->destroy;
        return;
    }
}

sub cvd_check {
    my $return;

    # this path is something to watch
    # (/var/clamav vs. /var/lib/clamav)
    if ($DEFPATH) {
        $return = `$SIGPATH --info $DEFPATH 2>&1`;
    } else {
        $return = "(No definitions found)";
        return $1;
    }

    if ( $return =~ /^Build time: (\d+\s\w+\s\d{4})/ ) {
        return $1;
    } else {
        $top_label->set_text("Date of ClamAV Signatures: $return");
    }
}

sub date_diff {
    my ( $day1, $month1, $year1 ) = split / /,
      strftime( '%d %m %Y', localtime );
    my ( $day2, $month2, $year2 ) = split / /, cvd_check();
    unless ( $day2 && $month2 && $year2 ) {
        $top_label->set_text("Warning: No virus definitions found!");
        return;
    }
    my %months = (
        'Jan' => 1,
        'Feb' => 2,
        'Mar' => 3,
        'Apr' => 4,
        'May' => 5,
        'Jun' => 6,
        'Jul' => 7,
        'Aug' => 8,
        'Sep' => 9,
        'Oct' => 10,
        'Nov' => 11,
        'Dec' => 12,
    );
    my $diff =
      Delta_Days( $year1, $month1, $day1, $year2, $months{$month2}, $day2 );
    if ( $diff <= -5 ) {

        #$diff returns a negative number, so...
        $diff *= -1;
        my $warning = "Warning! Signatures are $diff days old!";
        my $dialog  =
          Gtk2::MessageDialog->new_with_markup( $window,
            [qw(modal destroy-with-parent)],
            'warning', "ok", $warning );

        if ( "ok" eq $dialog->run ) {
            $dialog->destroy;
            return;
        }
    } else {
        return;
    }
}

sub first_run {
    return if ( -e "$c_dir/first_run" || $> != 0 );
    my $warning =
"This is a rebuild for Fedora Extras.\nUnlike Dag's ClamAV rpms, the Extras rpms do not\nautomatically edit freshclam.conf and clamd.conf under /etc.\nPlease edit those before attempting signature updates.\n";
    my $dialog =
      Gtk2::MessageDialog->new_with_markup( $window,
        [qw(modal destroy-with-parent)],
        'warning', "ok", $warning );
    open( FILE, ">", "$c_dir/first_run" )
      or warn "Couldn't create 'first_run' file...\n";
    close(FILE);

    if ( "ok" eq $dialog->run ) {
        $dialog->destroy;
        return;
    }
}

sub default_check {
    my $action = shift;
    if ( $action eq "ScanHidden" ) {
        $hidden ^= 1;
    }
    if ( $action eq "ShowAll" ) {
        $showall ^= 1;
    }
    if ( $action eq "SaveLog" ) {
        $save_log ^= 1;
    }
}

sub action_actions {
    my ( $previous, $new ) = @_;

    # $previous is the old value; $new is the new value :)
    if ( $new == 1 ) {
        $ren = "None";
    } elsif ( $new == 2 ) {
        $ren = "Quarantine";
    } elsif ( $new == 3 ) {
        $ren = "Delete";
    }
}

sub num_of_sigs {
    my $total;
    if ($SIGPATH) {
        $total = `$SIGPATH -l |wc -l 2>&1`;
        chomp($total);
        return $total;
    } else {
        return "Unknown";
    }
}

sub maintenance {
    my $new_win = Gtk2::Window->new;
    $new_win->signal_connect( destroy => sub { $new_win->destroy; } );
    $new_win->set_default_size( 200, 200 );
    $new_win->set_title("Quarantine");

    my $new_vbox = Gtk2::VBox->new;
    $new_win->add($new_vbox);

    @q_files = glob "$v_dir/*";
    my $s_win = Gtk2::ScrolledWindow->new;
    $s_win->set_shadow_type('etched-in');
    $s_win->set_policy( 'automatic', 'automatic' );
    $new_vbox->pack_start( $s_win, TRUE, TRUE, 0 );

    $new_slist = Gtk2::SimpleList->new( 'File' => 'text', );
    $s_win->add($new_slist);

    my $new_hbox = Gtk2::HBox->new;
    $new_vbox->pack_start( $new_hbox, FALSE, FALSE, 0 );

    my $false_pos = Gtk2::Button->new_with_label("False Positive");
    $new_hbox->pack_start( $false_pos, TRUE, TRUE, 0 );
    $false_pos->signal_connect( clicked => \&false_pos, "false_pos" );
    my $del_pos = Gtk2::Button->new_with_label("Delete");
    $new_hbox->pack_start( $del_pos, TRUE, FALSE, 0 );
    $del_pos->signal_connect( clicked => \&del_pos, "false_pos" );
    my $pos_quit = Gtk2::Button->new_with_label("Done");
    $new_hbox->pack_start( $pos_quit, TRUE, FALSE, 0 );
    $pos_quit->signal_connect( clicked => sub { $new_win->destroy } );

    $q_label = Gtk2::Label->new();
    $new_vbox->pack_start( $q_label, FALSE, FALSE, 2 );

    for my $opt (@q_files) {
        push @{ $new_slist->{data} }, basename($opt);
    }
    $new_win->show_all;
}

sub false_pos {
    my @sel = $new_slist->get_selected_indices;
    return if ( !@sel );
    my $deref = $sel[0];
    return if ( not exists $q_files[$deref] );
    my $base = basename( $q_files[$deref] );
    move( $q_files[$deref], $directory )
      or system( "mv", $q_files[$deref], $directory );
    my $new_name = $base;
    $new_name =~ s/.VIRUS$//;
    rename( "$directory/$base", "$directory/$new_name" );

    if ( -e $q_files[$deref] ) {
        $q_label->set_text("Unable to move $base.");
        return;
    }
    splice @{ $new_slist->{data} }, $deref, 1;
    $q_label->set_text("$base moved to home directory.");
    @q_files = glob "$v_dir/*";
}

sub del_pos {
    my @sel = $new_slist->get_selected_indices;
    return if ( !@sel );
    my $deref = $sel[0];
    return if ( not exists $q_files[$deref] );
    my $base = basename( $q_files[$deref] );
    unlink $q_files[$deref];
    if ( -e $q_files[$deref] ) {
        $q_label->set_text("Unable to delete $base.");
        return;
    }

    splice @{ $new_slist->{data} }, $deref, 1;
    $q_label->set_text("Deleted $base.");
    @q_files = glob "$v_dir/*";
}

sub create_menu {
    my $tree = [
        _File => {
            item_type => '<Branch>',
            children  => [
                'Scan _File' => {
                    callback    => sub { getfile('file') },
                    accelerator => '<ctrl>F',
                    item_type   => '<StockItem>',
                    extra_data  => 'gtk-new',
                },
                'Scan _Directory' => {
                    callback    => sub { getfile('dir') },
                    accelerator => '<ctrl>D',
                    item_type   => '<StockItem>',
                    extra_data  => 'gtk-directory',
                },
                '_Recursive Scan' => {
                    callback    => sub { getfile('recur') },
                    accelerator => '<ctrl>R',
                    item_type   => '<StockItem>',
                    extra_data  => 'gtk-open',
                },
                _Exit => {
                    callback    => sub { Gtk2->main_quit; },
                    accelerator => '<ctrl>X',
                    item_type   => '<StockItem>',
                    extra_data  => 'gtk-quit',
                },
            ],
        },
        _View => {
            item_type => '<Branch>',
            children  => [
                'View Histories'   => { callback => sub { history('view') }, },
                'Delete Histories' => {
                    callback   => sub { history('delete') },
                    item_type  => '<StockItem>',
                    extra_data => 'gtk-delete',
                },
                'Clear _Output' => {
                    callback    => \&clear_output,
                    accelerator => '<ctrl>O',
                    item_type   => '<StockItem>',
                    extra_data  => 'gtk-clear',
                },
            ],
        },
        _Options => {
            item_type => '<Branch>',
            children  => [
                'Save to Log' => {
                    callback      => \&default_check,
                    callback_data => 'SaveLog',
                    accelerator   => 'F1',
                    item_type     => '<CheckItem>',
                },
                'Scan Hidden Files (.*)' => {
                    callback      => \&default_check,
                    callback_data => 'ScanHidden',
                    accelerator   => 'F2',
                    item_type     => '<CheckItem>',
                },
                'Show ALL Files' => {
                    callback      => \&default_check,
                    callback_data => 'ShowAll',
                    accelerator   => 'F3',
                    item_type     => '<CheckItem>',
                },
            ],
        },
        _Actions => {
            item_type => '<Branch>',
            children  => [
                'No Action (default)' => {
                    callback        => \&action_actions,
                    callback_action => 1,
                    item_type       => '<RadioItem>',
                    groupid         => 1,
                },
                'Quarantine' => {
                    callback        => \&action_actions,
                    callback_action => 2,
                    item_type       => '<RadioItem>',
                    groupid         => 1,
                },
                'Delete' => {
                    callback        => \&action_actions,
                    callback_action => 3,
                    item_type       => '<RadioItem>',
                    groupid         => 1,
                },
            ],
        },
        _Quarantine => {
            item_type => '<Branch>',
            children  => [
                _Status      => { callback => \&quar_check },
                _Maintenance => {
                    callback   => \&maintenance,
                    item_type  => '<StockItem>',
                    extra_data => 'gtk-preferences',
                },
                _Empty => {
                    callback   => \&del_quar,
                    item_type  => '<StockItem>',
                    extra_data => 'gtk-delete',
                }
            ],
        },
        _Help => {
            item_type => '<Branch>',
            children  => [
                '_System Information' => {
                    callback    => \&sys_info,
                    accelerator => '<ctrl>S',
                    item_type   => '<StockItem>',
                    extra_data  => 'gtk-properties',
                },
                '_Update Signatures' => {
                    callback    => \&update,
                    accelerator => '<ctrl>U',
                    item_type   => '<StockItem>',
                    extra_data  => $authenticate,
                },
                _About => {
                    callback    => \&about,
                    accelerator => '<ctrl>A',
                    item_type   => '<StockItem>',
                    extra_data  => 'gtk-about',
                },
            ],
        },
    ];

    return $tree;
}
