<?php
/*
    PHP LDAP CLASS FOR MANIPULATING ACTIVE DIRECTORY
    Version 2.1+

	Adapted 2013-2024 by SysCo/al 5.9.8.2 (2024-12-20)

 *
 *   2024-12-20 5.9.8.2 SysCo/al $attributes["telephoneNumber"] checked
 *
 *   2023-01-18 5.9.5.5 SysCo/al New _ldap_filter attribute
 *                               New paging support for 7.3+
 *
 *   2021-04-07 5.8.2.1 SysCo/al Test connection filter changed from (dn=test-connection) to (objectClass=user)
 *                               (dn=xxx not supported by eDirectory)
 *                               New LDAP type 4 for eDirectory : users are searched with the class posixAccount or user
 *                               enhanced LDAP type 2 : users are searched with the class posixAccount or user
 *
 *   2018-07-16 5.2.0.0 SysCo/al Active Directory support enhancement (member:1.2.840.113556.1.4.1941:=)
 *                               Active Directory primary group optimized detection
 *                               _users_dn added
 *
 *   2018-02-01 5.1.0.2 SysCo/al using variable $pageSize instead of fixed value
 *                               LDAP control paged result called with $iscritical = false
 *
 *   2017-12-05 5.0.6.2 SysCo/al expired password option support
 *
 *   2017-01-24 5.0.3.3 SysCo/al cache support added
 *
 *   2015-08-13 4.3.2.8 SysCo/al ldap_dn_encode (RFC4514) method added
 *                               ldap_search_encode (RFC4515) method added
 *                               ldap_slashes method calls replaced by ldap_search_encode calls
 *
 *   2014-11-04 4.3.0.0 SysCo/al paged result added (if supported by the LDAP PHP library)
 *                               more internal attributes added
 *                               enhanced SSL support
 *                               enhanced debug message
 *                               cache added for better results
 *                               ldap_get_one_entry_raw method added
 *                               ldap_get_entries_raw method added



    Written by Scott Barnett
    email: scott@wiggumworld.com
    http://adldap.sourceforge.net/

    Copyright (C) 2006-2007 Scott Barnett

    I'd appreciate any improvements or additions to be submitted back
    to benefit the entire community :)

    Works with PHP 5, should be fine with PHP 4, let me know if/where it doesn't :)

    Please visit the project website for a full list of the functions and
    documentation on using them.
    http://adldap.sourceforge.net/documentation.php

    This library is free software; you can redistribute it and/or
    modify it under the terms of the GNU Lesser General Public
    License as published by the Free Software Foundation; either
    version 2.1 of the License, or (at your option) any later version.

    This library is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    Lesser General Public License for more details.

    ********************************************************************
    Something to keep in mind is that Active Directory is a permissions
    based directory. If you bind as a domain user, you can't fetch as
    much information on other users as you could as a domain admin.
    ********************************************************************

    Attributes documentation : http://www.selfadsi.org/user-attributes.htm
    
    LDAP information, also for other implementation: https://github.com/mfreiholz/iF.SVNAdmin/issues/53
    LDAP trick for > 1000: http://php.net/manual/en/function.ldap-search.php
*/

// Added by SysCo/al
if (!function_exists('constant_defined')) {
  function constant_defined(
    $constant_name
  ) {
    $result = false;
    foreach (get_defined_constants() as $key=>$value) {
      if (strtoupper($key) == strtoupper($constant_name)) {
        $result = true;
        break;
      }
    }
    return $result;
  }
}


if (!constant_defined('PHP_VERSION_ID'))
{
    $version = explode('.', PHP_VERSION);
    define('PHP_VERSION_ID', ($version[0] * 10000 + $version[1] * 100 + $version[2]));
}
putenv('LDAPTLS_REQCERT=never');

if (!constant_defined('LDAP_OPT_DIAGNOSTIC_MESSAGE')) {
    define ('LDAP_OPT_DIAGNOSTIC_MESSAGE', 0x0032);
}

// Different type of accounts in AD
define ('ADLDAP_NORMAL_ACCOUNT', 805306368);
define ('ADLDAP_WORKSTATION_TRUST', 805306369);
define ('ADLDAP_INTERDOMAIN_TRUST', 805306370);
define ('ADLDAP_SECURITY_GLOBAL_GROUP', 268435456);
define ('ADLDAP_DISTRIBUTION_GROUP', 268435457);
define ('ADLDAP_SECURITY_LOCAL_GROUP', 536870912);
define ('ADLDAP_DISTRIBUTION_LOCAL_GROUP', 536870913);


/* Used to convert the sid to a string (for Active Directory / LDAP purposes)
 * converts a binary SID into a string representation
 * @param string $sid
 * @return string
 * @link http://blogs.freebsdish.org/tmclaugh/2010/07/21/finding-a-users-primary-group-in-ad/#comment-2855
 */
if (!function_exists('sid2str')) {
    function sid2str($sid)
    {
		// The format of a SID binary string is as follows:
		// 1 byte for the revision level
		// 1 byte for the number n of variable sub-ids
		// 6 bytes for identifier authority value
		// n*4 bytes for n sub-ids
		//
		// Example: 010400000000000515000000a681e50e4d6c6c2bca32055f
		//  Legend: RRNNAAAAAAAAAAAA11111111222222223333333344444444
		$revision = ord($sid[0]);
		$numberSubID = ord($sid[1]);
		$subIdStart = 8; // 1 + 1 + 6
		$subIdLength = 4;
		if (strlen($sid) !== $subIdStart + $subIdLength * $numberSubID) {
			// Incorrect number of bytes present.
			return '';
		}
		// 6 bytes = 48 bits can be represented using floats without loss of
		// precision (see https://gist.github.com/bantu/886ac680b0aef5812f71)
		$iav = number_format(hexdec(bin2hex(substr($sid, 2, 6))), 0, '', '');
		$subIDs = array();
		for ($i = 0; $i < $numberSubID; $i++) {
			$subID = unpack('V', substr($sid, $subIdStart + $subIdLength * $i, $subIdLength));
			$subIDs[] = sprintf('%u', $subID[1]);
		}
		// Result for example above: S-1-5-21-249921958-728525901-1594176202
		return sprintf('S-%d-%s-%s', $revision, $iav, implode('-', $subIDs));
    }
}

class MultiotpAdLdap {
    // BEFORE YOU ASK A QUESTION, PLEASE READ THE DOCUMENTATION AND THE FAQ
    // http://adldap.sourceforge.net/documentation.php
    // http://adldap.sourceforge.net/faq.php

    // You can set your default variables here, or when you invoke the class
    var $_account_suffix="@mydomain.local"; // Reinitialized to '' by SysCo/al in the constructor
    var $_base_dn = "DC=mydomain,DC=local"; // Reinitialized to '' by SysCo/al in the constructor

    // An array of domain controllers. Specify multiple controllers if you 
    // would like the class to balance the LDAP queries amongst multiple servers
    var $_domain_controllers = array ("dc01.mydomain.local");

    // optional account with higher privileges for searching
    // not really that optional because you can't query much as a user
    var $_ad_username=NULL;
    var $_ad_password=NULL;

    // AD does not return the primary group. http://support.microsoft.com/?kbid=321360
    // This tweak will resolve the real primary group, but may be resource intensive. 
    // Setting to false will fudge "Domain Users" and is much faster. Keep in mind though that if
    // someone's primary group is NOT domain users, this is obviously going to bollocks the results
    var $_real_primarygroup=true;

    // Use SSL, your server needs to be setup, please see - http://adldap.sourceforge.net/ldap_ssl.php
    var $_use_ssl=false;

    var $_cn_identifier = "samaccountname";
    var $_group_cn_identifier = "samaccountname";
    var $_group_attribute = "memberof";

    // When querying group memberships, do it recursively
    // eg. User Fred is a member of Group A, which is a member of Group B, which is a member of Group C
    // user_ingroup("Fred","C") will returns true with this option turned on, false if turned off
    var $_recursive_groups=true;

    // You should not need to edit anything below this line
    //******************************************************************************************

    //other variables
    var $_conn;
    var $_conn_paged;
    var $_bind;
    var $_bind_paged;
    var $_cache_group_dn; // Added 2014-07-21 by SysCo/al
    var $_cache_recursive_groups; // Added 2014-07-21 by SysCo/al
    var $_cache_support; // Added 2014-07-21 by SysCo/al
    var $_entry_identifier; // Added 2014-07-21 by SysCo/al
    var $_error; // Added by SysCo/al
    var $_error_message; // Added by SysCo/al
    var $_error_no; // Added by SysCo/al
    var $_ldap_server_type; // Added by SysCo/al, 1 (default) for Active Directory, 2 for Generic LDAP, 3 for legacy Active Directory, 4 for eDirectory (Novell)
    var $_ldap_filter; // Added by SysCo/al
    var $_oui_sr; // Added by SysCo/al
    var $_paging_cookie; // Added 2023 by SysCo/al
    var $_paging_legacy; // Added 2023 by SysCo/al
    var $_paging_ldap_control; // Added 2023 by SysCo/al
    var $_paging_size; // Added 2023 by SysCo/al
    var $_debug_message; // Added by SysCo/al 4.3.2.2
    var $_warning_message; // Added by SysCo/al
    var $_server_reachable; // Added by SysCo/al
    var $_cache_folder; // Added by SysCo/al
    var $_users_dn;  // Added by SysCo/al 5.2.0.0

    var $_cache_timeout = 3600; // Added by SysCo/al

    var $_linux_file_mode = '0666'; // Added by SysCo/al
    
