# 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::Update;

use Glib 'TRUE', 'FALSE';

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

use LWP::UserAgent;
use Locale::gettext;

# Keeping these global for easy messaging.
my $infobar;      # InfoBar for status
my $pb;           # ProgressBar for ... showing progress
my $liststore;    # Information on current and remote versions
my $iter_hash;    # Must be global to update sig area

my $updated = 0;

sub show_window {
    my $box = Gtk3::Box->new( vertical, 5 );
    $box->set_homogeneous( FALSE );

    my $top_box = Gtk3::Box->new( vertical, 5 );
    $top_box->set_homogeneous( FALSE );
    $box->pack_start( $top_box, TRUE, TRUE, 0 );

    my $scrolled = Gtk3::ScrolledWindow->new( undef, undef );
    $scrolled->set_policy( 'never', 'never' );
    $scrolled->set_shadow_type( 'etched-out' );
    $top_box->pack_start( $scrolled, FALSE, TRUE, 2 );

    # update available images:
    # gtk-yes = yes
    # gtk-no  = no
    # gtk-dialog-error = unknown

    $liststore = Gtk3::ListStore->new(
        # product, local version,
        'Glib::String', 'Glib::String', 'Glib::String',
    );

    # Product column
    my $view = Gtk3::TreeView->new_with_model( $liststore );
    $view->set_can_focus( FALSE );
    $scrolled->add( $view );
    my $column = Gtk3::TreeViewColumn->new_with_attributes(
        _( 'Product' ),
        Gtk3::CellRendererText->new,
        text => 0,
    );
    $column->set_alignment( 0.5 );
    $view->append_column( $column );

    # Installed version column
    $column = Gtk3::TreeViewColumn->new_with_attributes(
        _( 'Installed' ),
        Gtk3::CellRendererText->new,
        text => 1,
    );
    $column->set_alignment( 0.5 );
    $view->append_column( $column );

    # Date of signatures
    $column = Gtk3::TreeViewColumn->new_with_attributes(
        _( 'Date' ),
        Gtk3::CellRendererText->new,
        text => 2,
    );
    $column->set_alignment( 0.5 );
    $view->append_column( $column );

    # Get local information
    my $local_sig_version = ClamTk::App->get_local_sig_version();

    # Get date of signatures
    my $av_date = ClamTk::App->get_sigtool_info( 'date' );

    #<<<
    my @data = (
        {
            product => _( 'Antivirus signatures' ),
            local   => $local_sig_version,
            date    => $av_date,
        },
    );

    for my $item ( @data ) {
        my $iter = $liststore->append;

        # make a copy for updating
        $iter_hash = $iter;

        $liststore->set( $iter,
                0, $item->{ product },
                1, $item->{ local },
                2, $item->{ date },
        );
    }
    #>>>

    $infobar = Gtk3::InfoBar->new;
    $infobar->set_message_type( 'other' );

    my $text = '';
    if ( ClamTk::Prefs->get_preference( 'Update' ) eq 'shared' ) {
        my $label = Gtk3::Label->new;
        $label->set_text(
            _( 'You are configured to automatically receive updates' ) );
        $infobar->get_content_area()->add( $label );
    } else {
        $text = _( 'Check for updates' );
        $infobar->add_button( $text, -5 );

        $infobar->signal_connect(
            response => sub {
                Gtk3::main_iteration while Gtk3::events_pending;
                update_signatures();
                Gtk3::main_iteration while Gtk3::events_pending;
            }
        );
    }

    $box->pack_start( Gtk3::VBox->new, TRUE, TRUE, 5 );

    $pb = Gtk3::ProgressBar->new;
    $box->pack_start( $infobar, FALSE, FALSE, 0 );
    $box->pack_start( $pb,      FALSE, FALSE, 0 );

    $view->columns_autosize();
    $box->show_all;
    $pb->hide;
    return $box;
}

sub get_remote_TK_version {
    my $url
        = 'https://raw.githubusercontent.com/dave-theunsub/clamtk/master/latest';

    $ENV{ HTTPS_DEBUG } = 1;

    my $ua = add_ua_proxy();

    my $response = $ua->get( $url );

    if ( $response->is_success ) {
        my $content = $response->content;
        chomp( $content );
        # warn "remote tk version = >$content<\n";
        return $content;
    } else {
        warn "failed remote tk check >", $response->status_line, "<\n";
        return '';
    }

    return '';
}

