#!/usr/bin/perl
# ldapextras.pl
# version 0.11
# licensed under the LGPL - see http://www.gnu.org
# Jules Agee
# August 17 2001
# configuration options need to be defined in ldapextras.conf

sub ldapextras {
    $userisadmin = 1 if( $uid eq $manageruid );
    for( $op ) {
        /ldapsearch/	and do { undef $gomodifyit; ldapsearch();  last; };
        /printuser/	and do { undef $gomodifyit; printuser();   last; };
        /printgroup/	and do { undef $gomodifyit; printgroup();  last; };
        /createuser/	and do { undef $gomodifyit; createuser();  last; };
        /creategroup/	and do { undef $gomodifyit; creategroup(); last; };
        /changeuser/	and do { undef $gomodifyit; changeuser();  last; };
        /changegroup/	and do { undef $gomodifyit; changegroup(); last; };
        /deleteuser/	and do { undef $gomodifyit; deleteuser();  last; };
        /deletegroup/	and do { undef $gomodifyit; deletegroup(); last; };
    }    
    return;
}


# Parses form data from the printuser HTML form when the op parameter
# is set to createuser, and creates a new INBOX and Trash folder,
# then creates an LDAP entry
sub createuser {
    checkbuttons();

    if( !$userisadmin ) {
        autherr();
        return;
    }
    
    # make sure the admin filled out at least the minimum number of fields to
    # successfully create an account
    my %reqatts = ( 'sn'=>'Last Name',
                    'givenname'=>'First Name',
                    'pass1'=>'Password',
                    'pass2'=>'Password (again)',
                    'ldapuid'=>'Employee UID'
                  );
    while( my( $att, $desc ) = each %reqatts ) {
        if( param( $att ) eq '' ) {
            printerr("\"$desc\" is a required field. Try again.");
            printuser();
            return;
        }
    }
    
    if( param( 'pass1' ) ne param( 'pass2' )) {
        printerr("Password fields don't match. Try again.");
        printuser();
        return;
    }

    # Get the new users data from the submitted HTML form
    my $ldapuid = param( 'ldapuid' );
    my $mail = $ldapuid . "\@" . $maildomain;
    my $cn = param( 'givenname' ) ." ". param( 'sn' ) ." (". $ldapuid .")";
    my $dn = "cn=" . $cn . "," . $ldapperson_ou;
    param( 'dn', $dn );

    # create their mailboxes
    $imap = openimap( $uid, $pass, $imapserver, $imapport, $useimapSSL );
    my $err = createmailfolder( "user." . $ldapuid );
    if( $err ) {
        printerr( "createuser: imaperr $err" );
        return;
    }
    $err = createmailfolder( "user." . $ldapuid . ".Trash" );
    if( $err ) {
        printerr( "createuser: imaperr $err" );
        return;
    }
    closeimap();

    $ldap = ldapbind();
    return if $ldap < 0;

    # here we're using the default LDAP atts array to start making a 
    # complete set of attributes for one user
    my $ldapentry = Net::LDAP::Entry->new;

    $ldapentry->dn( $dn );
    $ldapentry->add( %ldappersondefatts );
    $ldapentry->add( 'cn'=> $cn,
                     'uid'=> $ldapuid,
                     'mail'=> $mail,
                     'sn' => param( 'sn' ),
                     'givenname'=> param( 'givenname' ),
                     'mailHost'=> $imapserver,
                     $ldappassattr=>encrypt( param( 'pass1' ))
                   );

    # All possible entries for the @ldappersonatts array are defined
    # in the ldapextras.conf file, with the odd elements naming an LDAP attrib.
    # Or, the even elements, depending how you look at it. The first, third
    # and fifth elements of the array, or array index values 0, 2, 4...
    # In the loop below, for every @ldappersonatts attribute that is
    # defined in an HTML param in the current environment, we want to add
    # the name of the attribute and the value of its corresponding HTML param
    # to the $ldapentry object so we can just call $ldapentry->update to create
    # the user's directory entry
    
    for( $i = 0; $i < $#ldappersonatts; $i += 2 ) { 
        my $key = $ldappersonatts[ $i ];
        my $value = param( $key );
        if( $value ) {
            $ldapentry->add( $key => $value );
        }
    }

    my $result = $ldapentry->update( $ldap );

    if( $result->code ) {
        printerr( "createuser: error creating LDAP entry:". $result->error);
    } else {
        print "User ", param( 'cn' ), 
              " directory entry created successfully.<BR>";
    }
    $ldap->unbind;
    param( 'entrytype', 'user' );
    print hidden( 'entrytype' );
    printuser();
    return; 
}


