1 <?php
2 
3 use dokuwiki\Extension\AuthPlugin;
4 use dokuwiki\Utf8\Clean;
5 use dokuwiki\Utf8\PhpString;
6 use dokuwiki\Utf8\Sort;
7 use dokuwiki\Logger;
8 
9 /**
10  * Active Directory authentication backend for DokuWiki
11  *
12  * This makes authentication with a Active Directory server much easier
13  * than when using the normal LDAP backend by utilizing the adLDAP library
14  *
15  * Usage:
16  *   Set DokuWiki's local.protected.php auth setting to read
17  *
18  *   $conf['authtype']       = 'authad';
19  *
20  *   $conf['plugin']['authad']['account_suffix']     = '@my.domain.org';
21  *   $conf['plugin']['authad']['base_dn']            = 'DC=my,DC=domain,DC=org';
22  *   $conf['plugin']['authad']['domain_controllers'] = 'srv1.domain.org,srv2.domain.org';
23  *
24  *   //optional:
25  *   $conf['plugin']['authad']['sso']                = 1;
26  *   $conf['plugin']['authad']['admin_username']     = 'root';
27  *   $conf['plugin']['authad']['admin_password']     = 'pass';
28  *   $conf['plugin']['authad']['real_primarygroup']  = 1;
29  *   $conf['plugin']['authad']['use_ssl']            = 1;
30  *   $conf['plugin']['authad']['use_tls']            = 1;
31  *   $conf['plugin']['authad']['debug']              = 1;
32  *   // warn user about expiring password this many days in advance:
33  *   $conf['plugin']['authad']['expirywarn']         = 5;
34  *
35  *   // get additional information to the userinfo array
36  *   // add a list of comma separated ldap contact fields.
37  *   $conf['plugin']['authad']['additional'] = 'field1,field2';
38  *
39  * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
40  * @author  James Van Lommel <jamesvl@gmail.com>
41  * @link    http://www.nosq.com/blog/2005/08/ldap-activedirectory-and-dokuwiki/
42  * @author  Andreas Gohr <andi@splitbrain.org>
43  * @author  Jan Schumann <js@schumann-it.com>
44  */
45 class auth_plugin_authad extends AuthPlugin
46 {
47     /**
48      * @var array hold connection data for a specific AD domain
49      */
50     protected $opts = [];
51 
52     /**
53      * @var array open connections for each AD domain, as adLDAP objects
54      */
55     protected $adldap = [];
56 
57     /**
58      * @var bool message state
59      */
60     protected $msgshown = false;
61 
62     /**
63      * @var array user listing cache
64      */
65     protected $users = [];
66 
67     /**
68      * @var array filter patterns for listing users
69      */
70     protected $pattern = [];
71 
72     protected $grpsusers = [];
73 
74     /**
75      * Constructor
76      */
77     public function __construct()
78     {
79         global $INPUT;
80         parent::__construct();
81 
82         require_once(DOKU_PLUGIN . 'authad/adLDAP/adLDAP.php');
83         require_once(DOKU_PLUGIN . 'authad/adLDAP/classes/adLDAPUtils.php');
84 
85         // we load the config early to modify it a bit here
86         $this->loadConfig();
87 
88         // additional information fields
89         if (isset($this->conf['additional'])) {
90             $this->conf['additional'] = str_replace(' ', '', $this->conf['additional']);
91             $this->conf['additional'] = explode(',', $this->conf['additional']);
92         } else $this->conf['additional'] = [];
93 
94         // ldap extension is needed
95         if (!function_exists('ldap_connect')) {
96             if ($this->conf['debug'])
97                 msg("AD Auth: PHP LDAP extension not found.", -1);
98             $this->success = false;
99             return;
100         }
101 
102         // Prepare SSO
103         if (!empty($INPUT->server->str('REMOTE_USER'))) {
104             // make sure the right encoding is used
105             if ($this->getConf('sso_charset')) {
106                 $INPUT->server->set(
107                     'REMOTE_USER',
108                     iconv($this->getConf('sso_charset'), 'UTF-8', $INPUT->server->str('REMOTE_USER'))
109                 );
110             } elseif (!Clean::isUtf8($INPUT->server->str('REMOTE_USER'))) {
111                 $INPUT->server->set('REMOTE_USER', utf8_encode($INPUT->server->str('REMOTE_USER')));
112             }
113 
114             // trust the incoming user
115             if ($this->conf['sso']) {
116                 $INPUT->server->set('REMOTE_USER', $this->cleanUser($INPUT->server->str('REMOTE_USER')));
117 
118                 // we need to simulate a login
119                 if (empty($_COOKIE[DOKU_COOKIE])) {
120                     $INPUT->set('u', $INPUT->server->str('REMOTE_USER'));
121                     $INPUT->set('p', 'sso_only');
122                 }
123             }
124         }
125 
126         // other can do's are changed in $this->_loadServerConfig() base on domain setup
127         $this->cando['modName'] = (bool)$this->conf['update_name'];
128         $this->cando['modMail'] = (bool)$this->conf['update_mail'];
129         $this->cando['getUserCount'] = true;
130     }
131 
132     /**
133      * Load domain config on capability check
134      *
135      * @param string $cap
136      * @return bool
137      */
138     public function canDo($cap)
139     {
140         global $INPUT;
141         //capabilities depend on config, which may change depending on domain
142         $domain = $this->getUserDomain($INPUT->server->str('REMOTE_USER'));
143         $this->loadServerConfig($domain);
144         return parent::canDo($cap);
145     }
146 
147     /**
148      * Check user+password [required auth function]
149      *
150      * Checks if the given user exists and the given
151      * plaintext password is correct by trying to bind
152      * to the LDAP server
153      *
154      * @author  James Van Lommel <james@nosq.com>
155      * @param string $user
156      * @param string $pass
157      * @return  bool
158      */
159     public function checkPass($user, $pass)
160     {
161         global $INPUT;
162         if (
163             $INPUT->server->str('REMOTE_USER') == $user &&
164             $this->conf['sso']
165         ) return true;
166 
167         $adldap = $this->initAdLdap($this->getUserDomain($user));
168         if (!$adldap) return false;
169 
170         try {
171             return $adldap->authenticate($this->getUserName($user), $pass);
172         } catch (adLDAPException $e) {
173             // shouldn't really happen
174             return false;
175         }
176     }
177 
178     /**
179      * Return user info [required auth function]
180      *
181      * Returns info about the given user needs to contain
182      * at least these fields:
183      *
184      * name    string  full name of the user
185      * mail    string  email address of the user
186      * grps    array   list of groups the user is in
187      *
188      * This AD specific function returns the following
189      * addional fields:
190      *
191      * dn         string    distinguished name (DN)
192      * uid        string    samaccountname
193      * lastpwd    int       timestamp of the date when the password was set
194      * expires    true      if the password expires
195      * expiresin  int       seconds until the password expires
196      * any fields specified in the 'additional' config option
197      *
198      * @author  James Van Lommel <james@nosq.com>
199      * @param string $user
200      * @param bool $requireGroups (optional) - ignored, groups are always supplied by this plugin
201      * @return array|false
202      */
203     public function getUserData($user, $requireGroups = true)
204     {
205         global $conf;
206         global $lang;
207         global $ID;
208         global $INPUT;
209         $adldap = $this->initAdLdap($this->getUserDomain($user));
210         if (!$adldap) return false;
211         if ($user == '') return false;
212 
213         $fields = ['mail', 'displayname', 'samaccountname', 'lastpwd', 'pwdlastset', 'useraccountcontrol'];
214 
215         // add additional fields to read
216         $fields = array_merge($fields, $this->conf['additional']);
217         $fields = array_unique($fields);
218         $fields = array_filter($fields);
219 
220         //get info for given user
221         $result = $adldap->user()->info($this->getUserName($user), $fields);
222         if ($result == false) {
223             return false;
224         }
225 
226         //general user info
227         $info = [];
228         $info['name'] = $result[0]['displayname'][0];
229         $info['mail'] = $result[0]['mail'][0];
230         $info['uid']  = $result[0]['samaccountname'][0];
231         $info['dn']   = $result[0]['dn'];
232         //last password set (Windows counts from January 1st 1601)
233         $info['lastpwd'] = $result[0]['pwdlastset'][0] / 10_000_000 - 11_644_473_600;
234         //will it expire?
235         $info['expires'] = !($result[0]['useraccountcontrol'][0] & 0x10000); //ADS_UF_DONT_EXPIRE_PASSWD
236 
237         // additional information
238         foreach ($this->conf['additional'] as $field) {
239             if (isset($result[0][strtolower($field)])) {
240                 $info[$field] = $result[0][strtolower($field)][0];
241             }
242         }
243 
244         // handle ActiveDirectory memberOf
245         $info['grps'] = $adldap->user()->groups($this->getUserName($user), (bool) $this->opts['recursive_groups']);
246 
247         if (is_array($info['grps'])) {
248             foreach ($info['grps'] as $ndx => $group) {
249                 $info['grps'][$ndx] = $this->cleanGroup($group);
250             }
251         } else {
252             $info['grps'] = [];
253         }
254 
255         // always add the default group to the list of groups
256         if (!in_array($conf['defaultgroup'], $info['grps'])) {
257             $info['grps'][] = $conf['defaultgroup'];
258         }
259 
260         // add the user's domain to the groups
261         $domain = $this->getUserDomain($user);
262         if ($domain && !in_array("domain-$domain", $info['grps'])) {
263             $info['grps'][] = $this->cleanGroup("domain-$domain");
264         }
265 
266         // check expiry time
267         if ($info['expires'] && $this->conf['expirywarn']) {
268             try {
269                 $expiry = $adldap->user()->passwordExpiry($user);
270                 if (is_array($expiry)) {
271                     $info['expiresat'] = $expiry['expiryts'];
272                     $info['expiresin'] = round(($info['expiresat'] - time()) / (24 * 60 * 60));
273 
274                     // if this is the current user, warn him (once per request only)
275                     if (
276                         ($INPUT->server->str('REMOTE_USER') == $user) &&
277                         ($info['expiresin'] <= $this->conf['expirywarn']) &&
278                         !$this->msgshown
279                     ) {
280                         $msg = sprintf($this->getLang('authpwdexpire'), $info['expiresin']);
281                         if ($this->canDo('modPass')) {
282                             $url = wl($ID, ['do' => 'profile']);
283                             $msg .= ' <a href="' . $url . '">' . $lang['btn_profile'] . '</a>';
284                         }
285                         msg($msg);
286                         $this->msgshown = true;
287                     }
288                 }
289             } catch (adLDAPException $e) {
290                 // ignore. should usually not happen
291             }
292         }
293 
294         return $info;
295     }
296 
297     /**
298      * Make AD group names usable by DokuWiki.
299      *
300      * Removes backslashes ('\'), pound signs ('#'), and converts spaces to underscores.
301      *
302      * @author  James Van Lommel (jamesvl@gmail.com)
303      * @param string $group
304      * @return string
305      */
306     public function cleanGroup($group)
307     {
308         $group = str_replace('\\', '', $group);
309         $group = str_replace('#', '', $group);
310         $group = preg_replace('[\s]', '_', $group);
311         $group = PhpString::strtolower(trim($group));
312         return $group;
313     }
314 
315     /**
316      * Sanitize user names
317      *
318      * Normalizes domain parts, does not modify the user name itself (unlike cleanGroup)
319      *
320      * @author Andreas Gohr <gohr@cosmocode.de>
321      * @param string $user
322      * @return string
323      */
324     public function cleanUser($user)
325     {
326         $domain = '';
327 
328         // get NTLM or Kerberos domain part
329         [$dom, $user] = sexplode('\\', $user, 2, '');
330         if (!$user) $user = $dom;
331         if ($dom) $domain = $dom;
332         [$user, $dom] = sexplode('@', $user, 2, '');
333         if ($dom) $domain = $dom;
334 
335         // clean up both
336         $domain = PhpString::strtolower(trim($domain));
337         $user   = PhpString::strtolower(trim($user));
338 
339         // is this a known, valid domain or do we work without account suffix? if not discard
340         if (
341             (!isset($this->conf[$domain]) || !is_array($this->conf[$domain])) &&
342             $this->conf['account_suffix'] !== ''
343         ) {
344             $domain = '';
345         }
346 
347         // reattach domain
348         if ($domain) $user = "$user@$domain";
349         return $user;
350     }
351 
352     /**
353      * Most values in LDAP are case-insensitive
354      *
355      * @return bool
356      */
357     public function isCaseSensitive()
358     {
359         return false;
360     }
361 
362     /**
363      * Create a Search-String useable by adLDAPUsers::all($includeDescription = false, $search = "*", $sorted = true)
364      *
365      * @param array $filter
366      * @return string
367      */
368     protected function constructSearchString($filter)
369     {
370         if (!$filter) {
371             return '*';
372         }
373         $adldapUtils = new adLDAPUtils($this->initAdLdap(null));
374         $result = '*';
375         if (isset($filter['name'])) {
376             $result .= ')(displayname=*' . $adldapUtils->ldapSlashes($filter['name']) . '*';
377             unset($filter['name']);
378         }
379 
380         if (isset($filter['user'])) {
381             $result .= ')(samAccountName=*' . $adldapUtils->ldapSlashes($filter['user']) . '*';
382             unset($filter['user']);
383         }
384 
385         if (isset($filter['mail'])) {
386             $result .= ')(mail=*' . $adldapUtils->ldapSlashes($filter['mail']) . '*';
387             unset($filter['mail']);
388         }
389         return $result;
390     }
391 
392     /**
393      * Return a count of the number of user which meet $filter criteria
394      *
395      * @param array $filter  $filter array of field/pattern pairs, empty array for no filter
396      * @return int number of users
397      */
398     public function getUserCount($filter = [])
399     {
400         $adldap = $this->initAdLdap(null);
401         if (!$adldap) {
402             Logger::debug("authad/auth.php getUserCount(): _adldap not set.");
403             return -1;
404         }
405         if ($filter == []) {
406             $result = $adldap->user()->all();
407         } else {
408             $searchString = $this->constructSearchString($filter);
409             $result = $adldap->user()->all(false, $searchString);
410             if (isset($filter['grps'])) {
411                 $this->users = array_fill_keys($result, false);
412                 /** @var admin_plugin_usermanager $usermanager */
413                 $usermanager = plugin_load("admin", "usermanager", false);
414                 $usermanager->setLastdisabled(true);
415                 if (!isset($this->grpsusers[$this->filterToString($filter)])) {
416                     $this->fillGroupUserArray($filter, $usermanager->getStart() + 3 * $usermanager->getPagesize());
417                 } elseif (
418                     count($this->grpsusers[$this->filterToString($filter)]) <
419                     $usermanager->getStart() + 3 * $usermanager->getPagesize()
420                 ) {
421                     $this->fillGroupUserArray(
422                         $filter,
423                         $usermanager->getStart() +
424                         3 * $usermanager->getPagesize() -
425                         count($this->grpsusers[$this->filterToString($filter)])
426                     );
427                 }
428                 $result = $this->grpsusers[$this->filterToString($filter)];
429             } else {
430                 /** @var admin_plugin_usermanager $usermanager */
431                 $usermanager = plugin_load("admin", "usermanager", false);
432                 $usermanager->setLastdisabled(false);
433             }
434         }
435 
436         if (!$result) {
437             return 0;
438         }
439         return count($result);
440     }
441 
442     /**
443      *
444      * create a unique string for each filter used with a group
445      *
446      * @param array $filter
447      * @return string
448      */
449     protected function filterToString($filter)
450     {
451         $result = '';
452         if (isset($filter['user'])) {
453             $result .= 'user-' . $filter['user'];
454         }
455         if (isset($filter['name'])) {
456             $result .= 'name-' . $filter['name'];
457         }
458         if (isset($filter['mail'])) {
459             $result .= 'mail-' . $filter['mail'];
460         }
461         if (isset($filter['grps'])) {
462             $result .= 'grps-' . $filter['grps'];
463         }
464         return $result;
465     }
466 
467     /**
468      * Create an array of $numberOfAdds users passing a certain $filter, including belonging
469      * to a certain group and save them to a object-wide array. If the array
470      * already exists try to add $numberOfAdds further users to it.
471      *
472      * @param array $filter
473      * @param int $numberOfAdds additional number of users requested
474      * @return int number of Users actually add to Array
475      */
476     protected function fillGroupUserArray($filter, $numberOfAdds)
477     {
478         if (isset($this->grpsusers[$this->filterToString($filter)])) {
479             $actualstart = count($this->grpsusers[$this->filterToString($filter)]);
480         } else {
481             $this->grpsusers[$this->filterToString($filter)] = [];
482             $actualstart = 0;
483         }
484 
485         $i = 0;
486         $count = 0;
487         $this->constructPattern($filter);
488         foreach ($this->users as $user => &$info) {
489             if ($i++ < $actualstart) {
490                 continue;
491             }
492             if ($info === false) {
493                 $info = $this->getUserData($user);
494             }
495             if ($this->filter($user, $info)) {
496                 $this->grpsusers[$this->filterToString($filter)][$user] = $info;
497                 if (($numberOfAdds > 0) && (++$count >= $numberOfAdds)) break;
498             }
499         }
500         return $count;
501     }
502 
503     /**
504      * Bulk retrieval of user data
505      *
506      * @author  Dominik Eckelmann <dokuwiki@cosmocode.de>
507      *
508      * @param   int $start index of first user to be returned
509      * @param   int $limit max number of users to be returned
510      * @param   array $filter array of field/pattern pairs, null for no filter
511      * @return array userinfo (refer getUserData for internal userinfo details)
512      */
513     public function retrieveUsers($start = 0, $limit = 0, $filter = [])
514     {
515         $adldap = $this->initAdLdap(null);
516         if (!$adldap) return [];
517 
518         //if (!$this->users) {
519             //get info for given user
520             $result = $adldap->user()->all(false, $this->constructSearchString($filter));
521             if (!$result) return [];
522             $this->users = array_fill_keys($result, false);
523         //}
524 
525         $i     = 0;
526         $count = 0;
527         $result = [];
528 
529         if (!isset($filter['grps'])) {
530             /** @var admin_plugin_usermanager $usermanager */
531             $usermanager = plugin_load("admin", "usermanager", false);
532             $usermanager->setLastdisabled(false);
533             $this->constructPattern($filter);
534             foreach ($this->users as $user => &$info) {
535                 if ($i++ < $start) {
536                     continue;
537                 }
538                 if ($info === false) {
539                     $info = $this->getUserData($user);
540                 }
541                 $result[$user] = $info;
542                 if (($limit > 0) && (++$count >= $limit)) break;
543             }
544         } else {
545             /** @var admin_plugin_usermanager $usermanager */
546             $usermanager = plugin_load("admin", "usermanager", false);
547             $usermanager->setLastdisabled(true);
548             if (
549                 !isset($this->grpsusers[$this->filterToString($filter)]) ||
550                 count($this->grpsusers[$this->filterToString($filter)]) < ($start + $limit)
551             ) {
552                 if (!isset($this->grpsusers[$this->filterToString($filter)])) {
553                     $this->grpsusers[$this->filterToString($filter)] = [];
554                 }
555 
556                 $this->fillGroupUserArray(
557                     $filter,
558                     $start + $limit - count($this->grpsusers[$this->filterToString($filter)]) + 1
559                 );
560             }
561             if (!$this->grpsusers[$this->filterToString($filter)]) return [];
562             foreach ($this->grpsusers[$this->filterToString($filter)] as $user => &$info) {
563                 if ($i++ < $start) {
564                     continue;
565                 }
566                 $result[$user] = $info;
567                 if (($limit > 0) && (++$count >= $limit)) break;
568             }
569         }
570         return $result;
571     }
572 
573     /**
574      * Modify user data
575      *
576      * @param   string $user      nick of the user to be changed
577      * @param   array  $changes   array of field/value pairs to be changed
578      * @return  bool
579      */
580     public function modifyUser($user, $changes)
581     {
582         $return = true;
583         $adldap = $this->initAdLdap($this->getUserDomain($user));
584         if (!$adldap) {
585             msg($this->getLang('connectfail'), -1);
586             return false;
587         }
588 
589         // password changing
590         if (isset($changes['pass'])) {
591             try {
592                 $return = $adldap->user()->password($this->getUserName($user), $changes['pass']);
593             } catch (adLDAPException $e) {
594                 if ($this->conf['debug']) msg('AD Auth: ' . $e->getMessage(), -1);
595                 $return = false;
596             }
597             if (!$return) msg($this->getLang('passchangefail'), -1);
598         }
599 
600         // changing user data
601         $adchanges = [];
602         if (isset($changes['name'])) {
603             // get first and last name
604             $parts                     = explode(' ', $changes['name']);
605             $adchanges['surname']      = array_pop($parts);
606             $adchanges['firstname']    = implode(' ', $parts);
607             $adchanges['display_name'] = $changes['name'];
608         }
609         if (isset($changes['mail'])) {
610             $adchanges['email'] = $changes['mail'];
611         }
612         if ($adchanges !== []) {
613             try {
614                 $return &= $adldap->user()->modify($this->getUserName($user), $adchanges);
615             } catch (adLDAPException $e) {
616                 if ($this->conf['debug']) msg('AD Auth: ' . $e->getMessage(), -1);
617                 $return = false;
618             }
619             if (!$return) msg($this->getLang('userchangefail'), -1);
620         }
621 
622         return $return;
623     }
624 
625     /**
626      * Initialize the AdLDAP library and connect to the server
627      *
628      * When you pass null as domain, it will reuse any existing domain.
629      * Eg. the one of the logged in user. It falls back to the default
630      * domain if no current one is available.
631      *
632      * @param string|null $domain The AD domain to use
633      * @return adLDAP|bool true if a connection was established
634      */
635     protected function initAdLdap($domain)
636     {
637         if (is_null($domain) && is_array($this->opts)) {
638             $domain = $this->opts['domain'];
639         }
640 
641         $this->opts = $this->loadServerConfig((string) $domain);
642         if (isset($this->adldap[$domain])) return $this->adldap[$domain];
643 
644         // connect
645         try {
646             $this->adldap[$domain] = new adLDAP($this->opts);
647             return $this->adldap[$domain];
648         } catch (Exception $e) {
649             if ($this->conf['debug']) {
650                 msg('AD Auth: ' . $e->getMessage(), -1);
651             }
652             $this->success         = false;
653             $this->adldap[$domain] = null;
654         }
655         return false;
656     }
657 
658     /**
659      * Get the domain part from a user
660      *
661      * @param string $user
662      * @return string
663      */
664     public function getUserDomain($user)
665     {
666         [, $domain] = sexplode('@', $user, 2, '');
667         return $domain;
668     }
669 
670     /**
671      * Get the user part from a user
672      *
673      * When an account suffix is set, we strip the domain part from the user
674      *
675      * @param string $user
676      * @return string
677      */
678     public function getUserName($user)
679     {
680         if ($this->conf['account_suffix'] !== '') {
681             [$user] = explode('@', $user, 2);
682         }
683         return $user;
684     }
685 
686     /**
687      * Fetch the configuration for the given AD domain
688      *
689      * @param string $domain current AD domain
690      * @return array
691      */
692     protected function loadServerConfig($domain)
693     {
694         // prepare adLDAP standard configuration
695         $opts = $this->conf;
696 
697         $opts['domain'] = $domain;
698 
699         // add possible domain specific configuration
700         if ($domain && is_array($this->conf[$domain] ?? '')) foreach ($this->conf[$domain] as $key => $val) {
701             $opts[$key] = $val;
702         }
703 
704         // handle multiple AD servers
705         $opts['domain_controllers'] = explode(',', $opts['domain_controllers']);
706         $opts['domain_controllers'] = array_map('trim', $opts['domain_controllers']);
707         $opts['domain_controllers'] = array_filter($opts['domain_controllers']);
708 
709         // compatibility with old option name
710         if (empty($opts['admin_username']) && !empty($opts['ad_username'])) {
711             $opts['admin_username'] = $opts['ad_username'];
712         }
713         if (empty($opts['admin_password']) && !empty($opts['ad_password'])) {
714             $opts['admin_password'] = $opts['ad_password'];
715         }
716         $opts['admin_password'] = conf_decodeString($opts['admin_password']); // deobfuscate
717 
718         // we can change the password if SSL is set
719         if ($opts['update_pass'] && ($opts['use_ssl'] || $opts['use_tls'])) {
720             $this->cando['modPass'] = true;
721         } else {
722             $this->cando['modPass'] = false;
723         }
724 
725         // adLDAP expects empty user/pass as NULL, we're less strict FS#2781
726         if (empty($opts['admin_username'])) $opts['admin_username'] = null;
727         if (empty($opts['admin_password'])) $opts['admin_password'] = null;
728 
729         // user listing needs admin priviledges
730         if (!empty($opts['admin_username']) && !empty($opts['admin_password'])) {
731             $this->cando['getUsers'] = true;
732         } else {
733             $this->cando['getUsers'] = false;
734         }
735 
736         return $opts;
737     }
738 
739     /**
740      * Returns a list of configured domains
741      *
742      * The default domain has an empty string as key
743      *
744      * @return array associative array(key => domain)
745      */
746     public function getConfiguredDomains()
747     {
748         $domains = [];
749         if (empty($this->conf['account_suffix'])) return $domains; // not configured yet
750 
751         // add default domain, using the name from account suffix
752         $domains[''] = ltrim($this->conf['account_suffix'], '@');
753 
754         // find additional domains
755         foreach ($this->conf as $key => $val) {
756             if (is_array($val) && isset($val['account_suffix'])) {
757                 $domains[$key] = ltrim($val['account_suffix'], '@');
758             }
759         }
760         Sort::ksort($domains);
761 
762         return $domains;
763     }
764 
765     /**
766      * Check provided user and userinfo for matching patterns
767      *
768      * The patterns are set up with $this->_constructPattern()
769      *
770      * @author Chris Smith <chris@jalakai.co.uk>
771      *
772      * @param string $user
773      * @param array  $info
774      * @return bool
775      */
776     protected function filter($user, $info)
777     {
778         foreach ($this->pattern as $item => $pattern) {
779             if ($item == 'user') {
780                 if (!preg_match($pattern, $user)) return false;
781             } elseif ($item == 'grps') {
782                 if (!count(preg_grep($pattern, $info['grps']))) return false;
783             } elseif (!preg_match($pattern, $info[$item])) {
784                 return false;
785             }
786         }
787         return true;
788     }
789 
790     /**
791      * Create a pattern for $this->_filter()
792      *
793      * @author Chris Smith <chris@jalakai.co.uk>
794      *
795      * @param array $filter
796      */
797     protected function constructPattern($filter)
798     {
799         $this->pattern = [];
800         foreach ($filter as $item => $pattern) {
801             $this->pattern[$item] = '/' . str_replace('/', '\/', $pattern) . '/i'; // allow regex characters
802         }
803     }
804 }
805