xref: /dokuwiki/lib/plugins/authplain/auth.php (revision 5af3d1cd434f85882bb29253a3757cacc5c5338a)
1<?php
2// must be run within Dokuwiki
3if(!defined('DOKU_INC')) die();
4
5/**
6 * Plaintext authentication backend
7 *
8 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
9 * @author     Andreas Gohr <andi@splitbrain.org>
10 * @author     Chris Smith <chris@jalakai.co.uk>
11 * @author     Jan Schumann <js@schumann-it.com>
12 */
13class auth_plugin_authplain extends DokuWiki_Auth_Plugin {
14    /** @var array user cache */
15    protected $users = null;
16
17    /** @var array filter pattern */
18    protected $_pattern = array();
19
20    /** @var bool safe version of preg_split */
21    protected $_pregsplit_safe = false;
22
23    /**
24     * Constructor
25     *
26     * Carry out sanity checks to ensure the object is
27     * able to operate. Set capabilities.
28     *
29     * @author  Christopher Smith <chris@jalakai.co.uk>
30     */
31    public function __construct() {
32        parent::__construct();
33        global $config_cascade;
34
35        if(!@is_readable($config_cascade['plainauth.users']['default'])) {
36            $this->success = false;
37        } else {
38            if(@is_writable($config_cascade['plainauth.users']['default'])) {
39                $this->cando['addUser']   = true;
40                $this->cando['delUser']   = true;
41                $this->cando['modLogin']  = true;
42                $this->cando['modPass']   = true;
43                $this->cando['modName']   = true;
44                $this->cando['modMail']   = true;
45                $this->cando['modGroups'] = true;
46            }
47            $this->cando['getUsers']     = true;
48            $this->cando['getUserCount'] = true;
49        }
50
51        $this->_pregsplit_safe = version_compare(PCRE_VERSION,'6.7','>=');
52    }
53
54    /**
55     * Check user+password
56     *
57     * Checks if the given user exists and the given
58     * plaintext password is correct
59     *
60     * @author  Andreas Gohr <andi@splitbrain.org>
61     * @param string $user
62     * @param string $pass
63     * @return  bool
64     */
65    public function checkPass($user, $pass) {
66        $userinfo = $this->getUserData($user);
67        if($userinfo === false) return false;
68
69        return auth_verifyPassword($pass, $this->users[$user]['pass']);
70    }
71
72    /**
73     * Return user info
74     *
75     * Returns info about the given user needs to contain
76     * at least these fields:
77     *
78     * name string  full name of the user
79     * mail string  email addres of the user
80     * grps array   list of groups the user is in
81     *
82     * @author  Andreas Gohr <andi@splitbrain.org>
83     * @param string $user
84     * @param bool $requireGroups  (optional) ignored by this plugin, grps info always supplied
85     * @return array|false
86     */
87    public function getUserData($user, $requireGroups=true) {
88        if($this->users === null) $this->_loadUserData();
89        return isset($this->users[$user]) ? $this->users[$user] : false;
90    }
91
92    /**
93     * Creates a string suitable for saving as a line
94     * in the file database
95     * (delimiters escaped, etc.)
96     *
97     * @param string $user
98     * @param string $pass
99     * @param string $name
100     * @param string $mail
101     * @param array  $grps list of groups the user is in
102     * @return string
103     */
104    protected function _createUserLine($user, $pass, $name, $mail, $grps) {
105        $groups   = join(',', $grps);
106        $userline = array($user, $pass, $name, $mail, $groups);
107        $userline = str_replace('\\', '\\\\', $userline); // escape \ as \\
108        $userline = str_replace(':', '\\:', $userline); // escape : as \:
109        $userline = join(':', $userline)."\n";
110        return $userline;
111    }
112
113    /**
114     * Create a new User
115     *
116     * Returns false if the user already exists, null when an error
117     * occurred and true if everything went well.
118     *
119     * The new user will be added to the default group by this
120     * function if grps are not specified (default behaviour).
121     *
122     * @author  Andreas Gohr <andi@splitbrain.org>
123     * @author  Chris Smith <chris@jalakai.co.uk>
124     *
125     * @param string $user
126     * @param string $pwd
127     * @param string $name
128     * @param string $mail
129     * @param array  $grps
130     * @return bool|null|string
131     */
132    public function createUser($user, $pwd, $name, $mail, $grps = null) {
133        global $conf;
134        global $config_cascade;
135
136        // user mustn't already exist
137        if($this->getUserData($user) !== false) return false;
138
139        $pass = auth_cryptPassword($pwd);
140
141        // set default group if no groups specified
142        if(!is_array($grps)) $grps = array($conf['defaultgroup']);
143
144        // prepare user line
145        $userline = $this->_createUserLine($user, $pass, $name, $mail, $grps);
146
147        if(io_saveFile($config_cascade['plainauth.users']['default'], $userline, true)) {
148            $this->users[$user] = compact('pass', 'name', 'mail', 'grps');
149            return $pwd;
150        }
151
152        msg(
153            'The '.$config_cascade['plainauth.users']['default'].
154                ' file is not writable. Please inform the Wiki-Admin', -1
155        );
156        return null;
157    }
158
159    /**
160     * Modify user data
161     *
162     * @author  Chris Smith <chris@jalakai.co.uk>
163     * @param   string $user      nick of the user to be changed
164     * @param   array  $changes   array of field/value pairs to be changed (password will be clear text)
165     * @return  bool
166     */
167    public function modifyUser($user, $changes) {
168        global $ACT;
169        global $config_cascade;
170
171        // sanity checks, user must already exist and there must be something to change
172        if(($userinfo = $this->getUserData($user)) === false) return false;
173        if(!is_array($changes) || !count($changes)) return true;
174
175        // update userinfo with new data, remembering to encrypt any password
176        $newuser = $user;
177        foreach($changes as $field => $value) {
178            if($field == 'user') {
179                $newuser = $value;
180                continue;
181            }
182            if($field == 'pass') $value = auth_cryptPassword($value);
183            $userinfo[$field] = $value;
184        }
185
186        $userline = $this->_createUserLine($newuser, $userinfo['pass'], $userinfo['name'], $userinfo['mail'], $userinfo['grps']);
187
188        if(!$this->deleteUsers(array($user))) {
189            msg('Unable to modify user data. Please inform the Wiki-Admin', -1);
190            return false;
191        }
192
193        if(!io_saveFile($config_cascade['plainauth.users']['default'], $userline, true)) {
194            msg('There was an error modifying your user data. You should register again.', -1);
195            // FIXME, user has been deleted but not recreated, should force a logout and redirect to login page
196            $ACT = 'register';
197            return false;
198        }
199
200        $this->users[$newuser] = $userinfo;
201        return true;
202    }
203
204    /**
205     * Remove one or more users from the list of registered users
206     *
207     * @author  Christopher Smith <chris@jalakai.co.uk>
208     * @param   array  $users   array of users to be deleted
209     * @return  int             the number of users deleted
210     */
211    public function deleteUsers($users) {
212        global $config_cascade;
213
214        if(!is_array($users) || empty($users)) return 0;
215
216        if($this->users === null) $this->_loadUserData();
217
218        $deleted = array();
219        foreach($users as $user) {
220            if(isset($this->users[$user])) $deleted[] = preg_quote($user, '/');
221        }
222
223        if(empty($deleted)) return 0;
224
225        $pattern = '/^('.join('|', $deleted).'):/';
226        io_deleteFromFile($config_cascade['plainauth.users']['default'], $pattern, true);
227
228        // reload the user list and count the difference
229        $count = count($this->users);
230        $this->_loadUserData();
231        $count -= count($this->users);
232        return $count;
233    }
234
235    /**
236     * Return a count of the number of user which meet $filter criteria
237     *
238     * @author  Chris Smith <chris@jalakai.co.uk>
239     *
240     * @param array $filter
241     * @return int
242     */
243    public function getUserCount($filter = array()) {
244
245        if($this->users === null) $this->_loadUserData();
246
247        if(!count($filter)) return count($this->users);
248
249        $count = 0;
250        $this->_constructPattern($filter);
251
252        foreach($this->users as $user => $info) {
253            $count += $this->_filter($user, $info);
254        }
255
256        return $count;
257    }
258
259    /**
260     * Bulk retrieval of user data
261     *
262     * @author  Chris Smith <chris@jalakai.co.uk>
263     *
264     * @param   int   $start index of first user to be returned
265     * @param   int   $limit max number of users to be returned
266     * @param   array $filter array of field/pattern pairs
267     * @return  array userinfo (refer getUserData for internal userinfo details)
268     */
269    public function retrieveUsers($start = 0, $limit = 0, $filter = array()) {
270
271        if($this->users === null) $this->_loadUserData();
272
273        ksort($this->users);
274
275        $i     = 0;
276        $count = 0;
277        $out   = array();
278        $this->_constructPattern($filter);
279
280        foreach($this->users as $user => $info) {
281            if($this->_filter($user, $info)) {
282                if($i >= $start) {
283                    $out[$user] = $info;
284                    $count++;
285                    if(($limit > 0) && ($count >= $limit)) break;
286                }
287                $i++;
288            }
289        }
290
291        return $out;
292    }
293
294    /**
295     * Only valid pageid's (no namespaces) for usernames
296     *
297     * @param string $user
298     * @return string
299     */
300    public function cleanUser($user) {
301        global $conf;
302        return cleanID(str_replace(':', $conf['sepchar'], $user));
303    }
304
305    /**
306     * Only valid pageid's (no namespaces) for groupnames
307     *
308     * @param string $group
309     * @return string
310     */
311    public function cleanGroup($group) {
312        global $conf;
313        return cleanID(str_replace(':', $conf['sepchar'], $group));
314    }
315
316    /**
317     * Load all user data
318     *
319     * loads the user file into a datastructure
320     *
321     * @author  Andreas Gohr <andi@splitbrain.org>
322     */
323    protected function _loadUserData() {
324        global $config_cascade;
325
326        $this->users = array();
327
328        if(!file_exists($config_cascade['plainauth.users']['default'])) return;
329
330        $lines = file($config_cascade['plainauth.users']['default']);
331        foreach($lines as $line) {
332            $line = preg_replace('/#.*$/', '', $line); //ignore comments
333            $line = trim($line);
334            if(empty($line)) continue;
335
336            /* NB: preg_split can be deprecated/replaced with str_getcsv once dokuwiki is min php 5.3 */
337            $row = $this->_splitUserData($line);
338            $row = str_replace('\\:', ':', $row);
339            $row = str_replace('\\\\', '\\', $row);
340
341            $groups = array_values(array_filter(explode(",", $row[4])));
342
343            $this->users[$row[0]]['pass'] = $row[1];
344            $this->users[$row[0]]['name'] = urldecode($row[2]);
345            $this->users[$row[0]]['mail'] = $row[3];
346            $this->users[$row[0]]['grps'] = $groups;
347        }
348    }
349
350    protected function _splitUserData($line){
351        // due to a bug in PCRE 6.6, preg_split will fail with the regex we use here
352        // refer github issues 877 & 885
353        if ($this->_pregsplit_safe){
354            return preg_split('/(?<![^\\\\]\\\\)\:/', $line, 5);       // allow for : escaped as \:
355        }
356
357        $row = array();
358        $piece = '';
359        $len = strlen($line);
360        for($i=0; $i<$len; $i++){
361            if ($line[$i]=='\\'){
362                $piece .= $line[$i];
363                $i++;
364                if ($i>=$len) break;
365            } else if ($line[$i]==':'){
366                $row[] = $piece;
367                $piece = '';
368                continue;
369            }
370            $piece .= $line[$i];
371        }
372        $row[] = $piece;
373
374        return $row;
375    }
376
377    /**
378     * return true if $user + $info match $filter criteria, false otherwise
379     *
380     * @author   Chris Smith <chris@jalakai.co.uk>
381     *
382     * @param string $user User login
383     * @param array  $info User's userinfo array
384     * @return bool
385     */
386    protected function _filter($user, $info) {
387        foreach($this->_pattern as $item => $pattern) {
388            if($item == 'user') {
389                if(!preg_match($pattern, $user)) return false;
390            } else if($item == 'grps') {
391                if(!count(preg_grep($pattern, $info['grps']))) return false;
392            } else {
393                if(!preg_match($pattern, $info[$item])) return false;
394            }
395        }
396        return true;
397    }
398
399    /**
400     * construct a filter pattern
401     *
402     * @param array $filter
403     */
404    protected function _constructPattern($filter) {
405        $this->_pattern = array();
406        foreach($filter as $item => $pattern) {
407            $this->_pattern[$item] = '/'.str_replace('/', '\/', $pattern).'/i'; // allow regex characters
408        }
409    }
410}