Registration sketch

This commit is contained in:
Marcin Kurczewski 2013-10-05 12:55:03 +02:00
parent eebd297a24
commit 3a77bb7c59
18 changed files with 558 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
db.sqlite
db.sqlite-journal

25
config.ini Normal file
View file

@ -0,0 +1,25 @@
[chibi]
userCodeDir=./src/
prettyPrint=1
[main]
dbPath=./db.sqlite
[registration]
emailActivation = 0
adminActivation = 0
passMinLength = 5
passRegex = "/^.+$/"
userNameMinLength = 3
userNameRegex = "/^[\w_-]+$/ui"
salt = "1A2/$_4xVa"
activationEmailSenderName = "{host} registration engine"
activationEmailSenderEmail = "noreply@{host}"
activationEmailSubject = "{host} activation"
activationEmailBody = "Hello,
You received this e-mail because someone registered an user with this address at {host}. If it's you, visit {link} to finish registration process, otherwise you may ignore and delete this e-mail.
Kind regards,
{host} registration engine"

12
public_html/.htaccess Normal file
View file

@ -0,0 +1,12 @@
DirectorySlash Off
Options -Indexes
RewriteEngine On
ErrorDocument 403 /dispatch.php?request=error/http&code=403
ErrorDocument 404 /dispatch.php?request=error/http&code=404
ErrorDocument 500 /dispatch.php?request=error/http&code=500
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^.*$ /dispatch.php
RewriteRule ^/?$ /dispatch.php

27
public_html/dispatch.php Normal file
View file

@ -0,0 +1,27 @@
<?php
chdir('..');
require_once 'redbean/RedBean/redbean.inc.php';
require_once 'chibi-core/Facade.php';
date_default_timezone_set('UTC');
setlocale(LC_CTYPE, 'en_US.UTF-8');
class Bootstrap
{
public function workWrapper($workCallback)
{
$this->config->chibi->baseUrl = 'http://' . rtrim($_SERVER['HTTP_HOST'], '/') . '/';
R::setup('sqlite:' . $this->config->main->dbPath);
$workCallback();
}
}
$query = $_SERVER['REQUEST_URI'];
$configPaths =
[
__DIR__ . DIRECTORY_SEPARATOR . '../config.ini',
__DIR__ . DIRECTORY_SEPARATOR . '../local.ini'
];
$configPaths = array_filter($configPaths, 'file_exists');
\Chibi\Facade::run($query, $configPaths, new Bootstrap());

View file

@ -0,0 +1,49 @@
<?php
abstract class AbstractController
{
protected function attachUser()
{
$this->context->loggedIn = false;
if (isset($_SESSION['user-id']))
{
$this->context->user = R::findOne('user', 'id = ?', [$_SESSION['user-id']]);
if (!empty($this->context->user))
{
$this->context->loggedIn = true;
}
}
if (empty($this->context->user))
{
#todo: construct anonymous user
$this->context->user = null;
}
}
public function workWrapper($workCallback)
{
session_start();
$this->context->layoutName = isset($_GET['json'])
? 'layout-json'
: 'layout-normal';
$this->context->transport = new StdClass;
$this->context->transport->success = null;
$this->attachUser();
try
{
$workCallback();
}
catch (SimpleException $e)
{
$this->context->transport->errorMessage = rtrim($e->getMessage(), '.') . '.';
$this->context->transport->exception = $e;
$this->context->transport->success = false;
}
catch (Exception $e)
{
$this->context->exception = $e;
$this->context->viewName = 'error-exception';
}
}
}

View file

