# ClamTk, copyright (C) 2004-2024 Dave M # # This file is part of ClamTk # https://github.com/dave-theunsub/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;