File List
<?php
/**
* Definition of Userlogin class
*
* @author Lindsay Marshall <lindsay.marshall@ncl.ac.uk>
* @copyright 2012-2021 Newcastle University
* @package Framework
* @subpackage SystemPages
*/
namespace Framework\Pages;
use \Config\Config;
use \Config\Framework as FW;
use \Framework\Local;
use \R;
use \Support\Context;
/**
* A class to handle the /login, /logout, /register, /forgot and /resend actions
*/
final class UserLogin extends \Framework\SiteAction
{
use \Support\Login;
/**
* Find a user based on either a login or an email address
*
* @used-by \Support\Login
*
* @param $lg A username or email address
*/
public static function eorl(string $lg) : ?\RedBeanPHP\OODBBean
{
return R::findOne(FW::USER, (\filter_var($lg, FILTER_VALIDATE_EMAIL) !== FALSE ? 'email' : 'login').'=?', [$lg]);
}
/**
* Make a confirmation code and store it in the database
*/
private function makeCode(Context $context, \RedBeanPHP\OODBBean $user, string $kind) : string
{
R::trashAll($user->all()->{'own'.\ucfirst(FW::CONFIRM).'List'});
$code = \hash('sha256', $user->getID().$user->email.$user->login.\uniqid());
$conf = R::dispense(FW::CONFIRM);
$conf->code = $code;
$conf->issued = $context->utcnow();
$conf->kind = $kind;
$conf->user = $user;
R::store($conf);
return $code;
}
/**
* Mail a confirmation code
*
* @internal
*/
private function sendConfirm(Context $context, \RedBeanPHP\OODBBean $user) : void
{
$local = $context->local();
$code = $this->makeCode($context, $user, 'C');
$local->sendmail([$user->email],
'Please confirm your email address for '.$local->configval('sitename'),
"Please use this link to confirm your email address\n\n\n".
$local->configval('siteurl').'/confirm/'.$code."\n\n\nThank you,\n\n The ".$local->configval('sitename')." Team\n\n",
'',
['From' => $local->configval('noreply')]
);
}
/**
* Mail a password reset code
*
* @internal
*/
private function sendReset(Context $context, \RedBeanPHP\OODBBean $user) : void
{
$local = $context->local();
$code = $this->makeCode($context, $user, 'P');
$local->sendmail([$user->email], 'Reset your '.$local->configval('sitename').' password',
"Please use this link to reset your password\n\n\n".
$local->configval('siteurl').'/forgot/'.$code."\n\n\nThank you,\n\n The ".$local->configval('sitename')." Team\n\n",
'',
['From' => $local->configval('sitenoreply')]
);
}
/**
* Handle a login
*
* @uses \Support\Login
*/
private function login(Context $context) : string
{
$local = $context->local();
$local->addval('register', Config::REGISTER);
if ($context->hasuser())
{ // already logged in
$local->message(Local::WARNING, 'Please log out before trying to login');
}
else
{
$this->checkLogin($context);
}
return '@content/login.twig';
}
/**
* handle a registration
*
* @throws \Framework\Exception\BadOperation
*/
private function register(Context $context) : string
{
if (!Config::REGISTER)
{
$context->divert('/');
/* NOT REACHED */
}
if ($context->web()->isPost())
{
if ($context->hasUser())
{
throw new \Framework\Exception\BadOperation('Cannot register while logged in');
}
$fdt = $context->formdata('post');
$login = $fdt->fetch('login', '');
if ($login !== '')
{
$errmess = [];
$x = R::findOne(FW::USER, 'login=?', [$login]);
if (!\is_object($x))
{
$pw = $fdt->mustFetch('password');
$rpw = $fdt->mustFetch('repeat');
$email = $fdt->mustFetch('email');
if ($pw != $rpw)
{
$errmess[] = 'The passwords do not match';
}
$cmodel = FW::USERMCLASS;
if (!$cmodel::pwValid($pw))
{
$errmess[] = 'The password does not meet the specification';
}
if (\preg_match('/[^a-z0-9]/i', $login))
{
$errmess[] = 'Your username can only contain letters and numbers';
}
if (!\filter_var($email, FILTER_VALIDATE_EMAIL))
{
$errmess[] = 'Please provide a valid email address';
}
if (empty($errmess))
{ // no errors
$x = R::dispense(FW::USER);
$x->login = $login;
$x->email = $email;
$x->confirm = 0;
$x->active = 1;
$x->joined = $context->utcnow();
R::store($x);
$x->setpw($pw);
$rerr = $x->register($context); // do any extra registration
if (empty($rerr))
{
$this->confmessage($context, $x);
}
else
{ // extra registration failed
R::trash($x); // delete the user object
$errmess = \array_merge($errmess, $rerr);
}
}
}
else
{
$errmess[] = 'That login is not available';
}
if (!empty($errmess))
{
$context->local()->message(Local::ERROR, $errmess);
$context->local()->addval([
'login' => $login,
'email' => $email, // @phan-suppress-current-line PhanPossiblyUndeclaredVariable
]);
}
}
else
{
$context->local()->message(Local::ERROR, 'Please complete the registration form');
}
}
return '@content/register.twig';
}
/**
* Handle confirmation
*
* @internal
*/
private function confmessage(Context $context, \RedBeanPHP\OODBBean $user) : void
{
if (\Config\Framework::constant('CONFEMAIL', FALSE))
{
$this->sendconfirm($context, $user);
$msg = 'A confirmation link has been sent to your email address';
}
else
{
$user->confirm = 1;
R::store($user);
$msg = 'You have successfully registered with the system';
}
$context->local()->message(Local::MESSAGE, $msg);
$context->local()->addval('regok', TRUE);
}
/**
* Handle things to do with email address confirmation
*/
private function confirm(Context $context) : string
{
if ($context->hasuser())
{ // logged in, so this stupid....
$context->divert('/');
/* NOT REACHED */
}
$local = $context->local();
$tpl = '@users/reset.twig';
$rest = $context->rest();
if ($rest[0] === '' || $rest[0] == 'resend')
{ // asking for resend
$lg = $context->formdata('post')->fetch('eorl', '');
if ($lg === '')
{ // show the form
return '@users/resend.twig';
}
// now handle the form
$user = self::eorl($lg);
if (!\is_object($user))
{
$local->message(Local::ERROR, 'Sorry, there is no user with that name or email address.');
}
elseif ($user->confirm)
{
$local->message(Local::WARNING, 'Your email address has already been confirmed.');
}
else
{
$this->sendconfirm($context, $user);
$local->message(Local::MESSAGE, 'A new confirmation link has been sent to your email address.');
}
}
else
{ // confirming the email
$x = R::findOne(FW::CONFIRM, 'code=? and kind=?', [$rest[0], 'C']);
if (\is_object($x))
{
$interval = (new \DateTime($context->utcnow()))->diff(new \DateTime($x->issued));
if ($interval->days <= 3)
{
$x->user->doconfirm();
R::trash($x);
$local->message(Local::MESSAGE, 'Thank you for confirming your email address. You can now login.');
}
else
{
$local->message(Local::ERROR, 'Sorry, that code has expired!');
}
}
}
return $tpl;
}
/**
* Handle things to do with password reset
*
* @param Context $context The context object for the site
*/
public function forgot(Context $context) : string
{
$local = $context->local();
$tpl = '@users/reset.twig';
if ($context->hasuser())
{ // logged in, so this stupid....
$local->addval('done', TRUE);
$local->message(Local::WARNING, 'You are already logged in');
return $tpl;
}
$fdt = $context->formdata('post');
$rest = $context->rest();
if ($rest[0] === '')
{
$lg = $fdt->fetch('eorl', '');
if ($lg !== '')
{
$user = self::eorl($lg);
if (is_object($user))
{
$this->sendreset($context, $user);
$local->message(Local::MESSAGE, 'A password reset link has been sent to your email address.');
$local->addval('done', TRUE);
}
else
{
$local->message(Local::WARNING, 'Sorry, there is no user with that name or email address.');
}
}
}
elseif ($rest[0] === 'reset')
{
$tpl = '@users/pwreset.twig';
$user = $fdt->mustFetchBean('uid', FW::USER);
$code = $fdt->mustFetch('code');
$xc = R::findOne(FW::CONFIRM, 'code=? and kind=?', [$code, 'P']);
if (is_object($xc) && $xc->user_id == $user->getID())
{
$interval = (new \DateTime($context->utcnow()))->diff(new \DateTime($xc->issued));
if ($interval->days <= 1)
{
$pw = $fdt->mustFetch('password');
if ($pw === $fdt->mustFetch('repeat'))
{
$xc->user->setpw($pw);
R::trash($xc);
$local->message(Local::MESSAGE, 'You have reset your password. You can now login.');
$local->addval('done', TRUE);
}
else
{
$local->message(Local::ERROR, 'Sorry, the passwords do not match!');
}
}
else
{
$local->message(Local::ERROR, 'Sorry, that code has expired!');
}
}
else
{
$context->divert('/');
/* NOT REACHED */
}
}
else
{
$x = R::findOne(FW::CONFIRM, 'code=? and kind=?', [$rest[0], 'P']);
if (\is_object($x))
{
$interval = (new \DateTime($context->utcnow()))->diff(new \DateTime($x->issued));
if ($interval->days <= 1)
{
$local->addval([
'pwuser' => $x->user,
'code' => $x->code,
]);
$tpl = '@users/pwreset.twig';
}
else
{
$local->message(Local::ERROR, 'Sorry, that code has expired!');
}
}
}
return $tpl;
}
/**
* Login success so set up session
*/
private function loginSession(Context $context, \RedBeanPHP\OODBBean $user, string $page) // : never
{
if (\session_status() !== \PHP_SESSION_ACTIVE)
{ // no session started yet
\session_start(['name' => \Config\Config::SESSIONNAME, 'cookie_path' => $context->local()->base().'/']);
}
$_SESSION['userID'] = $user->getID();
$dpage = $context->local()->config('defaultpage');
$context->divert($page === '' ? ($dpage !== NULL ? $dpage->value : '/') : $page); // success - divert to default page or requested page
/* NOT REACHED */
}
/**
* Handle TwoFA
*/
private function twofa(Context $context) : string
{
if ($context->web()->isPost())
{
$fdt = $context->formdata('post');
$user = R::findOne(FW::USER, 'code2fa=?', [$fdt->mustFetch('hash')]);
if (!is_object($user))
{
$context->divert('/login/');
}
if (\Framework\Support\Security::getInstance()->check2FA($user->secret(), $fdt->mustFetch('validator')))
{
$user->code2fa = '';
R::store($user);
$this->loginSession($context, $user, $fdt->fetch('goto'));
/* NOT REACHED */
}
$context->local()->message(\Framework\Local::ERROR, 'Invalid code - please try again');
}
$fget = $context->formdata('get');
$hash = $fget->fetch('xx');
if ($hash === '' || !is_object(R::findOne(FW::USER, 'code2fa=?', [$hash])))
{
$context->divert('/login/');
/* NOT REACHED */
}
$context->local()->addval([
'hash' => $hash,
'goto' => $fget->fetch('goto'),
]);
return '@content/twofa.twig';
}
/**
* Handle /login /logout /register /forgot /confirm /twofa
*
* @param Context $context The context object for the site
*/
public function handle(Context $context) : array|string
{
$action = $context->action(); // the validity of the action value has been checked before we get here
\assert(\method_exists($this, $action));
return $this->$action($context);
}
}
?>