Completed
Push — extcli ( 2b0eb0...d915fa )
by Andreas
07:55 queued 04:48
created

auth_plugin_authplain   F

Complexity

Total Complexity 70

Size/Duplication

Total Lines 455
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 0
Metric Value
dl 0
loc 455
rs 2.8
c 0
b 0
f 0
wmc 70
lcom 1
cbo 1

17 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 23 3
A checkPass() 0 6 2
A getUserData() 0 4 3
A _createUserLine() 0 8 1
A createUser() 0 26 4
B modifyUser() 0 41 9
B deleteUsers() 0 31 9
A getUserCount() 0 15 4
B retrieveUsers() 0 24 7
A retrieveGroups() 0 14 4
A cleanUser() 0 4 1
A cleanGroup() 0 4 1
A _loadUserData() 0 14 3
A _readUserFile() 0 23 4
B _splitUserData() 0 26 6
B _filter() 0 12 7
A _constructPattern() 0 6 2

How to fix   Complexity   

Complex Class

Complex classes like auth_plugin_authplain often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use auth_plugin_authplain, and based on these observations, apply Extract Interface, too.

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