#! /usr/bin/perl -w
#
# ag_ldapserver
#
# yast2 agent to read/write slapd.conf
#
# TODO: - currently the order of option values is lost when more files are involved and some 
#         of them have to be reread. fix.
#
#       - implement reading of single options by giving the option name as second path part, e.g.
#         SCR->Read( ".ldapserver.global.schemainclude" )
#
#       - check constraints when reading config files
#
#       - IMPORTANT: check constraints before deleting a option (e.g. deleting rootdn when rootpw is still set)
#
#       - IMPORTANT: handle doubleticks -> " <- in option values
#
#       - return sensible error codes
#
#       - lots of those ugly direct data accesses (like $section->{$opt_name}->[0]{dataref}{__internal}{filename} )
#         could be hidden in subs, which would increase readability and stability
#
# $Id: ag_ldapserver 35872 2007-02-05 17:46:42Z rhafer $
#
package ag_ldapserver;
use strict;
use YaST::SCRAgent;
use YaST::YCP qw(:LOGGING);
#use ycp;

use Data::Dumper;

our @ISA = ( "YaST::SCRAgent" );

use Digest::MD5 qw( md5_hex );
use Encode qw( encode_utf8 );

## Globals

#name of configfile
my $slapdconf = "";

#debugsuffix
my $debugsuffix = undef;

my $delaywrite = 0;

#command handlers
my %command = (
    Read  => {
        backendlist     =>  \&readBackList,
        databaselist    =>  \&readDBList,
        global          =>  \&readUnparsedSection,
        backend         =>  \&readUnparsedSection,
        database        =>  \&readUnparsedSection,
    }
); 

#information about relationships between options
#IMPORTANT: don't use this hash directly, use the $class->getConstraints function
my $constraints = {
    database    => {
        database => {
            multi   => 0,
            backend => { all => 1 }
        },
        suffix  => { 
            multi   => 0, 
            backend => { all => 1 }
        },
        rootdn  => {
            after   => { suffix => 1 },
            multi   => 0,
            backend => { all => 1 }
        },
        rootpw  => {
            after   => { rootdn => 1 },
            multi   => 0,
            backend => { all => 1 }
        },
        index => {
            multi => 1,
            backend => { all => 1 }
        },
        directory   => { 
            multi => 0,
            backend => { bdb => 1, ldbm => 1 }
        },
        cachesize   => {
            multi => 0,
            backend => { bdb => 1, ldbm => 1 }
        },
        checkpoint  => { 
            multi => 0,
            backend => { bdb => 1 }
        },
        overlay => {
            subsection => 1,
            multi => 1,
            last => 1,
            backend => { bdb => 1 }
        }
    },
    global      => {
        schemainclude   => { multi => 1, before => { access => 1} },
        include     => { multi => 1 },
        allow       => { multi => 0 },
        loglevel    => { multi => 0 },
        TLSCipherSuite => { multi => 0 },
        TLSCACertificateFile => { multi => 0 },
        TLSCACertificatePath => { multi => 0 },
        TLSCertificateFile => { multi => 0 },
        TLSCertificateKeyFile => { multi => 0 },
        TLSVerifyClient => { multi => 0 }
    },
    overlay => {
            ppolicy_default => {
                multi => 0
            },
            ppolicy_hash_cleartext => {
                multi => 0
            },
            ppolicy_use_lockout => {
                multi => 0
            }
    }
};

my $quote = {
             suffix => 1,
             rootdn => 1,
             rootpw => 1,
             ppolicy_default => 1
            };

#internal representation of the config data
#TODO: write structure documentation
my $data = {};

#lookup hash to find the data points for a certain option 
#TODO: write structure documentation
my $lookup = {};

#main repository for all configuration files
#structure is:
# $files = { $filename => \%datahash }
my $files = {};

sub OtherCommand
{
    my ($self, $symbol, $config, @rest) = @_;
    y2debug("-> OtherCommand");
    return $self->SetError(
        summary => "first command has to be configfile, seen( $_ )",
        code    => "SCR_INIT_ERR"
    ) if( $symbol ne "LDAPCONFIG" );

    $slapdconf = $config->{file} || "/etc/openldap/slapd.conf";
}

sub Read
{
    my $class = shift || return undef;

    my $path = shift || return $class->SetError(
        summary => "Missing path",
        code    => "PARAM_CHECK_FAILED"
    );
    y2debug("-> Read: $path");
    my @path = split( /\./, $path );
    shift @path;
    
    #bail if wrong command was specified
    return $class->SetError(
        summary => "Wrong path '".$path."'",
        code    => "SCR_WRONG_PATH"
    ) if( !exists( $command{Read}->{$path[0]} ) );

    $class->initData( $slapdconf ) || return undef;

    #call the command function according to passed path
    my $cmdref = $command{Read}->{$path[0]};
    my $ret = $cmdref->( $class, {
        name    => shift,
        command => $path[0],
        option  => $path[1]
        });

    return $ret;
}

sub Write
{
    my $class = shift || return undef;
    my $path_str = shift;
    y2debug("-> Write: ".$path_str);

    my @path = split( /\./, $path_str ); # FIXME: what about path parts that contain dots?
    shift @path;
    my @args = @_;

    # !!! will be fixed with real ACL support later !!!
    # !!! better skip this while reading the code - ug@suse.de !!!
    if( $path[0] eq 'sambaACLHack' ) {
        my $dn = $args[0];
        return $class->SetError(
            summary => "DN missing in sambaACLHack",
            code    => "PARAM_CHECK_FAILED"
        ) unless( $dn );
        return $class->sambaACLHack( $dn );
    }
    # !!! okay, you can open your eyes now !!!

    $class->initData( $slapdconf ) || return undef;

    if( $path[0] eq 'global' )
    {
        my $options = shift;
        return $class->SetError(
            summary => "argument to 'write .global' needs to be a map",
            code    => "PARAM_CHECK_FAILED"
        ) if( ref( $options ) ne "HASH" );

        my @sorted_keys = $class->sortByConstraints( 'global', [ keys %$options ] );

        foreach my $opt_name ( @sorted_keys )
        {
            $class->updateSingleOption( 'global', $opt_name, $options->{$opt_name} ) || return undef;
        }
    } elsif( $path[0] eq 'backend' )
    {
        my $back_type = shift;
        my $options = shift;
        return $class->SetError(
            summary => "argument to 'write .backend' needs to be a map",
            code    => "PARAM_CHECK_FAILED"
        ) if( ref( $options ) ne "HASH" );

        my @sorted_keys = $class->sortByConstraints( 'backend', [ keys %$options ] );
        
        foreach my $opt_name ( @sorted_keys )
        {
            $class->updateSingleOption( 'backend '.$back_type, $opt_name, $options->{$opt_name} ) || return undef;
        }
    } elsif( $path[0] eq 'database' )
    {
        my $db_suffix = shift;
        my $options = shift;

        y2milestone("Write database $db_suffix ");
        y2debug("Options: ". qd($options));

        return $class->SetError(
            summary => "argument to 'write .database' needs to be a map",
            code    => "PARAM_CHECK_FAILED"
        ) if( ref( $options ) ne "HASH" );

        my @sorted_keys = $class->sortByConstraints( 'database', [ keys %$options ] );
        foreach my $opt_name ( @sorted_keys )
        {
            $class->updateSingleOption( 'database '.$db_suffix, $opt_name, $options->{$opt_name} ) || return undef;
        }
    } else
    {
        return $class->SetError(
            summary => "Illegal path '.".join( ".", @path )."'",
            code    => "SCR_WRONG_PATH"
        );
    }
    return $class->rewriteChangedFiles();
}

