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