*
* 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 .
*/
$RecipeInfo['UserAdmin']['Version'] = '2010-06-07';
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
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'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_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'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's e-mail address',
'UAresetpasswd_success' => 'Password reset link sent to user'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($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;
}
## return TRUE for authenticated user with admin rights
function Superuser($pagename, $prompt=FALSE) {
return (boolean)RetrieveAuthPage($pagename, 'admin', $prompt, READPAGE_CURRENT);
}
function Authorized($pagename, $username, $auth='edit', $prompt=FALSE) {
global $AuthId;
return ($username && ($AuthId == $username)) || $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 & 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();
$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->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->Write($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->Read($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->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->Write($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);
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);
$username = $this->input['username'];
if (empty($username)) {
$this->fields[$this->action] = array('username' => UserAdmin::REQ_NOT_EMPTY);
return NULL;
}
if (!$this->Authorized($pagename, $username, 'edit', TRUE)) return array('UAedit_fail', 'UA_unauthorized');
$admin = $this->Superuser($pagename);
if ($admin) unset($this->fields[$this->action]['useroldpasswd']);
$user = $this->Read($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 & 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))
);
$username = $this->Username($pagename, $_REQUEST);
if ($this->Authorized($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";
return $out;
}
function Form($pagename, $result) {
global $InputValues;
if (empty($this->fields[$this->action])) return '';
$form = "(:input form action='\$PageUrl?action=user/{$this->action}':)";
$form .= "\n(:table class=ua-form:)";
foreach ($this->fields[$this->action] 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';
if ($req & UserAdmin::REQ_PRESET) {
$req_note = empty($InputValues[$k]);
if (!$req_note) {
$form .= "'''{$InputValues[$k]}'''";
$type = 'hidden';
}
} else $req_note = !($req & UserAdmin::REQ_ANY);
$form .= "(:input $type name='$k':)";
if ($req_note) $form .= ' *';
if ($req & UserAdmin::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 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[] = "
$[".implode("]
\n$[", (array)$result).']
';
foreach((array)$this->input as $k => $v)
if (strpos($k, 'passwd') === FALSE) $InputValues[$k] = htmlspecialchars($v, ENT_QUOTES);
if (empty($UserAdminFmt)) {
if (!$this->action || $status) $markup = $this->Menu($pagename);
else {
if (PageExists("$SiteGroup.UserAdminTemplates")) SDV($UserAdminForm, "$SiteGroup.UserAdminTemplates");
if (empty($UserAdminForm) || !($markup = RetrieveAuthSection($UserAdminForm, "#ua-{$this->action}")))
$markup = $this->Form($pagename, $result);
$markup .= '$[UA_return_link]';
}
$UserAdminFmt = MarkupToHTML($pagename, "(:messages:)\n\n$markup");
}
$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);
}
}