Completed
Push — remoteapiGetversions ( b2f4ab...f28a57 )
by Gerrit
12:56 queued 07:54
created

auth_plugin_authplain::modifyUser()   D

Complexity

Conditions 9
Paths 11

Size

Total Lines 41
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 41
rs 4.909
cc 9
eloc 24
nc 11
nop 2
1
<?php
2
// must be run within Dokuwiki
3
if(!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 <[email protected]>
10
 * @author     Chris Smith <[email protected]>
11
 * @author     Jan Schumann <[email protected]>
12
 */
13
class 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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
123
     * @author  Chris Smith <[email protected]>
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;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $pwd; (string) is incompatible with the return type of the parent method DokuWiki_Auth_Plugin::createUser of type boolean|null.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
157
    }
158
159
    /**
160
     * Modify user data
161
     *
162
     * @author  Chris Smith <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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