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

use Glib 'TRUE', 'FALSE';

# use strict;
# use warnings;
$| = 1;

use LWP::UserAgent;
use LWP::Protocol::https;
use MIME::Base64 'decode_base64';
use Digest::SHA 'sha256_hex';
use File::Basename 'basename';
use File::Copy 'move';
use Text::CSV;
use JSON;
use Encode 'encode';

use POSIX 'locale_h';
use Locale::gettext;

binmode STDIN, ":encoding(UTF-8)";

my $store;       # Gtk2::ListStore for results
my $model;       # Gtk2::ListStore for combobox
my $combobox;    # Gtk2::ComboBox for previous analysis
my $nb;          # Gtk2::Notebook
my $filename = '';    # name of file analyzed
my $bar;              # Gtk2::InfoBar
my $window;           # Gtk2::Dialog; global for queue_draw;
my $submitted = 0;    # Boolean value to keep track if file
                      # has been submitted or not
my $from_scan;

sub show_window {
    ( undef, $from_scan, $parent ) = @_;
    $window = Gtk2::Dialog->new(
        _( 'Analysis' ),
        $parent, [ qw| modal destroy-with-parent no-separator | ],
    );
    $window->signal_connect(
        destroy => sub {
            $window->destroy;
            1;
        }
    );
    $window->set_border_width( 5 );
    $window->set_default_size( 450, 250 );
    $window->set_position( 'mouse' );

    my $box = Gtk2::VBox->new( FALSE, 5 );
    $window->get_content_area->add( $box );

    $nb = Gtk2::Notebook->new;
    $box->pack_start( $nb, TRUE, TRUE, 0 );

    #<<<
    # Add initial page (select file, etc)
    $nb->insert_page(
        page_one(),
        Gtk2::Label->new( _( 'File Analysis' ) ),
        0,
    );

    # Add results page
    $nb->insert_page(
        page_two(),
        Gtk2::Label->new( _( 'Results' ) ),
        1,
    );
    #>>>

    $bar = Gtk2::InfoBar->new;
    $box->pack_start( $bar, FALSE, FALSE, 0 );
    $bar->set_message_type( 'other' );
    $bar->add_button( 'gtk-close', -7 );
    $bar->signal_connect(
        response => sub {
            $submitted = 0;
            $window->destroy;
        }
    );

    $window->show_all;
    $window->run;
    $window->destroy;
    1;
}

sub page_one {
    # Analysis page
    my $box = Gtk2::VBox->new( FALSE, 5 );

    my $separator = Gtk2::HSeparator->new;

    $box->pack_start( analysis_frame_one(), FALSE, FALSE, 5 );
    $box->pack_start( $separator,           FALSE, FALSE, 5 );
    $box->pack_start( analysis_frame_two(), FALSE, FALSE, 5 );

    return $box;
}

sub page_two {
    # Results page
    my $box = Gtk2::VBox->new( FALSE, 0 );

    my $sw = Gtk2::ScrolledWindow->new;
    $sw->set_policy( 'never', 'automatic' );
    $box->pack_start( $sw, TRUE, TRUE, 0 );

    use constant VENDOR => 0;
    use constant RESULT => 1;
    use constant DATE   => 2;

    #<<<
    $store = Gtk2::ListStore->new(
            # VENDOR
            'Glib::String',
            # RESULT
            'Glib::String',
            # DATE
            'Glib::String',
    );

    my $tree = Gtk2::TreeView->new_with_model( $store );
    $tree->set_rules_hint( TRUE );
    $sw->add( $tree );

    my $renderer = Gtk2::CellRendererText->new;
    my $column
        = Gtk2::TreeViewColumn->new_with_attributes(
                _( 'Vendor' ),
                $renderer,
                text => VENDOR,
    );
    $column->set_expand( TRUE );
    $column->set_sort_column_id( VENDOR );
    $tree->append_column( $column );

    $column = Gtk2::TreeViewColumn->new_with_attributes(
                _( 'Date' ),
                $renderer,
                text => DATE,
    );
    $column->set_sort_column_id( DATE );
    $column->set_expand( TRUE );
    $tree->append_column( $column );

    $column = Gtk2::TreeViewColumn->new_with_attributes(
                _( 'Result' ),
                $renderer,
                text => RESULT,
    );
    $column->set_expand( TRUE );
    $column->set_sort_column_id( RESULT );
    $tree->append_column( $column );
    #>>>

    my $toolbar = Gtk2::Toolbar->new;
    $box->pack_start( $toolbar, FALSE, FALSE, 3 );
    $toolbar->set_style( 'both-horiz' );

    my $separator = Gtk2::SeparatorToolItem->new;
    $separator->set_draw( FALSE );
    $separator->set_expand( TRUE );
    $toolbar->insert( $separator, -1 );

    my $button = Gtk2::ToolButton->new_from_stock( 'gtk-save-as' );
    $button->set_tooltip_text( _( 'Save results' ) );
    $toolbar->insert( $button, -1 );
    $button->signal_connect(
        clicked => sub {
            return unless ( $store->iter_n_children );
            save_file();
        }
    );

    return $box;
}