sub Execute
{
    my $class = shift || return undef;
    y2debug("-> Execute");
    
    my @path = split( /\./, shift );
    shift @path;

    $class->initData( $slapdconf ) || return undef;

    if( $path[0] eq "setdebugsuffix" )
    {
        my $tmpsuffix = shift;
        if( $tmpsuffix =~ /[^A-Za-z0-9]/s )
        {
            $debugsuffix = "y2save";
            return $class->SetError(
                summary => "debugsuffix may only contain alphanumeric characters (A-Z,a-z,0-9), setting default 'y2save'",
                code    => "PARAM_CHECK_FAILED"
            );
        }
        
        $debugsuffix = $tmpsuffix;
        return 1;
    } elsif( $path[0] eq "delaywrite" )
    {
        my $dw = shift;
        if( $dw =~ /[^01]/ )
        {
            return $class->SetError(
                summary => "argument to execute .delaywrite must be either 0 or 1",
                code    => "PARAM_CHECK_FAILED"
            );
        }
        $delaywrite = $dw;
        return $class->rewriteChangedFiles();
    } elsif( $path[0] eq "adddatabase" )
    {
        my $params = shift;
        if( ref( $params ) ne "HASH" )
        {
            return $class->SetError(
                summary => "argument to execute .adddatabase needs to be a map",
                code    => "PARAM_CHECK_FAILED"
            );
        }
        if( !exists( $params->{suffix} ) )
        {
            return $class->SetError(
                summary => "execute .adddatabase: missing 'suffix' in argument map",
                code    => "PARAM_CHECK_FAILED"
            );
        }
        if( !exists( $params->{type} ) )
        {
            return $class->SetError(
                summary => "execute .adddatabase: missing 'type' in argument map",
                code    => "PARAM_CHECK_FAILED"
            );
        }
        
        my $suffix = $params->{suffix};
        $suffix =~ s/\s+$//;
        $suffix =~ s/\s+/ /g;
        my $section_name = 'database '.$suffix;
        if( exists( $lookup->{$section_name} ) )
        {
            return $class->SetError(
                summary => "execute .adddatabase: a database serving suffix '$suffix' does already exist",
                code    => "PARAM_CHECK_FAILED"
            );
        }

        #create new database section at end of config file

		#fix for bug #115579: subordinate database sections have to be before their parent section
		#my @dataposlist = sort { $class->keycompare( $a, $b ) } keys %$data;
        #my $datapos = ( split( /\./, $dataposlist[$#dataposlist] ))[0] + 1;

		my $datapos = $class->getNewDatabasePosition($params->{suffix});


        my $section = {};
        $lookup->{$section_name} = $section;
       
        #$class->addOptionData({
        #    data    =>  $data,
        #    section =>  $section,
        #    datapos =>  $datapos,
        #    name    =>  'comment',
        #    content =>  "#created automatically by ag_ldapserver\n",
        #    ws      =>  ''
        #});
        #$class->inc( \$datapos );
        $class->addOptionData({
            data    =>  $data,
            section =>  $section,
            datapos =>  $datapos,
            name    =>  'database',
            content =>  $params->{type},
            ws      =>  ' '
        });
        $class->inc( \$datapos );
        $class->addOptionData({
            data    =>  $data,
            section =>  $section,
            datapos =>  $datapos,
            name    =>  'suffix',
            content =>  $params->{suffix},
            ws      =>  ' '
        });
        
        $data->{__internal}{dirty} = 1;
        return $class->rewriteChangedFiles();
    } else
    {
        return $class->SetError(
            summary => "Wrong path ".join( ".", @path ),
            code    => "SCR_WRONG_PATH"
        );
    }
}