    // Creation of dynamic property is deprecated
    var $_oui_paged_sr;
    var $_expired_password_valid;

    // default constructor
    function __construct($options=array()) {

        $this->_account_suffix = ''; // Added by SysCo/al
        $this->_base_dn = ''; // Added by SysCo/al
        $this->_cache_group_dn = array(); // Added 2014-07-21 by SysCo/al
        $this->_cache_recursive_groups = array(); // Added 2014-07-21 by SysCo/al
        $this->_cache_support = TRUE; // Added 2014-07-21 by SysCo/al
        $this->_entry_identifier = array(); // Added 2014-07-21 by SysCo/al
        $this->_error = TRUE; // Added by SysCo/al
        $this->_error_message = ''; // Added by SysCo/al
        $this->_error_no = 0; // Added by SysCo/al
        $this->_ldap_server_type = 1; // Added by SysCo/al
        $this->_ldap_filter = ''; // Added by SysCo/al
        $this->_oui_paged_sr = NULL; // Added by SysCo/al
        $this->_paging_cookie = '';
        $this->_paging_legacy = false;
        $this->_paging_ldap_control = false;
        $this->_paging_size = 1000;

        $this->_debug_message = ''; // Added by SysCo/al
        $this->_warning_message = ''; // Added by SysCo/al
        $this->_server_reachable = FALSE; // Added by SysCo/al
        $this->_cache_folder = ""; // Added by SysCo/al
        $this->_expired_password_valid = FALSE;  // Added by SysCo/al

        //you can specifically override any of the default configuration options setup above
        if (count($options)>0){
            if (array_key_exists("account_suffix",$options)){ $this->_account_suffix=$options["account_suffix"]; }
            if (array_key_exists("base_dn",$options)){ $this->_base_dn=$options["base_dn"]; }
            if (array_key_exists("domain_controllers",$options)){ $this->_domain_controllers=$options["domain_controllers"]; }
            if (array_key_exists("ad_username",$options)){ $this->_ad_username=$options["ad_username"]; }
            if (array_key_exists("ad_password",$options)){ $this->_ad_password=$options["ad_password"]; }
            if (array_key_exists("real_primarygroup",$options)){ $this->_real_primarygroup=$options["real_primarygroup"]; }
            if (array_key_exists("use_ssl",$options)){ $this->_use_ssl=$options["use_ssl"]; }
            if (array_key_exists("recursive_groups",$options)){ $this->_recursive_groups=$options["recursive_groups"]; }
            if (array_key_exists("ldap_server_type",$options)){ $this->_ldap_server_type=$options["ldap_server_type"]; }
            if (array_key_exists("ldap_filter ",$options)){ $this->_ldap_filter=$options["ldap_filter"]; }
            if (array_key_exists("cache_support",$options)){ $this->_cache_support=$options["cache_support"]; }
            if (array_key_exists("cache_folder",$options)){ $this->_cache_folder=$options["cache_folder"]; }
            if (array_key_exists("expired_password_valid",$options)){ $this->_expired_password_valid=(TRUE == $options["expired_password_valid"]); }
            if (array_key_exists("users_dn",$options)){ $this->_users_dn=$options["users_dn"]; }
            
            // Added by SysCo/al
            if ($this->_use_ssl) {
                $ldap_port = 636;
            } else {
                $ldap_port = 389;
            }
            if (array_key_exists("cn_identifier",$options)){ $this->_cn_identifier=strtolower($options["cn_identifier"]); }
            if (array_key_exists("group_cn_identifier",$options)){ $this->_group_cn_identifier=strtolower($options["group_cn_identifier"]); }
            if (array_key_exists("group_attribute",$options)){ $this->_group_attribute=strtolower($options["group_attribute"]); }
            if (array_key_exists("port",$options)) { $ldap_port = intval($options["port"]); }
            // if (array_key_exists("time_limit",$options)) : see later, when connection is done
        }

        // ldap_set_option(NULL, LDAP_OPT_DEBUG_LEVEL, 7);

        $connected = FALSE;
        // Modified by SysCo/al (check also empty values)
        if (($this->_ad_username!=NULL) && ($this->_ad_password!=NULL) && ($this->_ad_password!='') && ($this->_ad_username!='')) {
            //connect to the LDAP server as the username/password
            // Modified by SysCo/al
            $count_controllers = count($this->_domain_controllers);
            foreach($this->_domain_controllers as $dc)
            {
                $port = $ldap_port;
                $controller = $dc;
                $protocol = "ldap://";
                // $dc=$this->random_controller();
                if ($this->_use_ssl) {
                    $protocol = "ldaps://";
                }
                $pos = strpos($dc, "://");
                if ($pos !== FALSE) {
                    $protocol = substr($dc, 0, $pos+3);
                    $dc = substr($dc, $pos+3);
                }
                $pos = strpos($dc, ":");
                if ($pos !== FALSE) {
                    $port = substr($dc, $pos+1);
                    $dc = substr($dc, 0, $pos);
                }
                if ($this->_conn = ldap_connect($protocol.$dc.":".$port)) {
                    $this->_conn_paged = ldap_connect($protocol.$dc.":".$port);
                    //set some ldap options for talking to AD
                    
                    if (array_key_exists("time_limit",$options))
                    {
                        ldap_set_option($this->_conn, LDAP_OPT_TIMELIMIT, intval($options["time_limit"]));
                        ldap_set_option($this->_conn_paged, LDAP_OPT_TIMELIMIT, intval($options["time_limit"]));
                    }
                    if ((PHP_VERSION_ID >= 50300) && (array_key_exists("network_timeout",$options)))
                    {
                        ldap_set_option($this->_conn, LDAP_OPT_NETWORK_TIMEOUT, intval($options["network_timeout"]));
                        ldap_set_option($this->_conn_paged, LDAP_OPT_NETWORK_TIMEOUT, intval($options["network_timeout"]));
                    }

                    ldap_set_option($this->_conn, LDAP_OPT_PROTOCOL_VERSION, 3);
                    ldap_set_option($this->_conn_paged, LDAP_OPT_PROTOCOL_VERSION, 3);
                    ldap_set_option($this->_conn, LDAP_OPT_REFERRALS, 0);
                    ldap_set_option($this->_conn_paged, LDAP_OPT_REFERRALS, 0);
                    
                    // Added 2018-07-12
                    ldap_set_option($this->_conn, LDAP_OPT_RESTART, 1);
                    ldap_set_option($this->_conn_paged, LDAP_OPT_RESTART, 1);

                    // Added 2023-01-01
                    $this->_paging_ldap_control = false; // Will be detected further
                    if (PHP_VERSION_ID >= 70300) {
                      $this->_paging_legacy = false;
                    } else {
                      $this->_paging_legacy = true;
                    }

                    //bind as a domain admin if they've set it up
                    $this->_bind = @ldap_bind($this->_conn,$this->_ad_username.$this->_account_suffix,$this->_ad_password);
                    $this->_bind_paged = @ldap_bind($this->_conn_paged,$this->_ad_username.$this->_account_suffix,$this->_ad_password);
                    if ($this->_bind) {
                      
                      
                        if (PHP_VERSION_ID >= 70300) {
                          $ldap_result = ldap_read($this->_conn, '', '(objectClass=*)', ['supportedControl']);
                          if (in_array(LDAP_CONTROL_PAGEDRESULTS, ldap_get_entries($this->_conn, $ldap_result)[0]['supportedcontrol'])) {
                            $this->_paging_ldap_control = true;
                          } else {
                            $this->_paging_ldap_control = false;
                          }
                        }

                        // Check BaseDN access, no paging needed
                        if (FALSE !== (@ldap_search($this->_conn, $this->_base_dn, "(objectClass=user)"))) {
                            $this->_error = FALSE;
                            $this->_error_message = '';
                            $connected = TRUE;
                            break;
                        } else {
                            $this->_error = TRUE;
                            $this->_error_message = 'FATAL: AD/LDAP bind failed. The BaseDN '.$this->_base_dn.' is not accepted ('.ldap_error($this->_conn).').';
                        }
                    } else {
                        $this->_server_reachable = (!(-1 == ldap_errno($this->_conn)));
                        if ($this->_use_ssl) {
                            //if you have problems troubleshooting, remove the @ character from the ldap_bind command above to get the actual error message
                            // Modified by SysCo/al
                            $this->_error = TRUE;
                            $this->_error_message = 'FATAL: AD bind failed. Either the LDAPS connection failed or the login credentials are incorrect ('.@ldap_error($this->_conn).').';
                        } else {
                            // Modified by SysCo/al
                            $this->_error = TRUE;
                            $this->_error_message = 'FATAL: AD bind failed. Check the login credentials ('.ldap_errno($this->_conn).": ".@ldap_error($this->_conn).').';
                        }
                        if (ldap_get_option($this->_conn, LDAP_OPT_ERROR_STRING, $extended_error)) {
                            if (!empty($extended_error))
                            {
                                $err_array = explode(',', $extended_error.',,');
                                $err_array = explode(' ', $err_array[2].'  ');
                                $errno = $err_array[2];
                                
                                if ($this->_expired_password_valid) {
                                    /*
                                    We accept also "password expired" and "user must reset password"
                                    525  user not found
                                    52e  invalid credentials
                                    530  not permitted to logon at this time
                                    531  not permitted to logon at this workstation
                                    532* password expired
                                    533  account disabled
                                    534  The user has not been granted the requested logon type at this machine
                                    701  account expired
                                    773* user must reset password
                                    775  user account locked
                                    */
                                    if (('532' == $errno) || ('773' == $errno)) {
                                        $this->_error = FALSE;
                                        $this->_error_message = 'WARNING: user must reset AD/LDAP password';
                                        $connected = TRUE;
                                        break;
                                    }
                                }
                                $this->_error_message.= " ($extended_error)";
                            }                          
                        }
                        // echo "DEBUG ERROR MESSAGE: ".$this->_error_message;
                    }
                    $this->_error = TRUE;
                    $connected = FALSE;
                }
            }
        }
        if (!$connected) {
            $this->_error = TRUE;
            if ('' == $this->_error_message) {
                $this->_error_message = 'FATAL: AD connection failed. Check the LDAP/AD controllers ('.@ldap_error($this->_conn).').';
            }
        } else {
            $this->_error = FALSE;
        }
        return ($connected); // But it's the constructor, the result cannot be used
    }

