Skip to content
Snippets Groups Projects
Select Git revision
  • 6851e8b37daaaa37a10acc10ad01b5fbc5049c8e
  • master default protected
  • simple-task/7248-eol-check-add-node-22
  • 6877_check_iml_deployment
4 results

check_clientbackup

Blame
  • ldap.class.php 30.12 KiB
    <?php
    
    /**
     * 
     * IML LDAP CONNECTOR
     *
     * 2022-02-22  ah  added objGet(), sanitizeFilter()
     * 2022-08-18  ah  mask password (showing 4 chars only)
     * 2022-08-22  ah  mhash is deprecated
     * 2022-08-26  ah  fix verifyPassword
     * 2024-07-11  ah  php8 only: use variable types; update phpdocs
     * 
     * @author axel.hahn@unibe.ch
     */
    class imlldap
    {
    
        // ----------------------------------------------------------------------
        // vars
        // ----------------------------------------------------------------------
    
        /**
         * @var array  options array for an ldap connection including some base settings and DNs
         */
        private array $_aLdap = [
            'server' => false,
            'port' => false,
            'DnLdapUser' => false, // ldap rdn oder dn
            'PwLdapUser' => false,
            'DnUserNode' => false, // ou=People...
            'DnAppNode' => false, // cn=AppGroup...
            'protoVersion' => 3,
            'debugLevel' => 0,
        ];
        /**
         * @var object  current ldap connection  
         */
        private object|bool $_ldapConn = false;
    
        /**
         * ldap bind object - bind was done?
         * @var object|bool
         */
        private object|bool $_ldapBind = false;
    
        /**
         * Flag if debug mode is on
         * @var bool
         */
        var bool $bDebug = false;
    
        // ----------------------------------------------------------------------
        // functions
        // ----------------------------------------------------------------------
    
        /**
         * constructor
         * @param array  $aConfig  optional set ldap connection
         */
        public function __construct(array $aConfig = [])
        {
            if (!function_exists("ldap_connect")) {
                die(__CLASS__ . " ERROR: php-ldap module is not installed on this server.");
            }
            if (count($aConfig)) {
                $this->setConfig($aConfig);
            }
        }
    
        public function __destruct()
        {
            $this->close();
        }
    
        // ----------------------------------------------------------------------
        // write debug text
        // ----------------------------------------------------------------------
    
        /**
         * turn debug messages on;
         * if this detail level is not enough, set a value with key debugLevel in 
         * ldap config array
         * @see setConfig()
         */
        public function debugOn(): void
        {
            $this->bDebug = true;
            if ($this->_aLdap['debugLevel']) {
                $this->_w(__FUNCTION__ . ' setting debug level ' . $this->_aLdap['debugLevel']);
                ldap_set_option(NULL, LDAP_OPT_DEBUG_LEVEL, $this->_aLdap['debugLevel']);
            }
        }
    
        /**
         * turn debug messages off
         */
        public function debugOff(): void
        {
            $this->bDebug = false;
            ldap_set_option(NULL, LDAP_OPT_DEBUG_LEVEL, 0);
        }
    
        /**
         * write debug message if denugOn() was fired.
         * 
         * @param string  $sText  message text
         * @return boolean
         */
        private function _w(string $sText): bool
        {
            if (!$this->bDebug) {
                return false;
            }
            echo __CLASS__ . ' DEBUG: ' . $sText . "<br>\n";
            return true;
        }
    
        /**
         * write last ldap error as debug 
         * 
         * @param string  $sText  message text
         * @return boolean
         */
        private function _wLdaperror(string $sText = ''): bool
        {
            $this->_w(($sText ? $sText . ' - ' : '') . 'last LDAP-ERROR: ' . ldap_error($this->_ldapConn));
            return true;
        }
    
        // ----------------------------------------------------------------------
        // setup
        // ----------------------------------------------------------------------
    
        /**
         * set a ldap config 
         * 
         * @param array  $aConfig   new config items
         *             'server'       => 'ldaps://ldap.example.com',
         *             'port'         => 636,
         *             'DnLdapUser' => 'cn=Lookup,ou=ServiceAccounts,dc=org,dc=example.com',     // ldap rdn oder dn
         *             'PwLdapUser' => 'IkHEFFzlZ...99j0h8WdI0LrLhxU',  // password
         *             'DnUserNode'   => 'ou=People,ou=ORG,dc=org,dc=example.com',
         *             'DnAppNode'    => '' optional dn ... if a user must be member of a given group
         *             'protoVersion' => 3
         *             'debugLevel'   => 0 // for debugging set higher 0 AND call debugOn()
         */
        public function setConfig(array $aConfig = []): void
        {
            if (is_array($aConfig)) {
                foreach (array_keys($this->_aLdap) as $sKey) {
                    if (array_key_exists($sKey, $aConfig)) {
                        $this->_w(__FUNCTION__ . ' setting ldap ' . $sKey . ' = ' . $aConfig[$sKey]);
                        $this->_aLdap[$sKey] = $aConfig[$sKey];
                    }
                }
            }
        }
    
        // ----------------------------------------------------------------------
        // ldap lowlevel functions
        // ----------------------------------------------------------------------
    
        /**
         * close an existing ldap connection
         */
        public function close(): void
        {
            if ($this->_ldapConn) {
                $this->_w(__FUNCTION__ . ' closing connection.');
                ldap_close($this->_ldapConn);
            } else {
                $this->_w(__FUNCTION__ . ' SKIP close.');
            }
    
            $this->_ldapConn = false;
        }
    
        /**
         * connect to ldap
         */
        public function connect(): void
        {
    
            if (!array_key_exists('server', $this->_aLdap) || !$this->_aLdap['server']) {
                die(__CLASS__ . " ERROR: no ldap server was setup set. Use setConfig() first.");
            }
    
            if ($this->_ldapConn) {
                $this->close();
            }
    
            $this->_w(__FUNCTION__ . ' connect to ' . $this->_aLdap['server'] . ':' . $this->_aLdap['port']);
            $this->_ldapConn = ldap_connect($this->_aLdap['server']);
            if (!$this->_ldapConn) {
                $this->_wLdaperror(__FUNCTION__);
                die(__CLASS__ . " ERROR: ldap connect failed.");
            }
            $this->_w(__FUNCTION__ . ' OK, connected.');
            ldap_set_option($this->_ldapConn, LDAP_OPT_NETWORK_TIMEOUT, 3);
            ldap_set_option($this->_ldapConn, LDAP_OPT_TIMELIMIT, 3);
    
            if ($this->_aLdap['protoVersion']) {
                $this->_w(__FUNCTION__ . ' setting protocol version .' . $this->_aLdap['protoVersion']);
                ldap_set_option($this->_ldapConn, LDAP_OPT_PROTOCOL_VERSION, $this->_aLdap['protoVersion']);
            }
    
            ldap_set_option($this->_ldapConn, LDAP_OPT_REFERRALS, 0); // for AD MS Windows 
        }
    
        /**
         * ldap bind connects with a ldap user. 
         * If the ldap connection was not opened yet the connection will be established.
         * If a binding exists it will be unbind
         * 
         * @see connect()
         * @see unbind()
         * 
         * @param string  $sUser   optional: username (overrides _aLdap['DnLdapUser'])
         * @param string  $sPw     optional: password (overrides _aLdap['PwLdapUser'])
         */
        public function bind(string $sUser = '', string $sPw = ''): bool
        {
            if (!$sUser) {
                $sUser = $this->_aLdap['DnLdapUser'];
                $sPw = $this->_aLdap['PwLdapUser'];
            }
    
            if (!$this->_ldapConn) {
                $this->connect();
            }
            if ($this->_ldapBind) {
                $this->unbind();
            }
    
            if (!$sUser) {
                $this->_w(__FUNCTION__ . ' ERROR: no user was set as first param.');
                die("ERROR: no user was given to connect to ldap.");
            }
            $this->_w(__FUNCTION__ . ' with user ' . $sUser . ' PW ' . substr($sPw, 0, 4) . '**********');
    
            $this->_ldapBind = @ldap_bind($this->_ldapConn, $sUser, $sPw);
            if (!$this->_ldapBind) {
                $this->_wLdaperror(__FUNCTION__);
                return false;
            }
            $this->_w(__FUNCTION__ . ' OK, successful.');
            return true;
        }
    
        /**
         * ldap unbind ... if a bind exists
         */
        public function unbind(): void
        {
            if ($this->_ldapBind && !is_bool($this->_ldapBind)) {
                $this->_w(__FUNCTION__ . ' ...');
                ldap_unbind($this->_ldapBind);
            } else {
                $this->_w(__FUNCTION__ . ' SKIP.');
            }
            $this->_ldapBind = false;
        }
    
        // ----------------------------------------------------------------------
        // ldap highlevel functions
        // ----------------------------------------------------------------------
    
        /**
         * check if a DN already exists; return is true/ false
         * @param string  $sDn  DN to check
         * @return boolean
         */
        public function DnExists(string $sDn): bool
        {
            $aData = $this->searchDn($sDn, '(&(objectclass=top))', ["*"]);
            return is_array($aData);
        }
    
        /**
         * get simpler array from ldap_get_entries after ldap_search
         * If the given array doesn't contain the key "dn" it returns "false"
         * 
         * @param array  $aRecord  single result item
         * @return array
         */
        public function normalizeSearchentry(array $aRecord): bool|array
        {
            if (!is_array($aRecord) || !isset($aRecord['dn'])) {
                return false;
            }
            $aItem = [];
            unset($aRecord['count']);
            foreach ($aRecord as $sAttr => $aData) {
                if (!is_integer($sAttr)) {
                    $value = $aData;
                    if (is_array($aData)) {
                        unset($aData['count']);
                        $bUseArray = count($aData) > 1 || array_search($sAttr, ['hieradata', 'member', 'memberof', 'objectclass']) !== false;
                        if ($bUseArray) {
                            sort($aData);
                        }
                        $value = $bUseArray ? $aData : $aData[0];
                    }
                    $aItem[$sAttr] = $value;
                }
            }
            return $aItem;
        }
    
        /**
         * sanitize value to put into a search filter
         * WARNING: the implementation is incomplete! I replaces the first N ascii chars only
         * 
         * source: https://www.rfc-editor.org/rfc/rfc4515.txt
         * 
         * @example:
         * $sCn = 'John Smith (john)';
         * $sSearchFilter = '(cn='.$oLdap->sanitizeFilter($sCn).')';
         * 
         * @param  string   $s  value to sanitize
         * @return string
         */
        static public function sanitizeFilter(string $s): string
        {
    
            // helper array to replace special chars
            $aReplace = [];
            for ($i = 0; $i < 65; $i++) {
                $val = dechex($i);
                if ($val < 10) {
                    $val = "0$val";
                }
                $aReplace[chr($i)] = '\\' . $val;
            }
    
            $sReturn = $s;
            $sReturn = str_replace(array_keys($aReplace), array_values($aReplace), $sReturn);
    
            return $sReturn;
        }
        /**
         * search in ldap directory and get result as array.
         * It returns "false" on error:
         * - no ldap connection
         * - search failed
         * 
         * @param string  $sDn               DN to search for
         * @param string  $sSearchFilter     filter in ldap filter syntax
         * @param array   $aAttributesToGet  flat array of attributes to fetch
         * @param boolean $bRecursive        recusrive (uses ldap_search) or not (ldap_list)
         * @return boolean|array
         */
        public function searchDn(string $sDn, string $sSearchFilter = '(objectclass=*)', array $aAttributesToGet = ["*"], bool $bRecursive = true): bool|array
        {
            if (!$this->_ldapBind) {
                if (!$this->bind($this->_aLdap['DnLdapUser'], $this->_aLdap['PwLdapUser'])) {
                    return false;
                }
            }
            $this->_w(__FUNCTION__ . ' DN = ' . $sDn . ' filter = ' . $sSearchFilter . ' attributes = ' . print_r($aAttributesToGet, 1) . ' recursive = ' . ($bRecursive ? 'yes' : 'no'));
    
            $oLdapSearch = $bRecursive
                ? ldap_search($this->_ldapConn, $sDn, $sSearchFilter, $aAttributesToGet)
                : ldap_list($this->_ldapConn, $sDn, $sSearchFilter, $aAttributesToGet)
            ;
    
            if (!$oLdapSearch) {
                $this->_w(__FUNCTION__ . " !!!ERROR!!! filter $sSearchFilter failed ");
                return false;
            }
            $aItems = ldap_get_entries($this->_ldapConn, $oLdapSearch);
            $this->_w(__FUNCTION__ . " count of returned items: " . count($aItems));
            // $this->_w(__FUNCTION__ . " <pre>".print_r($aItems,1).'</pre>');
            return $aItems;
        }
    
        /**
         * search for entries in in ldap user node and get result as array
         * 
         * @param string  $sSearchFilter     filter in ldap filter syntax
         * @param array   $aAttributesToGet  flat array of attributes to fetch
         * @param bool    $bRecursive        flag: recursive search? default: true (=yes, recursive)
         * 
         * @return boolean|array
         */
        public function searchUser(string $sSearchFilter = '', array $aAttributesToGet = ["*"], bool $bRecursive = true): bool|array
        {
            return $this->searchDn($this->_aLdap['DnUserNode'], $sSearchFilter, $aAttributesToGet, $bRecursive);
            /*
            if (!$this->_ldapBind) {
                $this->bind($this->_aLdap['DnLdapUser'], $this->_aLdap['PwLdapUser']);
            }
    
            $this->_w(__FUNCTION__ . ' DN = ' . $this->_aLdap['DnUserNode'] . ' filter = ' . $sSearchFilter);
    
            $oLdapSearch = ldap_search(
                    $this->_ldapConn, $this->_aLdap['DnUserNode'], $sSearchFilter, $aAttributesToGet
            );
    
            $aItems = $oLdapSearch ? ldap_get_entries($this->_ldapConn, $oLdapSearch) : false;
            return $aItems;
             */
        }
    
        /**
         * search user by a given username or email address. 
         * It returns false if the user does not exist or is
         * not member of the group 'DnAppNode' (if it was set).
         * 
         * @param string  $sUser             user id (uid) or email (mail) to search
         * @param array   $aAttributesToGet  i.e. ["ou", "sn", "vorname", "mail", "uid", "memberOf"]
         * @return boolean|array
         */
        public function getUserInfo(string $sUser, array $aAttributesToGet = ["*"]): bool|array
        {
            if (!$this->_ldapBind) {
                if (!$this->bind($this->_aLdap['DnLdapUser'], $this->_aLdap['PwLdapUser'])) {
                    return false;
                }
            }
    
            // generate search filter
            $sSearchFilter = (strpos($sUser, '@')) ? "(mail=$sUser)" : "(uid=$sUser)";
            if ($this->_aLdap['DnAppNode']) {
                $sSearchFilter .= '(memberof=' . $this->_aLdap['DnAppNode'] . ')';
            }
            // $sSearchFilter .= '(memberof=*)';
            $sSearchFilter = '(&' . $sSearchFilter . ')';
            $aItems = $this->searchUser($sSearchFilter, $aAttributesToGet);
    
            if (is_array($aItems) && count($aItems) == 2) {
                $this->_w(__FUNCTION__ . ' OK: I got a single result: ' . print_r($aItems[0], 1));
                return $aItems[0];
            } else {
                $this->_w(__FUNCTION__ . ' ERROR: result is: <pre>' . print_r($aItems, 1) . '</pre>');
            }
            return false;
        }
    
        /**
         * search for a DN entry with the lookup user by a given username or
         * email address. It returns false if the user does not exist or is
         * not member of the group 'DnAppNode' (if it was set).
         * 
         * @param string $sUser
         * @return string
         */
        public function getUserDn(string $sUser): bool|string
        {
            $this->_w(__FUNCTION__ . '(' . $sUser . ')');
            $aItem = $this->getUserInfo($sUser, ["dn"]);
            if (is_array($aItem) && array_key_exists('dn', $aItem)) {
                $this->_w(__FUNCTION__ . ' OK: dn was found ' . $aItem['dn']);
                return $aItem['dn'];
            }
            $this->_w(__FUNCTION__ . ' ERROR: dn was NOT found ' . print_r($aItem));
            return false;
        }
    
        /**
         * set a password for a given user; 
         * this requires a ldap bind with master/ admin account
         * 
         * @param string  $sUser  username or email
         * @param string  $sPW    password
         * @return boolean
         */
        public function setPassword(string $sUser, string $sPW): bool
        {
            if (!$this->_ldapBind) {
                if (!$this->bind($this->_aLdap['DnLdapUser'], $this->_aLdap['PwLdapUser'])) {
                    return false;
                }
            }
            $sDn = $this->getUserDn($sUser);
            if ($sDn) {
                if (!ldap_mod_replace($this->_ldapConn, $sDn, ['userpassword' => "{MD5}" . base64_encode(pack("H*", md5($sPW)))])) {
                    $this->_wLdaperror(__FUNCTION__);
                    return false;
                } else {
                    return true;
                }
            }
            $this->_w(__FUNCTION__ . ' dn not found (user does not exist in ldap) ' . $sUser);
            return false;
        }
    
        /**
         * get NTLM hash from a string
         * taken from https://secure.php.net/manual/en/ref.hash.php
         * 
         * @param string   $Input
         * @return string
         */
        private function _getNTLMHash(string $Input): string
        {
            // Convert the password from UTF8 to UTF16 (little endian)
            $Input = iconv('UTF-8', 'UTF-16LE', $Input);
    
            // Encrypt it with the MD4 hash
            $MD4Hash = hash('md4', $Input);
            // Make it uppercase, not necessary, but it's common to do so with NTLM hashes
            $NTLMHash = strtoupper($MD4Hash);
    
            // Return the result
            return ($NTLMHash);
        }
    
        /**
         * set a password for a given user for Samba
         * this requires a ldap bind with master/ admin account
         * see https://msdn.microsoft.com/en-us/library/cc223248.aspx
         * see http://php.net/ldap-modify-batch  - last examle
         * see https://secure.php.net/manual/en/ref.hash.php
         * 
         * @param string  $sUser  username or email
         * @param string  $sPW    password
         * @return boolean
         */
        public function setPasswordSamba(string $sUser, string $sPW): bool
        {
            $sDn = $this->getUserDn($sUser);
            if ($sDn) {
                $sPwField = 'sambaNTPassword';
                $sPwValue = $this->_getNTLMHash($sPW);
                return $this->objUpdate(
                    $sDn,
                    [
                        $sPwField => $sPwValue,
                        'SambaPwdLastSet' => date('U'),
                    ]
                );
            }
            $this->_w(__FUNCTION__ . ' dn not found (user does not exist in ldap) ' . $sUser);
            return false;
        }
    
        /**
         * update an ldap object
         * this requires a ldap bind with master/ admin account
         * It returns true if the action was successful
         * 
         * @param string  $sDn     dn to update
         * @param array   $aItem   array of new ldap properties
         * @return boolean
         */
        public function objAdd(string $sDn, array $aItem): bool
        {
            $this->_w(__FUNCTION__ . '("' . $sDn . '", <pre>[' . print_r($aItem, 1) . ']</pre>)');
            if (!$this->_ldapBind) {
                if (!$this->bind($this->_aLdap['DnLdapUser'], $this->_aLdap['PwLdapUser'])) {
                    return false;
                }
            }
            if (!ldap_add($this->_ldapConn, $sDn, $aItem)) {
                $this->_wLdaperror(__FUNCTION__);
                return false;
            }
            return true;
        }
    
        /**
         * update an ldap attribute
         * this requires a ldap bind with master/ admin account
         * 
         * @param string  $sDn     dn to update
         * @param array   $aItem   array of new ldap properties
         * @return boolean
         */
        public function objAddAttr(string $sDn, array $aItem): bool
        {
            $this->_w(__FUNCTION__ . '("' . $sDn . '", [array])');
            if (!$this->_ldapBind) {
                if (!$this->bind($this->_aLdap['DnLdapUser'], $this->_aLdap['PwLdapUser'])) {
                    return false;
                }
            }
            if ($sDn && is_array($aItem)) {
                $this->_w(__FUNCTION__ . ' ' . ($this->_ldapConn ? 'Verbindung da' : 'kein LDAP Connect'));
                $this->_w(__FUNCTION__ . ' ldap_mod_add($this->_ldapConn, "' . $sDn . '", ' . print_r($aItem, 1) . ')');
                if (!ldap_mod_add($this->_ldapConn, $sDn, $aItem)) {
                    $this->_w(__FUNCTION__ . ' ldap_mod_add FAILED');
                    $this->_wLdaperror(__FUNCTION__);
                    return false;
                }
                return true;
            }
            $this->_w(__FUNCTION__ . ' dn not found (item does not exist in ldap) or item was not ann array ' . print_r($aItem, 1));
            return false;
        }
    
        /**
         * read attributes from ldap node with given DN (using ldap_read)
         * It returns "false" if the action was not successful
         * - no ldap connection
         * - DN or filter didn't match
         * 
         * @param string  $sDn               DN to search for
         * @param string  $sSearchFilter     filter in ldap filter syntax
         * @param array   $aAttributesToGet  flat array of attributes to fetch
         * @return boolean|array
         */
        public function objGet(string $sDn, string $sSearchFilter = '(objectclass=*)', array $aAttributesToGet = ["*"]): bool|array
        {
    
            $this->_w(__FUNCTION__ . '("' . $sDn . '", filter = ' . $sSearchFilter . ', atttr= ' . print_r($aAttributesToGet, 1) . ' )');
            if (!$this->_ldapBind) {
                if (!$this->bind($this->_aLdap['DnLdapUser'], $this->_aLdap['PwLdapUser'])) {
                    return false;
                }
            }
    
            $oLdapResult = ldap_read($this->_ldapConn, $sDn, $sSearchFilter, $aAttributesToGet);
    
            if (!$oLdapResult) {
                $this->_w(__FUNCTION__ . " !!!ERROR!!! DN or filter did not match.");
                return false;
            }
            return ldap_get_entries($this->_ldapConn, $oLdapResult);
        }
    
        /**
         * update an ldap object with given key-value array
         * if the attribute (key) does not exist it will be created.
         * this requires a ldap bind with master/ admin account
         * It returns "false" if the action failed
         * 
         * @param string  $sDn    full DN where to update the item
         * @param array   $aItem  updated entry
         * @return boolean
         */
        public function objUpdate(string $sDn, array $aItem): bool
        {
            $this->_w(__FUNCTION__ . '("' . $sDn . '", ' . print_r($aItem, 1) . ')');
            if (!$this->_ldapBind) {
                if (!$this->bind($this->_aLdap['DnLdapUser'], $this->_aLdap['PwLdapUser'])) {
                    return false;
                }
            }
            if ($sDn && is_array($aItem)) {
                if (!ldap_mod_replace($this->_ldapConn, $sDn, $aItem)) {
                    $this->_w(__FUNCTION__ . ' ldap_mod_replace FAILED');
                    $this->_wLdaperror(__FUNCTION__);
                    return false;
                }
                return true;
            }
            $this->_w(__FUNCTION__ . ' dn not found (item does not exist in ldap) ' . print_r($aItem, 1));
            return false;
        }
    
        /**
         * delete an ldap object
         * this requires a ldap bind with master/ admin account
         * It returns "false" if the action failed
         * 
         * @param string  $sDn    full DN to remove 
         * @return boolean
         */
        public function objDelete(string $sDn): bool
        {
            $this->_w(__FUNCTION__ . '("' . $sDn . '")');
            if (!$this->_ldapBind) {
                if (!$this->bind($this->_aLdap['DnLdapUser'], $this->_aLdap['PwLdapUser'])) {
                    return false;
                }
            }
    
            if ($sDn) {
                if (!ldap_delete($this->_ldapConn, $sDn)) {
                    $this->_wLdaperror(__FUNCTION__);
                    return false;
                }
                return true;
            }
            $this->_w(__FUNCTION__ . ' missing parameter for DN');
            return false;
        }
    
        /**
         * delete attributes of an ldap object
         * this requires a ldap bind with master/ admin account
         * It returns "false" if the action failed
         * 
         * @example:
         * remove attribute "userPassword" of user $sUserDn:
         * <code>$oLdap->objDeleteAttr($sUserDn, ['userPassword'=>[]]</code>
         * 
         * @param string  $sDn    DN
         * @param array   $aItem  item to remove
         * @return boolean
         */
        public function objDeleteAttr(string $sDn, array $aItem): bool
        {
            $this->_w(__FUNCTION__ . '("' . $sDn . '", [array])');
            if (!$this->_ldapBind) {
                if (!$this->bind($this->_aLdap['DnLdapUser'], $this->_aLdap['PwLdapUser'])) {
                    return false;
                }
            }
            if ($sDn && is_array($aItem)) {
                $this->_w(__FUNCTION__ . ' ' . ($this->_ldapConn ? 'Verbindung da' : 'kein LDAP Connect'));
                $this->_w(__FUNCTION__ . ' ldap_mod_del($this->_ldapConn, "' . $sDn . '", ' . print_r($aItem, 1) . ')');
                if (!ldap_mod_del($this->_ldapConn, $sDn, $aItem)) {
                    $this->_wLdaperror(__FUNCTION__);
                    return false;
                }
                return true;
            }
            $this->_w(__FUNCTION__ . ' dn not found (item does not exist in ldap) or item was not an array ' . print_r($aItem, 1));
            return false;
        }
    
        /**
         * check if an attribute exists in a DN
         * 
         * @param string  $sDn         DN
         * @param string  $sAttribute  attribute name to check
         * @param string  $sAttrValue  value to check
         * @return boolean
         */
        public function objectAttributeExists(string $sDn, string $sAttribute): bool
        {
            $this->_w(__FUNCTION__ . '("' . $sDn . '", "' . $sAttribute . '")');
    
            if (!$this->_ldapBind) {
                if (!$this->bind($this->_aLdap['DnLdapUser'], $this->_aLdap['PwLdapUser'])) {
                    return false;
                }
            }
            $aData = $this->searchDn($sDn, '(&(objectclass=top))', [$sAttribute]);
            $return = (is_array($aData) && isset($aData[0][strtolower($sAttribute)]));
            $this->_w(__FUNCTION__ . '(...) returns ' . ($return ? 'true' : 'false'));
            return $return;
        }
    
        /**
         * check if an attribute and value exist in a DN
         * 
         * @param string  $sDn         DN
         * @param string  $sAttribute  attribute name to check
         * @param string  $sAttrValue  value to check
         * @return boolean
         */
        public function objectAttributeAndValueExist(string $sDn, string $sAttribute, string $sAttrValue): bool
        {
            $this->_w(__FUNCTION__ . '("' . $sDn . '", "' . $sAttribute . '", "' . $sAttrValue . '")');
    
            if (!$this->_ldapBind) {
                if (!$this->bind($this->_aLdap['DnLdapUser'], $this->_aLdap['PwLdapUser'])) {
                    return false;
                }
            }
            $aData = $this->searchDn($sDn, '(&(objectclass=top))', [$sAttribute]);
            $return = (is_array($aData) && isset($aData[0][strtolower($sAttribute)]) && array_search($sAttrValue, $aData[0][strtolower($sAttribute)]) !== false);
            $this->_w(__FUNCTION__ . '(...) returns ' . ($return ? 'true' : 'false'));
            return $return;
        }
    
        /**
         * check an attribute and value; it will be created if it does not exist
         * this requires a ldap bind with master/ admin account
         * 
         * @param string  $sDn         dn to update
         * @param string  $sAttribute   attribute name to check
         * @param string  $sAttrValue  value to check
         * @return boolean
         */
        public function objectAttributeAndValueMustExist(string $sDn, string $sAttribute, string $sAttrValue): bool
        {
            $this->_w(__FUNCTION__ . '("' . $sDn . '", "' . $sAttribute . '", "' . $sAttrValue . '")');
            // return if it already exists
            if ($this->objectAttributeAndValueExist($sDn, $sAttribute, $sAttrValue)) {
                return true;
            }
    
            // create it
            $this->_w(__FUNCTION__ . " create $sAttribute = $sAttrValue");
            $return = $this->objAddAttr($sDn, [$sAttribute => $sAttrValue]);
            return $return;
        }
    
        /**
         * create a new user item
         * this requires a ldap bind with master/ admin account
         * 
         * @param array   $aItem  ldap properties
         * @param string  $sDn    optional DN where to create the user
         * @return boolean
         */
        public function userAdd(array $aItem, string $sDn = ""): bool
        {
            if (!$sDn) {
                $sDn = 'cn=' . $aItem['cn'] . ',' . $this->_aLdap['DnUserNode'];
            }
            $this->_w(__FUNCTION__ . '([array], "' . $sDn . '")');
            if ($sDn) {
                return $this->objAdd($sDn, $aItem);
            }
            $this->_w(__FUNCTION__ . ' node dn where to put the user was not found; set a value DnUserNode in ldap config or set it as 2nd parameter ' . print_r($aItem, 1));
            return false;
        }
    
        /**
         * delete a user
         * this requires a ldap bind with master/ admin account
         * 
         * @param string  $sUser  user to update
         * @param string  $sPW    new password to set
         * @return boolean
         */
        public function userDelete(string $sUserDn): bool
        {
            $this->_w(__FUNCTION__ . '(' . $sUserDn . ')');
            return $this->objDelete($sUserDn);
        }
    
        /**
         * update an ldap object
         * this requires a ldap bind with master/ admin account
         * 
         * @param array   $aItem  new user data to update
         * @return boolean
         */
        public function userUpdate(array $aItem): bool
        {
            $this->_w(__FUNCTION__ . '([array])');
            $sDn = $this->getUserDn($aItem['uid']);
            if ($sDn) {
                if (array_key_exists('cn', $aItem)) {
                    $this->_w(__FUNCTION__ . ' deleting cn entry.');
                    unset($aItem['cn']);
                }
                return $this->objUpdate($sDn, $aItem);
            }
            $this->_w(__FUNCTION__ . ' dn not found (user does not exist in ldap) ' . $sDn);
            return false;
        }
    
        /**
         * verify user and password
         * @param string  $sUser  username or email
         * @param string  $sPW    password
         * @return boolean
         */
        public function verifyPassword(string $sUser, string $sPW): bool
        {
            $sDn = $this->getUserDn($sUser);
            if ($sDn) {
                return $this->bind($sDn, $sPW);
                /*
                if (!$this->bind($this->_aLdap['DnLdapUser'], $this->_aLdap['PwLdapUser'])){
                    return false;
                }
                */
            }
            $this->_w(__FUNCTION__ . ' dn not found (user does not exist in ldap) ' . $sUser);
            return false;
        }
    
    }