#!/usr/bin/perl

use lib qw(/usr/lib/libDrakX);
# i18n: IMPORTANT: to get correct namespace (drakguard instead of libDrakX)
BEGIN { unshift @::textdomains, 'drakguard' }
use strict;
use diagnostics;
use common;
use standalone;
use mygtk2;
use ugtk2 qw(:create :helpers :wrappers);
use Gtk2::SimpleList;
use interactive;
use network::shorewall;
use network::squid;
use services;

my $dansguardian_main_file = "/etc/dansguardian/dansguardian.conf";
my $dansguardian_filter_file = "/etc/dansguardian/dansguardianf1.conf";
my $time_control_file = "/etc/shorewall/time_control";
my %dansguardian_levels = (
    160 => N_("Low"),
    100 => N_("Normal"),
    50  => N_("High"),
);
my %dansguardian_langs = (
    arspanish => 'es_AR',
    bulgarian => 'bg',
    chinesebig5 => 'zh_TW',
    chinesegb2312 => 'zh_CN',
    czech => 'cs',
    danish => 'da',
    dutch => 'nl',
    french => 'fr',
    german => 'de',
    hebrew => 'he',
    hungarian => 'hu',
    indonesian => 'id',
    italian => 'it',
    japanese => 'ja',
    lithuanian => 'lt',
    malay => 'ms',
    mxspanish => 'es_MX',
    polish => 'pl',
    portuguese => 'pt',
    ptbrazilian => 'pt_BR',
    #russian-1251
    'russian-koi8-r' => 'ru',
    slovak => 'sk',
    spanish => 'es',
    swedish => 'sv',
    turkish => 'tr',
    ukenglish => 'en',
);

my $blacklist_url_file = "/etc/dansguardian/lists/blacklists/drakguard/urls";
my $whitelist_url_file = "/etc/dansguardian/lists/whitelists/drakguard/urls";
my ($enable, $level, $time_control, $time_start_h, $time_start_m, $time_stop_h, $time_stop_m, $allow_time_change);
my $shorewall = network::shorewall::read();
my $proxy_port = 3128;
my $proxy_user = 'squid';
my $guardian_port = 8080;
my $guardian_user = 'dansguardian';
load();


my $toolname = 'drakguard';
my $title = N("Parental Control");
my $icon = 'drakguard';

$ugtk2::wm_icon = $icon;
my $w = ugtk2->new($title);
#- so that transient_for is defined, for wait messages and popups to be centered
$::main_window = $w->{real_window};
my $in = interactive->vnew('su');

my $allusers_list = Gtk2::SimpleList->new(N("All users") => 'text');
$allusers_list->get_selection->set_mode('multiple');
@{$allusers_list->{data}} = sort(list_users());

my $users_list = Gtk2::SimpleList->new(N("Allowed users") => 'text');
$users_list->get_selection->set_mode('multiple');
@{$users_list->{data}} = difference2($shorewall->{accept_local_users}{http}, [ $proxy_user ]);

my @url_lists = (
    {
        tab_title => N("Blacklist"),
        list_title => N("Forbidden addresses"),
        remove_text => N("Remove from blacklist"),
        file => $blacklist_url_file,
        apply => \&apply_blacklist,
    },
    {
        tab_title => N("Whitelist"),
        list_title => N("Allowed addresses"),
        remove_text => N("Remove from whitelist"),
        file => $whitelist_url_file,
        apply => \&apply_whitelist,
    }
);

sub update_time_change() {
    gtkval_modify(\$allow_time_change, $enable && $time_control);
}

$w->{ok_clicked} = \&save;
$w->{cancel_clicked} = \&quit_gui;

