<?php

/**
 * @package     Joomla.Plugin
 * @subpackage  Authentication.ldap
 *
 * @copyright   (C) 2006 Open Source Matters, Inc. <https://www.joomla.org>
 * @license     GNU General Public License version 2 or later; see LICENSE.txt

 * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace
 */

use Joomla\CMS\Authentication\Authentication;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Plugin\CMSPlugin;
use Symfony\Component\Ldap\Entry;
use Symfony\Component\Ldap\Exception\ConnectionException;
use Symfony\Component\Ldap\Exception\LdapException;
use Symfony\Component\Ldap\Ldap;

// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects

/**
 * LDAP Authentication Plugin
 *
 * @since  1.5
 */
class PlgAuthenticationLdap extends CMSPlugin
{
    /**
     * This method should handle any authentication and report back to the subject
     *
     * @param   array   $credentials  Array holding the user credentials
     * @param   array   $options      Array of extra options
     * @param   object  &$response    Authentication response object
     *
     * @return  boolean
     *
     * @since   1.5
     */
    public function onUserAuthenticate($credentials, $options, &$response)
    {
        // If LDAP not correctly configured then bail early.
        if (!$this->params->get('host')) {
            return false;
        }

        // For JLog
        $response->type = 'LDAP';

        // Strip null bytes from the password
        $credentials['password'] = str_replace(chr(0), '', $credentials['password']);

        // LDAP does not like Blank passwords (tries to Anon Bind which is bad)
        if (empty($credentials['password'])) {
            $response->status = Authentication::STATUS_FAILURE;
            $response->error_message = Text::_('JGLOBAL_AUTH_EMPTY_PASS_NOT_ALLOWED');

            return false;
        }

        // Load plugin params info
        $ldap_email    = $this->params->get('ldap_email');
        $ldap_fullname = $this->params->get('ldap_fullname');
        $ldap_uid      = $this->params->get('ldap_uid');
        $auth_method   = $this->params->get('auth_method');

        $ldap = Ldap::create(
            'ext_ldap',
            [
                'host'       => $this->params->get('host'),
                'port'       => (int) $this->params->get('port'),
                'version'    => $this->params->get('use_ldapV3', '0') == '1' ? 3 : 2,
                'referrals'  => (bool) $this->params->get('no_referrals', '0'),
                'encryption' => $this->params->get('negotiate_tls', '0') == '1' ? 'tls' : 'none',
            ]
        );

        switch ($auth_method) {
            case 'search':
                try {
                    $dn = str_replace('[username]', $this->params->get('username', ''), $this->params->get('users_dn', ''));

                    $ldap->bind($dn, $this->params->get('password', ''));
                } catch (ConnectionException | LdapException $exception) {
                    $response->status = Authentication::STATUS_FAILURE;
                    $response->error_message = Text::_('JGLOBAL_AUTH_NOT_CONNECT');

                    return;
                }

                // Search for users DN
                try {
                    $entry = $this->searchByString(
                        str_replace(
                            '[search]',
                            str_replace(';', '\3b', $ldap->escape($credentials['username'], '', LDAP_ESCAPE_FILTER)),
                            $this->params->get('search_string')
                        ),
                        $ldap
                    );
                } catch (LdapException $exception) {
                    $response->status = Authentication::STATUS_FAILURE;
                    $response->error_message = Text::_('JGLOBAL_AUTH_UNKNOWN_ACCESS_DENIED');

                    return;
                }

                if (!$entry) {
                    $response->status = Authentication::STATUS_FAILURE;
                    $response->error_message = Text::_('JGLOBAL_AUTH_NOT_CONNECT');

                    return;
                }

                try {
                    // Verify Users Credentials
                    $ldap->bind($entry->getDn(), $credentials['password']);
                } catch (ConnectionException $exception) {
                    $response->status = Authentication::STATUS_FAILURE;
                    $response->error_message = Text::_('JGLOBAL_AUTH_INVALID_PASS');

                    return;
                }

                break;

            case 'bind':
                // We just accept the result here
                try {
                    $ldap->bind($ldap->escape($credentials['username'], '', LDAP_ESCAPE_DN), $credentials['password']);
                } catch (ConnectionException | LdapException $exception) {
                    $response->status = Authentication::STATUS_FAILURE;
                    $response->error_message = Text::_('JGLOBAL_AUTH_INVALID_PASS');

                    return;
                }

                try {
                    $entry = $this->searchByString(
                        str_replace(
                            '[search]',
                            str_replace(';', '\3b', $ldap->escape($credentials['username'], '', LDAP_ESCAPE_FILTER)),
                            $this->params->get('search_string')
                        ),
                        $ldap
                    );
                } catch (LdapException $exception) {
                    $response->status = Authentication::STATUS_FAILURE;
                    $response->error_message = Text::_('JGLOBAL_AUTH_UNKNOWN_ACCESS_DENIED');

                    return;
                }

                break;

            default:
                // Unsupported configuration
                $response->status = Authentication::STATUS_FAILURE;
                $response->error_message = Text::_('JGLOBAL_AUTH_UNKNOWN_ACCESS_DENIED');

                return;
        }

        // Grab some details from LDAP and return them
        $response->username = $entry->getAttribute($ldap_uid)[0] ?? false;
        $response->email    = $entry->getAttribute($ldap_email)[0] ?? false;
        $response->fullname = $entry->getAttribute($ldap_fullname)[0] ?? trim($entry->getAttribute($ldap_fullname)[0]) ?: $credentials['username'];

        // Were good - So say so.
        $response->status        = Authentication::STATUS_SUCCESS;
        $response->error_message = '';

        // The connection is no longer needed, destroy the object to close it
        unset($ldap);
    }

    /**
     * Shortcut method to perform a LDAP search based on a semicolon separated string
     *
     * Note that this method requires that semicolons which should be part of the search term to be escaped
     * to correctly split the search string into separate lookups
     *
     * @param   string  $search  search string of search values
     * @param   Ldap    $ldap    The LDAP client
     *
     * @return  Entry|null The search result entry if a matching record was found
     *
     * @since   3.8.2
     */
    private function searchByString($search, Ldap $ldap)
    {
        $dn = $this->params->get('base_dn');

        // We return the first entry from the first search result which contains data
        foreach (explode(';', $search) as $key => $result) {
            $results = $ldap->query($dn, '(' . str_replace('\3b', ';', $result) . ')')->execute();

            if (count($results)) {
                return $results[0];
            }
        }
    }
}
