xref: /plugin/pureldap/classes/Client.php (revision 9446f9efcf6a07f32e7ce6e6706899c1a2a9ef42)
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            'domain' => '',
52            'port' => '',
53            'encryption' => false,
54            'admin_username' => '',
55            'admin_password' => '',
56            'page_size' => 1000,
57            'use_ssl' => false,
58            'validate' => 'strict',
59        ];
60
61        $config = array_merge($defaults, $config);
62
63        // default port depends on SSL setting
64        if (!$config['port']) {
65            $config['port'] = ($config['encryption'] === 'ssl') ? 636 : 389;
66        }
67
68        // set ssl parameters
69        $config['use_ssl'] = ($config['encryption'] === 'ssl');
70        if ($config['validate'] === 'none') {
71            $config['ssl_validate_cert'] = false;
72        } elseif ($config['validate'] === 'self') {
73            $config['ssl_allow_self_signed'] = true;
74        }
75
76        $config['domain'] = PhpString::strtolower($config['domain']);
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->qualifiedUser($this->config['admin_username']);
89        $ok = $this->authenticate($user, $this->config['admin_password']);
90        if(!$ok) {
91            $this->debug('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->qualifiedUser($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 = 'equal');
205
206    /**
207     * Construst the fully qualified name to identify a user
208     *
209     * @param string $username
210     * @return string
211     */
212    abstract public function qualifiedUser($username);
213
214    /**
215     * Simplify the username if possible
216     *
217     * @param string $username
218     * @return string
219     */
220    abstract public function simpleUser($username);
221
222    /**
223     * Helper method to get the first value of the given attribute
224     *
225     * The given attribute may be null, an empty string is returned then
226     *
227     * @param Attribute|null $attribute
228     * @return string
229     */
230    protected function attr2str($attribute)
231    {
232        if ($attribute !== null) {
233            return $attribute->firstValue();
234        }
235        return '';
236    }
237
238    /**
239     * Handle fatal exceptions
240     *
241     * @param \Exception $e
242     */
243    protected function fatal(\Exception $e)
244    {
245        if (defined('DOKU_UNITTEST')) {
246            throw new \RuntimeException('', 0, $e);
247        }
248        msg('[pureldap] ' . hsc($e->getMessage()) . ' at ' . $e->getFile() . ':' . $e->getLine(), -1);
249    }
250
251    /**
252     * Handle debug output
253     *
254     * @param string $msg
255     * @param string $file
256     * @param int $line
257     */
258    protected function debug($msg, $file, $line)
259    {
260        msg('[pureldap] ' . hsc($msg) . ' at ' . $file . ':' . $line, 0);
261    }
262}
263