<?php if (!defined('PmWiki')) exit();
# vim: set ts=4 sw=4 et:
/*	=== UserAdmin-Core ===
 *	Copyright 2010 Eemeli Aro <eemeli@gmail.com>
 *
 *	AuthUser account self-registration and management
 *
 *	For more information, please see the online documentation at
 *		http://www.pmwiki.org/wiki/Cookbook/UserAdmin
 *
 *
 *  This script is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This script is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

$RecipeInfo['UserAdmin']['Version'] = '2011-03-29';


SDV($HandleActions[$action], 'HandleUserAdmin');
function HandleUserAdmin($pagename, $auth = 'read') {
	global $action, $UserAdmin;

	$UserAdmin->action = preg_match('#^user/(.+)$#', $action, $m) ? preg_replace('/\W+/', '', $m[1]) : NULL;
	$hm = 'Handle'.ucfirst($UserAdmin->action);
	if (method_exists($UserAdmin, $hm)) $result = $UserAdmin->{$hm}($pagename);
	else { $UserAdmin->action = NULL; $result = NULL; }

	$UserAdmin->PrintPage($pagename, $result);
}


########################################################################################################################
##                                                                                                          set defaults

SDV($FmtPV['$Username'], '$GLOBALS["UserAdmin"]->Username($pn, $_REQUEST)');
SDV($Conditions['superuser'], '@$GLOBALS["UserAdmin"]->Superuser()');

XLSDV('en', array(
	'UA_contact_admin' => 'Please contact site admin',
	'UA_diff_userpasswd' => 'New passwords don&#039;t match',
	'UA_email_fail' => 'Error sending email',
	'UA_email_from' => "no-reply@{$_SERVER['HTTP_HOST']}",
	'UA_empty_key' => 'Activation key is required',
	'UA_empty_useremail' => 'An e-mail address is required',
	'UA_empty_username' => 'Username is required',
	'UA_empty_useroldpasswd' => 'Current password is required',
	'UA_empty_userpasswd' => 'Password is required',
	'UA_empty_userpasswd2' => 'Please enter your password twice',
	'UA_exists' => 'User already exists',
	'UA_fail_unknown_action' => 'Error: unknown user action',
	'UA_invalid_useremail' => 'E-mail address is not valid',
	'UA_invalid_username' => 'Username is not valid',
	'UA_invalid_userpasswd' => 'Password is not valid',
	'UA_return_link' => "\\\\\n\n[[ {\$PageUrl}?action=user | $[Return to user actions] ]]",
	'UA_title' => 'User account management',
	'UA_txt_key' => 'Activation key',
	'UA_txt_useremail' => 'E-mail address',
	'UA_txt_usergroups' => 'User groups',
	'UA_txt_username' => 'Username',
	'UA_txt_useroldpasswd' => 'Current password',
	'UA_txt_userpasswd' => 'New password',
	'UA_txt_userpasswd2' => 'New password, again',
	'UA_unauthorized' => 'Not authorized',
	'UA_unsupported_user_format' => 'User data can&#039;t be edited',
	'UA_wrong_passwd' => 'Password not recognized',

	'UAedit_fail' => 'Error updating user account',
	'UAedit_submit' => 'Submit',
	'UAedit_success' => 'User account updated',
	'UAedit_success_unchanged' => 'User account not modified',
	'UAedit_title' => 'Edit user account',

	'UAgroup_fail' => 'Error updating user group',
	'UAgroup_submit' => 'Submit',
	'UAgroup_success' => 'User group updated',
	'UAgroup_success_unchanged' => 'User group not modified',
	'UAgroup_title' => 'Edit user group',

	'UAnew_email_body' => "Welcome \$username!\n
		Thank you for registering at $WikiTitle.

		To activate your account and confirm your e-mail address, please visit
		the following location:

		\$link\n",
	'UAnew_email_subject' => "Welcome to $WikiTitle",
	'UAnew_email_sent' => 'E-mail sent with activation link',
	'UAnew_fail' => 'Error creating account',
	'UAnew_submit' => 'Sign up',
	'UAnew_success' => 'New user account created',
	'UAnew_title' => 'Register as a new user',

	'UAresetpasswd_email_body' => "To set a new password for the user \$username at $WikiTitle, please
		visit the following location:

		\$link

		If you've received this message in error, please contact the site admin
		at <$ScriptUrl>.\n",
	'UAresetpasswd_email_empty' => 'User has no e-mail address defined',
	'UAresetpasswd_email_subject' => "Password reset for $WikiTitle",
	'UAresetpasswd_fail' => 'Error sending reset link',
	'UAresetpasswd_submit' => 'Send reset link to user&#039;s e-mail address',
	'UAresetpasswd_success' => 'Password reset link sent to user&#039;s e-mail address',
	'UAresetpasswd_title' => 'Reset password',

	'UAunlock_already_active' => 'Account is already active',
	'UAunlock_bad_key' => 'Bad activation key',
	'UAunlock_fail' => 'Error activating account',
	'UAunlock_new_passwd' => 'Enter new password',
	'UAunlock_submit' => 'Activate/set password',
	'UAunlock_success_activated' => 'User account activated',
	'UAunlock_success_set_pw' => 'User password set',
	'UAunlock_title' => 'Account activation',
));

define('UA_REQ_NOT_EMPTY', 0);
define('UA_REQ_ANY', 1);
define('UA_REQ_TWICE', 2); ## implies not empty
define('UA_REQ_TWICE_ANY', 3);
define('UA_REQ_PRESET', 4); ## implies not empty
define('UA_REQ_PRESET_ANY', 5);


## return array entries with keys that match the pattern
## ie. exactly like preg_grep but for array keys instead of values
if (!function_exists('preg_grep_keys')) {
function preg_grep_keys($pattern, $input, $flags = NULL) {
	$keys = preg_grep($pattern, array_keys($input), $flags);
	$out = array();
	foreach ($keys as $key) $out[$key] = $input[$key];
	return $out;
}}

########################################################################################################################
##                                                                                                       framework class
class UserAdmin {
	var $confirm_email = TRUE;
	var $fields = array(
		'new' => array(
			'username' => UA_REQ_NOT_EMPTY,
			'userpasswd' => UA_REQ_TWICE,
			'useremail' => UA_REQ_NOT_EMPTY),
		'resetpasswd' => array(
			'username' => UA_REQ_NOT_EMPTY),
		'unlock' => array(
			'username' => UA_REQ_PRESET,
			'key' => UA_REQ_PRESET),
		'newpasswd' => array(
			'username' => UA_REQ_PRESET,
			'key' => UA_REQ_PRESET,
			'userpasswd' => UA_REQ_TWICE),
		'edit' => array(
			'username' => UA_REQ_PRESET,
			'usergroups' => UA_REQ_PRESET_ANY,
			'useroldpasswd' => UA_REQ_NOT_EMPTY,
			'useremail' => UA_REQ_NOT_EMPTY,
			'userpasswd' => UA_REQ_TWICE_ANY),
		'group' => array(
			'groupname' => UA_REQ_NOT_EMPTY),
	);

	var $action;
	var $input;
    var $AuthUserData = array(); // contains all AuthUser contents
    var $AuthUserAuth = array(); // contains auths (hash or @group)

	########################################################################
	## AuthUserPage()
	##   Utility function to read SiteAdmin.AuthUser page
	## returns array('userA' => array(HASH, '@groupA', ...), 
	##               '@groupA' => array('userB', ...), ...)
	function AuthUserPage($userpat=null, $forceread=false) {
		global $AuthUserPat, $AuthUserPageFmt;
#echo "DEBUG: AuthUserPage(): Entering<br>\n";
		if ($this->AuthUserAuth && !$forceread) return $this->array_match($userpat, $this->AuthUserAuth);
		SDV($AuthUserPageFmt, '$SiteAdminGroup.AuthUser');
        SDV($AuthUserPat, "/^\\s*([@\\w][^\\s:]*):(.*)/m"); // default from authuser.php
		$pn = FmtPageName($AuthUserPageFmt, '');
		$apage = ReadPage($pn, READPAGE_CURRENT);
		if (!$apage
		    || empty($apage['text'])
		    || !preg_match_all($AuthUserPat, $apage['text'], $this->AuthUserData, PREG_SET_ORDER)
		) return array();
		$this->AuthUserAuth = array();
		foreach($this->AuthUserData as $m) {
			if (!preg_match_all('/\\bldaps?:\\S+|[^\\s,]+/', $m[2], $tokens)) continue;
#echo "DEBUG: m[2]=$m[2], m[3]=$m[3], tokens=<pre>".print_r($tokens,true)."</pre><br>\n";
			$this->AuthUserAuth[$m[1]] = empty($this->AuthUserAuth[$m[1]]) ? $tokens[0] : array_merge($this->AuthUserAuth[$m[1]], $tokens[0]);
		}
		#echo "DEBUG: AUP: Returning<pre>".print_r($this->array_match($userpat, $this->AuthUserAuth),true)."</pre><br>\n";
		return $this->array_match($userpat, $this->AuthUserAuth);
	}

    // array_match()
    //   Utility function to select elements of an array based on an 
    //   optional key-pattern applied by MatchPageName()
    // returns an array with all elements of the array which match keys
    function array_match($pat, $ary)
    {
#echo "array_match(): incoming ary=<pre>".print_r($ary,true)."</pre><br>\n";
        if ($pat) 
            return array_intersect_key($ary, array_flip(MatchPageNames(array_keys($ary), $pat)));
        return $ary;
    }


	function DelValue(&$page, $name, $value) {
		if (!preg_match('/^[@\w][^\s:]*$/', $name) || !preg_match('/^[^\s,]+$/', $value)) return FALSE;
		$page['text'] = preg_replace("/^(\s*$name:)(?:(.*?)[\s,]+)?$value([\s,]|$)/m", '$1$2$3', $page['text'], -1, $count);
        # Delete any "blank" lines (group-lines with no members, etc.)
		$page['text'] = preg_replace("/^(\s*$name:)[\s,]*\n/m", '', $page['text']);
		return $count;
	}

	function AddValue(&$page, $name, $value) {
		if (!preg_match('/^[@\w][^\s:]*$/', $name) || !preg_match('/^[^\s,]+$/', $value)) return FALSE;
		if (preg_match("/^\s*$name:/m", $page['text'])) {
			if (preg_match("/^\s*$name:(.*[\s,])?$value([\s,]|$)/m", $page['text'])) {
				return 0;
			} else {
				$page['text'] = preg_replace("/^(\s*$name:.*?)\s*$/m", "$1 $value", $page['text'], 1, $count);
				return $count;
			}
		} else {
			$page['text'] .= "\n$name: $value";
			return 1;
		}
	}

    var $Groups = array();
	############################################################################
	## ReadGroups()
	## with username: returns array('@groupA', '@groupB', ...)
	##  w/o username: returns array('@groupA' => array('userA', 'userB', ...), '@groupB' => ...)
	function ReadGroups($username=NULL) {
		if (!$this->Groups) {
			$this->Groups = $this->AuthUserPage('/^@/');
			$auth = $this->AuthUserPage('/^[^@]/');
            foreach ($auth as $k => $v) 
                foreach (preg_grep('/^@/', $v) as $g) {
                    $this->Groups[$g][] = $k;
                }
			foreach ($this->Groups as $g => $a) $this->Groups[$g] = array_unique($a);
			ksort($this->Groups);
		}

		if ($username) {
			$ug = array();
			foreach ($this->Groups as $g => $a) {
				if (in_array($username, $a) || in_array('*', $a)) $ug[] = $g;
				//if (in_array("-$username", $a)
			}
			return $ug;
		}

		return $this->Groups;
	}


	## returns array('userA', 'userB', ...)
	function ReadGroup($groupname) {
		$groups = $this->ReadGroups();
        return (array)@$groups[$groupname];
	}

    # $old is an array of the existing groups this user is a member of
    # $new is an array of the new groups this user will be a member of
    # return: 2 arrays, one the list of groups that needs to be added
    #         and the 2nd the list of groups that needs to be deleted
    # list($add, $del) = GroupDiff($old, $new);
    function GroupDiff($old, $new)
    {
        $del = $add = array();
        foreach ($old as $o)
            if (!in_array($o, $new))
                $del[] = $o;
        foreach ($new as $n)
            if (!in_array($n, $old))
                $add[] = $n;
        return array($add, $del);
    }

	## $add is an array of members to add to group
	## $del is an array of members to remove from group
	function WriteGroup($groupname, $add, $del, $csum='', $auth='read') {
		global $AuthUserPageFmt, $Now, $EditFunctions, $IsPagePosted;

		if (!preg_match('/^[@\w][^\s:]*$/', $groupname)) return FALSE;

		SDV($AuthUserPageFmt, '$SiteAdminGroup.AuthUser');
		$pn = FmtPageName($AuthUserPageFmt, '');
		Lock(2);
			$page = RetrieveAuthPage($pn, $auth, TRUE);
			if (!$page) Abort("?cannot write to $pn"); 
			$new = $page;

			$del_count = 0;
			if ($del) foreach ($del as $d) {
				$del_count += $this->DelValue($new, $d, $groupname);
				$del_count += $this->DelValue($new, $groupname, $d);
			}
			$add_count = 0;
			if ($add) foreach ($add as $a) {
				$add_count += $this->AddValue($new, $groupname, $a);
			}

			$new['csum'] = str_replace(array('$add', '$del'), array($add_count, $del_count), $csum);
			if ($csum) $new["csum:$Now"] = $new['csum'];

			PCache($pn, $new);
			$k = array_search('SaveAttributes', $EditFunctions);
			if ($k !== FALSE) unset($EditFunctions[$k]);
			UpdatePage($pn, $page, $new);
		Lock(0);
		return $IsPagePosted;
	}

	function ListGroups($pat=NULL) { return MatchPageNames(array_keys($this->ReadGroups()), $pat); }

	function AdminGroup($groupname) { return preg_match('/_admin$/', $groupname) ? $groupname : "{$groupname}_admin"; }


	############################################################################
	## users

	function ReadUser($username, $readgroup=false) {
		if (!($auth = $this->AuthUserPage($username))) return array();
		$data = array('username' => $username);
		foreach ($auth[$username] as $v) if ($v[0] == '$') $data['userpwhash'] = $v;
        if ($readgroup) $data['usergroups'] = $this->ReadGroups($username);
		return $data;
	}

	function WriteUser($username, $data, $csum='', $auth='read') { exit('UserAdmin::Write not implemented'); } // virtual


	function Exists($username) {
		return (boolean)$this->ReadUser($username);
	}

	function ListUsers($pat=NULL) {
		global $AuthUserFunctions;
		$pat = (array)$pat;
		$x = array('htpasswd' => 1, 'htgroup' => 1, 'ldap' => 1, 'userprofilegroup' => 1) + (array)@$AuthUserFunctions;
		array_push($pat, '!^('.implode('|', array_keys($x)).')$!');

		$ls = preg_grep('/^\w/', array_keys($this->AuthUserPage()));
		return MatchPageNames($ls, $pat);
	}


	############################################################################
	## utility functions

	function MailUser($username, $fmt, $opt = array()) {
		if ($username) $opt = array_merge($this->ReadUser($username), $opt);
		if (empty($opt['useremail'])) return FALSE;

		$fmt = array_merge(array('to' => '$useremail', 'head' => 'From: '.XL('UA_email_from')), $fmt);
		$msg = array();
		foreach ($fmt as $fk => $f) {
			foreach($opt as $k => $v) $f = preg_replace("/\\$$k\b`?/", $v, $f);
			$msg[$fk] = $f;
		}
		$msg['body'] = preg_replace('/^\t+/m', '', $msg['body']); ## allow pretty XLSDV with indentation

		//exit(pre_r($msg));
		return mail($msg['to'], @$msg['subject'], @$msg['body'], $msg['head']);
	}

	function MakeActivationKey() { return strval(mt_rand() + 1); }

	function MakeUserLink($username, $useraction = '', $opt = array()) {
		global $ScriptUrl;
		$action = $useraction ? 'user/'.urlencode($useraction) : 'user';
		$url = "$ScriptUrl?action=$action&username=".urlencode($username);
		foreach ($opt as $k => $v) $url .= '&'.urlencode($k).'='.urlencode($v);
		return $url;
	}


	############################################################################
	## authentication

	## return TRUE for authenticated user with admin rights
	function Superuser($pagename, $prompt=FALSE) {
		return (boolean)RetrieveAuthPage($pagename, 'admin', $prompt, READPAGE_CURRENT);
	}

	function Username($pagename, $opt) {
		global $AuthId;
		$n = @$opt['username'] or !$this->Superuser($pagename) and $n = @$AuthId;
		if (!$n) return FALSE;
		if (method_exists($this, 'ValidName') && !$this->ValidName($n)) return FALSE;
		if (!$this->Exists($n)) return FALSE;
		return $n;
	}

	function AuthorizedUser($pagename, $username, $auth='edit', $prompt=FALSE) {
		global $AuthId;
		return ($username && ($AuthId == $username)) || $this->Superuser($pagename, $prompt);
	}

	function AuthorizedGroup($pagename, $groupname, $auth='edit', $prompt=FALSE) {
		global $AuthId, $AuthList;
		if ($groupname[0] != '@') return FALSE;
		return !empty($AuthList[$this->AdminGroup($groupname)]) || $this->Superuser($pagename, $prompt);
	}


	############################################################################
	## input processing

	## returns TRUE if form has been posted
	function ReadInput($pagename, $valid_username=TRUE) {
		if (preg_grep('/^cancel/', array_keys($_POST))) Redirect($pagename, '$PageUrl?action=user');
		
		$this->input = array_merge($_GET, $_POST);
		if ($valid_username) $this->input['username'] = $this->Username($pagename, $_REQUEST);

		return (boolean)preg_grep('/^post/', array_keys($_POST));
	}

	function ValidEmail(&$address) { return !$address || preg_match('/^.+@.+\..+$/', $address); }

	function ValidateInput($fmt = '') {
		if (!$fmt) $fmt = $this->action;
		if (empty($this->fields[$fmt])) return 'UA_fail_unknown_action';
		$result = array();
		foreach ($this->fields[$fmt] as $k => $req) {
			$this->input[$k] = stripmagic($this->input[$k]);
			if (!($req & UA_REQ_ANY) && empty($this->input[$k])) {
				$result[$k] = "UA_empty_$k";
				continue;
			}
			if (($req & UA_REQ_TWICE) && (stripmagic(@$this->input["{$k}2"]) != $this->input[$k])) {
				$result[$k] = "UA_diff_$k";
				continue;
			}
			$vm = preg_replace('/^user(.+)$/e', "'Valid'.ucfirst('$1')", $k);
			if (method_exists($this, $vm) && !$this->{$vm}($this->input[$k])) {
				$result[$k] = "UA_invalid_$k";
				continue;
			}
		}
		return $result;
	}


	####################################################################################################################
	##                                                                                                   action handlers

	function HandleNew($pagename) {
		if (!$this->ReadInput($pagename, FALSE)) return NULL;

		$result = $this->ValidateInput();
		$username = $this->input['username'];
		if ($this->Exists($username)) $result['username'] = 'UA_exists';
		if ($result) return $result;

		$hash = crypt($this->input['userpasswd']);
		if ($this->confirm_email && !$this->Superuser($pagename)) {
			$key = $this->MakeActivationKey();
			$link = $this->MakeUserLink($username, 'unlock', array('key' => $key));
			//$link = $this->MakeActivationLink($username, $key);

			$mail_fmt = array(
				'subject' => XL('UAnew_email_subject'),
				'body' => XL('UAnew_email_body')
			);
			$mail_opt = array(
				'username' => $username,
				'useremail' => $this->input['useremail'],
				'key' => $key,
				'link' => $link
			);
			if (!$this->MailUser('', $mail_fmt, $mail_opt)) return array('UA_email_fail', 'UA_contact_admin');

			$success = array('UAnew_success', 'UAnew_email_sent');
			$data = array(
				'useremail' => $this->input['useremail'],
				'username' => $username,
				'userkey' => "$key $hash",
			);
		} else {
			$success = 'UAnew_success';
			$data = array(
				'userpwhash' => $hash,
				'useremail' => @$this->input['useremail'],
				'username' => $username,
			);
		}

		$fail = array('UAnew_fail', 'UA_contact_admin');
		return $this->WriteUser($username, $data, implode('; ', array_map('XL', (array)$success))) ? $success : $fail;
	}


	function HandleResetpasswd($pagename) {
		$posted = $this->ReadInput($pagename);
		if (!$posted) return NULL;

		$result = $this->ValidateInput();
		if ($result) return $result;

		$username = $this->input['username'];
		$user = $this->ReadUser($username);
		if (!$user) return array('UAresetpasswd_fail', 'UA_invalid_username');
		if (empty($user['useremail'])) return array('UAresetpasswd_fail', 'UAresetpasswd_email_empty', 'UA_contact_admin');

		$key = $this->MakeActivationKey();
		$link = $this->MakeUserLink($username, 'unlock', array('key' => $key));
		//$link = $this->MakeActivationLink($username, $key);

		$mail_fmt = array(
			'subject' => XL('UAresetpasswd_email_subject'),
			'body' => XL('UAresetpasswd_email_body')
		);
		$mail_opt = array(
			'key' => $key,
			'link' => $link
		);
		if (!$this->MailUser($username, $mail_fmt, $mail_opt)) return array('UA_email_fail', 'UA_contact_admin');

		return $this->WriteUser($username, array('userkey' => $key), XL('UAresetpasswd_success'))
			? 'UAresetpasswd_success'
			: array('UAresetpasswd_fail', 'UA_contact_admin');
	}


	## handles e-mail address verification and password resets
	function HandleUnlock($pagename) {
		$posted = $this->ReadInput($pagename);
#echo "Checking input[username]: <pre>".print_r($this->input,true)."</pre><br>\n";
		if (empty($this->input['username'])) return NULL;
#echo "GOT HERE<br>\n";
		$user = $this->ReadUser($this->input['username']);
		if (!$user) return array('UAunlock_fail', 'UA_invalid_username');
		if (empty($user['userkey'])) return array('UAunlock_fail', 'UAunlock_already_active');

		$result = $this->ValidateInput();
		if ($result) return $result;

		$key = preg_replace('/[^0-9]+/', '', $this->input['key']);
		if (!preg_match("/^$key( .*)?$/", $user['userkey'], $match)) return array('UAunlock_fail', 'UAunlock_bad_key');
		$hash = trim($match[1]);
		
		if (!$hash) {
			$this->fields['unlock'] = $this->fields['newpasswd'];
			if (!$posted) return NULL;
			$result = $this->ValidateInput('newpasswd');
			if ($result) return $result;
			$hash = crypt($this->input['userpasswd']);
			$reset = TRUE;
		} else $reset = FALSE;

		$result = $reset ? 'UAunlock_success_set_pw' : 'UAunlock_success_activated';
		$data = array('userpwhash' => $hash, 'userkey' => '');
		return $this->WriteUser($this->input['username'], $data, XL($result))
			? $result
			: array('UAnew_fail', 'UA_contact_admin');
	}


	function HandleEdit($pagename) {
		$posted = $this->ReadInput($pagename);
		unset($this->input['usergroups']);

		$username = $this->input['username'];
		if (empty($username)) {
			$this->fields[$this->action] = array('username' => UA_REQ_NOT_EMPTY);
			return NULL; // results in a list of users to choose from
		}

		if (!$this->AuthorizedUser($pagename, $username, 'edit', TRUE)) return array('UAedit_fail', 'UA_unauthorized');

		$admin = $this->Superuser($pagename);
		if ($admin) unset($this->fields[$this->action]['useroldpasswd']);

		$user = $this->ReadUser($username);
		if (!$user) return array(
			'UAedit_fail',
			$this->Exists($username) ? 'UA_unsupported_user_format' : 'UA_invalid_username'
		);
		$ef = preg_grep('/passwd|^username$/', array_keys($this->fields[$this->action]), PREG_GREP_INVERT);
		$posted = FALSE;
		foreach ($ef as $f) {
			if (isset($this->input[$f])) $posted = TRUE;
			else $this->input[$f] = @$user[$f];
		}

		if (!$posted) return NULL;

		$result = $this->ValidateInput();
		if ($result) return $result;

		if (!$admin && (_crypt($this->input['useroldpasswd'], $user['userpwhash']) != $user['userpwhash']))
			return 'UA_wrong_passwd';

		$data = array();
		foreach ($this->fields[$this->action] as $f => $req) {
			if (($req & UA_REQ_PRESET) || preg_match('/passwd/', $f)) continue;
			if (@$this->input[$f] === @$user[$f]) continue;
			$data[$f] = @$this->input[$f];
		}
		if (!empty($this->input['userpasswd'])) $data['userpwhash'] = crypt($this->input['userpasswd']);
		if (!$data) return 'UAedit_success_unchanged';

		return $this->WriteUser($user['username'], $data, XL('UAedit_success'))
			? 'UAedit_success'
			: array('UAedit_fail', 'UA_contact_admin');
	}


	function HandleGroup($pagename) {
		global $UserAdminFmt;
		$posted = $this->ReadInput($pagename, FALSE);

		$fields = &$this->fields[$this->action];

		if (empty($this->input['gn'])) {
			$fields = array('groupname' => UA_REQ_NOT_EMPTY);
			return NULL;
		} else $groupname = $this->input['gn'];

		if (!$this->AuthorizedGroup($pagename, $groupname, 'edit', TRUE)) return array('UAgroup_fail', 'UA_unauthorized');

		$groupmembers = $this->ReadGroup($groupname);
        $allusers = $this->ListUsers();
		$UserAdminFmt = "[++$[Group:] $groupname++]\n\n";
		$UserAdminFmt .= '$[Current group members:]';
		$UserAdminFmt .= "\n(:input form action='\$PageUrl?action=user/{$this->action}&gn=$groupname' class='uag-users':)";
		foreach ($allusers as $n) {
            if ($posted)
                $checked = !empty($this->input['select']) && in_array($n, $this->input['select']) ? ' checked' : '';
            else
                $checked = !empty($groupmembers) && in_array($n, $groupmembers) ? ' checked' : '';
			if (preg_match('/^\w/', $n) && $this->Exists($n)) {
				$url = $this->MakeUserLink($n, 'edit');
				$txt = "[[$url|$n]]";
			} else $txt = $n;
			$UserAdminFmt .= "\n* (:input checkbox name='select[]' value='$n' $checked:) $txt";
		}
		$UserAdminFmt .= "\n(:input submit name=postupdate value='$[Update Group Membership]':)\n(:input end:)";

		#$UserAdminFmt .= "\n\n$[Add new group members (each on a separate line):]";
		$UserAdminFmt .= "\n(:input form action='\$PageUrl?action=user/{$this->action}&gn=$groupname' class='uag-new':)";
		#$UserAdminFmt .= "\n(:input textarea name=new rows=8 cols=30:)";
		#$UserAdminFmt .= "\n(:input submit name=postadd value='$[Add new members]':)\n(:input end:)";
		$UserAdminFmt .= "\n(:input end:)";


		#$UserAdminFmt .= "\n\n".Keep(pre_r($this->input));

		#return NULL;

        #echo "Shall we post?<br>\n";
        if ($posted) {
            list($add, $del) = $this->GroupDiff($groupmembers, $this->input['select']);
            #echo "DEBUG: del=<pre>".print_r($del,true)."</pre><br>, add=<pre>".print_r($add,true)."</pre><br>\n";
            return $this->WriteGroup($groupname, $add, $del, '', 'edit');
        }
        #echo "Apparently not...<br>\n";

		//$this->WriteGroup('@obix', array(), array('EA3'), 'WriteGroup test: +$add/-$del');
	}


	####################################################################################################################
	##                                                                                                      display page

	function Menu($pagename) {
		global $AuthId, $UserAdminAnonActions;
		SDV($UserAdminAnonActions, array('new', 'resetpasswd', 'unlock'));

		$actions = preg_replace(
			'/^Handle(.)/e', "strtolower('$1')",
			preg_grep('/^Handle/', get_class_methods($this))
		);

		$username = $this->Username($pagename, $_REQUEST);
		if ($this->AuthorizedUser($pagename, $username, 'edit') && !$this->Superuser($pagename)) {
			$actions = array_diff($actions, $UserAdminAnonActions);
		} else if (empty($AuthId)) {
			$actions = array_intersect($actions, $UserAdminAnonActions);
		}

		$out = "\n!!! Available actions\n";
		foreach($actions as $a) $out .= "* [[ {\$PageUrl}?action=user/$a | $[UA{$a}_title] ]]\n";
		if (empty($AuthId)) $out .= "* [[ {\$PageUrl}?action=user/edit | $[Login to edit your account details] ]]\n";
		return $out;
	}


	function Form($pagename, $result) {
		global $InputValues;

		if (empty($this->fields[$this->action])) return '';

		$f = $this->fields[$this->action];
		if (count($f) == 1) switch (key($f)) {
			case 'username':
				$list = '$[Please select a user:]';
				foreach ($this->ListUsers() as $n) {
					$url = $this->MakeUserLink($n, 'edit');
					$list .= "\n* [[$url|$n]]";
				}
				return $list;
			case 'groupname':
				$list = '$[Please select a group:]';
				$url = $this->MakeUserLink('', 'group');
				foreach ($this->ListGroups() as $n) {
					$list .= "\n* [[$url&gn=$n|$n]]";
				}
				return $list;
		}

		$form = "(:input form action='\$PageUrl?action=user/{$this->action}':)";
		$form .= "\n(:table class=ua-form:)";
		foreach ($f as $k => $req) {
			$highlight = isset($result[$k]) ? ' class=ua-error' : '';
			$form .= "\n(:cellnr$highlight:)$[UA_txt_$k]\n(:cell$highlight:)";

			## note: autocomplete is not included by default in $InputAttrs, so setting it won't do anything
			$type = (strpos($k, 'passwd') !== FALSE) ? 'password autocomplete=off' : 'text';

			$req_note = !($req & UA_REQ_ANY);
			if ($req & UA_REQ_PRESET) {
				if (!($req_note && empty($InputValues[$k]))) {
					$req_note = FALSE;
					$form .= "'''{$InputValues[$k]}'''";
					$type = 'hidden';
				}
			}

			if (($k == 'username') && ($type == 'text') && ($this->action != 'new')) $type = 'select';

			$form .= "(:input $type name='$k':)";
			if ($req_note) $form .= ' *';

			if ($req & UA_REQ_TWICE) {
				$form .= "\n(:cellnr$highlight:)$[UA_txt_{$k}2]\n(:cell$highlight:)(:input $type name='{$k}2':)";
				if ($req_note) $form .= ' *';
			}
		}

		$form .= "\n(:cellnr:)\n(:cell:)(:input submit name=post value='$[UA{$this->action}_submit]':) (:input submit name=cancel value='$[Cancel]':)";
		$form .= "\n(:tableend:)\n(:input end:)";

		return $form;
	}

	function FormExpand($markup) {
		if (!preg_match('/^\(:input select\b(.*?):\)$/', $markup, $match))
			return preg_replace('/\(:input select\b.*?:\)/e', "\$this->FormExpand('$0')", $markup);

		$opt = ParseArgs($match[1]);
		if (empty($opt['name'])) {
			if (empty($opt[''])) return $markup;
			else $opt['name'] = $opt[''][0];
		}
		switch($opt['name']) {
			case 'username':
				//$out = "(:input select name='username' value='' '':)";
				//foreach ($this->ListUsers(NULL, TRUE) as $n) $out .= "\n(:input select name='username' value='$n' '$n':)";
				foreach ($this->ListUsers(NULL, TRUE) as $n) {
					$url = $this->MakeUserLink($n, 'edit');
					$out .= "\n* [[$url|$n]]";
				}
				return $out;
			default:
				return $markup;
		}
	}

	function PrintPage($pagename, $result) {
		global $MessagesFmt, $SiteGroup, $InputValues, $PageStartFmt, $PageEndFmt, $UserAdminForm, $UserAdminFmt, $HandleUserAdminFmt;
//$ls = array($this->ListGroups('@a*'), $this->ListUsers('/^[a-z]/', TRUE));
//exit(pre_r($ls));
//echo "DEBUG: PrintPage(): result=<pre>".print_r($result,true)."</pre><br>\n";
		$status = preg_match('/(\b|_)(fail|success)(\b|_)/', implode(' ', (array)$result), $m) ? " ua-{$m[2]}" : '';

		if ($result) $MessagesFmt[] = "<h3 class='wikimessage$status'>$[".implode("]<br />\n$[", (array)$result).']</h3>';

		foreach((array)$this->input as $k => $v)
			if (!is_array($v) && (strpos($k, 'passwd') === FALSE)) $InputValues[$k] = htmlspecialchars($v, ENT_QUOTES);

		if (empty($UserAdminFmt)) {
			if (!$this->action || $status) $UserAdminFmt = $this->Menu($pagename);
			else {
				//if (PageExists("$SiteGroup.UserAdminTemplates")) SDV($UserAdminForm, "$SiteGroup.UserAdminTemplates");
				if (empty($UserAdminForm) || !($UserAdminFmt = RetrieveAuthSection($UserAdminForm, "#ua-{$this->action}")))
					$UserAdminFmt = $this->Form($pagename, $result);
				$UserAdminFmt .= '$[UA_return_link]';
			}
			$UserAdminFmt = $this->FormExpand($UserAdminFmt);
		}

		$UserAdminFmt = MarkupToHTML($pagename, "(:messages:)\n\n$UserAdminFmt");

		$title = XL("UA{$this->action}_title");
		$username = $this->Username($pagename, $_REQUEST);
		if ($username && ($this->action != 'new')) $title .= ": $username";
		PCache($pagename, array('title' => $title));

		SDV($HandleUserAdminFmt, array(&$PageStartFmt, &$UserAdminFmt, &$PageEndFmt));
		PrintFmt($pagename, $HandleUserAdminFmt);
	}

}