xref: /plugin/pureldap/classes/Client.php (revision b914569fe2bf0cc586f78951bd0c636bc6597916)
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' => 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['domain'] = PhpString::strtolower($config['domain']);
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->qualifiedUser($this->config['admin_username']);
90        $ok = $this->authenticate($user, $this->config['admin_password']);
91        if(!$ok) {
92            $this->debug('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->qualifiedUser($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     * Construst the fully qualified name to identify a user
209     *
210     * @param string $username
211     * @return string
212     */
213    abstract public function qualifiedUser($username);
214
215    /**
216     * Simplify the username if possible
217     *
218     * @param string $username
219     * @return string
220     */
221    abstract public function simpleUser($username);
222
223    /**
224     * Helper method to get the first value of the given attribute
225     *
226     * The given attribute may be null, an empty string is returned then
227     *
228     * @param Attribute|null $attribute
229     * @return string
230     */
231    protected function attr2str($attribute)
232    {
233        if ($attribute !== null) {
234            return $attribute->firstValue();
235        }
236        return '';
237    }
238
239    /**
240     * Handle fatal exceptions
241     *
242     * @param \Exception $e
243     */
244    protected function fatal(\Exception $e)
245    {
246        if (defined('DOKU_UNITTEST')) {
247            throw new \RuntimeException('', 0, $e);
248        }
249        msg('[pureldap] ' . hsc($e->getMessage()) . ' at ' . $e->getFile() . ':' . $e->getLine(), -1);
250    }
251
252    /**
253     * Handle debug output
254     *
255     * @param string $msg
256     * @param string $file
257     * @param int $line
258     */
259    protected function debug($msg, $file, $line)
260    {
261        msg('[pureldap] ' . hsc($msg) . ' at ' . $file . ':' . $line, 0);
262    }
263}
264