<?php
/**
 * i-doit
 *
 * ldapi wrapper
 *
 * @package    i-doit
 * @subpackage Libraries
 * @author     Dennis Stuecken <dstuecken@i-doit.de>
 * @copyright  synetics GmbH
 * @license    http://www.i-doit.com/license
 */

use idoit\Component\Helper\LdapUrlGenerator;
use idoit\Component\Helper\Unserialize;
use idoit\Component\Logger;

include_once(__DIR__ . "/ldapi/ldapi.class.php");
include_once(__DIR__ . "/ldapi/ldapi_acc.class.php");

class isys_library_ldap extends ldapi_acc
{
    /**
     * @var  boolean
     */
    private $m_connected = false;

    /**
     * @var Logger
     */
    private $logger = null;

    /**
     * @param            $p_hostname
     * @param            $p_dn
     * @param            $p_password
     * @param int        $p_port
     * @param int        $p_protocol_version
     * @param bool|false $p_tls
     * @param int        $p_debug_level
     *
     * @return isys_library_ldap
     */
    public static function factory($p_hostname, $p_dn, $p_password, $p_port = 389, $p_protocol_version = 3, $p_tls = false, $p_debug_level = 0)
    {
        return new self($p_hostname, $p_dn, $p_password, $p_port, $p_protocol_version, $p_tls, $p_debug_level);
    }

    /**
     * @return boolean
     */
    public function connected()
    {
        return $this->m_connected;
    }

    /**
     * @param Logger $logger
     *
     * @return $this
     */
    public function set_logger(Logger $logger)
    {
        $this->logger = $logger;

        return $this;
    }

    /**
     * Single-level search through LDAP-Directory.
     * Check: http://www.php.net/manual/en/function.ldap-list.php
     *
     * @param   string  $p_dn
     * @param   string  $p_filter
     * @param   string  $p_attributes
     * @param   string  $p_attributes_only
     * @param   integer $p_sizelimit
     * @param   integer $p_timelimit
     * @param   integer $p_deref
     *
     * @return  resource
     */
    public function read($p_dn, $p_filter, $p_attributes = null, $p_attributes_only = null, $p_sizelimit = null, $p_timelimit = null, $p_deref = null)
    {
        $this->debug(" ->read: dn=" . $p_dn . " | filter=" . $p_filter);

        parent::read($p_dn, $p_filter, $p_attributes, $p_attributes_only, $p_sizelimit, $p_timelimit, $p_deref);
    }

    /**
     * @param string $p_dn
     * @param string $p_filter
     * @param array  $p_attributes
     * @param int    $p_attributes_only
     * @param null   $p_sizelimit
     * @param null   $p_timelimit
     * @param null   $p_deref
     * @param int    $p_scope
     * @param array  $serverctls
     */
    public function search(
        $p_dn,
        $p_filter,
        $p_attributes = [],
        $p_attributes_only = 0,
        $p_sizelimit = null,
        $p_timelimit = null,
        $p_deref = null,
        $p_scope = C__LDAP_SCOPE__RECURSIVE,
        $serverctls = []
    ) {
        $this->debug(' ->search: dn=' . $p_dn . ' | filter=' . $p_filter);

        $result = parent::search($p_dn, $p_filter, $p_attributes, $p_attributes_only, $p_sizelimit, $p_timelimit, $p_deref, $p_scope, $serverctls);

        // @see  ID-8248  Record a clear message in case the search fails.
        if ($result === false) {
            $this->debug(' The search resulted in an error: "' . ldap_error($this->get_connection()) . '" (error number ' . ldap_errno($this->get_connection()) . ')');
        }

        return $result;
    }

    /**
     * Extract the filter preparation, so we can call it from other places.
     *
     * @param string $username
     * @param array  $ldapConfig
     *
     * @return string
     * @see ID-9079
     */
    public function prepareFilter(string $username, array $ldapConfig): string
    {
        // Unpack the mapping (LDAP -> Directories -> LDAP-Mapping).
        $mapping = Unserialize::toArray($ldapConfig['isys_ldap_directory__mapping']);

        $mapUsername = $mapping[C__LDAP_MAPPING__USERNAME];

        if (empty($mapUsername)) {
            $mapUsername = 'cn';
        }

        if (empty($ldapConfig['isys_ldap__filter']) || !strstr($ldapConfig['isys_ldap__filter'], "=")) {
            return "({$mapUsername}={$username})";
        }

        if ($ldapConfig['isys_ldap__filter'][0] != '(') {
            $currentFilter = "({$ldapConfig['isys_ldap__filter']})";
        } else {
            $currentFilter = $ldapConfig['isys_ldap__filter'];
        }

        return "(&{$currentFilter}({$mapUsername}={$username}))";
    }