# Parses form data from the printgroup HTML form when the op parameter
# is set to creategroup, and creates a new LDAP entry for that group
sub creategroup {
    checkbuttons();

    if( !$userisadmin ) {
        autherr();
        return();
    }

    my $cn = param( 'cn' );
    my $dn = "cn=" . $cn . "," . $ldapgroup_ou;
    my $mail = $cn . "\@" . $maildomain;

    my $ldap = ldapbind();
    return if $ldap < 0;

    # @memberDNs is a list of the DNs of entries we are adding to this group

    my @memberDNs;
    my @memberarray = split( '\n', param( 'memberchanges' ));
    my $filter = "(|";

    # for performance reasons we don't want to perform a separate search
    # for every DN in the group, so we'll build a search filter that will
    # only capture these DNs.
    for( @memberarray ) {
        chop;
        $filter .= "(cn=$_)(uid=$_)";
    }
    $filter .= ")";

    $mesg = $ldap->search( base=>$LDAP_BASEDN,
                           filter=>$filter,
                           attrs=>[ "uid", "cn", "objectClass" ]
                         );

    printerr("creategroup:" . $mesg->error) if( $mesg->code );
    @entries = $mesg->entries;
    for( @entries ) {
        push( @memberDNs, $_->dn() );
    }

    # here we're using the default LDAP atts array to start making a 
    # complete set of attributes for one group
    my $ldapentry = Net::LDAP::Entry->new;

    $ldapentry->dn( $dn );
    $ldapentry->add( %ldapgroupdefatts );
    $ldapentry->add( 'cn'=> $cn,
                     'mail'=> $mail,
                     $ldapmemberatt=>\@memberDNs
                   );

    my $result = $ldapentry->update( $ldap );

    if( $result->code ) {
        printerr( "creategroup: error creating LDAP entry:". $result->error);
    } else {
        print "Group ", param( 'cn' )," created successfully in directory.<BR>";
    }
    $ldap->unbind;
    param( 'dn', $dn );
    param( 'entrytype', 'group' );
    print hidden( 'entrytype' );
    printgroup();
    return; 
}


# Parses form data from the printuser HTML form when the ldapaction parameter
# is set to changeuser, and makes changes to the user's LDAP directory entry and
# their quotas if necessary
sub changeuser {
    checkbuttons();

    if( !$userisadmin ) {
        autherr();
        return;
    }
    my $pass1 = param( 'pass1' );
    my $pass2 = param( 'pass2' );

    if( $pass1 ne $pass2 ) {
        printerr("Password fields don't match. Try again.");
        printuser();
        return;
    }

    my $ldapuid = param( 'ldapuid' );

    # set new maxquota value

    my $maxquota = param( 'maxquota' );
    $imap = openimap( $uid, $pass, $imapserver, $imapport, $useimapSSL );
    my $err = setquota( "user.$ldapuid", $maxquota );
    closeimap();

    my $ldap = ldapbind();
    my $dn = param( 'dn' );

    # get the existing entry for this user from the directory
    $ldapentry = ldapget( $ldap, $dn );

    # and update it with the new data
    for( $i = 0; $i < $#ldappersonatts; $i += 2 ) {
        $ldapentry->replace( $ldappersonatts[ $i ]=>
                             param( $ldappersonatts[ $i ] ));
    }
   
    # change password if necessary
    if( $pass1 && $pass1 ne "" ) {
        $ldapentry->replace( $ldappassattr=>encrypt( $pass1 ));
    }

    # and dump it all back onto the server
    my $result = $ldapentry->update( $ldap );

    if( $result->code ) {
        printerr( "changeuser: error changing LDAP entry:". $result->error);
    } else {
        print "User ", param( 'cn' )," updated successfully.<BR>";
    }

    $ldap->unbind;
    undef $ldap;
    print hidden( 'entrytype', 'user' );
    printuser();
    return;
}


# Parses form data from the printgroup HTML form when the op parameter
# is set to changegroup, and makes changes to that group's directory entry
sub changegroup {
    checkbuttons();

    if( !$userisadmin ) {
        autherr();
        return;
    }

    my $dn = param( 'dn' );

    my $ldap = ldapbind();
    return if $ldap < 0;

    # @memberDNs is a list of the DNs of entries in this group

    my @memberDNs;
    my @memberarray = split( '\n', param( 'memberchanges' ));
    my $filter = "(|";

    # for performance reasons we don't want to perform a separate search
    # for every DN in the group, so we'll build a search filter that will
    # capture all the DNs in one search.
    for( @memberarray ) {
        chop;
        $filter .= "(cn=$_)(uid=$_)";
    }
    $filter .= ")";

    $mesg = $ldap->search( base=>$LDAP_BASEDN,
                           filter=>$filter,
                           attrs=>[ "uid", "cn", "objectClass" ]
                         );

    printerr("changegroup:" . $mesg->error) if( $mesg->code );
    @entries = $mesg->entries;
    for( @entries ) {
        push( @memberDNs, $_->dn() );
    }

    # get the existing entry for this group from the directory
    $ldapentry = ldapget( $ldap, $dn );

    # and update it with the new list of members
    $ldapentry->replace( $ldapmemberatt=>\@memberDNs );
    my $result = $ldapentry->update( $ldap );

    if( $result->code ) {
        printerr( "changegroup: error changing LDAP entry:". $result->error);
    } else {
        print "Group ", param( 'cn' )," updated successfully.<BR>";
    }
    $ldap->unbind;
    param( 'dn', $dn );
    param( 'entrytype', 'group' );
    print hidden( 'entrytype' );
    printgroup();
    return; 
}


