mirror of
https://github.com/wheelybird/ldap-user-manager.git
synced 2025-01-18 15:32:54 +01:00
608 lines
20 KiB
PHP
608 lines
20 KiB
PHP
<?php
|
|
|
|
set_include_path( ".:" . __DIR__ . "/../includes/");
|
|
|
|
include_once "web_functions.inc.php";
|
|
include_once "ldap_functions.inc.php";
|
|
include_once "module_functions.inc.php";
|
|
set_page_access("admin");
|
|
|
|
render_header("$ORGANISATION_NAME account manager");
|
|
render_submenu();
|
|
|
|
$invalid_password = FALSE;
|
|
$mismatched_passwords = FALSE;
|
|
$invalid_username = FALSE;
|
|
$weak_password = FALSE;
|
|
$to_update = array();
|
|
|
|
if ($SMTP['host'] != "") { $can_send_email = TRUE; } else { $can_send_email = FALSE; }
|
|
|
|
$LDAP['default_attribute_map']["mail"] = array("label" => "Email", "onkeyup" => "check_if_we_should_enable_sending_email();");
|
|
|
|
$attribute_map = $LDAP['default_attribute_map'];
|
|
if (isset($LDAP['account_additional_attributes'])) { $attribute_map = ldap_complete_attribute_array($attribute_map,$LDAP['account_additional_attributes']); }
|
|
if (! array_key_exists($LDAP['account_attribute'], $attribute_map)) {
|
|
$attribute_r = array_merge($attribute_map, array($LDAP['account_attribute'] => array("label" => "Account UID")));
|
|
}
|
|
|
|
if (!isset($_POST['account_identifier']) and !isset($_GET['account_identifier'])) {
|
|
?>
|
|
<div class="alert alert-danger">
|
|
<p class="text-center">The account identifier is missing.</p>
|
|
</div>
|
|
<?php
|
|
render_footer();
|
|
exit(0);
|
|
}
|
|
else {
|
|
$account_identifier = (isset($_POST['account_identifier']) ? $_POST['account_identifier'] : $_GET['account_identifier']);
|
|
$account_identifier = urldecode($account_identifier);
|
|
}
|
|
|
|
$ldap_connection = open_ldap_connection();
|
|
$ldap_search_query="({$LDAP['account_attribute']}=". ldap_escape($account_identifier, "", LDAP_ESCAPE_FILTER) . ")";
|
|
$ldap_search = ldap_search( $ldap_connection, $LDAP['user_dn'], $ldap_search_query);
|
|
|
|
|
|
#########################
|
|
|
|
if ($ldap_search) {
|
|
|
|
$user = ldap_get_entries($ldap_connection, $ldap_search);
|
|
|
|
if ($user["count"] > 0) {
|
|
|
|
foreach ($attribute_map as $attribute => $attr_r) {
|
|
|
|
if (isset($user[0][$attribute]) and $user[0][$attribute]['count'] > 0) {
|
|
$$attribute = $user[0][$attribute];
|
|
}
|
|
else {
|
|
$$attribute = array();
|
|
}
|
|
|
|
if (isset($_FILES[$attribute]['size']) and $_FILES[$attribute]['size'] > 0) {
|
|
|
|
$this_attribute = array();
|
|
$this_attribute['count'] = 1;
|
|
$this_attribute[0] = file_get_contents($_FILES[$attribute]['tmp_name']);
|
|
$$attribute = $this_attribute;
|
|
$to_update[$attribute] = $this_attribute;
|
|
unset($to_update[$attribute]['count']);
|
|
|
|
}
|
|
|
|
if (isset($_POST['update_account']) and isset($_POST[$attribute])) {
|
|
|
|
$this_attribute = array();
|
|
|
|
if (is_array($_POST[$attribute])) {
|
|
foreach($_POST[$attribute] as $key => $value) {
|
|
if ($value != "") { $this_attribute[$key] = filter_var($value, FILTER_SANITIZE_FULL_SPECIAL_CHARS); }
|
|
}
|
|
$this_attribute['count'] = count($this_attribute);
|
|
}
|
|
elseif ($_POST[$attribute] != "") {
|
|
$this_attribute['count'] = 1;
|
|
$this_attribute[0] = filter_var($_POST[$attribute], FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
|
}
|
|
|
|
if ($this_attribute != $$attribute) {
|
|
$$attribute = $this_attribute;
|
|
$to_update[$attribute] = $this_attribute;
|
|
unset($to_update[$attribute]['count']);
|
|
}
|
|
|
|
}
|
|
|
|
if (!isset($$attribute) and isset($attr_r['default'])) {
|
|
$$attribute['count'] = 1;
|
|
$$attribute[0] = $attr_r['default'];
|
|
}
|
|
|
|
}
|
|
$dn = $user[0]['dn'];
|
|
|
|
}
|
|
else {
|
|
?>
|
|
<div class="alert alert-danger">
|
|
<p class="text-center">This account doesn't exist.</p>
|
|
</div>
|
|
<?php
|
|
render_footer();
|
|
exit(0);
|
|
}
|
|
|
|
### Update values
|
|
|
|
if (isset($_POST['update_account'])) {
|
|
|
|
if (!isset($uid[0])) {
|
|
$uid[0] = generate_username($givenname[0],$sn[0]);
|
|
$to_update['uid'] = $uid;
|
|
unset($to_update['uid']['count']);
|
|
}
|
|
|
|
if (!isset($cn[0])) {
|
|
if ($ENFORCE_SAFE_SYSTEM_NAMES == TRUE) {
|
|
$cn[0] = $givenname[0] . $sn[0];
|
|
}
|
|
else {
|
|
$cn[0] = $givenname[0] . " " . $sn[0];
|
|
}
|
|
$to_update['cn'] = $cn;
|
|
unset($to_update['cn']['count']);
|
|
}
|
|
|
|
if (isset($_POST['password']) and $_POST['password'] != "") {
|
|
|
|
$password = $_POST['password'];
|
|
|
|
if ((!is_numeric($_POST['pass_score']) or $_POST['pass_score'] < 3) and $ACCEPT_WEAK_PASSWORDS != TRUE) { $weak_password = TRUE; }
|
|
if (preg_match("/\"|'/",$password)) { $invalid_password = TRUE; }
|
|
if ($_POST['password'] != $_POST['password_match']) { $mismatched_passwords = TRUE; }
|
|
if ($ENFORCE_SAFE_SYSTEM_NAMES == TRUE and !preg_match("/$USERNAME_REGEX/",$account_identifier)) { $invalid_username = TRUE; }
|
|
|
|
if ( !$mismatched_passwords
|
|
and !$weak_password
|
|
and !$invalid_password
|
|
) {
|
|
$to_update['userpassword'][0] = ldap_hashed_password($password);
|
|
}
|
|
}
|
|
|
|
if (array_key_exists($LDAP['account_attribute'], $to_update)) {
|
|
$account_attribute = $LDAP['account_attribute'];
|
|
$new_account_identifier = $to_update[$account_attribute][0];
|
|
$new_rdn = "{$account_attribute}={$new_account_identifier}";
|
|
$renamed_entry = ldap_rename($ldap_connection, $dn, $new_rdn, $LDAP['user_dn'], true);
|
|
if ($renamed_entry) {
|
|
$dn = "{$new_rdn},{$LDAP['user_dn']}";
|
|
$account_identifier = $new_account_identifier;
|
|
}
|
|
else {
|
|
ldap_get_option($ldap_connection, LDAP_OPT_DIAGNOSTIC_MESSAGE, $detailed_err);
|
|
error_log("$log_prefix Failed to rename the DN for {$account_identifier}: " . ldap_error($ldap_connection) . " -- " . $detailed_err,0);
|
|
}
|
|
}
|
|
|
|
$existing_objectclasses = $user[0]['objectclass'];
|
|
unset($existing_objectclasses['count']);
|
|
if ($existing_objectclasses != $LDAP['account_objectclasses']) { $to_update['objectclass'] = $LDAP['account_objectclasses']; }
|
|
|
|
$updated_account = @ ldap_mod_replace($ldap_connection, $dn, $to_update);
|
|
|
|
if (!$updated_account) {
|
|
ldap_get_option($ldap_connection, LDAP_OPT_DIAGNOSTIC_MESSAGE, $detailed_err);
|
|
error_log("$log_prefix Failed to modify account details for {$account_identifier}: " . ldap_error($ldap_connection) . " -- " . $detailed_err,0);
|
|
}
|
|
|
|
$sent_email_message="";
|
|
if ($updated_account and isset($mail) and $can_send_email == TRUE and isset($_POST['send_email'])) {
|
|
|
|
include_once "mail_functions.inc.php";
|
|
|
|
$mail_body = parse_mail_text($new_account_mail_body, $password, $account_identifier, $givenname[0], $sn[0]);
|
|
$mail_subject = parse_mail_text($new_account_mail_subject, $password, $account_identifier, $givenname[0], $sn[0]);
|
|
|
|
$sent_email = send_email($mail[0],"{$givenname[0]} {$sn[0]}",$mail_subject,$mail_body);
|
|
if ($sent_email) {
|
|
$sent_email_message .= " An email sent to {$mail[0]}.";
|
|
}
|
|
else {
|
|
$sent_email_message .= " Unfortunately the email wasn't sent; check the logs for more information.";
|
|
}
|
|
}
|
|
|
|
if ($updated_account) {
|
|
render_alert_banner("The account has been updated. $sent_email_message");
|
|
}
|
|
else {
|
|
render_alert_banner("There was a problem updating the account. Check the logs for more information.","danger",15000);
|
|
}
|
|
}
|
|
|
|
|
|
if ($weak_password) { ?>
|
|
<div class="alert alert-warning">
|
|
<p class="text-center">The password wasn't strong enough.</p>
|
|
</div>
|
|
<?php }
|
|
|
|
if ($invalid_password) { ?>
|
|
<div class="alert alert-warning">
|
|
<p class="text-center">The password contained invalid characters.</p>
|
|
</div>
|
|
<?php }
|
|
|
|
if ($mismatched_passwords) { ?>
|
|
<div class="alert alert-warning">
|
|
<p class="text-center">The passwords didn't match.</p>
|
|
</div>
|
|
<?php }
|
|
|
|
|
|
################################################
|
|
|
|
|
|
$all_groups = ldap_get_group_list($ldap_connection);
|
|
|
|
$currently_member_of = ldap_user_group_membership($ldap_connection,$account_identifier);
|
|
|
|
$not_member_of = array_diff($all_groups,$currently_member_of);
|
|
|
|
######### Add/remove from groups
|
|
|
|
if (isset($_POST["update_member_of"])) {
|
|
|
|
$updated_group_membership = array();
|
|
|
|
foreach ($_POST as $index => $group) {
|
|
if (is_numeric($index)) {
|
|
array_push($updated_group_membership,$group);
|
|
}
|
|
}
|
|
|
|
if ($USER_ID == $account_identifier and !array_search($USER_ID, $updated_group_membership)){
|
|
array_push($updated_group_membership,$LDAP["admins_group"]);
|
|
}
|
|
|
|
$groups_to_add = array_diff($updated_group_membership,$currently_member_of);
|
|
$groups_to_del = array_diff($currently_member_of,$updated_group_membership);
|
|
|
|
foreach ($groups_to_del as $this_group) {
|
|
ldap_delete_member_from_group($ldap_connection,$this_group,$account_identifier);
|
|
}
|
|
foreach ($groups_to_add as $this_group) {
|
|
ldap_add_member_to_group($ldap_connection,$this_group,$account_identifier);
|
|
}
|
|
|
|
$not_member_of = array_diff($all_groups,$updated_group_membership);
|
|
$member_of = $updated_group_membership;
|
|
render_alert_banner("The group membership has been updated.");
|
|
|
|
}
|
|
else {
|
|
$member_of = $currently_member_of;
|
|
}
|
|
|
|
################
|
|
|
|
|
|
?>
|
|
<script src="<?php print $SERVER_PATH; ?>js/zxcvbn.min.js"></script>
|
|
<script type="text/javascript" src="<?php print $SERVER_PATH; ?>js/zxcvbn-bootstrap-strength-meter.js"></script>
|
|
<script type="text/javascript">
|
|
$(document).ready(function(){
|
|
$("#StrengthProgressBar").zxcvbnProgressBar({ passwordInput: "#password" });
|
|
});
|
|
</script>
|
|
<script type="text/javascript" src="<?php print $SERVER_PATH; ?>js/generate_passphrase.js"></script>
|
|
<script type="text/javascript" src="<?php print $SERVER_PATH; ?>js/wordlist.js"></script>
|
|
<script>
|
|
|
|
function show_delete_user_button() {
|
|
|
|
group_del_submit = document.getElementById('delete_user');
|
|
group_del_submit.classList.replace('invisible','visible');
|
|
|
|
|
|
}
|
|
|
|
function check_passwords_match() {
|
|
|
|
if (document.getElementById('password').value != document.getElementById('confirm').value ) {
|
|
document.getElementById('password_div').classList.add("has-error");
|
|
document.getElementById('confirm_div').classList.add("has-error");
|
|
}
|
|
else {
|
|
document.getElementById('password_div').classList.remove("has-error");
|
|
document.getElementById('confirm_div').classList.remove("has-error");
|
|
}
|
|
}
|
|
|
|
function random_password() {
|
|
|
|
generatePassword(4,'-','password','confirm');
|
|
$("#StrengthProgressBar").zxcvbnProgressBar({ passwordInput: "#password" });
|
|
}
|
|
|
|
function back_to_hidden(passwordField,confirmField) {
|
|
|
|
var passwordField = document.getElementById(passwordField).type = 'password';
|
|
var confirmField = document.getElementById(confirmField).type = 'password';
|
|
|
|
}
|
|
|
|
function update_form_with_groups() {
|
|
|
|
var group_form = document.getElementById('update_with_groups');
|
|
var group_list_ul = document.getElementById('member_of_list');
|
|
|
|
var group_list = group_list_ul.getElementsByTagName("li");
|
|
|
|
for (var i = 0; i < group_list.length; ++i) {
|
|
var hidden = document.createElement("input");
|
|
hidden.type = "hidden";
|
|
hidden.name = i;
|
|
hidden.value = group_list[i]['textContent'];
|
|
group_form.appendChild(hidden);
|
|
|
|
}
|
|
|
|
group_form.submit();
|
|
|
|
}
|
|
|
|
$(function () {
|
|
|
|
$('body').on('click', '.list-group .list-group-item', function () {
|
|
$(this).toggleClass('active');
|
|
});
|
|
$('.list-arrows button').click(function () {
|
|
var $button = $(this), actives = '';
|
|
if ($button.hasClass('move-left')) {
|
|
actives = $('.list-right ul li.active');
|
|
actives.clone().appendTo('.list-left ul');
|
|
$('.list-left ul li.active').removeClass('active');
|
|
actives.remove();
|
|
} else if ($button.hasClass('move-right')) {
|
|
actives = $('.list-left ul li.active');
|
|
actives.clone().appendTo('.list-right ul');
|
|
$('.list-right ul li.active').removeClass('active');
|
|
actives.remove();
|
|
}
|
|
$("#submit_members").prop("disabled", false);
|
|
});
|
|
$('.dual-list .selector').click(function () {
|
|
var $checkBox = $(this);
|
|
if (!$checkBox.hasClass('selected')) {
|
|
$checkBox.addClass('selected').closest('.well').find('ul li:not(.active)').addClass('active');
|
|
$checkBox.children('i').removeClass('glyphicon-unchecked').addClass('glyphicon-check');
|
|
} else {
|
|
$checkBox.removeClass('selected').closest('.well').find('ul li.active').removeClass('active');
|
|
$checkBox.children('i').removeClass('glyphicon-check').addClass('glyphicon-unchecked');
|
|
}
|
|
});
|
|
$('[name="SearchDualList"]').keyup(function (e) {
|
|
var code = e.keyCode || e.which;
|
|
if (code == '9') return;
|
|
if (code == '27') $(this).val(null);
|
|
var $rows = $(this).closest('.dual-list').find('.list-group li');
|
|
var val = $.trim($(this).val()).replace(/ +/g, ' ').toLowerCase();
|
|
$rows.show().filter(function () {
|
|
var text = $(this).text().replace(/\s+/g, ' ').toLowerCase();
|
|
return !~text.indexOf(val);
|
|
}).hide();
|
|
});
|
|
|
|
});
|
|
|
|
|
|
</script>
|
|
|
|
<script>
|
|
|
|
function check_if_we_should_enable_sending_email() {
|
|
|
|
var check_regex = <?php print $JS_EMAIL_REGEX; ?>
|
|
|
|
|
|
<?php if ($can_send_email == TRUE) { ?>
|
|
if (check_regex.test(document.getElementById("mail").value) && document.getElementById("password").value.length > 0 ) {
|
|
document.getElementById("send_email_checkbox").disabled = false;
|
|
}
|
|
else {
|
|
document.getElementById("send_email_checkbox").disabled = true;
|
|
}
|
|
|
|
<?php } ?>
|
|
if (check_regex.test(document.getElementById('mail').value)) {
|
|
document.getElementById("mail_div").classList.remove("has-error");
|
|
}
|
|
else {
|
|
document.getElementById("mail_div").classList.add("has-error");
|
|
}
|
|
|
|
}
|
|
|
|
</script>
|
|
|
|
<?php render_dynamic_field_js(); ?>
|
|
|
|
<style type='text/css'>
|
|
.dual-list .list-group {
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.list-left li, .list-right li {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.list-arrows {
|
|
padding-top: 100px;
|
|
}
|
|
|
|
.list-arrows button {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.right_button {
|
|
width: 200px;
|
|
float: right;
|
|
}
|
|
</style>
|
|
|
|
|
|
<div class="container">
|
|
<div class="col-sm-8 col-md-offset-2">
|
|
|
|
<div class="panel panel-default">
|
|
<div class="panel-heading clearfix">
|
|
<span class="panel-title pull-left"><h3><?php print $account_identifier; ?></h3></span>
|
|
<button class="btn btn-warning pull-right align-self-end" style="margin-top: auto;" onclick="show_delete_user_button();" <?php if ($account_identifier == $USER_ID) { print "disabled"; }?>>Delete account</button>
|
|
<form action="<?php print "{$THIS_MODULE_PATH}"; ?>/index.php" method="post"><input type="hidden" name="delete_user" value="<?php print urlencode($account_identifier); ?>"><button class="btn btn-danger pull-right invisible" id="delete_user">Confirm deletion</button></form>
|
|
</div>
|
|
<ul class="list-group">
|
|
<li class="list-group-item"><?php print $dn; ?></li>
|
|
</li>
|
|
<div class="panel-body">
|
|
<form class="form-horizontal" action="" enctype="multipart/form-data" method="post">
|
|
|
|
<input type="hidden" name="update_account">
|
|
<input type="hidden" id="pass_score" value="0" name="pass_score">
|
|
<input type="hidden" name="account_identifier" value="<?php print $account_identifier; ?>">
|
|
|
|
<?php
|
|
foreach ($attribute_map as $attribute => $attr_r) {
|
|
$label = $attr_r['label'];
|
|
if (isset($attr_r['onkeyup'])) { $onkeyup = $attr_r['onkeyup']; } else { $onkeyup = ""; }
|
|
if (isset($attr_r['inputtype'])) { $inputtype = $attr_r['inputtype']; } else { $inputtype = ""; }
|
|
if ($attribute == $LDAP['account_attribute']) { $label = "<strong>$label</strong><sup>*</sup>"; }
|
|
if (isset($$attribute)) { $these_values=$$attribute; } else { $these_values = array(); }
|
|
render_attribute_fields($attribute,$label,$these_values,$dn,$onkeyup,$inputtype);
|
|
}
|
|
?>
|
|
|
|
<div class="form-group" id="password_div">
|
|
<label for="password" class="col-sm-3 control-label">Password</label>
|
|
<div class="col-sm-6">
|
|
<input type="password" class="form-control" id="password" name="password" onkeyup="back_to_hidden('password','confirm'); check_if_we_should_enable_sending_email();">
|
|
</div>
|
|
<div class="col-sm-1">
|
|
<input type="button" class="btn btn-sm" id="password_generator" onclick="random_password(); check_if_we_should_enable_sending_email();" value="Generate password">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group" id="confirm_div">
|
|
<label for="confirm" class="col-sm-3 control-label">Confirm</label>
|
|
<div class="col-sm-6">
|
|
<input type="password" class="form-control" id="confirm" name="password_match" onkeyup="check_passwords_match()">
|
|
</div>
|
|
</div>
|
|
|
|
<?php if ($can_send_email == TRUE) { ?>
|
|
<div class="form-group" id="send_email_div">
|
|
<label for="send_email" class="col-sm-3 control-label"> </label>
|
|
<div class="col-sm-6">
|
|
<input type="checkbox" class="form-check-input" id="send_email_checkbox" name="send_email" disabled> Email the updated credentials to the user?
|
|
</div>
|
|
</div>
|
|
<?php } ?>
|
|
|
|
|
|
<div class="form-group">
|
|
<p align='center'><button type="submit" class="btn btn-default">Update account details</button></p>
|
|
</div>
|
|
|
|
</form>
|
|
|
|
<div class="progress">
|
|
<div id="StrengthProgressBar" class="progress-bar"></div>
|
|
</div>
|
|
|
|
<div><p align='center'><sup>*</sup>The account identifier. Changing this will change the full <strong>DN</strong>.</p></div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<div class="container">
|
|
<div class="col-sm-12">
|
|
|
|
<div class="panel panel-default">
|
|
<div class="panel-heading clearfix">
|
|
<h3 class="panel-title pull-left" style="padding-top: 7.5px;">Group membership</h3>
|
|
</div>
|
|
<div class="panel-body">
|
|
|
|
<div class="row">
|
|
|
|
<div class="dual-list list-left col-md-5">
|
|
<strong>Member of</strong>
|
|
<div class="well">
|
|
<div class="row">
|
|
<div class="col-md-10">
|
|
<div class="input-group">
|
|
<span class="input-group-addon glyphicon glyphicon-search"></span>
|
|
<input type="text" name="SearchDualList" class="form-control" placeholder="search" />
|
|
</div>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<div class="btn-group">
|
|
<a class="btn btn-default selector" title="select all"><i class="glyphicon glyphicon-unchecked"></i></a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<ul class="list-group" id="member_of_list">
|
|
<?php
|
|
foreach ($member_of as $group) {
|
|
if ($group == $LDAP["admins_group"] and $USER_ID == $account_identifier) {
|
|
print "<div class='list-group-item' style='opacity: 0.5; pointer-events:none;'>{$group}</div>\n";
|
|
}
|
|
else {
|
|
print "<li class='list-group-item'>$group</li>\n";
|
|
}
|
|
}
|
|
?>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="list-arrows col-md-1 text-center">
|
|
<button class="btn btn-default btn-sm move-left">
|
|
<span class="glyphicon glyphicon-chevron-left"></span>
|
|
</button>
|
|
<button class="btn btn-default btn-sm move-right">
|
|
<span class="glyphicon glyphicon-chevron-right"></span>
|
|
</button>
|
|
<form id="update_with_groups" action="<?php print $CURRENT_PAGE ?>" method="post">
|
|
<input type="hidden" name="update_member_of">
|
|
<input type="hidden" name="account_identifier" value="<?php print $account_identifier; ?>">
|
|
</form>
|
|
<button id="submit_members" class="btn btn-info" disabled type="submit" onclick="update_form_with_groups()">Save</button>
|
|
</div>
|
|
|
|
<div class="dual-list list-right col-md-5">
|
|
<strong>Available groups</strong>
|
|
<div class="well">
|
|
<div class="row">
|
|
<div class="col-md-2">
|
|
<div class="btn-group">
|
|
<a class="btn btn-default selector" title="select all"><i class="glyphicon glyphicon-unchecked"></i></a>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-10">
|
|
<div class="input-group">
|
|
<input type="text" name="SearchDualList" class="form-control" placeholder="search" />
|
|
<span class="input-group-addon glyphicon glyphicon-search"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<ul class="list-group">
|
|
<?php
|
|
foreach ($not_member_of as $group) {
|
|
print "<li class='list-group-item'>$group</li>\n";
|
|
}
|
|
?>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<?php
|
|
|
|
}
|
|
|
|
render_footer();
|
|
|
|
?>
|