@ -0,0 +1,201 @@
<?php
class AuthController extends AbstractController
{
private static function hashPassword($pass, $salt2)
{
$salt1 = \Chibi\Registry::getConfig()->registration->salt;
return sha1($salt1 . $salt2 . $pass);
}
/**
* @route /auth/login
*/
public function loginAction()
{
//check if already logged in
if ($this->context->loggedIn)
{
\Chibi\HeadersHelper::set('Location', \Chibi\UrlHelper::route('post', 'search'));
return;
}
$suppliedUser = InputHelper::get('user');
$suppliedPass = InputHelper::get('pass');
if ($suppliedUser !== null and $suppliedPass !== null)
{
$dbUser = R::findOne('user', 'name = ?', [$suppliedUser]);
if ($dbUser === null)
throw new SimpleException('Invalid username');
$suppliedPassHash = self::hashPassword($suppliedPass, $dbUser->pass_salt);
if ($suppliedPassHash != $dbUser->pass_hash)
throw new SimpleException('Invalid password');
if (!$dbUser->admin_confirmed)
throw new SimpleException('An admin hasn\'t confirmed your registration yet');
if (!$dbUser->email_confirmed)
throw new SimpleException('You haven\'t confirmed your e-mail address yet');
$_SESSION['user-id'] = $dbUser->id;
\Chibi\HeadersHelper::set('Location', \Chibi\UrlHelper::route('post', 'search'));
$this->context->transport->success = true;
}
}
/**
* @route /auth/logout
*/
public function logoutAction()
{
$this->context->viewName = null;
$this->context->viewName = null;
unset($_SESSION['user-id']);
\Chibi\HeadersHelper::set('Location', \Chibi\UrlHelper::route('post', 'search'));
}
/**
* @route /register
*/
public function registerAction()
{
//check if already logged in
if ($this->context->loggedIn)
{
\Chibi\HeadersHelper::set('Location', \Chibi\UrlHelper::route('post', 'search'));
return;
}
$suppliedUser = InputHelper::get('user');
$suppliedPass1 = InputHelper::get('pass1');
$suppliedPass2 = InputHelper::get('pass2');
$suppliedEmail = InputHelper::get('email');
$this->context->suppliedUser = $suppliedUser;
$this->context->suppliedPass1 = $suppliedPass1;
$this->context->suppliedPass2 = $suppliedPass2;
$this->context->suppliedEmail = $suppliedEmail;
$regConfig = $this->config->registration;
$passMinLength = intval($regConfig->passMinLength);
$passRegex = $regConfig->passRegex;
$userNameMinLength = intval($regConfig->userNameMinLength);
$userNameRegex = $regConfig->userNameRegex;
$emailActivation = $regConfig->emailActivation;
$adminActivation = $regConfig->adminActivation;
$this->context->transport->adminActivation = $adminActivation;
$this->context->transport->emailActivation = $emailActivation;
if ($suppliedUser !== null)
{
$dbUser = R::findOne('user', 'name = ?', [$suppliedUser]);
if ($dbUser !== null)
{
if (!$dbUser->email_confirmed)
throw new SimpleException('User with this name is already registered and awaits e-mail confirmation');
if (!$dbUser->admin_confirmed)
throw new SimpleException('User with this name is already registered and awaits admin confirmation');
throw new SimpleException('User with this name is already registered');
}
if ($suppliedPass1 != $suppliedPass2)
throw new SimpleException('Specified passwords must be the same');
if (strlen($suppliedPass1) < $passMinLength)
throw new SimpleException(sprintf('Password must have at least %d characters', $passMinLength));
if (!preg_match($passRegex, $suppliedPass1))
throw new SimpleException('Password contains invalid characters');
if (strlen($suppliedUser) < $userNameMinLength)
throw new SimpleException(sprintf('User name must have at least %d characters', $userNameMinLength));
if (!preg_match($userNameRegex, $suppliedUser))
throw new SimpleException('User name contains invalid characters');
if (empty($suppliedEmail) and $emailActivation)
throw new SimpleException('E-mail address is required - you will be sent confirmation e-mail.');
if (!empty($suppliedEmail) and !TextHelper::isValidEmail($suppliedEmail))
throw new SimpleException('E-mail address appears to be invalid');
//register the user
$dbUser = R::dispense('user');
$dbUser->name = $suppliedUser;
$dbUser->pass_salt = md5(mt_rand() . uniqid());
$dbUser->pass_hash = self::hashPassword($suppliedPass1, $dbUser->pass_salt);
$dbUser->email = $suppliedEmail;
$dbUser->admin_confirmed = $adminActivation ? false : true;
$dbUser->email_confirmed = $emailActivation ? false : true;
$dbUser->email_token = md5(mt_rand() . uniqid());
$dbUser->access_rank = R::findOne('user') === null ? AccessRank::Admin : AccessRank::Registered;
//send the e-mail
if ($emailActivation)
{
$tokens = [];
$tokens['host'] = $_SERVER['HTTP_HOST'];
$tokens['link'] = \Chibi\UrlHelper::route('auth', 'activation', ['token' => $dbUser->email_token]);
$body = wordwrap(TextHelper::replaceTokens($regConfig->activationEmailBody, $tokens), 70);
$subject = TextHelper::replaceTokens($regConfig->activationEmailSubject, $tokens);
$senderName = TextHelper::replaceTokens($regConfig->activationEmailSenderName, $tokens);
$senderEmail = $regConfig->activationEmailSenderEmail;
$headers = [];
$headers[] = sprintf('From: %s <%s>', $senderName, $senderEmail);
$headers[] = sprintf('Subject: %s', $subject);
$headers[] = sprintf('X-Mailer: PHP/%s', phpversion());
mail($dbUser->email, $subject, $body, implode("\r\n", $headers));
}
//save the user to db if everything went okay
R::store($dbUser);
$this->context->transport->success = true;
if (!$emailActivation and !$adminActivation)
{
$_SESSION['user-id'] = $dbUser->id;
$this->attachUser();
}
}
}
/**
* @route /activation/{token}
*/
public function activationAction($token)
{
//check if already logged in
if ($this->context->loggedIn)
{
\Chibi\HeadersHelper::set('Location', \Chibi\UrlHelper::route('post', 'search'));
return;
}
if (empty($token))
throw new SimpleException('Invalid activation token');
$dbUser = R::findOne('user', 'email_token = ?', [$token]);
if ($dbUser === null)
throw new SimpleException('No user with such activation token');
if ($dbUser->email_confirmed)
throw new SimpleException('This user was already activated');
$dbUser->email_confirmed = true;
R::store($dbUser);
$this->context->transport->success = true;
$adminActivation = $this->config->registration->adminActivation;
$this->context->transport->adminActivation = $adminActivation;
if (!$adminActivation)
{
$_SESSION['user-id'] = $dbUser->id;
$this->attachUser();
}
}
}