# Parses form data from the confirmdelete HTML form and deletes that user's
# LDAP entry and IMAP mailboxes. Also deletes references to that user from
# all mail groups.
sub deleteuser {

    if( !$userisadmin ) {
        autherr();
        return;
    }
    $imap = openimap( $uid, $pass, $imapserver, $imapport, $useimapSSL );
    my $err = setacl( "user." . param( 'ldapuid' ), 
                      $uid, 
                      "lrswipcda" 
                    );
    $err .= deletemailbox( "user." . param( 'ldapuid' ));
    if( $err ) {
        printerr( "deleteuser: imapdelerr $err" );
        if( $err =~ m/Mailbox does not exist/ ) {
            print "<BR>LDAP entry only will be deleted<BR>";
        } else {
            return;
        }
    } else {
        print "IMAP account for " . param( 'ldapuid' ) . " deleted.<BR>";
    }
    closeimap();

    $ldap = ldapbind();
    return if( $ldap < 0 );

    $err = "";
    my $dn = param( 'dn' );
    my $mesg = $ldap->delete( ldapclean( $dn ));
    $err .= $mesg->error if ( $mesg->code );

    # get a list of all groups this person was a member of
    my @groups;
    $mesg = $ldap->search( base=>$LDAP_BASEDN,
                              filter=>"( $ldapmemberatt=" .ldapclean($dn). ")",
                              attrs=>[ 'dn' ]
                            );
    $err .= $mesg->error if( $mesg->code );
    
    if( $err ) {
        printerr( "Deleting LDAP entry: $err" );
        return;
    }
    $err = "";
    
    my @results = $mesg->entries;
    foreach $result ( @results ) {
        push( @groups, $result->dn() );
    }

    my $whattodelete = { $ldapmemberatt, $dn };
  
    # And delete the user from each group he/she was listed in
    for( @groups ) {
        my $mesg = $ldap->modify( $_, delete=>$whattodelete );
        $err .= $mesg->error if( $mesg->code );
    }

    if( $err ) {
        printerr( "Deleting LDAP entries: $err" );
    } else {
        print "User ", param( 'cn' ), " deleted successfully.<BR>";
    }

    $ldap->unbind;
    param( 'op', 'ldapsearch' );
    param( 'dn', '' );
    ldapsearch();
    return;
}


# Parses form data from the confirmdelete HTML form and deletes that group's
# entry from the directory

sub deletegroup {
    checkbuttons();

    if( !$userisadmin ) {
        autherr();
        return;
    }

    $ldap = ldapbind();
    return if( $ldap < 0 );

    my $err = "";
    my $dn = param( 'dn' );
    my $mesg = $ldap->delete( ldapclean( $dn ));
    $err = $mesg->error if ( $mesg->code );

    if( $err ) {
        printerr( "Deleting LDAP entries: $err" );
    } else {
        print "Group ", param( 'cn' ), " deleted successfully.<BR>";
    }

    $ldap->unbind;
    param( 'op', 'ldapsearch' );
    param( 'dn', '' );
    ldapsearch();
    return;
}


# Prints a form asking the user to confirm the deletion of a user or group
# when the op attribute is set to confirmdelete. Possible op attribute values 
# generated from this form are deleteuser, deletegroup

sub confirmdelete {
    my $dn = param( 'dn' );
    my $cn = param( 'cn' );
    my $entrytype = param( 'entrytype' );

    if( !$userisadmin ) {
        autherr();
        return;
    }

    if(( $entrytype ne "user" )&&( $entrytype ne "group" )) {
        printerr( "Unknown entry type - can't delete entry $cn." );
        return;
    }
    $op = "delete" . $entrytype;
    param( 'op', $op );
    print hidden( 'dn' );
    print hidden( 'cn' );
    print hidden( 'ldapuid' );
    
    print "<CENTER><BR><H4> Confirm: Really delete $entrytype $cn? </H4><BR>";
    print "<BR> If so, press the Save Changes button.\n";
    print "<BR> If not, press the back button in your browser.\n</CENTER>";
    return;
}


# Prints an HTML form that lets an admin edit user data or create new users.
# If the user running this script doesn't have admin privs, it prints the data
# in a read-only format instead of as an editable form.
# Possible op attributes generated from this form are createuser and changeuser