gtkadd($w->{window},
       gtknew('VBox', spacing => 5, children => [
           $::isEmbedded ? () : (0, Gtk2::Banner->new($icon, $title)),
           1, gtknew('Notebook', children => [
               gtknew('Label', text => N("Configuration")),
               gtknew('VBox', spacing => 5, border_width => 5, children => [
                   1, gtknew('WrappedLabel', text => N("This tool allows to configure parental control. It can block access to web sites and restrict connection during a specified timeframe.")),
                   1, gtknew('Label'),
                   0, gtknew('Title2', label => N("Main options")),
                   0, gtknew('CheckButton', text => N("Enable parental control"),
                             active_ref => \$enable, toggled => \&update_time_change),
                   0, gtknew('HBox', children_tight => [
                       gtknew('Label_Left', text_markup => N("Control level"),
                                 alignment => [ 0, 0.5 ]),
                       gtknew('ComboBox',
                                 list => [ keys %dansguardian_levels ],
                                 text_ref => \$level,
                                 sensitive_ref => \$enable,
                                 format => sub { translate($dansguardian_levels{$_[0]}) }),
                   ]),
                   1, gtknew('Label'),
                   0, gtknew('Title2', label => N("User access")),
                   0, gtknew('HBox', spacing => 5, children_tight => [
                       gtknew('ScrolledWindow', width => 220, height => 120, child => $allusers_list),
                       gtknew('VBox', spacing => 5, children_tight => [
                           gtknew('Button', stock => "gtk-add", clicked => \&add_user),
                           gtknew('Button', stock => "gtk-remove", clicked => \&remove_user),
                       ]),
                       gtknew('ScrolledWindow', width => 220, height => 120, child => $users_list),
                   ]),
                   1, gtknew('Label'),
                   0, gtknew('Title2', label => N("Time control")),
                   0, gtknew('CheckButton', text => N("Allow connections only between these times:"),
                             active_ref => \$time_control, sensitive_ref => \$enable,
                             toggled => \&update_time_change),
                   0, gtknew('HBox', sensitive_ref => \$allow_time_change, spacing => 20, children_tight => [
                       gtknew('Label', text => N("Start:")),
                       gtknew('HBox', spacing => 2, children_tight => [
                           gtknew('SpinButton', lower => 0, upper => 23, step_increment => 1, value => $time_start_h,
                                 value_changed => sub { $time_start_h = $_[0]->get_value } ),
                           gtknew('Label', text => ':'),
                           gtknew('SpinButton', lower => 0, upper => 59, step_increment => 1, value => $time_start_m,
                                 value_changed => sub { $time_start_m = $_[0]->get_value } ),
                       ]),
                       gtknew('Label'),
                       gtknew('Label', text => N("End:")),
                       gtknew('HBox', spacing => 2, children_tight => [
                           gtknew('SpinButton', lower => 0, upper => 23, step_increment => 1, value => $time_stop_h,
                                 value_changed => sub { $time_stop_h = $_[0]->get_value } ),
                           gtknew('Label', text => ':'),
                           gtknew('SpinButton', lower => 0, upper => 59, step_increment => 1, value => $time_stop_m,
                                 value_changed => sub { $time_stop_m = $_[0]->get_value } ),
                       ]),
                   ]),
               ]),
               (map {
                   my $url_list = $_;
                   $url_list->{list} = Gtk2::SimpleList->new($url_list->{list_title} => 'text');
                   $url_list->{list}->get_selection->set_mode('multiple');
                   @{$url_list->{list}{data}} = read_url_list($url_list->{file});
                   my $entry;

                   (
                       gtknew('Label', text => $url_list->{tab_title}),
                       gtknew('VBox', spacing => 5, children => [
                           0, gtknew('HBox', border_width => 5, spacing => 5, children_loose => [
                               $entry = gtknew('Entry'),
                               gtknew('Button', text => N("Add"), clicked => sub {
                                          my $text = $entry->get_text;
                                          $text =~ s,^[^:]+://,,g; #- strip protocol
                                          list_add_entry($url_list->{list}, $text);
                                          $entry->set_text("");
                                      }),
                           ]),
                           1, gtknew('ScrolledWindow', width => 500, height => 300, child => $url_list->{list}),
                           0, gtknew('HButtonBox', border_width => 5, layout => 'edge', children_loose => [
                               gtknew('Button', text => $url_list->{remove_text}, clicked => sub {
                                          list_remove_selected($url_list->{list});
                                      }),
                           ]),
                       ]),
                   );
               } @url_lists),
           ]),
           0, $w->create_okcancel(
               undef, undef, undef,
               [ N("Help"), sub { run_program::raw({ detach => 1 }, 'drakhelp', '--id', $toolname) } ]),
       ]),
);

