Failed Conditions
Push — psr2 ( 64159a )
by Andreas
04:14
created

auth_plugin_authplain::retrieveUsers()   C

Complexity

Conditions 7
Paths 10

Size

Total Lines 24
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

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