xref: /plugin/pureldap/classes/ADClient.php (revision fb75804e73edf4af608854927a231691f3206614)
11078ec26SAndreas Gohr<?php
21078ec26SAndreas Gohr
31078ec26SAndreas Gohrnamespace dokuwiki\plugin\pureldap\classes;
41078ec26SAndreas Gohr
580ac552fSAndreas Gohruse dokuwiki\Utf8\PhpString;
69c590892SAndreas Gohruse FreeDSx\Ldap\Entry\Attribute;
71078ec26SAndreas Gohruse FreeDSx\Ldap\Entry\Entries;
81078ec26SAndreas Gohruse FreeDSx\Ldap\Entry\Entry;
91078ec26SAndreas Gohruse FreeDSx\Ldap\Exception\OperationException;
101078ec26SAndreas Gohruse FreeDSx\Ldap\Operations;
111078ec26SAndreas Gohruse FreeDSx\Ldap\Search\Filters;
121078ec26SAndreas Gohr
13f17bb68bSAndreas Gohr/**
14f17bb68bSAndreas Gohr * Implement Active Directory Specifics
15f17bb68bSAndreas Gohr */
161078ec26SAndreas Gohrclass ADClient extends Client
171078ec26SAndreas Gohr{
18208fe81aSAndreas Gohr    public const ADS_UF_DONT_EXPIRE_PASSWD = 0x10000;
190f498d06SAndreas Gohr
20e7339d5aSAndreas Gohr    /**
21e7339d5aSAndreas Gohr     * @var GroupHierarchyCache
22e7339d5aSAndreas Gohr     * @see getGroupHierarchyCache
23e7339d5aSAndreas Gohr     */
24208fe81aSAndreas Gohr    protected $gch;
251078ec26SAndreas Gohr
261078ec26SAndreas Gohr    /** @inheritDoc */
271078ec26SAndreas Gohr    public function getUser($username, $fetchgroups = true)
281078ec26SAndreas Gohr    {
2908ace392SAndreas Gohr        $entry = $this->getUserEntry($username);
3008ace392SAndreas Gohr        if ($entry === null) return null;
3108ace392SAndreas Gohr        return $this->entry2User($entry);
3208ace392SAndreas Gohr    }
3308ace392SAndreas Gohr
3408ace392SAndreas Gohr    /**
3508ace392SAndreas Gohr     * Get the LDAP entry for the given user
3608ace392SAndreas Gohr     *
3708ace392SAndreas Gohr     * @param string $username
3808ace392SAndreas Gohr     * @return Entry|null
3908ace392SAndreas Gohr     */
4008ace392SAndreas Gohr    protected function getUserEntry($username)
4108ace392SAndreas Gohr    {
421078ec26SAndreas Gohr        if (!$this->autoAuth()) return null;
439bafffeaSAndreas Gohr        $samaccountname = $this->simpleUser($username);
449bafffeaSAndreas Gohr        $userprincipal = $this->qualifiedUser($username);
451078ec26SAndreas Gohr
461078ec26SAndreas Gohr        $filter = Filters::and(
471078ec26SAndreas Gohr            Filters::equal('objectClass', 'user'),
489bafffeaSAndreas Gohr            Filters::or(
499bafffeaSAndreas Gohr                Filters::equal('sAMAccountName', $samaccountname),
509bafffeaSAndreas Gohr                Filters::equal('userPrincipalName', $userprincipal)
519bafffeaSAndreas Gohr            )
529bafffeaSAndreas Gohr
531078ec26SAndreas Gohr        );
54b21740b4SAndreas Gohr        $this->debug('Searching ' . $filter->toString(), __FILE__, __LINE__);
551078ec26SAndreas Gohr
561078ec26SAndreas Gohr        try {
579c590892SAndreas Gohr            $attributes = $this->userAttributes();
589c590892SAndreas Gohr            $entries = $this->ldap->search(Operations::search($filter, ...$attributes));
591078ec26SAndreas Gohr        } catch (OperationException $e) {
60b21740b4SAndreas Gohr            $this->fatal($e);
611078ec26SAndreas Gohr            return null;
621078ec26SAndreas Gohr        }
631078ec26SAndreas Gohr        if ($entries->count() !== 1) return null;
6408ace392SAndreas Gohr        return $entries->first();
6508ace392SAndreas Gohr    }
6608ace392SAndreas Gohr
6708ace392SAndreas Gohr    /** @inheritDoc */
6808ace392SAndreas Gohr    public function setPassword($username, $newpass, $oldpass = null)
6908ace392SAndreas Gohr    {
7008ace392SAndreas Gohr        if (!$this->autoAuth()) return false;
7108ace392SAndreas Gohr
7208ace392SAndreas Gohr        $entry = $this->getUserEntry($username);
7308ace392SAndreas Gohr        if ($entry === null) {
7408ace392SAndreas Gohr            $this->error("User '$username' not found", __FILE__, __LINE__);
7508ace392SAndreas Gohr            return false;
7608ace392SAndreas Gohr        }
7708ace392SAndreas Gohr
7808ace392SAndreas Gohr        if ($oldpass) {
7908ace392SAndreas Gohr            // if an old password is given, this is a self-service password change
8008ace392SAndreas Gohr            // this has to be executed as the user themselves, not as the admin
8108ace392SAndreas Gohr            if ($this->isAuthenticated !== $this->prepareBindUser($username)) {
82*fb75804eSAndreas Gohr                try {
83*fb75804eSAndreas Gohr                    $this->authenticate($username, $oldpass);
84*fb75804eSAndreas Gohr                } catch (\Exception $e) {
8508ace392SAndreas Gohr                    $this->error("Old password for '$username' is wrong", __FILE__, __LINE__);
8608ace392SAndreas Gohr                    return false;
8708ace392SAndreas Gohr                }
8808ace392SAndreas Gohr            }
8908ace392SAndreas Gohr
9008ace392SAndreas Gohr            $entry->remove('unicodePwd', $this->encodePassword($oldpass));
9108ace392SAndreas Gohr            $entry->add('unicodePwd', $this->encodePassword($newpass));
9208ace392SAndreas Gohr        } else {
9308ace392SAndreas Gohr            // run as admin user
9408ace392SAndreas Gohr            $entry->set('unicodePwd', $this->encodePassword($newpass));
9508ace392SAndreas Gohr        }
9608ace392SAndreas Gohr
9708ace392SAndreas Gohr        try {
9808ace392SAndreas Gohr            $this->ldap->update($entry);
9908ace392SAndreas Gohr        } catch (OperationException $e) {
10008ace392SAndreas Gohr            $this->fatal($e);
10108ace392SAndreas Gohr            return false;
10208ace392SAndreas Gohr        }
10308ace392SAndreas Gohr        return true;
104b21740b4SAndreas Gohr    }
1051078ec26SAndreas Gohr
106b21740b4SAndreas Gohr    /** @inheritDoc */
107204fba68SAndreas Gohr    public function getGroups($match = null, $filtermethod = self::FILTER_EQUAL)
108b21740b4SAndreas Gohr    {
109b21740b4SAndreas Gohr        if (!$this->autoAuth()) return [];
110b21740b4SAndreas Gohr
111b21740b4SAndreas Gohr        $filter = Filters::and(
112b21740b4SAndreas Gohr            Filters::equal('objectClass', 'group')
113b21740b4SAndreas Gohr        );
114b21740b4SAndreas Gohr        if ($match !== null) {
115e7c3e817SAndreas Gohr            // FIXME this is a workaround that removes regex anchors and quoting as passed by the groupuser plugin
116e7c3e817SAndreas Gohr            // a proper fix requires splitbrain/dokuwiki#3028 to be implemented
117fce018daSAndreas Gohr            $match = ltrim($match, '^');
118fce018daSAndreas Gohr            $match = rtrim($match, '$');
119e7c3e817SAndreas Gohr            $match = stripslashes($match);
120fce018daSAndreas Gohr
121b21740b4SAndreas Gohr            $filter->add(Filters::$filtermethod('cn', $match));
122b21740b4SAndreas Gohr        }
123b21740b4SAndreas Gohr
124b21740b4SAndreas Gohr        $this->debug('Searching ' . $filter->toString(), __FILE__, __LINE__);
125b21740b4SAndreas Gohr        $search = Operations::search($filter, 'cn');
126b21740b4SAndreas Gohr        $paging = $this->ldap->paging($search);
127b21740b4SAndreas Gohr
128b21740b4SAndreas Gohr        $groups = [];
129b21740b4SAndreas Gohr        while ($paging->hasEntries()) {
130b21740b4SAndreas Gohr            try {
131b21740b4SAndreas Gohr                $entries = $paging->getEntries();
13208ace392SAndreas Gohr            } catch (OperationException $e) {
133b21740b4SAndreas Gohr                $this->fatal($e);
134b21740b4SAndreas Gohr                return $groups; // we return what we got so far
135b21740b4SAndreas Gohr            }
136b21740b4SAndreas Gohr
137b21740b4SAndreas Gohr            foreach ($entries as $entry) {
138b21740b4SAndreas Gohr                /** @var Entry $entry */
139204fba68SAndreas Gohr                $groups[$entry->getDn()->toString()] = $this->cleanGroup($this->attr2str($entry->get('cn')));
140b21740b4SAndreas Gohr            }
141b21740b4SAndreas Gohr        }
142b21740b4SAndreas Gohr
1431b0eb9b3SAndreas Gohr        asort($groups);
144b21740b4SAndreas Gohr        return $groups;
145b21740b4SAndreas Gohr    }
146b21740b4SAndreas Gohr
147b21740b4SAndreas Gohr    /**
148b21740b4SAndreas Gohr     * Fetch users matching the given filters
149b21740b4SAndreas Gohr     *
150b21740b4SAndreas Gohr     * @param array $match
151b21740b4SAndreas Gohr     * @param string $filtermethod The method to use for filtering
152b21740b4SAndreas Gohr     * @return array
153b21740b4SAndreas Gohr     */
154204fba68SAndreas Gohr    public function getFilteredUsers($match, $filtermethod = self::FILTER_EQUAL)
155b21740b4SAndreas Gohr    {
156b21740b4SAndreas Gohr        if (!$this->autoAuth()) return [];
157b21740b4SAndreas Gohr
158b21740b4SAndreas Gohr        $filter = Filters::and(Filters::equal('objectClass', 'user'));
159b21740b4SAndreas Gohr        if (isset($match['user'])) {
160a1128cc0SAndreas Gohr            $filter->add(Filters::$filtermethod('sAMAccountName', $this->simpleUser($match['user'])));
161b21740b4SAndreas Gohr        }
162b21740b4SAndreas Gohr        if (isset($match['name'])) {
163b21740b4SAndreas Gohr            $filter->add(Filters::$filtermethod('displayName', $match['name']));
164b21740b4SAndreas Gohr        }
165b21740b4SAndreas Gohr        if (isset($match['mail'])) {
166b21740b4SAndreas Gohr            $filter->add(Filters::$filtermethod('mail', $match['mail']));
167b21740b4SAndreas Gohr        }
168b21740b4SAndreas Gohr        if (isset($match['grps'])) {
169b21740b4SAndreas Gohr            // memberOf can not be checked with a substring match, so we need to get the right groups first
170b21740b4SAndreas Gohr            $groups = $this->getGroups($match['grps'], $filtermethod);
171e7339d5aSAndreas Gohr            $groupDNs = array_keys($groups);
172e7339d5aSAndreas Gohr
173e7339d5aSAndreas Gohr            if ($this->config['recursivegroups']) {
174e7339d5aSAndreas Gohr                $gch = $this->getGroupHierarchyCache();
175e7339d5aSAndreas Gohr                foreach ($groupDNs as $dn) {
176e7339d5aSAndreas Gohr                    $groupDNs = array_merge($groupDNs, $gch->getChildren($dn));
177e7339d5aSAndreas Gohr                }
178e7339d5aSAndreas Gohr                $groupDNs = array_unique($groupDNs);
179e7339d5aSAndreas Gohr            }
180e7339d5aSAndreas Gohr
181b21740b4SAndreas Gohr            $or = Filters::or();
182e7339d5aSAndreas Gohr            foreach ($groupDNs as $dn) {
183204fba68SAndreas Gohr                // domain users membership is in primary group
184e7339d5aSAndreas Gohr                if ($this->dn2group($dn) === $this->config['primarygroup']) {
185204fba68SAndreas Gohr                    $or->add(Filters::equal('primaryGroupID', 513));
186204fba68SAndreas Gohr                    continue;
187204fba68SAndreas Gohr                }
1887a36c1b4SAndreas Gohr                // find members of this exact group
1897a36c1b4SAndreas Gohr                $or->add(Filters::equal('memberOf', $dn));
190b21740b4SAndreas Gohr            }
191b21740b4SAndreas Gohr            $filter->add($or);
192b21740b4SAndreas Gohr        }
193e7339d5aSAndreas Gohr
194b21740b4SAndreas Gohr        $this->debug('Searching ' . $filter->toString(), __FILE__, __LINE__);
1959c590892SAndreas Gohr        $attributes = $this->userAttributes();
1969c590892SAndreas Gohr        $search = Operations::search($filter, ...$attributes);
197b21740b4SAndreas Gohr        $paging = $this->ldap->paging($search);
198b21740b4SAndreas Gohr
199b21740b4SAndreas Gohr        $users = [];
200b21740b4SAndreas Gohr        while ($paging->hasEntries()) {
201b21740b4SAndreas Gohr            try {
202b21740b4SAndreas Gohr                $entries = $paging->getEntries();
20308ace392SAndreas Gohr            } catch (OperationException $e) {
204b21740b4SAndreas Gohr                $this->fatal($e);
20580ac552fSAndreas Gohr                break; // we abort and return what we have so far
206b21740b4SAndreas Gohr            }
207b21740b4SAndreas Gohr
208b21740b4SAndreas Gohr            foreach ($entries as $entry) {
20908ace392SAndreas Gohr                $userinfo = $this->entry2User($entry);
210746af42cSAndreas Gohr                $users[$userinfo['user']] = $userinfo;
211b21740b4SAndreas Gohr            }
212b21740b4SAndreas Gohr        }
213b21740b4SAndreas Gohr
2141b0eb9b3SAndreas Gohr        ksort($users);
215b21740b4SAndreas Gohr        return $users;
216b21740b4SAndreas Gohr    }
217b21740b4SAndreas Gohr
218a1128cc0SAndreas Gohr    /** @inheritDoc */
219a1128cc0SAndreas Gohr    public function cleanUser($user)
22080ac552fSAndreas Gohr    {
221a1128cc0SAndreas Gohr        return $this->simpleUser($user);
22280ac552fSAndreas Gohr    }
22380ac552fSAndreas Gohr
224a1128cc0SAndreas Gohr    /** @inheritDoc */
225a1128cc0SAndreas Gohr    public function cleanGroup($group)
226a1128cc0SAndreas Gohr    {
227a1128cc0SAndreas Gohr        return PhpString::strtolower($group);
228a1128cc0SAndreas Gohr    }
229a1128cc0SAndreas Gohr
230a1128cc0SAndreas Gohr    /** @inheritDoc */
231208fe81aSAndreas Gohr    protected function prepareBindUser($user)
232a1128cc0SAndreas Gohr    {
23308ace392SAndreas Gohr        // add account suffix
23408ace392SAndreas Gohr        return $this->qualifiedUser($user);
23580ac552fSAndreas Gohr    }
23680ac552fSAndreas Gohr
23780ac552fSAndreas Gohr    /**
238e7339d5aSAndreas Gohr     * Initializes the Group Cache for nested groups
239e7339d5aSAndreas Gohr     *
240e7339d5aSAndreas Gohr     * @return GroupHierarchyCache
241e7339d5aSAndreas Gohr     */
242e7339d5aSAndreas Gohr    public function getGroupHierarchyCache()
243e7339d5aSAndreas Gohr    {
244e7339d5aSAndreas Gohr        if ($this->gch === null) {
245e7339d5aSAndreas Gohr            if (!$this->autoAuth()) return null;
2465dcabedaSAndreas Gohr            $this->gch = new GroupHierarchyCache($this->ldap, $this->config['usefscache']);
247e7339d5aSAndreas Gohr        }
248e7339d5aSAndreas Gohr        return $this->gch;
249e7339d5aSAndreas Gohr    }
250e7339d5aSAndreas Gohr
251e7339d5aSAndreas Gohr    /**
252a1128cc0SAndreas Gohr     * userPrincipalName in the form <user>@<suffix>
25308ace392SAndreas Gohr     *
25408ace392SAndreas Gohr     * @param string $user
25508ace392SAndreas Gohr     * @return string
25680ac552fSAndreas Gohr     */
257a1128cc0SAndreas Gohr    protected function qualifiedUser($user)
258a1128cc0SAndreas Gohr    {
259a1128cc0SAndreas Gohr        $user = $this->simpleUser($user); // strip any existing qualifiers
260a1128cc0SAndreas Gohr        if (!$this->config['suffix']) {
261a1128cc0SAndreas Gohr            $this->error('No account suffix set. Logins may fail.', __FILE__, __LINE__);
262a1128cc0SAndreas Gohr        }
263a1128cc0SAndreas Gohr
264a1128cc0SAndreas Gohr        return $user . '@' . $this->config['suffix'];
265a1128cc0SAndreas Gohr    }
266a1128cc0SAndreas Gohr
267a1128cc0SAndreas Gohr    /**
268a1128cc0SAndreas Gohr     * Removes the account suffix from the given user. Should match the SAMAccountName
26908ace392SAndreas Gohr     *
27008ace392SAndreas Gohr     * @param string $user
27108ace392SAndreas Gohr     * @return string
272a1128cc0SAndreas Gohr     */
273a1128cc0SAndreas Gohr    protected function simpleUser($user)
27480ac552fSAndreas Gohr    {
27580ac552fSAndreas Gohr        $user = PhpString::strtolower($user);
276a1128cc0SAndreas Gohr        $user = preg_replace('/@.*$/', '', $user);
277a1128cc0SAndreas Gohr        $user = preg_replace('/^.*\\\\/', '', $user);
27880ac552fSAndreas Gohr        return $user;
27980ac552fSAndreas Gohr    }
28080ac552fSAndreas Gohr
28180ac552fSAndreas Gohr    /**
282b21740b4SAndreas Gohr     * Transform an LDAP entry to a user info array
283b21740b4SAndreas Gohr     *
284b21740b4SAndreas Gohr     * @param Entry $entry
285b21740b4SAndreas Gohr     * @return array
286b21740b4SAndreas Gohr     */
287b21740b4SAndreas Gohr    protected function entry2User(Entry $entry)
288b21740b4SAndreas Gohr    {
2899bafffeaSAndreas Gohr        // prefer userPrincipalName over sAMAccountName
2909bafffeaSAndreas Gohr        $user = $this->simpleUser($this->attr2str($entry->get('userPrincipalName')));
2919bafffeaSAndreas Gohr        if($user === '') $user = $this->simpleUser($this->attr2str($entry->get('sAMAccountName')));
2929bafffeaSAndreas Gohr
293b914569fSAndreas Gohr        $user = [
2949bafffeaSAndreas Gohr            'user' => $user,
2951078ec26SAndreas Gohr            'name' => $this->attr2str($entry->get('DisplayName')) ?: $this->attr2str($entry->get('Name')),
2961078ec26SAndreas Gohr            'mail' => $this->attr2str($entry->get('mail')),
2971078ec26SAndreas Gohr            'dn' => $entry->getDn()->toString(),
2981078ec26SAndreas Gohr            'grps' => $this->getUserGroups($entry), // we always return groups because its currently inexpensive
2991078ec26SAndreas Gohr        ];
300b914569fSAndreas Gohr
3010f498d06SAndreas Gohr        // handle password expiry info
3020f498d06SAndreas Gohr        $lastChange = $this->attr2str($entry->get('pwdlastset'));
3030f498d06SAndreas Gohr        if ($lastChange) {
3040f498d06SAndreas Gohr            $lastChange = (int)substr($lastChange, 0, -7); // remove last 7 digits (100ns intervals to seconds)
305208fe81aSAndreas Gohr            $lastChange -= 11_644_473_600; // convert from 1601 to 1970 epoch
3060f498d06SAndreas Gohr        }
3070f498d06SAndreas Gohr        $user['lastpwd'] = (int)$lastChange;
3080f498d06SAndreas Gohr        $user['expires'] = !($this->attr2str($entry->get('useraccountcontrol')) & self::ADS_UF_DONT_EXPIRE_PASSWD);
3090f498d06SAndreas Gohr
310b914569fSAndreas Gohr        // get additional attributes
311b914569fSAndreas Gohr        foreach ($this->config['attributes'] as $attr) {
312b914569fSAndreas Gohr            $user[$attr] = $this->attr2str($entry->get($attr));
313b914569fSAndreas Gohr        }
314b914569fSAndreas Gohr
315b914569fSAndreas Gohr        return $user;
3161078ec26SAndreas Gohr    }
3171078ec26SAndreas Gohr
3181078ec26SAndreas Gohr    /**
3191078ec26SAndreas Gohr     * Get the list of groups the given user is member of
3201078ec26SAndreas Gohr     *
3211078ec26SAndreas Gohr     * This method currently does no LDAP queries and thus is inexpensive.
3221078ec26SAndreas Gohr     *
3231078ec26SAndreas Gohr     * @param Entry $userentry
3241078ec26SAndreas Gohr     * @return array
3251078ec26SAndreas Gohr     */
3261078ec26SAndreas Gohr    protected function getUserGroups(Entry $userentry)
3271078ec26SAndreas Gohr    {
328e7339d5aSAndreas Gohr        $groups = [];
329e7339d5aSAndreas Gohr
330e7339d5aSAndreas Gohr        if ($userentry->has('memberOf')) {
331e7339d5aSAndreas Gohr            $groupDNs = $userentry->get('memberOf')->getValues();
332e7339d5aSAndreas Gohr
333e7339d5aSAndreas Gohr            if ($this->config['recursivegroups']) {
334e7339d5aSAndreas Gohr                $gch = $this->getGroupHierarchyCache();
335e7339d5aSAndreas Gohr                foreach ($groupDNs as $dn) {
336e7339d5aSAndreas Gohr                    $groupDNs = array_merge($groupDNs, $gch->getParents($dn));
337e7339d5aSAndreas Gohr                }
338e7339d5aSAndreas Gohr
339e7339d5aSAndreas Gohr                $groupDNs = array_unique($groupDNs);
340e7339d5aSAndreas Gohr            }
341e7339d5aSAndreas Gohr            $groups = array_map([$this, 'dn2group'], $groupDNs);
342e7339d5aSAndreas Gohr        }
343e7339d5aSAndreas Gohr
344e7339d5aSAndreas Gohr        $groups[] = $this->config['defaultgroup']; // always add default
3451078ec26SAndreas Gohr
3461078ec26SAndreas Gohr        // resolving the primary group in AD is complicated but basically never needed
3471078ec26SAndreas Gohr        // http://support.microsoft.com/?kbid=321360
3481078ec26SAndreas Gohr        $gid = $userentry->get('primaryGroupID')->firstValue();
3491078ec26SAndreas Gohr        if ($gid == 513) {
350e7339d5aSAndreas Gohr            $groups[] = $this->cleanGroup($this->config['primarygroup']);
35151e92298SAndreas Gohr        }
35251e92298SAndreas Gohr
353f17bb68bSAndreas Gohr        sort($groups);
354f17bb68bSAndreas Gohr        return $groups;
35551e92298SAndreas Gohr    }
35651e92298SAndreas Gohr
3579c590892SAndreas Gohr    /** @inheritDoc */
3589c590892SAndreas Gohr    protected function userAttributes()
3599c590892SAndreas Gohr    {
3609c590892SAndreas Gohr        $attr = parent::userAttributes();
361a1128cc0SAndreas Gohr        $attr[] = new Attribute('sAMAccountName');
3629bafffeaSAndreas Gohr        $attr[] = new Attribute('userPrincipalName');
3639c590892SAndreas Gohr        $attr[] = new Attribute('Name');
3649c590892SAndreas Gohr        $attr[] = new Attribute('primaryGroupID');
3659c590892SAndreas Gohr        $attr[] = new Attribute('memberOf');
3660f498d06SAndreas Gohr        $attr[] = new Attribute('pwdlastset');
3670f498d06SAndreas Gohr        $attr[] = new Attribute('useraccountcontrol');
3689c590892SAndreas Gohr
3699c590892SAndreas Gohr        return $attr;
3709c590892SAndreas Gohr    }
371e7339d5aSAndreas Gohr
372e7339d5aSAndreas Gohr    /**
3730f498d06SAndreas Gohr     * Queries the maximum password age from the AD server
3740f498d06SAndreas Gohr     *
3750f498d06SAndreas Gohr     * Note: we do not check if passwords actually are set to expire here. This is encoded in the lower 32bit
3760f498d06SAndreas Gohr     * of the returned 64bit integer (see link below). We do not check this because it would require us to
3770f498d06SAndreas Gohr     * actually do large integer math and we can simply assume it's enabled when the age check was requested in
3780f498d06SAndreas Gohr     * DokuWiki configuration.
3790f498d06SAndreas Gohr     *
3800f498d06SAndreas Gohr     * @link http://msdn.microsoft.com/en-us/library/ms974598.aspx
3810f498d06SAndreas Gohr     * @param bool $useCache should a filesystem cache be used if available?
3820f498d06SAndreas Gohr     * @return int The maximum password age in seconds
3830f498d06SAndreas Gohr     */
3840f498d06SAndreas Gohr    public function getMaxPasswordAge($useCache = true)
3850f498d06SAndreas Gohr    {
3860f498d06SAndreas Gohr        global $conf;
3870f498d06SAndreas Gohr        $cachename = getCacheName('maxPwdAge', '.pureldap-maxPwdAge');
3880f498d06SAndreas Gohr        $cachetime = @filemtime($cachename);
3890f498d06SAndreas Gohr
3900f498d06SAndreas Gohr        // valid file system cache? use it
3910f498d06SAndreas Gohr        if ($useCache && $cachetime && (time() - $cachetime) < $conf['auth_security_timeout']) {
3920f498d06SAndreas Gohr            return (int)file_get_contents($cachename);
3930f498d06SAndreas Gohr        }
3940f498d06SAndreas Gohr
3950f498d06SAndreas Gohr        if (!$this->autoAuth()) return 0;
3960f498d06SAndreas Gohr
3970f498d06SAndreas Gohr        $attr = new Attribute('maxPwdAge');
3980f498d06SAndreas Gohr        try {
3990f498d06SAndreas Gohr            $entry = $this->ldap->read(
4000f498d06SAndreas Gohr                $this->getConf('base_dn'),
4010f498d06SAndreas Gohr                [$attr]
4020f498d06SAndreas Gohr            );
4030f498d06SAndreas Gohr        } catch (OperationException $e) {
4040f498d06SAndreas Gohr            $this->fatal($e);
4050f498d06SAndreas Gohr            return 0;
4060f498d06SAndreas Gohr        }
4070f498d06SAndreas Gohr        if (!$entry) return 0;
4080f498d06SAndreas Gohr        $maxPwdAge = $entry->get($attr)->firstValue();
4090f498d06SAndreas Gohr
4100f498d06SAndreas Gohr        // MS returns 100 nanosecond intervals, we want seconds
4110f498d06SAndreas Gohr        // we operate on strings to avoid integer overflow
4120f498d06SAndreas Gohr        // we also want a positive value, so we trim off the leading minus sign
4130f498d06SAndreas Gohr        // only then we convert to int
4140f498d06SAndreas Gohr        $maxPwdAge = (int)ltrim(substr($maxPwdAge, 0, -7), '-');
4150f498d06SAndreas Gohr
4160f498d06SAndreas Gohr        file_put_contents($cachename, $maxPwdAge);
4170f498d06SAndreas Gohr        return $maxPwdAge;
4180f498d06SAndreas Gohr    }
4190f498d06SAndreas Gohr
4200f498d06SAndreas Gohr    /**
421e7339d5aSAndreas Gohr     * Extract the group name from the DN
422e7339d5aSAndreas Gohr     *
423e7339d5aSAndreas Gohr     * @param string $dn
424e7339d5aSAndreas Gohr     * @return string
425e7339d5aSAndreas Gohr     */
426e7339d5aSAndreas Gohr    protected function dn2group($dn)
427e7339d5aSAndreas Gohr    {
428208fe81aSAndreas Gohr        [$cn] = explode(',', $dn, 2);
429e7339d5aSAndreas Gohr        return $this->cleanGroup(substr($cn, 3));
430e7339d5aSAndreas Gohr    }
43108ace392SAndreas Gohr
43208ace392SAndreas Gohr    /**
43308ace392SAndreas Gohr     * Encode a password for transmission over LDAP
43408ace392SAndreas Gohr     *
43508ace392SAndreas Gohr     * Passwords are encoded as UTF-16LE strings encapsulated in quotes.
43608ace392SAndreas Gohr     *
43708ace392SAndreas Gohr     * @param string $password The password to encode
43808ace392SAndreas Gohr     * @return string
43908ace392SAndreas Gohr     */
44008ace392SAndreas Gohr    protected function encodePassword($password)
44108ace392SAndreas Gohr    {
44208ace392SAndreas Gohr        $password = "\"" . $password . "\"";
44308ace392SAndreas Gohr
44408ace392SAndreas Gohr        if (function_exists('iconv')) {
44508ace392SAndreas Gohr            $adpassword = iconv('UTF-8', 'UTF-16LE', $password);
44608ace392SAndreas Gohr        } elseif (function_exists('mb_convert_encoding')) {
44708ace392SAndreas Gohr            $adpassword = mb_convert_encoding($password, "UTF-16LE", "UTF-8");
44808ace392SAndreas Gohr        } else {
44908ace392SAndreas Gohr            // this will only work for ASCII7 passwords
45008ace392SAndreas Gohr            $adpassword = '';
45108ace392SAndreas Gohr            for ($i = 0; $i < strlen($password); $i++) {
45208ace392SAndreas Gohr                $adpassword .= "$password[$i]\000";
45308ace392SAndreas Gohr            }
45408ace392SAndreas Gohr        }
45508ace392SAndreas Gohr        return $adpassword;
45608ace392SAndreas Gohr    }
4571078ec26SAndreas Gohr}
458