* @original author Alex Forencich * * Syntax: * ~~groupmanager|[groups to manage]|[allowed users and groups]~~ * * Examples: * ~~groupmanager|posters|@moderators~~ * Members of group 'posters' can be managed by group 'moderators' * * ~~groupmanager|groupa, groupb|joe, @admin~~ * Members of groups 'groupa' and 'groupb' can be managed by user 'joe' * members of the 'admin' group * * Note: superuser groups can only be managed by super users, * forbidden groups can be configured, * and users cannot remove themselves from the group that lets them access * the group manager (including admins) * * Note: if require_conf_namespace config option is set, then plugin looks in * conf_namespace:$ID for configuration. Plugin will also check config * namespace if a placeholder tag is used (~~groupmanager~~). This is the * default configuration for security reasons. * */ // must be run within Dokuwiki if (!defined('DOKU_INC')) die(); if (!defined('DOKU_LF')) define('DOKU_LF', "\n"); if (!defined('DOKU_TAB')) define('DOKU_TAB', "\t"); if (!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN', DOKU_INC . 'lib/plugins/'); if (!defined('GROUPMANAGER_IMAGES')) define('GROUPMANAGER_IMAGES', DOKU_BASE . 'lib/plugins/groupmanager/images/'); require_once DOKU_PLUGIN . 'syntax.php'; function remove_item_by_value($val, $arr, $preserve = true) { if (empty($arr) || !is_array($arr)) { return false; } foreach (array_keys($arr, $val) as $key) { unset($arr[$key]); } return ($preserve) ? $arr : array_values($arr); } class syntax_plugin_groupmanager extends DokuWiki_Syntax_Plugin { /** * Plugin information */ var $_auth = null; // auth object var $_user_total = 0; // number of registered users var $_filter = array(); // user selection filter(s) var $_start = 0; // index of first user to be displayed var $_last = 0; // index of the last user to be displayed var $_pagesize = 20; // number of users to list on one page var $DefaultGroup = ''; var $grplst = array(); var $userlist = array(); /** * Constructor */ function syntax_plugin_groupmanager() { global $auth; $this->setupLocale(); if (!isset($auth)) { $this->disabled = $this->lang['noauth']; } else if (!$auth->canDo('getUsers')) { $this->disabled = $this->lang['nosupport']; } else { // we're good to go $this->_auth = & $auth; } } function getInfo() { return array( 'author' => 'Harald Ronge', 'email' => 'harald@turtur.nl', 'date' => '2013-05-26', 'name' => 'Group Manager Syntax plugin', 'desc' => 'Embeddable group manager, based on groupmgr from Alex Forencich and usermanager from Christopher Smith', 'url' => 'http://www.dokuwiki.org/plugin:groupmanager/', 'original author' => 'Alex Forencich', 'original email' => 'alex@alexforencich.com' ); } /** * Plugin type */ function getType() { return 'substition'; } /** * PType */ function getPType() { return 'normal'; } /** * Sort order */ function getSort() { return 160; } /** * Register syntax handler */ function connectTo($mode) { $this->Lexer->addSpecialPattern('~~groupmanager\|[^~]*?~~', $mode, 'plugin_groupmanager'); $this->Lexer->addSpecialPattern('~~groupmanager~~', $mode, 'plugin_groupmanager'); } /** * Handle match */ // is called without config, but do not know by whom, possibly with literal match function handle($match, $state, $pos, Doku_Handler $handler) { // groupmanager only $data = array(null, $state, $pos); if (strlen($match) == 16) return $data; // Strip away tag $match = substr($match, 15, -2); // split arguments $ar = explode("|", $match); $match = array(); // reorganize into array foreach ($ar as $it) { $ar2 = explode(",", $it); foreach ($ar2 as &$it2) $it2 = trim($it2); $match[] = $ar2; } // pass to render method $data[0] = $match; return $data; } /** * Render it */ function render($mode, Doku_Renderer $renderer, $data) { // start usermanager global $auth; global $lang; global $INFO; global $conf; global $ID; // start groupmanager if ($mode == 'xhtml') { //TurTur, if submit and security token does not match stop anyway if(isset($_REQUEST['fn']) && !checkSecurityToken()) return false; // need config namespace? $allow_add_user = $conf_namespace = $this->getConf('allow_add_user'); $allow_delete_user = $conf_namespace = $this->getConf('allow_delete_user'); $conf_namespace = $this->getConf('conf_namespace'); if ($this->getConf('require_conf_namespace')) { if (!$conf_namespace) return false; else $data[0] = null; // set it to null, it will be reloaded anyway } // empty tag? if (is_null($data[0]) || count($data[0]) == 0) { // load from conf namespace // build page name $conf_page = ""; if (substr($ID, 0, strlen($conf_namespace)) != $conf_namespace) { $conf_page .= $conf_namespace; if (substr($conf_page, -1) != ':') $conf_page .= ":"; } $conf_page .= $ID; // get file name $fn = wikiFN($conf_page); if (!file_exists($fn)) return false; // read file $page = file_get_contents($fn); // find config tag $i = preg_match('/~~groupmanager\|[^~]*?~~/', $page, $match); if ($i == 0) return false; // parse config $match = substr($match[0], 15, -2); $ar = explode("|", $match); $match = array(); // reorganize into array foreach ($ar as $it) { $ar2 = explode(",", $it); foreach ($ar2 as &$it2) $it2 = trim($it2); $match[] = $ar2; } // pass to render method $data[0] = $match; } // don't render if an argument hasn't been specified if (!isset($data[0][0]) || !isset($data[0][1])) return false; $this->grplst = $data[0][0]; $authlst = $data[0][1]; // parse forbidden groups $forbiddengrplst = array(); $str = $this->getConf('forbidden_groups'); if (isset($str)) { $arr = explode(",", $str); foreach ($arr as $val) { $val = trim($val); $forbiddengrplst[] = $val; } } // parse admin groups $admingrplst = array(); if (isset($conf['superuser'])) { $arr = explode(",", $conf['superuser']); foreach ($arr as $val) { $val = trim($val); if ($val[0] == "@") { $val = substr($val, 1); $admingrplst[] = $val; } } } // forbid admin groups if user is not a superuser if (!$INFO['isadmin']) { foreach ($admingrplst as $val) { $forbiddengrplst[] = $val; } } // remove forbidden groups from group list foreach ($forbiddengrplst as $val) { $this->grplst = remove_item_by_value($val, $this->grplst, false); } if (count($this->grplst) > 0) $this->DefaultGroup = $this->grplst[0]; // build array of user's credentials $check = array($_SERVER['REMOTE_USER']); if (is_array($INFO['userinfo'])) { foreach ($INFO['userinfo']['grps'] as $val) { $check[] = "@" . $val; } } // does user have permission? // Also, save authenticated group for later $authbygrp = ""; $ok = 0; foreach ($authlst as $val) { if (in_array($val, $check)) { $ok = 1; if ($val[0] == "@") { $authbygrp = substr($val, 1); } } } // continue if user has explicit permission or is an admin if ($INFO['isadmin'] || $ok) { // authorized $status = 0; // Begin inserted from usermanager if (is_null($this->_auth)) return false; // extract the command and any specific parameters // submit button name is of the form - fn[cmd][param(s)] $fn = $_REQUEST['fn']; if (is_array($fn)) { $cmd = key($fn); $param = is_array($fn[$cmd]) ? key($fn[$cmd]) : null; } else { $cmd = $fn; $param = null; } if ($cmd != "search") { if (!empty($_REQUEST['start'])) { $this->_start = $_REQUEST['start']; } $this->_filter = $this->_retrieveFilter(); $this->_setFilter("new"); } switch ($cmd) { case "add" : if ($allow_add_user) { $this->_addUser(); } else msg($this->lang['add_without_form'], -1); break; case "update" : $this->_deleteUser(); break; /* //case "add" : if ($allow_add_user) {$this->_addUser()} else msg('Trying to add user without form!',-1); break;/* case "modify" : $this->_modifyUser(); break; case "edit" : $this->_editUser($param); break; */ case "search" : $this->_setFilter($param); $this->_start = 0; break; } /* else { $this->_setFilter($param); $this->_start = 0; } */ $this->_user_total = $this->_auth->canDo('getUserCount') ? $this->_auth->getUserCount($this->_filter) : -1; // page handling switch ($cmd) { case 'start' : $this->_start = 0; break; // case 'update' : $this->_start = $this->_start; break; //do nothing case 'prev' : $this->_start -= $this->_pagesize; break; case 'next' : $this->_start += $this->_pagesize; break; case 'last' : $this->_start = $this->_user_total; break; } $this->_validatePagination(); // we are parsing a submitted comment... if (isset($_REQUEST['comment'])) return false; // disable caching $renderer->info['cache'] = false; if (!method_exists($auth, "retrieveUsers")) return false; if (is_null($this->_auth)) { print $this->lang['badauth']; return false; } // watch out: plain authentication takes limit = 0 for get all users // MySQL will take 0 to retrieve none (makes sense), so the code below did not work // $users = $auth->retrieveUsers(0, 10000, array()); $this->userlist = $this->_auth->retrieveUsers($this->_start, $this->_pagesize, $this->_filter); $page_buttons = $this->_pagination(); $colspan = 4 + count($this->grplst) + ($allow_delete_user?0:-1) ; // open form $renderer->doc .= "
"; //TurTur $renderer->doc .= formSecurityToken(false); // open table and print header if ($this->_user_total > 0) { $renderer->doc .= "