View file

@ -0,0 +1,16 @@
<?php
class PostController extends AbstractController
{
/**
* @route /
* @route /index
*/
public function searchAction()
{
$tp = new StdClass;
$tp->posts = [];
$tp->posts []= 1;
$tp->posts []= 2;
$this->context->transport = $tp;
}
}

View file

@ -0,0 +1,23 @@
<?php
class InputHelper
{
public static function get($keyName)
{
if (isset($_POST[$keyName]))
{
return $_POST[$keyName];
}
if (isset($_GET[$keyName]))
{
return $_GET[$keyName];
}
if (isset($_COOKIE[$keyName]))
{
return $_COOKIE[$keyName];
}
return null;
}
}

View file

@ -0,0 +1,19 @@
<?php
class TextHelper
{
public static function isValidEmail($email)
{
$emailRegex = '/^[^@]+@[^@]+\.[^@]+$/';
return preg_match($emailRegex, $email);
}
public static function replaceTokens($text, array $tokens)
{
foreach ($tokens as $key => $value)
{
$token = '{' . $key . '}';
$text = str_replace($token, $value, $text);
}
return $text;
}
}

View file

@ -0,0 +1,9 @@
<?php
class AccessRank
{
const Anonymous = 0;
const Registered = 1;
const PowerUser = 2;
const Moderator = 3;
const Admin = 4;
}

4
src/SimpleException.php Normal file
View file

@ -0,0 +1,4 @@
<?php
class SimpleException extends Exception
{
}

View file

@ -0,0 +1,10 @@
<?php if ($this->context->transport->success === true): ?>
<p>Activation completed successfully.</p>
<?php if ($this->context->transport->adminActivation): ?>
<p>However, you still need to be approved by admin.</p>
<?php endif ?>
<?php endif ?>
<?php if (isset($this->context->transport->errorMessage)): ?>
<p class="alert alert-error"><?php echo $this->context->transport->errorMessage ?></p>
<?php endif ?>

View file

@ -0,0 +1,19 @@
<form action="<?php echo \Chibi\UrlHelper::route('auth', 'login') ?>" method="post">
<div>
<label for="user">User:</label>
<input id="user" name="user"/>
</div>
<div>
<label for="pass">Password:</label>
<input type="password" id="pass" name="pass"/>
</div>
<div>
<input type="submit" value="Login">
</div>
</form>
<?php if (isset($this->context->transport->errorMessage)): ?>
<p class="alert alert-error"><?php echo $this->context->transport->errorMessage ?></p>
<?php endif ?>

