mirror of
https://github.com/dave-theunsub/clamtk.git
synced 2026-01-04 20:57:58 -05:00
405 lines
11 KiB
Perl
405 lines
11 KiB
Perl
# ClamTk, copyright (C) 2004-2021 Dave M
|
|
#
|
|
# This file is part of ClamTk
|
|
# (https://gitlab.com/dave_m/clamtk/).
|
|
#
|
|
# ClamTk is free software; you can redistribute it and/or modify it
|
|
# under the terms of either:
|
|
#
|
|
# a) the GNU General Public License as published by the Free Software
|
|
# Foundation; either version 1, or (at your option) any later version, or
|
|
#
|
|
# b) the "Artistic License".
|
|
package ClamTk::Quarantine;
|
|
|
|
use Glib 'TRUE', 'FALSE';
|
|
|
|
# use strict;
|
|
# use warnings;
|
|
$| = 1;
|
|
|
|
use POSIX 'locale_h';
|
|
use File::Basename 'basename', 'dirname';
|
|
use File::Copy 'move';
|
|
use Digest::SHA 'sha256_hex';
|
|
use Locale::gettext;
|
|
use Encode 'decode';
|
|
|
|
use constant ROW => 0;
|
|
use constant FILE => 1;
|
|
|
|
my $liststore;
|
|
|
|
binmode( STDIN, ':utf8' );
|
|
binmode( STDOUT, ':utf8' );
|
|
|
|
sub show_window {
|
|
my $box = Gtk3::Box->new( 'vertical', 5 );
|
|
$box->set_homogeneous( FALSE );
|
|
|
|
my $sw = Gtk3::ScrolledWindow->new( undef, undef );
|
|
$sw->set_policy( 'automatic', 'automatic' );
|
|
$sw->set_vexpand( TRUE );
|
|
$box->pack_start( $sw, TRUE, TRUE, 5 );
|
|
|
|
#<<<
|
|
$liststore = Gtk3::ListStore->new(
|
|
'Glib::Int',
|
|
'Glib::String',
|
|
);
|
|
#>>>
|
|
|
|
my $view = Gtk3::TreeView->new_with_model( $liststore );
|
|
$view->set_rules_hint( TRUE );
|
|
|
|
my $column = Gtk3::TreeViewColumn->new_with_attributes(
|
|
_( 'Number' ),
|
|
Gtk3::CellRendererText->new,
|
|
text => ROW,
|
|
);
|
|
$column->set_sort_column_id( 0 );
|
|
$column->set_sizing( 'fixed' );
|
|
$column->set_expand( TRUE );
|
|
$column->set_resizable( TRUE );
|
|
$view->append_column( $column );
|
|
|
|
$column = Gtk3::TreeViewColumn->new_with_attributes(
|
|
_( 'File' ),
|
|
Gtk3::CellRendererText->new,
|
|
text => FILE,
|
|
);
|
|
$column->set_sort_column_id( 1 );
|
|
$column->set_sizing( 'fixed' );
|
|
$column->set_expand( TRUE );
|
|
$column->set_resizable( TRUE );
|
|
$view->append_column( $column );
|
|
$sw->add( $view );
|
|
|
|
my $viewbar = Gtk3::Toolbar->new;
|
|
$box->pack_start( $viewbar, FALSE, FALSE, 5 );
|
|
$viewbar->set_style( 'both-horiz' );
|
|
|
|
my $button = Gtk3::ToolButton->new();
|
|
my $use_image = ClamTk::Icons->get_image( 'edit-undo' );
|
|
$button->set_icon_name( $use_image );
|
|
$button->set_label( _( 'Restore' ) );
|
|
$viewbar->insert( $button, -1 );
|
|
$button->set_is_important( TRUE );
|
|
$button->signal_connect( clicked => \&restore, $view );
|
|
|
|
my $v_sep = Gtk3::SeparatorToolItem->new;
|
|
$v_sep->set_draw( FALSE );
|
|
$v_sep->set_expand( TRUE );
|
|
$viewbar->insert( $v_sep, -1 );
|
|
|
|
$button = Gtk3::ToolButton->new();
|
|
$use_image = ClamTk::Icons->get_image( 'edit-delete' );
|
|
$button->set_icon_name( $use_image );
|
|
$button->set_label( _( 'Delete' ) );
|
|
$viewbar->insert( $button, -1 );
|
|
$button->set_is_important( TRUE );
|
|
$button->signal_connect( clicked => \&delete, $view );
|
|
|
|
push_viruses();
|
|
|
|
$box->show_all;
|
|
return $box;
|
|
}
|
|
|
|
sub delete {
|
|
my ( $toolbutton, $treeview ) = @_;
|
|
|
|
my $selected = $treeview->get_selection;
|
|
my ( $model, $iter ) = $selected->get_selected;
|
|
return unless $iter;
|
|
|
|
my $row = $model->get_value( $iter, ROW );
|
|
my $viruses = get_and_sort_viruses();
|
|
my $file = $viruses->{ $row }->{ basename };
|
|
my $fullname = $viruses->{ $row }->{ fullname };
|
|
|
|
# Get confirmation about deletion
|
|
#<<<
|
|
my $question = sprintf(
|
|
_( 'Really delete this file (%s) ?' ),
|
|
$file,
|
|
);
|
|
|
|
my $message
|
|
= Gtk3::MessageDialog->new(
|
|
undef,
|
|
[ qw| modal destroy-with-parent | ],
|
|
'question',
|
|
'ok-cancel',
|
|
$question,
|
|
);
|
|
#>>>
|
|
|
|
if ( 'ok' eq $message->run ) {
|
|
$message->destroy;
|
|
unlink( $fullname ) or do {
|
|
warn "Unable to delete >$fullname<: $!\n";
|
|
return FALSE;
|
|
};
|
|
$model->clear;
|
|
push_viruses();
|
|
return TRUE;
|
|
} else {
|
|
$message->destroy;
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
sub restore {
|
|
my ( $toolbutton, $treeview ) = @_;
|
|
|
|
my $selected = $treeview->get_selection;
|
|
my ( $model, $iter ) = $selected->get_selected;
|
|
return unless $iter;
|
|
|
|
my $row = $model->get_value( $iter, ROW );
|
|
my $viruses = get_and_sort_viruses();
|
|
my $fullname = $viruses->{ $row }->{ fullname };
|
|
my $basename = $viruses->{ $row }->{ basename };
|
|
|
|
# Current location of quarantined file
|
|
my $current_path = ClamTk::App->get_path( 'viruses' );
|
|
$current_path .= '/';
|
|
$current_path .= $basename;
|
|
|
|
# By default, the final destination will be $HOME
|
|
my $final_destination = ClamTk::App->get_path( 'directory' );
|
|
$final_destination .= '/';
|
|
$final_destination .= $basename;
|
|
|
|
my $current_hash = get_hash( $current_path );
|
|
|
|
# See if we have a record of this file
|
|
my ( $hopeful_path, $hopeful_mode ) = query_hash( $current_hash );
|
|
if ( $hopeful_path ) {
|
|
$final_destination = decode( 'utf8', $hopeful_path );
|
|
}
|
|
|
|
# Save-as dialog
|
|
my $dialog = Gtk3::FileChooserDialog->new(
|
|
_( 'Save file as...' ),
|
|
undef,
|
|
'save',
|
|
'gtk-cancel' => 'cancel',
|
|
'gtk-ok' => 'ok',
|
|
);
|
|
$dialog->set_current_name( $final_destination );
|
|
$dialog->set_do_overwrite_confirmation( TRUE );
|
|
|
|
if ( 'ok' eq $dialog->run ) {
|
|
$dialog->destroy;
|
|
# If we obtained permissions, apply them;
|
|
# or, 644 by default. (?)
|
|
# Unless someone has a better idea for default perms.
|
|
$hopeful_mode ||= 644;
|
|
chmod oct( $hopeful_mode ), $current_path;
|
|
move( $current_path, $final_destination ) or do {
|
|
warn "Unable to move >$current_path< to ",
|
|
">", $final_destination, "<\n";
|
|
return FALSE;
|
|
};
|
|
if ( remove_hash( undef, $current_hash ) ) {
|
|
$liststore->clear;
|
|
push_viruses();
|
|
return TRUE;
|
|
} else {
|
|
warn "error removing file >$fullname< from restore file: $!\n";
|
|
$liststore->clear;
|
|
push_viruses();
|
|
return FALSE;
|
|
}
|
|
} else {
|
|
$dialog->destroy;
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
sub get_and_sort_viruses {
|
|
# Where we hold the quarantined stuff
|
|
my $path = ClamTk::App->get_path( 'viruses' );
|
|
|
|
my $hash;
|
|
my $i = 1;
|
|
for my $virus ( glob "$path/*" ) {
|
|
$hash->{ $i }->{ fullname } = decode( 'utf8', $virus );
|
|
$hash->{ $i }->{ basename } = decode( 'utf8', basename( $virus ) );
|
|
$hash->{ $i }->{ number } = $i;
|
|
$i++;
|
|
}
|
|
return $hash;
|
|
}
|
|
|
|
sub push_viruses {
|
|
my $hash = shift;
|
|
|
|
# If we weren't given a hash of info,
|
|
# go and get it:
|
|
if ( !$hash ) {
|
|
$hash = get_and_sort_viruses();
|
|
}
|
|
|
|
#<<<
|
|
for my $key ( sort { $a <=> $b } keys %$hash ) {
|
|
my $iter = $liststore->append;
|
|
$liststore->set(
|
|
$iter,
|
|
0, $hash->{ $key }->{ number },
|
|
1, $hash->{ $key }->{ basename },
|
|
);
|
|
}
|
|
#<<<
|
|
}
|
|
|
|
sub query_hash {
|
|
# See if queried file exists in 'restore' file;
|
|
# this might have to change to allow for querying
|
|
# by hash or fullpath
|
|
my $filehash = shift;
|
|
|
|
my $restore_path = ClamTk::App->get_path( 'restore' );
|
|
#open( my $F, '<:encoding(UTF-8)', $restore_path ) or do {
|
|
open( my $F, '<', $restore_path ) or do {
|
|
warn "Can't open restore file for reading: $!\n";
|
|
return FALSE;
|
|
};
|
|
|
|
my $current_list;
|
|
while ( <$F> ) {
|
|
chomp;
|
|
my ( $qhash, $qpath, $qmode ) = split /:/;
|
|
$current_list->{ $qhash }->{ hash } = $qhash;
|
|
$current_list->{ $qhash }->{ path } = $qpath;
|
|
$current_list->{ $qhash }->{ mode } = $qmode;
|
|
|
|
# See if it already exists in the restore file
|
|
if ( $current_list->{ $qhash }->{ hash } eq $filehash ) {
|
|
close( $F );
|
|
return (
|
|
$current_list->{ $qhash }->{ path },
|
|
$current_list->{ $qhash }->{ mode }
|
|
);
|
|
}
|
|
}
|
|
close( $F );
|
|
return FALSE;
|
|
}
|
|
|
|
sub add_hash {
|
|
# Add quarantined file to 'restore' file
|
|
my ( $pkg_name, $file, $permissions ) = @_;
|
|
my $hash = get_hash( $file );
|
|
|
|
my $restore_path = ClamTk::App->get_path( 'restore' );
|
|
open( my $F, '<:encoding(UTF-8)', $restore_path ) or do {
|
|
warn "Can't open restore file for reading: $!\n";
|
|
return FALSE;
|
|
};
|
|
|
|
my $current_list;
|
|
while ( <$F> ) {
|
|
chomp;
|
|
next if ( /^\s*$/ );
|
|
my ( $qhash, $qpath, $qmode ) = split /:/;
|
|
$current_list->{ $qhash }->{ hash } = $qhash;
|
|
$current_list->{ $qhash }->{ path } = $qpath;
|
|
$current_list->{ $qhash }->{ mode } = $qmode;
|
|
|
|
# See if it already exists in the restore file
|
|
if ( $current_list->{ $qhash }->{ hash } eq $hash
|
|
|| $current_list->{ $qhash }->{ path } eq $file )
|
|
{
|
|
return FALSE;
|
|
}
|
|
}
|
|
close( $F );
|
|
|
|
# Rewrite restore file with new addition
|
|
open( $F, '>:encoding(UTF-8)', $restore_path ) or do {
|
|
# open( $F, '>', $restore_path ) or do {
|
|
warn "Can't open restore file for reading: $!\n";
|
|
return FALSE;
|
|
};
|
|
|
|
for my $key ( keys %$current_list ) {
|
|
print $F join( ':',
|
|
$current_list->{ $key }->{ hash },
|
|
$current_list->{ $key }->{ path },
|
|
$current_list->{ $key }->{ mode },
|
|
);
|
|
print $F "\n";
|
|
}
|
|
print $F $hash, ':', $file, ':', $permissions, "\n";
|
|
close( $F );
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
sub remove_hash {
|
|
# Remove quarantined file from 'restore' file
|
|
my ( $pkg_name, $hash ) = @_;
|
|
|
|
my $restore_path = ClamTk::App->get_path( 'restore' );
|
|
#open( my $F, '<:encoding(UTF-8)', $restore_path ) or do {
|
|
open( my $F, '<', $restore_path ) or do {
|
|
warn "Can't open restore file for reading: $!\n";
|
|
return FALSE;
|
|
};
|
|
|
|
my $current_list;
|
|
while ( <$F> ) {
|
|
chomp;
|
|
my ( $qhash, $qpath, $qmode ) = split /:/;
|
|
$current_list->{ $qhash }->{ hash } = $qhash;
|
|
$current_list->{ $qhash }->{ path } = $qpath;
|
|
$current_list->{ $qhash }->{ mode } = $qmode;
|
|
}
|
|
close( $F );
|
|
|
|
warn "no files listed in restore file!\n"
|
|
and return FALSE
|
|
unless scalar keys %$current_list;
|
|
|
|
# Rewrite restore file
|
|
#open( $F, '>:encoding(UTF-8)', $restore_path ) or do {
|
|
open( $F, '>', $restore_path ) or do {
|
|
warn "Can't open restore file for reading: $!\n";
|
|
return FALSE;
|
|
};
|
|
|
|
# Next over the one we're removing
|
|
for my $key ( keys %$current_list ) {
|
|
next if ( $current_list->{ $key }->{ hash } eq $hash );
|
|
print $F join( ':',
|
|
$current_list->{ $key }->{ hash },
|
|
$current_list->{ $key }->{ path },
|
|
$current_list->{ $key }->{ mode },
|
|
);
|
|
print $F "\n";
|
|
}
|
|
close( $F );
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
sub get_hash {
|
|
my $file = shift;
|
|
|
|
my $slurp = do {
|
|
local $/ = undef;
|
|
open( my $f, '<', $file ) or do {
|
|
warn "Unable to open >$file< for hashing: $!\n";
|
|
return;
|
|
};
|
|
binmode( $f );
|
|
<$f>;
|
|
};
|
|
return sha256_hex( $slurp );
|
|
}
|
|
|
|
1;
|