PageRenderTime 52ms CodeModel.GetById 18ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/plugins/usermanager/admin.php

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