$w->show;
Gtk2->main;

$w->exit(0);

sub list_add_entry {
    my ($list, @addr) = @_;
    foreach my $a (@addr) {
        push @{$list->{data}}, $a
          unless any { $_->[0] eq $a } @{$list->{data}};
    }
}

sub list_remove_entry {
    my ($list, @addr) = @_;
    #- workaround buggy Gtk2::SimpleList array abstraction, it destroys references
    @{$list->{data}} = map { member($_->[0], @addr) ? () : [ @$_ ] } @{$list->{data}};
}

sub list_get_selected {
    my ($list) = @_;
    uniq(map { $list->{data}[$_][0] } $list->get_selected_indices);
}

sub list_remove_selected {
    my ($list) = @_;
    list_remove_entry($list, list_get_selected($list));
}

sub list_get_entries {
    my ($list) = @_;
    map { $_->[0] } @{$list->{data}};
}

sub add_user() {
    list_add_entry($users_list, list_get_selected($allusers_list));
}

sub remove_user() {
    list_remove_selected($users_list);
}

sub quit_gui {
    my ($o_code) = @_;
    $w->exit($o_code);
}

sub load() {
    my $guardian = read_dansguardian();
    $level = { reverse %dansguardian_levels }->{$guardian->{naughtynesslimit}};
    $level ||= { reverse %dansguardian_levels }->{High};
    $enable = services::starts_on_boot('dansguardian');

    $time_control = cat_($::prefix . "/etc/shorewall/start") =~ /^INCLUDE $time_control_file$/m;
    my @time_control_settings = grep { /\bnet2fw\b/ } cat_($::prefix . $time_control_file);
    my ($drop_start, $drop_stop);
    if (my ($drop_start_h, $drop_start_m) = top(@time_control_settings) =~ /\B--timestart\s(\d+):(\d+)\b/) {
	$drop_start = $drop_start_h*60 + $drop_start_m - 1;
    }
    if (my ($drop_stop_h, $drop_stop_m) = first(@time_control_settings) =~ /\B--timestop\s(\d+):(\d+)\b/) {
	$drop_stop = $drop_stop_h*60 + $drop_stop_m + 1;
    }
    if (defined($drop_start) && defined($drop_stop)) {
	my $day_time = 24*60;
	$drop_start = ($drop_start + $day_time) % $day_time;
	$drop_stop = ($drop_stop + $day_time) % $day_time;

	$time_start_h = int($drop_stop/60);
	$time_start_m = $drop_stop%60;
	$time_stop_h = int($drop_start/60);
	$time_stop_m = $drop_start%60;
    }

    $time_start_h //= 18;
    $time_start_m //= 0;
    $time_stop_h //= 21;
    $time_stop_m //= 0;
}