    function set_paging_size($paging_size) {
      $this->_paging_size = $paging_size;
    }

    function get_paging_size() {
      return ($this->_paging_size);
    }

    function set_base_dn($base_dn) {
        $this->_base_dn = $base_dn;
    }

    function set_users_dn($users_dn) {
        $this->_users_dn = $users_dn;
    }

    // Added 2016-11-22 by SysCo/al
    function set_cache_folder($folder) {
        $this->_cache_folder = $folder;   
    }
    function get_cache_folder() {
        return (trim($this->_cache_folder));   
    }
    function disable_cache_support() {
        $this->_cache_support = FALSE;
    }
    function enable_cache_support() {
        $this->_cache_support = TRUE;
    }
    function set_cache_timeout($value) {
        $this->_cache_timeout = intval($value);
    }
    function get_cache_timeout() {
        return intval($this->_cache_timeout);
    }


	// Added 2015-06-07 by SysCo/al
    function get_debug_message()
    {
        return trim($this->_debug_message);
    }


	// Added 2018-07-10 by SysCo/al
    function clear_warning_message()
    {
        $this->_warning_message = "";
    }


	// Added 2014-07-21 by SysCo/al
    function get_warning_message()
    {
        return trim($this->_warning_message);
        clear_warning_message();
    }


	// Added 2014-07-21 by SysCo/al
	function ldap_get_one_entry_raw($id = "GENERIC", $first = FALSE, $srch_id = FALSE, $link_identifier = FALSE)
	{
        $linkIdentifier = ((FALSE !== $link_identifier) ? $link_identifier : $this->_conn_paged);
        $rawData = FALSE;
        if ($srch_id) {
            if ($first)
            {
                $this->_entry_identifier[$id] = ldap_first_entry($linkIdentifier, $srch_id);
            }
            elseif (FALSE !== $this->_entry_identifier[$id]) {
                $this->_entry_identifier[$id] = ldap_next_entry($linkIdentifier, $this->_entry_identifier[$id]);
            }
            if (FALSE !== $this->_entry_identifier[$id]) {
                $rawData = array();
                $rawData['count'] = 0; // To be compatible with the old data organisation (counter at the beginning)
                $attributes = ldap_get_attributes($linkIdentifier, $this->_entry_identifier[$id]);
                $distinguishedname_in_attributes = FALSE;

                for($j=0; $j<$attributes['count']; $j++) {
                    if ('distinguishedname' == strtolower($attributes[$j]))
                    {
                        $distinguishedname_in_attributes = TRUE;
                    }
                    $values = ldap_get_values_len($linkIdentifier, $this->_entry_identifier[$id],$attributes[$j]);
                    $rawData[strtolower($attributes[$j])] = $values;
                    $rawData[strtolower($attributes[$j])]['count'] = (isset($values['count'])?$values['count']:0);
                }
                if (!$distinguishedname_in_attributes)
                {
                    $rawData['distinguishedname'][0] = ldap_get_dn($linkIdentifier, $this->_entry_identifier[$id]);
                    $rawData['distinguishedname']['count'] = 1;
                    $attributes['count']++;
                }
                $rawData['count'] = $attributes['count'];
            }
        }
		return $rawData;
	}


	// Added by SysCo/al
	// New implementation 2014-07-21 by SysCo/al
	function ldap_get_entries_raw($srch_id, $id = "ALL-IN-ONE-LOOP")
	{
		$rawData = array();
		$rawData['count'] = 0; // To be compatible with the old data organisation (counter at the beginning)
		$i = 0;
		if ($result = $this->ldap_get_one_entry_raw($id, TRUE, $srch_id)) {
			do {
				$rawData[$i] = $result;
				$i++;
			} while ($result = $this->ldap_get_one_entry_raw($id));
		}
		unset($this->_entry_identifier[$id]);
		$rawData['count'] = $i; // and not count($rawData) because of the ['count'] argument
		return $rawData;
	}


    // Added by SysCo/al
    function IsError()
    {
        return $this->_error;
    }


    // Added by SysCo/al
    function ErrorMessage()
    {
        return ($this->IsError()?($this->_error_message):'');
    }

    
    // Added by SysCo/al
    function IsServerReachable()
    {
        return $this->_server_reachable;
    }


    // default destructor
    // Test added by SysCo/al
    function __destruct(){
        if ($this->_conn) { ldap_close ($this->_conn); }
        if ($this->_conn_paged) { ldap_close ($this->_conn_paged); }
    }

    //validate a users login credentials
    function authenticate($username,$password,$prevent_rebind=false){
        if ($username==NULL || $password==NULL){ return (false); } //prevent null binding
        
        //bind as the user		
        $this->_bind = @ldap_bind($this->_conn,$username.$this->_account_suffix,$password);
        $this->_bind_paged = @ldap_bind($this->_conn_paged,$username.$this->_account_suffix,$password);
        if (!$this->_bind){ return (false); }
        
        //once we've checked their details, kick back into admin mode if we have it
        if ($this->_ad_username!=NULL && !$prevent_rebind){
            $this->_bind = @ldap_bind($this->_conn,$this->_ad_username.$this->_account_suffix,$this->_ad_password);
            $this->_bind_paged = @ldap_bind($this->_conn_paged,$this->_ad_username.$this->_account_suffix,$this->_ad_password);
            if (!$this->_bind){
                // Modified by SysCo/al
                $this->_error = TRUE;
                $this->_error_message = 'FATAL: AD rebind failed.';
                exit();
            } //this should never happen in theory
        }
        
        return (true);
    }

    //*****************************************************************************************************************
    // GROUP FUNCTIONS

    // Add a group to a group
    function group_add_group($parent,$child){

        //find the parent group's dn
        $parent_group=$this->group_info($parent,array("cn"));
        if ($parent_group[0]["dn"]==NULL){ return (false); }
        $parent_dn=$parent_group[0]["dn"];
        
        //find the child group's dn
        $child_group=$this->group_info($child,array("cn"));
        if ($child_group[0]["dn"]==NULL){ return (false); }
        $child_dn=$child_group[0]["dn"];
                
        $add=array();
        $add["member"] = $child_dn;
        
        $result=@ldap_mod_add($this->_conn,$parent_dn,$add);
        if ($result==false){ return (false); }
        return (true);
    }

    // Add a user to a group
    function group_add_user($group,$user){
        //adding a user is a bit fiddly, we need to get the full DN of the user
        //and add it using the full DN of the group
        
        //find the user's dn
        $user_info=$this->user_info($user,array("cn"));
        if ($user_info[0]["dn"]==NULL){ return (false); }
        $user_dn=$user_info[0]["dn"];
        
        //find the group's dn
        $group_info=$this->group_info($group,array("cn"));
        if ($group_info[0]["dn"]==NULL){ return (false); }
        $group_dn=$group_info[0]["dn"];
        
        $add=array();
        $add["member"] = $user_dn;
        
        $result=@ldap_mod_add($this->_conn,$group_dn,$add);
        if ($result==false){ return (false); }
        return (true);
    }

    // Create a group
    function group_create($attributes){
        if (!is_array($attributes)){ return ("Attributes must be an array"); }
        if (!array_key_exists("group_name",$attributes)){ return ("Missing compulsory field [group_name]"); }
        if (!array_key_exists("container",$attributes)){ return ("Missing compulsory field [container]"); }
        if (!array_key_exists("description",$attributes)){ return ("Missing compulsory field [description]"); }
        if (!is_array($attributes["container"])){ return ("Container attribute must be an array."); }
        $attributes["container"]=array_reverse($attributes["container"]);

        //$member_array = array();
        //$member_array[0] = "cn=user1,cn=Users,dc=yourdomain,dc=com";
        //$member_array[1] = "cn=administrator,cn=Users,dc=yourdomain,dc=com";
        
        $add=array();
        $add["cn"] = $attributes["group_name"];
        $add[$this->_group_cn_identifier] = $attributes["group_name"];
        $add["objectClass"] = "Group";
        $add["description"] = $attributes["description"];
        //$add["member"] = $member_array; UNTESTED

        $container="OU=".implode(",OU=",$attributes["container"]);
        $result=ldap_add($this->_conn,"CN=".$add["cn"].", ".$container.",".$this->_base_dn,$add);
        if ($result!=true){ return (false); }
        
        return (true);
    }

    // Remove a group from a group
    function group_del_group($parent,$child){

        //find the parent dn
        $parent_group=$this->group_info($parent,array("cn"));
        if ($parent_group[0]["dn"]==NULL){ return (false); }
        $parent_dn=$parent_group[0]["dn"];
        
        //find the child dn
        $child_group=$this->group_info($child,array("cn"));
        if ($child_group[0]["dn"]==NULL){ return (false); }
        $child_dn=$child_group[0]["dn"];
        
        $del=array();
        $del["member"] = $child_dn;
        
        $result=@ldap_mod_del($this->_conn,$parent_dn,$del);
        if ($result==false){ return (false); }
        return (true);
    }

