# ClamTk, copyright (C) 2004-2010 Dave M
#
# This file is part of 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 strict;
#use warnings;    # disabled upon release
$|++;

use LWP::UserAgent;

use encoding 'utf8';

use ClamTk::GUI;

use Locale::gettext;
use POSIX qw/locale_h/;
textdomain('clamtk');
setlocale( LC_MESSAGES, '' );
bind_textdomain_codeset( 'clamtk', 'UTF-8' );

use Gtk2;
use Glib qw/TRUE FALSE/;

my ( $win, $update_list );
my ( $go_btn, $close_btn, $cancel_btn );
my ($update_sig_pid);

sub update_dialog {
    $win = Gtk2::Dialog->new();
    $win->signal_connect( destroy => sub { $win->destroy; } );
    $win->set_title( gettext('Updates') );
    $win->set_default_size( 625, 130 );

    if ( -e '/usr/share/pixmaps/clamtk.png' ) {
        $win->set_default_icon_from_file('/usr/share/pixmaps/clamtk.png');
    } elsif ( -e '/usr/share/pixmaps/clamtk.xpm' ) {
        $win->set_default_icon_from_file('/usr/share/pixmaps/clamtk.xpm');
    }

    my $tt = Gtk2::Tooltips->new();

    my $update_win = Gtk2::ScrolledWindow->new();
    $win->vbox->pack_start( $update_win, TRUE, TRUE, 0 );
    $update_win->set_policy( 'never', 'never' );
    $update_win->set_shadow_type('etched-out');

    $update_list = Gtk2::SimpleList->new(
        gettext('Updates')     => 'text',
        gettext('Description') => 'text',
        gettext('Select')      => 'bool',
        gettext('Status')      => 'text',
    );
    $update_win->add($update_list);

    my $sig_box = Gtk2::CheckButton->new();
    $sig_box->set_active(FALSE);
    if ( $> != 0 ) {
        $sig_box->set_sensitive(FALSE);
        $tt->set_tip( $sig_box, gettext('You must be root to enable this.') );
    }

    my $gui_box = Gtk2::CheckButton->new();

    my $user_can = 0;
    if ( ClamTk::Prefs->get_preference('Update') eq 'single' ) {
        $user_can = 1;
    }

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

    if ( $> == 0 || $user_can ) {
        if ( $paths->{freshclam} ) {
            push @{ $update_list->{data} },
                [
                gettext('Signature updates'),
                gettext('Check for antivirus signature updates'),
                $sig_box, gettext('N/A'),
                ];
        }
    }

    push @{ $update_list->{data} },
        [
        gettext('GUI updates'),
        gettext('Check for updates to the graphical interface'),
        $gui_box, gettext('N/A'),
        ];

    my $hbox = Gtk2::HButtonBox->new();
    $win->vbox->pack_start( $hbox, FALSE, FALSE, 0 );
    $hbox->set_layout_default('spread');
    $win->vbox->set_focus_child($hbox);

    my $go_img = Gtk2::Image->new_from_stock( 'gtk-redo', 'small-toolbar' );
    $go_btn = Gtk2::Button->new();
    $go_btn->signal_connect( clicked => \&decision );
    $go_btn->set_property( 'image' => $go_img );
    $go_btn->set_label( gettext('Check for updates') );
    $tt->set_tip( $go_btn, gettext('Check for updates') );
    $hbox->add($go_btn);

    $close_btn = Gtk2::Button->new_from_stock('gtk-close');
    $hbox->add($close_btn);
    $close_btn->signal_connect(
        clicked => sub {
            $update_win->destroy;
            $win->destroy;
        }
    );
    $tt->set_tip( $close_btn, gettext('Close this window') );

    $cancel_btn = Gtk2::Button->new_from_stock('gtk-cancel');
    $hbox->add($cancel_btn);
    $cancel_btn->signal_connect(
        clicked => sub {
            kill 15, $update_sig_pid if ($update_sig_pid);
            waitpid( $update_sig_pid, 0 );
            $update_sig_pid            = '';
            $update_list->{data}[0][3] = gettext('N/A');
            $update_list->{data}[1][3] = gettext('N/A');
            $go_btn->set_sensitive(TRUE);
            $close_btn->set_sensitive(TRUE);
            $cancel_btn->set_sensitive(FALSE);
        }
    );
    $cancel_btn->set_sensitive(FALSE);

    if ( $> == 0 ) {
        my $warning_btn = Gtk2::Button->new_from_stock('gtk-dialog-warning');
        $hbox->add($warning_btn);
        $warning_btn->signal_connect(
            clicked => sub {
                my $message = gettext(
                    "It is recommended you do not run this application as root.\n"
                        . 'Please see http://clamtk.sf.net/faq.html.' );
                my $dialog =
                    Gtk2::MessageDialog->new( $win,
                    [qw(modal destroy-with-parent)],
                    'warning', 'close', $message );

                $dialog->run;
                $dialog->destroy;
            }
        );
    }

    # Rotate (actually truncate) user's freshclam.log
    # We'll just keep about 30 lines for the regular user
    # since root will (hopefully) use the system's area which
    # will already do log rotation.
    # The 30 lines will be useful for diagnosing problems
    # Don't rotate if TruncateLog=0
    # This should only be expensive the first time or if the user
    # has automatic (cron) updates scheduled and doesn't come here often.
    if ( $> != 0 && ClamTk::Prefs->get_preference('TruncateLog') ) {
        if ( open( my $f, '<', "$paths->{db}/freshclam.log" ) ) {
            my @log = ();
            @log = <$f>;
            close($f);
            if ( scalar(@log) > 30 ) {
                open( my $t, '>', "$paths->{db}/temp.tmp" )
                    or last;
                for ( -30 .. 0 ) {
                    print $t $log[$_];
                }
                close($t);
                rename( "$paths->{db}/temp.tmp",
                    "$paths->{db}/freshclam.log" );
                unlink("$paths->{db}/temp.tmp");
            }
        }
    }

    $win->show_all();
    $win->run;
    $win->destroy;
    return;
}

