/**
* smd_user_manager
*
* A Textpattern CMS plugin for complete user administration:
* -> Search / filter / alter info on users (with asset counts)
* -> Create / alter groups (roles)
* -> Create / customise privs (areas)
* -> Online user list
*
* @author Stef Dawson
* @link http://stefdawson.com/
*/
if (!defined('SMD_UM_PRIVS')) {
define("SMD_UM_PRIVS", 'smd_um_privs');
}
if (!defined('SMD_UM_GROUPS')) {
define("SMD_UM_GROUPS", 'smd_um_groups');
}
if(@txpinterface == 'admin') {
global $smd_um_event, $smd_um_styles, $smd_um_prefs, $txp_permissions, $txp_groups, $txp_user, $event, $step;
$smd_um_event = 'smd_um';
// Styles include a hack for Remora (#nav li ul) to prevent menu from disappearing under the
// privs content. This only occurs because the .smd_um_privgroup class uses position:relative.
// Needs workaround as it affects other plugins.
$smd_um_styles = array(
'control-panel' =>
'#smd_um_control { text-align:center; }
#smd_um_control .search-form, #smd_um_control .smd_um_title, .smd_um_buttons { display:inline-block; margin:0 30px; }
.smd_um_privgroup { margin:10px auto; width:50%; position:relative; }
.smd_um_privgroup h2 { text-align:left; font-weight:bold; }
.smd_active { font-weight:bold; }
.smd_um_privsave { position:absolute; left:-55px; top:0; }
.smd_um_title { font-size: 130%; margin:0 30px 10px!important; }
.smd_um_active_users { margin:15px auto; text-align:center; width:80%; }
.smd_um_selected { background-color:#e2dfce; }
.smd_um_grp_name, .smd_um_prv_name, .smd_um_reset_col { cursor:pointer; }
.smd_um_checkbox, .smd_um_prv_hdr { text-align:center!important; }
#nav li ul { z-index:10000; }',
);
add_privs($smd_um_event.'.usr.list', '1, 2, 3');
add_privs($smd_um_event.'.usr.edit', '1, 2');
add_privs($smd_um_event.'.usr.create', '1');
add_privs($smd_um_event.'.grp', '1');
add_privs($smd_um_event.'.prv', '1');
add_privs($smd_um_event.'.prf', '1');
register_tab('admin', $smd_um_event, smd_um_gTxt('smd_um_tab_name'));
register_callback('smd_um_dispatcher', 'admin');
register_callback('smd_um_dispatcher', $smd_um_event);
register_callback('smd_um_dispatcher', $smd_um_event.'.usr.edit.own');
register_callback('smd_um_users_tab', 'admin_side', 'head_end');
register_callback('smd_um_welcome', 'plugin_lifecycle.smd_user_manager');
// Log the time of this access attempt
$curr_users = unserialize(get_pref('smd_um_current_users', ''));
$curr_users[$txp_user] = time();
set_pref('smd_um_current_users', serialize($curr_users), 'smd_um', PREF_HIDDEN, '', 0);
// Merge in the groups only for now
smd_um_priv_merge(1, 0);
include_once txpath.'/lib/txplib_admin.php';
// Permit user self-editing
$smd_um_grps = array_keys(smd_um_get_groups(0));
unset($smd_um_grps[0]); // Remove None user
$allprivs = join(',',$smd_um_grps);
add_privs($smd_um_event, $allprivs); // Required to display anything at all on the User Manager tab
add_privs($smd_um_event.'.usr.edit.own', $allprivs);
// Now the privs are established for all admin steps so we can go ahead
// and merge in the changes. One caveat: if we're saving the privs we
// need to delay the database merge until after the resets have been applied,
// otherwise we won't know what the defaults (in admin_config.php) are
$do_privs = (($step == 'smd_um_privs') && ps('smd_um_priv_save')) ? 0 : 1;
smd_um_priv_merge(0, $do_privs);
$smd_um_prefs = array(
'smd_um_hierarchical_groups' => array(
'html' => 'yesnoradio',
'type' => PREF_HIDDEN,
'position' => 10,
'default' => '0',
),
'smd_um_admin_group' => array(
'html' => 'selectlist',
'type' => PREF_HIDDEN,
'position' => 20,
'content' => array(get_groups(), false),
'default' => '1',
),
'smd_um_max_search_limit' => array(
'html' => 'text_input',
'type' => PREF_HIDDEN,
'position' => 30,
'default' => '500',
),
'smd_um_pass_length' => array(
'html' => 'text_input',
'type' => PREF_HIDDEN,
'position' => 40,
'default' => '12',
),
'smd_um_active_timeout' => array(
'html' => 'text_input',
'type' => PREF_HIDDEN,
'position' => 50,
'default' => '60',
),
'smd_um_self_alter' => array(
'html' => 'yesnoradio',
'type' => PREF_HIDDEN,
'position' => 60,
'default' => '0',
),
);
}
// ********************
// ADMIN SIDE INTERFACE
// ********************
// Plugin jump off point
function smd_um_dispatcher($evt, $stp) {
global $smd_um_event, $txp_permissions, $txp_user, $event;
ob_end_clean(); // Kill any existing Admin tab panel...
ob_start(); // ... and start again
$event = $smd_um_event;
$stp = gps('step');
$available_steps = array(
'smd_um' => false,
'smd_um_edit' => false,
'smd_um_save' => true,
'smd_um_save_new' => true,
'smd_um_groups' => true,
'smd_um_privs' => true,
'smd_um_prefs' => true,
'smd_um_multi_edit' => true,
'smd_um_change_pass' => true,
'smd_um_change_pageby' => true,
'smd_um_table_install' => true,
'smd_um_table_remove' => true,
'save_pane_state' => true,
);
if (!has_privs($smd_um_event.'.usr.list')) {
$stp = ($stp) ? $stp : 'smd_um_edit';
$uid = safe_field('user_id', 'txp_users', "name='" . doSlash($txp_user) . "'");
if ($uid) {
// Inject this value so the edit step picks it up and edits only the current user.
// The edit/save steps will verify if the current user is the one trying to be edited
// to prevent people adding &user_id=N to the URL
$_POST['user_id'] = $uid;
}
}
if ($stp == 'save_pane_state') {
smd_um_save_pane_state();
} else if (!$stp or !bouncer($stp, $available_steps)) {
$stp = $smd_um_event;
}
$stp();
}
// ------------------------
// Try to hide the Admin->Users tab in the secondary nav via jQuery.
// May fail with inventive DOM structures / themes
// (note to self: campaign for improvement in this area because jQuery smells hackish)
function smd_um_users_tab() {
$userStr = gTxt('tab_site_admin');
echo <<< EOJS
EOJS;
}
// ------------------------
function smd_um_welcome($evt, $stp) {
$msg = '';
switch ($stp) {
case 'installed':
smd_um_table_install(0);
$msg = 'Super duper users!';
break;
case 'deleted':
smd_um_table_remove(0);
break;
}
return $msg;
}
// ------------------------
// Main user display list
function smd_um($msg='') {
global $smd_um_event, $smd_um_prefs, $txp_user, $smd_um_list_pageby, $smd_um_styles;
require_privs($smd_um_event.'.usr.list');
if (!smd_um_table_exist(1)) {
smd_um_table_install(0);
}
pagetop(smd_um_gTxt('smd_um_tab_name').' » '.smd_um_gTxt('smd_um_usr_lbl'), $msg);
extract(smd_um_buttons('usr'));
extract(gpsa(array('page', 'sort', 'dir', 'crit', 'search_method')));
if ($sort === '') $sort = get_pref('smd_um_sort_column', 'login');
if ($dir === '') $dir = get_pref('smd_um_sort_dir', 'desc');
$dir = ($dir == 'asc') ? 'asc' : 'desc';
switch ($sort) {
case 'real_name':
$sort_sql = 'RealName '.$dir.', last_access desc';
break;
case 'email':
$sort_sql = 'email '.$dir.', last_access desc';
break;
case 'privs':
$sort_sql = 'privs '.$dir.', last_access desc';
break;
case 'article_count':
$sort_sql = 'article_count '.$dir.', last_access desc';
break;
case 'image_count':
$sort_sql = 'image_count '.$dir.', last_access desc';
break;
case 'file_count':
$sort_sql = 'file_count '.$dir.', last_access desc';
break;
case 'link_count':
$sort_sql = 'link_count '.$dir.', last_access desc';
break;
case 'last_login':
$sort_sql = 'last_access '.$dir;
break;
default:
$sort = 'name';
$sort_sql = 'name '.$dir.', last_access desc';
break;
}
set_pref('smd_um_sort_column', $sort, 'smd_um', PREF_HIDDEN, '', 0, PREF_PRIVATE);
set_pref('smd_um_sort_dir', $dir, 'smd_um', PREF_HIDDEN, '', 0, PREF_PRIVATE);
$switch_dir = ($dir == 'desc') ? 'asc' : 'desc';
$criteria = 1;
$count_columns = array('article_count', 'image_count', 'file_count', 'link_count');
if ($search_method and $crit != '') {
$crit_escaped = doSlash(str_replace(array('\\','%','_','\''), array('\\\\','\\%','\\_', '\\\''), $crit));
// Permit searching by privilege name (sort of)
if ($search_method == 'privileges' && !is_numeric($crit_escaped)) {
$levels = get_groups();
foreach ($levels as $idx => $group) {
if (strpos(strtolower($group), strtolower($crit_escaped)) !== false) {
$crit_escaped = $idx;
break;
}
}
}
// Permit <, =, and > operators in count searches.
// nullcheck is not required for privs searches because the value is a true 0 (whereas in the computed
// columns it's empty/null)
$operator = '=';
$nullcheck = '';
if (in_array($search_method, $count_columns) || $search_method == 'privileges') {
preg_match('/([<=>]+)?([0-9]+)/', $crit_escaped, $matches);
$operator = (isset($matches[1]) && $matches[1] != '') ? $matches[1] : '=';
$crit_escaped = (isset($matches[2]) && $matches[2] != '') ? $matches[2] : $crit_escaped;
$char_one = substr($operator, 0, 1);
$char_two = substr($operator, 1, 1);
$nullcheck = ($char_one == '<' || ($char_one == '>' && $char_two == '=' && $crit_escaped == '0')) ? ' OR ISNULL('.$search_method.')' : '';
}
$critsql = array(
'login_name' => "name like '%$crit_escaped%'",
'real_name' => "RealName like '%$crit_escaped%'",
'email' => "email like '%$crit_escaped%'",
'privileges' => "privs $operator '$crit_escaped'",
'article_count' => "article_count $operator '$crit_escaped'$nullcheck",
'image_count' => "image_count $operator '$crit_escaped'$nullcheck",
'file_count' => "file_count $operator '$crit_escaped'$nullcheck",
'link_count' => "link_count $operator '$crit_escaped'$nullcheck",
);
if (array_key_exists($search_method, $critsql)) {
$criteria = $critsql[$search_method];
$limit = get_pref('smd_um_max_search_limit', $smd_um_prefs['smd_um_max_search_limit']['default']);
} else {
$search_method = '';
$crit = '';
}
} else {
$search_method = '';
$crit = '';
}
// Since the *_count columns are computed we need to some jiggery pokery here.
// Thus, if we're looking for counts of 'zero' the actual search value should be isnull()
if (in_array($search_method, $count_columns) && $operator == '=' && $crit_escaped == '0') {
$criteria = "ISNULL($search_method)";
}
// The fields, joins and sub-queries that make up the real and computed columns
$fields = 'txu.user_id, txu.name, txu.RealName, txu.email, txu.privs, unix_timestamp(txu.last_access) as last_login, txp.total AS article_count, txi.total AS image_count, txf.total AS file_count, txl.total AS link_count';
$clause = ' FROM '.PFX.'txp_users as txu
LEFT JOIN (SELECT AuthorID, count(ID) AS total FROM '.PFX.'textpattern GROUP BY AuthorID) AS txp ON txp.AuthorID = txu.name
LEFT JOIN (SELECT author, count(id) AS total FROM '.PFX.'txp_image GROUP BY author) AS txi ON txi.author = txu.name
LEFT JOIN (SELECT author, count(id) AS total FROM '.PFX.'txp_file GROUP BY author) AS txf ON txf.author = txu.name
LEFT JOIN (SELECT author, count(id) AS total FROM '.PFX.'txp_link GROUP BY author) AS txl ON txl.author = txu.name';
// Perform a count on the relevant search item. Doing a count(*) is awkward due to the computed columns
// so a straight query is performed with a loop to increment the total. getThing() or getRows for some reason
// failed under certain conditions
$total = 0;
$totrs = safe_query('SELECT '.$fields.$clause.' HAVING '.$criteria);
while ($row = nextRow($totrs)) {
$total++;
}
$btnbar = ''.$btnUsr.$btnGrp.$btnPrv.$btnPrf.'';
$newbtn = has_privs($smd_um_event.'.usr.create') ? '' .smd_um_gTxt('smd_um_new_user'). '' : '';
// Inject styles
echo '';
echo '
';
if ($total < 1) {
if ($criteria != 1) {
echo n.smd_um_search_form($crit, $search_method).$btnbar.
n.graf(gTxt('no_results_found'), ' class="indicator"').'
';
}
return;
}
$limit = max($smd_um_list_pageby, 15);
list($page, $offset, $numPages) = pager($total, $limit, $page);
echo n.smd_um_search_form($crit, $search_method).$btnbar.'';
// Retrieve the user info and related counts
$rs = safe_query('SELECT '.$fields.$clause.' HAVING '.$criteria.' ORDER BY '.$sort_sql.' LIMIT '.$offset.', '.$limit);
if ($rs) {
echo n.'
';
// Retrieve the group info and user counts per privilege level
$fields = 'smdg.id, smdg.name, smdg.core, txu.total AS user_count';
$clause = ' FROM '.PFX.'smd_um_groups AS smdg
LEFT JOIN (SELECT privs, count(privs) AS total FROM '.PFX.'txp_users GROUP BY privs) AS txu ON smdg.id = txu.privs';
$rs = getRows('SELECT ' . $fields.$clause . ' ORDER BY id');
if ($rs) {
echo n.'
';
echo ''.
n.smd_um_active_users().
n.'
';
}
}
// ------------------------
// Privs management panel
function smd_um_privs($msg='') {
global $smd_um_event, $smd_um_prefs, $smd_um_styles, $txp_user, $txp_permissions;
require_privs($smd_um_event.'.prv');
if (!smd_um_table_exist()) {
smd_um_table_install(0);
}
if (ps('smd_um_priv_save')) {
$areas = ps('smd_um_areas');
foreach ($areas as $area) {
$ar_fakename = str_replace('.', '---', $area);
$privs = ps($ar_fakename);
$area = strtolower(sanitizeForPage($area));
// Delete the old area privs if they exist
safe_delete(SMD_UM_PRIVS, "area='".doSlash($area)."'");
if (is_array($privs)) {
foreach ($privs as $priv) {
// Reset should always be first in the list since it's the first checkbox col
// If reset, don't add the privs again (thus they will be read from admin_config.php)
if ($priv == 'smd_um_reset') {
break;
} else {
assert_int($priv);
safe_insert(SMD_UM_PRIVS, "area='" . doSlash($area) . "', priv='" . doSlash($priv) . "'");
}
}
}
}
// Merge the changes into the priv table
smd_um_priv_merge(0,1);
$msg = smd_um_gTxt('smd_um_prv_saved');
} else if (ps('smd_um_priv_add')) {
$name = ps('smd_um_new_prv');
$name = strtolower(sanitizeForPage($name));
if ($name) {
if (strpos($name, 'smd_um') === 0) {
// Can't create privs for this plugin
$msg = array(smd_um_gTxt('smd_um_prv_smd_um'), E_USER_WARNING);
} else {
$exists = array_key_exists($name, $txp_permissions);
if ($exists) {
$msg = array(smd_um_gTxt('smd_um_prv_exists'), E_USER_WARNING);
} else {
safe_insert(SMD_UM_PRIVS, "area='" . doSlash($name) . "'");
smd_um_priv_merge(0,1);
$msg = smd_um_gTxt('smd_um_prv_created', array('{area}' => $name));
}
}
} else {
$msg = array(smd_um_gTxt('smd_um_name_required'), E_ERROR);
}
}
pagetop(smd_um_gTxt('smd_um_tab_name').' » '.smd_um_gTxt('smd_um_prv_lbl'), $msg);
extract(smd_um_buttons('prv'));
$btnbar = ''.$btnUsr.$btnGrp.$btnPrv.$btnPrf.'';
// Inject styles
echo '';
echo '