sub printuser {
    my ( $getgroupinfo ) = @_;
    checkbuttons();
    ldapsearch();

    param( 'entrytype', 'user' );
    my $ldap = ldapbind();
    my $op = param( 'op' );
    my $ldapdata;
    my $passvalue;
    my $entrytype = 'user';
    my $dn = param( 'dn' );

    if ( $dn ) {
        # get user info from LDAP and ( if $userisadmin ) IMAP servers

        $ldapdata = ldapget( $ldap, $dn );

        # First we want to clear out all the HTML parameters in case $ldapdata
        # returns null values and fails to overwrite existing param values
        param( 'cn', '' );
        param( 'ldapuid', '' );
        param( 'mail', '' );
        param( 'sn', '' );
        param( 'givenname', '' );
        param( 'maxquota', '' );

        if( $userisadmin ) {
            $op = "changeuser";
            $imap = openimap($uid, $pass, $imapserver, $imapport, $useimapSSL);
            my @imapquota = 
                getquota( "user." . scalar( $ldapdata->get_value( 'uid' )));
            param( 'usedquota',  $imapquota[1] );
            param( 'maxquota',  $imapquota[2] );
            closeimap();
        }

        param( 'cn',        scalar( $ldapdata->get_value( 'cn' )));
        param( 'ldapuid',   scalar( $ldapdata->get_value( 'uid' )));
        param( 'mail',      scalar( $ldapdata->get_value( 'mail' )));
        param( 'sn',        scalar( $ldapdata->get_value( 'sn' )));
        param( 'givenname', scalar( $ldapdata->get_value( 'givenname' )));

        # loop through the rest of the attributes
        for( $i = 0; $i < $#ldappersonatts; $i += 2 ) { 
            param( $ldappersonatts[ $i ], '' );
            param( $ldappersonatts[ $i ], 
                   scalar( $ldapdata->get_value( $ldappersonatts[ $i ] )));
        }
    } else {
        $op = "createuser" if( $userisadmin );
    }
    
    # print the HTML - different for users than admins
    print "<TABLE WIDTH=90%><TR><TD>";
    if( param( 'photo' )) {
        print "<IMG ALIGN=RIGHT SRC=", param( 'photo' ), " TITLE=\"Photo of ",
              param( 'cn' ), "\" >";
    }
    print "<CENTER>";
    if( $userisadmin ) {
        # print the screen that lets one make changes to user information 
        print "<TABLE WIDTH=60%><TR><TD>Employee UID:</TD><TD>";

        if( $op eq "changeuser" ) {  #user is admin and op is changeuser

            print param( 'ldapuid' ), "</TD></TR>\n<TR><TD>E-mail Address:<BR>";
            print "</TD><TD>", param( 'ldapuid' ), "\@", $maildomain, "<BR>";
            print "</TD></TR>\n<TR><TD>Name:</TD><TD>",
                  param( 'givenname' ), " ", param( 'sn' );

            print hidden( 'entrytype' );
            print hidden( 'ldapuid' );
            print hidden( 'dn' );
            print hidden( 'mail' );
            print hidden( 'cn' );

            # Note that we don't allow the admin to change the user's uid.
            # With a Cyrus server using LDAP that would be a complex,
            # configuration-dependant process that would involve changing the
            # the RDN on the LDAP server, and on the Cyrus server we would have
            # to create a new user, then copy all the mailboxes from the user's
            # old account to their new one, and delete the old ones. With all
            # the possible different system configurations that seems risky
            # and very difficult to implement robustly. We'll just force
            # the admin to create a new user account and have the user copy
            # their messages from the old account to the new one.

        } else {      # user is admin and op is createuser
            # if not "changeuser" then $op must equal "createuser", so we will
            # need to allow the admin to enter the new user's uid and passwd

            print textfield( -name=>'ldapuid',
                             -maxlength=>40,
                             -size=>40
                           );
            print "</TD></TR>\n<TR><TD>First Name:</TD><TD>",
                  textfield(-name=>'givenname', 
                            -value=>param( 'givenname' ) || "",
                            -maxlength=>75,
                            -size=>40
                           );
            print "</TD></TR>\n<TR><TD>Last Name:</TD><TD>",
                  textfield(-name=>'sn',
                            -value=>param( 'sn' ) || "",
                            -maxlength=>75,
                            -size=>40
                           );

            if( $suggestpass ) {
                # suggest a random password when creating new users
                $passvalue = scalar( `$suggestpass` );
                chomp( $passvalue );
            }
        }        # below here printed for any op value if user is admin
        if( $passvalue ) {  # if configured to suggest a default password
            # Note that if $suggestpasswd is undefined in the ldapextras.conf
            # file, $passvalue will always be undefined, too
            print "</TD></TR><TR><TD COLSPAN=2>A random alphabetic",
                  " password has been entered for your convenience.";
            print "</TD></TR><TR><TD>Password:</TD><TD>",
                   textfield( -name=>pass1,
                              -value=>$passvalue || "",
                              -override=>1,
                              -size=>40,
                              -maxlength=>15
                            );
            print "</TD></TR><TR><TD>Password (again):</TD><TD>",
                   textfield( -name=>pass2,
                              -value=>$passvalue || "",
                              -override=>1,
                              -size=>40,
                              -maxlength=>15
                            );
        } else {         #print blank password fields
            print "</TD></TR><TR><TD>Password:</TD><TD>",
                   password_field( -name=>pass1,
                                   -value=>"",
                                   -override=>1,
                                   -size=>40,
                                   -maxlength=>15
                                 );
            print "</TD></TR><TR><TD>Password (again):</TD><TD>",
                   password_field( -name=>pass2,
                                   -value=>"",
                                   -override=>1,
                                   -size=>40,
                                   -maxlength=>15
                                 );
        }
        # still in the userisadmin block
        #Loop through the rest of the attributes
        for( $i = 0; $i < $#ldappersonatts; $i += 2 ) { 
            print "</TD></TR>\n<TR><TD>", 
                  $ldappersonatts[ $i + 1 ], ":</TD><TD>";
            if( $ldappersonatts[ $i ] !~ /street/i ) {

                # for any attributes except the street address:
                print textfield(-name=>$ldappersonatts[ $i ],
                                -value=>param( $ldappersonatts[ $i ])||(""),
                                -maxlength=>75,
                                -size=>40
                               );
            } else {  

                # print a textarea instead of textbox for street addresses
                print textarea(-name=>$ldappersonatts[ $i ],
                               -value=>param( $ldappersonatts[ $i ] )||(""),
                               -rows=>4,
                               -columns=>38
                              );
            
            }
        }
        
        if( $op eq 'changeuser' ) {
            print "</TD></TR>\n<TR><TD>Disk Quota Limit (KB):</TD><TD>",
                  textfield(-name=>'maxquota',
                            -maxlength=>20,
                            -size=>40
                           ),
                  "</TD></TR>\n<TR><TD>Disk Quota Used (KB):</TD><TD>", 
                  param('usedquota'), "</TD></TR>\n<TR><TD COLSPAN=2>";
            print "<HR><CENTER>", submit( 'Save Changes' ),
                   submit( 'Get Group Info' ), submit( 'Delete This User' ), 
                   "</CENTER>";
        }
        print "</TD></TR></TABLE>\n";

    } else {    # if user is not the admin
        # print the screen ordinary mortals will see
        print hidden( 'dn' );
        print hidden( 'mail' );
        print hidden( 'cn' );
        print hidden( 'sn' );
        print hidden( 'givenname' );

        print "<TABLE WIDTH=60%><TR><TD>Employee:</TD><TD>", param( 'cn' ), 
            "</TD></TR>\n";
        print "<TR><TD>E-mail Address:</TD><TD>", param( 'mail' ), 
            "</TD></TR>\n";

        # loop through the rest of the attributes
        for( $i = 0; $i < $#ldappersonatts; $i += 2 ) { 
            next if exists $donotdisplay{ $ldappersonatts[ $i ] };
            print hidden( $ldappersonatts[ $i ] );
            if( param( $ldappersonatts[ $i ] )) {
                print "<TR><TD>", $ldappersonatts[ $i + 1 ], "</TD><TD>",
                      param( $ldappersonatts[ $i ] ), "</TD></TR>";
            }
        }
        print "<TR><TD COLSPAN=2><BR></TD></TR><TR><TD>",
              "Click here to see which e-mail groups<BR>",
              "will forward mail to this person. </TD><TD>";
        print submit( 'Get Group Info' );
        print "</TD></TR><TR><TD COLSPAN=2><BR></TD></TR></TABLE>\n";
    }     # Below here will apply whether or not $userisadmin


    getallgroups( $ldap, $dn, param( 'cn' )) if( $getgroupinfo ); 
    print "</TD></TR></TABLE>\n";
    $ldap->unbind;
    undef $ldap;
    param( 'op', $op );
    return;
}