View file

@ -0,0 +1,39 @@
<?php if ($this->context->transport->success === true): ?>
<p>Congratulations, you are registered.</p>
<?php if ($this->context->transport->emailActivation): ?>
<p>Please wait for activation e-mail.</p>
<?php endif ?>
<?php if ($this->context->transport->adminActivation): ?>
<p>After this, an admin will have to confirm your registration.</p>
<?php endif ?>
<?php else: ?>
<form action="<?php echo \Chibi\UrlHelper::route('auth', 'register') ?>" method="post">
<div>
<label for="user">User:</label>
<input id="user" name="user" value="<?php echo $this->context->suppliedUser ?>"/ placeholder="e.g. darth_vader" autocomplete="off">
</div>
<div>
<label for="pass">E-mail address<?php if ($this->context->transport->emailActivation) echo ' (required)' ?>:</label>
<input id="email" name="email" value="<?php echo $this->context->suppliedEmail ?>" placeholder="e.g. vader@empire.gov"/ autocomplete="off">
</div>
<div>
<label for="pass">Password:</label>
<input type="password" id="pass" name="pass1" value="<?php echo $this->context->suppliedPass1 ?>" placeholder="e.g. <?php echo str_repeat('&#x25cf;', 8) ?>"/ autocomplete="off">
</div>
<div>
<label for="pass">Password (repeat):</label>
<input type="password" id="pass" name="pass2" value="<?php echo $this->context->suppliedPass2 ?>" placeholder="e.g. <?php echo str_repeat('&#x25cf;', 8) ?>"/ autocomplete="off">
</div>
<div>
<input type="submit" value="Register">
</div>
</form>
<?php if (isset($this->context->transport->errorMessage)): ?>
<p class="alert alert-error"><?php echo $this->context->transport->errorMessage ?></p>
<?php endif ?>
<?php endif ?>

View file

@ -0,0 +1,72 @@
<?php
echo '<h1>Unhandled exception</h1>';
echo '<p>';
printf('%s &bdquo;%s&rdquo; thrown at %s:%d',
get_class($this->context->exception),
$this->context->exception->getMessage(),
$this->context->exception->getFile(),
$this->context->exception->getLine());
echo '</p>';
echo '<ul>';
$count = 0;
foreach ($this->context->exception->getTrace() as $frame)
{
$args = '';
if (isset($frame['args']))
{
$args = array();
foreach ($frame['args'] as $arg)
{
if (is_string($arg))
{
$args[] = "'" . $arg . "'";
}
elseif (is_array($arg))
{
$args[] = "Array";
}
elseif (is_null($arg))
{
$args[] = 'NULL';
}
elseif (is_bool($arg))
{
$args[] = ($arg) ? "true" : "false";
}
elseif (is_object($arg))
{
$args[] = get_class($arg);
}
elseif (is_resource($arg))
{
$args[] = get_resource_type($arg);
}
else
{
$args[] = $arg;
}
}
$args = join(', ', $args);
}
echo '<li>';
printf('#%s %s(%s): %s(%s)<br>',
$count,
isset($frame['file'])
? $frame['file']
: 'unknown file',
isset($frame['line'])
? $frame['line']
: 'unknown line',
isset($frame['class'])
? $frame['class'] . $frame['type'] . $frame['function']
: $frame['function'],
$args);
echo '</li>';
$count++;
}
echo '</ul>';

View file

@ -0,0 +1 @@
<?php echo json_encode($this->context->transport, true) ?>

View file

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>no title yet...</title>
</head>
<body>
<div>
<?php if (empty($this->context->user)): ?>
<a href="<?php echo \Chibi\UrlHelper::route('auth', 'login') ?>">
login
</a>
&nbsp;or&nbsp;
<a href="<?php echo \Chibi\UrlHelper::route('auth', 'register') ?>">
register
</a>
<?php else: ?>
logged in as <?php echo $this->context->user->name ?>
&nbsp;
<a href="<?php echo \Chibi\UrlHelper::route('auth', 'logout') ?>">
logout
</a>
<?php endif ?>
</div>
<?php echo $this->renderView() ?>
</body>
</html>

View file

@ -0,0 +1 @@
Todo: view posts