<?php if (!defined('PmWiki')) exit();

/*	=== 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'] = '2010-06-04';


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;

	$pform = RetrieveAuthPage($pagename, $auth, TRUE);
	if (!$pform) Abort("?read error");
	$pform['title'] = XL("UA{$UserAdmin->action}_title");
	PCache($pagename, $pform);

	$hm = 'Handle'.ucfirst($UserAdmin->action);
	if (method_exists($UserAdmin, $hm)) $result = $UserAdmin->{$hm}($pagename);
	else { $result = NULL; $UserAdmin->action = NULL; }

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


########################################################################################################################
##                                                                                                          set defaults
SDVA($FmtPV, array(
	'$Username' => '$GLOBALS["UserAdmin"]->Username($pn, $_REQUEST)'
));

SDVA($Conditions, array(
	'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_fmt' => "<br />\n<a href='\$PageUrl?action=user'>Return to user actions</a>",
	'UA_title' => 'User account management',
	'UA_txt_key' => 'Activation key',
	'UA_txt_useremail' => 'E-mail address',
	'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',

	'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',
));


########################################################################################################################
##                                                                                                       framework class
class UserAdmin {
	const REQ_NOT_EMPTY = 0;
	const REQ_ANY = 1;
	const REQ_TWICE = 2; ## implies not empty
	const REQ_TWICE_ANY = 3;
	const REQ_PRESET = 4; ## implies not empty

	var $confirm_email = TRUE;
	var $fields = array(
		'new' => array(
			'username' => UserAdmin::REQ_NOT_EMPTY,
			'userpasswd' => UserAdmin::REQ_TWICE,
			'useremail' => UserAdmin::REQ_NOT_EMPTY),
		'resetpasswd' => array(
			'username' => UserAdmin::REQ_NOT_EMPTY),
		'unlock' => array(
			'username' => UserAdmin::REQ_PRESET,
			'key' => UserAdmin::REQ_PRESET),
		'newpasswd' => array(
			'username' => UserAdmin::REQ_PRESET,
			'key' => UserAdmin::REQ_PRESET,
			'userpasswd' => UserAdmin::REQ_TWICE),
		'edit' => array(
			'username' => UserAdmin::REQ_PRESET,
			'useroldpasswd' => UserAdmin::REQ_NOT_EMPTY,
			'useremail' => UserAdmin::REQ_NOT_EMPTY,
			'userpasswd' => UserAdmin::REQ_TWICE_ANY),
	);

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


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

	function ExistsInAuthUserPage($username) {
		global $AuthUserPageFmt;
		$username = trim($username);
		if (preg_match('/^\w[^\s:]*$/', $username)) {
			SDV($AuthUserPageFmt, '$SiteAdminGroup.AuthUser');
			$pn = FmtPageName($AuthUserPageFmt, $pagename);
			$apage = ReadPage($pn, READPAGE_CURRENT);
			if ($apage && preg_match("/^\s*$username:/m", $apage['text'])) return TRUE;
		}
		return FALSE;
	}

	function Exists($username) {
		return $this->ExistsInAuthUserPage($username) || $this->Read($username);
	}

	function MailUser($username, $fmt, $opt = array()) {
		if ($username) $opt = array_merge($this->Read($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 MakeActivationLink($username, $key) {
		return "{$GLOBALS['ScriptUrl']}?action=user/unlock&username=".urlencode($username).'&key='.urlencode($key);
	}

	function Username($pagename, $opt) {
		global $AuthId;
		$n = @$opt['username'] or !$this->Superuser() 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;
	}

	## return TRUE for authenticated user with admin rights
	function Superuser($prompt=FALSE) { return FALSE; }

	function Authorized($name, $auth='edit', $prompt=FALSE) {
		global $AuthId;
		return ($name && ($AuthId == $name)) || $this->Superuser($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 & UserAdmin::REQ_ANY) && empty($this->input[$k])) {
				$result[$k] = "UA_empty_$k";
				continue;
			}
			if (($req & UserAdmin::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();
		$uname = $this->input['username'];
		if ($this->Exists($uname)) $result['username'] = 'UA_exists';
		if ($result) return $result;

		$hash = crypt($this->input['userpasswd']);
		if ($this->confirm_email && !$this->Superuser()) {
			$key = $this->MakeActivationKey();
			$link = $this->MakeActivationLink($uname, $key);

			$mail_fmt = array(
				'subject' => XL('UAnew_email_subject'),
				'body' => XL('UAnew_email_body')
			);
			$mail_opt = array(
				'username' => $uname,
				'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' => $uname,
				'userkey' => "$key $hash",
			);
		} else {
			$success = 'UAnew_success';
			$data = array(
				'userpwhash' => $hash,
				'useremail' => @$this->input['useremail'],
				'username' => $uname,
			);
		}

		$fail = array('UAnew_fail', 'UA_contact_admin');
		return $this->Write($uname, $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;

		$uname = $this->input['username'];
		$user = $this->Read($uname);
		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->MakeActivationLink($uname, $key);

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

		return $this->Write($uname, 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);
		if (empty($this->input['username'])) return NULL;

		$user = $this->Read($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->Write($this->input['username'], $data, XL($result))
			? $result
			: array('UAnew_fail', 'UA_contact_admin');
	}


	function HandleEdit($pagename) {
		$posted = $this->ReadInput($pagename);

		$uname = $this->input['username'];
		if (empty($uname)) {
			$this->fields[$this->action] = array('username' => UserAdmin::REQ_NOT_EMPTY);
			return NULL;
		}

		if (!$this->Authorized($uname, 'edit', TRUE)) return array('UAedit_fail', 'UA_unauthorized');

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

		$user = $this->Read($uname);
		if (!$user) return array(
			'UAedit_fail',
			$this->Exists($uname) ? '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 & UserAdmin::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->Write($user['username'], $data, XL('UAedit_success'))
			? 'UAedit_success'
			: array('UAedit_fail', 'UA_contact_admin');
	}


	####################################################################################################################
	##                                                                                                      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))
		);

		if ($this->Authorized($this->Username($pagename, $_REQUEST), 'edit') && !$this->Superuser()) {
			$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";
		return $out;
	}


	function Form($pagename, $result) {
		if (empty($this->fields[$this->action])) return '';

		$form = "<form action='\$PageUrl?action=user/{$this->action}' method='post' autocomplete='off'>";
		$form .= "\n<table class='ua-form'>";
		$aform = array();
		foreach ($this->fields[$this->action] as $k => $req) {
			$type = (strpos($k, 'passwd') !== FALSE) ? 'password' : 'text';
			$value = (($type == 'password') || empty($this->input[$k])) ? '' : htmlspecialchars($this->input[$k], ENT_QUOTES);
			$highlight = isset($result[$k]) ? ' class=\'ua-error\'' : '';

			if ($value && ($req & UserAdmin::REQ_PRESET)) {
				$form .= "\n<tr$highlight><td>$[UA_txt_$k]</td><td><b>$value</b><input type='hidden' name='$k' value='$value' /></td></tr>";
				continue;
			}
			$post = ($req & UserAdmin::REQ_ANY) ? '' : ' *';
			$form .= "\n<tr$highlight><td>$[UA_txt_$k]</td><td><input type='$type' name='$k' value='$value' />$post</td></tr>";

			if ($req & UserAdmin::REQ_TWICE)
				$form .= "\n<tr$highlight><td>$[UA_txt_{$k}2]</td><td><input type='$type' name='{$k}2' />$post</td></tr>";
		}

		$form .= "\n<tr><td></td><td><input type='submit' name='post' value='$[UA{$this->action}_submit]' /></td></tr>";
		$form .= "\n</table></form>";

		return $form;
	}

	function PrintPage($pagename, $result) {
		global $MessagesFmt, $SiteGroup, $InputValues, $PageStartFmt, $PageEndFmt, $UserAdminForm, $UserAdminFmt, $HandleUserAdminFmt;

		$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>';

		if (empty($UserAdminFmt)) {
			if (!$this->action || $status) $UserAdminFmt = MarkupToHTML($pagename, "(:messages:)\n\n".$this->Menu($pagename));
			else {
				$UserAdminFmt = '';
				if (!empty($MessagesFmt)) $UserAdminFmt .= FmtPageName(implode('', (array)$MessagesFmt), $pagename);

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

				if (PageExists("$SiteGroup.UserAdminTemplates")) SDV($UserAdminForm, "$SiteGroup.UserAdminTemplates");
				if (empty($UserAdminForm)) $form = $this->Form($pagename, $result);
				else {
					$mform = RetrieveAuthSection($UserAdminForm, "#ua-{$this->action}");
					$form = $mform ? MarkupToHTML($pagename, $mform) : $this->Form($pagename, $result);
				}
				$UserAdminFmt .= $form;

				$UserAdminFmt .= '$[UA_return_link_fmt]';
			}
		};

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

}