xref: /plugin/pureldap/classes/Client.php (revision fb75804e73edf4af608854927a231691f3206614)
11078ec26SAndreas Gohr<?php
21078ec26SAndreas Gohr
31078ec26SAndreas Gohrnamespace dokuwiki\plugin\pureldap\classes;
41078ec26SAndreas Gohr
508ace392SAndreas Gohruse dokuwiki\ErrorHandler;
608ace392SAndreas Gohruse dokuwiki\Logger;
708ace392SAndreas Gohruse dokuwiki\Utf8\Clean;
880ac552fSAndreas Gohruse dokuwiki\Utf8\PhpString;
91078ec26SAndreas Gohruse FreeDSx\Ldap\Entry\Attribute;
101078ec26SAndreas Gohruse FreeDSx\Ldap\Exception\BindException;
111078ec26SAndreas Gohruse FreeDSx\Ldap\Exception\ConnectionException;
121078ec26SAndreas Gohruse FreeDSx\Ldap\Exception\OperationException;
131078ec26SAndreas Gohruse FreeDSx\Ldap\LdapClient;
141078ec26SAndreas Gohr
151078ec26SAndreas Gohrabstract class Client
161078ec26SAndreas Gohr{
17208fe81aSAndreas Gohr    public const FILTER_EQUAL = 'equal';
18208fe81aSAndreas Gohr    public const FILTER_CONTAINS = 'contains';
19208fe81aSAndreas Gohr    public const FILTER_STARTSWITH = 'startsWith';
20208fe81aSAndreas Gohr    public const FILTER_ENDSWITH = 'endsWith';
21204fba68SAndreas Gohr
221078ec26SAndreas Gohr    /** @var array the configuration */
231078ec26SAndreas Gohr    protected $config;
241078ec26SAndreas Gohr
251078ec26SAndreas Gohr    /** @var LdapClient */
261078ec26SAndreas Gohr    protected $ldap;
271078ec26SAndreas Gohr
2808ace392SAndreas Gohr    /** @var bool|string is this client authenticated already? Contains username if yes */
291078ec26SAndreas Gohr    protected $isAuthenticated = false;
301078ec26SAndreas Gohr
311078ec26SAndreas Gohr    /** @var array cached user info */
321078ec26SAndreas Gohr    protected $userCache = [];
331078ec26SAndreas Gohr
345a3b9122SAndreas Gohr    /** @var array cached group list */
355a3b9122SAndreas Gohr    protected $groupCache = [];
365a3b9122SAndreas Gohr
371078ec26SAndreas Gohr    /**
381078ec26SAndreas Gohr     * Client constructor.
391078ec26SAndreas Gohr     * @param array $config
401078ec26SAndreas Gohr     */
411078ec26SAndreas Gohr    public function __construct($config)
421078ec26SAndreas Gohr    {
43208fe81aSAndreas Gohr        require_once __DIR__ . '/../vendor/autoload.php';
44208fe81aSAndreas Gohr
451078ec26SAndreas Gohr        $this->config = $this->prepareConfig($config);
46bf69b89cSAndreas Gohr        $this->prepareSSO();
471078ec26SAndreas Gohr        $this->ldap = new LdapClient($this->config);
481078ec26SAndreas Gohr    }
491078ec26SAndreas Gohr
501078ec26SAndreas Gohr    /**
511078ec26SAndreas Gohr     * Setup sane config defaults
521078ec26SAndreas Gohr     *
531078ec26SAndreas Gohr     * @param array $config
541078ec26SAndreas Gohr     * @return array
551078ec26SAndreas Gohr     */
561078ec26SAndreas Gohr    protected function prepareConfig($config)
571078ec26SAndreas Gohr    {
581a4f0e1fSAndreas Gohr        // ensure we have the default keys
591a4f0e1fSAndreas Gohr        /** @var array $conf */
601a4f0e1fSAndreas Gohr        include __DIR__ . '/../conf/default.php';
611a4f0e1fSAndreas Gohr        $defaults = $conf;
621a4f0e1fSAndreas Gohr        $defaults['defaultgroup'] = 'user'; // we expect this to be passed from global conf
631078ec26SAndreas Gohr
641078ec26SAndreas Gohr        $config = array_merge($defaults, $config);
651078ec26SAndreas Gohr
661078ec26SAndreas Gohr        // default port depends on SSL setting
671078ec26SAndreas Gohr        if (!$config['port']) {
686d90d5c8SAndreas Gohr            $config['port'] = ($config['encryption'] === 'ssl') ? 636 : 389;
696d90d5c8SAndreas Gohr        }
706d90d5c8SAndreas Gohr
716d90d5c8SAndreas Gohr        // set ssl parameters
726d90d5c8SAndreas Gohr        $config['use_ssl'] = ($config['encryption'] === 'ssl');
736d90d5c8SAndreas Gohr        if ($config['validate'] === 'none') {
746d90d5c8SAndreas Gohr            $config['ssl_validate_cert'] = false;
756d90d5c8SAndreas Gohr        } elseif ($config['validate'] === 'self') {
766d90d5c8SAndreas Gohr            $config['ssl_allow_self_signed'] = true;
771078ec26SAndreas Gohr        }
781078ec26SAndreas Gohr
79fde03b26SAndreas Gohr        $config['suffix'] = ltrim(PhpString::strtolower($config['suffix']), '@');
80c2500b44SAndreas Gohr        $config['primarygroup'] = $this->cleanGroup($config['primarygroup']);
8180ac552fSAndreas Gohr
821078ec26SAndreas Gohr        return $config;
831078ec26SAndreas Gohr    }
841078ec26SAndreas Gohr
851078ec26SAndreas Gohr    /**
86bf69b89cSAndreas Gohr     * Extract user info from environment for SSO
87bf69b89cSAndreas Gohr     */
88bf69b89cSAndreas Gohr    protected function prepareSSO()
89bf69b89cSAndreas Gohr    {
90bf69b89cSAndreas Gohr        global $INPUT;
91bf69b89cSAndreas Gohr
92bf69b89cSAndreas Gohr        if (!$this->config['sso']) return;
93bf69b89cSAndreas Gohr        if ($INPUT->server->str('REMOTE_USER') === '') return;
94bf69b89cSAndreas Gohr
95bf69b89cSAndreas Gohr        $user = $INPUT->server->str('REMOTE_USER');
96bf69b89cSAndreas Gohr
97bf69b89cSAndreas Gohr        // make sure the right encoding is used
98bf69b89cSAndreas Gohr        if ($this->config['sso_charset']) {
9908ace392SAndreas Gohr            if (function_exists('iconv')) {
100bf69b89cSAndreas Gohr                $user = iconv($this->config['sso_charset'], 'UTF-8', $user);
10108ace392SAndreas Gohr            } elseif (function_exists('mb_convert_encoding')) {
10208ace392SAndreas Gohr                $user = mb_convert_encoding($user, 'UTF-8', $this->config['sso_charset']);
10308ace392SAndreas Gohr            }
10408ace392SAndreas Gohr        } elseif (!Clean::isUtf8($user)) {
105bf69b89cSAndreas Gohr            $user = utf8_encode($user);
106bf69b89cSAndreas Gohr        }
107bf69b89cSAndreas Gohr        $user = $this->cleanUser($user);
108bf69b89cSAndreas Gohr
109bf69b89cSAndreas Gohr        // Prepare SSO
110bf69b89cSAndreas Gohr
111bf69b89cSAndreas Gohr        // trust the incoming user
112bf69b89cSAndreas Gohr        $INPUT->server->set('REMOTE_USER', $user);
113bf69b89cSAndreas Gohr
114bf69b89cSAndreas Gohr        // we need to simulate a login
115bf69b89cSAndreas Gohr        if (empty($_COOKIE[DOKU_COOKIE])) {
116bf69b89cSAndreas Gohr            $INPUT->set('u', $user);
117bf69b89cSAndreas Gohr            $INPUT->set('p', 'sso_only');
118bf69b89cSAndreas Gohr        }
119bf69b89cSAndreas Gohr    }
120bf69b89cSAndreas Gohr
121bf69b89cSAndreas Gohr    /**
12222654fdeSAndreas Gohr     * Access to the config values
12322654fdeSAndreas Gohr     *
12422654fdeSAndreas Gohr     * Because the client class does configuration cleanup, the auth class should access
12522654fdeSAndreas Gohr     * config values through the client
12622654fdeSAndreas Gohr     *
12722654fdeSAndreas Gohr     * @param string $key
12822654fdeSAndreas Gohr     * @return mixed returns null on missing config
12922654fdeSAndreas Gohr     */
13008ace392SAndreas Gohr    public function getConf($key)
13108ace392SAndreas Gohr    {
13222654fdeSAndreas Gohr        if (!isset($this->config[$key])) return null;
13322654fdeSAndreas Gohr        return $this->config[$key];
13422654fdeSAndreas Gohr    }
13522654fdeSAndreas Gohr
13622654fdeSAndreas Gohr    /**
13708ace392SAndreas Gohr     * Authenticate as admin if not authenticated yet
1381078ec26SAndreas Gohr     */
1391078ec26SAndreas Gohr    public function autoAuth()
1401078ec26SAndreas Gohr    {
1411078ec26SAndreas Gohr        if ($this->isAuthenticated) return true;
1429446f9efSAndreas Gohr
143a1128cc0SAndreas Gohr        $user = $this->prepareBindUser($this->config['admin_username']);
144*fb75804eSAndreas Gohr        try {
145*fb75804eSAndreas Gohr            $this->authenticate($user, $this->config['admin_password']);
146*fb75804eSAndreas Gohr            return true;
147*fb75804eSAndreas Gohr        } catch (\Exception $e) {
14808ace392SAndreas Gohr            $this->error('Automatic bind failed. Probably wrong user/password.', __FILE__, __LINE__);
1498b2677edSAndreas Gohr        }
150*fb75804eSAndreas Gohr        return false;
1511078ec26SAndreas Gohr    }
1521078ec26SAndreas Gohr
1531078ec26SAndreas Gohr    /**
1541078ec26SAndreas Gohr     * Authenticates a given user. This client will remain authenticated
1551078ec26SAndreas Gohr     *
1561078ec26SAndreas Gohr     * @param string $user
1571078ec26SAndreas Gohr     * @param string $pass
1585a3b9122SAndreas Gohr     * @noinspection PhpRedundantCatchClauseInspection
159*fb75804eSAndreas Gohr     * @return true
160*fb75804eSAndreas Gohr     * @throws ConnectionException
161*fb75804eSAndreas Gohr     * @throws OperationException
162*fb75804eSAndreas Gohr     * @throws BindException
1631078ec26SAndreas Gohr     */
1641078ec26SAndreas Gohr    public function authenticate($user, $pass)
1651078ec26SAndreas Gohr    {
166a1128cc0SAndreas Gohr        $user = $this->prepareBindUser($user);
16708ace392SAndreas Gohr        $this->isAuthenticated = false;
16880ac552fSAndreas Gohr
16908ace392SAndreas Gohr        if (!$this->ldap->isConnected() && $this->config['encryption'] === 'tls') {
1701078ec26SAndreas Gohr            try {
1711078ec26SAndreas Gohr                $this->ldap->startTls();
17208ace392SAndreas Gohr            } catch (ConnectionException|OperationException $e) {
173da369b60SAndreas Gohr                $this->fatal($e);
174*fb75804eSAndreas Gohr                throw $e;
1751078ec26SAndreas Gohr            }
1761078ec26SAndreas Gohr        }
1771078ec26SAndreas Gohr
1781078ec26SAndreas Gohr        try {
1791078ec26SAndreas Gohr            $this->ldap->bind($user, $pass);
1801078ec26SAndreas Gohr        } catch (BindException $e) {
181fde03b26SAndreas Gohr            $this->debug("Bind for $user failed: " . $e->getMessage(), $e->getFile(), $e->getLine());
182*fb75804eSAndreas Gohr            throw $e;
18308ace392SAndreas Gohr        } catch (ConnectionException|OperationException $e) {
184da369b60SAndreas Gohr            $this->fatal($e);
185*fb75804eSAndreas Gohr            throw $e;
1861078ec26SAndreas Gohr        }
1871078ec26SAndreas Gohr
18808ace392SAndreas Gohr        $this->isAuthenticated = $user;
1891078ec26SAndreas Gohr        return true;
1901078ec26SAndreas Gohr    }
1911078ec26SAndreas Gohr
1921078ec26SAndreas Gohr    /**
1931078ec26SAndreas Gohr     * Get info for a single user, use cache if available
1941078ec26SAndreas Gohr     *
1951078ec26SAndreas Gohr     * @param string $username
1961078ec26SAndreas Gohr     * @param bool $fetchgroups Are groups needed?
1971078ec26SAndreas Gohr     * @return array|null
1981078ec26SAndreas Gohr     */
1991078ec26SAndreas Gohr    public function getCachedUser($username, $fetchgroups = true)
2001078ec26SAndreas Gohr    {
2016d90d5c8SAndreas Gohr        global $conf;
2026d90d5c8SAndreas Gohr
2036d90d5c8SAndreas Gohr        // memory cache first
2041078ec26SAndreas Gohr        if (isset($this->userCache[$username])) {
2051078ec26SAndreas Gohr            if (!$fetchgroups || is_array($this->userCache[$username]['grps'])) {
2061078ec26SAndreas Gohr                return $this->userCache[$username];
2071078ec26SAndreas Gohr            }
2081078ec26SAndreas Gohr        }
2091078ec26SAndreas Gohr
2106d90d5c8SAndreas Gohr        // disk cache second
2115dcabedaSAndreas Gohr        if ($this->config['usefscache']) {
2126d90d5c8SAndreas Gohr            $cachename = getCacheName($username, '.pureldap-user');
2136d90d5c8SAndreas Gohr            $cachetime = @filemtime($cachename);
2146d90d5c8SAndreas Gohr            if ($cachetime && (time() - $cachetime) < $conf['auth_security_timeout']) {
215208fe81aSAndreas Gohr                $this->userCache[$username] = json_decode(
216208fe81aSAndreas Gohr                    file_get_contents($cachename),
217208fe81aSAndreas Gohr                    true,
218208fe81aSAndreas Gohr                    512,
219208fe81aSAndreas Gohr                    JSON_THROW_ON_ERROR
220208fe81aSAndreas Gohr                );
2216d90d5c8SAndreas Gohr                if (!$fetchgroups || is_array($this->userCache[$username]['grps'])) {
2226d90d5c8SAndreas Gohr                    return $this->userCache[$username];
2236d90d5c8SAndreas Gohr                }
2246d90d5c8SAndreas Gohr            }
2255dcabedaSAndreas Gohr        }
2266d90d5c8SAndreas Gohr
2271078ec26SAndreas Gohr        // fetch fresh data
2281078ec26SAndreas Gohr        $info = $this->getUser($username, $fetchgroups);
2291078ec26SAndreas Gohr
2301078ec26SAndreas Gohr        // store in cache
2315dcabedaSAndreas Gohr        if ($this->config['usefscache'] && $info !== null) {
2321078ec26SAndreas Gohr            $this->userCache[$username] = $info;
23308ace392SAndreas Gohr            /** @noinspection PhpUndefinedVariableInspection We know that cachename is defined */
234208fe81aSAndreas Gohr            file_put_contents($cachename, json_encode($info, JSON_THROW_ON_ERROR));
2351078ec26SAndreas Gohr        }
2361078ec26SAndreas Gohr
2371078ec26SAndreas Gohr        return $info;
2381078ec26SAndreas Gohr    }
2391078ec26SAndreas Gohr
2401078ec26SAndreas Gohr    /**
2411078ec26SAndreas Gohr     * Fetch a single user
2421078ec26SAndreas Gohr     *
2431078ec26SAndreas Gohr     * @param string $username
2441078ec26SAndreas Gohr     * @param bool $fetchgroups Shall groups be fetched, too?
2451078ec26SAndreas Gohr     * @return null|array
2461078ec26SAndreas Gohr     */
2471078ec26SAndreas Gohr    abstract public function getUser($username, $fetchgroups = true);
2481078ec26SAndreas Gohr
2491078ec26SAndreas Gohr    /**
25008ace392SAndreas Gohr     * Set a new password for a user
25108ace392SAndreas Gohr     *
25208ace392SAndreas Gohr     * @param string $username
25308ace392SAndreas Gohr     * @param string $newpass
25408ace392SAndreas Gohr     * @param string $oldpass Needed for self-service password change in AD
25508ace392SAndreas Gohr     * @return bool
25608ace392SAndreas Gohr     */
25708ace392SAndreas Gohr    abstract public function setPassword($username, $newpass, $oldpass = null);
25808ace392SAndreas Gohr
25908ace392SAndreas Gohr    /**
2605a3b9122SAndreas Gohr     * Return a list of all available groups, use cache if available
2615a3b9122SAndreas Gohr     *
2625a3b9122SAndreas Gohr     * @return string[]
2635a3b9122SAndreas Gohr     */
2645a3b9122SAndreas Gohr    public function getCachedGroups()
2655a3b9122SAndreas Gohr    {
2665a3b9122SAndreas Gohr        if (empty($this->groupCache)) {
2675a3b9122SAndreas Gohr            $this->groupCache = $this->getGroups();
2685a3b9122SAndreas Gohr        }
2695a3b9122SAndreas Gohr
2705a3b9122SAndreas Gohr        return $this->groupCache;
2715a3b9122SAndreas Gohr    }
2725a3b9122SAndreas Gohr
2735a3b9122SAndreas Gohr    /**
2745a3b9122SAndreas Gohr     * Return a list of all available groups
2755a3b9122SAndreas Gohr     *
276b21740b4SAndreas Gohr     * Optionally filter the list
277b21740b4SAndreas Gohr     *
278b21740b4SAndreas Gohr     * @param null|string $match Filter for this, null for all groups
279b21740b4SAndreas Gohr     * @param string $filtermethod How to match the groups
2805a3b9122SAndreas Gohr     * @return string[]
2815a3b9122SAndreas Gohr     */
282204fba68SAndreas Gohr    abstract public function getGroups($match = null, $filtermethod = self::FILTER_EQUAL);
2835a3b9122SAndreas Gohr
28480ac552fSAndreas Gohr    /**
285a1128cc0SAndreas Gohr     * Clean the user name for use in DokuWiki
28680ac552fSAndreas Gohr     *
287a1128cc0SAndreas Gohr     * @param string $user
28880ac552fSAndreas Gohr     * @return string
28980ac552fSAndreas Gohr     */
290a1128cc0SAndreas Gohr    abstract public function cleanUser($user);
29180ac552fSAndreas Gohr
29280ac552fSAndreas Gohr    /**
293a1128cc0SAndreas Gohr     * Clean the group name for use in DokuWiki
29480ac552fSAndreas Gohr     *
295a1128cc0SAndreas Gohr     * @param string $group
29680ac552fSAndreas Gohr     * @return string
29780ac552fSAndreas Gohr     */
298a1128cc0SAndreas Gohr    abstract public function cleanGroup($group);
299a1128cc0SAndreas Gohr
300a1128cc0SAndreas Gohr    /**
301a1128cc0SAndreas Gohr     * Inheriting classes may want to manipulate the user before binding
302a1128cc0SAndreas Gohr     *
303a1128cc0SAndreas Gohr     * @param string $user
304a1128cc0SAndreas Gohr     * @return string
305a1128cc0SAndreas Gohr     */
306a1128cc0SAndreas Gohr    protected function prepareBindUser($user)
307a1128cc0SAndreas Gohr    {
308a1128cc0SAndreas Gohr        return $user;
309a1128cc0SAndreas Gohr    }
31080ac552fSAndreas Gohr
3115a3b9122SAndreas Gohr    /**
3121078ec26SAndreas Gohr     * Helper method to get the first value of the given attribute
3131078ec26SAndreas Gohr     *
3141078ec26SAndreas Gohr     * The given attribute may be null, an empty string is returned then
3151078ec26SAndreas Gohr     *
3161078ec26SAndreas Gohr     * @param Attribute|null $attribute
3171078ec26SAndreas Gohr     * @return string
3181078ec26SAndreas Gohr     */
3195a3b9122SAndreas Gohr    protected function attr2str($attribute)
3205a3b9122SAndreas Gohr    {
3211078ec26SAndreas Gohr        if ($attribute !== null) {
3228de38791SAndreas Gohr            return $attribute->firstValue() ?? '';
3231078ec26SAndreas Gohr        }
3241078ec26SAndreas Gohr        return '';
3251078ec26SAndreas Gohr    }
3261078ec26SAndreas Gohr
3271078ec26SAndreas Gohr    /**
3289c590892SAndreas Gohr     * Get the attributes that should be fetched for a user
3299c590892SAndreas Gohr     *
3309c590892SAndreas Gohr     * Can be extended in sub classes
3319c590892SAndreas Gohr     *
3329c590892SAndreas Gohr     * @return Attribute[]
3339c590892SAndreas Gohr     */
3349c590892SAndreas Gohr    protected function userAttributes()
3359c590892SAndreas Gohr    {
3369c590892SAndreas Gohr        // defaults
3379c590892SAndreas Gohr        $attr = [
3389c590892SAndreas Gohr            new Attribute('dn'),
3399c590892SAndreas Gohr            new Attribute('displayName'),
3409c590892SAndreas Gohr            new Attribute('mail'),
3419c590892SAndreas Gohr        ];
3429c590892SAndreas Gohr        // additionals
3439c590892SAndreas Gohr        foreach ($this->config['attributes'] as $attribute) {
3449c590892SAndreas Gohr            $attr[] = new Attribute($attribute);
3459c590892SAndreas Gohr        }
3469c590892SAndreas Gohr        return $attr;
3479c590892SAndreas Gohr    }
3489c590892SAndreas Gohr
3499c590892SAndreas Gohr    /**
3500f498d06SAndreas Gohr     * Get the maximum age a password may have before it needs to be changed
3510f498d06SAndreas Gohr     *
3520f498d06SAndreas Gohr     * @return int 0 if no maximum age is set
3530f498d06SAndreas Gohr     */
354208fe81aSAndreas Gohr    public function getMaxPasswordAge()
355208fe81aSAndreas Gohr    {
3560f498d06SAndreas Gohr        return 0;
3570f498d06SAndreas Gohr    }
3580f498d06SAndreas Gohr
3590f498d06SAndreas Gohr    /**
360da369b60SAndreas Gohr     * Handle fatal exceptions
3611078ec26SAndreas Gohr     *
3621078ec26SAndreas Gohr     * @param \Exception $e
3631078ec26SAndreas Gohr     */
364da369b60SAndreas Gohr    protected function fatal(\Exception $e)
3651078ec26SAndreas Gohr    {
36608ace392SAndreas Gohr        ErrorHandler::logException($e);
367c872f0e3SAndreas Gohr
3681078ec26SAndreas Gohr        if (defined('DOKU_UNITTEST')) {
3698595f73eSAndreas Gohr            throw new \RuntimeException('', 0, $e);
3701078ec26SAndreas Gohr        }
371c872f0e3SAndreas Gohr
372c872f0e3SAndreas Gohr        msg('[pureldap] ' . hsc($e->getMessage()), -1);
373da369b60SAndreas Gohr    }
3741078ec26SAndreas Gohr
375da369b60SAndreas Gohr    /**
376a1128cc0SAndreas Gohr     * Handle error output
377a1128cc0SAndreas Gohr     *
378a1128cc0SAndreas Gohr     * @param string $msg
379a1128cc0SAndreas Gohr     * @param string $file
380a1128cc0SAndreas Gohr     * @param int $line
381a1128cc0SAndreas Gohr     */
38208ace392SAndreas Gohr    public function error($msg, $file, $line)
383a1128cc0SAndreas Gohr    {
38408ace392SAndreas Gohr        Logger::error('[pureldap] ' . $msg, '', $file, $line);
385c872f0e3SAndreas Gohr
386a1128cc0SAndreas Gohr        if (defined('DOKU_UNITTEST')) {
387a1128cc0SAndreas Gohr            throw new \RuntimeException($msg . ' at ' . $file . ':' . $line);
388a1128cc0SAndreas Gohr        }
389a1128cc0SAndreas Gohr
390c872f0e3SAndreas Gohr        msg('[pureldap] ' . hsc($msg), -1);
391a1128cc0SAndreas Gohr    }
392a1128cc0SAndreas Gohr
393a1128cc0SAndreas Gohr    /**
394da369b60SAndreas Gohr     * Handle debug output
395da369b60SAndreas Gohr     *
396da369b60SAndreas Gohr     * @param string $msg
397da369b60SAndreas Gohr     * @param string $file
398da369b60SAndreas Gohr     * @param int $line
399da369b60SAndreas Gohr     */
40008ace392SAndreas Gohr    public function debug($msg, $file, $line)
401da369b60SAndreas Gohr    {
402c872f0e3SAndreas Gohr        global $conf;
403c872f0e3SAndreas Gohr
40408ace392SAndreas Gohr        Logger::debug('[pureldap] ' . $msg, '', $file, $line);
405c872f0e3SAndreas Gohr
406c872f0e3SAndreas Gohr        if ($conf['allowdebug']) {
40708ace392SAndreas Gohr            msg('[pureldap] ' . hsc($msg) . ' at ' . $file . ':' . $line);
4081078ec26SAndreas Gohr        }
4091078ec26SAndreas Gohr    }
410c872f0e3SAndreas Gohr}
411