* * 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-03'; SDV($HandleActions[$action], 'HandleUserAdmin'); function HandleUserAdmin($pagename, $auth = 'read') { global $action, $UserAdmin; $UserAdmin->action = preg_match('#^user/(.+)$#', $action, $m) ? preg_replace('/\W+/', '', $m[1]) : 'menu'; $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)) $hm = 'HandleMenu'; $result = $UserAdmin->{$hm}($pagename); $UserAdmin->PrintPage($pagename, $result); } ######################################################################################################################## ## set defaults 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_fmt' => "
\n$[UA_return_link_title]", 'UA_return_link_title' => 'Return to user actions', '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', 'UAmenu_title' => 'User admin', '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()) { $fmt = array_merge(array('head' => 'From: '.XL('UA_email_from')), $fmt); if ($username) $opt = array_merge($this->Read($username), $opt); if (empty($opt['useremail'])) return FALSE; $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($opt['useremail'], @$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) { if (empty($opt['username'])) return FALSE; $n = $opt['username']; 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') { global $AuthId; return ($name && ($AuthId == $name)) || $this->Superuser(TRUE); } #################################################################################################################### ## input processing ## returns TRUE if form has been posted function ReadInput($pagename) { $this->input = array_merge($_GET, $_POST); $this->input['username'] = $this->Username($pagename, $_REQUEST); return 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[] = "UA_empty_$k"; continue; } if (($req & UserAdmin::REQ_TWICE) && (stripmagic(@$this->input["{$k}2"]) != $this->input[$k])) { $result[] = "UA_diff_$k"; continue; } $vm = preg_replace('/^user(.+)$/e', "'Valid'.ucfirst('$1')", $k); if (method_exists($this, $vm) && !$this->{$vm}($this->input[$k])) { $result[] = "UA_invalid_$k"; continue; } } return $result; } #################################################################################################################### ## action handlers function HandleNew($pagename) { if (!$this->ReadInput($pagename)) return NULL; $result = $this->ValidateInput(); if ($this->Exists($this->input['username'])) $result[] = 'UA_exists'; if ($result) return $result; $hash = crypt($this->input['userpasswd']); if ($this->confirm_email) { $key = $this->MakeActivationKey(); $link = $this->MakeActivationLink($this->input['username'], $key); $mail_fmt = array('subject' => XL('UAnew_email_subject'), 'body' => XL('UAnew_email_body')); $mail_opt = array('username' => $this->input['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' => $this->input['username'], 'userkey' => "$key $hash", ); } else { $success = 'UAnew_success'; $data = array( 'userpwhash' => $hash, 'useremail' => @$this->input['useremail'], 'username' => $this->input['username'], ); } $fail = array('UAnew_fail', 'UA_contact_admin'); return $this->Write($this->input['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; $user = $this->Read($this->input['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($user['username'], $key); $mail_fmt = array('subject' => XL('UAresetpasswd_email_subject'), 'body' => XL('UAresetpasswd_email_body')); $mail_opt = array('username' => $user['username'], 'useremail' => $user['useremail'], 'key' => $key, 'link' => $link); if (!$this->MailUser('', $mail_fmt, $mail_opt)) return array('UA_email_fail', 'UA_contact_admin'); return $this->Write($user['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); if (empty($this->input['username'])) { $this->fields[$this->action] = array('username' => UserAdmin::REQ_NOT_EMPTY); return NULL; } if (!$this->Authorized($this->input['username'], 'edit')) return array('UAedit_fail', 'UA_unauthorized'); $admin = $this->Superuser(); if ($admin) unset($this->fields[$this->action]['useroldpasswd']); $user = $this->Read($this->input['username']); if (!$user) return array('UAedit_fail', $this->Exists($this->input['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'); } function HandleMenu($pagename) { global $AuthId, $PCache, $UserAdminAnonActions, $UserAdminFmt; $actions = array(); foreach(preg_grep('/^Handle/', get_class_methods($this)) as $h) { if ($h == 'HandleMenu') continue; $actions[] = preg_replace('/^Handle(.)/e', "strtolower('$1')", $h); } if (!empty($AuthId) && empty($_GET['list-all'])) { SDV($UserAdminAnonActions, array('new', 'resetpasswd', 'unlock')); $actions = array_diff($actions, $UserAdminAnonActions); } if (empty($UserAdminFmt)) $UserAdminFmt = ''; $UserAdminFmt .= "\n"; $name = $this->Username($pagename, $_REQUEST); if ($name && !empty($PCache[$pagename]['title'])) $PCache[$pagename]['title'] .= ": $name"; return NULL; } #################################################################################################################### ## display form function Form($pagename) { if (empty($this->fields[$this->action])) return ''; $form = FmtPageName("\n
", $pagename); foreach (array('n' => $pagename, 'action' => "user/{$this->action}") as $k => $v) $form .= "\n"; foreach ($this->fields[$this->action] as $k => $req) { if (strpos($k, 'passwd') !== FALSE) { $value = ''; $type = 'password'; } else { $value = isset($this->input[$k]) ? htmlspecialchars($this->input[$k], ENT_QUOTES) : ''; $type = 'text'; } if ($value && ($req & UserAdmin::REQ_PRESET)) { $form .= "\n"; continue; } $post = ($req & UserAdmin::REQ_ANY) ? '' : ' *'; $form .= "\n"; if ($req & UserAdmin::REQ_TWICE) $form .= "\n"; } $form .= FmtPageName("\n
".XL("UA_txt_$k")."$value
".XL("UA_txt_$k")."$post
".XL("UA_txt_{$k}2")."$post
\n
", $pagename); return $form; } function PrintPage($pagename, $result) { global $MessagesFmt, $PageStartFmt, $PageEndFmt, $UserAdminFmt, $HandleUserAdminFmt; $exit_cond = preg_match('/(\b|_)(fail|success)(\b|_)/', implode(' ', (array)$result), $m) ? " ua-{$m[2]}" : ''; if ($result) $MessagesFmt[] = "

$[".implode("]
\n$[", (array)$result).']

'; if (empty($UserAdminFmt)) { $UserAdminFmt = ''; if (!empty($MessagesFmt)) $UserAdminFmt .= FmtPageName(implode('', (array)$MessagesFmt), $pagename); if (!$exit_cond) $UserAdminFmt .= $this->Form($pagename); $UserAdminFmt .= FmtPageName('$[UA_return_link_fmt]', $pagename); } SDV($HandleUserAdminFmt, array(&$PageStartFmt, &$UserAdminFmt, &$PageEndFmt)); PrintFmt($pagename, $HandleUserAdminFmt); } }