    /**
     * Searches a username in specified ldap server (connection_array) and returns its DN.
     *
     * @param   string $p_username
     * @param   array  $p_connection_array
     *
     * @throws  Exception
     * @return  mixed
     */
    public function get_user($p_username, $p_connection_array)
    {
        if ($this->is_connected()) {
            /* Get and set the timelimit */
            $l_timelimit = (!empty($p_connection_array["isys_ldap__timelimit"]) ? intval($p_connection_array["isys_ldap__timelimit"]) : C__DEFAULT__TIMELIMIT);

            $this->set_option(LDAP_OPT_TIMELIMIT, $l_timelimit);

            /**
             * Setting LDAP protocol version to 3 is necessary for retrieving utf8 strings from ldap!
             *
             * @see http://www.php.net/manual/en/ref.ldap.php#108670
             */
            $this->set_option(LDAP_OPT_PROTOCOL_VERSION, 3);

            /* Search recursive or not? */
            if ($p_connection_array["isys_ldap__recursive"] > 0) {
                $l_scope = C__LDAP_SCOPE__RECURSIVE;
            } else {
                $l_scope = C__LDAP_SCOPE__SINGLE;
            }

            // Unpack the mapping (LDAP->Directories->LDAP-Mapping).
            $l_mapping = Unserialize::toArray($p_connection_array["isys_ldap_directory__mapping"]);

            $l_filter = $this->prepareFilter($p_username, $p_connection_array);

            $this->set_search_path($p_connection_array["isys_ldap__user_search"]);

            $this->debug("Getting user(s) using filter: " . $l_filter . " in search-path: " . $this->get_search_path());

            $l_res = $this->search($this->get_search_path(), $l_filter, $l_mapping, 0, 1, $l_timelimit, null, $l_scope);

            if ($l_res && ($l_count = $this->count($l_res)) > 0) {
                $l_entries = $this->get_entries($l_res);
                $l_ar = $this->format_entry($l_entries[0]);

                $l_attributes = [
                    'dn' => $l_entries[0]["dn"]
                ];

                foreach ($l_mapping as $l_const => $l_attr) {
                    if (is_countable($l_ar[strtolower($l_attr)]) && count($l_ar[strtolower($l_attr)]) > 0) {
                        if ($l_const == C__LDAP_MAPPING__GROUP) {
                            $l_value = $l_ar[strtolower($l_attr)];
                        } else {
                            $l_value = $l_ar[strtolower($l_attr)][count($l_ar[strtolower($l_attr)]) - 1];
                        }

                        $l_attributes[$l_const] = $l_value;
                    }
                }

                return count($l_attributes) > 0 ? $l_attributes : false;
            } else {
                $this->debug("** No user found.");
            }
        } else {
            throw new Exception("Not connected to ldap server.");
        }

        return false;
    }

    /**
     * @param  $p_message
     */
    private function debug($p_message)
    {
        if ($this->logger) {
            $this->logger->debug($p_message);
        }

        return $this;
    }

    /**
     * $p_protool_version parameter is deprecated!
     *
     * @param   string  $p_hostname
     * @param   string  $p_dn
     * @param   string  $p_password
     * @param   integer $p_port
     * @param   integer $p_protocol_version
     * @param   boolean $p_tls
     * @param   integer $p_debug_level
     *
     * @throws  Exception
     */
    public function __construct($p_hostname, $p_dn, $p_password, $p_port = 389, $p_protocol_version = 3, $p_tls = LdapUrlGenerator::LDAP_ENCODING_OFF, $p_debug_level = 0)
    {
        parent::__construct($p_hostname);
        $this->set_port($p_port);

        // Set encoding of ldap connection
        $this->setEncoding($p_tls);

        if (($this->m_connected = $this->connect())) {
            $this->set_option(LDAP_OPT_DEBUG_LEVEL, $p_debug_level);

            /**
             * Windows 2003 fix for searching the whole DC
             *
             * @see http://www.php.net/manual/de/function.ldap-search.php#45388
             */
            $this->set_option(LDAP_OPT_REFERRALS, 0);

            /**
             * Setting LDAP protocol version to 3 is necessary for retrieving utf8 strings from ldap!
             *
             * @see http://www.php.net/manual/en/ref.ldap.php#108670
             */
            $this->set_option(LDAP_OPT_PROTOCOL_VERSION, 3);

            // Check for encoding type is equal STARTTLS
            if ($p_tls == LdapUrlGenerator::LDAP_ENCODING_STARTTLS) {
                if (!$this->start_tls()) {
                    throw new Exception("Failed to start TLS (" . $this->get_ldap_error() .
                        "). Check https://php.net/ldap-start-tls for help configuring secure ldap connections. " . "Host: " . $p_hostname . ":" . $p_port . ". User: " . $p_dn .
                        ", TLS: On");
                }
            }

            if (!($this->m_connected = $this->bind($p_dn, $p_password))) {
                $this->debug("Connection failed. (" . $this->get_ldap_error() . ")");
                throw new Exception("LDAP Bind failed (" . $this->get_ldap_error() . "). " . "Host: " . $p_hostname . ":" . $p_port . ". User: " . $p_dn);
            }

            $this->debug("Connected to {$p_hostname}");
        } else {
            throw new Exception("LDAP Connection failed (" . $this->get_ldap_error() . ")");
        }
    }

    /**
     * @param $l_search
     * @param $errcode
     * @param $matcheddn
     * @param $errmsg
     * @param $referrals
     * @param $serverControls
     */
    public function ldapParseResult($l_search, &$errcode, &$matcheddn, &$errmsg, &$referrals, &$serverControls)
    {
        ldap_parse_result($this->get_connection(), $l_search, $errcode, $matcheddn, $errmsg, $referrals, $serverControls); // phpcs:ignore
    }
}