    // Remove a user from a group
    function group_del_user($group,$user){

        //find the parent dn
        $group_info=$this->group_info($group,array("cn"));
        if ($group_info[0]["dn"]==NULL){ return (false); }
        $group_dn=$group_info[0]["dn"];
        
        //find the child dn
        $user_info=$this->user_info($user,array("cn"));
        if ($user_info[0]["dn"]==NULL){ return (false); }
        $user_dn=$user_info[0]["dn"];

        $del=array();
        $del["member"] = $user_dn;
        
        $result=@ldap_mod_del($this->_conn,$group_dn,$del);
        if ($result==false){ return (false); }
        return (true);
    }

    // Returns an array of information for a specified group
    function group_info(
      $group_name,
      $fields=NULL
    ) {
        if ($group_name==NULL) {
          return (false);
        }
        if (!$this->_bind) {
          return (false);
        }

        $filter="(&(objectCategory=group)(".$this->_group_cn_identifier."=".$this->ldap_search_encode($group_name)."))";

        if ($fields==NULL) {
          $fields=array("member",$this->_group_attribute,"cn","description","distinguishedname","objectcategory",$this->_group_cn_identifier);
        }

        // Search info for a specific group, no paging needed
        $sr = @ldap_search($this->_conn, $this->_base_dn, $filter, $fields);
        if (FALSE === $sr) {
            $this->_warning_message = "group_info: ldap_search error ".ldap_errno($this->_conn).": ".ldap_error($this->_conn);
            // echo "DEBUG: ".$this->_warning_message."\n";
        }
        $entries = $this->ldap_get_entries_raw($sr);

        // DEBUG
        if (0 == count($entries)) {
            $this->_warning_message = "group_info: No entry for the specified filter $filter";
            // echo "DEBUG: ".$this->_warning_message."\n";
        }

        return ($entries);
    }

    // Return a complete list of "groups in groups"	
	// Cache added 2014-07-21 by SysCo/al
    function recursive_groups($group, $cache_only = FALSE) {
        $this->_debug_message = "";
        $this->_warning_message = "";
        if ($group==NULL){ return (false); }

        $ret_groups=array();
        $ret_data = FALSE;

        // echo "DEBUG GROUP is group $group in cache ?\n";
        // print_r($this->_cache_recursive_groups);

        clearstatcache();
        
		if ($this->_cache_support) {
            if ("" != $this->get_cache_folder()) {
                if (file_exists($this->get_cache_folder()."ldap_rgroup_".md5($group).".cache")) {
                    if ((filemtime($this->get_cache_folder()."ldap_rgroup_".md5($group).".cache") + $this->get_cache_timeout()) > time()) {
                        if ($cache_handle = fopen($this->get_cache_folder()."ldap_rgroup_".md5($group).".cache", "rb")) {
                            $ret_groups = unserialize(fgets($cache_handle));
                            $ret_data = TRUE;
                            fclose($cache_handle);
                            @touch($this->get_cache_folder()."ldap_rgroup_".md5($group).".cache");
                        }
                    }
                }
            } elseif (isset($this->_cache_recursive_groups[$group])) {
                $ret_groups = $this->_cache_recursive_groups[$group];
                $ret_data = TRUE;
            }
        }

    	if (!$ret_data) {
            if (!$cache_only) {
    			$groups=$this->group_info($group,array($this->_group_attribute));
    			
    			// Additional test by SysCo/al
    			if (isset($groups[0][$this->_group_attribute]))
    			{
    				$groups=$groups[0][$this->_group_attribute];

    				if ($groups){
    					$group_names=$this->nice_names($groups);

    					$ret_groups=array_merge($ret_groups,$group_names); //final groups to return
    					
    					foreach ($group_names as $id => $group_name){
    						$child_groups=$this->recursive_groups($group_name);
    						$ret_groups=array_merge($ret_groups,$child_groups);
    					}
    				}
    			}
    			if ($this->_cache_support) {
                    if ("" != $this->get_cache_folder()) {
                        if (file_exists($this->get_cache_folder())) {
                            if ($cache_handle = fopen($this->get_cache_folder()."ldap_rgroup_".md5($group).".cache", "wb")) {
                                fwrite($cache_handle, serialize($ret_groups));
                                fclose($cache_handle);
                                @chmod($this->get_cache_folder()."ldap_rgroup_".md5($group).".cache", octdec($this->_linux_file_mode));
                            }
                        }
                    } else {
        				$this->_cache_recursive_groups[$group] = $ret_groups;
                    }
    			}
            } else {
                $this->_debug_message = "The requested group $group is not in cache.";
            }
        }
        return ($ret_groups);
    }

    //*****************************************************************************************************************
    // USER FUNCTIONS

