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