Issues (847)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

lib/plugins/usermanager/admin.php (3 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/*
3
 *  User Manager
4
 *
5
 *  Dokuwiki Admin Plugin
6
 *
7
 *  This version of the user manager has been modified to only work with
8
 *  objectified version of auth system
9
 *
10
 *  @author  neolao <[email protected]>
11
 *  @author  Chris Smith <[email protected]>
12
 */
13
14
/**
15
 * All DokuWiki plugins to extend the admin function
16
 * need to inherit from this class
17
 */
18
class admin_plugin_usermanager extends DokuWiki_Admin_Plugin
19
{
20
    const IMAGE_DIR = DOKU_BASE.'lib/plugins/usermanager/images/';
21
22
    protected $auth = null;        // auth object
23
    protected $users_total = 0;     // number of registered users
24
    protected $filter = array();   // user selection filter(s)
25
    protected $start = 0;          // index of first user to be displayed
26
    protected $last = 0;           // index of the last user to be displayed
27
    protected $pagesize = 20;      // number of users to list on one page
28
    protected $edit_user = '';     // set to user selected for editing
29
    protected $edit_userdata = array();
30
    protected $disabled = '';      // if disabled set to explanatory string
31
    protected $import_failures = array();
32
    protected $lastdisabled = false; // set to true if last user is unknown and last button is hence buggy
33
34
    /**
35
     * Constructor
36
     */
37
    public function __construct()
38
    {
39
        /** @var DokuWiki_Auth_Plugin $auth */
40
        global $auth;
41
42
        $this->setupLocale();
43
44
        if (!isset($auth)) {
45
            $this->disabled = $this->lang['noauth'];
46
        } elseif (!$auth->canDo('getUsers')) {
47
            $this->disabled = $this->lang['nosupport'];
48
        } else {
49
            // we're good to go
50
            $this->auth = & $auth;
51
        }
52
53
        // attempt to retrieve any import failures from the session
54
        if (!empty($_SESSION['import_failures'])) {
55
            $this->import_failures = $_SESSION['import_failures'];
56
        }
57
    }
58
59
    /**
60
     * Return prompt for admin menu
61
     *
62
     * @param string $language
63
     * @return string
64
     */
65
    public function getMenuText($language)
66
    {
67
68
        if (!is_null($this->auth))
69
          return parent::getMenuText($language);
70
71
        return $this->getLang('menu').' '.$this->disabled;
72
    }
73
74
    /**
75
     * return sort order for position in admin menu
76
     *
77
     * @return int
78
     */
79
    public function getMenuSort()
80
    {
81
        return 2;
82
    }
83
84
    /**
85
     * @return int current start value for pageination
86
     */
87
    public function getStart()
88
    {
89
        return $this->start;
90
    }
91
92
    /**
93
     * @return int number of users per page
94
     */
95
    public function getPagesize()
96
    {
97
        return $this->pagesize;
98
    }
99
100
    /**
101
     * @param boolean $lastdisabled
102
     */
103
    public function setLastdisabled($lastdisabled)
104
    {
105
        $this->lastdisabled = $lastdisabled;
106
    }
107
108
    /**
109
     * Handle user request
110
     *
111
     * @return bool
112
     */
113
    public function handle()
114
    {
115
        global $INPUT;
116
        if (is_null($this->auth)) return false;
117
118
        // extract the command and any specific parameters
119
        // submit button name is of the form - fn[cmd][param(s)]
120
        $fn   = $INPUT->param('fn');
121
122
        if (is_array($fn)) {
123
            $cmd = key($fn);
124
            $param = is_array($fn[$cmd]) ? key($fn[$cmd]) : null;
125
        } else {
126
            $cmd = $fn;
127
            $param = null;
128
        }
129
130
        if ($cmd != "search") {
131
            $this->start = $INPUT->int('start', 0);
132
            $this->filter = $this->retrieveFilter();
133
        }
134
135
        switch ($cmd) {
136
            case "add":
137
                $this->addUser();
138
                break;
139
            case "delete":
140
                $this->deleteUser();
141
                break;
142
            case "modify":
143
                $this->modifyUser();
144
                break;
145
            case "edit":
146
                $this->editUser($param);
147
                break;
148
            case "search":
149
                $this->setFilter($param);
150
                            $this->start = 0;
151
                break;
152
            case "export":
153
                $this->exportCSV();
154
                break;
155
            case "import":
156
                $this->importCSV();
157
                break;
158
            case "importfails":
159
                $this->downloadImportFailures();
160
                break;
161
        }
162
163
        $this->users_total = $this->auth->canDo('getUserCount') ? $this->auth->getUserCount($this->filter) : -1;
164
165
        // page handling
166
        switch ($cmd) {
167
            case 'start':
168
                $this->start = 0;
169
                break;
170
            case 'prev':
171
                $this->start -= $this->pagesize;
172
                break;
173
            case 'next':
174
                $this->start += $this->pagesize;
175
                break;
176
            case 'last':
177
                $this->start = $this->users_total;
178
                break;
179
        }
180
        $this->validatePagination();
181
        return true;
182
    }
183
184
    /**
185
     * Output appropriate html
186
     *
187
     * @return bool
188
     */
189
    public function html()
190
    {
191
        global $ID;
192
193
        if (is_null($this->auth)) {
194
            print $this->lang['badauth'];
195
            return false;
196
        }
197
198
        $user_list = $this->auth->retrieveUsers($this->start, $this->pagesize, $this->filter);
199
200
        $page_buttons = $this->pagination();
201
        $delete_disable = $this->auth->canDo('delUser') ? '' : 'disabled="disabled"';
202
203
        $editable = $this->auth->canDo('UserMod');
204
        $export_label = empty($this->filter) ? $this->lang['export_all'] : $this->lang['export_filtered'];
205
206
        print $this->locale_xhtml('intro');
207
        print $this->locale_xhtml('list');
208
209
        ptln("<div id=\"user__manager\">");
210
        ptln("<div class=\"level2\">");
211
212
        if ($this->users_total > 0) {
213
            ptln(
214
                "<p>" . sprintf(
215
                    $this->lang['summary'],
216
                    $this->start + 1,
217
                    $this->last,
218
                    $this->users_total,
219
                    $this->auth->getUserCount()
220
                ) . "</p>"
221
            );
222
        } else {
223
            if ($this->users_total < 0) {
224
                $allUserTotal = 0;
225
            } else {
226
                $allUserTotal = $this->auth->getUserCount();
227
            }
228
            ptln("<p>".sprintf($this->lang['nonefound'], $allUserTotal)."</p>");
229
        }
230
        ptln("<form action=\"".wl($ID)."\" method=\"post\">");
231
        formSecurityToken();
232
        ptln("  <div class=\"table\">");
233
        ptln("  <table class=\"inline\">");
234
        ptln("    <thead>");
235
        ptln("      <tr>");
236
        ptln("        <th>&#160;</th>
237
            <th>".$this->lang["user_id"]."</th>
238
            <th>".$this->lang["user_name"]."</th>
239
            <th>".$this->lang["user_mail"]."</th>
240
            <th>".$this->lang["user_groups"]."</th>");
241
        ptln("      </tr>");
242
243
        ptln("      <tr>");
244
        ptln("        <td class=\"rightalign\"><input type=\"image\" src=\"".
245
             self::IMAGE_DIR."search.png\" name=\"fn[search][new]\" title=\"".
246
             $this->lang['search_prompt']."\" alt=\"".$this->lang['search']."\" class=\"button\" /></td>");
247
        ptln("        <td><input type=\"text\" name=\"userid\" class=\"edit\" value=\"".
248
             $this->htmlFilter('user')."\" /></td>");
249
        ptln("        <td><input type=\"text\" name=\"username\" class=\"edit\" value=\"".
250
             $this->htmlFilter('name')."\" /></td>");
251
        ptln("        <td><input type=\"text\" name=\"usermail\" class=\"edit\" value=\"".
252
             $this->htmlFilter('mail')."\" /></td>");
253
        ptln("        <td><input type=\"text\" name=\"usergroups\" class=\"edit\" value=\"".
254
             $this->htmlFilter('grps')."\" /></td>");
255
        ptln("      </tr>");
256
        ptln("    </thead>");
257
258
        if ($this->users_total) {
259
            ptln("    <tbody>");
260
            foreach ($user_list as $user => $userinfo) {
261
                extract($userinfo);
262
                /**
263
                 * @var string $name
264
                 * @var string $pass
265
                 * @var string $mail
266
                 * @var array  $grps
267
                 */
268
                $groups = join(', ', $grps);
269
                ptln("    <tr class=\"user_info\">");
270
                ptln("      <td class=\"centeralign\"><input type=\"checkbox\" name=\"delete[".hsc($user).
271
                     "]\" ".$delete_disable." /></td>");
272
                if ($editable) {
273
                    ptln("    <td><a href=\"".wl($ID, array('fn[edit]['.$user.']' => 1,
274
                                                           'do' => 'admin',
275
                                                           'page' => 'usermanager',
276
                                                           'sectok' => getSecurityToken())).
277
                         "\" title=\"".$this->lang['edit_prompt']."\">".hsc($user)."</a></td>");
278
                } else {
279
                    ptln("    <td>".hsc($user)."</td>");
280
                }
281
                ptln("      <td>".hsc($name)."</td><td>".hsc($mail)."</td><td>".hsc($groups)."</td>");
282
                ptln("    </tr>");
283
            }
284
            ptln("    </tbody>");
285
        }
286
287
        ptln("    <tbody>");
288
        ptln("      <tr><td colspan=\"5\" class=\"centeralign\">");
289
        ptln("        <span class=\"medialeft\">");
290
        ptln("          <button type=\"submit\" name=\"fn[delete]\" id=\"usrmgr__del\" ".$delete_disable.">".
291
             $this->lang['delete_selected']."</button>");
292
        ptln("        </span>");
293
        ptln("        <span class=\"mediaright\">");
294
        ptln("          <button type=\"submit\" name=\"fn[start]\" ".$page_buttons['start'].">".
295
             $this->lang['start']."</button>");
296
        ptln("          <button type=\"submit\" name=\"fn[prev]\" ".$page_buttons['prev'].">".
297
             $this->lang['prev']."</button>");
298
        ptln("          <button type=\"submit\" name=\"fn[next]\" ".$page_buttons['next'].">".
299
             $this->lang['next']."</button>");
300
        ptln("          <button type=\"submit\" name=\"fn[last]\" ".$page_buttons['last'].">".
301
             $this->lang['last']."</button>");
302
        ptln("        </span>");
303
        if (!empty($this->filter)) {
304
            ptln("    <button type=\"submit\" name=\"fn[search][clear]\">".$this->lang['clear']."</button>");
305
        }
306
        ptln("        <button type=\"submit\" name=\"fn[export]\">".$export_label."</button>");
307
        ptln("        <input type=\"hidden\" name=\"do\"    value=\"admin\" />");
308
        ptln("        <input type=\"hidden\" name=\"page\"  value=\"usermanager\" />");
309
310
        $this->htmlFilterSettings(2);
311
312
        ptln("      </td></tr>");
313
        ptln("    </tbody>");
314
        ptln("  </table>");
315
        ptln("  </div>");
316
317
        ptln("</form>");
318
        ptln("</div>");
319
320
        $style = $this->edit_user ? " class=\"edit_user\"" : "";
321
322
        if ($this->auth->canDo('addUser')) {
323
            ptln("<div".$style.">");
324
            print $this->locale_xhtml('add');
325
            ptln("  <div class=\"level2\">");
326
327
            $this->htmlUserForm('add', null, array(), 4);
328
329
            ptln("  </div>");
330
            ptln("</div>");
331
        }
332
333
        if ($this->edit_user  && $this->auth->canDo('UserMod')) {
334
            ptln("<div".$style." id=\"scroll__here\">");
335
            print $this->locale_xhtml('edit');
336
            ptln("  <div class=\"level2\">");
337
338
            $this->htmlUserForm('modify', $this->edit_user, $this->edit_userdata, 4);
339
340
            ptln("  </div>");
341
            ptln("</div>");
342
        }
343
344
        if ($this->auth->canDo('addUser')) {
345
            $this->htmlImportForm();
346
        }
347
        ptln("</div>");
348
        return true;
349
    }
350
351
    /**
352
     * User Manager is only available if the auth backend supports it
353
     *
354
     * @inheritdoc
355
     * @return bool
356
     */
357
    public function isAccessibleByCurrentUser()
358
    {
359
        /** @var DokuWiki_Auth_Plugin $auth */
360
        global $auth;
361
        if(!$auth || !$auth->canDo('getUsers') ) {
362
            return false;
363
        }
364
365
        return parent::isAccessibleByCurrentUser();
366
    }
367
368
369
    /**
370
     * Display form to add or modify a user
371
     *
372
     * @param string $cmd 'add' or 'modify'
373
     * @param string $user id of user
374
     * @param array  $userdata array with name, mail, pass and grps
375
     * @param int    $indent
376
     */
377
    protected function htmlUserForm($cmd, $user = '', $userdata = array(), $indent = 0)
378
    {
379
        global $conf;
380
        global $ID;
381
        global $lang;
382
383
        $name = $mail = $groups = '';
384
        $notes = array();
385
386
        if ($user) {
387
            extract($userdata);
388
            if (!empty($grps)) $groups = join(',', $grps);
389
        } else {
390
            $notes[] = sprintf($this->lang['note_group'], $conf['defaultgroup']);
391
        }
392
393
        ptln("<form action=\"".wl($ID)."\" method=\"post\">", $indent);
394
        formSecurityToken();
395
        ptln("  <div class=\"table\">", $indent);
396
        ptln("  <table class=\"inline\">", $indent);
397
        ptln("    <thead>", $indent);
398
        ptln("      <tr><th>".$this->lang["field"]."</th><th>".$this->lang["value"]."</th></tr>", $indent);
399
        ptln("    </thead>", $indent);
400
        ptln("    <tbody>", $indent);
401
402
        $this->htmlInputField(
403
            $cmd . "_userid",
404
            "userid",
405
            $this->lang["user_id"],
406
            $user,
407
            $this->auth->canDo("modLogin"),
408
            true,
409
            $indent + 6
410
        );
411
        $this->htmlInputField(
412
            $cmd . "_userpass",
413
            "userpass",
414
            $this->lang["user_pass"],
415
            "",
416
            $this->auth->canDo("modPass"),
417
            false,
418
            $indent + 6
419
        );
420
        $this->htmlInputField(
421
            $cmd . "_userpass2",
422
            "userpass2",
423
            $lang["passchk"],
424
            "",
425
            $this->auth->canDo("modPass"),
426
            false,
427
            $indent + 6
428
        );
429
        $this->htmlInputField(
430
            $cmd . "_username",
431
            "username",
432
            $this->lang["user_name"],
433
            $name,
434
            $this->auth->canDo("modName"),
435
            true,
436
            $indent + 6
437
        );
438
        $this->htmlInputField(
439
            $cmd . "_usermail",
440
            "usermail",
441
            $this->lang["user_mail"],
442
            $mail,
443
            $this->auth->canDo("modMail"),
444
            true,
445
            $indent + 6
446
        );
447
        $this->htmlInputField(
448
            $cmd . "_usergroups",
449
            "usergroups",
450
            $this->lang["user_groups"],
451
            $groups,
452
            $this->auth->canDo("modGroups"),
453
            false,
454
            $indent + 6
455
        );
456
457
        if ($this->auth->canDo("modPass")) {
458
            if ($cmd == 'add') {
459
                $notes[] = $this->lang['note_pass'];
460
            }
461
            if ($user) {
462
                $notes[] = $this->lang['note_notify'];
463
            }
464
465
            ptln("<tr><td><label for=\"".$cmd."_usernotify\" >".
466
                 $this->lang["user_notify"].": </label></td>
467
                 <td><input type=\"checkbox\" id=\"".$cmd."_usernotify\" name=\"usernotify\" value=\"1\" />
468
                 </td></tr>", $indent);
469
        }
470
471
        ptln("    </tbody>", $indent);
472
        ptln("    <tbody>", $indent);
473
        ptln("      <tr>", $indent);
474
        ptln("        <td colspan=\"2\">", $indent);
475
        ptln("          <input type=\"hidden\" name=\"do\"    value=\"admin\" />", $indent);
476
        ptln("          <input type=\"hidden\" name=\"page\"  value=\"usermanager\" />", $indent);
477
478
        // save current $user, we need this to access details if the name is changed
479
        if ($user)
480
          ptln("          <input type=\"hidden\" name=\"userid_old\"  value=\"".hsc($user)."\" />", $indent);
481
482
        $this->htmlFilterSettings($indent+10);
483
484
        ptln("          <button type=\"submit\" name=\"fn[".$cmd."]\">".$this->lang[$cmd]."</button>", $indent);
485
        ptln("        </td>", $indent);
486
        ptln("      </tr>", $indent);
487
        ptln("    </tbody>", $indent);
488
        ptln("  </table>", $indent);
489
490
        if ($notes) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $notes of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
491
            ptln("    <ul class=\"notes\">");
492
            foreach ($notes as $note) {
493
                ptln("      <li><span class=\"li\">".$note."</li>", $indent);
494
            }
495
            ptln("    </ul>");
496
        }
497
        ptln("  </div>", $indent);
498
        ptln("</form>", $indent);
499
    }
500
501
    /**
502
     * Prints a inputfield
503
     *
504
     * @param string $id
505
     * @param string $name
506
     * @param string $label
507
     * @param string $value
508
     * @param bool   $cando whether auth backend is capable to do this action
509
     * @param bool   $required is this field required?
510
     * @param int $indent
511
     */
512
    protected function htmlInputField($id, $name, $label, $value, $cando, $required, $indent = 0)
513
    {
514
        $class = $cando ? '' : ' class="disabled"';
515
        echo str_pad('', $indent);
516
517
        if ($name == 'userpass' || $name == 'userpass2') {
518
            $fieldtype = 'password';
519
            $autocomp  = 'autocomplete="off"';
520
        } elseif ($name == 'usermail') {
521
            $fieldtype = 'email';
522
            $autocomp  = '';
523
        } else {
524
            $fieldtype = 'text';
525
            $autocomp  = '';
526
        }
527
        $value = hsc($value);
528
529
        echo "<tr $class>";
530
        echo "<td><label for=\"$id\" >$label: </label></td>";
531
        echo "<td>";
532
        if ($cando) {
533
            $req = '';
534
            if ($required) $req = 'required="required"';
535
            echo "<input type=\"$fieldtype\" id=\"$id\" name=\"$name\"
536
                  value=\"$value\" class=\"edit\" $autocomp $req />";
537
        } else {
538
            echo "<input type=\"hidden\" name=\"$name\" value=\"$value\" />";
539
            echo "<input type=\"$fieldtype\" id=\"$id\" name=\"$name\"
540
                  value=\"$value\" class=\"edit disabled\" disabled=\"disabled\" />";
541
        }
542
        echo "</td>";
543
        echo "</tr>";
544
    }
545
546
    /**
547
     * Returns htmlescaped filter value
548
     *
549
     * @param string $key name of search field
550
     * @return string html escaped value
551
     */
552
    protected function htmlFilter($key)
553
    {
554
        if (empty($this->filter)) return '';
555
        return (isset($this->filter[$key]) ? hsc($this->filter[$key]) : '');
556
    }
557
558
    /**
559
     * Print hidden inputs with the current filter values
560
     *
561
     * @param int $indent
562
     */
563
    protected function htmlFilterSettings($indent = 0)
564
    {
565
566
        ptln("<input type=\"hidden\" name=\"start\" value=\"".$this->start."\" />", $indent);
567
568
        foreach ($this->filter as $key => $filter) {
569
            ptln("<input type=\"hidden\" name=\"filter[".$key."]\" value=\"".hsc($filter)."\" />", $indent);
570
        }
571
    }
572
573
    /**
574
     * Print import form and summary of previous import
575
     *
576
     * @param int $indent
577
     */
578
    protected function htmlImportForm($indent = 0)
579
    {
580
        global $ID;
581
582
        $failure_download_link = wl($ID, array('do'=>'admin','page'=>'usermanager','fn[importfails]'=>1));
583
584
        ptln('<div class="level2 import_users">', $indent);
585
        print $this->locale_xhtml('import');
586
        ptln('  <form action="'.wl($ID).'" method="post" enctype="multipart/form-data">', $indent);
587
        formSecurityToken();
588
        ptln('    <label>'.$this->lang['import_userlistcsv'].'<input type="file" name="import" /></label>', $indent);
589
        ptln('    <button type="submit" name="fn[import]">'.$this->lang['import'].'</button>', $indent);
590
        ptln('    <input type="hidden" name="do"    value="admin" />', $indent);
591
        ptln('    <input type="hidden" name="page"  value="usermanager" />', $indent);
592
593
        $this->htmlFilterSettings($indent+4);
594
        ptln('  </form>', $indent);
595
        ptln('</div>');
596
597
        // list failures from the previous import
598
        if ($this->import_failures) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->import_failures of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
599
            $digits = strlen(count($this->import_failures));
600
            ptln('<div class="level3 import_failures">', $indent);
601
            ptln('  <h3>'.$this->lang['import_header'].'</h3>');
602
            ptln('  <table class="import_failures">', $indent);
603
            ptln('    <thead>', $indent);
604
            ptln('      <tr>', $indent);
605
            ptln('        <th class="line">'.$this->lang['line'].'</th>', $indent);
606
            ptln('        <th class="error">'.$this->lang['error'].'</th>', $indent);
607
            ptln('        <th class="userid">'.$this->lang['user_id'].'</th>', $indent);
608
            ptln('        <th class="username">'.$this->lang['user_name'].'</th>', $indent);
609
            ptln('        <th class="usermail">'.$this->lang['user_mail'].'</th>', $indent);
610
            ptln('        <th class="usergroups">'.$this->lang['user_groups'].'</th>', $indent);
611
            ptln('      </tr>', $indent);
612
            ptln('    </thead>', $indent);
613
            ptln('    <tbody>', $indent);
614
            foreach ($this->import_failures as $line => $failure) {
615
                ptln('      <tr>', $indent);
616
                ptln('        <td class="lineno"> '.sprintf('%0'.$digits.'d', $line).' </td>', $indent);
617
                ptln('        <td class="error">' .$failure['error'].' </td>', $indent);
618
                ptln('        <td class="field userid"> '.hsc($failure['user'][0]).' </td>', $indent);
619
                ptln('        <td class="field username"> '.hsc($failure['user'][2]).' </td>', $indent);
620
                ptln('        <td class="field usermail"> '.hsc($failure['user'][3]).' </td>', $indent);
621
                ptln('        <td class="field usergroups"> '.hsc($failure['user'][4]).' </td>', $indent);
622
                ptln('      </tr>', $indent);
623
            }
624
            ptln('    </tbody>', $indent);
625
            ptln('  </table>', $indent);
626
            ptln('  <p><a href="'.$failure_download_link.'">'.$this->lang['import_downloadfailures'].'</a></p>');
627
            ptln('</div>');
628
        }
629
    }
630
631
    /**
632
     * Add an user to auth backend
633
     *
634
     * @return bool whether succesful
635
     */
636
    protected function addUser()
637
    {
638
        global $INPUT;
639
        if (!checkSecurityToken()) return false;
640
        if (!$this->auth->canDo('addUser')) return false;
641
642
        list($user,$pass,$name,$mail,$grps,$passconfirm) = $this->retrieveUser();
643
        if (empty($user)) return false;
644
645
        if ($this->auth->canDo('modPass')) {
646
            if (empty($pass)) {
647
                if ($INPUT->has('usernotify')) {
648
                    $pass = auth_pwgen($user);
649
                } else {
650
                    msg($this->lang['add_fail'], -1);
651
                    msg($this->lang['addUser_error_missing_pass'], -1);
652
                    return false;
653
                }
654
            } else {
655
                if (!$this->verifyPassword($pass, $passconfirm)) {
656
                    msg($this->lang['add_fail'], -1);
657
                    msg($this->lang['addUser_error_pass_not_identical'], -1);
658
                    return false;
659
                }
660
            }
661
        } else {
662
            if (!empty($pass)) {
663
                msg($this->lang['add_fail'], -1);
664
                msg($this->lang['addUser_error_modPass_disabled'], -1);
665
                return false;
666
            }
667
        }
668
669
        if ($this->auth->canDo('modName')) {
670
            if (empty($name)) {
671
                msg($this->lang['add_fail'], -1);
672
                msg($this->lang['addUser_error_name_missing'], -1);
673
                return false;
674
            }
675
        } else {
676
            if (!empty($name)) {
677
                msg($this->lang['add_fail'], -1);
678
                msg($this->lang['addUser_error_modName_disabled'], -1);
679
                return false;
680
            }
681
        }
682
683
        if ($this->auth->canDo('modMail')) {
684
            if (empty($mail)) {
685
                msg($this->lang['add_fail'], -1);
686
                msg($this->lang['addUser_error_mail_missing'], -1);
687
                return false;
688
            }
689
        } else {
690
            if (!empty($mail)) {
691
                msg($this->lang['add_fail'], -1);
692
                msg($this->lang['addUser_error_modMail_disabled'], -1);
693
                return false;
694
            }
695
        }
696
697
        if ($ok = $this->auth->triggerUserMod('create', array($user, $pass, $name, $mail, $grps))) {
698
            msg($this->lang['add_ok'], 1);
699
700
            if ($INPUT->has('usernotify') && $pass) {
701
                $this->notifyUser($user, $pass);
702
            }
703
        } else {
704
            msg($this->lang['add_fail'], -1);
705
            msg($this->lang['addUser_error_create_event_failed'], -1);
706
        }
707
708
        return $ok;
709
    }
710
711
    /**
712
     * Delete user from auth backend
713
     *
714
     * @return bool whether succesful
715
     */
716
    protected function deleteUser()
717
    {
718
        global $conf, $INPUT;
719
720
        if (!checkSecurityToken()) return false;
721
        if (!$this->auth->canDo('delUser')) return false;
722
723
        $selected = $INPUT->arr('delete');
724
        if (empty($selected)) return false;
725
        $selected = array_keys($selected);
726
727
        if (in_array($_SERVER['REMOTE_USER'], $selected)) {
728
            msg("You can't delete yourself!", -1);
729
            return false;
730
        }
731
732
        $count = $this->auth->triggerUserMod('delete', array($selected));
733
        if ($count == count($selected)) {
734
            $text = str_replace('%d', $count, $this->lang['delete_ok']);
735
            msg("$text.", 1);
736
        } else {
737
            $part1 = str_replace('%d', $count, $this->lang['delete_ok']);
738
            $part2 = str_replace('%d', (count($selected)-$count), $this->lang['delete_fail']);
739
            msg("$part1, $part2", -1);
740
        }
741
742
        // invalidate all sessions
743
        io_saveFile($conf['cachedir'].'/sessionpurge', time());
744
745
        return true;
746
    }
747
748
    /**
749
     * Edit user (a user has been selected for editing)
750
     *
751
     * @param string $param id of the user
752
     * @return bool whether succesful
753
     */
754
    protected function editUser($param)
755
    {
756
        if (!checkSecurityToken()) return false;
757
        if (!$this->auth->canDo('UserMod')) return false;
758
        $user = $this->auth->cleanUser(preg_replace('/.*[:\/]/', '', $param));
759
        $userdata = $this->auth->getUserData($user);
760
761
        // no user found?
762
        if (!$userdata) {
763
            msg($this->lang['edit_usermissing'], -1);
764
            return false;
765
        }
766
767
        $this->edit_user = $user;
768
        $this->edit_userdata = $userdata;
769
770
        return true;
771
    }
772
773
    /**
774
     * Modify user in the auth backend (modified user data has been recieved)
775
     *
776
     * @return bool whether succesful
777
     */
778
    protected function modifyUser()
779
    {
780
        global $conf, $INPUT;
781
782
        if (!checkSecurityToken()) return false;
783
        if (!$this->auth->canDo('UserMod')) return false;
784
785
        // get currently valid  user data
786
        $olduser = $this->auth->cleanUser(preg_replace('/.*[:\/]/', '', $INPUT->str('userid_old')));
787
        $oldinfo = $this->auth->getUserData($olduser);
788
789
        // get new user data subject to change
790
        list($newuser,$newpass,$newname,$newmail,$newgrps,$passconfirm) = $this->retrieveUser();
791
        if (empty($newuser)) return false;
792
793
        $changes = array();
794
        if ($newuser != $olduser) {
795
            if (!$this->auth->canDo('modLogin')) {        // sanity check, shouldn't be possible
796
                msg($this->lang['update_fail'], -1);
797
                return false;
798
            }
799
800
            // check if $newuser already exists
801
            if ($this->auth->getUserData($newuser)) {
802
                msg(sprintf($this->lang['update_exists'], $newuser), -1);
803
                $re_edit = true;
804
            } else {
805
                $changes['user'] = $newuser;
806
            }
807
        }
808
        if ($this->auth->canDo('modPass')) {
809
            if ($newpass || $passconfirm) {
810
                if ($this->verifyPassword($newpass, $passconfirm)) {
811
                    $changes['pass'] = $newpass;
812
                } else {
813
                    return false;
814
                }
815
            } else {
816
                // no new password supplied, check if we need to generate one (or it stays unchanged)
817
                if ($INPUT->has('usernotify')) {
818
                    $changes['pass'] = auth_pwgen($olduser);
819
                }
820
            }
821
        }
822
823
        if (!empty($newname) && $this->auth->canDo('modName') && $newname != $oldinfo['name']) {
824
            $changes['name'] = $newname;
825
        }
826
        if (!empty($newmail) && $this->auth->canDo('modMail') && $newmail != $oldinfo['mail']) {
827
            $changes['mail'] = $newmail;
828
        }
829
        if (!empty($newgrps) && $this->auth->canDo('modGroups') && $newgrps != $oldinfo['grps']) {
830
            $changes['grps'] = $newgrps;
831
        }
832
833
        if ($ok = $this->auth->triggerUserMod('modify', array($olduser, $changes))) {
834
            msg($this->lang['update_ok'], 1);
835
836
            if ($INPUT->has('usernotify') && !empty($changes['pass'])) {
837
                $notify = empty($changes['user']) ? $olduser : $newuser;
838
                $this->notifyUser($notify, $changes['pass']);
839
            }
840
841
            // invalidate all sessions
842
            io_saveFile($conf['cachedir'].'/sessionpurge', time());
843
        } else {
844
            msg($this->lang['update_fail'], -1);
845
        }
846
847
        if (!empty($re_edit)) {
848
            $this->editUser($olduser);
849
        }
850
851
        return $ok;
852
    }
853
854
    /**
855
     * Send password change notification email
856
     *
857
     * @param string $user         id of user
858
     * @param string $password     plain text
859
     * @param bool   $status_alert whether status alert should be shown
860
     * @return bool whether succesful
861
     */
862
    protected function notifyUser($user, $password, $status_alert = true)
863
    {
864
865
        if ($sent = auth_sendPassword($user, $password)) {
866
            if ($status_alert) {
867
                msg($this->lang['notify_ok'], 1);
868
            }
869
        } else {
870
            if ($status_alert) {
871
                msg($this->lang['notify_fail'], -1);
872
            }
873
        }
874
875
        return $sent;
876
    }
877
878
    /**
879
     * Verify password meets minimum requirements
880
     * :TODO: extend to support password strength
881
     *
882
     * @param string  $password   candidate string for new password
883
     * @param string  $confirm    repeated password for confirmation
884
     * @return bool   true if meets requirements, false otherwise
885
     */
886
    protected function verifyPassword($password, $confirm)
887
    {
888
        global $lang;
889
890
        if (empty($password) && empty($confirm)) {
891
            return false;
892
        }
893
894
        if ($password !== $confirm) {
895
            msg($lang['regbadpass'], -1);
896
            return false;
897
        }
898
899
        // :TODO: test password for required strength
900
901
        // if we make it this far the password is good
902
        return true;
903
    }
904
905
    /**
906
     * Retrieve & clean user data from the form
907
     *
908
     * @param bool $clean whether the cleanUser method of the authentication backend is applied
909
     * @return array (user, password, full name, email, array(groups))
910
     */
911
    protected function retrieveUser($clean = true)
912
    {
913
        /** @var DokuWiki_Auth_Plugin $auth */
914
        global $auth;
915
        global $INPUT;
916
917
        $user = array();
918
        $user[0] = ($clean) ? $auth->cleanUser($INPUT->str('userid')) : $INPUT->str('userid');
919
        $user[1] = $INPUT->str('userpass');
920
        $user[2] = $INPUT->str('username');
921
        $user[3] = $INPUT->str('usermail');
922
        $user[4] = explode(',', $INPUT->str('usergroups'));
923
        $user[5] = $INPUT->str('userpass2');                // repeated password for confirmation
924
925
        $user[4] = array_map('trim', $user[4]);
926
        if ($clean) $user[4] = array_map(array($auth,'cleanGroup'), $user[4]);
927
        $user[4] = array_filter($user[4]);
928
        $user[4] = array_unique($user[4]);
929
        if (!count($user[4])) $user[4] = null;
930
931
        return $user;
932
    }
933
934
    /**
935
     * Set the filter with the current search terms or clear the filter
936
     *
937
     * @param string $op 'new' or 'clear'
938
     */
939
    protected function setFilter($op)
940
    {
941
942
        $this->filter = array();
943
944
        if ($op == 'new') {
945
            list($user,/* $pass */,$name,$mail,$grps) = $this->retrieveUser(false);
946
947
            if (!empty($user)) $this->filter['user'] = $user;
948
            if (!empty($name)) $this->filter['name'] = $name;
949
            if (!empty($mail)) $this->filter['mail'] = $mail;
950
            if (!empty($grps)) $this->filter['grps'] = join('|', $grps);
951
        }
952
    }
953
954
    /**
955
     * Get the current search terms
956
     *
957
     * @return array
958
     */
959
    protected function retrieveFilter()
960
    {
961
        global $INPUT;
962
963
        $t_filter = $INPUT->arr('filter');
964
965
        // messy, but this way we ensure we aren't getting any additional crap from malicious users
966
        $filter = array();
967
968
        if (isset($t_filter['user'])) $filter['user'] = $t_filter['user'];
969
        if (isset($t_filter['name'])) $filter['name'] = $t_filter['name'];
970
        if (isset($t_filter['mail'])) $filter['mail'] = $t_filter['mail'];
971
        if (isset($t_filter['grps'])) $filter['grps'] = $t_filter['grps'];
972
973
        return $filter;
974
    }
975
976
    /**
977
     * Validate and improve the pagination values
978
     */
979
    protected function validatePagination()
980
    {
981
982
        if ($this->start >= $this->users_total) {
983
            $this->start = $this->users_total - $this->pagesize;
984
        }
985
        if ($this->start < 0) $this->start = 0;
986
987
        $this->last = min($this->users_total, $this->start + $this->pagesize);
988
    }
989
990
    /**
991
     * Return an array of strings to enable/disable pagination buttons
992
     *
993
     * @return array with enable/disable attributes
994
     */
995
    protected function pagination()
996
    {
997
998
        $disabled = 'disabled="disabled"';
999
1000
        $buttons = array();
1001
        $buttons['start'] = $buttons['prev'] = ($this->start == 0) ? $disabled : '';
1002
1003
        if ($this->users_total == -1) {
1004
            $buttons['last'] = $disabled;
1005
            $buttons['next'] = '';
1006
        } else {
1007
            $buttons['last'] = $buttons['next'] =
1008
                (($this->start + $this->pagesize) >= $this->users_total) ? $disabled : '';
1009
        }
1010
1011
        if ($this->lastdisabled) {
1012
            $buttons['last'] = $disabled;
1013
        }
1014
1015
        return $buttons;
1016
    }
1017
1018
    /**
1019
     * Export a list of users in csv format using the current filter criteria
1020
     */
1021
    protected function exportCSV()
1022
    {
1023
        // list of users for export - based on current filter criteria
1024
        $user_list = $this->auth->retrieveUsers(0, 0, $this->filter);
1025
        $column_headings = array(
1026
            $this->lang["user_id"],
1027
            $this->lang["user_name"],
1028
            $this->lang["user_mail"],
1029
            $this->lang["user_groups"]
1030
        );
1031
1032
        // ==============================================================================================
1033
        // GENERATE OUTPUT
1034
        // normal headers for downloading...
1035
        header('Content-type: text/csv;charset=utf-8');
1036
        header('Content-Disposition: attachment; filename="wikiusers.csv"');
1037
#       // for debugging assistance, send as text plain to the browser
1038
#       header('Content-type: text/plain;charset=utf-8');
1039
1040
        // output the csv
1041
        $fd = fopen('php://output', 'w');
1042
        fputcsv($fd, $column_headings);
1043
        foreach ($user_list as $user => $info) {
1044
            $line = array($user, $info['name'], $info['mail'], join(',', $info['grps']));
1045
            fputcsv($fd, $line);
1046
        }
1047
        fclose($fd);
1048
        if (defined('DOKU_UNITTEST')) {
1049
            return;
1050
        }
1051
1052
        die;
1053
    }
1054
1055
    /**
1056
     * Import a file of users in csv format
1057
     *
1058
     * csv file should have 4 columns, user_id, full name, email, groups (comma separated)
1059
     *
1060
     * @return bool whether successful
1061
     */
1062
    protected function importCSV()
1063
    {
1064
        // check we are allowed to add users
1065
        if (!checkSecurityToken()) return false;
1066
        if (!$this->auth->canDo('addUser')) return false;
1067
1068
        // check file uploaded ok.
1069
        if (empty($_FILES['import']['size']) ||
1070
            !empty($_FILES['import']['error']) && $this->isUploadedFile($_FILES['import']['tmp_name'])
1071
        ) {
1072
            msg($this->lang['import_error_upload'], -1);
1073
            return false;
1074
        }
1075
        // retrieve users from the file
1076
        $this->import_failures = array();
1077
        $import_success_count = 0;
1078
        $import_fail_count = 0;
1079
        $line = 0;
1080
        $fd = fopen($_FILES['import']['tmp_name'], 'r');
1081
        if ($fd) {
1082
            while ($csv = fgets($fd)) {
1083
                if (!\dokuwiki\Utf8\Clean::isUtf8($csv)) {
1084
                    $csv = utf8_encode($csv);
1085
                }
1086
                $raw = str_getcsv($csv);
1087
                $error = '';                        // clean out any errors from the previous line
1088
                // data checks...
1089
                if (1 == ++$line) {
1090
                    if ($raw[0] == 'user_id' || $raw[0] == $this->lang['user_id']) continue;    // skip headers
1091
                }
1092
                if (count($raw) < 4) {                                        // need at least four fields
1093
                    $import_fail_count++;
1094
                    $error = sprintf($this->lang['import_error_fields'], count($raw));
1095
                    $this->import_failures[$line] = array('error' => $error, 'user' => $raw, 'orig' => $csv);
1096
                    continue;
1097
                }
1098
                array_splice($raw, 1, 0, auth_pwgen());                          // splice in a generated password
1099
                $clean = $this->cleanImportUser($raw, $error);
1100
                if ($clean && $this->importUser($clean, $error)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $clean of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1101
                    $sent = $this->notifyUser($clean[0], $clean[1], false);
1102
                    if (!$sent) {
1103
                        msg(sprintf($this->lang['import_notify_fail'], $clean[0], $clean[3]), -1);
1104
                    }
1105
                    $import_success_count++;
1106
                } else {
1107
                    $import_fail_count++;
1108
                    array_splice($raw, 1, 1);                                  // remove the spliced in password
1109
                    $this->import_failures[$line] = array('error' => $error, 'user' => $raw, 'orig' => $csv);
1110
                }
1111
            }
1112
            msg(
1113
                sprintf(
1114
                    $this->lang['import_success_count'],
1115
                    ($import_success_count + $import_fail_count),
1116
                    $import_success_count
1117
                ),
1118
                ($import_success_count ? 1 : -1)
1119
            );
1120
            if ($import_fail_count) {
1121
                msg(sprintf($this->lang['import_failure_count'], $import_fail_count), -1);
1122
            }
1123
        } else {
1124
            msg($this->lang['import_error_readfail'], -1);
1125
        }
1126
1127
        // save import failures into the session
1128
        if (!headers_sent()) {
1129
            session_start();
1130
            $_SESSION['import_failures'] = $this->import_failures;
1131
            session_write_close();
1132
        }
1133
        return true;
1134
    }
1135
1136
    /**
1137
     * Returns cleaned user data
1138
     *
1139
     * @param array $candidate raw values of line from input file
1140
     * @param string $error
1141
     * @return array|false cleaned data or false
1142
     */
1143
    protected function cleanImportUser($candidate, & $error)
1144
    {
1145
        global $INPUT;
1146
1147
        // FIXME kludgy ....
1148
        $INPUT->set('userid', $candidate[0]);
1149
        $INPUT->set('userpass', $candidate[1]);
1150
        $INPUT->set('username', $candidate[2]);
1151
        $INPUT->set('usermail', $candidate[3]);
1152
        $INPUT->set('usergroups', $candidate[4]);
1153
1154
        $cleaned = $this->retrieveUser();
1155
        list($user,/* $pass */,$name,$mail,/* $grps */) = $cleaned;
1156
        if (empty($user)) {
1157
            $error = $this->lang['import_error_baduserid'];
1158
            return false;
1159
        }
1160
1161
        // no need to check password, handled elsewhere
1162
1163
        if (!($this->auth->canDo('modName') xor empty($name))) {
1164
            $error = $this->lang['import_error_badname'];
1165
            return false;
1166
        }
1167
1168
        if ($this->auth->canDo('modMail')) {
1169
            if (empty($mail) || !mail_isvalid($mail)) {
1170
                $error = $this->lang['import_error_badmail'];
1171
                return false;
1172
            }
1173
        } else {
1174
            if (!empty($mail)) {
1175
                $error = $this->lang['import_error_badmail'];
1176
                return false;
1177
            }
1178
        }
1179
1180
        return $cleaned;
1181
    }
1182
1183
    /**
1184
     * Adds imported user to auth backend
1185
     *
1186
     * Required a check of canDo('addUser') before
1187
     *
1188
     * @param array  $user   data of user
1189
     * @param string &$error reference catched error message
1190
     * @return bool whether successful
1191
     */
1192
    protected function importUser($user, &$error)
1193
    {
1194
        if (!$this->auth->triggerUserMod('create', $user)) {
1195
            $error = $this->lang['import_error_create'];
1196
            return false;
1197
        }
1198
1199
        return true;
1200
    }
1201
1202
    /**
1203
     * Downloads failures as csv file
1204
     */
1205
    protected function downloadImportFailures()
1206
    {
1207
1208
        // ==============================================================================================
1209
        // GENERATE OUTPUT
1210
        // normal headers for downloading...
1211
        header('Content-type: text/csv;charset=utf-8');
1212
        header('Content-Disposition: attachment; filename="importfails.csv"');
1213
#       // for debugging assistance, send as text plain to the browser
1214
#       header('Content-type: text/plain;charset=utf-8');
1215
1216
        // output the csv
1217
        $fd = fopen('php://output', 'w');
1218
        foreach ($this->import_failures as $fail) {
1219
            fputs($fd, $fail['orig']);
1220
        }
1221
        fclose($fd);
1222
        die;
1223
    }
1224
1225
    /**
1226
     * wrapper for is_uploaded_file to facilitate overriding by test suite
1227
     *
1228
     * @param string $file filename
1229
     * @return bool
1230
     */
1231
    protected function isUploadedFile($file)
1232
    {
1233
        return is_uploaded_file($file);
1234
    }
1235
}
1236