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