xref: /dokuwiki/lib/plugins/authplain/auth.php (revision 45cb016dfe855693f366d498806620e09e4ab257)
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) {
138            msg($this->getLang('userexists'), -1);
139            return false;
140        }
141
142        $pass = auth_cryptPassword($pwd);
143
144        // set default group if no groups specified
145        if(!is_array($grps)) $grps = array($conf['defaultgroup']);
146
147        // prepare user line
148        $userline = $this->_createUserLine($user, $pass, $name, $mail, $grps);
149
150        if(!io_saveFile($config_cascade['plainauth.users']['default'], $userline, true)) {
151            msg($this->getLang('writefail'), -1);
152            return null;
153        }
154
155        $this->users[$user] = compact('pass', 'name', 'mail', 'grps');
156        return $pwd;
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) {
173            msg($this->getLang('usernotexists'), -1);
174            return false;
175        }
176
177        // don't modify protected users
178        if(!empty($userinfo['protected'])) {
179            msg(sprintf($this->getLang('protected'), hsc($user)), -1);
180            return false;
181        }
182
183        if(!is_array($changes) || !count($changes)) return true;
184
185        // update userinfo with new data, remembering to encrypt any password
186        $newuser = $user;
187        foreach($changes as $field => $value) {
188            if($field == 'user') {
189                $newuser = $value;
190                continue;
191            }
192            if($field == 'pass') $value = auth_cryptPassword($value);
193            $userinfo[$field] = $value;
194        }
195
196        $userline = $this->_createUserLine($newuser, $userinfo['pass'], $userinfo['name'], $userinfo['mail'], $userinfo['grps']);
197
198        if(!io_replaceInFile($config_cascade['plainauth.users']['default'], '/^'.$user.':/', $userline, true)) {
199            msg('There was an error modifying your user data. You may need to register again.', -1);
200            // FIXME, io functions should be fail-safe so existing data isn't lost
201            $ACT = 'register';
202            return false;
203        }
204
205        $this->users[$newuser] = $userinfo;
206        return true;
207    }
208
209    /**
210     * Remove one or more users from the list of registered users
211     *
212     * @author  Christopher Smith <chris@jalakai.co.uk>
213     * @param   array  $users   array of users to be deleted
214     * @return  int             the number of users deleted
215     */
216    public function deleteUsers($users) {
217        global $config_cascade;
218
219        if(!is_array($users) || empty($users)) return 0;
220
221        if($this->users === null) $this->_loadUserData();
222
223        $deleted = array();
224        foreach($users as $user) {
225            // don't delete protected users
226            if(!empty($this->users[$user]['protected'])) {
227                msg(sprintf($this->getLang('protected'), hsc($user)), -1);
228                continue;
229            }
230            if(isset($this->users[$user])) $deleted[] = preg_quote($user, '/');
231        }
232
233        if(empty($deleted)) return 0;
234
235        $pattern = '/^('.join('|', $deleted).'):/';
236        if (!io_deleteFromFile($config_cascade['plainauth.users']['default'], $pattern, true)) {
237            msg($this->getLang('writefail'), -1);
238            return 0;
239        }
240
241        // reload the user list and count the difference
242        $count = count($this->users);
243        $this->_loadUserData();
244        $count -= count($this->users);
245        return $count;
246    }
247
248    /**
249     * Return a count of the number of user which meet $filter criteria
250     *
251     * @author  Chris Smith <chris@jalakai.co.uk>
252     *
253     * @param array $filter
254     * @return int
255     */
256    public function getUserCount($filter = array()) {
257
258        if($this->users === null) $this->_loadUserData();
259
260        if(!count($filter)) return count($this->users);
261
262        $count = 0;
263        $this->_constructPattern($filter);
264
265        foreach($this->users as $user => $info) {
266            $count += $this->_filter($user, $info);
267        }
268
269        return $count;
270    }
271
272    /**
273     * Bulk retrieval of user data
274     *
275     * @author  Chris Smith <chris@jalakai.co.uk>
276     *
277     * @param   int   $start index of first user to be returned
278     * @param   int   $limit max number of users to be returned
279     * @param   array $filter array of field/pattern pairs
280     * @return  array userinfo (refer getUserData for internal userinfo details)
281     */
282    public function retrieveUsers($start = 0, $limit = 0, $filter = array()) {
283
284        if($this->users === null) $this->_loadUserData();
285
286        ksort($this->users);
287
288        $i     = 0;
289        $count = 0;
290        $out   = array();
291        $this->_constructPattern($filter);
292
293        foreach($this->users as $user => $info) {
294            if($this->_filter($user, $info)) {
295                if($i >= $start) {
296                    $out[$user] = $info;
297                    $count++;
298                    if(($limit > 0) && ($count >= $limit)) break;
299                }
300                $i++;
301            }
302        }
303
304        return $out;
305    }
306
307    /**
308     * Only valid pageid's (no namespaces) for usernames
309     *
310     * @param string $user
311     * @return string
312     */
313    public function cleanUser($user) {
314        global $conf;
315        return cleanID(str_replace(':', $conf['sepchar'], $user));
316    }
317
318    /**
319     * Only valid pageid's (no namespaces) for groupnames
320     *
321     * @param string $group
322     * @return string
323     */
324    public function cleanGroup($group) {
325        global $conf;
326        return cleanID(str_replace(':', $conf['sepchar'], $group));
327    }
328
329    /**
330     * Load all user data
331     *
332     * loads the user file into a datastructure
333     *
334     * @author  Andreas Gohr <andi@splitbrain.org>
335     */
336    protected function _loadUserData() {
337        global $config_cascade;
338
339        $this->users = $this->_readUserFile($config_cascade['plainauth.users']['default']);
340
341        // support protected users
342        if(!empty($config_cascade['plainauth.users']['protected'])) {
343            $protected = $this->_readUserFile($config_cascade['plainauth.users']['protected']);
344            foreach(array_keys($protected) as $key) {
345                $protected[$key]['protected'] = true;
346            }
347            $this->users = array_merge($this->users, $protected);
348        }
349    }
350
351    /**
352     * Read user data from given file
353     *
354     * ignores non existing files
355     *
356     * @param string $file the file to load data from
357     * @return array
358     */
359    protected function _readUserFile($file) {
360        $users = array();
361        if(!file_exists($file)) return $users;
362
363        $lines = file($file);
364        foreach($lines as $line) {
365            $line = preg_replace('/#.*$/', '', $line); //ignore comments
366            $line = trim($line);
367            if(empty($line)) continue;
368
369            /* NB: preg_split can be deprecated/replaced with str_getcsv once dokuwiki is min php 5.3 */
370            $row = $this->_splitUserData($line);
371            $row = str_replace('\\:', ':', $row);
372            $row = str_replace('\\\\', '\\', $row);
373
374            $groups = array_values(array_filter(explode(",", $row[4])));
375
376            $users[$row[0]]['pass'] = $row[1];
377            $users[$row[0]]['name'] = urldecode($row[2]);
378            $users[$row[0]]['mail'] = $row[3];
379            $users[$row[0]]['grps'] = $groups;
380        }
381        return $users;
382    }
383
384    protected function _splitUserData($line){
385        // due to a bug in PCRE 6.6, preg_split will fail with the regex we use here
386        // refer github issues 877 & 885
387        if ($this->_pregsplit_safe){
388            return preg_split('/(?<![^\\\\]\\\\)\:/', $line, 5);       // allow for : escaped as \:
389        }
390
391        $row = array();
392        $piece = '';
393        $len = strlen($line);
394        for($i=0; $i<$len; $i++){
395            if ($line[$i]=='\\'){
396                $piece .= $line[$i];
397                $i++;
398                if ($i>=$len) break;
399            } else if ($line[$i]==':'){
400                $row[] = $piece;
401                $piece = '';
402                continue;
403            }
404            $piece .= $line[$i];
405        }
406        $row[] = $piece;
407
408        return $row;
409    }
410
411    /**
412     * return true if $user + $info match $filter criteria, false otherwise
413     *
414     * @author   Chris Smith <chris@jalakai.co.uk>
415     *
416     * @param string $user User login
417     * @param array  $info User's userinfo array
418     * @return bool
419     */
420    protected function _filter($user, $info) {
421        foreach($this->_pattern as $item => $pattern) {
422            if($item == 'user') {
423                if(!preg_match($pattern, $user)) return false;
424            } else if($item == 'grps') {
425                if(!count(preg_grep($pattern, $info['grps']))) return false;
426            } else {
427                if(!preg_match($pattern, $info[$item])) return false;
428            }
429        }
430        return true;
431    }
432
433    /**
434     * construct a filter pattern
435     *
436     * @param array $filter
437     */
438    protected function _constructPattern($filter) {
439        $this->_pattern = array();
440        foreach($filter as $item => $pattern) {
441            $this->_pattern[$item] = '/'.str_replace('/', '\/', $pattern).'/i'; // allow regex characters
442        }
443    }
444}
445