" . sprintf($this->lang['summary'], $this->_start + 1, $this->_last, $this->_user_total, $this->_auth->getUserCount()) . "

"; } else { $renderer->doc .= "

" . sprintf($this->lang['nonefound'], $this->_auth->getUserCount()) . "

"; } $renderer->doc .= "\n"; //width=\"95%\" style=\"max-width: 500px; overflow:scroll;\">\n"; // $renderer->doc .= " \n"; $renderer->doc .= " "; $renderer->doc .= " "; $renderer->doc .= " "; //if delete column is hidden, Filter-header is part of the same column if ($allow_delete_user) {$renderer->doc .= " ";} $renderer->doc .= " "; $renderer->doc .= " "; $renderer->doc .= " "; $renderer->doc .= " "; $renderer->doc .= " \n"; $renderer->doc .= " \n"; if ($allow_delete_user) $renderer->doc .= " \n"; $renderer->doc .= " \n"; $renderer->doc .= " \n"; $renderer->doc .= " \n"; // loop through available groups foreach ($this->grplst as $g) { $renderer->doc .= " \n"; } $renderer->doc .= " \n"; // loop through users foreach ($this->userlist as $name => $u) { // print user info $renderer->doc .= " \n"; if ($allow_delete_user) $renderer->doc .= ""; $renderer->doc .= " \n"; $renderer->doc .= " \n"; $renderer->doc .= " \n"; // loop through groups foreach ($this->grplst as $g) { $renderer->doc .= " \n"; } $renderer->doc .= " \n\n"; } $renderer->doc .= " \n"; $renderer->doc .= " "; $renderer->doc .= " "; $renderer->doc .= " "; $renderer->doc .= "
"; $renderer->doc .= " "; $renderer->doc .= " lang['search_prompt'] . "\" alt=\"" . $this->lang['search'] . "\" />"; $renderer->doc .= " lang['search_default_group'] . "\" alt=\"" . $this->lang['search_default_group'] . "\" />"; $renderer->doc .= " lang['clear'] . "\" alt=\"" . $this->lang['clear'] . "\" />"; $renderer->doc .= " "; $renderer->doc .= " "; $renderer->doc .= " lang['start'] . "\" />"; $renderer->doc .= " lang['prev'] . "\" />"; $renderer->doc .= " lang['next'] . "\" />"; $renderer->doc .= " lang['last'] . "\" />"; $renderer->doc .= " "; $renderer->doc .= "
Filter:_htmlFilter('user') . "\" />_htmlFilter('name') . "\" />_htmlFilter('mail') . "\" />"; $renderer->doc .= " _htmlFilter('grps') . "\" />
" . $this->lang['delete'] . "" . $this->lang['user_id'] . "" . $this->lang['user_name'] . "" . $this->lang['user_mail'] . "" . ucwords(str_replace("_", " ", str_replace("wg_", "", htmlspecialchars($g)))) . "
" . htmlspecialchars($name); // need tag so user isn't pulled out of a group if it was added // between initial page load and update //change TurTur: //$hn = md5($name); // caused trouble, on dreamhost md5 did not work //output is better readable too //end change $renderer->doc .= ""; $renderer->doc .= "" . htmlspecialchars($u['name']) . ""; $renderer->emaillink($u['mail']); $renderer->doc .= ""; $chk = "chk_" . $name . "_" . $g; // does this box need to be disabled? // prevents user from taking himself out of an important group $disabled = 0; // if this box applies to a current group membership of the current user, continue check if (in_array($g, $u['grps']) && $_SERVER['REMOTE_USER'] == $name) { // if user is an admin and group is an admin group, disable if ($INFO['isadmin'] && in_array($g, $admingrplst)) { $disabled = 1; // if user was authenticated by this group, disable } else if (strlen($authbygrp) > 0 && $g == $authbygrp) { $disabled = 1; } } // update user group membership // only update if something changed // keep track of status $update = array(); if (!$disabled && $_POST["id_" . $name]) { if ($_POST[$chk]) { if (!in_array($g, $u['grps'])) { $u['grps'][] = $g; $update['grps'] = $u['grps']; } } else { if (in_array($g, $u['grps'])) { $u['grps'] = remove_item_by_value($g, $u['grps'], false); $update['grps'] = $u['grps']; } } if (count($update) > 0) { if ($this->_auth->triggerUserMod('modify',array($name, $update))) { io_saveFile($conf['cachedir'] . '/sessionpurge', time()); //invalidate all sessions if ($status == 0) $status = 1; } else { $status = 2; } } } // display check box $renderer->doc .= "doc .= " checked=\"true\""; } if ($disabled) { $renderer->doc .= " disabled=\"true\""; } $renderer->doc .= " />"; $renderer->doc .= "
"; $renderer->doc .= " "; $renderer->doc .= " lang['search_prompt'] . "\" alt=\"" . $this->lang['search'] . "\" />"; $renderer->doc .= " lang['search_default_group'] . "\" alt=\"" . $this->lang['search_default_group'] . "\" />"; $renderer->doc .= " lang['clear'] . "\" alt=\"" . $this->lang['clear'] . "\" />"; $renderer->doc .= " "; $renderer->doc .= " "; $renderer->doc .= " lang['start'] . "\" />"; $renderer->doc .= " lang['prev'] . "\" />"; $renderer->doc .= " lang['next'] . "\" />"; $renderer->doc .= " lang['last'] . "\" />"; $renderer->doc .= " "; $renderer->doc .= "
\n"; $renderer->doc .= "_start . "\" />"; // update button $renderer->doc .= "
lang['btn_update_group'] . "\" />
"; $renderer->doc .= "
"; if ($this->_auth->canDo('addUser') && $allow_add_user) { $style = $this->_add_user ? " class=\"add_user\"" : ""; $renderer->doc .= ""; $renderer->doc .= $this->locale_xhtml('add'); $renderer->doc .= "
"; $UserData['grps'][0] = $this->DefaultGroup; $this->_htmlUserForm($renderer, 'add', null, $UserData, 4); $renderer->doc .= "
"; $renderer->doc .= ""; } // display relevant status message if ($status == 1) { msg($this->lang['updatesuccess'], 1); } else if ($status == 2) { msg($this->lang['updatefailed'], -1); } } else { // not authorized $renderer->doc .= "

