<?php

/**
 * IML LDAP CONNECTOR FOR USER AUTHENTICATION
 *
 * @author axel.hahn@iml.unibe.ch
 */
class imlldap {

    private $_aLdap = array(
        'server'       => false,
        'port'         => false,
        'DnLdapUser' => false, // ldap rdn oder dn
        'PwLdapUser' => false,
        'DnUserNode'   => false,   // ou=People...
        'DnAppNode'    => false,   // cn=AppGroup...
        'protoVersion' => 3,
        'debugLevel'   => 0,
    );
    private $_ldapConn = false;
    private $_ldapBind = false;
    var $bDebug = false;

    /**
     * constructor
     * @param array  $aConfig  optional set ldap connection
     */
    public function __construct($aConfig = array()) {
        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(){
        $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(){
        $this->bDebug=false;
        ldap_set_option(NULL, LDAP_OPT_DEBUG_LEVEL, 0);
    }
    

    private function _w($sText) {
        if (!$this->bDebug) {
            return false;
        }
        echo __CLASS__ . ' DEBUG: ' . $sText . "<br>\n";
        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($aConfig = array()) {
        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() {
        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() {

        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'], $this->_aLdap['port']);
        if (!$this->_ldapConn) {
            die(__CLASS__ . " ERROR: ldap connect failed.");
        }
        $this->_w(__FUNCTION__ . ' OK, connected.');
        
        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   username
     * @param string  $sPw     password
     */
    public function bind($sUser, $sPw='') {
        
        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 '.$sPw);
        
        $this->_ldapBind = @ldap_bind($this->_ldapConn, $sUser, $sPw);
        if (!$this->_ldapBind) {
            $this->_w(__FUNCTION__ . ' failed with er error ' . ldap_error($this->_ldapConn));
            return false;
        }
        $this->_w(__FUNCTION__ . ' OK, successful.');
        return true;
    }

    /**
     * ldap unbind ... if a bind exists
     */
    public function unbind() {
        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($sDn) {
        $aData=$this->searchDn($sDn, '(&(objectclass=top))', $aAttributesToGet=array("*"));
        return is_array($aData);
    }

    /**
     * search in ldap directory and get result as array
     * 
     * @param string  $sSearchFilter     filter in ldap filter syntax
     * @param array   $aAttributesToGet  flat array of attributes to fetch
     * @return array
     */
    public function searchDn($sDn, $sSearchFilter, $aAttributesToGet=array("*")) {
        if (!$this->_ldapBind) {
            $this->bind($this->_aLdap['DnLdapUser'], $this->_aLdap['PwLdapUser']);
        }
    
        $this->_w(__FUNCTION__ . ' DN = '.$sDn . ' filter = '.$sSearchFilter);
        
        $oLdapSearch = ldap_search(
                $this->_ldapConn, 
                $sDn, 
                $sSearchFilter, 
                $aAttributesToGet
        );

        $aItems = $oLdapSearch?ldap_get_entries($this->_ldapConn, $oLdapSearch):false;
        return $aItems;
    }
    /**
     * search in ldap directory and get result as array
     * 
     * @param string  $sSearchFilter     filter in ldap filter syntax
     * @param array   $aAttributesToGet  flat array of attributes to fetch
     * @return array
     */
    public function searchUser($sSearchFilter, $aAttributesToGet=array("*")) {
        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 type $sUser             user id or email to search
     * @param type $aAttributesToGet  i.e. array("ou", "sn", "vorname", "mail", "uid", "memberOf")
     * @return boolean
     */
    public function getUserInfo($sUser, $aAttributesToGet=array("*")) {
        if (!$this->_ldapBind) {
            $this->bind($this->_aLdap['DnLdapUser'], $this->_aLdap['PwLdapUser']);
        }

        // generate search filter
        $sSearchFilter = (strpos($sUser, '@'))?"(mail=$sUser)" : "(uid=$sUser)";
        if($this->_aLdap['DnAppNode']){
            $sSearchFilter.='(memberof='.$this->_aLdap['DnAppNode'] .')';
        }
        $sSearchFilter='(&'.$sSearchFilter.')';
        
        $aItems = $this->searchUser($sSearchFilter, $aAttributesToGet);

        if(count($aItems)==2){
            $this->_w(__FUNCTION__ . ' OK: I got a single result: ' . print_r($aItems[0],1) );
            return $aItems[0];
        }
        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 type $sUser
     * @return string
     */
    public function getUserDn($sUser) {
        $this->_w(__FUNCTION__ . '('.$sUser.')');
        $aItem=$this->getUserInfo($sUser, array("dn"));
        if(is_array($aItem) && array_key_exists('dn', $aItem)){
            $this->_w(__FUNCTION__ . ' OK: dn was found ' . $aItem['dn']);
            return $aItem['dn'];
        }
        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($sUser, $sPW){
        if (!$this->_ldapBind) {
            $this->bind($this->_aLdap['DnLdapUser'], $this->_aLdap['PwLdapUser']);
        }
        $sDn=$this->getUserDn($sUser);
        if ($sDn){
            return ldap_mod_replace ($this->_ldapConn, $sDn, array('userpassword' => "{MD5}".base64_encode(pack("H*",md5($sPW)))));
        }
        $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
     * 
     * @param string  $sDn     dn to update
     * @param string  $aItem   array of new ldap properties
     * @return boolean
     */
    public function objAdd($sDn, $aItem){
        $this->_w(__FUNCTION__ . '("'.$sDn.'", [array])');
        if (!$this->_ldapBind) {
            $this->bind($this->_aLdap['DnLdapUser'], $this->_aLdap['PwLdapUser']);
        }
        if (!ldap_add($this->_ldapConn, $sDn, $aItem)){
            $this->_w(__FUNCTION__ . ' failed with er error ' . ldap_error($this->_ldapConn));
            return false;
        }
        return true;
    }
    
    /**
     * update an ldap attribute
     * this requires a ldap bind with master/ admin account
     * 
     * @param string  $sDn     dn to update
     * @param string  $aItem   array of new ldap properties
     * @return boolean
     */
    public function objAddAttr($sDn, $aItem){
        $this->_w(__FUNCTION__ . '("'.$sDn.'", [array])');
        if (!$this->_ldapBind) {
            $this->bind($this->_aLdap['DnLdapUser'], $this->_aLdap['PwLdapUser']);
        }
        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__ . ' ERROR: ' . ldap_error($this->_ldapConn));
                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;
    }
    
    /**
     * update an ldap object
     * this requires a ldap bind with master/ admin account
     * 
     * @param string  $sDn    full DN where to update the item
     * @param array   $aItem  updated entry
     * @return boolean
     */
    public function objUpdate($sDn, $aItem){
        $this->_w(__FUNCTION__ . '("'.$sDn.'", [array])');
        if (!$this->_ldapBind) {
            $this->bind($this->_aLdap['DnLdapUser'], $this->_aLdap['PwLdapUser']);
        }
        if ($sDn && is_array($aItem)){
            return ldap_mod_replace($this->_ldapConn, $sDn, $aItem);
        } 
        $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
     * 
     * @param string  $sDn    full DN to remove 
     * @return boolean
     */
    public function objDelete($sDn){
        $this->_w(__FUNCTION__ . '("'.$sDn.'")');
        if (!$this->_ldapBind) {
            $this->bind($this->_aLdap['DnLdapUser'], $this->_aLdap['PwLdapUser']);
        }
        
        if ($sDn){
            if (!ldap_delete($this->_ldapConn, $sDn)){
                $this->_w(__FUNCTION__ . ' ERROR: ' . ldap_error($this->_ldapConn));
                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
     * 
     * TODO: Test me
     * 
     * @param string  $sDn    DN
     * @param string  $aItem  item to remove
     * @return boolean
     */
    public function objDeleteAttr($sDn, $aItem){
        $this->_w(__FUNCTION__ . '("'.$sDn.'", [array])');
        if (!$this->_ldapBind) {
            $this->bind($this->_aLdap['DnLdapUser'], $this->_aLdap['PwLdapUser']);
        }
        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->_w(__FUNCTION__ . ' ERROR: ' . ldap_error($this->_ldapConn));
                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;
    }
    
    /**
     * 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($aItem, $sDn=false){
        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($sUserDn){
        $this->_w(__FUNCTION__ . '('.$sUserDn.')');
        return $this->objDelete($sUserDn);
    }
    
    /**
     * update an ldap object
     * 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 userUpdate($aItem){
        $this->_w(__FUNCTION__ . '([array])');
        $sDn=$this->getUserDn($aItem['uid']);
        if ($sDn){
            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($sUser, $sPW){
        $sDn=$this->getUserDn($sUser);
        if ($sDn){
            return $this->bind($sDn, $sPW);
        }
        $this->_w(__FUNCTION__ . ' dn not found (user does not exist in ldap) ' . $sUser);
        return false;
    }
    
}