<?php
/**
 * This code is licensed under AGPLv3 license or Afterlogic Software License
 * if commercial version of the product was purchased.
 * For full statements of the licenses see LICENSE-AFTERLOGIC and LICENSE-AGPL3 files.
 */

namespace Aurora\System\Utils;

/**
 * @license https://www.gnu.org/licenses/agpl-3.0.html AGPL-3.0
 * @license https://afterlogic.com/products/common-licensing Afterlogic Software License
 * @copyright Copyright (c) 2019, Afterlogic Corp.
 */
class Ldap
{
    /**
     * @var \LDAP\Connection
     */
    private $rLink;

    /**
     * @var \LDAP\Result
     */
    private $rSearch;

    /**
     * @var string
     */
    private $sSearchDN;

    /**
     * @var string
     */
    private $sLastRequest;

    public function __construct($sSearchDN = '')
    {
        $this->rLink = null;
        $this->rSearch = null;
        $this->sSearchDN = $sSearchDN;
    }

    public function Escape($sStr, $bForDn = false)
    {
        if ($bForDn) {
            $aMetaChars = array(',', '=', '+', '<', '>', ';', '\\', '"', '#');
        } else {
            $aMetaChars = array('*', '(', ')', '\\', chr(0));
        }

        $aQuotedMetaChars = array();
        foreach ($aMetaChars as $iKey => $sValue) {
            $aQuotedMetaChars[$iKey] = '\\' . str_pad(dechex(ord($sValue)), 2, '0');
        }

        return str_replace($aMetaChars, $aQuotedMetaChars, $sStr);
    }

    /**
     * @param string $sSearchDN
     * @return Ldap
     */
    public function SetSearchDN($sSearchDN)
    {
        $this->sSearchDN = $sSearchDN;
        return $this;
    }

    /**
     * @return string
     */
    public function GetSearchDN()
    {
        return $this->sSearchDN;
    }

    /**
     * @param string $sHost
     * @param int $iPort
     * @param string $sBindDb = ''
     * @param string $sBindPassword = ''
     * @param string $sHostBack = ''
     * @param int $iPortBack = null
     * @return bool
     */
    public function Connect($sHost, $iPort, $sBindDb = '', $sBindPassword = '', $sHostBack = '', $iPortBack = null)
    {
        if (!extension_loaded('ldap')) {
            \Aurora\System\Api::Log('LDAP: Can\'t load LDAP extension.', \Aurora\System\Enums\LogLevel::Error);
            return false;
        }

        if (!(is_resource($this->rLink) || is_object($this->rLink))) {
            \Aurora\System\Api::Log('LDAP: connect to ' . $sHost . ':' . $iPort);

            $rLink = ldap_connect($sHost, $iPort);
            if ($rLink) {
                @ldap_set_option($rLink, LDAP_OPT_PROTOCOL_VERSION, 3);
                @ldap_set_option($rLink, LDAP_OPT_REFERRALS, 0);

                \Aurora\System\Api::Log('LDAP: bind = "' . $sBindDb . '" / "' . $sBindPassword . '"');
                if (0 < strlen($sBindDb) && 0 < strlen($sBindPassword) ?
                    !@ldap_bind($rLink, $sBindDb, $sBindPassword) : !@ldap_bind($rLink)
                ) {
                    $this->rLink = $rLink;

                    $this->validateLdapErrorOnFalse(false);

                    $this->rLink = null;
                    if (0 < strlen($sHostBack)) {
                        return $this->Connect($sHostBack, $iPortBack, $sBindDb, $sBindPassword);
                    }

                    return false;
                } else {
                    //					@register_shutdown_function(array(&$this, 'Disconnect'));
                    $this->rLink = $rLink;
                }
            } else {
                $this->validateLdapErrorOnFalse(false);
                if (0 < strlen($sHostBack)) {
                    return $this->Connect($sHostBack, $iPortBack, $sBindDb, $sBindPassword);
                }

                return false;
            }
        }

        return true;
    }

    /**
     * @param string $sBindDb
     * @param string $sBindPassword
     * @return bool
     */
    public function ReBind($sBindDb, $sBindPassword)
    {
        if (is_resource($this->rLink) || is_object($this->rLink)) {
            \Aurora\System\Api::Log('LDAP: rebind ' . $sBindDb);

            if (!@ldap_bind($this->rLink, $sBindDb, $sBindPassword)) {
                $this->validateLdapErrorOnFalse(false);
                $this->rLink = null;
            } else {
                return true;
            }
        }

        return false;
    }


    /**
     * @param mixed $mReturn
     * @return mixed
     */
    private function validateLdapErrorOnFalse($mReturn)
    {
        if (false === $mReturn) {
            if ($this->rLink) {
                \Aurora\System\Api::Log('LDAP: error #' . @ldap_errno($this->rLink) . ': ' . @ldap_error($this->rLink), \Aurora\System\Enums\LogLevel::Error);
            } else {
                \Aurora\System\Api::Log('LDAP: unknown ldap error', \Aurora\System\Enums\LogLevel::Error);
            }
        }

        return $mReturn;
    }