# Prints an HTML form that lets an admin edit the members of a group.
# If the user running this script doesn't have admin privs, it prints the data
# read-only instead of as an HTML form, and members of the group are listed
# as links to display info about that member.
# Possible op attributes from this form are creategroup and changegroup 

sub printgroup {
    my $buttonpush = checkbuttons();
    return if( $buttonpush );

    # print the ldapsearch textbox
    ldapsearch();

    my $dn = param( 'dn' );
    my $ldap = ldapbind();

    if ( $dn ) {
        # get group info from LDAP server

        my $ldapdata = ldapget( $ldap, $dn );
        # clear out old param values in case $get_value returns null and fails
        # to overwrite the HTML parameter will a null value
        param( 'cn', '' );
        param( 'members', '' );

        param( 'cn', scalar( $ldapdata->get_value( 'cn' )));

        my @members;
        my $entrytype;
        my $filter = "(|";
        my @memberarray = $ldapdata->get_value( $ldapmemberatt );

        # for performance reasons we don't want to perform a seperate search
        # for every DN in the group, so we'll build a search filter that will
        # only capture these DNs.
        for( @memberarray ) {
            $_ =~ m/([^,]*),.*/;
            my $cn = ldapclean( $1 );
            $filter .= "($cn)";
        }
        $filter .= ")";

        $mesg = $ldap->search( base=>$LDAP_BASEDN,
                               filter=>$filter,
                               attrs=>[ "uid", "cn", "objectClass" ]
                             );
        printerr( "printgroup:" . $mesg->error ) if( $mesg->code );

        my @entries = $mesg->entries;
        my $count = 0;
        foreach $memberentry ( @entries ) {
            getentrytype( $memberentry );
            my $displayname;
            my $entrytype = $memberentry->get_value( 'entrytype' );
            my $cn = $memberentry->get_value( 'cn' );
            if( $entrytype eq 'group' ) {
                $displayname = $cn
            } else {
                $displayname = $memberentry->get_value( 'uid' );
            }
            my $dn = $memberentry->dn();
            
            # the data to be stored in the 'members' HTML param  will be a tab-
            # delimited string.
            # @members is an array of anonymous arrays. The first element of
            # these subarrays will be the line as written to the members HTML
            # param. The second and third elements will be the cn and entrytype
            # respectively, lowercased to use in sorting the @members array
            $members[ $count ] = [ $entrytype . "\t" . $displayname . "\t" .
                                          $cn . "\t" . $dn . "\n" ,
                              lc( $cn ),
                              $entrytype 
                             ];
            $count++;
        }

        # Sort @members first by entrytype, then alphabetically
        @members = sort {
            $b->[2] cmp $a->[2]
                    ||
            $a->[1] cmp $b->[1] 
        } @members; 

        # write the sorted results to the members HTML param
        my $output = "";
        for( @members ) {
           $output .= $_->[0];
        } 
        param( "members", $output );

        $op = 'changegroup' if( $userisadmin );
    } else {
        $op = 'creategroup' if( $userisadmin );
    } 

    my @members = split( /\n/, param( 'members' ));

    # print the HTML
    print "<CENTER>";

    if( $userisadmin ) {
        if( $op eq 'changegroup' ) {
            print "<H3>Members of Group ",param('cn'),":</H3><BR>";
            my $displaystring;
            print hidden( 'members' );
            print hidden( 'dn' );
            print hidden( 'cn' );
            for( @members ) {
                my ( $entrytype, $displayname, $cn, $dn ) = split( '\t' );
                $displaystring .= $displayname . "\n";    
            }
            param( 'memberchanges', '' );
            param( 'memberchanges', $displaystring );
            param( 'entrytype', 'group' );
            print hidden( 'entrytype' );
            print "<CENTER>";
            print textarea(-name=>'memberchanges',
                           -rows=>15,
                           -columns=>20
                          );
            print "<BR>", submit( 'Save Changes' ),
                          submit( 'Delete This Group' );
        } elsif( $op eq 'creategroup' ) {
            param( 'cn', '' );
            param( 'members', '' );
            print "Group Name:&nbsp;&nbsp;";
            print textfield( -name=>'cn',
                             -size=>10,
                             -maxlength=>40
                           );
            print "<BR><BR>" , textarea(-name=>'memberchanges',
                                        -rows=>30,
                                        -columns=>20
                                       );

        }
    } else {                     # print the page as everyday users will see it

        print "<H3>Members of Group ",param('cn'),":</H3><BR>";

        param( 'members', '' );
        param( 'mail', '' );
        print "<TABLE WIDTH=80%><TR>";

        my $count = -1;
        my $realop = $op;


        for ( @members ) {
            my ( $entrytype, $displayname, $cn, $dn ) = split( '\t' );

            param( 'op', "print" . $entrytype );
            param( 'dn', $dn );
            print "<TD WIDTH=33%><A HREF=", url( -query_string=>1 ), ">", $cn, 
                  "</A></TD>";
            print "</TR>\n<TR>" if(( ++$count % 3 ) == 2 );
        }    
        print "</TR></TABLE>\n";
        $op = $realop;
    }

    $ldap->unbind;
    undef $ldap;

    param( 'op', $op );
    return;
}


# Accepts a search string in an HTML param, searches the directory for entries
# that match the string, and prints an HTML page with a list of the matching
# entries. Matching entries are printed as HTML links to display info about
# those entries.
# Possible op attributes from this form are printgroup, printuser and ldapsearch

sub ldapsearch {
    # If the user entered a string to search for, search and get the results
    my $searchstring = param( 'searchstring' );
    param( 'searchstring', '' );
    my @results;
    my $realop = param( 'op' );
    if( $searchstring ) {
        # make sure searchstring doesn't contain funny chars such as ';' that
        # might cause unpredictable behavior 
        $searchstring =~ s/[^\w\(\)\-]//;
        ldapclean( $searchstring );
        param( 'Search', '' );
        my $ldap = ldapbind();

        my $mesg = $ldap->search( base=>$LDAP_BASEDN,
                                  filter=>"(cn=\*$searchstring\*)",
                                  attrs=>[ "cn", "objectclass", "uid" ]
                                );
        printerr("ldapsearch error:" . $mesg->error) if( $mesg->code );
        @results = $mesg->entries(["cn"]);

        # Figure out whether each entry is a user or group 
        # and put that data into the entry perl object as attribute 'entrytype'
        # note that this data is not written to the LDAP server
        foreach $result ( @results ) {
            getentrytype( $result );
        }

        @results = sortldapentries( @results );

        $ldap->unbind;
        undef $ldap;

    }

    # print the HTML
    print "<CENTER><TABLE border=1 WIDTH=90%><TR $cb><TD $tb><CENTER>",
          "Search the Directory for:</TD></CENTER><TD $tb><CENTER>";
    print textfield( -name=>'searchstring',
                     -default=>$searchstring,
                     -size=>40,
                     -maxlength=>40
                   );
    print "</CENTER></TD><TD $tb><CENTER>", submit( 'Search' );
    print "</CENTER></TD></TR></TABLE>";
    if( $searchstring && @results ) {
        print "<BR><CENTER>You can get more information about any entry ",
              "listed below by clicking on it.<HR>";
    }
    print "<TABLE WIDTH=100% ALIGN=RIGHT><TR>";
    # print a table of all the LDAP entries that matched the searchstring
    my $count = -1;

    # prevent putting too much data in link query_string
    param( 'members', '' );
    param( 'mail', '' );

    foreach $result ( @results ) {
        my $dn = $result->dn;
        my ( $cn ) = $result->get_value( 'cn' );
        my ( $ldapuid ) = $result->get_value( 'uid' );
        $op = "print" . $result->get_value( 'entrytype' );

        # clear out previous values of params in case one of the get_value
        # calls returned null
        param( 'op', '' );
        param( 'dn', '' );
        param( 'cn', '' );
        param( 'ldapuid', '' );
        param( 'op', $op );
        param( 'dn', $dn );
        param( 'cn', $cn );
        param( 'ldapuid', $ldapuid );
        print "<TD WIDTH=33%><A HREF=", url( -query_string=>1 ), ">", $cn, 
              "</A></TD>";
        print "</TR>\n<TR>" if(( ++$count % 3 ) == 2 );
    }    
    if( !@results && $realop eq "ldapsearch" ) {
        print "<TD><CENTER><BR>",
              "Hint: Enter someone's first or last name, or the first few ",
              "letters of<BR>their e-mail address, or part of the name of an ",
              "e-mail group.</TD></CENTER>";
    }
    print "</TR></TABLE><BR><HR>\n";
    $op = $realop;
    param( 'op', $op );
    return;
}


# Accepts the entry's DN, a printable name, and a reference to a Net::LDAP object
# that has already been been successfully binded to (bound?). Uses the
# grouprecurse function to search the LDAP group structure to determine all
# groups that refer to that uid or cn, either directly or indirectly through
# references from groups that are members of other groups. 

sub getallgroups {
    my( $ldap, $dn, $name ) = @_;
    my @indirectgroups;
    my @directgroups;

    # first, we'll search for groups that the DN is directly a member of
    # so we can differentiate those from indirect ones when printing the HTML
    my $mesg = $ldap->search( base=>$LDAP_BASEDN,
                              filter=>"( $ldapmemberatt=" .ldapclean($dn). ")",
                              attrs=>[ 'dn' ]
                            );
    my $err .= $mesg->error if( $mesg->code );
    my @results = $mesg->entries;
    foreach $result ( @results ) {
        push( @directgroups, $result->dn() );
    }

    # then, we'll use the grouprecurse sub to get a list of all other groups
    # that contain groups that the DN is a member of to make sure we get 'em all
    for( @directgroups ) {
        my $arrayref = \@indirectgroups;
        my $count = 0;
        $err .= grouprecurse( $ldap, $arrayref, $_, $count );
        return $err if( $err );
    }

    # and print the HTML
    print "<TABLE WIDTH=60%><TR><TD>$name is a member of the following groups:",
          "</TD></TR><TR><TD><CENTER>\n";

    # prevent putting extra data in link querystring
    param( 'mail', '' );
    param( 'members', '' );

    for( @directgroups ) {
        my ( $cn ) = ldapgetatt( $ldap, $_, "cn" );
        param( 'op', "printgroup" );
        param( 'dn', $_ );
        print "<A HREF=", url(-query=>1), ">", $cn, "</A><BR>\n";
    }
    print "<BR></TD></TR><TR><TD>This user will also receive mail ",
          "sent to these groups:</TD></TR><TR><TD><CENTER>\n";
    for( @indirectgroups ) {
        my ( $cn ) = ldapgetatt( $ldap, $_, "cn" );
        param( 'op', "printgroup" );
        param( 'dn', $_ );
        print "<A HREF=", url(-query=>1), ">", $cn, "</A><BR>\n";
    }
    print "</TD></TR></TABLE>";
    return;
}

# recursive function called by getallgroups above. Accepts a reference to a
# Net::LDAP object that has already been binded successfully, a reference to the
# array that will be storing all the results, the name of the cn to search
# the groups in the directory for, and a counter variable to detect circular
# references.

sub grouprecurse {
    my( $ldap, $array, $dn, $count ) = @_;
    $count++;
    return "Error! Circular references in groups?\n" if( $count > 20 );
    my $mesg = $ldap->search( base=>$LDAP_BASEDN,
                              filter=>"( $ldapmemberatt=".ldapclean($dn).")",
                              attrs=>[ "dn" ]
                            );
    my $err .= $mesg->error if( $mesg->code );
    my @results = $mesg->entries;
    foreach $result ( @results ) {
        push( @$array, $result->dn );
        $err .= grouprecurse( $ldap, $array, $result->dn, $count );
    }
    return $err;
}