sub analysis_frame_one {
    my $box = Gtk2::VBox->new( FALSE, 5 );

    my $label
        = Gtk2::Label->new( _( "Check or recheck a file's reputation" ) );
    $label->set_alignment( 0.0, 0.5 );
    $box->pack_start( $label, FALSE, FALSE, 5 );

    my $grid = Gtk2::Table->new( 1, 3, FALSE );
    $box->pack_start( $grid, TRUE, TRUE, 5 );
    $grid->set_col_spacings( 10 );
    $grid->set_row_spacings( 10 );
    $grid->set_homogeneous( TRUE );

    #<<<
    # Declaring this now for setting sensitive/insensitive
    my $button = Gtk2::ToolButton->new_from_stock( 'gtk-find' );
    #$button->set_sensitive( FALSE );

    my $select_button
        = Gtk2::FileChooserButton->new(
                _( 'Select a file' ),
                'open',
    );

    # If we've arrived from scanning results, set the file:
    if( $from_scan ) {
            $select_button->set_filename( $from_scan );
    }
    $button->set_sensitive( TRUE );
    $select_button->signal_connect(
            'file-set' => sub {
                    # Ensure it does not become re-enabled
                    # after submitting a file.  The stupid
                    # FileChooserButton cannot be reliably
                    # reset.
                    if( ! $submitted ) {
                            $button->set_sensitive( TRUE );
                    }
            }
    );
    $select_button->set_current_folder(
        ClamTk::App->get_path( 'directory' )
    );
    #$grid->attach( $select_button, 0, 0, 1, 1 );
    $grid->attach_defaults( $select_button, 0, 1, 0, 1 );
    #>>>

    my $separator = Gtk2::SeparatorToolItem->new;
    $separator->set_draw( FALSE );
    #$grid->attach( $separator, 1, 0, 1, 1 );
    $grid->attach_defaults( $separator, 1, 2, 0, 1 );

    #$grid->attach( $button, 2, 0, 1, 1 );
    $grid->attach_defaults( $button, 2, 3, 0, 1 );
    $button->set_tooltip_text( _( 'Submit file for analysis' ) );

    $button->signal_connect(
        clicked => sub {
            $filename = $select_button->get_filename;
            return unless -e $filename;

            # Since FileChooserButton cannot reset itself,
            # we'll just disable the submit button.
            # Getting real tired of Gtk2.
            $button->set_sensitive( FALSE );
            $select_button->set_sensitive( FALSE );
            # To be sure:
            $submitted++;

            # VT size limit using the API is 32MB
            # https://www.virustotal.com/en/faq/
            my $size = -s $filename;
            my $mb = $size / ( 1024 * 1024 );
            if ( $mb > 32 ) {
                warn "filesize too large - must be smaller than 32MB\n";
                popup( _( 'Uploaded files must be smaller than 32MB' ) );
                return;
            }

            Gtk2->main_iteration while ( Gtk2->events_pending );
            my ( $vt_results, $new_window, $is_error )
                = check_for_existing( $filename );
            # warn "VT results = >$vt_results<, ",
            # "open new_win = >$new_window<, error = >$is_error<\n";
            #$results->set_text( $vt_results );

            # Information exists on this file; show results
            if ( $new_window ) {
                $nb->show_all;
                $nb->set_current_page( 1 );
                #} elsif ( $new_window && !$is_error ) {
            } elsif ( $vt_results == 0 ) {
                # No information exists; offer to submit file
                my $confirm = popup(
                    _(        'No information exists for this file.' . ' '
                            . 'Press OK to submit this file for analysis.'
                    ),
                    1
                );
                if ( $confirm ) {
                    $submitted++;
                    submit_new();
                    return;
                }
            } else {
                popup( _( 'Unable to submit file: try again later' ) );
                return;
            }
        }
    );

    return $box;
}