sub decision {
    my ( $rowref, $scalar );
    $scalar = scalar( @{ $update_list->{data} } );
    return unless ($scalar);
    my $value = 0;
    $go_btn->set_sensitive(FALSE);
    $close_btn->set_sensitive(FALSE);
    $cancel_btn->set_sensitive(TRUE);
    $win->queue_draw;

    for my $row ( 0 .. $scalar - 1 ) {
        Gtk2->main_iteration while Gtk2->events_pending;
        $rowref = $update_list->{data}[$row];
        if ( $rowref->[2] == 1 ) {    # is it enabled?
            if ( $rowref->[0] eq gettext('Signature updates') ) {
                $update_list->{data}[$row][3] = gettext('Checking...');
                $value = update_signatures($row);
                $update_list->{data}[$row][3] =
                      $value == -1 ? gettext('Update failed')
                    : $value == 1  ? gettext('Signatures are current')
                    : $value == 2  ? gettext('Updated')
                    :                '';
                ClamTk::GUI->set_sig_status();
            } elsif ( $rowref->[0] eq gettext('GUI updates') ) {
                $update_list->{data}[$row][3] = gettext('Checking...');
                $value = update_gui( 'dummy', 'not-startup' );
                $update_list->{data}[$row][3] =
                      $value == 1 ? gettext('GUI is current')
                    : $value == 2 ? gettext('A newer version is available')
                    : $value == 3 ? gettext('GUI is current')
                    : $value == 4 ? gettext('Check failed')
                    : $value == 5 ? gettext('Check failed')
                    :               '';
                ClamTk::GUI->set_tk_status($value);
            } else {
                #warn 'ref = ', $rowref->[0], "\n";
            }
        }
    }
    $win->resize( 625, 130 );
    $win->queue_draw;
    $go_btn->set_sensitive(TRUE);
    $close_btn->set_sensitive(TRUE);
    $cancel_btn->set_sensitive(FALSE);
    return;
}