sub save() {
    my $_wait = $in->wait_message(N("Please wait"), N("Please wait"));

    network::shorewall::set_in_file('start', $enable && $time_control, "INCLUDE $time_control_file");
    if ($enable && $time_control) {
	my $day_time = 24*60;
	#- start/stop dropping the minute after/before traffic is allowed
	#- and make sure times are positive and in the 00:00 <-> 23:59 interval
	my $drop_start = ($time_stop_h*60 + $time_stop_m + 1 + $day_time) % $day_time;
	my $drop_stop = ($time_start_h*60 + $time_start_m - 1 + $day_time) % $day_time;
	output_p($::prefix . $time_control_file,
		 join('', map {
		     my $chain = $_;
		     map {
			 sprintf("iptables -I $chain -j DROP -m time --timestart %02d:%02d --timestop %02d:%02d\n",
				 int($_->[0]/60), $_->[0]%60,
				 int($_->[1]/60), $_->[1]%60,
			     );
		     } ($drop_stop >= $drop_start ? [ $drop_start, $drop_stop] : ([ 0, $drop_stop ], [ $drop_start, $day_time-1 ]));
		     #- if allowing start time is before allowing stop time,
		     #- we have to use two intervals to cover the completary parts of the day
		 } qw(net2fw fw2net)),
	    );
	#- allowing from 00:00 to 23:59 is a special case that does not need rules
	$time_control = 0 if $drop_stop == $day_time - 1 && $drop_start == 0;
    }
    network::shorewall::set_in_file('start', $enable && $time_control, "INCLUDE $time_control_file");

    if ($enable) {
        $in->do_pkgs->ensure_are_installed([ qw(shorewall squid dansguardian) ])
          or quit_gui(1);

        $_->{apply}(list_get_entries($_->{list})) foreach @url_lists;
        write_dansguardian();
        enable_transparent_proxy($proxy_port);
        #- reload shorewall config if it has just been installed
        $shorewall ||= network::shorewall::read();
    }
    services::set_status($_, $enable) foreach qw(squid dansguardian);

    if ($shorewall) {
        $shorewall->{disabled} = 0 if $enable;
        @{$shorewall->{accept_local_users}{http}} = if_($enable, uniq($proxy_user, list_get_entries($users_list)));
        @{$shorewall->{accept_local_users}{$proxy_port}} = if_($enable, $guardian_user);
        network::shorewall::set_redirected_ports($shorewall, 'tcp', $guardian_port, if_($enable, 'http', $proxy_port));
        network::shorewall::write($shorewall, $in);
    }

    quit_gui();
}

sub subst_config_line {
    my ($file, $line) = @_;
    my $key = first(split(' ', $line));
    my $done;
    substInFile {
        $done = 1 if s|^\s*$key\b.*\n|$line|;
        $_ .= $line if eof && !$done;
    } $file;
}

sub enable_transparent_proxy {
    my ($port) = @_;
    #- FIXME: use network::squid once it is rewritten to be more gentle with the config file
    subst_config_line($network::squid::squid_conf_file, "http_port $port transparent\n");
}

#- mostly duplicated for MDK::Common::System::getVarsFromSh
sub read_dansguardian() {
    my $guardian = {};
    foreach (cat_($dansguardian_filter_file)) {
	s/#.*//;
        s/^\s*//;
	my ($v, $val) = /^(\w+)\s*=\s*(.*)/ or next;
	$val = $1 if $val =~ /^"(.*)"$/ || $val =~ /^'(.*)'$/;
	$guardian->{$v} = $val;
    }
    $guardian;
}

sub write_dansguardian() {
    require lang;
    my $locale = lang::read();
    my $locale_lang = lang::getlocale_for_lang($locale->{lang}, $locale->{country});
    my %lang_to_dansguardian = reverse %dansguardian_langs;
    my $dansguardian_lang = $lang_to_dansguardian{$locale_lang} || $lang_to_dansguardian{$locale->{lang}};

    subst_config_line($dansguardian_main_file, "language = '$dansguardian_lang'\n") if $dansguardian_lang;
    subst_config_line($dansguardian_filter_file, "naughtynesslimit = $level\n");
}

sub include_guardian_file {
    my ($guardian_file, $external_file) = @_;
    my $to_add = ".Include<$external_file>\n";
    my @all = cat_($guardian_file);
    if (!member($to_add, @all)) {
        output_p($guardian_file, @all, $to_add);
    }
}

sub read_url_list {
    my ($file) = @_;
    grep { $_ && !/^\s*#/ } chomp_(cat_($file));
}

sub apply_blacklist {
    my @addr = @_;
    my $blacklist_top = "/etc/dansguardian/lists/bannedsitelist";
    my $blacklist_category = "blocked by Mandriva parental control tool";
    output_p($blacklist_url_file, map { $_ . "\n" }
               qq(#listcategory: "$blacklist_category"), @addr);
    include_guardian_file($blacklist_top, $blacklist_url_file);
}

sub apply_whitelist {
    my @addr = @_;
    my $whitelist_top = "/etc/dansguardian/lists/exceptionsitelist";
    output_p($whitelist_url_file, map { $_ . "\n" } @addr);
    include_guardian_file($whitelist_top, $whitelist_url_file);
}