# Quick shortcut for typing $ldap->search(blah blah blah) if you only want
# the contents of one attribute from one dn. Returns an array containing
# the value(s) of that attribute for the dn in question.
sub ldapgetatt {
    my @results;
    my( $ldap, $dn, $attribute ) = @_;
    my $user = ldapget( $ldap, $dn );
    @results = $user->get_value( $attribute );
    return @results;
}

# Gets an entry from the LDAP server given the dn and a bound Net::LDAP object
sub ldapget {
    my( $ldap, $dn ) = @_;
    my $mesg = $ldap->search( base=>$dn,
                              filter=>"(objectclass=*)",
                            );
    if( $mesg->code ) {
        printerr( "ldapget: $dn <BR>" . $mesg->error );
    }
    my @entries = $mesg->entries;
    return $entries[0];
}


# Accepts a string containing error text and prints an error screen

sub printerr {
    my( $err ) = @_;
    print "<H3> Error: </H3><B> $err </B>";
    return;
}

# what to do in the even of an authentication err

sub autherr {
    printerr( "<BR>Think you're sneaky, huh? :)<BR>\nYou don't have access." );
    return;
}


# We need to escape certain characters to prevent Net::LDAP from interpreting
# some characters in DN's from screwing up LDAP searches when the DN is part
# of the searchfilter - this adds backslashes in front of parenthesis, asterisks
# or backslasheatts that appear as literal characters in the DN. 

sub ldapclean {
    my ( $dn ) = @_;
    $dn =~ s/([\(\)\\\*])/\\$1/g;
    return $dn;
}

# for a given Net::LDAP::Entry object, determine whether it's a user or group
# and add an attribute to that object called entrytype 
sub getentrytype {
    my ( $ldapentry ) = @_;
    my @objectclasses = $ldapentry->get_value( "objectClass" );
    for( @objectclasses ) {
        if( /person/i ) {
            $ldapentry->add('entrytype'=>'user');
            last;
        } elsif( /group/i ) {
            $ldapentry->add('entrytype'=>'group');
            last;
        }
    } 
}

# for a given array of Net::LDAP::Entry objects, sort the objects by
# entrytype (see getentrytype function above) and then alphabetically by cn
sub sortldapentries {
    my @results = @_;
    # sort the entries alphabetically and by entrytype
    @results = sort {
        lc( $b->get_value( 'entrytype' )) cmp lc( $a->get_value( 'entrytype' ))
        ||
        lc( $a->get_value( 'cn' ))        cmp lc( $b->get_value( 'cn' ))
    } @results;
        
    return @results;
}

# Bind to the LDAP directory as the current user, return the $ldap object
sub ldapbind {
    # first, bind anonymously to get the DN of the current user 
    my $ldap = Net::LDAP->new( $LDAP_SERVER ) || do {
        printerr( "ldapbind: Couldn't connect to LDAP server: $@" );
        return -1;
    };
    my $mesg = $ldap->bind;
    if ( $mesg->code ) {
        printerr( "ldapbind: couldn't bind anonymously: $@" );
        return -1;
    }

    $mesg = $ldap->search( base=>$LDAP_BASEDN,
                           filter=>"(uid=$uid)"
                         );

    my @entries = $mesg->entries;
    my $user = $entries[0]; 
    my $dn = $user->dn;
    $ldap->unbind;
    undef $ldap;
    $ldap = Net::LDAP->new( $LDAP_SERVER ) || do {
        printerr( "ldapbind: Couldn't connect to LDAP server: $@" );
        return -1;
    };
    $mesg = $ldap->bind( dn=>$dn, password=>$pass );
    if ( $mesg->code ) {
        printerr( "ldapbind: couldn't bind with dn $dn: $@" );
        return -1;
    }
    return $ldap;
}

# check for button pushes and run the appropriate function if
# they have been pushed
sub checkbuttons {

    if( param( 'Search' )) {
        param( 'Search', '' );
        param( 'op', 'ldapsearch' );
        ldapsearch();
        print hidden( 'op' );
        print end_form;
        print end_html;
        exit;
    }

    if( param( 'Delete This Group' )) {
        param( 'Delete This Group', '' );
        param( 'op', 'confirmdelete' );
        confirmdelete();
        print hidden( 'op' );
        print end_form;
        print end_html;
        exit;
    }

    if( param( 'Delete This User' )) {
        param( 'Delete This User', '' );
        param( 'op', 'confirmdelete' );
        confirmdelete();
        print hidden( 'op' );
        print end_form;
        print end_html;
        exit;
    }

    if( param( 'Get Group Info' )) {
        param( 'Get Group Info', "");
        printuser( "getgroupinfo" );
        print hidden( 'op' );
        print end_form;
        print end_html;
        exit;
    }
    return;
}

1; # make require happy


syntax highlighted by Code2HTML, v. 0.9.1