#!/usr/bin/perl 

use strict;
use diagnostics;
use lib qw(/usr/lib/libDrakX);
use standalone;

use common;
use mygtk2 qw(gtknew gtkset);
use ugtk2 qw(:create :dialogs :helpers :wrappers);

$ugtk2::wm_icon = "/usr/share/mcc/themes/default/drakperm-mdk.png";
require_root_capability();

#- vars declaration
my ($level) = chomp_(`cat /etc/sysconfig/msec | grep SECURE_LEVEL= |cut -d= -f2`);
my $default_perm_level = "level " . $level;
my %perm_files = ($default_perm_level => '/usr/share/msec/perm.' . $level,
                  'editable' => '/etc/security/msec/perm.local',
                 );

my %perm_l10n = ($default_perm_level => N("System settings"),
                 'editable' => N("Custom settings"),
                 'all' => N("Custom & system settings"),
                );
my %rev_perm_l10n = reverse %perm_l10n;
my ($editable, $modified) = (0, 0);

my @rules;

#- Widget declaration
my $w = ugtk2->new(N("Security Permissions"));
$w->{rwindow}->set_size_request(620, 400) unless $::isEmbedded;
my $W = $w->{window};
$W->signal_connect(delete_event => sub { ugtk2->exit });
my $model = Gtk2::ListStore->new("Gtk2::Gdk::Pixbuf", ("Glib::String") x 5);
my $permList = Gtk2::TreeView->new_with_model($model);

my $pixbuf = gtknew('Pixbuf', file => 'non-editable');

my @column_sizes = (150, 100, 100, 15, -1);

# TreeView layout is (Editable, Path, User, Group, Permissions, [hidden]index_id)
$permList->append_column(Gtk2::TreeViewColumn->new_with_attributes(N("Editable"), Gtk2::CellRendererPixbuf->new, 'pixbuf' => 0));
each_index {
    my $col = Gtk2::TreeViewColumn->new_with_attributes($_, Gtk2::CellRendererText->new, 'text' => $::i + 1);
    $col->set_min_width($column_sizes[$::i+1]);
    $permList->append_column($col);
} (N("Path"), N("User"), N("Group"), N("Permissions"));

my $index = 0;
load_perms();

#- widgets settings
my $combo_perm_value = $perm_l10n{all};
my $combo_perm = gtknew('ComboBox', list => [ sort(values %perm_l10n) ], text_ref => \$combo_perm_value);

sub add_callback() {
    row_setting_dialog(N("Add a new rule"));
    $modified++;
}

sub edit_callback() {
    my (undef, $iter) = $permList->get_selection->get_selected;
    return unless $iter;
    row_setting_dialog(N("Edit current rule"), $iter);
}

my @buttons;

sub del_callback() {
    my ($tree, $iter) = $permList->get_selection->get_selected;
    my $removed_idx = $tree->get($iter, 5);
    @rules = grep { $_->{index} ne $removed_idx } @rules;
    $tree->remove($iter);
    sensitive_buttons(0);
    $modified++;
}

sub move_callback {
    my ($direction) = @_;
    my ($model, $iter) = $permList->get_selection->get_selected;
    return if !$iter;
    my $path = $model->get_path($iter) or return;
    $direction eq 'up' ? $path->prev : $path->next;
    my $iter2 = $model->get_iter($path);
    return if !$iter2 || $model->get($iter2, 0);
    $model->swap($iter, $iter2);
    $modified = 1;
    hide_up_button_iffirst_item($path);
    hide_down_button_iflast_item($path);
    $permList->get_selection->select_iter($iter);
    $permList->queue_draw;
}

$permList->signal_connect(button_press_event => sub { 
                     return unless $editable;
                     my (undef, $event) = @_;
                     my (undef, $iter) = $permList->get_selection->get_selected;
                     return unless $iter;
                     row_setting_dialog(N("Edit current rule"), $iter) if $event->type eq '2button-press';
                 });


my $tips = Gtk2::Tooltips->new;

$W->add(gtknew('VBox', spacing => 5, children => [
                 0, gtknew('Label_Left', text => N("Here you can see files to use in order to fix permissions, owners, and groups via msec.\nYou can also edit your own rules which will owerwrite the default rules."), alignment => [ 0.5, 0 ]),
                 1, gtknew('VBox', border_width => 0, children => [
                                    0, gtknew('Label_Left', text => N("The current security level is %s.
Select permissions to see/edit", $level), alignment => [ 0, 0 ]),
                                    0, gtknew('HButtonBox', layout => 'start', children => [
                                                0, $combo_perm,
                                               ]),
                                    0, gtknew('Label'), 
                                    1, gtknew('ScrolledWindow', child => $permList),
                                    0, my $up_down_box = gtknew('HBox', spacing => 5, children_loose => [ @buttons =
                                                                map {
                                                                    gtkset_tip($tips, 
                                                                               gtknew('Button', text => $_->[0], clicked => $_->[2]),
                                                                               $_->[1]);
                                                                } ([ N("Up"), N("Move selected rule up one level"), sub { move_callback('up') } ], 
                                                                   [ N("Down"), N("Move selected rule down one level"), sub { move_callback('down') } ], 
                                                                   [ N("Add a rule"), N("Add a new rule at the end"), \&add_callback ],
                                                                   [ N("Delete"), N("Delete selected rule"), \&del_callback ], 
                                                                   [ N("Edit"), N("Edit current rule"), \&edit_callback ])]),
                                    0, gtknew('VBox'),
                                   ]),
                 0, create_okcancel({
                                     cancel_clicked => sub { ugtk2->exit },
                                     ok_clicked => \&save_perm,
                                    },
                                    undef, undef, '',
                                    [ N("Help"), sub { run_program::raw({ detach => 1 }, 'drakhelp', '--id', 'drakperm') } ],
                                   )
                ])
       );
$W->show_all;
$w->{rwindow}->set_position('center') unless $::isEmbedded;

display_perm('all');
my $_combo_sig = $combo_perm->entry->signal_connect(changed => sub {
                                                        my $class = $rev_perm_l10n{$combo_perm_value};
                                                        $permList->set_reorderable($class eq 'editable');
                                                        display_perm($class , @_);
                                                    });

$permList->get_selection->signal_connect('changed' => sub {
                                               my ($select) = @_;
                                               my (undef, $iter) = $select->get_selected;
                                               return if !$iter;
                                               my $locked = $model->get($iter, 0);
                                               sensitive_buttons($iter ? $editable && !$locked : 0);
                                               return if $locked;
                                               my $curr_path = $model->get_path($iter);
                                               hide_up_button_iffirst_item($curr_path);
                                               hide_down_button_iflast_item($curr_path);
                                           });

$w->main;
ugtk2->exit;


sub hide_up_button_iffirst_item {
    my ($curr_path) = @_;
    my $first_path = $model->get_path($model->get_iter_first);
    $buttons[0]->set_sensitive($first_path && $first_path->compare($curr_path));
}

sub hide_down_button_iflast_item {
    my ($curr_path) = @_;
    $curr_path->next;
    my $next_item = $model->get_iter($curr_path);
    $buttons[1]->set_sensitive($next_item && !$model->get($next_item, 0));
}


sub display_perm {
    my ($perm_level) = @_;
    return unless $perm_level;
    my $show_sys_rules  = $perm_level eq $default_perm_level;
    my $show_user_rules = $perm_level eq 'editable';
    my $show_all_rules  = $perm_level eq 'all';
    # cleaner way: only remove filtered out rules, add those not any more filtered rather than refilling the whole tree
    $model->clear;
    foreach my $rule (@rules) {
        next if !$show_all_rules && ($show_user_rules && $rule->{editable} || $show_sys_rules && !$rule->{editable});
        $model->append_set(map_index { if_(defined $rule->{$_}, $::i => $rule->{$_}) } qw(editable path user group perms index));
    }

    # alter button box behavior
    $editable = $perm_level =~ /^level \d/ ? 0 : 1;
    $up_down_box->set_sensitive($editable);
    sensitive_buttons(0) if $editable;
}

sub save_perm() {
    my $val;
    if ($modified) {
        my $F;
        open $F, '>' . $perm_files{editable} or die(qq(Impossible to process "$perm_files{editable}"));
        $model->foreach(sub {
                            my ($model, $_path, $iter) = @_;
                            return 0 if $model->get($iter, 0);
                            my $line = $model->get($iter, 1) . "\t" . $model->get($iter, 2) . ($model->get($iter, 3) ? "." . $model->get($iter, 3) : "") . "\t" . $model->get($iter, 4) . "\n";
                            print $F $line;
                            return 0;
                        }, $val);
        close $F;
    }
    $modified = 0;
    ugtk2->exit;
}

sub load_perms() {
    foreach my $file (@perm_files{($default_perm_level, 'editable')}) {
        my @editable = if_($file ne $perm_files{editable}, editable => $pixbuf);
        local $_;
        foreach (cat_($file)) {
            next if /^#/;
            # Editable, Path, User, Group, Permissions
            if (m/^(\S+)\s+([^.\s]+)\.(\S+)?\s+(\d+)/) {
                push @rules, { @editable, path => $1, user => $2, group => $3, perms => $4, index => $index };
            } elsif (m/^(\S+)\s+current?\s+(\d+)/) {
                push @rules, { @editable, path => $1, user => 'current', group => '', perms => $2, index => $index };
            } else {
                warn qq(unparsable "$_"line);
            }
            $index++;
        }
    }
}

sub row_setting_dialog {
    my ($title, $o_iter) = @_;
    
    my $dlg = gtknew('Dialog');
    $dlg->set_transient_for($w->{real_window});
    $dlg->set_title($title);
    $dlg->set_modal(1);
#    $dlg->set_resizable(0);
    my $browse = gtknew('Button', text => N("browse"));
    my $file        = gtknew('Entry');
    my ($other, $group, $user, $s) = $o_iter ? reverse(split(//, $model->get($o_iter, 4))) : ();
    my @bits = qw(sticky gid suid);
    my @rights = qw(read write execute);
    my @owners = (N_("user"), N_("group"), N_("other"));
    
    my %rights = (user => $user, group => $group, other => $other);
    my %rights_labels = (user => N("User"), group => N("Group"), other => N("Other"));
    my %checks = ('read' => {
                             label => N("Read"),
                             tip => { map { $_ => 
                                              #-PO: here %s will be either "user", "group" or "other"
                                              N("Enable \"%s\" to read the file", translate($_));
                                          } keys %rights },
                            },
                  'write' => {
                              label => N("Write"),
                              tip => { map { $_ => 
                                              #-PO: here %s will be either "user", "group" or "other"
                                              N("Enable \"%s\" to write the file", translate($_));
                                          } keys %rights },
                             },
                  'execute' => {
                                label => N("Execute"),
                                tip => { map { $_ => 
                                              #-PO: here %s will be either "user", "group" or "other"
                                              N("Enable \"%s\" to execute the file", translate($_));
                                          } keys %rights },
                               },
                  sticky => { label => N("Sticky-bit"), tip => N("Used for directory:\n only owner of directory or file in this directory can delete it") },
                  suid => { label => N("Set-UID"), tip => N("Use owner id for execution") },
                  gid => { label => N("Set-GID"), tip => N("Use group id for execution") },
                 );

    #- dlg widgets settings
    my %s_right = get_right($s);
     
    my $alrd_exsts = defined $o_iter;
    $file->set_text($model->get($o_iter, 1)) if $o_iter;

    my $users  = gtknew('ComboBox', list => [ my @users = get_user_or_group('users') ]);
    $users->entry->set_text($model->get($o_iter, 2)) if $o_iter;
    $users->set_wrap_width(3);
     
    my $groups = gtknew('ComboBox', list => [ my @groups = get_user_or_group('groups') ]);
    $groups->entry->set_text($model->get($o_iter, 3)) if $o_iter;
    $groups->set_wrap_width(3);

    my $id_box = gtknew('Table', homogeneous => 0, xpadding => 0.1, ypadding => 0, border_width => 0, children => [
                        [ gtknew('Label_Left', text => N("User:")), $users ],
                        [ gtknew('Label_Left', text => N("Group:")), $groups ],
                        ]
                       );

    my $usr_check = gtksignal_connect(gtkset_tip($tips, gtknew('CheckButton', text => N("Current user")),
                                                 N("When checked, owner and group will not be changed")),
                                      clicked => sub { $id_box->set_sensitive(!$_[0]->get_active) });
     
    if ($o_iter && $model->get($o_iter, 2) eq 'current') {
     $usr_check->set_active(1);
     $id_box->set_sensitive(0);
    } else { $usr_check->set_active(0) }
     
     
    $browse->signal_connect(clicked => sub {
                     my $file_dlg = Gtk2::FileSelection->new(N("Path selection"));
                     $file_dlg->set_modal(1);
                     $file_dlg->set_transient_for($dlg);
                     $file_dlg->show;
                     $file_dlg->set_filename($file->get_text);
                     $file_dlg->cancel_button->signal_connect(clicked => sub { $file_dlg->destroy });
                     $file_dlg->ok_button->signal_connect(clicked => sub {
                                                $file->set_text($file_dlg->get_filename);
                                                $file_dlg->destroy;
                                               });
                    });
    my %perms;

    gtkpack_($dlg->vbox,
             0, gtknew('Title2', label => N("Path")),
             0, gtknew('HBox', border_width => 18, children => [
                                1, $file,
                                0, $browse
                               ]
                      ),
             0, gtknew('Title2', label => N("Property")),
             0, gtknew('VBox', border_width => 18, children => [
                                0, $usr_check,
                               ]
                      ),
             0, $id_box,
             0, gtknew('Title2', label => N("Permissions")),
             1, gtknew('HBox', border_width => 0, children_loose => [
                               gtknew('VBox', border_width => 0, children_loose => [
                                      gtknew('Label', text => ""),
                                      map { gtknew('Label_Left', text => $checks{$_}{label}, alignment => [ 0, 0 ]) } @rights,
                                     ]),
                               (map {
                                   my $owner = $_;
                                   $perms{$owner} = { get_right($rights{$owner}) };
                                   my $vbox = gtknew('VBox', children_loose => [ 
                                                     gtknew('Label', text => $rights_labels{$owner}),
                                                     map {
                                                         my $c = $_;
                                                         my $active = $perms{$owner}{$c};
                                                         $perms{$owner}{$c} = gtknew('CheckButton');
                                                         $tips->set_tip($perms{$owner}{$c},
                                                                        $checks{$c}{tip}{$owner},
                                                                       );
                                                         gtkset_active($perms{$owner}{$c}, $active);
                                                     } @rights,
                                                    ]);
                                   
                                   $vbox;
                               } @owners),
                               gtknew('VBox', children_loose => [
                                       gtknew('Label', text => ' '),
                                       map { $perms{$_} = gtkset(gtknew('CheckButton', text => $checks{$_}{label}), tip => $checks{$_}{tip}) } @bits,
                                      ]),
                              ]),
            );
    $perms{sticky}->set_active($s_right{execute});
    $perms{gid}->set_active($s_right{write});
    $perms{suid}->set_active($s_right{read});

    $dlg->set_has_separator(0);

    gtkadd($dlg->action_area, 
           create_okcancel(my $w =
                           {
                            cancel_clicked => sub { $dlg->destroy },
                            ok_clicked => sub {
                                my ($path, $user, $group, $perms, $_idx);
                                $path = $file->get_text;
                                if ($path !~ m!^/!) {
                                    warn_dialog(N("Warning"), N("The first character of the path must be a slash (\"/\"):\n\"%s\"",  $path));
                                    return 1;
                                }
                                if ($usr_check->get_active) {
                                    $user  = 'current';
                                    $group = '';
                                } else {
                                    $user  = $users->entry->get_text;
                                    $group = $groups->entry->get_text;
                                    if (!member($user, @users) || !member($group, @groups)) {
                                        warn_dialog(N("Warning"), join("\n", N("Both the username and the group must valid!"),
                                                                       N("User: %s",  $user),
                                                                       N("Group: %s",  $group),
                                                                   )
                                                );
                                        return 1;
                                    }
                                }
                                $perms = sprintf("%03o", eval(join('', "0b",
                                                                   (map { $perms{$_}->get_active || 0 } reverse @bits),
                                                                   (map { my $owner = $_;map_index {
                                                                       $perms{$owner}{$_}->get_active || 0;
                                                                   } @rights } @owners))));
                                # create new item if needed (that is when adding a new one) at end of list
                                if (!$o_iter) {
                                    $o_iter = $model->append;
                                    push @rules, { path => $path, user => $user, group => $group, perms => $perms, index => $index };
                                    $model->set($o_iter, 5 => $index++);
                                }
                                $model->set($o_iter, 1 => $path, 2 => $user, 3 => $group, 4 => $perms);
                                $dlg->destroy;
                                $modified++;
                            }
                           },
                          ),
          );
     
    $w->{ok}->set_sensitive(!$model->get($o_iter, 0)) if $alrd_exsts;
    $dlg->show_all;

}

sub get_user_or_group {
    my ($what) = @_;
    my @users;
     
    local $_;
    my $is_users = $what eq 'users';
    foreach (cat_($is_users ? '/etc/passwd' : '/etc/group')) {
        if ($is_users) {
            push @users, $1 if m/^([^#:]+):[^:]+:[^:]+:/; # or next;
        } else {
            push @users, $1 if m/^([^#:]+):[^:]*:[^:]*:/; # or next;
        }
    }
    return sort(@users);
}

sub get_right {
    my ($right) = @_;
    my %rght   = ('read' => 0, 'write' => 0, 'execute' => 0);
    $right - 4 >= 0 and $rght{read}=1 and $right = $right-4;
    $right - 2 >= 0 and $rght{write}=1 and $right = $right-2;
    $right - 1 >= 0 and $rght{execute}=1 and $right = $right-1;
    return %rght;
}

sub sensitive_buttons {
    foreach my $i (0, 1, 3, 4) {
        $buttons[$i]->set_sensitive($_[0]);
    }
}