sub analysis_frame_two {
    my $box = Gtk2::VBox->new( FALSE, 5 );

    my $label = Gtk2::Label->new( _( 'View or delete previous results' ) );
    $label->set_alignment( 0.0, 0.5 );
    $box->pack_start( $label, FALSE, FALSE, 0 );

    my $grid = Gtk2::Table->new( 1, 4, FALSE );
    $box->pack_start( $grid, TRUE, TRUE, 5 );
    $grid->set_col_spacings( 10 );
    $grid->set_row_spacings( 10 );
    $grid->set_homogeneous( FALSE );

    $model = Gtk2::ListStore->new( 'Glib::String' );

    $combobox = Gtk2::ComboBox->new_with_model( $model );
    $grid->attach_defaults( $combobox, 0, 1, 0, 1 );
    my $render = Gtk2::CellRendererText->new;
    $combobox->pack_start( $render, TRUE );
    $combobox->add_attribute( $render, text => 0 );
    # $combobox->signal_connect( changed => \&read_files );
    read_files();

    my $separator = Gtk2::SeparatorToolItem->new;
    $separator->set_draw( FALSE );
    $grid->attach_defaults( $separator, 1, 2, 0, 1 );

    my $button = Gtk2::ToolButton->new_from_stock( 'gtk-index' );
    $grid->attach_defaults( $button, 2, 3, 0, 1 );
    $button->set_tooltip_text( _( 'View file results' ) );
    $button->signal_connect(
        clicked => sub {
            my $file = $combobox->get_active_text;
            return unless ( $file );
            my $file_to_use = '';
            my $files       = read_files();

            # Written so as to not choke older Perls
            for my $key ( sort @$files ) {
                if ( $key->{ basefilepath } eq $file ) {
                    $file_to_use = $key->{ readpath };
                }
            }

            # For some reason, we'll use csv files
            my $csv = Text::CSV->new( { binary => 1, eol => "\n" } )
                or do {
                warn "unable to begin Text::CSV: $!\n";
                # popup( _( 'Error with Text::CSV file' ) );
                return;
                };

            open( my $f, '<:encoding(utf8)', $file_to_use ) or do {
                warn "unable to opening VT CSV file: $!\n";
                # popup( _( 'Error opening VT CSV file' ) );
                return;
            };
            # File, Hash, Date
            my $fields;
            my $counter = 0;
            while ( my $row = <$f> ) {
                chomp( $row );
                next if ( $row =~ /^#/ );
                next if ( $row =~ /^\s*$/ );
                if ( $csv->parse( $row ) ) {
                    $fields->[ $counter ] = [ $csv->fields() ];
                    $counter++;
                }
            }
            close( $f );
            $store->clear;

            #<<<
            # Written so as to not choke older Perls
            for my $v ( sort @{ $fields } ) {
                my $iter = $store->append;
                $store->set(
                    $iter,
                    VENDOR, $v->[0],
                    RESULT, $v->[1],
                    DATE, $v->[2],
            );
            #>>>
            }
            $nb->show_all;
            $nb->set_current_page( 1 );
        }
    );

    $button = Gtk2::ToolButton->new_from_stock( 'gtk-delete' );
    $grid->attach_defaults( $button, 3, 4, 0, 1 );
    $button->set_tooltip_text( _( 'Delete file results' ) );
    $button->signal_connect(
        clicked => sub {
            my $file = $combobox->get_active_text;
            return unless ( $file );
            my $file_to_use = '';
            my $active_text = $combobox->get_active_text;
            my $active_iter = $combobox->get_active;
            my $files       = read_files();

            # This has to be written a certain way so
            # that older Perls don't $choke
            for my $key ( sort @{ $files } ) {
                if ( $key->{ basefilepath } eq $file ) {
                    $file_to_use = $key->{ readpath };
                    if ( -e $file_to_use ) {
                        unlink( $file_to_use ) or do {
                            warn "unable to delete VT $file_to_use: $!\n";
                            return;
                        };
                        $combobox->set_active( -1 );
                    }
                    read_files();
                    # we're done!
                    last;
                }
            }
        }
    );

    return $box;
}

sub read_files {
    my $submit_dir = ClamTk::App->get_path( 'submit' );
    my $results;

    $model->clear;

    my $arr_count = 0;
    # Grab all files
    for my $f ( glob "$submit_dir/*" ) {
        my $basename = basename( $f );
        # Might be other garbage in the folder; just
        # look for ones that look like hashes
        next unless ( length $basename == 64 );
        $results->[ $arr_count ]->{ readpath } = $f;
        $results->[ $arr_count ]->{ hash }     = basename( $f );
        open( my $t, '<:encoding(utf8)', $f ) or do {
            warn "unable to open analysis file >$f<: $!\n";
            next;
        };
        while ( <$t> ) {
            chomp;
            if ( /^# File (.*?)$/ ) {
                $results->[ $arr_count ]->{ filepath } = $1;
                $results->[ $arr_count ]->{ basefilepath }
                    = basename( $1 );
                $model->set( $model->append, 0,
                    basename( $results->[ $arr_count ]->{ filepath } ) );
                last;
            }
        }
        close( $t );
        $arr_count++;
    }
    return $results;
}

sub check_for_existing {
    # First, set the infobar so the
    # user knows something is happening:
    Gtk2->main_iteration while ( Gtk2->events_pending );
    set_infobar_mode( _( 'Please wait...' ) );
    $window->queue_draw;
    Gtk2->main_iteration while ( Gtk2->events_pending );

    my $file = shift;
    my $url  = 'https://www.virustotal.com/vtapi/v2/file/report';

    my $local_tk_version = ClamTk::App->get_TK_version();

    my $hash = get_hash( $file );

    my $ua = add_ua_proxy();

    #<<<
    my @req = (
        $url,
        [
            resource => $hash,
            apikey   => getapi(),
        ]
    );
    #>>>

    set_infobar_mode( ' ' );
    # $new_window = switch to results tab
    my $new_window = TRUE;
    # $is_error = connection (or other) issue
    my $is_error = FALSE;

    Gtk2->main_iteration while ( Gtk2->events_pending );
    my $response = $ua->post( @req );
    # warn "is_err = >", $response->is_error, "<\n";

    Gtk2->main_iteration while ( Gtk2->events_pending );

    my $data;
    if ( $response->is_success ) {
        my $json = JSON->new->utf8->allow_nonref;
        eval { $data = $json->decode( $response->decoded_content ); };
        if ( $@ ) {
            warn "error reading/decoding json: $@\n";
            $new_window              = FALSE;
            $is_error                = TRUE;
            $data->{ response_code } = -4;
            return ( $data->{ response_code }, $new_window, $is_error );
        }
        # warn "response_code from submission >", $data->{ response_code },
        #    "<\n";
        # Response codes:
        # 0 = not present in dataset
        # -2 = queued for analysis
        # 1 = present and can be retrieved
        if ( $data->{ response_code } == 1 ) {
            # $return = $data->{ positives } . ' / ' . $data->{ total };
            for my $mark ( $data->{ scans } ) {
                while ( my ( $vendor, $v ) = each %$mark ) {
                    my $iter = $store->append;
                    #<<<
                    $store->set(
                        $iter,
                        VENDOR, $vendor,
                        RESULT,
                        ( $v->{ result } )
                        ? $v->{ result }
                        : '---',
                        DATE, $v->{ update },
                    );
                    #>>>
                }
            }
        } elsif ( $data->{ response_code } == 0 ) {
            # $return     = _( 'No information on this file' );
            $new_window = FALSE;
            $is_error   = FALSE;
        } elsif ( $data->{ response_code } == -2 ) {
            # $return     = _( 'File is pending analysis' );
            $new_window = FALSE;
            $is_error   = FALSE;
        } else {
            # $return     = _( 'An unknown error occurred' );
            $data->{ response_code } = -4;
            $new_window              = FALSE;
            $is_error                = TRUE;
        }
    } else {
        $data->{ response_code } = -4;
        $new_window              = FALSE;
        $is_error                = TRUE;
        warn "Unable to connect in submission: ", $response->status_line,
            "\n";
    }
    Gtk2->main_iteration while ( Gtk2->events_pending );
    set_infobar_mode( ' ' );
    Gtk2->main_iteration while ( Gtk2->events_pending );
    return ( $data->{ response_code }, $new_window, $is_error );
}

sub submit_new {
    # First, set the infobar so the user knows
    # something is happening:
    Gtk2->main_iteration while ( Gtk2->events_pending );
    set_infobar_mode( _( 'Please wait...' ) );
    $window->queue_draw;
    my $url = 'https://www.virustotal.com/vtapi/v2/file/scan';

    my $ua               = add_ua_proxy();
    my $local_tk_version = ClamTk::App->get_TK_version();

    my $u_filename = encode( 'UTF-8', $filename );

    #<<<
    my $response = $ua->post(
        $url,
            Content_Type => 'multipart/form-data',
            Content => [
                    apikey   => getapi(),
                    file     => [ $u_filename ]
                    #file     => [ $filename ]
            ],
    );
    #>>>

    Gtk2->main_iteration while ( Gtk2->events_pending );
    set_infobar_mode( ' ' );
    $bar->show_all;
    $window->queue_draw;
    Gtk2->main_iteration while ( Gtk2->events_pending );

    if ( $response->is_success ) {
        # Save off the results; might use them later
        my $file = ClamTk::App->get_path( 'virustotal_links' );
        my $hash = get_hash( $filename );

        my $json_text = $response->decoded_content;
        my $data      = $response->as_string;
        my $json_hash = decode_json( $json_text );

        open( my $f, '>>:encoding(UTF-8)', $file );
        # or warn "unable to save virustotal results: $!\n";
        my $csv = Text::CSV->new( { binary => 1, eol => "\n" } );
        my $ref;
        $ref->[ 0 ] = [ $filename, $hash, $json_hash->{ permalink } ];
        $csv->print( $f, $_ ) for ( @$ref );
        close( $f );

        popup( _( 'File successfully submitted for analysis.' ) );
    } else {
        popup( _( 'Unable to submit file: try again later' ) );
    }
}

sub getapi {
    #<<<
    return decode_base64(
              'MDMwNmU3MT'
            . 'ZjYjQxNTMz'
            . 'OWFkZDQ5ND'
            . 'JkOTg4ZWVm'
            . 'MDU3MjlmND'
            . 'AxYmM0NjIw'
            . 'MjZmNDQ2OD'
            . 'gzYjcxNmIy'
            . 'NTAxZg=='
    );
    #>>>
}

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 );
}

