xref: /plugin/pureldap/classes/Client.php (revision 204fba68543421287b88b2a48c04b5dea32b5394)
1<?php
2
3namespace dokuwiki\plugin\pureldap\classes;
4
5use dokuwiki\Utf8\PhpString;
6use FreeDSx\Ldap\Entry\Attribute;
7use FreeDSx\Ldap\Exception\BindException;
8use FreeDSx\Ldap\Exception\ConnectionException;
9use FreeDSx\Ldap\Exception\OperationException;
10use FreeDSx\Ldap\LdapClient;
11
12require_once __DIR__ . '/../vendor/autoload.php';
13
14abstract class Client
15{
16    const FILTER_EQUAL = 'equal';
17    const FILTER_CONTAINS = 'contains';
18    const FILTER_STARTSWITH = 'startsWith';
19    const FILTER_ENDSWITH = 'endsWith';
20
21    /** @var array the configuration */
22    protected $config;
23
24    /** @var LdapClient */
25    protected $ldap;
26
27    /** @var bool is this client authenticated already? */
28    protected $isAuthenticated = false;
29
30    /** @var array cached user info */
31    protected $userCache = [];
32
33    /** @var array cached group list */
34    protected $groupCache = [];
35
36    /**
37     * Client constructor.
38     * @param array $config
39     */
40    public function __construct($config)
41    {
42        $this->config = $this->prepareConfig($config);
43        $this->ldap = new LdapClient($this->config);
44    }
45
46    /**
47     * Setup sane config defaults
48     *
49     * @param array $config
50     * @return array
51     */
52    protected function prepareConfig($config)
53    {
54        $defaults = [
55            'defaultgroup' => 'user', // we expect this to be passed from global conf
56            'suffix' => '',
57            'port' => '',
58            'encryption' => false,
59            'admin_username' => '',
60            'admin_password' => '',
61            'page_size' => 150,
62            'use_ssl' => false,
63            'validate' => 'strict',
64            'attributes' => [],
65        ];
66
67        $config = array_merge($defaults, $config);
68
69        // default port depends on SSL setting
70        if (!$config['port']) {
71            $config['port'] = ($config['encryption'] === 'ssl') ? 636 : 389;
72        }
73
74        // set ssl parameters
75        $config['use_ssl'] = ($config['encryption'] === 'ssl');
76        if ($config['validate'] === 'none') {
77            $config['ssl_validate_cert'] = false;
78        } elseif ($config['validate'] === 'self') {
79            $config['ssl_allow_self_signed'] = true;
80        }
81
82        $config['suffix'] = PhpString::strtolower($config['suffix']);
83
84        return $config;
85    }
86
87    /**
88     * Authenticate as admin
89     */
90    public function autoAuth()
91    {
92        if ($this->isAuthenticated) return true;
93
94        $user = $this->prepareBindUser($this->config['admin_username']);
95        $ok = $this->authenticate($user, $this->config['admin_password']);
96        if (!$ok) {
97            $this->error('Administrative bind failed. Probably wrong user/password.', __FILE__, __LINE__);
98        }
99        return $ok;
100    }
101
102    /**
103     * Authenticates a given user. This client will remain authenticated
104     *
105     * @param string $user
106     * @param string $pass
107     * @return bool was the authentication successful?
108     * @noinspection PhpRedundantCatchClauseInspection
109     */
110    public function authenticate($user, $pass)
111    {
112        $user = $this->prepareBindUser($user);
113
114        if ($this->config['encryption'] === 'tls') {
115            try {
116                $this->ldap->startTls();
117            } catch (OperationException $e) {
118                $this->fatal($e);
119            }
120        }
121
122        try {
123            $this->ldap->bind($user, $pass);
124        } catch (BindException $e) {
125            return false;
126        } catch (ConnectionException $e) {
127            $this->fatal($e);
128            return false;
129        } catch (OperationException $e) {
130            $this->fatal($e);
131            return false;
132        }
133
134        $this->isAuthenticated = true;
135        return true;
136    }
137
138    /**
139     * Get info for a single user, use cache if available
140     *
141     * @param string $username
142     * @param bool $fetchgroups Are groups needed?
143     * @return array|null
144     */
145    public function getCachedUser($username, $fetchgroups = true)
146    {
147        global $conf;
148
149        // memory cache first
150        if (isset($this->userCache[$username])) {
151            if (!$fetchgroups || is_array($this->userCache[$username]['grps'])) {
152                return $this->userCache[$username];
153            }
154        }
155
156        // disk cache second
157        $cachename = getCacheName($username, '.pureldap-user');
158        $cachetime = @filemtime($cachename);
159        if ($cachetime && (time() - $cachetime) < $conf['auth_security_timeout']) {
160            $this->userCache[$username] = json_decode(file_get_contents($cachename), true);
161            if (!$fetchgroups || is_array($this->userCache[$username]['grps'])) {
162                return $this->userCache[$username];
163            }
164        }
165
166        // fetch fresh data
167        $info = $this->getUser($username, $fetchgroups);
168
169        // store in cache
170        if ($info !== null) {
171            $this->userCache[$username] = $info;
172            file_put_contents($cachename, json_encode($info));
173        }
174
175        return $info;
176    }
177
178    /**
179     * Fetch a single user
180     *
181     * @param string $username
182     * @param bool $fetchgroups Shall groups be fetched, too?
183     * @return null|array
184     */
185    abstract public function getUser($username, $fetchgroups = true);
186
187    /**
188     * Return a list of all available groups, use cache if available
189     *
190     * @return string[]
191     */
192    public function getCachedGroups()
193    {
194        if (empty($this->groupCache)) {
195            $this->groupCache = $this->getGroups();
196        }
197
198        return $this->groupCache;
199    }
200
201    /**
202     * Return a list of all available groups
203     *
204     * Optionally filter the list
205     *
206     * @param null|string $match Filter for this, null for all groups
207     * @param string $filtermethod How to match the groups
208     * @return string[]
209     */
210    abstract public function getGroups($match = null, $filtermethod = self::FILTER_EQUAL);
211
212    /**
213     * Clean the user name for use in DokuWiki
214     *
215     * @param string $user
216     * @return string
217     */
218    abstract public function cleanUser($user);
219
220    /**
221     * Clean the group name for use in DokuWiki
222     *
223     * @param string $group
224     * @return string
225     */
226    abstract public function cleanGroup($group);
227
228    /**
229     * Inheriting classes may want to manipulate the user before binding
230     *
231     * @param string $user
232     * @return string
233     */
234    protected function prepareBindUser($user)
235    {
236        return $user;
237    }
238
239    /**
240     * Helper method to get the first value of the given attribute
241     *
242     * The given attribute may be null, an empty string is returned then
243     *
244     * @param Attribute|null $attribute
245     * @return string
246     */
247    protected function attr2str($attribute)
248    {
249        if ($attribute !== null) {
250            return $attribute->firstValue();
251        }
252        return '';
253    }
254
255    /**
256     * Get the attributes that should be fetched for a user
257     *
258     * Can be extended in sub classes
259     *
260     * @return Attribute[]
261     */
262    protected function userAttributes()
263    {
264        // defaults
265        $attr = [
266            new Attribute('dn'),
267            new Attribute('displayName'),
268            new Attribute('mail'),
269        ];
270        // additionals
271        foreach ($this->config['attributes'] as $attribute) {
272            $attr[] = new Attribute($attribute);
273        }
274        return $attr;
275    }
276
277    /**
278     * Handle fatal exceptions
279     *
280     * @param \Exception $e
281     */
282    protected function fatal(\Exception $e)
283    {
284        if (class_exists('\dokuwiki\ErrorHandler')) {
285            \dokuwiki\ErrorHandler::logException($e);
286        }
287
288        if (defined('DOKU_UNITTEST')) {
289            throw new \RuntimeException('', 0, $e);
290        }
291
292        msg('[pureldap] ' . hsc($e->getMessage()), -1);
293    }
294
295    /**
296     * Handle error output
297     *
298     * @param string $msg
299     * @param string $file
300     * @param int $line
301     */
302    protected function error($msg, $file, $line)
303    {
304        if (class_exists('\dokuwiki\Logger')) {
305            \dokuwiki\Logger::error('[pureldap] ' . $msg, '', $file, $line);
306        }
307
308        if (defined('DOKU_UNITTEST')) {
309            throw new \RuntimeException($msg . ' at ' . $file . ':' . $line);
310        }
311
312        msg('[pureldap] ' . hsc($msg), -1);
313    }
314
315    /**
316     * Handle debug output
317     *
318     * @param string $msg
319     * @param string $file
320     * @param int $line
321     */
322    protected function debug($msg, $file, $line)
323    {
324        global $conf;
325
326        if (class_exists('\dokuwiki\Logger')) {
327            \dokuwiki\Logger::debug('[pureldap] ' . $msg, '', $file, $line);
328        }
329
330        if ($conf['allowdebug']) {
331            msg('[pureldap] ' . hsc($msg) . ' at ' . $file . ':' . $line, 0);
332        }
333    }
334}
335