sub update_signatures {
    $pb->{ timer } = Glib::Timeout->add( 100, \&progress_timeout, $pb );
    $pb->show;
    $pb->set_text( _( 'Please wait...' ) );

    my $freshclam = get_freshclam_path();
    if ( ClamTk::Prefs->get_preference( 'Update' ) eq 'single' ) {
        my $dbpath = ClamTk::App->get_path( 'localfreshclamconf' );
        if ( -e $dbpath ) {
            $freshclam .= " --config-file=$dbpath";
        }
    }

    # The mirrors can be slow sometimes and may return/die
    # 'failed' despite that the update is still in progress.

    # my $update = file_handle
    # my $update_sig_pid = process ID for $update

    my $update;
    my $update_sig_pid;
    eval {
        local $SIG{ ALRM } = sub {
            die "failed updating signatures (timeout)\n";
        };
        alarm 100;

        $update_sig_pid = open( $update, '-|', "$freshclam --stdout" );
        defined( $update_sig_pid )
            or do {
            set_infobar_text( 'error',
                _( 'Error updating: try again later' ) );
            return 0;
            };
        alarm 0;
    };
    if ( $@ && $@ eq "failed\n" ) {
        set_infobar_text( 'error', _( 'Error updating: try again later' ) );
        return 0;
    }

    # We don't want to print out the following lines beginning with:
    # my $do_not_print = "DON'T|WARNING|ClamAV update process";

    # We can't just print stuff out; that's bad for non-English
    # speaking users. So, we'll grab the first couple words
    # and try to sum it up.
    #
    # logg("!Database update process failed: %s (%d)\n"
    # Downloading database patch # 25961..

    while ( defined( my $line = <$update> ) ) {
        Gtk3::main_iteration while Gtk3::events_pending;
        $pb->set_text( _( 'Downloading...' ) );
        chomp( $line );

        if ( $line =~ /failed/ ) {
            # Print these out to terminal window for now
            warn $line, "\n";

        } elsif ( $line =~ /Database test passed./ ) {
            warn "Database test passed.\n";

        } elsif ( $line =~ /^Downloading daily-(\d+).*?$/ ) {
            # This one should probably be removed;
            # was probably changed to the next elsif
            my $new_daily = $1;

            $liststore->set( $iter_hash, 0, _( 'Antivirus signatures' ),
                1, $new_daily, );

        } elsif ( $line
            =~ q#^Retrieving https://database.clamav.net/daily-(\d+).cdiff# )
        {
            my $new_daily = $1;

            $liststore->set( $iter_hash, 0, _( 'Antivirus signatures' ),
                1, $new_daily, );

        } elsif ( $line =~ /^Testing database/ ) {
            $pb->set_text( _( 'Testing database' ) );

        } elsif ( $line =~ /^Downloading database patch # (\d+).*?$/ ) {
            my $new_daily = $1;

            $liststore->set( $iter_hash, 0, _( 'Antivirus signatures' ),
                1, $new_daily, );

        } elsif ( $line =~ /Database updated/ ) {
            $pb->set_fraction( 1.0 );

        } elsif (
            # bytecode appears to be last
            $line =~ /.*?bytecode.*?$/ && ( $line =~ /.*?up-to-date\.$/
                || $line =~ /.*?up to date .*?/
                || $line =~ /.*?updated\.$/ )
            )
        {
            $pb->set_fraction( 1.0 );
        } else {
            # warn "skipping line: >$line<\n";
            next;
        }
        Gtk3::main_iteration while Gtk3::events_pending;
    }
    # Get local information. It would probably be okay to just
    # keep the same number we saw during the update, but this
    # gives the "for sure" sig version installed:
    my $local_sig_version = ClamTk::App->get_local_sig_version();

    $liststore->set( $iter_hash, 0, _( 'Antivirus signatures' ),
        1, $local_sig_version, );
    Glib::Source->remove( $pb->{ timer } );
    $pb->set_fraction( 1.0 );
    $pb->set_text( _( 'Complete' ) );

    # Update infobar type and text; remove button
    set_infobar_text( 'info', _( 'Complete' ) );
    ClamTk::GUI::set_infobar_mode( 'info', '' );
    $pb->hide;
    destroy_button();

    return TRUE;
}

sub get_freshclam_path {
    my $paths = ClamTk::App->get_path( 'all' );

    my $command = $paths->{ freshclam };
    # If the user will update the signatures manually,
    # append the appropriate paths
    if ( ClamTk::Prefs->get_preference( 'Update' ) eq 'single' ) {
        $command
            .= " --datadir=$paths->{db} --log=$paths->{db}/freshclam.log";
    }
    # Add verbosity
    $command .= " --verbose";

    # Was the proxy option set?
    if ( ClamTk::Prefs->get_preference( 'HTTPProxy' ) ) {
        if ( ClamTk::Prefs->get_preference( 'HTTPProxy' ) == 2 ) {
            if ( -e $paths->{ localfreshclamconf } ) {
                $command .= " --config-file=$paths->{ localfreshclamconf }";
            }
        }
    }

    return $command;
}

sub set_infobar_text {
    my ( $type, $text ) = @_;
    $infobar->set_message_type( $type );

    for my $child ( $infobar->get_content_area->get_children ) {
        if ( $child->isa( 'Gtk3::Label' ) ) {
            $child->set_text( $text );
            $infobar->queue_draw;
        }
    }
}

sub set_infobar_button {
    my ( $stock_icon, $signal ) = @_;
    if ( !$infobar->get_action_area->get_children ) {
        $infobar->add_button( $stock_icon, $signal );
    } else {
        for my $child ( $infobar->get_action_area->get_children ) {
            if ( $child->isa( 'Gtk3::Button' ) ) {
                $child->set_label( $stock_icon );
            }
        }
    }
}

sub destroy_button {
    # Remove button from $infobar
    for my $child ( $infobar->get_action_area->get_children ) {
        if ( $child->isa( 'Gtk3::Button' ) ) {
            $child->destroy;
        }
    }
}

sub progress_timeout {
    $pb->pulse;

    return TRUE;
}

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

    $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 );
                    $ENV{ HTTPS_PROXY }                  = "$url:$port";
                    $ENV{ HTTP_PROXY }                   = "$url:$port";
                    $ENV{ PERL_LWP_SSL_VERIFY_HOSTNAME } = 0;
                    $ENV{ HTTPS_DEBUG }                  = 1;
                    $agent->proxy( http  => "$url:$port" );
                    $agent->proxy( https => "$url:$port" );
                }
            }
        }
    }
    return $agent;
}

1;