sub save_file {
    # VENDOR, RESULT, DATE
    # temporary filename
    my $hash = get_hash( $filename );
    my $file = ClamTk::App->get_path( 'submit' ) . '/' . $hash;

    my $ref;
    my $count = 0;

    $store->foreach(
        sub {
            my ( $liststore, $path, $iter ) = @_;
            my ( $vendor, $result, $date )
                = $liststore->get( $iter, 0, 1, 2 );
            $ref->[ $count ] = [ $vendor, $result, $date ];
            $count++;
            return FALSE;
        }
    );

    # For some reason, we'll use csv files
    my $csv = Text::CSV->new( { binary => 1, eol => "\n" } )
        or do {
        popup( _( 'Unable to save file' ) );
        return;
        };

    open( my $f, '>:encoding(utf8)', $file ) or do {
        popup( _( 'Unable to save file' ) );
        return;
    };
    print $f '# File', ' ', $filename, "\n";
    print $f '# Hash', ' ', $hash,     "\n";
    print $f '# Date', ' ', scalar localtime, "\n\n";
    $csv->print( $f, $_ ) for ( @$ref );
    close( $f );

    popup( _( 'File has been saved' ) );
    read_files();

    return TRUE;
}

sub popup {
    my ( $message, $option ) = @_;

    my $dialog = Gtk2::MessageDialog->new(
        undef,    # no parent
        [ qw| modal destroy-with-parent | ],
        'info',
        $option ? 'ok-cancel' : 'close',
        $message,
    );

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

    return FALSE;
}

