You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

1231 lines
40 KiB

  1. <?php
  2. use dokuwiki\Extension\AdminPlugin;
  3. use dokuwiki\Extension\AuthPlugin;
  4. use dokuwiki\Utf8\Clean;
  5. /*
  6. * User Manager
  7. *
  8. * Dokuwiki Admin Plugin
  9. *
  10. * This version of the user manager has been modified to only work with
  11. * objectified version of auth system
  12. *
  13. * @author neolao <neolao@neolao.com>
  14. * @author Chris Smith <chris@jalakai.co.uk>
  15. */
  16. /**
  17. * All DokuWiki plugins to extend the admin function
  18. * need to inherit from this class
  19. */
  20. class admin_plugin_usermanager extends AdminPlugin
  21. {
  22. protected const IMAGE_DIR = DOKU_BASE . 'lib/plugins/usermanager/images/';
  23. protected $auth; // auth object
  24. protected $users_total = 0; // number of registered users
  25. protected $filter = []; // user selection filter(s)
  26. protected $start = 0; // index of first user to be displayed
  27. protected $last = 0; // index of the last user to be displayed
  28. protected $pagesize = 20; // number of users to list on one page
  29. protected $edit_user = ''; // set to user selected for editing
  30. protected $edit_userdata = [];
  31. protected $disabled = ''; // if disabled set to explanatory string
  32. protected $import_failures = [];
  33. protected $lastdisabled = false; // set to true if last user is unknown and last button is hence buggy
  34. /**
  35. * Constructor
  36. */
  37. public function __construct()
  38. {
  39. /** @var AuthPlugin $auth */
  40. global $auth;
  41. $this->setupLocale();
  42. if (!$auth instanceof AuthPlugin) {
  43. $this->disabled = $this->lang['noauth'];
  44. } elseif (!$auth->canDo('getUsers')) {
  45. $this->disabled = $this->lang['nosupport'];
  46. } else {
  47. // we're good to go
  48. $this->auth = &$auth;
  49. }
  50. // attempt to retrieve any import failures from the session
  51. if (!empty($_SESSION['import_failures'])) {
  52. $this->import_failures = $_SESSION['import_failures'];
  53. }
  54. }
  55. /**
  56. * Return prompt for admin menu
  57. *
  58. * @param string $language
  59. * @return string
  60. */
  61. public function getMenuText($language)
  62. {
  63. if (!is_null($this->auth))
  64. return parent::getMenuText($language);
  65. return $this->getLang('menu') . ' ' . $this->disabled;
  66. }
  67. /**
  68. * return sort order for position in admin menu
  69. *
  70. * @return int
  71. */
  72. public function getMenuSort()
  73. {
  74. return 2;
  75. }
  76. /**
  77. * @return int current start value for pageination
  78. */
  79. public function getStart()
  80. {
  81. return $this->start;
  82. }
  83. /**
  84. * @return int number of users per page
  85. */
  86. public function getPagesize()
  87. {
  88. return $this->pagesize;
  89. }
  90. /**
  91. * @param boolean $lastdisabled
  92. */
  93. public function setLastdisabled($lastdisabled)
  94. {
  95. $this->lastdisabled = $lastdisabled;
  96. }
  97. /**
  98. * Handle user request
  99. *
  100. * @return bool
  101. */
  102. public function handle()
  103. {
  104. global $INPUT;
  105. if (is_null($this->auth)) return false;
  106. // extract the command and any specific parameters
  107. // submit button name is of the form - fn[cmd][param(s)]
  108. $fn = $INPUT->param('fn');
  109. if (is_array($fn)) {
  110. $cmd = key($fn);
  111. $param = is_array($fn[$cmd]) ? key($fn[$cmd]) : null;
  112. } else {
  113. $cmd = $fn;
  114. $param = null;
  115. }
  116. if ($cmd != "search") {
  117. $this->start = $INPUT->int('start', 0);
  118. $this->filter = $this->retrieveFilter();
  119. }
  120. switch ($cmd) {
  121. case "add":
  122. $this->addUser();
  123. break;
  124. case "delete":
  125. $this->deleteUser();
  126. break;
  127. case "modify":
  128. $this->modifyUser();
  129. break;
  130. case "edit":
  131. $this->editUser($param);
  132. break;
  133. case "search":
  134. $this->setFilter($param);
  135. $this->start = 0;
  136. break;
  137. case "export":
  138. $this->exportCSV();
  139. break;
  140. case "import":
  141. $this->importCSV();
  142. break;
  143. case "importfails":
  144. $this->downloadImportFailures();
  145. break;
  146. }
  147. $this->users_total = $this->auth->canDo('getUserCount') ? $this->auth->getUserCount($this->filter) : -1;
  148. // page handling
  149. switch ($cmd) {
  150. case 'start':
  151. $this->start = 0;
  152. break;
  153. case 'prev':
  154. $this->start -= $this->pagesize;
  155. break;
  156. case 'next':
  157. $this->start += $this->pagesize;
  158. break;
  159. case 'last':
  160. $this->start = $this->users_total;
  161. break;
  162. }
  163. $this->validatePagination();
  164. return true;
  165. }
  166. /**
  167. * Output appropriate html
  168. *
  169. * @return bool
  170. * @todo split into smaller functions, use Form class
  171. */
  172. public function html()
  173. {
  174. global $ID;
  175. if (is_null($this->auth)) {
  176. echo $this->lang['badauth'];
  177. return false;
  178. }
  179. $user_list = $this->auth->retrieveUsers($this->start, $this->pagesize, $this->filter);
  180. $page_buttons = $this->pagination();
  181. $delete_disable = $this->auth->canDo('delUser') ? '' : 'disabled="disabled"';
  182. $editable = $this->auth->canDo('UserMod');
  183. $export_label = empty($this->filter) ? $this->lang['export_all'] : $this->lang['export_filtered'];
  184. echo $this->locale_xhtml('intro');
  185. echo $this->locale_xhtml('list');
  186. echo '<div id="user__manager">';
  187. echo '<div class="level2">';
  188. if ($this->users_total > 0) {
  189. printf(
  190. '<p>' . $this->lang['summary'] . '</p>',
  191. $this->start + 1,
  192. $this->last,
  193. $this->users_total,
  194. $this->auth->getUserCount()
  195. );
  196. } else {
  197. if ($this->users_total < 0) {
  198. $allUserTotal = 0;
  199. } else {
  200. $allUserTotal = $this->auth->getUserCount();
  201. }
  202. printf('<p>%s</p>', sprintf($this->lang['nonefound'], $allUserTotal));
  203. }
  204. printf('<form action="%s" method="post">', wl($ID));
  205. formSecurityToken();
  206. echo '<div class="table">';
  207. echo '<table class="inline">';
  208. echo '<thead>';
  209. echo '<tr>';
  210. echo '<th>&#160;</th>';
  211. echo '<th>' . $this->lang["user_id"] . '</th>';
  212. echo '<th>' . $this->lang["user_name"] . '</th>';
  213. echo '<th>' . $this->lang["user_mail"] . '</th>';
  214. echo '<th>' . $this->lang["user_groups"] . '</th>';
  215. echo '</tr>';
  216. echo '<tr>';
  217. echo '<td class="rightalign"><input type="image" src="' .
  218. self::IMAGE_DIR . 'search.png" name="fn[search][new]" title="' .
  219. $this->lang['search_prompt'] . '" alt="' . $this->lang['search'] . '" class="button" /></td>';
  220. echo '<td><input type="text" name="userid" class="edit" value="' . $this->htmlFilter('user') . '" /></td>';
  221. echo '<td><input type="text" name="username" class="edit" value="' . $this->htmlFilter('name') . '" /></td>';
  222. echo '<td><input type="text" name="usermail" class="edit" value="' . $this->htmlFilter('mail') . '" /></td>';
  223. echo '<td><input type="text" name="usergroups" class="edit" value="' . $this->htmlFilter('grps') . '" /></td>';
  224. echo '</tr>';
  225. echo '</thead>';
  226. if ($this->users_total) {
  227. echo '<tbody>';
  228. foreach ($user_list as $user => $userinfo) {
  229. extract($userinfo);
  230. /**
  231. * @var string $name
  232. * @var string $pass
  233. * @var string $mail
  234. * @var array $grps
  235. */
  236. $groups = implode(', ', $grps);
  237. echo '<tr class="user_info">';
  238. echo '<td class="centeralign"><input type="checkbox" name="delete[' . hsc($user) .
  239. ']" ' . $delete_disable . ' /></td>';
  240. if ($editable) {
  241. echo '<td><a href="' . wl($ID, ['fn[edit][' . $user . ']' => 1,
  242. 'do' => 'admin',
  243. 'page' => 'usermanager',
  244. 'sectok' => getSecurityToken()]) .
  245. '" title="' . $this->lang['edit_prompt'] . '">' . hsc($user) . '</a></td>';
  246. } else {
  247. echo '<td>' . hsc($user) . '</td>';
  248. }
  249. echo '<td>' . hsc($name) . '</td><td>' . hsc($mail) . '</td><td>' . hsc($groups) . '</td>';
  250. echo '</tr>';
  251. }
  252. echo '</tbody>';
  253. }
  254. echo '<tbody>';
  255. echo '<tr><td colspan="5" class="centeralign">';
  256. echo '<span class="medialeft">';
  257. echo '<button type="submit" name="fn[delete]" id="usrmgr__del" ' . $delete_disable . '>' .
  258. $this->lang['delete_selected'] . '</button>';
  259. echo '</span>';
  260. echo '<span class="mediaright">';
  261. echo '<button type="submit" name="fn[start]" ' . $page_buttons['start'] . '>' .
  262. $this->lang['start'] . '</button>';
  263. echo '<button type="submit" name="fn[prev]" ' . $page_buttons['prev'] . '>' .
  264. $this->lang['prev'] . "</button>";
  265. echo '<button type="submit" name="fn[next]" ' . $page_buttons['next'] . '>' .
  266. $this->lang['next'] . '</button>';
  267. echo '<button type="submit" name="fn[last]" ' . $page_buttons['last'] . '>' .
  268. $this->lang['last'] . '</button>';
  269. echo '</span>';
  270. if (!empty($this->filter)) {
  271. echo '<button type="submit" name="fn[search][clear]">' . $this->lang['clear'] . '</button>';
  272. }
  273. echo '<button type="submit" name="fn[export]">' . $export_label . '</button>';
  274. echo '<input type="hidden" name="do" value="admin" />';
  275. echo '<input type="hidden" name="page" value="usermanager" />';
  276. $this->htmlFilterSettings(2);
  277. echo '</td></tr>';
  278. echo '</tbody>';
  279. echo '</table>';
  280. echo '</div>';
  281. echo '</form>';
  282. echo '</div>';
  283. $style = $this->edit_user ? ' class="edit_user"' : '';
  284. if ($this->auth->canDo('addUser')) {
  285. echo '<div' . $style . '>';
  286. echo $this->locale_xhtml('add');
  287. echo '<div class="level2">';
  288. $this->htmlUserForm('add', null, [], 4);
  289. echo '</div>';
  290. echo '</div>';
  291. }
  292. if ($this->edit_user && $this->auth->canDo('UserMod')) {
  293. echo '<div' . $style . ' id="scroll__here">';
  294. echo $this->locale_xhtml('edit');
  295. echo '<div class="level2">';
  296. $this->htmlUserForm('modify', $this->edit_user, $this->edit_userdata, 4);
  297. echo '</div>';
  298. echo '</div>';
  299. }
  300. if ($this->auth->canDo('addUser')) {
  301. $this->htmlImportForm();
  302. }
  303. echo '</div>';
  304. return true;
  305. }
  306. /**
  307. * User Manager is only available if the auth backend supports it
  308. *
  309. * @inheritdoc
  310. * @return bool
  311. */
  312. public function isAccessibleByCurrentUser()
  313. {
  314. /** @var AuthPlugin $auth */
  315. global $auth;
  316. if (!$auth instanceof AuthPlugin || !$auth->canDo('getUsers')) {
  317. return false;
  318. }
  319. return parent::isAccessibleByCurrentUser();
  320. }
  321. /**
  322. * Display form to add or modify a user
  323. *
  324. * @param string $cmd 'add' or 'modify'
  325. * @param string $user id of user
  326. * @param array $userdata array with name, mail, pass and grps
  327. * @param int $indent
  328. * @todo use Form class
  329. */
  330. protected function htmlUserForm($cmd, $user = '', $userdata = [], $indent = 0)
  331. {
  332. global $conf;
  333. global $ID;
  334. global $lang;
  335. $name = '';
  336. $mail = '';
  337. $groups = '';
  338. $notes = [];
  339. if ($user) {
  340. extract($userdata);
  341. if (!empty($grps)) $groups = implode(',', $grps);
  342. } else {
  343. $notes[] = sprintf($this->lang['note_group'], $conf['defaultgroup']);
  344. }
  345. printf('<form action="%s" method="post">', wl($ID));
  346. formSecurityToken();
  347. echo '<div class="table">';
  348. echo '<table class="inline">';
  349. echo '<thead>';
  350. echo '<tr><th>' . $this->lang["field"] . "</th><th>" . $this->lang["value"] . "</th></tr>";
  351. echo '</thead>';
  352. echo '<tbody>';
  353. $this->htmlInputField(
  354. $cmd . "_userid",
  355. "userid",
  356. $this->lang["user_id"],
  357. $user,
  358. $this->auth->canDo("modLogin"),
  359. true,
  360. $indent + 6
  361. );
  362. $this->htmlInputField(
  363. $cmd . "_userpass",
  364. "userpass",
  365. $this->lang["user_pass"],
  366. "",
  367. $this->auth->canDo("modPass"),
  368. false,
  369. $indent + 6
  370. );
  371. $this->htmlInputField(
  372. $cmd . "_userpass2",
  373. "userpass2",
  374. $lang["passchk"],
  375. "",
  376. $this->auth->canDo("modPass"),
  377. false,
  378. $indent + 6
  379. );
  380. $this->htmlInputField(
  381. $cmd . "_username",
  382. "username",
  383. $this->lang["user_name"],
  384. $name,
  385. $this->auth->canDo("modName"),
  386. true,
  387. $indent + 6
  388. );
  389. $this->htmlInputField(
  390. $cmd . "_usermail",
  391. "usermail",
  392. $this->lang["user_mail"],
  393. $mail,
  394. $this->auth->canDo("modMail"),
  395. true,
  396. $indent + 6
  397. );
  398. $this->htmlInputField(
  399. $cmd . "_usergroups",
  400. "usergroups",
  401. $this->lang["user_groups"],
  402. $groups,
  403. $this->auth->canDo("modGroups"),
  404. false,
  405. $indent + 6
  406. );
  407. if ($this->auth->canDo("modPass")) {
  408. if ($cmd == 'add') {
  409. $notes[] = $this->lang['note_pass'];
  410. }
  411. if ($user) {
  412. $notes[] = $this->lang['note_notify'];
  413. }
  414. echo '<tr><td><label for="' . $cmd . "_usernotify\" >" .
  415. $this->lang["user_notify"] . ': </label></td>
  416. <td><input type="checkbox" id="' . $cmd . '_usernotify" name="usernotify" value="1" />
  417. </td></tr>';
  418. }
  419. echo '</tbody>';
  420. echo '<tbody>';
  421. echo '<tr>';
  422. echo '<td colspan="2">';
  423. echo '<input type="hidden" name="do" value="admin" />';
  424. echo '<input type="hidden" name="page" value="usermanager" />';
  425. // save current $user, we need this to access details if the name is changed
  426. if ($user) {
  427. echo '<input type="hidden" name="userid_old" value="' . hsc($user) . "\" />";
  428. }
  429. $this->htmlFilterSettings($indent + 10);
  430. echo '<button type="submit" name="fn[' . $cmd . ']">' . $this->lang[$cmd] . '</button>';
  431. echo '</td>';
  432. echo '</tr>';
  433. echo '</tbody>';
  434. echo '</table>';
  435. if ($notes) {
  436. echo '<ul class="notes">';
  437. foreach ($notes as $note) {
  438. echo '<li><span class="li">' . $note . '</li>';
  439. }
  440. echo '</ul>';
  441. }
  442. echo '</div>';
  443. echo '</form>';
  444. }
  445. /**
  446. * Prints a inputfield
  447. *
  448. * @param string $id
  449. * @param string $name
  450. * @param string $label
  451. * @param string $value
  452. * @param bool $cando whether auth backend is capable to do this action
  453. * @param bool $required is this field required?
  454. * @param int $indent
  455. * @todo obsolete when Form class is used
  456. */
  457. protected function htmlInputField($id, $name, $label, $value, $cando, $required, $indent = 0)
  458. {
  459. $class = $cando ? '' : ' class="disabled"';
  460. echo str_pad('', $indent);
  461. if ($name == 'userpass' || $name == 'userpass2') {
  462. $fieldtype = 'password';
  463. $autocomp = 'autocomplete="off"';
  464. } elseif ($name == 'usermail') {
  465. $fieldtype = 'email';
  466. $autocomp = '';
  467. } else {
  468. $fieldtype = 'text';
  469. $autocomp = '';
  470. }
  471. $value = hsc($value);
  472. echo "<tr $class>";
  473. echo "<td><label for=\"$id\" >$label: </label></td>";
  474. echo '<td>';
  475. if ($cando) {
  476. $req = '';
  477. if ($required) $req = 'required="required"';
  478. echo "<input type=\"$fieldtype\" id=\"$id\" name=\"$name\"
  479. value=\"$value\" class=\"edit\" $autocomp $req />";
  480. } else {
  481. echo "<input type=\"hidden\" name=\"$name\" value=\"$value\" />";
  482. echo "<input type=\"$fieldtype\" id=\"$id\" name=\"$name\"
  483. value=\"$value\" class=\"edit disabled\" disabled=\"disabled\" />";
  484. }
  485. echo '</td>';
  486. echo '</tr>';
  487. }
  488. /**
  489. * Returns htmlescaped filter value
  490. *
  491. * @param string $key name of search field
  492. * @return string html escaped value
  493. */
  494. protected function htmlFilter($key)
  495. {
  496. if (empty($this->filter)) return '';
  497. return (isset($this->filter[$key]) ? hsc($this->filter[$key]) : '');
  498. }
  499. /**
  500. * Print hidden inputs with the current filter values
  501. *
  502. * @param int $indent
  503. */
  504. protected function htmlFilterSettings($indent = 0)
  505. {
  506. echo '<input type="hidden" name="start" value="' . $this->start . '" />';
  507. foreach ($this->filter as $key => $filter) {
  508. echo '<input type="hidden" name="filter[' . $key . ']" value="' . hsc($filter) . '" />';
  509. }
  510. }
  511. /**
  512. * Print import form and summary of previous import
  513. *
  514. * @param int $indent
  515. */
  516. protected function htmlImportForm($indent = 0)
  517. {
  518. global $ID;
  519. $failure_download_link = wl($ID, ['do' => 'admin', 'page' => 'usermanager', 'fn[importfails]' => 1]);
  520. echo '<div class="level2 import_users">';
  521. echo $this->locale_xhtml('import');
  522. echo '<form action="' . wl($ID) . '" method="post" enctype="multipart/form-data">';
  523. formSecurityToken();
  524. echo '<label>' . $this->lang['import_userlistcsv'] . '<input type="file" name="import" /></label>';
  525. echo '<button type="submit" name="fn[import]">' . $this->lang['import'] . '</button>';
  526. echo '<input type="hidden" name="do" value="admin" />';
  527. echo '<input type="hidden" name="page" value="usermanager" />';
  528. $this->htmlFilterSettings($indent + 4);
  529. echo '</form>';
  530. echo '</div>';
  531. // list failures from the previous import
  532. if ($this->import_failures) {
  533. $digits = strlen(count($this->import_failures));
  534. echo '<div class="level3 import_failures">';
  535. echo '<h3>' . $this->lang['import_header'] . '</h3>';
  536. echo '<table class="import_failures">';
  537. echo '<thead>';
  538. echo '<tr>';
  539. echo '<th class="line">' . $this->lang['line'] . '</th>';
  540. echo '<th class="error">' . $this->lang['error'] . '</th>';
  541. echo '<th class="userid">' . $this->lang['user_id'] . '</th>';
  542. echo '<th class="username">' . $this->lang['user_name'] . '</th>';
  543. echo '<th class="usermail">' . $this->lang['user_mail'] . '</th>';
  544. echo '<th class="usergroups">' . $this->lang['user_groups'] . '</th>';
  545. echo '</tr>';
  546. echo '</thead>';
  547. echo '<tbody>';
  548. foreach ($this->import_failures as $line => $failure) {
  549. echo '<tr>';
  550. echo '<td class="lineno"> ' . sprintf('%0' . $digits . 'd', $line) . ' </td>';
  551. echo '<td class="error">' . $failure['error'] . ' </td>';
  552. echo '<td class="field userid"> ' . hsc($failure['user'][0]) . ' </td>';
  553. echo '<td class="field username"> ' . hsc($failure['user'][2]) . ' </td>';
  554. echo '<td class="field usermail"> ' . hsc($failure['user'][3]) . ' </td>';
  555. echo '<td class="field usergroups"> ' . hsc($failure['user'][4]) . ' </td>';
  556. echo '</tr>';
  557. }
  558. echo '</tbody>';
  559. echo '</table>';
  560. echo '<p><a href="' . $failure_download_link . '">' . $this->lang['import_downloadfailures'] . '</a></p>';
  561. echo '</div>';
  562. }
  563. }
  564. /**
  565. * Add an user to auth backend
  566. *
  567. * @return bool whether succesful
  568. */
  569. protected function addUser()
  570. {
  571. global $INPUT;
  572. if (!checkSecurityToken()) return false;
  573. if (!$this->auth->canDo('addUser')) return false;
  574. [$user, $pass, $name, $mail, $grps, $passconfirm] = $this->retrieveUser();
  575. if (empty($user)) return false;
  576. if ($this->auth->canDo('modPass')) {
  577. if (empty($pass)) {
  578. if ($INPUT->has('usernotify')) {
  579. $pass = auth_pwgen($user);
  580. } else {
  581. msg($this->lang['add_fail'], -1);
  582. msg($this->lang['addUser_error_missing_pass'], -1);
  583. return false;
  584. }
  585. } elseif (!$this->verifyPassword($pass, $passconfirm)) {
  586. msg($this->lang['add_fail'], -1);
  587. msg($this->lang['addUser_error_pass_not_identical'], -1);
  588. return false;
  589. }
  590. } elseif (!empty($pass)) {
  591. msg($this->lang['add_fail'], -1);
  592. msg($this->lang['addUser_error_modPass_disabled'], -1);
  593. return false;
  594. }
  595. if ($this->auth->canDo('modName')) {
  596. if (empty($name)) {
  597. msg($this->lang['add_fail'], -1);
  598. msg($this->lang['addUser_error_name_missing'], -1);
  599. return false;
  600. }
  601. } elseif (!empty($name)) {
  602. msg($this->lang['add_fail'], -1);
  603. msg($this->lang['addUser_error_modName_disabled'], -1);
  604. return false;
  605. }
  606. if ($this->auth->canDo('modMail')) {
  607. if (empty($mail)) {
  608. msg($this->lang['add_fail'], -1);
  609. msg($this->lang['addUser_error_mail_missing'], -1);
  610. return false;
  611. }
  612. } elseif (!empty($mail)) {
  613. msg($this->lang['add_fail'], -1);
  614. msg($this->lang['addUser_error_modMail_disabled'], -1);
  615. return false;
  616. }
  617. if ($ok = $this->auth->triggerUserMod('create', [$user, $pass, $name, $mail, $grps])) {
  618. msg($this->lang['add_ok'], 1);
  619. if ($INPUT->has('usernotify') && $pass) {
  620. $this->notifyUser($user, $pass);
  621. }
  622. } else {
  623. msg($this->lang['add_fail'], -1);
  624. msg($this->lang['addUser_error_create_event_failed'], -1);
  625. }
  626. return $ok;
  627. }
  628. /**
  629. * Delete user from auth backend
  630. *
  631. * @return bool whether succesful
  632. */
  633. protected function deleteUser()
  634. {
  635. global $conf, $INPUT;
  636. if (!checkSecurityToken()) return false;
  637. if (!$this->auth->canDo('delUser')) return false;
  638. $selected = $INPUT->arr('delete');
  639. if (empty($selected)) return false;
  640. $selected = array_keys($selected);
  641. if (in_array($_SERVER['REMOTE_USER'], $selected)) {
  642. msg("You can't delete yourself!", -1);
  643. return false;
  644. }
  645. $count = $this->auth->triggerUserMod('delete', [$selected]);
  646. if ($count == count($selected)) {
  647. $text = str_replace('%d', $count, $this->lang['delete_ok']);
  648. msg("$text.", 1);
  649. } else {
  650. $part1 = str_replace('%d', $count, $this->lang['delete_ok']);
  651. $part2 = str_replace('%d', (count($selected) - $count), $this->lang['delete_fail']);
  652. msg("$part1, $part2", -1);
  653. }
  654. // invalidate all sessions
  655. io_saveFile($conf['cachedir'] . '/sessionpurge', time());
  656. return true;
  657. }
  658. /**
  659. * Edit user (a user has been selected for editing)
  660. *
  661. * @param string $param id of the user
  662. * @return bool whether succesful
  663. */
  664. protected function editUser($param)
  665. {
  666. if (!checkSecurityToken()) return false;
  667. if (!$this->auth->canDo('UserMod')) return false;
  668. $user = $this->auth->cleanUser(preg_replace('/.*[:\/]/', '', $param));
  669. $userdata = $this->auth->getUserData($user);
  670. // no user found?
  671. if (!$userdata) {
  672. msg($this->lang['edit_usermissing'], -1);
  673. return false;
  674. }
  675. $this->edit_user = $user;
  676. $this->edit_userdata = $userdata;
  677. return true;
  678. }
  679. /**
  680. * Modify user in the auth backend (modified user data has been recieved)
  681. *
  682. * @return bool whether succesful
  683. */
  684. protected function modifyUser()
  685. {
  686. global $conf, $INPUT;
  687. if (!checkSecurityToken()) return false;
  688. if (!$this->auth->canDo('UserMod')) return false;
  689. // get currently valid user data
  690. $olduser = $this->auth->cleanUser(preg_replace('/.*[:\/]/', '', $INPUT->str('userid_old')));
  691. $oldinfo = $this->auth->getUserData($olduser);
  692. // get new user data subject to change
  693. [$newuser, $newpass, $newname, $newmail, $newgrps, $passconfirm] = $this->retrieveUser();
  694. if (empty($newuser)) return false;
  695. $changes = [];
  696. if ($newuser != $olduser) {
  697. if (!$this->auth->canDo('modLogin')) { // sanity check, shouldn't be possible
  698. msg($this->lang['update_fail'], -1);
  699. return false;
  700. }
  701. // check if $newuser already exists
  702. if ($this->auth->getUserData($newuser)) {
  703. msg(sprintf($this->lang['update_exists'], $newuser), -1);
  704. $re_edit = true;
  705. } else {
  706. $changes['user'] = $newuser;
  707. }
  708. }
  709. if ($this->auth->canDo('modPass')) {
  710. if ($newpass || $passconfirm) {
  711. if ($this->verifyPassword($newpass, $passconfirm)) {
  712. $changes['pass'] = $newpass;
  713. } else {
  714. return false;
  715. }
  716. } elseif ($INPUT->has('usernotify')) {
  717. // no new password supplied, check if we need to generate one (or it stays unchanged)
  718. $changes['pass'] = auth_pwgen($olduser);
  719. }
  720. }
  721. if (!empty($newname) && $this->auth->canDo('modName') && $newname != $oldinfo['name']) {
  722. $changes['name'] = $newname;
  723. }
  724. if (!empty($newmail) && $this->auth->canDo('modMail') && $newmail != $oldinfo['mail']) {
  725. $changes['mail'] = $newmail;
  726. }
  727. if (!empty($newgrps) && $this->auth->canDo('modGroups') && $newgrps != $oldinfo['grps']) {
  728. $changes['grps'] = $newgrps;
  729. }
  730. if ($ok = $this->auth->triggerUserMod('modify', [$olduser, $changes])) {
  731. msg($this->lang['update_ok'], 1);
  732. if ($INPUT->has('usernotify') && !empty($changes['pass'])) {
  733. $notify = empty($changes['user']) ? $olduser : $newuser;
  734. $this->notifyUser($notify, $changes['pass']);
  735. }
  736. // invalidate all sessions
  737. io_saveFile($conf['cachedir'] . '/sessionpurge', time());
  738. } else {
  739. msg($this->lang['update_fail'], -1);
  740. }
  741. if (!empty($re_edit)) {
  742. $this->editUser($olduser);
  743. }
  744. return $ok;
  745. }
  746. /**
  747. * Send password change notification email
  748. *
  749. * @param string $user id of user
  750. * @param string $password plain text
  751. * @param bool $status_alert whether status alert should be shown
  752. * @return bool whether succesful
  753. */
  754. protected function notifyUser($user, $password, $status_alert = true)
  755. {
  756. if ($sent = auth_sendPassword($user, $password)) {
  757. if ($status_alert) {
  758. msg($this->lang['notify_ok'], 1);
  759. }
  760. } elseif ($status_alert) {
  761. msg($this->lang['notify_fail'], -1);
  762. }
  763. return $sent;
  764. }
  765. /**
  766. * Verify password meets minimum requirements
  767. * :TODO: extend to support password strength
  768. *
  769. * @param string $password candidate string for new password
  770. * @param string $confirm repeated password for confirmation
  771. * @return bool true if meets requirements, false otherwise
  772. */
  773. protected function verifyPassword($password, $confirm)
  774. {
  775. global $lang;
  776. if (empty($password) && empty($confirm)) {
  777. return false;
  778. }
  779. if ($password !== $confirm) {
  780. msg($lang['regbadpass'], -1);
  781. return false;
  782. }
  783. // :TODO: test password for required strength
  784. // if we make it this far the password is good
  785. return true;
  786. }
  787. /**
  788. * Retrieve & clean user data from the form
  789. *
  790. * @param bool $clean whether the cleanUser method of the authentication backend is applied
  791. * @return array (user, password, full name, email, array(groups))
  792. */
  793. protected function retrieveUser($clean = true)
  794. {
  795. /** @var AuthPlugin $auth */
  796. global $auth;
  797. global $INPUT;
  798. $user = [];
  799. $user[0] = ($clean) ? $auth->cleanUser($INPUT->str('userid')) : $INPUT->str('userid');
  800. $user[1] = $INPUT->str('userpass');
  801. $user[2] = $INPUT->str('username');
  802. $user[3] = $INPUT->str('usermail');
  803. $user[4] = explode(',', $INPUT->str('usergroups'));
  804. $user[5] = $INPUT->str('userpass2'); // repeated password for confirmation
  805. $user[4] = array_map('trim', $user[4]);
  806. if ($clean) {
  807. $user[4] = array_map([$auth, 'cleanGroup'], $user[4]);
  808. }
  809. $user[4] = array_filter($user[4]);
  810. $user[4] = array_unique($user[4]);
  811. if ($user[4] === []) {
  812. $user[4] = null;
  813. }
  814. return $user;
  815. }
  816. /**
  817. * Set the filter with the current search terms or clear the filter
  818. *
  819. * @param string $op 'new' or 'clear'
  820. */
  821. protected function setFilter($op)
  822. {
  823. $this->filter = [];
  824. if ($op == 'new') {
  825. [$user, /* pass */, $name, $mail, $grps] = $this->retrieveUser(false);
  826. if (!empty($user)) $this->filter['user'] = $user;
  827. if (!empty($name)) $this->filter['name'] = $name;
  828. if (!empty($mail)) $this->filter['mail'] = $mail;
  829. if (!empty($grps)) $this->filter['grps'] = implode('|', $grps);
  830. }
  831. }
  832. /**
  833. * Get the current search terms
  834. *
  835. * @return array
  836. */
  837. protected function retrieveFilter()
  838. {
  839. global $INPUT;
  840. $t_filter = $INPUT->arr('filter');
  841. // messy, but this way we ensure we aren't getting any additional crap from malicious users
  842. $filter = [];
  843. if (isset($t_filter['user'])) $filter['user'] = $t_filter['user'];
  844. if (isset($t_filter['name'])) $filter['name'] = $t_filter['name'];
  845. if (isset($t_filter['mail'])) $filter['mail'] = $t_filter['mail'];
  846. if (isset($t_filter['grps'])) $filter['grps'] = $t_filter['grps'];
  847. return $filter;
  848. }
  849. /**
  850. * Validate and improve the pagination values
  851. */
  852. protected function validatePagination()
  853. {
  854. if ($this->start >= $this->users_total) {
  855. $this->start = $this->users_total - $this->pagesize;
  856. }
  857. if ($this->start < 0) $this->start = 0;
  858. $this->last = min($this->users_total, $this->start + $this->pagesize);
  859. }
  860. /**
  861. * Return an array of strings to enable/disable pagination buttons
  862. *
  863. * @return array with enable/disable attributes
  864. */
  865. protected function pagination()
  866. {
  867. $disabled = 'disabled="disabled"';
  868. $buttons = [];
  869. $buttons['start'] = $buttons['prev'] = ($this->start == 0) ? $disabled : '';
  870. if ($this->users_total == -1) {
  871. $buttons['last'] = $disabled;
  872. $buttons['next'] = '';
  873. } else {
  874. $buttons['last'] = $buttons['next'] =
  875. (($this->start + $this->pagesize) >= $this->users_total) ? $disabled : '';
  876. }
  877. if ($this->lastdisabled) {
  878. $buttons['last'] = $disabled;
  879. }
  880. return $buttons;
  881. }
  882. /**
  883. * Export a list of users in csv format using the current filter criteria
  884. */
  885. protected function exportCSV()
  886. {
  887. // list of users for export - based on current filter criteria
  888. $user_list = $this->auth->retrieveUsers(0, 0, $this->filter);
  889. $column_headings = [
  890. $this->lang["user_id"],
  891. $this->lang["user_name"],
  892. $this->lang["user_mail"],
  893. $this->lang["user_groups"]
  894. ];
  895. // ==============================================================================================
  896. // GENERATE OUTPUT
  897. // normal headers for downloading...
  898. header('Content-type: text/csv;charset=utf-8');
  899. header('Content-Disposition: attachment; filename="wikiusers.csv"');
  900. # // for debugging assistance, send as text plain to the browser
  901. # header('Content-type: text/plain;charset=utf-8');
  902. // output the csv
  903. $fd = fopen('php://output', 'w');
  904. fputcsv($fd, $column_headings);
  905. foreach ($user_list as $user => $info) {
  906. $line = [$user, $info['name'], $info['mail'], implode(',', $info['grps'])];
  907. fputcsv($fd, $line);
  908. }
  909. fclose($fd);
  910. if (defined('DOKU_UNITTEST')) {
  911. return;
  912. }
  913. die;
  914. }
  915. /**
  916. * Import a file of users in csv format
  917. *
  918. * csv file should have 4 columns, user_id, full name, email, groups (comma separated)
  919. *
  920. * @return bool whether successful
  921. */
  922. protected function importCSV()
  923. {
  924. // check we are allowed to add users
  925. if (!checkSecurityToken()) return false;
  926. if (!$this->auth->canDo('addUser')) return false;
  927. // check file uploaded ok.
  928. if (
  929. empty($_FILES['import']['size']) ||
  930. !empty($_FILES['import']['error']) && $this->isUploadedFile($_FILES['import']['tmp_name'])
  931. ) {
  932. msg($this->lang['import_error_upload'], -1);
  933. return false;
  934. }
  935. // retrieve users from the file
  936. $this->import_failures = [];
  937. $import_success_count = 0;
  938. $import_fail_count = 0;
  939. $line = 0;
  940. $fd = fopen($_FILES['import']['tmp_name'], 'r');
  941. if ($fd) {
  942. while ($csv = fgets($fd)) {
  943. if (!Clean::isUtf8($csv)) {
  944. $csv = utf8_encode($csv);
  945. }
  946. $raw = str_getcsv($csv);
  947. $error = ''; // clean out any errors from the previous line
  948. // data checks...
  949. if (1 == ++$line) {
  950. if ($raw[0] == 'user_id' || $raw[0] == $this->lang['user_id']) continue; // skip headers
  951. }
  952. if (count($raw) < 4) { // need at least four fields
  953. $import_fail_count++;
  954. $error = sprintf($this->lang['import_error_fields'], count($raw));
  955. $this->import_failures[$line] = ['error' => $error, 'user' => $raw, 'orig' => $csv];
  956. continue;
  957. }
  958. array_splice($raw, 1, 0, auth_pwgen()); // splice in a generated password
  959. $clean = $this->cleanImportUser($raw, $error);
  960. if ($clean && $this->importUser($clean, $error)) {
  961. $sent = $this->notifyUser($clean[0], $clean[1], false);
  962. if (!$sent) {
  963. msg(sprintf($this->lang['import_notify_fail'], $clean[0], $clean[3]), -1);
  964. }
  965. $import_success_count++;
  966. } else {
  967. $import_fail_count++;
  968. array_splice($raw, 1, 1); // remove the spliced in password
  969. $this->import_failures[$line] = ['error' => $error, 'user' => $raw, 'orig' => $csv];
  970. }
  971. }
  972. msg(
  973. sprintf(
  974. $this->lang['import_success_count'],
  975. ($import_success_count + $import_fail_count),
  976. $import_success_count
  977. ),
  978. ($import_success_count ? 1 : -1)
  979. );
  980. if ($import_fail_count) {
  981. msg(sprintf($this->lang['import_failure_count'], $import_fail_count), -1);
  982. }
  983. } else {
  984. msg($this->lang['import_error_readfail'], -1);
  985. }
  986. // save import failures into the session
  987. if (!headers_sent()) {
  988. session_start();
  989. $_SESSION['import_failures'] = $this->import_failures;
  990. session_write_close();
  991. }
  992. return true;
  993. }
  994. /**
  995. * Returns cleaned user data
  996. *
  997. * @param array $candidate raw values of line from input file
  998. * @param string $error
  999. * @return array|false cleaned data or false
  1000. */
  1001. protected function cleanImportUser($candidate, &$error)
  1002. {
  1003. global $INPUT;
  1004. // FIXME kludgy ....
  1005. $INPUT->set('userid', $candidate[0]);
  1006. $INPUT->set('userpass', $candidate[1]);
  1007. $INPUT->set('username', $candidate[2]);
  1008. $INPUT->set('usermail', $candidate[3]);
  1009. $INPUT->set('usergroups', $candidate[4]);
  1010. $cleaned = $this->retrieveUser();
  1011. [$user, /* pass */, $name, $mail, /* grps */] = $cleaned;
  1012. if (empty($user)) {
  1013. $error = $this->lang['import_error_baduserid'];
  1014. return false;
  1015. }
  1016. // no need to check password, handled elsewhere
  1017. if (!($this->auth->canDo('modName') xor empty($name))) {
  1018. $error = $this->lang['import_error_badname'];
  1019. return false;
  1020. }
  1021. if ($this->auth->canDo('modMail')) {
  1022. if (empty($mail) || !mail_isvalid($mail)) {
  1023. $error = $this->lang['import_error_badmail'];
  1024. return false;
  1025. }
  1026. } elseif (!empty($mail)) {
  1027. $error = $this->lang['import_error_badmail'];
  1028. return false;
  1029. }
  1030. return $cleaned;
  1031. }
  1032. /**
  1033. * Adds imported user to auth backend
  1034. *
  1035. * Required a check of canDo('addUser') before
  1036. *
  1037. * @param array $user data of user
  1038. * @param string &$error reference catched error message
  1039. * @return bool whether successful
  1040. */
  1041. protected function importUser($user, &$error)
  1042. {
  1043. if (!$this->auth->triggerUserMod('create', $user)) {
  1044. $error = $this->lang['import_error_create'];
  1045. return false;
  1046. }
  1047. return true;
  1048. }
  1049. /**
  1050. * Downloads failures as csv file
  1051. */
  1052. protected function downloadImportFailures()
  1053. {
  1054. // ==============================================================================================
  1055. // GENERATE OUTPUT
  1056. // normal headers for downloading...
  1057. header('Content-type: text/csv;charset=utf-8');
  1058. header('Content-Disposition: attachment; filename="importfails.csv"');
  1059. # // for debugging assistance, send as text plain to the browser
  1060. # header('Content-type: text/plain;charset=utf-8');
  1061. // output the csv
  1062. $fd = fopen('php://output', 'w');
  1063. foreach ($this->import_failures as $fail) {
  1064. fwrite($fd, $fail['orig']);
  1065. }
  1066. fclose($fd);
  1067. die;
  1068. }
  1069. /**
  1070. * wrapper for is_uploaded_file to facilitate overriding by test suite
  1071. *
  1072. * @param string $file filename
  1073. * @return bool
  1074. */
  1075. protected function isUploadedFile($file)
  1076. {
  1077. return is_uploaded_file($file);
  1078. }
  1079. }