    //create a user
    function user_create($attributes){
        //check for compulsory fields
        if (!array_key_exists("username",$attributes)){ return ("Missing compulsory field [username]"); }
        if (!array_key_exists("firstname",$attributes)){ return ("Missing compulsory field [firstname]"); }
        if (!array_key_exists("surname",$attributes)){ return ("Missing compulsory field [surname]"); }
        if (!array_key_exists("email",$attributes)){ return ("Missing compulsory field [email]"); }
        if (!array_key_exists("container",$attributes)){ return ("Missing compulsory field [container]"); }
        if (!is_array($attributes["container"])){ return ("Container attribute must be an array."); }

        if (array_key_exists("password",$attributes) && !$this->_use_ssl) {
          // Modified by SysCo/al
          $this->_error = TRUE;
          $this->_error_message = 'FATAL: SSL must be configured on your webserver and enabled in the class to set passwords.';
          exit();
        }

        if (!array_key_exists("display_name",$attributes)) {
          $attributes["display_name"]=$attributes["firstname"]." ".$attributes["surname"];
        }

        //translate the schema
        $add = $this->adldap_schema($attributes);
        
        //additional stuff only used for adding accounts
        $add["cn"][0]=$attributes["display_name"];
        $add[$this->_cn_identifier][0]=$attributes["username"];
        $add["objectclass"][0]="top";
        $add["objectclass"][1]="person";
        $add["objectclass"][2]="organizationalPerson";
        $add["objectclass"][3]="user"; //person?
        //$add["name"][0]=$attributes["firstname"]." ".$attributes["surname"];

        //set the account control attribute
        $control_options=array("NORMAL_ACCOUNT");
        if (!$attributes["enabled"]){ $control_options[]="ACCOUNTDISABLE"; }
        $add["userAccountControl"][0]=$this->account_control($control_options);
        //echo ("<pre>"); print_r($add);

        //determine the container
        $attributes["container"]=array_reverse($attributes["container"]);
        $container="OU=".implode(",OU=",$attributes["container"]);

        //add the entry
        $result=@ldap_add($this->_conn, "CN=".$add["cn"][0].", ".$container.",".$this->_base_dn, $add);
        if ($result!=true){ return (false); }
        
        return (true);
    }

    
    // group_users($group_name)
    //	Returns an array of users that are members of a group
    function group_users(
      $group_name=NUL
    ) {
      $result = array();
      if ($group_name==NULL) {
        return (false);
      }
      if (!$this->_bind) {
        return (false);
      }

      if ((2 == $this->_ldap_server_type) || (4 == $this->_ldap_server_type)) { // Generic LDAP or eDirectory
        $filter = "(&(|(objectClass=posixGroup)(objectClass=groupofNames))(".$this->_group_cn_identifier."=".$this->ldap_search_encode($group_name)."))";
      } else { // Active Directory
        $group_account_type = "(|(samaccounttype=".ADLDAP_SECURITY_LOCAL_GROUP.")(samaccounttype=". ADLDAP_SECURITY_GLOBAL_GROUP."))";
        $filter = "(&(objectCategory=group)".$group_account_type."(".$this->_group_cn_identifier."=".$this->ldap_search_encode($group_name)."))";
      }
      
      $fields = array("member", "memberuid");
      $paging_cookie = '';

      // New paging support for PHP 7.3+, paging loop not implemented yet
      if ($this->_paging_ldap_control) {
        $sr = ldap_search($this->_conn, $this->_base_dn, $filter, $fields,
          0, 0, 0, LDAP_DEREF_NEVER,
          [['oid' => LDAP_CONTROL_PAGEDRESULTS, 'value' => ['size' => $this->_paging_size, 'cookie' => $paging_cookie]]]);
          ldap_parse_result($this->_conn, $sr, $error_code , $matched_dn , $error_message , $referrals, $controls);
        if (isset($controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'])) {
          $paging_cookie = $controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'];
        } else {
          $paging_cookie = '';
        }
      } else {
        $sr = ldap_search($this->_conn, $this->_base_dn, $filter, $fields);
      }

      if (FALSE === $sr) {
        $this->_warning_message = "group_users: ldap_search error ".ldap_errno($this->_conn).": ".ldap_error($this->_conn);
        // echo "DEBUG: ".$this->_warning_message."\n";
      }
      $entries = $this->ldap_get_entries_raw($sr);

      if (0 == count($entries)) {
        $this->_warning_message = "group_users: No entry for the specified filter $filter";
        // echo "DEBUG: ".$this->_warning_message;
      }

      if (isset($entries[0]["member"][0])) {
        $result = $this->nice_names($entries[0]["member"]);
        /*
        for ($i=0; $i++; $i < $entries[0]["member"][count])
        {
            $result[] == ($entries[0]["member"][$i]);
        }
        */
      }
      elseif (isset($entries[0]["memberuid"][0])) {
        $result = $this->nice_names($entries[0]["memberuid"]);
      } else {
        $result = array();
      }
      return ($result);
    }

    // user_groups($user)
    //	Returns an array of groups that a user is a member off
    function user_groups($username,$recursive=NULL){
      if ($username==NULL){ return (false); }
      if ($recursive==NULL){ $recursive=$this->_recursive_groups; } //use the default option if they haven't set it
      if (!$this->_bind){ return (false); }
      
      //search the directory for their information
      $info=@$this->user_info($username,array($this->_group_attribute,"member","primarygroupid"));
      
      $groups=$this->nice_names($info[0][$this->_group_attribute]); //presuming the entry returned is our guy (unique usernames)

      if ($recursive){
          foreach ($groups as $id => $group_name){
              $extra_groups=$this->recursive_groups($group_name);
              $groups=array_merge($groups,$extra_groups);
          }
      }
      return ($groups);
    }
    
    
    function rCountRemover($arr) {
      foreach($arr as $key=>$val) {
        # (int)0 == "count", so we need to use ===
        if($key === "count")
          unset($arr[$key]);
        elseif(is_array($val))
          $arr[$key] = $this->rCountRemover($arr[$key]);
      }
      return $arr;
    }

    // New function, for enhanced Active Directory
    function user_all_groups($username, $groups_filtering = '') {
      if ($username==NULL){ return (false); }
      if (!$this->_bind){ return (false); }
      $filter = "(&(objectCategory=group)(member:1.2.840.113556.1.4.1941:=".$username.")".$groups_filtering.")";
      // $fields = array($this->_group_cn_identifier,$this->_group_attribute,"distinguishedname");
      $fields = array("cn");
      // Search all groups of one user, it should not reach the paging size, no paging implemented
      $sr = ldap_search($this->_conn, $this->_base_dn, $filter, $fields);
      if (FALSE === $sr) {
        $this->_warning_message = "user_all_groups: ldap_search error ".ldap_errno($this->_conn).": ".ldap_error($this->_conn);
        // echo "DEBUG: ".$this->_warning_message."\n";
      }
      $group_entries = $this->rCountRemover(ldap_get_entries($this->_conn, $sr));

      // echo "DEBUG: info group_entries\n";
      // print_r($group_entries);
      
      $group_array = array();
      foreach ($group_entries as $group_entry) {
        $group_array[] = $group_entry['cn'][0];
      }

      // echo "DEBUG: group_array\n";
      // print_r($group_array);

      return ($group_array);
    }


	// Added by SysCo/al
	// New implementation 2014-07-21 by SysCo/al
    // Returns an array of information for filtered users
    function users_info($username=NULL, $fields=NULL, $groups_filtering = '')
	{
		$entries = array();
		$entries['count'] = 0; // To be compatible with the old data organisation (counter at the beginning)
		$i = 0;
		if ($result = $this->one_user_info(TRUE, $username, $fields, FALSE, $groups_filtering)) {
			do {
				$entries[$i] = $result;
				$i++;
			} while ($result = $this->one_user_info());
		}
		$entries['count'] = $i; // and not count($entries) because of the ['count'] argument
      return ($entries);
    }


	// Added 2014-07-21 by SysCo/al, enhanced 2018-07-12 and 2023-01-02
    function one_user_info(
      $first = FALSE,
      $username = NULL,
      $fields = NULL,
      $group_cn_cache_only = FALSE,
      $groups_filtering = '',
      $in_groups_filtering = array()
    ) {
      $this->_warning_message = '';
      $sr = FALSE;
      if ($first) {
        if ($username==NULL){
          return (false);
        }
        if (!$this->_bind_paged){
          return (false);
        }
        if  ('' == $this->_users_dn) {
          $this->_users_dn = $this->_base_dn;
        }

        if (1 == $this->_ldap_server_type) { // Active Directory
          $filter = "(&(objectClass=user)(samaccounttype=". ADLDAP_NORMAL_ACCOUNT .")(objectCategory=person)(".$this->_cn_identifier."=".$username.")".$groups_filtering.")";
          if ($fields==NULL){ $fields=array($this->_cn_identifier,"mail",$this->_group_attribute,"department","description","displayname","telephonenumber","primarygroupid","distinguishedname"); }
        } elseif (3 == $this->_ldap_server_type) { // legacy Active Directory
          $filter = "(&(objectClass=user)(samaccounttype=". ADLDAP_NORMAL_ACCOUNT .")(objectCategory=person)(".$this->_cn_identifier."=".$username."))";
          if ($fields==NULL){ $fields=array($this->_cn_identifier,"mail",$this->_group_attribute,"department","description","displayname","telephonenumber","primarygroupid","distinguishedname"); }
        } else { // eDirectory (4) or Generic LDAP (2) or others
          $filter = "(&(|(objectClass=posixAccount)(objectClass=user))(".$this->_cn_identifier."=".$username."))";
          if ($fields==NULL){ $fields=array($this->_cn_identifier,"mail",$this->_group_attribute,"department","gecos","description","displayname","telephonenumber","gidnumber","distinguishedname"); }
        }
        if ("" != $this->_ldap_filter) {
          $filter = $this->_ldap_filter;
          $filter = str_replace('{cn_identifier}', $this->_cn_identifier, $filter);
          $filter = str_replace('{username}', $username, $filter);
          $filter = str_replace('{groups_filtering}', $groups_filtering, $filter);
        }
        
        // New paging support for PHP 7.3+, old and new paging implemented outside the function
        if ($this->_paging_ldap_control) {
          $this->_oui_paged_sr = @ldap_search($this->_conn_paged, $this->_users_dn, $filter, $fields,
            0, 0, 0, LDAP_DEREF_NEVER,
            [['oid' => LDAP_CONTROL_PAGEDRESULTS, 'value' => ['size' => $this->_paging_size, 'cookie' => $this->_paging_cookie]]]);
            ldap_parse_result($this->_conn_paged, $this->_oui_paged_sr, $error_code , $matched_dn , $error_message , $referrals, $controls);
            if (isset($controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'])) {
              $this->_paging_cookie = $controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'];
          } else {
            $this->_paging_cookie = '';
          }
        } else {
          $this->_oui_paged_sr = @ldap_search($this->_conn_paged, $this->_users_dn, $filter, $fields);
        }

        if (4 == ldap_errno($this->_conn_paged)) { // 4 = LDAP_SIZELIMIT_EXCEEDED
          $cr = @ldap_count_entries($this->_conn_paged, $this->_oui_paged_sr);
          $this->_warning_message = "LDAP server cannot return more than $cr records.";
        }
        // echo "DEBUG: Error: ".ldap_errno($this->_conn_paged);
        // $cr = ldap_count_entries($this->_conn_paged,$sr);
        // echo $cr;
        // echo "DEBUG: Error: ".ldap_errno($this->_conn_paged);
      }
      if ($one_entry = $this->ldap_get_one_entry_raw("ONE_USER", $first, $this->_oui_paged_sr, $this->_conn_paged)) {
        // AD does not return the primary group in the ldap query, we may need to fudge it
        // SysCo/al added a test to check if $entries[0]["primarygroupid"][0] exists
        $add_primary_group = FALSE;
        if ($this->_real_primarygroup) {
          if (isset($one_entry["primarygroupid"][0])) {
            $pri_grp_rid = $one_entry["primarygroupid"][0];
            if (isset($this->_cache_group_dn[$pri_grp_rid])) {
              $dn_group = $this->_cache_group_dn[$pri_grp_rid];
              $one_entry[$this->_group_attribute][] = $dn_group;
              $add_primary_group = TRUE;
            } else {
              $r = ldap_read($this->_conn, $this->_base_dn, "(objectclass=*)", array("objectSid")) or exit();
              $data = ldap_get_entries($this->_conn, $r);
              if (isset($data[0]["objectsid"][0])) {
                $domain_sid = $data[0]["objectsid"][0];
                $domain_sid_s = sid2str($domain_sid);
                // No paging implemented here, it's a specific object, no paging needed
                $r = ldap_search($this->_conn, $this->_base_dn, "objectSid=".$domain_sid_s."-".$pri_grp_rid, array("distinguishedname")) or exit();
                $data = ldap_get_entries($this->_conn, $r);
                if (isset($data[0]["distinguishedname"][0])) {
                  $dn_group = $data[0]["distinguishedname"][0];
                  $one_entry[$this->_group_attribute][] = $dn_group;
                  $add_primary_group = TRUE;
                  if (count($this->_cache_group_dn) < 1000) { // Don't overload the memory cache
                    $this->_cache_group_dn[$pri_grp_rid] = $dn_group;
                  }
                }
              }
            }
          }
        } else {
          $one_entry[$this->_group_attribute][]="CN=Domain Users,CN=Users,".$this->_base_dn;
          $add_primary_group = TRUE;
        }

        if ($add_primary_group) {
          @$one_entry[$this->_group_attribute]["count"]++;
        }
        
        if ((1 == $this->_ldap_server_type) && ( '' != $groups_filtering)) { // Active Directory (but not legacy)
          // Add the nested groups of the user
          $filter = "(&(objectCategory=group)(member:1.2.840.113556.1.4.1941:=".$one_entry[$this->_cn_identifier][0].")".$groups_filtering.")";
          $internal_fields = array("cn");
          $group_array = array();
          // SysCo/al added ldap_search error handling, looking for a specific info, no paging needed
          if (FALSE !== ($sr = @ldap_search($this->_conn, $this->_base_dn, $filter, $internal_fields))) {
            $group_entries = $this->rCountRemover(ldap_get_entries($this->_conn, $sr));
            foreach ($group_entries as $group_entry) {
              $group_array[] = $group_entry['cn'][0];
            }
          }
          foreach($group_array as $one_group) {
            $add_it = TRUE;
            foreach($this->nice_names($one_entry[$this->_group_attribute]) as $level_one_group) {
              if (strpos($one_group, $level_one_group) !== FALSE) {
                $add_it = FALSE;
              }
            }
            if ($add_it) {
              $one_entry[$this->_group_attribute][] = $one_group;
              @$one_entry[$this->_group_attribute]["count"]++;
            }
          }

          // Adding all implicit groups if needed
          foreach($in_groups_filtering as $one_group_filtering) {
            $filter = "(&(objectClass=user)(samaccounttype=". ADLDAP_NORMAL_ACCOUNT .")(objectCategory=person)(".$this->_cn_identifier."=".$one_entry[$this->_cn_identifier][0].")".$one_group_filtering['distinguishedname'].")";
            $internal_fields = array("cn");
            // SysCo/al added ldap_search error handling, no paging needed
            if (FALSE !== ($sr = @ldap_search($this->_conn, $this->_base_dn, $filter, $internal_fields))) {
              if (ldap_count_entries($this->_conn, $sr) > 0) {
                $add_it = TRUE;
                foreach($this->nice_names($one_entry[$this->_group_attribute]) as $level_one_group) {
                  if (strpos($one_group_filtering['name'], $level_one_group) !== FALSE) {
                    $add_it = FALSE;
                  }
                }
                if ($add_it) {
                  $one_entry[$this->_group_attribute][] = $one_group_filtering['name'];
                  @$one_entry[$this->_group_attribute]["count"]++;
                }
              }
            }
          }
        }
      }
      return ($one_entry);
    }


    // Returns an array of information for a specific user
    function user_info($username,$fields=NULL){
        if ($username==NULL){ return (false); }
        if (!$this->_bind){ return (false); }
        if  ('' == $this->_users_dn) {
            $this->_users_dn = $this->_base_dn;
        }

        $filter = "(&(".$this->_cn_identifier."=".$username."))";
        if ($fields==NULL){ $fields=array($this->_cn_identifier,"mail",$this->_group_attribute,"department","description","displayname","gecos","telephonenumber","primarygroupid"); }
        // No paging implemented here, it's a specific object, no paging needed
        $sr=ldap_search($this->_conn, $this->_users_dn, $filter, $fields);
        $entries = $this->ldap_get_entries_raw($sr);
        
        // AD does not return the primary group in the ldap query, we may need to fudge it
        // SysCo/al added a test to check if $entries[0]["primarygroupid"][0] exists
        $add_primary_group = FALSE;
        if ($this->_real_primarygroup){
            if (isset($one_entry["primarygroupid"][0])) {
                $pri_grp_rid = $one_entry["primarygroupid"][0];
                if (isset($this->_cache_group_dn[$pri_grp_rid])) {
                    $dn_group = $this->_cache_group_dn[$pri_grp_rid];
                    $one_entry[$this->_group_attribute][] = $dn_group;
                    $add_primary_group = TRUE;
                } else {
                    $r = ldap_read($this->_conn, $this->_base_dn, "(objectclass=*)", array("objectSid")) or exit();
                    $data = ldap_get_entries($this->_conn, $r);
                    if (isset($data[0]["objectsid"][0])) {
                        $domain_sid = $data[0]["objectsid"][0];
                        $domain_sid_s = sid2str($domain_sid);
                        // No paging implemented here, it's a specific object, no paging needed
                        $r = ldap_search($this->_conn, $this->_base_dn, "objectSid=".$domain_sid_s."-".$pri_grp_rid, array("distinguishedname")) or exit();
                        $data = ldap_get_entries($this->_conn, $r);
                        if (isset($data[0]["distinguishedname"][0])) {
                            $dn_group = $data[0]["distinguishedname"][0];
                            $one_entry[$this->_group_attribute][] = $dn_group;
                            $add_primary_group = TRUE;
                                if (count($this->_cache_group_dn) < 1000) { // Don't overload the memory cache
                                $this->_cache_group_dn[$pri_grp_rid] = $dn_group;
                            }
                        }
                    }
                }
            }
        } else {
            $entries[0][$this->_group_attribute][]="CN=Domain Users,CN=Users,".$this->_base_dn;
            $add_primary_group = TRUE;
        }
        if ($add_primary_group) {
            @$entries[0][$this->_group_attribute]["count"]++;
        }

        return ($entries);
    }

    // Returns true if the user is a member of the group
    function user_ingroup($username,$group,$recursive=NULL){
        if ($username==NULL){ return (false); }
        if ($group==NULL){ return (false); }
        if (!$this->_bind){ return (false); }
        if ($recursive==NULL){ $recursive=$this->_recursive_groups; } //use the default option if they haven't set it
        
        //get a list of the groups
        $groups=$this->user_groups($username,array($this->_group_attribute),$recursive);
        
        //return true if the specified group is in the group list
        if (in_array($group,$groups)){ return (true); }

        return (false);
    }

    //modify a user
    function user_modify($username,$attributes){
        if ($username==NULL){ return ("Missing compulsory field [username]"); }
        if (array_key_exists("password",$attributes) && !$this->_use_ssl){ echo ("FATAL: SSL must be configured on your webserver and enabled in the class to set passwords."); exit(); }
        //if (array_key_exists("container",$attributes)){
            //if (!is_array($attributes["container"])){ return ("Container attribute must be an array."); }
            //$attributes["container"]=array_reverse($attributes["container"]);
        //}

        //find the dn of the user
        $user=$this->user_info($username,array("cn"));
        if ($user[0]["dn"]==NULL){ return (false); }
        $user_dn=$user[0]["dn"];

        //translate the update to the LDAP schema				
        $mod=$this->adldap_schema($attributes);
        if (!$mod){ return (false); }
        
        //set the account control attribute (only if specified)
        if (array_key_exists("enabled",$attributes)){
            if ($attributes["enabled"]){ $control_options=array("NORMAL_ACCOUNT"); }
            else { $control_options=array("NORMAL_ACCOUNT","ACCOUNTDISABLE"); }
            $mod["userAccountControl"][0]=$this->account_control($control_options);
        }

        //do the update
        $result=ldap_modify($this->_conn,$user_dn,$mod);
        if ($result==false){ return (false); }
        
        return (true);
    }
        
    // Set the password of a user
    function user_password($username,$password) {
      if ($username==NULL){ return (false); }
      if ($password==NULL){ return (false); }
      if (!$this->_bind){ return (false); }
      if (!$this->_use_ssl){ echo ("FATAL: SSL must be configured on your webserver and enabled in the class to set passwords."); exit(); }
      
      $user=$this->user_info($username,array("cn"));
      if ($user[0]["dn"]==NULL){ return (false); }
      $user_dn=$user[0]["dn"];
              
      $add=array();
      $add["unicodePwd"][0]=$this->encode_password($password);
      
      $result=ldap_mod_replace($this->_conn,$user_dn,$add);
      if ($result==false){ return (false); }
      
      return (true);
    }

    //*****************************************************************************************************************
    // COMPUTER FUNCTIONS

    // Returns an array of information for a specific computer
    function computer_info($computer_name,$fields=NULL) {
      if ($computer_name==NULL){ return (false); }
      if (!$this->_bind){ return (false); }

      $filter="(&(objectClass=computer)(cn=".$computer_name."))";
      if ($fields==NULL) {
        $fields=array($this->_group_attribute,"cn","displayname","dnshostname","distinguishedname","objectcategory","operatingsystem","operatingsystemservicepack","operatingsystemversion");
      }
      // No paging implemented here, it's a specific object, no paging needed
      $sr = ldap_search($this->_conn,$this->_base_dn,$filter,$fields);
      $entries = $this->ldap_get_entries_raw($sr);
      
      return ($entries);
    }

    // Returns all AD users
    function all_users($include_desc = false, $search = "*", $sorted = true) {
      if (!$this->_bind){
        return (false);
      }
      //perform the search and grab all their details
      $filter = "(&(objectClass=user)(samaccounttype=". ADLDAP_NORMAL_ACCOUNT .")(objectCategory=person)(cn=".$search."))";
      $fields = array($this->_cn_identifier,"displayname");
      $sr = ldap_search($this->_conn, $this->_base_dn, $filter, $fields);
      $entries = $this->ldap_get_entries_raw($sr);

      $users_array = array();
      for ($i=0; $i<$entries["count"]; $i++){
        if ($include_desc && strlen($entries[$i]["displayname"][0])>0){
          $users_array[ $entries[$i][$this->_cn_identifier][0] ] = $entries[$i]["displayname"][0];
        } elseif ($include_desc){
          $users_array[ $entries[$i][$this->_cn_identifier][0] ] = $entries[$i][$this->_cn_identifier][0];
        } else {
          array_push($users_array, $entries[$i][$this->_cn_identifier][0]);
        }
      }
      if ($sorted){ asort($users_array); }
      return ($users_array);
    }


    // Returns a complete list of the groups in AD
    // New implementation 2014-07-22 by SysCo/al (with paging support)
    function all_groups($include_desc = false,
                        $search = "*",
                        $sorted = true,
                        $local_group = FALSE // $local_group switch added by SysCo/al
                       )
    {
      $this->_warning_message = "";
      if (!$this->_bind) {
        return (false);
      }

      if ((2 == $this->_ldap_server_type) || (4 == $this->_ldap_server_type)) { // Generic LDAP or eDirectory
        $filter="(|(objectClass=posixGroup)(objectClass=groupofNames))";
        $fields=array($this->_group_cn_identifier,"description");
      } else { // Active Directory
        //perform the search and grab all their details
        if ($local_group) {
          $group_account_type = "(|(samaccounttype=".ADLDAP_SECURITY_LOCAL_GROUP.")(samaccounttype=". ADLDAP_SECURITY_GLOBAL_GROUP."))";
        } else {
          $group_account_type = "(samaccounttype=".ADLDAP_SECURITY_GLOBAL_GROUP.")";
        }
        $filter = "(&(objectCategory=group)".$group_account_type."(cn=".$search."))";
        $fields = array($this->_group_cn_identifier,"description");
      }

      $groups_array = array();

      $paging_cookie = '';
      do { // LDAP pagination loop
        if ($this->_paging_legacy) {
          ldap_control_paged_result($this->_conn, $this->_paging_size, false, $paging_cookie);
        }

        // New paging support for PHP 7.3+
        if ($this->_paging_ldap_control) {
          $sr = @ldap_search($this->_conn, $this->_base_dn, $filter, $fields,
            0, 0, 0, LDAP_DEREF_NEVER,
            [['oid' => LDAP_CONTROL_PAGEDRESULTS, 'value' => ['size' => $this->_paging_size, 'cookie' => $paging_cookie]]]);
            ldap_parse_result($this->_conn, $sr, $error_code , $matched_dn , $error_message , $referrals, $controls);
          if (isset($controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'])) {
            $paging_cookie = $controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'];
          } else {
            $paging_cookie = '';
          }
        } else {
          $sr = @ldap_search($this->_conn, $this->_base_dn, $filter, $fields);
        }
    
        if (4 == ldap_errno($this->_conn)) { // 4 = LDAP_SIZELIMIT_EXCEEDED
          $cr = @ldap_count_entries($this->_conn,$sr);
          $this->_warning_message = "LDAP server cannot return more than $cr records.";
        }
    
        $entries = $this->ldap_get_entries_raw($sr);

        for ($i=0; $i<$entries["count"]; $i++){
          if ($include_desc && strlen($entries[$i]["description"][0]) > 0 ) {
            $groups_array[ $entries[$i][$this->_group_cn_identifier][0] ] = $entries[$i]["description"][0];
          } elseif ($include_desc){
            if (isset($entries[$i][$this->_group_cn_identifier][0])) {
              $groups_array[ $entries[$i][$this->_group_cn_identifier][0] ] = $entries[$i][$this->_group_cn_identifier][0];
            }
          } else {
            if (isset($entries[$i][$this->_group_cn_identifier][0])) {
              array_push($groups_array, $entries[$i][$this->_group_cn_identifier][0]);
            }
          }
        }
        if ($this->_paging_legacy) {
          ldap_control_paged_result_response($this->_conn, $sr, $paging_cookie);
        }
      } while(($paging_cookie !== null) && ($paging_cookie != ''));
      // LDAP pagination loop
      
      if ($this->_paging_legacy) {
        // Reset LDAP paged result
        ldap_control_paged_result($this->_conn, $this->_paging_size, false);
      }
      
      if( $sorted ){
        asort($groups_array);
      }
      
      return ($groups_array);

    }


    //************************************************************************************************************
    // UTILITY FUNCTIONS (not intended to be called directly but I suppose you could?)

    function adldap_schema($attributes) {

      //ldap doesn't like NULL attributes, only set them if they have values
      // I'd like to know how to set an LDAP attribute to NULL though, at the moment I set it to a space
      // SysCo/al added "mobile"
      $mod = array();
      if ($attributes["address_city"]){ $mod["l"][0]=$attributes["address_city"]; }
      if ($attributes["address_code"]){ $mod["postalCode"][0]=$attributes["address_code"]; }
      //if ($attributes["address_country"]){ $mod["countryCode"][0]=$attributes["address_country"]; } // use country codes?
      if ($attributes["address_pobox"]){ $mod["postOfficeBox"][0]=$attributes["address_pobox"]; }
      if ($attributes["address_state"]){ $mod["st"][0]=$attributes["address_state"]; }
      if ($attributes["address_street"]){ $mod["streetAddress"][0]=$attributes["address_street"]; }
      if ($attributes["company"]){ $mod["company"][0]=$attributes["company"]; }
      if ($attributes["change_password"]){ $mod["pwdLastSet"][0]=0; }
      if ($attributes["company"]){ $mod["company"][0]=$attributes["company"]; }
      if ($attributes["department"]){ $mod["department"][0]=$attributes["department"]; }
      if ($attributes["description"]){ $mod["description"][0]=$attributes["description"]; }
      if ($attributes["display_name"]){ $mod["displayName"][0]=$attributes["display_name"]; }
      if ($attributes["email"]){ $mod["mail"][0]=$attributes["email"]; }
      if ($attributes["expires"]){ $mod["accountExpires"][0]=$attributes["expires"]; } //unix epoch format?
      if ($attributes["firstname"]){ $mod["givenName"][0]=$attributes["firstname"]; }
      if ($attributes["home_directory"]){ $mod["homeDirectory"][0]=$attributes["home_directory"]; }
      if ($attributes["home_drive"]){ $mod["homeDrive"][0]=$attributes["home_drive"]; }
      if ($attributes["initials"]){ $mod["initials"][0]=$attributes["initials"]; }
      if ($attributes["logon_name"]){ $mod["userPrincipalName"][0]=$attributes["logon_name"]; }
      if ($attributes["manager"]){ $mod["manager"][0]=$attributes["manager"]; }  //UNTESTED ***Use DistinguishedName***
      if ($attributes["office"]){ $mod["physicalDeliveryOfficeName"][0]=$attributes["office"]; }
      if ($attributes["password"]){ $mod["unicodePwd"][0]=$this->encode_password($attributes["password"]); }
      if ($attributes["profile_path"]){ $mod["profilepath"][0]=$attributes["profile_path"]; }
      if ($attributes["script_path"]){ $mod["scriptPath"][0]=$attributes["script_path"]; }
      if ($attributes["surname"]){ $mod["sn"][0]=$attributes["surname"]; }
      if ($attributes["title"]){ $mod["title"][0]=$attributes["title"]; }
      if ($attributes["telephoneNumber"]){ $mod["telephoneNumber"][0]=$attributes["telephoneNumber"]; }
      if ($attributes["telephone"]){ $mod["telephoneNumber"][0]=$attributes["telephone"]; }
      if ($attributes["mobile"]){ $mod["telephoneNumber"][0]=$attributes["mobile"]; }
      if ($attributes["web_page"]){ $mod["wWWHomePage"][0]=$attributes["web_page"]; }
      //echo ("<pre>"); print_r($mod);
  /*
      // modifying a name is a bit fiddly
      if ($attributes["firstname"] && $attributes["surname"]){
          $mod["cn"][0]=$attributes["firstname"]." ".$attributes["surname"];
          $mod["displayname"][0]=$attributes["firstname"]." ".$attributes["surname"];
          $mod["name"][0]=$attributes["firstname"]." ".$attributes["surname"];
      }
  */


      if (count($mod)==0) {
        return (false);
      }
      return ($mod);
    }


    function group_cn($gid, $cache_only = FALSE, $local_group = FALSE){
        // coping with AD not returning the primary group
        // http://support.microsoft.com/?kbid=321360
        // for some reason it's not possible to search on primarygrouptoken=XXX
        // if someone can show otherwise, I'd like to know about it :)
        // this way is resource intensive and generally a pain in the @#%^
		// Cache added 2014-07-21 by SysCo/al
        // Cache only added 2014-07-23 by SysCo/al
        $this->_warning_message = "";
        if ($gid==NULL){ return (false); }
        $r=false;
        $r_data = FALSE;

        if ($this->_cache_support) {
            if ("" != $this->get_cache_folder()) {
                if (file_exists($this->get_cache_folder()."ldap_cgroup_".md5($gid).".cache")) {
                    if ((filemtime($this->get_cache_folder()."ldap_cgroup_".md5($gid).".cache") + $this->get_cache_timeout()) > time()) {
                        if ($cache_handle = fopen($this->get_cache_folder()."ldap_cgroup_".md5($gid).".cache", "rb")) {
                            $r = unserialize(fgets($cache_handle));
                            $r_data = TRUE;
                            fclose($cache_handle);
                            touch($this->get_cache_folder()."ldap_group_cn_$gid.cache");
                        }
                    }
                }
            } elseif (isset($this->_cache_group_dn[$gid])) {
    			$r = $this->_cache_group_dn[$gid];
                $r_data = TRUE;
            }
        }
        if (!$r_data) {
            if (!$cache_only) {
                if ((2 == $this->_ldap_server_type) || (4 == $this->_ldap_server_type)) { // Generic LDAP or eDirectory
                    // http://www.rainingpackets.com/ldap-posixgroup-groupofnames/
                    $filter="(|(objectClass=posixGroup)(objectClass=groupofNames))";
                    $fields=array("gidnumber",$this->_group_cn_identifier,"distinguishedname");
                } else { // Active Directory or legacy Active Directory
                    if ($local_group) {
                        $group_account_type = "(|(samaccounttype=".ADLDAP_SECURITY_LOCAL_GROUP.")(samaccounttype=". ADLDAP_SECURITY_GLOBAL_GROUP."))";
                    } else {
                        $group_account_type = "(samaccounttype=".ADLDAP_SECURITY_GLOBAL_GROUP.")";
                    }
                    $filter="(&(objectCategory=group)".$group_account_type.")";
                    $fields=array("primarygrouptoken",$this->_group_cn_identifier,"distinguishedname");
                }
                
                $paging_cookie = '';
                do { // LDAP pagination loop
                    if ($this->_paging_legacy) {
                      ldap_control_paged_result($this->_conn, $this->_paging_size, false, $paging_cookie);
                    }
                    $sr = @ldap_search($this->_conn,$this->_base_dn,$filter,$fields);
                
                    if (4 == ldap_errno($this->_conn)) { // 4 = LDAP_SIZELIMIT_EXCEEDED
                        $cr = @ldap_count_entries($this->_conn,$sr);
                        $this->_warning_message = "LDAP server cannot return more than $cr records.";
                    }
                
                    $entries = $this->ldap_get_entries_raw($sr);

                    for ($i=0; $i<$entries["count"]; $i++) {
                        // if (!isset($entries[$i]["distinguishedname"][0]))
                        if ((2 == $this->_ldap_server_type) || (4 == $this->_ldap_server_type)) { // Generic LDAP and eDirectory, we don't want the full distinguishedname for posixGroups, cn only
                            // $entries[$i]["distinguishedname"][0] = ldap_get_dn($this->_conn, $entries[$i]);
                            // We want to use the cn only

                            if (isset($entries[$i][$this->_group_cn_identifier][0])) {
                                $entries[$i]["distinguishedname"][0] = $entries[$i][$this->_group_cn_identifier][0];
                            }
                        }
                        if (!isset($entries[$i]["primarygrouptoken"][0])) {
                            $entries[$i]["primarygrouptoken"][0] = (isset($entries[$i]["gidnumber"][0])?$entries[$i]["gidnumber"][0]:NULL);
                        }

                        if ($this->_cache_support) {
                            if (NULL !== $entries[$i]["primarygrouptoken"][0]) {
                                if ("" != $this->get_cache_folder()) {
                                    if (file_exists($this->get_cache_folder())) {
                                        if ($cache_handle = fopen($this->get_cache_folder()."ldap_cgroup_".md5($entries[$i]["primarygrouptoken"][0]).".cache", "wb")) {
                                            fwrite($cache_handle, serialize($entries[$i]["distinguishedname"][0]));
                                            fclose($cache_handle);
                                            @chmod($this->get_cache_folder()."ldap_cgroup_".md5($entries[$i]["primarygrouptoken"][0]).".cache", octdec($this->_linux_file_mode));
                                        }
                                    }
                                } else {
                                    $this->_cache_group_dn[$entries[$i]["primarygrouptoken"][0]] = $entries[$i]["distinguishedname"][0];
                                }
                            }
                        }
                        if ($entries[$i]["primarygrouptoken"][0]==$gid){
                            $r=$entries[$i]["distinguishedname"][0];
                            $i=$entries["count"];
                        }
                    }
                    if ($this->_paging_legacy) {
                      ldap_control_paged_result_response($this->_conn, $sr, $paging_cookie);
                    }
                } while(($paging_cookie !== null) && ($paging_cookie != ''));
                // LDAP pagination loop
                
                if ($this->_paging_legacy) {
                  // Reset LDAP paged result
                  ldap_control_paged_result($this->_conn, $this->_paging_size, false);
                }
            }
        }
        return ($r);
    }

    // Encode a password for transmission over LDAP
    function encode_password($password){
        $password="\"".$password."\"";
        $encoded="";
        for ($i=0; $i <strlen($password); $i++){ $encoded.=$password[$i]."\000"; }
        return ($encoded);
    }

    // Escape bad characters
    // DEVELOPERS SHOULD BE DOING PROPER FILTERING IF THEY'RE ACCEPTING USER INPUT
    // this is just a list of characters with known problems and I'm trying not to strip out other languages
    function ldap_slashes($str){
        $illegal=array("(",")","#"); // the + character has problems too, but it's an illegal character
        
        $legal=array();
        foreach ($illegal as $id => $char){ $legal[$id]="\\".$char; } //make up the array of legal chars
        
        $str=str_replace($illegal,$legal,$str); //replace them
        return ($str);
    }

    // Added by SysCo/al 2015-08-13
    // This method will encode the string using RFC4514
    // https://www.ietf.org/rfc/rfc4514.txt
    // http://social.technet.microsoft.com/wiki/contents/articles/5312.active-directory-characters-to-escape.aspx
    function ldap_dn_encode($str) {
        $decoded=array("\\",",","+","<",">",";","\"","=");
        
        $encoded=array();
        foreach ($decoded as $id => $char) {
            $encoded[$id]="\\".$char;
        }
        
        $str = str_replace($decoded,$encoded,$str);
        
        // Leading and trailing space
        if (" " == (substr($str,0,1))) {
            $str = "\\ ".$str;
        }
        if (" " == (substr($str,-1))) {
            $str = substr($str,0,-1)."\\ ";
        }
        // Leading # (RFC says leading one only)
        if ("#" == (substr($str,0,1))) {
            $str = "\\#".$str;
        }
        return ($str);
    }

    
    // Added by SysCo/al 2015-08-13
    // This method will decode the string using RFC4514
    // https://www.ietf.org/rfc/rfc4514.txt
    // http://social.technet.microsoft.com/wiki/contents/articles/5312.active-directory-characters-to-escape.aspx
    function ldap_dn_decode($str) {
        $decoded=array(",","#","+","<",">",";","\"","="," ","\\");
        $encoded=array();
        foreach ($decoded as $id => $char) {
            $encoded[$id]="\\".$char;
        }
        $str = str_replace($encoded,$decoded,$str);
        return ($str);
    }


    // Added by SysCo/al 2015-08-13
    // This method will encode the string using RFC4515 (and we added the / as stated by Microsoft)
    // https://www.ietf.org/rfc/rfc4515.txt
    // https://msdn.microsoft.com/en-us/library/aa746475%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396
    function ldap_search_encode($str) {
        $decoded=array("\\","*","(",")",chr(0),"/");
        $encoded=array("\\5c","\\2a","\\28","\\29","\\00","\\2f");
        $str=str_replace($decoded,$encoded,$str);
        return ($str);
    }


    // Return a random controller
    function random_controller(){
        //select a random domain controller
        mt_srand(doubleval(microtime()) * 100000000); // for older php versions
        return ($this->_domain_controllers[array_rand($this->_domain_controllers)]);
    }

    function account_control($options){
        $val=0;

        if (is_array($options)){
            if (in_array("SCRIPT",$options)){ $val=$val+1; }
            if (in_array("ACCOUNTDISABLE",$options)){ $val=$val+2; }
            if (in_array("HOMEDIR_REQUIRED",$options)){ $val=$val+8; }
            if (in_array("LOCKOUT",$options)){ $val=$val+16; }
            if (in_array("PASSWD_NOTREQD",$options)){ $val=$val+32; }
            //PASSWD_CANT_CHANGE Note You cannot assign this permission by directly modifying the UserAccountControl attribute.
            //For information about how to set the permission programmatically, see the "Property flag descriptions" section.
            if (in_array("ENCRYPTED_TEXT_PWD_ALLOWED",$options)){ $val=$val+128; }
            if (in_array("TEMP_DUPLICATE_ACCOUNT",$options)){ $val=$val+256; }
            if (in_array("NORMAL_ACCOUNT",$options)){ $val=$val+512; }
            if (in_array("INTERDOMAIN_TRUST_ACCOUNT",$options)){ $val=$val+2048; }
            if (in_array("WORKSTATION_TRUST_ACCOUNT",$options)){ $val=$val+4096; }
            if (in_array("SERVER_TRUST_ACCOUNT",$options)){ $val=$val+8192; }
            if (in_array("DONT_EXPIRE_PASSWORD",$options)){ $val=$val+65536; }
            if (in_array("MNS_LOGON_ACCOUNT",$options)){ $val=$val+131072; }
            if (in_array("SMARTCARD_REQUIRED",$options)){ $val=$val+262144; }
            if (in_array("TRUSTED_FOR_DELEGATION",$options)){ $val=$val+524288; }
            if (in_array("NOT_DELEGATED",$options)){ $val=$val+1048576; }
            if (in_array("USE_DES_KEY_ONLY",$options)){ $val=$val+2097152; }
            if (in_array("DONT_REQ_PREAUTH",$options)){ $val=$val+4194304; } 
            if (in_array("PASSWORD_EXPIRED",$options)){ $val=$val+8388608; }
            if (in_array("TRUSTED_TO_AUTH_FOR_DELEGATION",$options)){ $val=$val+16777216; }
        }
        return ($val);
    }

    // Take an ldap query and return the nice names, without all the LDAP prefixes (eg. CN, DN)
    function nice_names($groups){

        $group_array=array();
        for ($i=0; $i<$groups["count"]; $i++) { //for each group
            if (isset($groups[$i])) { // Patched by SysCo/al
                $line=trim($groups[$i]);
                
                if (strlen($line)>0){ 
                    //more presumptions, they're all prefixed with CN= (but no more yet, patched by SysCo/al
                    //so we ditch the first three characters and the group
                    //name goes up to the first comma
                    $bits=explode(",",$line);
                    if (1== count($bits)) {
                        $group_array[]=$bits[0];  // Patched by SysCo/al
                    } else {
                        $prefix_len=strpos($bits[0], "=");  // Patched by SysCo/al to allow also various length (not only 3)
                        if (FALSE === $prefix_len) {
                            $group_array[]=$bits[0];
                        } else {
                            $group_array[]=substr($bits[0],$prefix_len+1);  // Patched by SysCo/al
                        }
                    }
                }
            }
        }
        return ($group_array);	
    }
}

?>