##
## sub getNewDatabasePosition( $suffix )
##
## returns the correct position for adding a database with given suffix.
##
sub getNewDatabasePosition
{
	my $class = shift || return undef;
	my $suffix = shift || return $class->SetError(
		summary	=> "Missing suffix parameter",
		code	=> "LDAPSERVER_INTERNAL"
	);

        y2debug("-> getNewDatabasePosition");
	#create list of all suffixes in the config file;
	my @dblist = map {(/^database (.*)/)[0]} grep {m/^database /} keys %$lookup;
	my $pos;
	foreach (@dblist)
	{
		if( $suffix =~ /$_/ )
		{
			y2debug("new database <$suffix> is successor of <$_>, adding config before it");
			#this is a successor database section, put new database config section before it
			$pos = $lookup->{"database $_"}{database}[0]{datapos};
			$pos = ($pos-1).'.1';

			y2debug( "inserting database <$suffix> at position: ".$pos );
			
			return $pos;
		}
	}

	y2debug( "inserting database <$suffix> at end of config file" );
	my @poslist = sort { $class->keycompare( $a, $b ) } keys %$data;
    $pos = ( split( /\./, $poslist[$#poslist] ))[0] + 1;	
	
	return $pos;
	
}

##
## sub readUnparsedSection( \%section )
##
## returns the unparsed option values of the specified section
## values of single only options are returned as scalars, not listrefs
## unknown options are not returned
##
## TODO: parameter checks
sub readUnparsedSection
{
    my $class = shift || return undef;

    my $conf = shift || return $class->SetError(
        summary => "Missing argv hash in command handler",
        code    => "LDAPSERVER_INTERNAL"
    );
    y2debug("-> readUnparsedSection");
    y2debug(qd($conf));

    my $section_name = "";
    my $section;
    if ( $conf->{'option'} ) {
        $section_name = $conf->{'option'};
        $section = shift;
    } else {
        $section_name = $conf->{command};
        $section_name .= " ".$conf->{name} if defined $conf->{name} && $conf->{command} ne 'global';
        chomp($section_name);
        $section_name =~ /^(\S+)/;
        $section = $lookup->{$section_name};
    }
    my $section_type = $conf->{command};
    y2debug( "Retrieving section '$section_name'" );
    y2debug( "Section has ".$#{[keys %$section]}." entries." );
    if ( $conf->{'command'} eq "database" ){
        y2debug(qd($section));
    }
    my $ret = {};
    while( my( $opt, $list ) = each %$section )
    {
        next if $opt eq 'comment';
        my $constr = $class->getConstraints( $section_type, $opt );
        y2debug( "constraints: ".qd($constr));
        next if not defined $constr;
        if ( $opt eq 'overlay' ) {
            y2debug( "overlay options: ".qd($list) );
            foreach my $pos ( @$list )
            {
                y2debug( "overlay pos: ".qd($pos) );
                my $type = $pos->[0];
                my $sub_options = $pos->[1];
                my $subret = $class->readUnparsedSection({ option => $type, command => 'overlay' }, $sub_options);
                y2milestone( "result: ".qd($subret));
                $ret->{$opt} = [] if( not exists( $ret->{$opt} ) );
                push( @{$ret->{$opt}}, [ $type, $subret] );
            }
        }
        elsif( $constr->{multi} )
        {
            foreach my $pos ( @$list )
            {
                my $option = $pos->{dataref}->{$pos->{datapos}};
                
                my $value = $option->{'content_string'};
                $value =~ s/\s+/ /sg;
                $value =~ s/\s+$//;

                $ret->{$opt} = [] if( not exists( $ret->{$opt} ) );

                push( @{$ret->{$opt}}, $value );
            }
        } else
        {
            my $option = $list->[0]{dataref}{$list->[0]{datapos}};
            my $value = $option->{content_string};
            
            $value =~ s/\s+/ /sg;
            $value =~ s/\s+$//;

            $ret->{$opt} = $value;
        }
    }
    return $ret;    
}


##
## readBackList
##
## return value:
##  \@backlist - list of backend names of all backend sections in the 
##               configuration file 
##
sub readBackList
{
    my $class = shift || return undef;
    my $conf = shift || return $class->SetError(
        summary => "Missing argv hash in command handler",
        code    => "LDAPSERVER_INTERNAL"
    );
    y2debug("-> readBackList");

    my @backlist;
    foreach( keys %$lookup )
    {
        push( @backlist, $1 ) if( /^backend (.+)$/ ) && $#{[keys %{$lookup->{$_}}]} > -1;
    }
    return [@backlist];
}

##
## readDBList
##
## return value:
##  \@dblist - list of basedn values of all databases  
##
sub readDBList
{
    my $class = shift || return undef;
    my $conf = shift || return $class->SetError(
        summary => "Missing argv hash in command handler",
        code    => "LDAPSERVER_INTERNAL"
    );
    y2debug("-> readDBList");

    my @dblist;
    foreach( keys %$lookup )
    {
        push( @dblist, $1 ) if( /^database (.+)$/ ) && $#{[keys %{$lookup->{$_}}]} > -1;
    }

    @dblist = sort {
        keycompare( $lookup->{"database ".$a}{database}[0]{datapos}, $lookup->{"database ".$b}{database}[0]{datapos} );
    } @dblist;
    y2debug(qd(\@dblist));
    y2debug("<- readDBList");
    return [@dblist];
}

#---------------------------------------------------------------------------#
#
#   parser functions
#
#---------------------------------------------------------------------------#

##
## sub initData( $fh )
##
## fills the data structure from opened filehandle $fh
##
## returns true on success, undef on error
##
sub initData
{
    my $class = shift || return undef;
    my $filename = shift || return $class->SetError(
        summary => "Missing filehandle in initData",
        code    => "LDAPSERVER_INTERNAL"
    );

    y2debug("-> initData");
    #check filename... this is done again in __init_rec, but there it would produce a
    #possibly misleading error summary ('Include file location...')
    if( $filename !~ /^\// )
    {
        return $class->SetError(
            summary => "Config file location has to be absolute, seen '$filename'",
            code    => "LDAPSERVER_ILLEGAL_FILENAME"
        );
    }

    #FIXME: this function gets called before the md5check is made, so it should not change
    #anything when a config is already there.

    #register main config file
    $files->{$filename} = $data;

    #create global lookup section
    $lookup->{'global'} = {} if not defined $lookup->{'global'};
    
    my $ret = $class->__init_rec( $filename, $data, $lookup->{'global'} );

    y2debug("Data after __init_rec:". qd($data));
    y2debug("Lookup after __init_rec:". qd($lookup));
    #check if the dummy database (default of openldap2.rpm) exists
    my @dblist = grep { /^database / } keys %$lookup;
    if( scalar @dblist == 1 && defined $ret )
    {
        my $opt = $lookup->{$dblist[0]}{directory};
        my $opt_val = $opt->[0]{dataref}{$opt->[0]{datapos}}{content_string} if defined $opt;

		#FIXME: extremely ugly check to determine if the database entry is the one from slapd.conf.default
        if( defined $opt_val && $opt_val =~ m#^/var/lib/ldap/?$# )
        {
			chomp $opt_val;
			y2debug("checking for default slapd.conf database entry in <$opt_val>");
            #print STDERR "clean_dummy: seen the right directory value: $opt_val\n";
            #check if directory contains files
            my $database_exists = 0;
            opendir( my $dir, $opt_val ) || ( my $dir_not_there = 1 );
            if( not defined  $dir_not_there )
            {
                while( defined( my $file = readdir( $dir ) ) )
                {
                    next if $file =~ /^\.\.?$/;
                    if( $file eq "id2entry.bdb" || $file eq "id2entry.dbb" )
                    {
                        #print STDERR "file is $file, -> database is valid\n";
						y2debug("found database, leaving alone");
                        $database_exists = 1;
                        last;
                    }
                }

                
                if( not $database_exists )
                {
                    #delete database
                    #print STDERR "clean_dummy: directory is empty, deleting database\n";
					y2debug("did not find database, clearing section <$dblist[0]>");
                    $class->deleteSection( $dblist[0] );
                }
            }
        }
    }

    return $ret;
}

##
## sub __init_rec( $filename, $data, $section )
##
## reads configuration from $filename into datastructure $data and into lookup section $section
## descending through include directives (that contain no schema definitions)
##
## TODO: rewrite/split up
##
sub __init_rec
{
    my $class = shift || return undef;
    my $filename = shift || return $class->SetError(
        summary => "Missing filename in __init_rec",
        code    => "SCR_MISSING_ARG"
    );
    
    my $active_data = shift || return $class->SetError(
        summary => "Missing data reference in __init_rec",
        code    => "SCR_MISSING_ARG"
    );
    
    my $active_section = shift || return $class->SetError(
        summary => "Missing section reference in __init_rec",
        code    => "SCR_MISSING_ARG"
    );
    
    open( my $cfh, '<', $filename ) or return $class->SetError(
        summary => "Failed to open file $filename for reading!",
        code    => "LDAPSERVER_OPEN_FAILED"
    );

    y2debug("-> __init_rec");
    #generate md5sum
    binmode( $cfh );
    my $md5sum_new = Digest::MD5->new->addfile( $cfh )->hexdigest || do {
        close( $cfh );
        return $class->SetError(
            summary => "generation of MD5 checksum failed in __init_rec",
            code    => "LDAPSERVER_INTERNAL"
        );
    };
    binmode ( $cfh, ':perlio' );
    seek( $cfh, 0, 0 );

    if( exists( $active_data->{__internal} ) && $active_data->{__internal}{md5sum} eq $md5sum_new )
    {
        y2debug( "cached data from $filename is still up to date, skipping initialization" );
        close( $cfh );
        return 1;
    }

    #remove old data
    foreach( keys %$active_data )
    {
        delete $active_data->{$_};
    }

    #remove old entries in lookup hash
    while( my ($sect_name, $section) = each %$lookup )
    {
        y2debug("cleanup");
        y2debug("act sectname: $sect_name");
        while( my ($name, $optlist) = each %$section )
        {
            y2debug("$name optlist: ".qd($optlist));
            $sect_name =~ /^(\S+)/;
            my $section_type = $1;
            my $constr = $class->getConstraints( $section_type, $name );
            y2debug("constr: ".qd($constr));
            if( $constr && $constr->{subsection} ){
                foreach my $opt (@$optlist) {
                    while ( my ($subsect_key, $subsect_opt) = each %{$opt->[1]} ){
                        my @newlist = grep { $#{[keys %{$_->{dataref}}]} > -1 } @$subsect_opt;
                        y2debug("$name $#newlist newlist: ".qd(\@newlist));
                        if( $#newlist > -1 )
                        {
                            $opt->[1]->{$subsect_key} = \@newlist;
                        } else
                        {
                            delete $opt->[1]->{$subsect_key};
                        }
                    }
                }
                y2debug("new $name optlist: ".qd($optlist));
            } else {
                my @newlist = grep { $#{[keys %{$_->{dataref}}]} > -1 } @$optlist;
                y2debug("$name $#newlist newlist: ".qd(\@newlist));
                if( $#newlist > -1 )
                {
                    $section->{$name} = \@newlist;
                } else
                {
                    delete $section->{$name};
                }
            }
        }
#        #delete $lookup->{$sect_name} if( $#{[keys %$section]} == -1 );
    }

    $active_data->{__internal} = {};
    $active_data->{__internal}{md5sum} = $md5sum_new;
    $active_data->{__internal}{filename} = $filename;

    y2debug( "reading data from file $filename" );
   

    #currently active option
    my $last_opt = "";
    my $overlay_prefix ="";
    my $parent_section = {};

    #value of the currently active option
    my $content = "";

    #current position in data structure
    my $datapos = 1;
    
    #whitespace between option name and content
    my $opt_ws = '';
    while( <$cfh> )
    {    
        if( /^\s+/ || /^$/ )
        {
            #FIXME: what happens if first line starts with whitespace?
            $content .= $_;
            #s/\n/\\n/;
            #y2debug( "adding '$_' to <$last_opt> at position <$datapos>" );
            next;
        } elsif( /^#/ )
        {
            if( $last_opt ne 'comment' )
            {
                if( $last_opt ne "" )
                {
                    $class->addOptionData({
                        data    =>  $active_data,
                        section =>  $active_section,
                        datapos =>  $datapos,
                        name    =>  $last_opt,
                        content =>  $content,
                        ws      =>  $opt_ws
                    });
                    $class->inc( \$datapos );
                    $content = "";
                }
                $last_opt = 'comment';
                $opt_ws = '';
            }
            $content .= $_;
            next;
        } elsif( /^([^\s#]+)((\s+)(.+))?/s ) 
        {
            my $opt_name    = $1;
            my $ws_tmp      = "";
            my $opt_val     = "";
            if ( defined $2 ) {
                $ws_tmp      = $3;
                $opt_val     = $4;
            }
            if( $last_opt ne "" ) {
                $class->addOptionData({
                    data    =>  $active_data,
                    section =>  $active_section,
                    datapos =>  $datapos,
                    name    =>  $last_opt,
                    content =>  $content,
                    ws      =>  $opt_ws
                });
                $class->inc( \$datapos );
                $content = "";
            }
            if( $overlay_prefix ne "" ) {
                if ( $opt_name =~ /^$overlay_prefix/s ) {
                    y2debug("option for current overlay : $opt_name <$opt_val> lastopt: $last_opt");
                } else {
                    y2debug("overlay section ends here $opt_name $overlay_prefix");
                    y2debug("overlay section: ".qd($active_section));
                    if ( exists $parent_section->{'overlay'} ) {
                        push @{$parent_section->{'overlay'}}, ( [ $overlay_prefix,$active_section ] );
                    } else {
                        $parent_section->{'overlay'} = [ [ $overlay_prefix, $active_section ] ];
                    }
                    $overlay_prefix = "";
                    y2debug("parent section: ".qd($parent_section));
                    $active_section = $parent_section;
                    $parent_section = {};
                }
            }
            $opt_ws      = $ws_tmp;
            if( $opt_name eq "include" )
            {
                #FIXME: use a more intelligent way to detect schema includes
                if( $opt_val =~ /\.schema$/ )
                {
                    $last_opt = "schemainclude";
                    $content .= $opt_val;
					chomp $opt_val;
                    #$opt_val =~ s/\n/\\n/;
                    y2debug( "starting new <$last_opt> with value <$opt_val> at position <$datapos>" );
                    next;
                }
                
                #store current read position and close file
                my $pos = tell( $cfh );
                close( $cfh ) || return $class->SetError(
                    summary => "failed to close file '$filename'",
                    code    => "LDAPSERVER_CLOSE_FAILED"
                );

                #write the include option
                my $option_data = $class->addOptionData({
                    data    =>  $active_data,
                    section =>  $active_section,
                    datapos =>  $datapos,
                    name    =>  $opt_name,
                    content =>  $opt_val,
                    ws      =>  $opt_ws
                });

                $class->inc( \$datapos );
                $last_opt = "";
                $content = "";

                my $newfilename = $opt_val;
                $newfilename =~ s/\s+$//;

                #register the file in the global file repository
                $files->{$newfilename} = {} if( !defined( $files->{$newfilename} ) );

                #put a reference to the data structure in the include option hash
                $option_data->{parsed_file} = $files->{$newfilename};

                #parse file
                $class->__init_rec( $newfilename, $files->{$newfilename}, $active_section ) || return undef;

                #reopen the own file
                open( $cfh, '<', $filename ) or return $class->SetError(
                    summary => "Failed to open file $filename for reading!",
                    code    => "LDAPSERVER_OPEN_FAILED"
                );

                #restore saved file position
                seek( $cfh, $pos, 0 );
                next;
                
                
            } elsif( $opt_name eq "backend" ) {
                #start new backend section
                $opt_val =~ /^(\S+)/;
                $active_section = {};
                $lookup->{"backend ".$1} = $active_section;
            } elsif( $opt_name eq "database" ) {
                #start new database section
                my $suffix = $class->findFirstSuffix( $cfh );
                $active_section = {};
                $lookup->{"database ".$suffix} = $active_section;
            } elsif( $opt_name eq "overlay" ) {
                $overlay_prefix = $opt_val;
                chomp($overlay_prefix);
                $parent_section = $active_section;
                $active_section = {};
                y2milestone("found overlay section: $opt_name <$overlay_prefix>");
            }
            $last_opt = $opt_name;
            $content .= $opt_val;
            
            #$opt_val =~ s/\n/\\n/;
            #y2debug( "starting new <$last_opt> with value <$opt_val> at position <$datapos>" );
            next;
        } else {
            y2milestone("__init_rec() bogus line: $_");
        }
    }

    #write last option
    if( $last_opt ne "" ) {
        $class->addOptionData({
            data    =>  $active_data,
            section =>  $active_section,
            datapos =>  $datapos,
            name    =>  $last_opt,
            content =>  $content,
            ws      =>  $opt_ws
        });
        if( $overlay_prefix ne "" ) {
            y2debug("overlay section ends here prefix: $overlay_prefix");
            y2debug("overlay section: ".qd($active_section));
            if ( exists $parent_section->{'overlay'} ) {
                push @{$parent_section->{'overlay'}}, ( [ $overlay_prefix,$active_section ] );
            } else {
                $parent_section->{'overlay'} = [ [ $overlay_prefix, $active_section ] ];
            }
            $overlay_prefix = "";
            y2debug("parent section: ".qd($parent_section));
            $active_section = $parent_section;
            $parent_section = {};
        }
    }
    
    close $cfh or return $class->SetError(
        summary => "Failed to close file $slapdconf",
        code    => "LDAPSERVER_CLOSE_FAILED"
    );

    return 1;
    
}

##
## sub addOptionData( $name, $content, $datapos, \%section )
##
## stores an option in the internal data structure
## returns the option's created data hash
##
## TODO: error checking
##
sub addOptionData
{
    my $class   = shift;
    my $config  = shift;
    my $data    = $config->{data};
    my $section = $config->{section};
    my $datapos = $config->{datapos};
    my $name    = $config->{name};
    my $content = $config->{content};
    my $ws      = $config->{ws};
    
    y2debug("-> addOptionData");
    $data->{"$datapos"} = {
        type            => $name,
        content_string  => $content,
        ws              => $ws,
        md5             => $class->getOptionMD5( $content )
    };           

    if($class->__mayQuote($name)) {
        if($content =~ /^"/) {
            y2debug("addOptionData: dequote '$content'");
            $content =~ s/^"//;
            $content =~ s/"(\s*)$/$1/;
            $content =~ s/\\"/"/g;
            $content =~ s/\\\\/\\/g;
            $data->{"$datapos"}->{content_string} = $content;
            y2debug("addOptionData: finish dequote '$content'");
        }
    }

    if( !exists( $section->{$name} ) )
    {
        $section->{$name} = [{datapos=>"$datapos",dataref=>$data}];
    } else
    {
        push( @{$section->{$name}}, {datapos=>"$datapos",dataref=>$data} );
    }
    return $data->{"$datapos"};
}

##
## sub findFirstSuffix( $fh )
##
## scans through the file behind filehandle $fh and returns the value of the first 
## suffix line it finds. $fh is reset to the file position in had before the function
## call. $fh has to be positioned at the beginning of the 'database'statement line.
##
## TODO: error when seeing database statement without seeing suffix before?
##
sub findFirstSuffix
{
    my $class = shift || return undef;
    my $cfh = shift;
    
    y2debug("-> findFirstSuffix");

    my $pos = tell( $cfh );

    while( <$cfh> )
    {
        next if( !/^suffix\s+(.+)$/ );
        my $suffix = $1;
        $suffix =~ s/\s+$//;

        if($class->__mayQuote("suffix")) {
            if($suffix =~ /^"/) {
                y2debug("findFirstSuffix: do dequote '$suffix'");
                $suffix =~ s/^"//;
                $suffix =~ s/"(\s*)$/$1/;
                $suffix =~ s/\\"/"/g;
                $suffix =~ s/\\\\/\\/g;
                y2debug("findFirstSuffix: finish dequote '$suffix'");
            }
        }

        seek( $cfh, $pos, 0 );
        return $suffix;
    }
    seek( $cfh, $pos, 0 );
    return undef;

}

##
## sub updateSingleOption( $section_name, $option_name, ( \@valuelist | $single_value ) )
##
## updates the values of the given option
##
## this is the place where the decision is made at what position in which file 
## some option will be written.
##
## i guess this could be optimized/enhanced indefinitely :)
##
## TODO: support multiple files
## TODO: split up in smaller functions
##
sub updateSingleOption
{
    my $class = shift || return undef;

    my ($section_name, $opt_name, $opt_val ) = @_;
    y2debug("-> updateSingleOption $section_name, $opt_name");
    y2debug("-> updateSingleOption optval: ". qd($opt_val));

    my $section = $lookup->{$section_name};
    y2debug("Old data section: ". qd($data));
    y2debug("Old lookup section: ". qd($section));
    $section_name =~ /^(\S+)/;
    my $section_type = $1;
    y2debug( "updateSingleOption: trying to add option <$opt_name> to section <$section_name>" );

    my $insert_pos = undef;
    #return successfully if a non-existing option is tried to delete
    if( !exists( $section->{$opt_name} ) && !defined $opt_val )
    {
        y2debug( "updateSingleOption: trying to delete non-existing option '$opt_name' from section '$section_name', skipping function" );
        return 1;
    }
    
    #get constraints for the option
    my $constr = $class->getConstraints( $section_type, $opt_name ) || return $class->SetError(
        summary => "writing option '$opt_name' in $section_type section is not supported",
        code    => "PARAM_CHECK_FAILED"
    );


    my $reftype = ref( $opt_val ) || undef;
    #only array references are allowed
    if( defined $reftype && $reftype ne "ARRAY" ) {
        return $class->SetError(
            summary => "option value has to be list reference or scalar, seen [$reftype]",
            code    => "PARAM_CHECK_FAILED"
        );
    }

    #bail if option list is passed for single only option
    if( !$constr->{multi} && $reftype ) {
        return $class->SetError(
            summary => "multiple '$opt_name' options are not allowed",
            code    => "PARAM_CHECK_FAILED"
        );
    }

    #bail if option is not supported by backend
    if( $section_type eq "database" || $section_type eq "backend" )
    {
        y2debug("updateSingleOption: checking constraints for option <$opt_name> in section <$section_type>");
        my $back_opt = $section->{$section_type}->[0];
        
        my $back_type = $back_opt->{dataref}->{$back_opt->{datapos}}->{content_string};
        chomp $back_type;
        y2debug("updateSingleOption: back_opt: <$back_opt>, back_type: <$back_type>");
        
        #local $Data::Dumper::Maxdepth = 2;
        #print STDERR "Constraints hash for option '$opt_name':\n".qd( $constr );
        
        $back_type =~ s/^(\S+).*$/$1/s;

        if( !exists( $constr->{backend}->{$back_type} ) && !exists( $constr->{backend}->{all} ) )
        {
            return $class->SetError(
                summary => "option '$opt_name' is not supported by backend $back_type",
                code    => "PARAM_CHECK_FAILED"
            );
        }
    }

    #bail if multiple options of the same type are distributed between different files
    if( !$constr->{subsection} && $constr->{multi} && exists( $section->{$opt_name} ) )
    {
        my $file = $section->{$opt_name}->[0]{dataref}{__internal}{filename};
        local $Data::Dumper::Maxdepth = 2;
        foreach my $opt ( @{$section->{$opt_name}} )
        {
            my $tmpfile = $opt->{dataref}->{__internal}->{filename};
            #print STDERR "tmpfile: $tmpfile\tfile: $file\n";
            next if( $tmpfile eq $file );
            return $class->SetError(
                summary => "Multiple options of type $opt_name are spanned between 2 or more files. The current agent version cannot handle this.",
                code    => "LDAPSERVER_INTERNAL"
            );
        }
    }

    #generate dummy option (which will be replaced later) if the option does not exist yet
    #TODO: some of this could be sourced out into a sub (getWritePosition or similar)
    if( !exists( $section->{$opt_name} ) )
    {
        y2debug( "updateSingleOption: generating dummy option for option '$opt_name' in section '$section_name'" );
        #check if option depends on something
        foreach my $deps_on ( keys %{$constr->{after}} )
        {
            if( !exists( $section->{$deps_on} ) )
            {
                return $class->SetError(
                    summary => "option '$opt_name' depends on '$deps_on', which doesnt exist in section '$section_name'",
                    code    => "PARAM_CHECK_FAILED"
                );
            }
        }

        #FIXME: finish this part (reverse constraint checking)
        #my $before_pos = undef;
        #foreach my $sect_opt_name ( keys %$section )
        #{
        #    next if $sect_opt_name eq $opt_name;
        #    my $so_constr = $class->getConstraints( $section_type, $sect_opt_name );
        #    if( exists( $so_constr->{after}{$opt_name} ) )
        #    {
        #        #the 'to be written' option has to appear before the currently checked option
        #        my @so_poslist = sort {
        #                         $class->keycompare( $a->{datapos}, $b->{datapos} ) 
        #                      } $section->{$sect_opt_name};
        #        #TODO: finish here
        #    }
        #}


        #special handling for the schema includes: insert at beginning of file (bug #40331)
        #TODO: solve with 'before' constraint
        if( $opt_name eq "schemainclude" )
        {
            #get first position in list
            my @dataposlist = $class->sortDataposList( keys %$data );
            $insert_pos = $dataposlist[0];
            $class->dec( \$insert_pos );
        }
        
        #check the 'before' constraint
        #if( !defined $insert_pos && exists( $constr->{before} ) )
        #{
        #}

        if( !defined $insert_pos )
        {
            y2debug( "searching to last datapos of section '$section_name'" );
            #FIXME: this has to be changed for multiple files support
            my @dataposlist = sort { $class->keycompare( $a, $b ) } keys %$data;
            shift @dataposlist;

            my $section_start = undef;
            $section_start = $dataposlist[0] if $section_type eq 'global';
            $section_start = $section->{$section_type}[0]{datapos} if not defined $section_start;
            my $in_section = 0;
            y2debug( "starting to find end position of section '$section_name'" );
            y2debug( "section_start is: $section_start" );
            foreach my $dp ( @dataposlist )
            {
                my $cp = $class->keycompare( $dp, $section_start );
                #y2debug( "----------------------------------------" );
                #y2debug( "dp: $dp" );
                if( $cp == -1 ) {
                    next;
                } elsif( $cp == 0 ) {
                    $in_section = 1;
                    next;
                } elsif( $cp == 1 )  {
                    my $type = $data->{$dp}{type};
                    #y2debug( "type: $type" );
                    $in_section = 0 if( $type eq 'backend' || $type eq 'database' );
                    $in_section = 0 if( $type eq 'overlay' && $opt_name ne 'overlay' );
                    if( $in_section )
                    {
                        $insert_pos = $dp;
                        next;
                    } else
                    {
                        last;
                    }
                } else
                {
                    return $class->SetError(
                        summary => "some strange undefinable error occured while comparing two datapositions",
                        code    => "LDAPSERVER_INTERNAL"
                    );
                }
            } #foreach ( @dataposlist )
            $insert_pos .= ".1";
        } #if( !defined $insert_pos )

        y2debug( "updateSingleOption: found insert position for '$opt_name' at <$insert_pos>" );

        #create the dummy option
        if (! $constr->{'subsection'}) {
            $class->addOptionData({
                data    =>  $data,
                section =>  $section,
                datapos =>  $insert_pos,
                name    =>  $opt_name,
                content =>  "DUMMY",
                ws      =>  ' '
            });
        }

        y2debug( "Dummy data:" .qd( $section->{$opt_name} ));
    }
    
    if ( $constr->{subsection} )  # this is a subsection (e.g. overlay)
    {
        my $insert_pos_add = 1;
        foreach my $act_optval ( @$opt_val ) {
            my $subsection_name = $act_optval->[0];
            my $subsection_opts = $act_optval->[1];
            y2debug("subsection name: $subsection_name");
            y2debug("subsection opts: ". qd($subsection_opts));
            
            # check if the subsection is already present
            my $subsection_pos = 0;
            my $subsection_exists = 0;
            my $subsection_hash = undef;
            foreach my $subsection (@{$section->{$opt_name}}){
                if( $subsection->[0] eq $subsection_name ) {
                    $subsection_hash = $subsection->[1];
                    $subsection_exists = 1;
                    last;
                }
                $subsection_pos++;
            }
            y2debug("subsection position is: $subsection_pos");
            y2debug("subsection is: ". qd ($subsection_hash));

            # should this subsection be removed ?
            if (! defined $subsection_opts ) {
                y2debug("deleteing subsection $opt_name $subsection_name");
                y2debug("old lookup section ". qd($section));
                foreach my $key (keys %{$section->{$opt_name}->[$subsection_pos]->[1]}) {
                    delete $data->{ $section->{$opt_name}->[$subsection_pos]->[1]->{$key}->[0]->{'datapos'}};
                }
                splice @{$section->{$opt_name}},$subsection_pos,1;
            } else {
                if (! defined $insert_pos){
                    y2debug("looking for new insert_pos");
                    # seems the current subjection does already exist in the file
                    # find  the data position of the current option
                    y2debug('$section->{$opt_name}->[0]->[1]' .qd($section->{$opt_name}->[0]->[1]));
                    $insert_pos = $section->{$opt_name}->[0]->[1]->{$opt_name}->[0]->{'datapos'};
                }
                y2debug("insert_pos: $insert_pos");
                my $sub_insertpos = "$insert_pos.$insert_pos_add";
                if (! defined $subsection_hash) {
                    y2debug("this is a new subsection");
                    $data->{"$sub_insertpos"} = {
                        type            => $opt_name,
                        content_string  => $subsection_name,
                        ws              => ' ',
                        md5             => $class->getOptionMD5( $subsection_name )
                    };           

                    if($class->__mayQuote($opt_name)) {
                        if($subsection_name =~ /^"/) {
                            $subsection_name =~ s/^"//;
                            $subsection_name =~ s/"(\s*)$/$1/;
                            $subsection_name =~ s/\\"/"/g;
                            $subsection_name =~ s/\\\\/\\/g;
                            $data->{"$sub_insertpos"}->{content_string} = $subsection_name;
                        }
                    }
                    $subsection_hash = {  $opt_name => [{datapos=>"$sub_insertpos",dataref=>$data}] };
                    $insert_pos_add++;
                } else {
                    # clean up existing subsection
                    y2debug("clean up existing subsection ");
                    y2debug("subsection_opt ".qd($subsection_opts));
                    foreach my $key ( keys %$subsection_hash ) {
                        if  ( $key eq $opt_name ) {
                            next;
                        } else {
                            delete $data->{ $section->{$opt_name}->[$subsection_pos]->[1]->{$key}->[0]->{'datapos'}};
                            delete $subsection_hash->{$key};
                        }
                    }
                }
                while( my( $opt, $val ) = each %$subsection_opts ) {
                    $sub_insertpos = "$insert_pos.$insert_pos_add";
                    $data->{"$sub_insertpos"} = {
                        type            => $opt,
                        content_string  => $val,
                        ws              => ' ',
                        md5             => $class->getOptionMD5( $val )
                    };           

                    if($class->__mayQuote($opt)) {
                        if($val =~ /^"/) {
                            $val =~ s/^"//;
                            $val =~ s/"(\s*)$/$1/;
                            $val =~ s/\\"/"/g;
                            $val =~ s/\\\\/\\/g;
                            $data->{"$sub_insertpos"}->{content_string} = $val;
                        }
                    }
                    $subsection_hash->{$opt} =  [{datapos=>"$sub_insertpos",dataref=>$data}];
                    $insert_pos_add++;
                }
                if( !exists( $section->{$opt_name} ) )
                {
                    $section->{$opt_name} = [[$subsection_name, $subsection_hash]];
                } else
                {
                    if ($subsection_exists == 1) {
                        $section->{$opt_name}->[$subsection_pos] = [$subsection_name, $subsection_hash] ;
                    } else {
                        push( @{$section->{$opt_name}},[$subsection_name, $subsection_hash]  );
                    }
                }
            }
       }
       y2debug("New data section: ". qd($data));
       y2debug("New lookup section: ". qd($section));
       return 1;
    }
    elsif( $constr->{multi} )
    {
        #multi option
        #TODO: has to be changed for multiple files support
        
        my $optionlist = $class->sortOptionList( $section->{$opt_name} );
        my @dataposlist = sort { $class->keycompare( $a, $b ) } keys %$data;

        #check if all options of this are placed with no other option between
        my $i = 1;
        my $found = 0;
        my $option_between = undef;
        foreach my $pos ( @dataposlist )
        {
            last if( scalar @$optionlist == 1 );
            if( not $found )
            {
                next if( $pos ne $optionlist->[0]->{datapos} );
                $found = 1;
            } else
            {
                last if( $i == $#$optionlist );
                if( $pos ne $optionlist->[$i++]->{datapos} )
                {
                    $option_between = $data->{$pos}->{type};
                    last;
                }
            }
        } # foreach( @dataposlist )
       
        return $class->SetError(
            summary => "while trying to write option '$opt_name', some other option ('".$option_between."') was found inbetween. This cannot be handled by the current agent version.",
            code    => "LDAPSERVER_INTERNAL"
        ) if defined $option_between;

        #no option is between the options to write, so just remove the old ones and put the new ones
        #at the same place
        
        my $base_pos = $optionlist->[0]->{datapos};
        my $opt_ws = $data->{$base_pos}->{ws};
        
        foreach( @$optionlist )
        {
            delete( $data->{$_->{datapos}} );
        }

        $section->{$opt_name} = [];
        
        if( ref( $opt_val ) )
        {
            for( $i = 0; $i <= $#$opt_val; $i++ )
            {
                $class->addOptionData({
                    data    =>  $data,
                    section =>  $section,
                    datapos =>  "$base_pos.".($i+1),
                    name    =>  $opt_name,
                    content =>  $opt_val->[$i],
                    ws      =>  $opt_ws
                });
            }
        } elsif( defined( $opt_val ) )
        {
            $class->addOptionData({
                data    =>  $data,
                section =>  $section,
                datapos =>  $base_pos,
                name    =>  $opt_name,
                content =>  $opt_val,
                ws      =>  $opt_ws
            });
        } else
        {
            delete $section->{$opt_name};
        }
        y2debug( "Section for '$opt_name': " );
        $data->{__internal}{dirty} = 1;
        return 1;
        
    }
    else  # simple single values statement
    {
        #single-only option - just replace the content_string
        my $opt = $section->{$opt_name}->[0];
        if( defined $opt_val )
        {
            $opt->{dataref}->{$opt->{datapos}}->{content_string} = $opt_val;
        } else
        {
            delete( $opt->{dataref}->{$opt->{datapos}} );
            delete( $section->{$opt_name} );
        }
        $opt->{'dataref'}{'__internal'}{'dirty'} = 1;
        
        return 1;
    }
        
    return $class->SetError(
        summary => "undefined error while trying to update option '$opt_name' in section '$section_name'",
        code    => "LDAPSERVER_INTERNAL"
    );
}

##
## sub isOptionBetween
##

##
## sub getConstraints( $section_type, $opt_name )
##
## returns the constraints hash for specified option or undef if option
## is not supported in this section
##
## constraints are inherited from the previous section, so database inherits from backend,
## backend inherits from global
##
## section_type is one of 'global', 'backend', 'database'
##
sub getConstraints
{
    my $class = shift || return undef;
    my ($section_type, $opt_name ) = @_;
    
    y2debug("-> getConstraints <$section_type> <$opt_name>");
    
    my $c_hash = undef;
    my $not_found = 0;
    
    SWITCH: for( $section_type )
    {
        /^database$/ && do
            {
                $c_hash = $constraints->{'database'}->{$opt_name};
                $not_found = 1 if( !$c_hash );
            };
        ( /^backend$/ || $not_found ) && do
            {
                $c_hash = $constraints->{'backend'}->{$opt_name};
                $not_found = 1 if( !$c_hash );
            };
        ( /^overlay$/ || $not_found ) && do
            {
                $c_hash = $constraints->{'overlay'}->{$opt_name};
                $not_found = 1 if( !$c_hash );
            };
        ( /^global$/ || $not_found ) && do
            {
                $c_hash = $constraints->{'global'}->{$opt_name};
                $c_hash->{backend} = { all => 1 } if defined $c_hash && $_ ne "global";
            };
        #let the option depend on section delimiter (backend/database option) if no 'after'
        #constraint exists yet
        $c_hash->{after}->{$_} = 1 if( defined $c_hash && !defined $c_hash->{after} && $_ ne "global" );
        last SWITCH;
    }
    return $c_hash;
}

##
## sub rewriteChangedFiles()
##
## writes all files that have changed to disk
##
sub rewriteChangedFiles
{
    my $class = shift || return undef;
    #return 1 if $delaywrite;
    y2debug("-> rewriteChangedFile");

    while( my( $filename, $data ) = each %$files )
    {
        next if !defined( $data->{__internal}->{dirty} );
        delete $data->{__internal}{dirty};
        my @range = sort { $class->keycompare( $a, $b ) } keys %$data;
      
        my $file = "";
        foreach my $key ( @range )
        {
            next if $key eq "__internal";
            my $opt = $data->{"$key"};

            if( $opt->{type} eq "comment" ){
            } elsif( $opt->{type} eq "schemainclude" )
            {
                $file .= "include";
            } else
            {
                $file .= $opt->{type};
            }
            $file .= $opt->{ws};

            if($class->__mayQuote($opt->{type})) {
                my $content = $opt->{content_string};

                y2debug("rewriteChangedFiles: quote '$content'");

                $content =~ s/\\/\\\\/g;
                $content =~ s/"/\\"/g;
                $content =~ s/(\S)(\s*)$/$1"$2/;
                $content =~ s/^/"/;

                y2debug("rewriteChangedFiles: finish quote '$content'");

                $file .= $content;
            } else {
                $file .= $opt->{content_string};
            }
            if( $file !~ /\n$/ )
            {
                $file .= "\n";
                y2debug( "adding \\n to <".$opt->{type}."> at position <$key>" );
            };
        }
        
        $filename .= ".".$debugsuffix if defined $debugsuffix;
        open( my $fh, ">", $filename ) or return $class->SetError(
            summary => "unable to open $filename for writing",
            code    => "LDAPSERVER_OPEN_FAILED"
        );

        print $fh $file;

        close( $fh );
    }
    return 1;
}

##
## sub deleteSection( $section_name )
##
## deletes specified section from configuration
## returns 1 on success and undef on error
##
## TODO: change for multiple file support
## TODO: parameter checking
##
sub deleteSection
{
    my $class = shift;
    my $s_name = shift || return undef;

    return 1 if not exists $lookup->{$s_name};

    y2debug( "deleting section '$s_name'" );
    
    my $section = $lookup->{$s_name};
    while( my ($opt_name, $optlist) = each %$section )
    {
        $s_name =~ /^(\S+)/;
        my $section_type = $1;
        my $constr = $class->getConstraints( $section_type, $opt_name );
        if( $constr && $constr->{subsection} ){
            foreach my $opt (@$optlist) {
                while ( my ($subsect_key, $subsect_opt) = each %{$opt->[1]} ){
                    my $datapos =  $opt->[1]->{$subsect_key}->[0]->{datapos};
                    delete $opt->[1]->{$subsect_key}->[0]->{dataref}->{$datapos};
                    delete $opt->[1]->{$subsect_key};
                }
            }
        } else {
            foreach my $opt ( @$optlist )
            {
                delete $opt->{dataref}{$opt->{datapos}};
            }
        }
    }
    delete $lookup->{$s_name};
    return 1;
}

##
## sub inc( \$datapos )
##
## increases the internal data index type by 1
##
sub inc
{
    my $class = shift || return undef;
    my $num = shift || return $class->SetError(
        summary => "Missing argument to function inc",
        code    => "LDAPSERVER_MISSING_ARG"
    );
    y2debug("-> inc");
    my @a = split( /\./, $$num );
    $a[$#a]++;
    $$num = join( ".", @a );
}

##
## sub dec( \$datapos )
##
## decreases internal data index type by 1
##
sub dec
{
    my $class = shift || return undef;
    my $num = shift || return undef;
    y2debug("-> dec");

    my @a = split( /\./, $$num );
    $a[$#a]--;
    $$num = join( ".", @a );
}

sub sortDataposList
{
    my $class = shift || return undef;
    my @list = @_ || return undef;
    y2debug("-> sortDataposList");
    return undef if( ref($list[0]) eq "LIST" );

    my @newlist = sort { $class->keycompare( $a, $b ) } @list;
    return \@newlist;
}

sub __mayQuote {
    my $class = shift;
    my $key = shift;
    y2debug("-> __mayQuote");

    if(exists $quote->{$key}) {
        return $quote->{$key};
    }
    return 0;
}

sub sambaACLHack {
    my $class = shift;
    my $dn = shift;
    y2debug("-> sambaACLHack");
    my $file = '/etc/openldap/slapd.conf';
    my $acl = "## Yast2 samba hack ACL\n";
    $acl   .= "## allow the \"ldap admin dn\" access, but deny everyone else\n";
    $acl   .= "access to attrs=SambaLMPassword,SambaNTPassword\n";
    $acl   .= "    by dn=\"$dn\" write\n";
    $acl   .= "    by * none\n";
    $acl   .= "## Yast2 samba hack ACL done\n";

    open( SLAPD, "< $file" ) or return $class->SetError(
            summary => "unable to open $file for reading",
            code    => "LDAPSERVER_OPEN_FAILED"
    );
    my @slapd = <SLAPD>;
    close(SLAPD);

    # kick out old samba hack ACLs
    for( my $i=0; $i<@slapd; $i++ ) {
        next unless( $slapd[$i] =~ /^## Yast2 samba hack ACL/ );
        do {
            $slapd[$i] = '';
        } while( $slapd[++$i] !~ /^## Yast2 samba hack ACL done/ );
        $slapd[$i] = '';
    }

    for( my $i=0; $i<@slapd; $i++ ) {
        # make sure the include is there and our ACL is inserted later
        next unless( $slapd[$i] =~ /^include.*samba3.schema/ );

        # search for first ACL or backend oder database section and put the
        # ALC in front of that.
        while( $slapd[$i] !~ /^(access|database|backend)/ and ++$i<@slapd) {}
        $slapd[$i-1] .= "$acl";
        last;
    }

    open( SLAPD, "> $file" ) or return $class->SetError(
            summary => "unable to open $file for writing",
            code    => "LDAPSERVER_OPEN_FAILED"
    );
    print SLAPD @slapd;
    close(SLAPD);
    return 1;
}

##
## sub computeOptionMD5( $option_string )
##
## computes a md5 for a option value
## TODO: handle doubleticks (whatever they are called... -> " <- this char ;) )
## TODO: parameter checks
##
sub getOptionMD5
{
    my $class = shift || return undef;
    my $val = shift;
    y2debug("-> getOptionMD5");

    #chop whitespace
    $val =~ s/\s+$//s;
    #compress whitespace
    $val =~ s/\s+/ /gs;

    return md5_hex( encode_utf8( $val ) );
}

##
## sub sortOptionList( \@list )
##
## sorts a list of options as contained in the lookup section
##
## TODO: support multiple files
##
sub sortOptionList
{
    my $class = shift || return undef;
    my $list = shift;
    y2debug("-> sortOptionList");

    my @newlist = sort { $class->keycompare( $a->{datapos}, $b->{datapos} ) } @$list;
    local $Data::Dumper::Maxdepth = 2;

    return \@newlist;
}

## sub sortByConstraints( $section_type, \@option_names )
##
## sorts a list of option names according to constraints
## after sorting, the options depending on less other options are before the 
## options depending on more.
##
## TODO: parameter checks
##
sub sortByConstraints
{
    my $class = shift;
    my $s = shift;
    my $list = shift;
    y2debug("-> sortByConstraints");
    return sort {
        #print STDERR "comparing $a <-> $b\n";
        my $tmp = $class->getConstraints( $s, $a ) || return 0;
        if ( exists $tmp->{'last'} ) { return 1; }
        my $dep_a = $tmp->{after} || return 0;
        $tmp = $class->getConstraints( $s, $b ) || return 0;
        if ( exists $tmp->{'last'} ) { return -1; }
        my $dep_b = $tmp->{after} || return 0;
        #print STDERR "-------------------------------------\n$a comes after:\n".qd( $dep_a );
        #print STDERR "-------------------------------------\n$b comes after:\n".qd( $dep_b );

        return 1 if exists( $dep_a->{$b} );
        return -1 if exists( $dep_b->{$a} );
        return 0;
    } @$list;
}

##
## sub keycompare( $a, $b )
##
## comparison function to compare two internal data indices
## use as custom comparison function to sort a list of internal data indices
##
## TODO: handle undefined values properly to get rid of 'no warnings'
##
sub keycompare
{
    my $class = shift;
    y2debug("-> keycompare");
    return 0 if $_[0] eq $_[1];
    no warnings;
    my $a = ref( $_[0] ) ? $_[0] : [ split( /\./, $_[0] ) ];
    my $b = ref( $_[0] ) ? $_[1] : [ split( /\./, $_[1] ) ];
    
    my $ax = shift( @$a );
    my $bx = shift( @$b );
    return $ax <=> $bx if $ax != $bx;
    return $class->keycompare( $a, $b );
}

sub qd
{
    return Data::Dumper->Dump( [ @_ ] );
}

package main;
ag_ldapserver->Run;
