1<?php
2
3use dokuwiki\Extension\AuthPlugin;
4use dokuwiki\plugin\pureldap\classes\ADClient;
5use dokuwiki\plugin\pureldap\classes\Client;
6
7/**
8 * DokuWiki Plugin pureldap (Auth Component)
9 *
10 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
11 * @author  Andreas Gohr <andi@splitbrain.org>
12 */
13class auth_plugin_pureldap extends AuthPlugin
14{
15    /** @var Client */
16    public $client;
17
18    /**
19     * Constructor.
20     */
21    public function __construct()
22    {
23        global $conf;
24        parent::__construct(); // for compatibility
25
26        // prepare the base client
27        $this->loadConfig();
28        $this->conf['admin_password'] = conf_decodeString($this->conf['admin_password']);
29        $this->conf['defaultgroup'] = $conf['defaultgroup'];
30
31        $this->client = new ADClient($this->conf); // FIXME decide class on config
32
33        // set capabilities
34        $this->cando['getUsers'] = true;
35        $this->cando['getGroups'] = true;
36        $this->cando['logout'] = !$this->client->getConf('sso');
37        if ($this->client->getConf('encryption') !== 'none') {
38            // with encryption passwords can be changed
39            // for resetting passwords a privileged user is needed
40            $this->cando['modPass'] = true;
41        }
42
43
44        $this->success = true;
45    }
46
47    /** @inheritDoc */
48    public function checkPass($user, $pass)
49    {
50        global $INPUT;
51
52        // when SSO is enabled, the login is autotriggered and we simply trust the environment
53        if (
54            $this->client->getConf('sso') &&
55            $INPUT->server->str('REMOTE_USER') !== '' &&
56            $INPUT->server->str('REMOTE_USER') == $user
57        ) {
58            return true;
59        }
60
61        // try to bind with the user credentials, client will stay authenticated as user
62        $this->client = new ADClient($this->conf); // FIXME decide class on config
63        try {
64            $this->client->authenticate($user, $pass);
65            return true;
66        } catch (\Exception $e) {
67            $this->parseErrorCodesToMessages($e);
68            return false;
69        }
70    }
71
72    /** @inheritDoc */
73    public function getUserData($user, $requireGroups = true)
74    {
75        $info = $this->client->getCachedUser($user, $requireGroups);
76        return $info ?: false;
77    }
78
79    /**
80     * @inheritDoc
81     */
82    public function retrieveUsers($start = 0, $limit = 0, $filter = null)
83    {
84        return array_slice(
85            $this->client->getFilteredUsers(
86                $filter,
87                Client::FILTER_CONTAINS
88            ),
89            $start,
90            $limit
91        );
92    }
93
94    /** @inheritDoc */
95    public function retrieveGroups($start = 0, $limit = 0)
96    {
97        return array_slice($this->client->getCachedGroups(), $start, $limit);
98    }
99
100    /** @inheritDoc */
101    public function isCaseSensitive()
102    {
103        return false;
104    }
105
106    /** @inheritDoc */
107    public function cleanUser($user)
108    {
109        return $this->client->cleanUser($user);
110    }
111
112    /** @inheritDoc */
113    public function cleanGroup($group)
114    {
115        return $group;
116    }
117
118    /** @inheritDoc */
119    public function useSessionCache($user)
120    {
121        return true;
122    }
123
124    /**
125     * Support password changing
126     * @inheritDoc
127     */
128    public function modifyUser($user, $changes)
129    {
130        if (empty($changes['pass'])) {
131            $this->client->error('Only password changes are supported', __FILE__, __LINE__);
132            return false;
133        }
134
135        global $INPUT;
136        return $this->client->setPassword($user, $changes['pass'], $INPUT->str('oldpass', null, true));
137    }
138
139    /**
140     * Parse error codes from LDAP exceptions and output them as user-friendly messages.
141     *
142     * This is currently tailored for Active Directory bind errors.
143     *
144     * @param Exception $e
145     * @return void
146     */
147    public function parseErrorCodesToMessages(\Exception $e)
148    {
149        // See https://ldapwiki.com/wiki/Wiki.jsp?page=Common%20Active%20Directory%20Bind%20Errors
150        $bind_errors = [
151            '52f' => 'ERROR_ACCOUNT_RESTRICTION',
152            '530' => 'ERROR_INVALID_LOGON_HOURS',
153            '531' => 'ERROR_INVALID_WORKSTATION',
154            '532' => 'ERROR_PASSWORD_EXPIRED',
155            '533' => 'ERROR_ACCOUNT_DISABLED',
156            '701' => 'ERROR_ACCOUNT_EXPIRED',
157            '773' => 'ERROR_PASSWORD_MUST_CHANGE',
158        ];
159
160        if (
161            $e instanceof \FreeDSx\Ldap\Exception\BindException &&
162            $e->getCode() === 49 &&
163            preg_match('/ data ([0-9a-f]{3})/', $e->getMessage(), $matches)
164        ) {
165            $code = $matches[1];
166            if (isset($bind_errors[$code])) {
167                $message = $this->getLang($bind_errors[$code]) ?: $bind_errors[$code];
168
169                // on password expired or must change, add reset hint
170                if ($this->canDo('modPass') && ($code == 532 || $code == 773)) {
171                    $link = '<a href="' . wl('start', ['do' => 'resendpwd']) . '" class="pureldap-reset-link">' .
172                        $this->getLang('pass_reset') . '</a>';
173                    $message .= ' ' . $link;
174                }
175
176                msg($message, -1);
177            }
178        }
179    }
180}
181