* * 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-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'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' => "
\nReturn 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() 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 .= "\n"; $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"; continue; } $post = ($req & UserAdmin::REQ_ANY) ? '' : ' *'; $form .= "\n"; if ($req & UserAdmin::REQ_TWICE) $form .= "\n"; } $form .= "\n"; $form .= "\n
$[UA_txt_$k]$value
$[UA_txt_$k]$post
$[UA_txt_{$k}2]$post
"; 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).']

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