    /**
     * @param string $sObjectFilter
     * @return bool
     */
    public function Search($sObjectFilter)
    {
        $result = false;
        if ($this->rSearch && $this->sLastRequest === $this->sSearchDN . $sObjectFilter) {
            \Aurora\System\Api::Log('LDAP: search repeat = "' . $this->sSearchDN . '" / ' . $sObjectFilter);

            $this->validateLdapErrorOnFalse($this->rSearch);
            $result = is_resource($this->rSearch) || is_object($this->rSearch);
        } else {
            \Aurora\System\Api::Log('LDAP: search = "' . $this->sSearchDN . '" / ' . $sObjectFilter);
            $this->rSearch = @ldap_search($this->rLink, $this->sSearchDN, $sObjectFilter, array('*'), 0, 3000);

            $this->validateLdapErrorOnFalse($this->rSearch);

            $this->sLastRequest = $this->sSearchDN . $sObjectFilter;
            $result = is_resource($this->rSearch) || is_object($this->rSearch);
        }

        return $result;
    }

    /**
     * @return bool
     */
    public function Add($sNewDn, $aEntry)
    {
        \Aurora\System\Api::Log('ldap_add = ' . ((empty($sNewDn) ? '' : $sNewDn . ',') . $this->sSearchDN));
        \Aurora\System\Api::LogObject($aEntry);

        $bResult = !!@ldap_add($this->rLink, (empty($sNewDn) ? '' : $sNewDn . ',') . $this->sSearchDN, $aEntry);
        $this->validateLdapErrorOnFalse($bResult);

        return $bResult;
    }

    /**
     * @return bool
     */
    public function Delete($sDeleteDn)
    {
        $bResult = false;
        if (!empty($sDeleteDn)) {
            \Aurora\System\Api::Log('ldap_delete = ' . ($sDeleteDn . ',' . $this->sSearchDN));
            $bResult = !!@ldap_delete($this->rLink, $sDeleteDn . ',' . $this->sSearchDN);
            $this->validateLdapErrorOnFalse($bResult);
        }

        return $bResult;
    }

    /**
     * @return bool
     */
    public function Modify($sModifyDn, $aModifyEntry)
    {
        $bResult = false;
        if (!empty($sModifyDn)) {
            if (!empty($this->sSearchDN)) {
                $sModifyDn = $sModifyDn . ',' . $this->sSearchDN;
            }

            \Aurora\System\Api::Log('ldap_modify = ' . $sModifyDn);
            \Aurora\System\Api::LogObject($aModifyEntry);

            $bResult = !!@ldap_modify($this->rLink, $sModifyDn, $aModifyEntry);
            $this->validateLdapErrorOnFalse($bResult);
        }

        return $bResult;
    }

    /**
     * @return int
     */
    public function ResultCount()
    {
        $iResult = 0;

        $iCount = ldap_count_entries($this->rLink, $this->rSearch);
        $this->validateLdapErrorOnFalse($iCount);
        if (false !== $iCount) {
            $iResult = $iCount;
        }

        return $iResult;
    }

    /**
     * @return mixed
     */
    public function ResultItem()
    {
        $mResult = false;

        $aResurn = @ldap_get_entries($this->rLink, $this->rSearch);
        $this->validateLdapErrorOnFalse($aResurn);
        if (false !== $aResurn && isset($aResurn[0]) && is_array($aResurn[0])) {
            $mResult = $aResurn[0];
        }

        return $mResult;
    }

    /**
     * @param string $sSortField
     * @param bool $bAsc 'asc' or 'desc'
     * @param int $iOffset = null
     * @param int $iRequestLimit = null
     * @return array
     */
    public function SortPaginate($sSortField, $bAsc = true, $iOffset = null, $iRequestLimit = null)
    {
        $iTotalEntries = @ldap_count_entries($this->rLink, $this->rSearch);

        $iEnd = 0;
        $iStart = 0;
        if ($iOffset === null || $iRequestLimit === null) {
            $iStart = 0;
            $iEnd = $iTotalEntries - 1;
        } else {
            $iStart = $iOffset;
            $iStart = ($iStart < 0) ? 0 : $iStart;

            $iEnd = $iStart + $iRequestLimit;
            $iEnd = ($iEnd > $iTotalEntries) ? $iTotalEntries : $iEnd;
        }

        if (0 < strlen($sSortField)) {
            if (function_exists('ldap_sort')) {
                @\ldap_sort($this->rLink, $this->rSearch, $sSortField);
            }
        }

        $aList = array();
        $iCurrent = 0;
        $rEntry = ldap_first_entry($this->rLink, $this->rSearch);
        do {
            if ($iCurrent >= $iStart) {
                array_push($aList, ldap_get_attributes($this->rLink, $rEntry));
            }
            $rEntry = ldap_next_entry($this->rLink, $rEntry);
            $iCurrent++;
        } while ($iCurrent < $iEnd && (is_resource($rEntry) || is_object($rEntry)));

        return $bAsc ? $aList : array_reverse($aList);
    }
}
