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