sub update_signatures {
    my $print_row = shift;
    Gtk2->main_iteration while Gtk2->events_pending;
    $win->queue_draw;

    # return code:
    # -1 = failed, 1 = current, 2 = has been updated

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

    my $command = $paths->{freshclam};
    if ( ClamTk::Prefs->get_preference('Update') eq 'single' ) {
        $command
            .= " --datadir=$paths->{db} --log=$paths->{db}/freshclam.log";
    }
    if ( ClamTk::Prefs->get_preference('HTTPProxy') ) {
        if ( ClamTk::Prefs->get_preference('HTTPProxy') == 2 ) {
            if ( -e "$paths->{db}/local.conf" ) {
                $command .= " --config-file=$paths->{db}/local.conf";
            }
        }
    }

    # The mirrors can be slow sometimes and may return/die
    # 'failed' despite that the update is still in progress.
    my $update;
    eval {
        local $SIG{ALRM} = sub { die "failed\n" };
        alarm 60;

        $update_sig_pid = open( $update, '-|', "$command --stdout" );
        defined($update_sig_pid) or return -1;
        alarm 0;
    };
    if ( $@ && $@ eq "failed\n" ) {
        return -1;
    }

    # 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.

    while ( defined( my $line = <$update> ) ) {
        Gtk2->main_iteration while ( Gtk2->events_pending );

        # skip the bad stuff
        next if ( $line =~ /$do_not_print/ );
        chomp($line);

        # $final is the gettext-ed version
        my $final = '';

        if ( $line =~ /^Trying host/ ) {
            $final = gettext('Trying to connect...');
            Gtk2->main_iteration while ( Gtk2->events_pending );
            $update_list->{data}[$print_row][3] = $final;
            Gtk2->main_iteration while ( Gtk2->events_pending );
        } elsif ( $line =~ /Downloading daily|Retrieving http/ ) {
            $final = gettext('Downloading updates...');
            Gtk2->main_iteration while ( Gtk2->events_pending );
            $update_list->{data}[$print_row][3] = $final;
            Gtk2->main_iteration while ( Gtk2->events_pending );
        } elsif ( $line =~ /nonblock_connect|Can't connect to/ ) {
            $final = gettext('Cannot connect...');
            Gtk2->main_iteration while ( Gtk2->events_pending );
            $update_list->{data}[$print_row][3] = $final;
            Gtk2->main_iteration while ( Gtk2->events_pending );
        } elsif ( $line =~ /^daily.c.d updated/ ) {
            $final = gettext('Daily signatures have been updated');
            Gtk2->main_iteration while ( Gtk2->events_pending );
            $update_list->{data}[$print_row][3] = $final;
            Gtk2->main_iteration while ( Gtk2->events_pending );
        } elsif ( $line =~ /Database updated .(\d+) signatures/ ) {
            Gtk2->main_iteration while ( Gtk2->events_pending );
            $update_list->{data}[$print_row][3] = $final;
            Gtk2->main_iteration while ( Gtk2->events_pending );
            return 2;
        } elsif ( $line =~ /daily.c.d is up to date/ ) {
            Gtk2->main_iteration while ( Gtk2->events_pending );
            return 1;
        } elsif ( $line =~ /main.cvd version from DNS/ ) {
            $final = gettext('Checking main virus database version');
            Gtk2->main_iteration while ( Gtk2->events_pending );
            $update_list->{data}[$print_row][3] = $final;
            Gtk2->main_iteration while ( Gtk2->events_pending );
        } elsif ( $line =~ /main.c.d is up to date/ ) {
            $final = gettext('Main virus database is current');
            Gtk2->main_iteration while ( Gtk2->events_pending );
            $update_list->{data}[$print_row][3] = $final;
            Gtk2->main_iteration while ( Gtk2->events_pending );
        } else {
            next;
        }
        Gtk2->main_iteration while ( Gtk2->events_pending );
        $win->queue_draw;
    }

    # We could try closing the filehandle <$update>,
    # but it will go out of scope anyway.
    return;
}

sub update_gui {
    my ( undef, $caller ) = @_;

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

    if ( $caller and $caller eq 'startup' ) {
        # The user may have set the preference ('GUICheck')
        # to not check this on startup
        return -1
            unless ( ClamTk::Prefs->get_preference('GUICheck') );
    }

    # return code:
    # -1 = failed, 1 = current, 2 = not current, 3 = too updated

    # We'll remove everything but the numbers
    # from both the local and remote versions to compare
    $version =~ s/[^0-9]//g;

    my $ua = LWP::UserAgent->new;
    $ua->timeout(10);
    if ( ClamTk::Prefs->get_preference('HTTPProxy') ) {
        if ( ClamTk::Prefs->get_preference('HTTPProxy') == 1 ) {
            $ua->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+(.*?)$/) {
                            $port = $1;
                        }
                    }
                    close($FH);
                    $ua->proxy( http => "$url:$port" );
                }
            }
        }
    }

    my $response = $ua->get('http://clamtk.sourceforge.net/latest');

    if ( $response->is_success ) {
        my $content = $response->content;
        chomp($content);
        $content =~ s/[^0-9]//g;
        return 1 if ( $version == $content );    # current
        return 2 if ( $content > $version );     # outdated
        return 3 if ( $version > $content );     # too current?
        return 4;                                # shouldn't happen
    } else {
        # warn $response->as_string, "\n";
        return 5;                                # failed, unable to check
    }
}