sub set_infobar_mode {
    my $text = shift;

    for my $c ( $bar->get_content_area->get_children ) {
        if ( $c->isa( 'Gtk2::Label' ) ) {
            $c->set_text( $text );
            $bar->show_all;
            $window->queue_draw;
            Gtk2->main_iteration while Gtk2->events_pending;
            return;
        }
    }

    # We don't have a Label, so make one:
    my $label = Gtk2::Label->new( $text );
    $label->set_use_markup( TRUE );
    $bar->get_content_area->add( $label );
    $bar->show_all;
    $window->queue_draw;
    Gtk2->main_iteration while Gtk2->events_pending;

    return;
}

sub add_ua_proxy {
    my $agent = LWP::UserAgent->new( ssl_opts => { verify_hostname => 1 } );

    my $local_tk_version = ClamTk::App->get_TK_version();
    $agent->agent( "ClamTk/$local_tk_version" );
    $agent->timeout( 60 );

    $agent->protocols_allowed( [ 'http', 'https' ] );

    if ( ClamTk::Prefs->get_preference( 'HTTPProxy' ) ) {
        if ( ClamTk::Prefs->get_preference( 'HTTPProxy' ) == 1 ) {
            $agent->env_proxy;
        } elsif ( ClamTk::Prefs->get_preference( 'HTTPProxy' ) == 2 ) {
            my $path = ClamTk::App->get_path( 'db' );
            $path .= '/local.conf';
            my ( $url, $port );
            if ( -e $path ) {
                if ( open( my $FH, '<', $path ) ) {
                    while ( <$FH> ) {
                        if ( /HTTPProxyServer\s+(.*?)$/ ) {
                            $url = $1;
                        }
                        last if ( !$url );
                        if ( /HTTPProxyPort\s+(\d+)$/ ) {
                            $port = $1;
                        }
                    }
                    close( $FH );
                    $agent->proxy( http  => "$url:$port" );
                    $agent->proxy( https => "$url:$port" );
                    $ENV{ HTTPS_PROXY }                  = "$url:$port";
                    $ENV{ HTTP_PROXY }                   = "$url:$port";
                    $ENV{ PERL_LWP_SSL_VERIFY_HOSTNAME } = 0;
                    $ENV{ HTTPS_DEBUG }                  = 1;
                }
            }
        }
    }

    return $agent;
}

sub button_test {
    # This is the worst thing ever.  The set_filename function
    # only works with certain versions of gtk2.  So, this is a test
    # to determine if it works, which is tested by seeing if we
    # can get_filename.
    # Return TRUE if it succeeds so we can add the Analysis button
    # to the Results window.
    # Otherwise return FALSE, and don't draw it.
    my $fcb = Gtk2::FileChooserButton->new( 'just a test', 'open', );
    $fcb->set_filename( '/usr/bin/clamtk' );
    if ( $fcb->get_filename ) {
        return TRUE;
    } else {
        return FALSE;
    }
}

1;