" . $this->lang['notauthorized'] . "

\n"; } return true; } return false; } function _htmlUserForm(&$renderer, $cmd, $user = '', $userdata = array(), $indent = 0) { global $conf; global $ID; $name = $mail = $groups = ''; $notes = array(); extract($userdata); if (!empty($grps)) $groups = join(',', $grps); if (!$user) { //$groups will contain the default group when users are added $notes[] = sprintf($this->lang['note_group'], $groups); } $renderer->doc .= "
"; $renderer->doc .= formSecurityToken(false); $renderer->doc .= " "; $renderer->doc .= " "; $renderer->doc .= " "; $renderer->doc .= " "; $renderer->doc .= " "; $this->_htmlInputField($renderer, $cmd . "_userid", "userid", $this->lang["user_id"], $user, $this->_auth->canDo("modLogin"), $indent + 6); $this->_htmlInputField($renderer, $cmd . "_userpass", "userpass", $this->lang["user_pass"], "", $this->_auth->canDo("modPass"), $indent + 6); $this->_htmlInputField($renderer, $cmd . "_username", "username", $this->lang["user_name"], $name, $this->_auth->canDo("modName"), $indent + 6); $this->_htmlInputField($renderer, $cmd . "_usermail", "usermail", $this->lang["user_mail"], $mail, $this->_auth->canDo("modMail"), $indent + 6); $renderer->doc .= "_auth->canDo("modPass")) { $notes[] = $this->lang['note_pass']; if ($user) { $notes[] = $this->lang['note_notify']; } $renderer->doc .= ""; } $renderer->doc .= " "; $renderer->doc .= " "; $renderer->doc .= " "; $renderer->doc .= " "; $renderer->doc .= " "; $renderer->doc .= " "; $renderer->doc .= "
" . $this->lang["field"] . "" . $this->lang["value"] . "
"; $this->_htmlFilterSettings($renderer, $indent + 10); $renderer->doc .= " lang[$cmd] . "\" />"; $renderer->doc .= "
"; foreach ($notes as $note) $renderer->doc .= "
" . $note . "
"; $renderer->doc .= "
"; } function _htmlInputField(&$renderer, $id, $name, $label, $value, $cando, $indent = 0) { $class = $cando ? '' : ' class="disabled"'; $disabled = $cando ? '' : ' disabled="disabled"'; $renderer->doc .= str_pad('', $indent); if ($name == 'userpass') { $fieldtype = 'password'; $autocomp = 'autocomplete="off"'; } else { $fieldtype = 'text'; $autocomp = ''; } $renderer->doc .= ""; $renderer->doc .= ""; $renderer->doc .= ""; if ($cando) { $renderer->doc .= ""; } else { $renderer->doc .= ""; $renderer->doc .= ""; } $renderer->doc .= ""; $renderer->doc .= ""; } function _addUser() { if (!checkSecurityToken()) return false; if (!$this->_auth->canDo('addUser')) return false; list($user, $pass, $name, $mail, $grps) = $this->_retrieveUser(); if (empty($user)) return false; if ($this->_auth->canDo('modPass')) { if (empty($pass)) { if (!empty($_REQUEST['usernotify'])) { $pass = auth_pwgen(); } else { msg($this->lang['user_must_be_notified_with_generated_pwd'], -1); msg($this->lang['add_fail'], -1); return false; } } } else { if (!empty($pass)) { msg($this->lang['add_fail'], -1); return false; } } if ($this->_auth->canDo('modName')) { if (empty($name)) { msg($this->lang['add_fail'], -1); return false; } } else { if (!empty($name)) { return false; } } if ($this->_auth->canDo('modMail')) { if (empty($mail)) { msg($this->lang['mail_required'], -1); msg($this->lang['add_fail'], -1); return false; } } else { if (!empty($mail)) { return false; } } if ($ok = $this->_auth->triggerUserMod('create', array($user, $pass, $name, $mail, $grps))) { msg($this->lang['add_ok'], 1); if (!empty($_REQUEST['usernotify']) && $pass) { $this->_notifyUser($user, $pass); } } else { msg($this->lang['add_fail'], -1); } return $ok; } /** * Delete user */ function _deleteUser() { global $conf; //$MayDelete = false; if (!checkSecurityToken()) return false; if (!$this->_auth->canDo('delUser')) return false; $selected = $_REQUEST['delete']; if (!is_array($selected) || empty($selected)) return false; $selected = array_keys($selected); if (in_array($_SERVER['REMOTE_USER'], $selected)) { msg($this->lang['cant_delete_yourself'], -1); return false; } //user may only be deleted if not member of any group other than the current working group roles foreach ($selected as $selection) { $currentfilter['user'] = $selection; $currentuser = $this->_auth->retrieveUsers(0, 100000, $currentfilter); $currentgroups = $currentuser[$selection]['grps']; //user may only be part of working group parts //if (count($currentgroups) <= count($this->grplst)) { commented out, If the user is in more groups than this page manages it would skip the test to see if the user was in any other groups! foreach ($currentgroups as $g) { if (!in_array($g, $this->grplst)) { msg($this->lang['cant_delete_if_more_groups'], -1); return false; } } //} } $count = $this->_auth->triggerUserMod('delete', array($selected)); if ($count == count($selected)) { $text = str_replace('%d', $count, $this->lang['delete_ok']); msg("$text.", 1); } else { $part1 = str_replace('%d', $count, $this->lang['delete_ok']); $part2 = str_replace('%d', (count($selected) - $count), $this->lang['delete_fail']); msg("$part1, $part2", -1); } // invalidate all sessions io_saveFile($conf['cachedir'] . '/sessionpurge', time()); return true; } /** * send password change notification email */ function _notifyUser($user, $password) { if ($sent = auth_sendPassword($user, $password)) { msg($this->lang['notify_ok'], 1); } else { msg($this->lang['notify_fail'], -1); } return $sent; } function _htmlFilter($key) { if (empty($this->_filter)) return ''; return (isset($this->_filter[$key]) ? hsc($this->_filter[$key]) : ''); } function _htmlFilterSettings(&$renderer, $indent = 0) { $renderer->doc .= "_start . "\" />"; foreach ($this->_filter as $key => $filter) { $renderer->doc .= ""; } } /** * retrieve & clean user data from the form * * @return array(user, password, full name, email, array(groups)) */ function _retrieveUser($clean = true) { global $auth; $user[0] = ($clean) ? $auth->cleanUser($_REQUEST['userid']) : $_REQUEST['userid']; $user[1] = $_REQUEST['userpass']; $user[2] = $_REQUEST['username']; $user[3] = $_REQUEST['usermail']; $user[4] = explode(',', $_REQUEST['usergroups']); $user[4] = array_map('trim', $user[4]); if ($clean) $user[4] = array_map(array($auth, 'cleanGroup'), $user[4]); $user[4] = array_filter($user[4]); $user[4] = array_unique($user[4]); if (!count($user[4])) $user[4] = null; return $user; } function _setFilter($op) { $this->_filter = array(); switch ($op) { case 'clear': break; case 'new': list($user, $pass, $name, $mail, $grps) = $this->_retrieveUser(false); if (!empty($user)) $this->_filter['user'] = str_replace(' ', '_', $user); if (!empty($name)) $this->_filter['name'] = $name; if (!empty($mail)) $this->_filter['mail'] = $mail; if (!empty($grps)) $this->_filter['grps'] = str_replace(' ', '_', join('|', $grps)); //if (!empty($grps)) $this->_filter['grps'] = join('|',$grps); break; case show_default: $this->_filter['grps'] = $this->DefaultGroup; break; } } function _retrieveFilter() { $t_filter = $_REQUEST['filter']; if (!is_array($t_filter)) return array(); // messy, but this way we ensure we aren't getting any additional crap from malicious users $filter = array(); if (isset($t_filter['user'])) $filter['user'] = $t_filter['user']; if (isset($t_filter['name'])) $filter['name'] = $t_filter['name']; if (isset($t_filter['mail'])) $filter['mail'] = $t_filter['mail']; if (isset($t_filter['grps'])) $filter['grps'] = $t_filter['grps']; return $filter; } function _validatePagination() { if ($this->_start >= $this->_user_total) { $this->_start = $this->_user_total - $this->_pagesize; } if ($this->_start < 0) $this->_start = 0; $this->_last = min($this->_user_total, $this->_start + $this->_pagesize); } /* * return an array of strings to enable/disable pagination buttons */ function _pagination() { $disabled = 'disabled="disabled"'; $buttons['start'] = $buttons['prev'] = ($this->_start == 0) ? $disabled : ''; if ($this->_user_total == -1) { $buttons['last'] = $disabled; $buttons['next'] = ''; } else { $buttons['last'] = $buttons['next'] = (($this->_start + $this->_pagesize) >= $this->_user_total) ? $disabled : ''; } $buttons['update'] = ''; return $buttons; } } // vim:ts=4:sw=4:et:enc=utf-8: