#!/usr/bin/perl -w
# This script (sophomorix-extraclass) is maintained by Rüdiger Beck
# It is Free Software (License GPLv3)
# If you find errors, contact the author
# jeffbeck@web.de  or  jeffbeck@linuxmuster.net

# modules
use strict;
use Getopt::Long;
use Time::Local;
Getopt::Long::Configure ("bundling");
use Sophomorix::SophomorixConfig;
use List::MoreUtils qw(uniq);
use Net::LDAP;
use Data::Dumper;
$Data::Dumper::Indent = 1;
$Data::Dumper::Sortkeys = 1;
$Data::Dumper::Useqq = 1;
$Data::Dumper::Terse = 1; 
use JSON;
use Sophomorix::SophomorixBase qw(
                                 print_line
                                 print_title
                                 unlock_sophomorix
                                 lock_sophomorix
                                 log_script_start
                                 log_script_end
                                 log_script_exit
                                 backup_auk_file
                                 get_passwd_charlist
                                 get_plain_password
                                 check_options
                                 config_sophomorix_read
                                 result_sophomorix_init
                                 result_sophomorix_add
                                 result_sophomorix_check_exit
                                 result_sophomorix_print
                                 remove_from_list
                                 json_dump
                                 epoch_to_ymdhms
                                 );
use Sophomorix::SophomorixSambaAD qw(
                                 AD_school_create
                                 AD_bind_admin
                                 AD_dns_get
                                 AD_unbind_admin
                                 AD_user_create
                                 AD_user_kill
                                 AD_get_user
                                 AD_group_create
                                 AD_group_kill
                                 AD_group_addmember
                                 AD_get_schoolname
                                 AD_get_name_tokened
                                 AD_class_fetch
                                 AD_group_update
                                 AD_project_sync_members
                                 AD_dn_fetch_multivalue
                                 AD_get_passwd
                                 AD_get_groups_v
                                 AD_get_full_groupdata
                                 AD_debug_logdump
                                 AD_get_AD_for_check
                                    );
my @arguments = @ARGV;


############################################################
# Option handling
############################################################
my %options=();
# define possible action in a script and what OBJECT they need
$options{'CONFIG'}{'ACTION'}{'GROUP'}="create,".
                                      "kill,".
                                      "user-basename,".
                                      "user-number,".
                                      "comment,".
                                      "password,".
                                      "valid-until,".
                                      "description,".
                                      "school";

# define which options deliver which object
$options{'CONFIG'}{'ONE_OF'}{'GROUP'}="extraclass";
#$options{'CONFIG'}{'SINGLE'}{'ACTION'}="delete-all-empty-classes,delete-all-teacheronly-classes,update-maildomain";
## define option that can modify which object
#$options{'CONFIG'}{'MAYBE'}{'GROUP'}="all,".
#                                     "teacherclass,".
#                                     "adminclass,".
#                                     "extraclass,";
## define more dependencies
#$options{'CONFIG'}{'DEPENDS'}{'gidnumber-migrate'}="create";
#$options{'CONFIG'}{'DEPENDS'}{'skip-school-creation'}="create";

my $testopt=GetOptions(\%options, 
                       "help|h",
                       "info|i",
                       "json|j+",
                       "verbose|v+",
                       "create",
                       "kill",
                       "extraclass|class|c=s",
                       "user-basename=s",
                       "user-number=i",
                       "comment=s",
                       "password=s",
                       "valid-until=s",
                       "description=s",
                       "school=s",
                      );    

my %sophomorix_result=&result_sophomorix_init("sophomorix-extraclass");
# Prüfen, ob Optionen erkannt wurden
&check_options($testopt,\%sophomorix_result,$options{'json'},\%options);

if (not defined $options{'school'} or 
    $options{'school'} eq $DevelConf::name_default_school
   ){
    $options{'school'}="---";
}


print Dumper (\%options);

# Reading Configuration
my ($ldap,$root_dse) = &AD_bind_admin(\@arguments,\%sophomorix_result,$options{'json'});
my $root_dns=&AD_dns_get($root_dse);
my %sophomorix_config=&config_sophomorix_read($ldap,$root_dse,\%sophomorix_result);
my ($smb_admin_pass)=&AD_get_passwd($DevelConf::sophomorix_file_admin,
                                     $DevelConf::secret_file_sophomorix_file_admin);

my $class_type=$sophomorix_config{'INI'}{'EXTRACLASS'}{'GROUP_TYPE'};


# --help
#if ($help==1) {
if (defined $options{'help'}) {
   # Scriptname ermitteln
   my @list = split(/\//,$0);
   my $scriptname = pop @list;
   # Befehlbeschreibung
   print('
sophomorix-extraclass creates and manages extraclasses

Options
  -h  / --help
  -v  / --verbose
  -vv / --verbose --verbose

  -i  / --info
  -i -c <extraclass> / --info --class <extraclass>

Create an extraclass with a random password for each user:
  --create
  --extraclass <gruppenname>
  --user-basename <basename>
  --user-number <30>
  --valid-until <10.10.2020> (days: 1-28, month: 1-12, Year: in the future!)

Optional:
  Common password for all users created (password must be complex enough):
    --password \'<password>\'
  Add a comment to all users:
    --comment "Für AK Linux"


For more changes and option use the command sophomorix-class:
  Show all extraclasses:
    # sophomorix-extraclass -i
  Show detailed info for a class:
    # sophomorix-class -i -c <extraclass>

Extraclasses can not be converted to normal classes!
  

Please see the sophomorix-extraclass(8) man pages for full documentation
');
   print "\n";
   exit;
}


############################################################
# config values
############################################################
my %convert_extension = qw(
     1     01
     2     02
     3     03
     4     04
     5     05
     6     06
     7     07
     8     08
     9     09
);

# allowed numbers for days
my %convert_day = qw(
     1     01          01    01
     2     02          02    02
     3     03          03    03
     4     04          04    04
     5     05          05    05
     6     06          06    06
     7     07          07    07
     8     08          08    08
     9     09          09    09
     10    10          11    11
     12    12          13    13
     14    14          15    15
     16    16          17    17
     18    18          19    19
     20    20          21    21
     22    22          23    23
     24    24          25    25
     26    26          27    27
     28    28
);

# allowed numbers for months
my %convert_month = qw(
     1     01          01    01
     2     02          02    02
     3     03          03    03
     4     04          04    04
     5     05          05    05
     6     06          06    06
     7     07          07    07
     8     08          08    08
     9     09          09    09
     10    10          11    11
     12    12
);

# allowed numbers for years
my %convert_year = qw(
     20      2020          21    2021
     22      2022          23    2023
     24      2024          25    2025
     26      2026          27    2027
     28      2028          29    2029
     30      2030          31    2031
     32      2032          33    2033
     34      2034          35    2035
     36      2036          37    2037
     38      2038          39    2039
     40      2040          41    2041
     42      2042          43    2043
     44      2044          45    2045
     46      2046          47    2047
     48      2048          49    2049
     50      2050          51    2051
     52      2052          53    2053
     54      2054          55    2055
     56      2056          57    2057
     58      2058          59    2059
     60      2060          61    2061
     62      2062          63    2063
     64      2064          65    2065
     66      2066          67    2067
     68      2068          69    2069
     70      2070          71    2071
     72      2072          73    2073
     74      2074          75    2075
     76      2076          77    2077
     78      2078          79    2079
     80      2080          81    2081
     82      2082          83    2083
     84      2084          85    2085
     86      2086          87    2087
     88      2088          89    2089
     90      2090          91    2091
     92      2092          93    2093
     94      2094          95    2095
     96      2096          97    2097
     98      2098          99    2099
     0       2100
     1       2101          01    2101
     2       2102          02    2102
     3       2103          03    2103
     4       2104          04    2104
     5       2105          05    2105
     6       2106          06    2106
     7       2107          07    2107
     8       2108          08    2108
     9       2109          09    2109
     10      2110          11    2111
     12      2112          13    2113
     14      2114          15    2115
     16      2116          17    2117
     18      2118          19    2119
     2020    2020          2021    2021
     2022    2022          2023    2023
     2024    2024          2025    2025
     2026    2026          2027    2027
     2028    2028          2029    2029
     2030    2030          2031    2031
     2032    2032          2033    2033
     2034    2034          2035    2035
     2036    2036          2037    2037
     2038    2038          2039    2039
     2040    2040          2041    2041
     2042    2042          2043    2043
     2044    2044          2045    2045
     2046    2046          2047    2047
     2048    2048          2049    2049
     2050    2050          2051    2051
     2052    2052          2053    2053
     2054    2054          2055    2055
     2056    2056          2057    2057
     2058    2058          2059    2059
     2060    2060          2061    2061
     2062    2062          2063    2063
     2064    2064          2065    2065
     2066    2066          2067    2067
     2068    2068          2069    2069
     2070    2070          2071    2071
     2072    2072          2073    2073
     2074    2074          2075    2075
     2076    2076          2077    2077
     2078    2078          2079    2079
     2080    2080          2081    2081
     2082    2082          2083    2083
     2084    2084          2085    2085
     2086    2086          2087    2087
     2088    2088          2089    2089
     2090    2090          2091    2091
     2092    2092          2093    2093
     2094    2094          2095    2095
     2096    2096          2097    2097
     2098    2098          2099    2099
     2100    2100
     2101    2101          2101    2101
     2102    2102          2102    2102
     2103    2103          2103    2103
     2104    2104          2104    2104
     2105    2105          2105    2105
     2106    2106          2106    2106
     2107    2107          2107    2107
     2108    2108          2108    2108
     2109    2109          2109    2109
     2110    2110          2111    2111
     2112    2112          2113    2113
     2114    2114          2115    2115
     2116    2116          2117    2117
     2118    2118          2119    2119
);



&result_sophomorix_check_exit(\%sophomorix_result,\%sophomorix_config,$options{'json'});
############################################################
# Start
############################################################
&log_script_start(\@arguments,\%sophomorix_result,\%sophomorix_config);

# --info
# show all class when no specific class is given
if ($options{'info'}==1 and not defined $options{'extraclass'}){
    my $ref_group_v=&AD_get_groups_v({ldap=>$ldap,
                                   root_dse=>$root_dse,
                                   root_dns=>$root_dns,
                                   school=>$options{'school'},
                                   sophomorix_config=>\%sophomorix_config,
                                 });

    #print Dumper($ref_group_v->{'LISTS'});
    my $jsoninfo="CLASSES_OVERVIEW";
    my $jsoncomment="All Groups";
    &json_dump({json => $options{'json'},
                jsoninfo => $jsoninfo,
                jsoncomment => $jsoncomment,
                object_name => $options{'school'},
                type => $class_type,
                log_level => $options{'verbose'},
                hash_ref => $ref_group_v,
                sophomorix_config => \%sophomorix_config,
               });
   exit;
}



# --info --extraclass <name>
# list extraclass(es) and exit
if ($options{'info'}==1 and defined $options{'extraclass'}){
   my $ref_groups=&AD_get_full_groupdata({ldap=>$ldap,
                                           root_dse=>$root_dse,
                                           root_dns=>$root_dns,
                                           grouplist=>$options{'extraclass'},
                                           sophomorix_config=>\%sophomorix_config,
                                      });
    #print Dumper($ref_groups);
    my $jsoninfo="CLASS";
    my $jsoncomment="Class";
    &json_dump({json => $options{'json'},
                jsoninfo => $jsoninfo,
                jsoncomment => $jsoncomment,
                object_name => $options{'school'},
                log_level => $options{'verbose'},
                hash_ref => $ref_groups,
                sophomorix_config => \%sophomorix_config,
               });
    exit;
}





# --create --extraclass ...
if (defined $options{'create'} and 
    defined $options{'extraclass'} and
    defined $options{'user-basename'} and
    defined $options{'user-number'} and
    defined $options{'valid-until'}
   ){


    # extraclass must be nonexistent
    my ($ref_AD_check) = &AD_get_AD_for_check({ldap=>$ldap,
                                     root_dse=>$root_dse,
                                     root_dns=>$root_dns,
                                     admins=>"TRUE",
                                     sophomorix_config=>\%sophomorix_config,
                                   });

    # extraclassnames must be nonexistent
    if (exists $ref_AD_check->{'FORBIDDEN'}{$options{'extraclass'}}){
        &log_script_exit($ref_AD_check->{'FORBIDDEN'}{$options{'extraclass'}},1,1,0,
                     \@arguments,\%sophomorix_result,\%sophomorix_config,$options{'json'});
    }

    # create usernames
    my @usernames=();
    for (my $i=1;$i<=$options{'user-number'};$i++){
        my $extension;
        if (exists $convert_extension{$i}){
            $extension=$convert_extension{$i};
        } else {
            $extension=$i;
        }
        my $username=$options{'user-basename'}.$extension;
        push @usernames, $username;
        # usernames must be nonexistent
        if (exists $ref_AD_check->{'FORBIDDEN'}{$username}){
            &log_script_exit($ref_AD_check->{'FORBIDDEN'}{$username},1,1,0,
                             \@arguments,\%sophomorix_result,\%sophomorix_config,$options{'json'});
        }
    }


    # testing date
    my ($until_day, $until_month, $until_year)=split(/\./, $options{'valid-until'});

    if (exists $convert_day{$until_day}){
        $until_day=$convert_day{$until_day};
    } else {
        &log_script_exit("$until_day is not allowed as day",1,1,0,
                         \@arguments,\%sophomorix_result,\%sophomorix_config,$options{'json'});
    }

    if (exists $convert_month{$until_month}){
        $until_month=$convert_month{$until_month};
    } else {
        &log_script_exit("$until_month is not allowed as month",1,1,0,
                         \@arguments,\%sophomorix_result,\%sophomorix_config,$options{'json'});
    }

    if (exists $convert_year{$until_year}){
        $until_year=$convert_year{$until_year};
    } else {
        &log_script_exit("$until_year is not allowed as year",1,1,0,
                         \@arguments,\%sophomorix_result,\%sophomorix_config,$options{'json'});
    }

    # testing for future
    my $extraclass_enddate_epoch=timelocal(0, 0, 0, $until_day, $until_month-1, $until_year);
    if ($extraclass_enddate_epoch>=$sophomorix_config{'DATE'}{'LOCAL'}{'EPOCH'}){
    } else {
        print "\nWARNING: valid-until is NOT in the future!\n\n";
    }

    my $extraclass_enddate_AD=$until_year.$until_month.$until_day."000000".".0Z";

    my $group_token=&AD_get_name_tokened($options{'extraclass'},$options{'school'},$class_type);
    &AD_school_create({ldap=>$ldap,
                       root_dse=>$root_dse,
                       root_dns=>$root_dns,
                       school=>$options{'school'},
                       smb_admin_pass=>$smb_admin_pass,
                       sophomorix_config=>\%sophomorix_config,
                       sophomorix_result=>\%sophomorix_result,
                     });
    &AD_group_create({ldap=>$ldap,
                      root_dse=>$root_dse,
                      root_dns=>$root_dns,
                      school=>$options{'school'},
                      group=>$group_token,
                      group_basename=>$options{'extraclass'},
                      description=>$sophomorix_config{'INI'}{'EXTRACLASS'}{'CLASS_DESCRIPTION'},
                      type=>$class_type,
                      status=>$sophomorix_config{'INI'}{'EXTRACLASS'}{'GROUP_STATUS'},
	              sub_ou=>"OU=".$options{'extraclass'}.",".$sophomorix_config{'INI'}{'OU'}{'AD_student_ou'},
                      joinable=>"FALSE",
                      hidden=>"FALSE",
                      smb_admin_pass=>$smb_admin_pass,
                      sophomorix_config=>\%sophomorix_config,
                      sophomorix_result=>\%sophomorix_result,
                    });

    my @password_chars=&get_passwd_charlist();

    my $file;
    if ($options{'school'} eq "---"){
        $file="students.csv";
    } else {
        $file=$options{'school'}.".students.csv";
    }

    my $comment;
    if (defined $options{'comment'}){
        $comment=$options{'comment'};
    } else {
        $comment="---";
    }

    my $i=0;
    foreach my $username (@usernames){
	$i++;
        my $password;
	my $random_pwd;
	if (defined $options{'password'}){
            $random_pwd="FALSE";
            $password=$options{'password'};
	} else {
            $random_pwd="TRUE";
            my $pwd_length;
            if (exists $sophomorix_config{'FILES'}{'USER_FILE'}{$file}{'PWD_LENGTH'}){
                $pwd_length=$sophomorix_config{'FILES'}{'USER_FILE'}{$file}{'PWD_LENGTH'};
            } else {
                $pwd_length=33;
	    }

            $password=&get_plain_password(
                      $sophomorix_config{'INI'}{'EXTRACLASS'}{'USER_ROLE'},
                      $file,
                      $random_pwd, # TRUE/FALSE
                      $pwd_length, # length of random pwd
                      $sophomorix_config{'INI'}{'EXTRACLASS'}{'USER_BIRTHDATE'},
                      \%sophomorix_config,
                      @password_chars);

	}

        &AD_user_create({ldap=>$ldap,
                        root_dse => $root_dse, 
                        root_dns => $root_dns, 
                        user_count => $i,
                        max_user_count => $options{'user-number'},
                        login => $username,
                        group => $group_token,
                        group_basename => $options{'extraclass'},
                        firstname_ascii => $sophomorix_config{'INI'}{'EXTRACLASS'}{'USER_FIRSTNAME_ASCII'},
                        surname_ascii => $username,
                        firstname_utf8 => $sophomorix_config{'INI'}{'EXTRACLASS'}{'USER_FIRSTNAME_UTF8'},
                        surname_utf8 => $username,
                        birthdate => $sophomorix_config{'INI'}{'EXTRACLASS'}{'USER_BIRTHDATE'},
                        sophomorix_first_password => $password,
                        unid => "---",
                        role => $sophomorix_config{'INI'}{'EXTRACLASS'}{'USER_ROLE'},
                        type => $sophomorix_config{'INI'}{'EXTRACLASS'}{'GROUP_TYPE'},
                        school => $options{'school'},
                        tolerationdate => '---',
                        deactivationdate => $extraclass_enddate_AD,
                        status => $sophomorix_config{'INI'}{'EXTRACLASS'}{'USER_STATUS'},
                        file => $file,
                        smb_admin_pass=>$smb_admin_pass,
                        comment=>$comment,
                        json=>$options{'json'},
                        webui_permissions_calculated=>$sophomorix_config{'ROLES'}{$options{'school'}}{'student'}{'UI'}{'WEBUI_PERMISSIONS'},
                        sophomorix_config=>\%sophomorix_config,
                        sophomorix_result=>\%sophomorix_result,
                       });
    }
}



# --kill --extraclass ...
if (defined $options{'kill'} and
    defined $options{'extraclass'}
    ){
    my %kill=();
    my $filter="(&(objectClass=group)(sAMAccountName=".
        $options{'extraclass'}."))";
    if($Conf::log_level>=2){
        print "Filter: $filter\n";
    }
    my $mesg1 = $ldap->search(
                      base   => $root_dse,
                      scope => 'sub',
                      filter => $filter,
                      attr => ['sophomorixSchoolname',
                               'description',
                               'dn',
                               'sophomorixStatus',
                               'sophomorixType',
                               'member']
                            );
    &AD_debug_logdump($mesg1,2,(caller(0))[3]);

    my $count1 = $mesg1->count;
    my $entry1 = $mesg1->entry(0);
    if (not defined $entry1){
        print "\nNo extraclass \"$options{'extraclass'}\" found!\n\n";
	exit;
    } else {
        my $type = $entry1->get_value('sophomorixType');
        my $school = $entry1->get_value('sophomorixSchoolname');
        my $status = $entry1->get_value('sophomorixStatus');
	my @members = $entry1->get_value('member');
        &print_title("extraclass $options{'extraclass'} found!");

        foreach my $user_dn (@members){
            my $mesg2 = $ldap->search(
                              base   => $user_dn,
                              scope => 'base',
                              filter => '(sophomorixStatus=*)',
                              attr => ['sophomorixSchoolname',
                                       'sophomorixComment',
                                       'dn',
                                       'sophomorixStatus',
                                       'sophomorixRole',
			               'sAMAccountName'
                                      ]
                            );
            &AD_debug_logdump($mesg2,2,(caller(0))[3]);

            my $count2 = $mesg2->count;
            my $entry2 = $mesg2->entry(0);
            my $sam=$entry2->get_value('sAMAccountName');
            my $role=$entry2->get_value('sophomorixRole');
            my $status=$entry2->get_value('sophomorixStatus');
            my $comment=$entry2->get_value('sophomorixComment');
            my $school=$entry2->get_value('sophomorixSchoolname');

            if ($status eq "M"){
		# KILL
		push @{ $kill{'LISTS'}{'KILL'} },$sam;
		$kill{'KILL'}{$sam}{'sophomorixComment'}=$comment;
		$kill{'KILL'}{$sam}{'sophomorixStatus'}=$status;
		$kill{'KILL'}{$sam}{'sophomorixRole'}=$role;
		$kill{'KILL'}{$sam}{'sophomorixSchoolname'}=$school;
		$kill{'KILL'}{$sam}{'DN'}=$user_dn;
            } else {
		# NOT KILL
		push @{ $kill{'LISTS'}{'NOT_KILL'} },$sam;
		$kill{'KILL'}{$sam}{'sophomorixComment'}=$comment;
		$kill{'NOT_KILL'}{$sam}{'sophomorixStatus'}=$status;
		$kill{'KILL'}{$sam}{'sophomorixRole'}=$role;
		$kill{'KILL'}{$sam}{'sophomorixSchoolname'}=$school;
		$kill{'KILL'}{$sam}{'DN'}=$user_dn;
	    }
	}
    }

        #print Dumper(\%kill);
    if ($#{ $kill{'LISTS'}{'NOT_KILL'} }>0){
        @{ $kill{'LISTS'}{'NOT_KILL'} } = sort @{ $kill{'LISTS'}{'NOT_KILL'} };
    }
    if ($#{ $kill{'LISTS'}{'NOT_KILL'} }>0){
        @{ $kill{'LISTS'}{'KILL'} } = sort @{ $kill{'LISTS'}{'KILL'} };
    }

    # kill the users if status M
    foreach my $user ( @{ $kill{'LISTS'}{'NOT_KILL'}} ){
	my $status=$kill{'NOT_KILL'}{$user}{'sophomorixStatus'};
        &print_title("Not Deleting user $user because of status $status");
    }

    my $kill_count=0;
    my $kill_count_max=$#{ $kill{'LISTS'}{'KILL'} }+1;
    foreach my $user ( @{ $kill{'LISTS'}{'KILL'} } ){
	$kill_count++;
        my $comment=$kill{'KILL'}{$user}{'sophomorixComment'};
        &print_title("Deleting user $user ($comment)");
        &AD_user_kill({ldap=>$ldap,
                       root_dse=>$root_dse,
                       root_dns=>$root_dns,
                       login=>$user,
                       user_count=>$kill_count,
                       max_user_count=>$kill_count_max,
                       smb_admin_pass=>$smb_admin_pass,
                       json=>$options{'json'},
                       sophomorix_config=>\%sophomorix_config,
                       sophomorix_result=>\%sophomorix_result,
                     });
    }


    # kill the extraclass if empty and status M
    # search again
    my $mesg3 = $ldap->search(
                      base   => $root_dse,
                      scope => 'sub',
                      filter => $filter,
                      attr => ['sophomorixSchoolname',
                               'description',
                               'dn',
                               'sophomorixStatus',
                               'sophomorixType',
                               'member']
                            );
    &AD_debug_logdump($mesg3,2,(caller(0))[3]);

    my $count3 = $mesg3->count;
    my $entry3 = $mesg3->entry(0);
    my $status = $entry3->get_value('sophomorixStatus');
    my @members = $entry3->get_value('member');
    my $count_members=$#members+1;
    if ($count_members==0 and $status eq "M"){
        &print_title("Deleting extraclass $options{'extraclass'} (Status $status, $count_members users)");
	&AD_group_kill({ldap=>$ldap,
                        root_dse=>$root_dse,
                        root_dns=>$root_dns,
                        group=>$options{'extraclass'},
                        smb_admin_pass=>$smb_admin_pass,
                        type=>$sophomorix_config{'INI'}{'EXTRACLASS'}{'GROUP_TYPE'},
                        sophomorix_config=>\%sophomorix_config,
                      });
    } else {
        &print_title("Not Deleting group $options{'extraclass'} because of status $status or $count_members members");
    }
}




############################################################
# End
############################################################
&AD_unbind_admin($ldap);
&log_script_end(\@arguments,\%sophomorix_result,\%sophomorix_config,$options{'json'});







