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