xref: /plugin/pureldap/classes/Client.php (revision 8b2677ed853ea95dff78fc187fcaa99cb99f824a)
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        $ok = $this->authenticate($this->config['admin_username'], $this->config['admin_password']);
88        if(!$ok) {
89            $this->debug('Administrative bind failed. Probably wrong user/password.', __FILE__, __LINE__);
90        }
91        return $ok;
92    }
93
94    /**
95     * Authenticates a given user. This client will remain authenticated
96     *
97     * @param string $user
98     * @param string $pass
99     * @return bool was the authentication successful?
100     * @noinspection PhpRedundantCatchClauseInspection
101     */
102    public function authenticate($user, $pass)
103    {
104        $user = $this->qualifiedUser($user);
105
106        if ($this->config['encryption'] === 'tls') {
107            try {
108                $this->ldap->startTls();
109            } catch (OperationException $e) {
110                $this->fatal($e);
111            }
112        }
113
114        try {
115            $this->ldap->bind($user, $pass);
116        } catch (BindException $e) {
117            return false;
118        } catch (ConnectionException $e) {
119            $this->fatal($e);
120            return false;
121        } catch (OperationException $e) {
122            $this->fatal($e);
123            return false;
124        }
125
126        $this->isAuthenticated = true;
127        return true;
128    }
129
130    /**
131     * Get info for a single user, use cache if available
132     *
133     * @param string $username
134     * @param bool $fetchgroups Are groups needed?
135     * @return array|null
136     */
137    public function getCachedUser($username, $fetchgroups = true)
138    {
139        global $conf;
140
141        // memory cache first
142        if (isset($this->userCache[$username])) {
143            if (!$fetchgroups || is_array($this->userCache[$username]['grps'])) {
144                return $this->userCache[$username];
145            }
146        }
147
148        // disk cache second
149        $cachename = getCacheName($username, '.pureldap-user');
150        $cachetime = @filemtime($cachename);
151        if ($cachetime && (time() - $cachetime) < $conf['auth_security_timeout']) {
152            $this->userCache[$username] = json_decode(file_get_contents($cachename), true);
153            if (!$fetchgroups || is_array($this->userCache[$username]['grps'])) {
154                return $this->userCache[$username];
155            }
156        }
157
158        // fetch fresh data
159        $info = $this->getUser($username, $fetchgroups);
160
161        // store in cache
162        if ($info !== null) {
163            $this->userCache[$username] = $info;
164            file_put_contents($cachename, json_encode($info));
165        }
166
167        return $info;
168    }
169
170    /**
171     * Fetch a single user
172     *
173     * @param string $username
174     * @param bool $fetchgroups Shall groups be fetched, too?
175     * @return null|array
176     */
177    abstract public function getUser($username, $fetchgroups = true);
178
179    /**
180     * Return a list of all available groups, use cache if available
181     *
182     * @return string[]
183     */
184    public function getCachedGroups()
185    {
186        if (empty($this->groupCache)) {
187            $this->groupCache = $this->getGroups();
188        }
189
190        return $this->groupCache;
191    }
192
193    /**
194     * Return a list of all available groups
195     *
196     * Optionally filter the list
197     *
198     * @param null|string $match Filter for this, null for all groups
199     * @param string $filtermethod How to match the groups
200     * @return string[]
201     */
202    abstract public function getGroups($match = null, $filtermethod = 'equal');
203
204    /**
205     * Construst the fully qualified name to identify a user
206     *
207     * @param string $username
208     * @return string
209     */
210    abstract public function qualifiedUser($username);
211
212    /**
213     * Simplify the username if possible
214     *
215     * @param string $username
216     * @return string
217     */
218    abstract public function simpleUser($username);
219
220    /**
221     * Helper method to get the first value of the given attribute
222     *
223     * The given attribute may be null, an empty string is returned then
224     *
225     * @param Attribute|null $attribute
226     * @return string
227     */
228    protected function attr2str($attribute)
229    {
230        if ($attribute !== null) {
231            return $attribute->firstValue();
232        }
233        return '';
234    }
235
236    /**
237     * Handle fatal exceptions
238     *
239     * @param \Exception $e
240     */
241    protected function fatal(\Exception $e)
242    {
243        if (defined('DOKU_UNITTEST')) {
244            throw new \RuntimeException('', 0, $e);
245        }
246        msg('[pureldap] ' . hsc($e->getMessage()) . ' at ' . $e->getFile() . ':' . $e->getLine(), -1);
247    }
248
249    /**
250     * Handle debug output
251     *
252     * @param string $msg
253     * @param string $file
254     * @param int $line
255     */
256    protected function debug($msg, $file, $line)
257    {
258        msg('[pureldap] ' . hsc($msg) . ' at ' . $file . ':' . $line, 0);
259    }
260}
261