sub av_db_select {
    my $choice = Gtk2::Dialog->new();
    $choice->signal_connect( destroy => sub { $choice->destroy; } );
    $choice->set_title( gettext('Antivirus Signatures') );
    $choice->set_default_size( 325, 230 );

    if ( -e '/usr/share/pixmaps/clamtk.png' ) {
        $choice->set_default_icon_from_file('/usr/share/pixmaps/clamtk.png');
    } elsif ( -e '/usr/share/pixmaps/clamtk.xpm' ) {
        $choice->set_default_icon_from_file('/usr/share/pixmaps/clamtk.xpm');
    }

    my $view = Gtk2::TextView->new;
    $view->set_wrap_mode('word');
    $view->set_editable(FALSE);
    $view->set_cursor_visible(FALSE);
    $view->set_indent(5);

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

    # This is horrible and will be changed in 4.5 or 5.0.
    my $dialog =
        gettext( "\nPlease choose how you will update your antivirus "
            . "signatures.\n\nIf this is a multi-user system or you have an "
            . "administrator, you should probably choose 'System Wide'.\n\n"
            . "If you need to be able to update the signatures yourself, "
            . "you should probably choose 'Single User'.\n" );

    my $buffer = $view->get_buffer;
    $buffer->create_tag( 'mono', family => 'Monospace' );
    $buffer->insert_with_tags_by_name( $buffer->get_start_iter, $dialog,
        'mono' );

    $sw->add($view);
    $choice->vbox->pack_start( $sw, TRUE, TRUE, 0 );

    my $selection;

    my $c_box = Gtk2::HButtonBox->new();
    $choice->vbox->pack_start( $c_box, FALSE, FALSE, 0 );

    # The {single,save}_btn buttons are
    # set sensitive->FALSE when selected.
    my ( $cancel_btn, $single_btn, $sys_btn, $save_btn, $quit_btn, $label );

    # Cancel button
    $cancel_btn = Gtk2::Button->new_from_stock('gtk-cancel');
    $c_box->add($cancel_btn);
    $cancel_btn->signal_connect( clicked => sub { $choice->destroy } );

    # 'Single' button - user will update
    $single_btn = Gtk2::Button->new( gettext('Single User') );
    $c_box->add($single_btn);
    $single_btn->signal_connect(
        clicked => sub {
            $single_btn->set_relief('none');
            $sys_btn->set_relief('normal');
            $selection = 'single';
            $save_btn->set_sensitive(TRUE);
        }
    );

    # 'Shared' button - system updates
    $sys_btn = Gtk2::Button->new( gettext('System Wide') );
    $c_box->add($sys_btn);
    $sys_btn->signal_connect(
        clicked => sub {
            $sys_btn->set_relief('none');
            $single_btn->set_relief('normal');
            $selection = 'shared';
            $save_btn->set_sensitive(TRUE);
        }
    );

    # 'Save' button - not sensitive until
    # 'single' or 'shared' is selected
    $save_btn = Gtk2::Button->new_from_stock('gtk-save');
    $c_box->add($save_btn);
    $save_btn->signal_connect(
        clicked => sub {
            save($selection);
            $label->set_text( gettext('Your preferences were saved.') );
            $save_btn->set_sensitive(FALSE);
            $single_btn->set_sensitive(FALSE);
            $sys_btn->set_sensitive(FALSE);
            $quit_btn->set_sensitive(TRUE);
        }
    );
    $save_btn->set_sensitive(FALSE);

    # 'Quit' button
    $quit_btn = Gtk2::Button->new_from_stock('gtk-quit');
    $c_box->add($quit_btn);
    $quit_btn->signal_connect( clicked => sub { $choice->destroy } );
    $quit_btn->set_sensitive(FALSE);

    $label = Gtk2::Label->new();
    $choice->vbox->pack_start( $label, FALSE, FALSE, 0 );

    $view->show();
    $choice->show_all();
    $choice->run;
    $choice->destroy;

    ClamTk::GUI->set_sig_status();
    return;
}

sub save {
    my $update = shift;

    my ($ret) = ClamTk::Prefs->set_preference( 'Update', $update );

    if ( $ret == 1 ) {
        # It worked, so see if there are system signatures around
        # we can copy to save bandwidth and time

        my $paths = ClamTk::App->get_path('db');

        if ( $update eq 'single' ) {
            my ( $d, $m ) = (0) x 2;
            Gtk2->main_iteration while ( Gtk2->events_pending );
            for my $dir_list (
                '/var/lib/clamav',         '/var/clamav',
                '/opt/local/share/clamav', '/usr/share/clamav',
                '/usr/local/share/clamav', '/var/db/clamav',
                )
            {
                if ( -e "$dir_list/daily.cld" ) {
                    system( 'cp', "$dir_list/daily.cld", "$paths/daily.cld" );
                    $d = 1;
                } elsif ( -e "$dir_list/daily.cvd" ) {
                    system( 'cp', "$dir_list/daily.cvd", "$paths/daily.cvd" );
                    $d = 1;
                }
                if ( -e "$dir_list/main.cld" ) {
                    system( 'cp', "$dir_list/main.cld", "$paths/main.cld" );
                    $m = 1;
                } elsif ( -e "$dir_list/main.cvd" ) {
                    system( "cp", "$dir_list/main.cvd", "$paths/main.cvd" );
                    $m = 1;
                }
                last if ( $d && $m );
            }
        }
    }
    return;
}

1;
