From 3a77bb7c59076abb26f56a61274ac0c164acba5a Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Sat, 5 Oct 2013 12:55:03 +0200 Subject: [PATCH] Registration sketch --- .gitignore | 2 + config.ini | 25 +++ public_html/.htaccess | 12 ++ public_html/dispatch.php | 27 ++++ src/Controllers/AbstractController.php | 49 ++++++ src/Controllers/AuthController.php | 201 +++++++++++++++++++++++++ src/Controllers/PostController.php | 16 ++ src/Helpers/InputHelper.php | 23 +++ src/Helpers/TextHelper.php | 19 +++ src/Models/AccessRank.php | 9 ++ src/SimpleException.php | 4 + src/Views/auth-activation.phtml | 10 ++ src/Views/auth-login.phtml | 19 +++ src/Views/auth-register.phtml | 39 +++++ src/Views/error-exception.phtml | 72 +++++++++ src/Views/layout-json.phtml | 1 + src/Views/layout-normal.phtml | 29 ++++ src/Views/post-search.phtml | 1 + 18 files changed, 558 insertions(+) create mode 100644 .gitignore create mode 100644 config.ini create mode 100644 public_html/.htaccess create mode 100644 public_html/dispatch.php create mode 100644 src/Controllers/AbstractController.php create mode 100644 src/Controllers/AuthController.php create mode 100644 src/Controllers/PostController.php create mode 100644 src/Helpers/InputHelper.php create mode 100644 src/Helpers/TextHelper.php create mode 100644 src/Models/AccessRank.php create mode 100644 src/SimpleException.php create mode 100644 src/Views/auth-activation.phtml create mode 100644 src/Views/auth-login.phtml create mode 100644 src/Views/auth-register.phtml create mode 100644 src/Views/error-exception.phtml create mode 100644 src/Views/layout-json.phtml create mode 100644 src/Views/layout-normal.phtml create mode 100644 src/Views/post-search.phtml diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..f138d8c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +db.sqlite +db.sqlite-journal diff --git a/config.ini b/config.ini new file mode 100644 index 00000000..ac1375c2 --- /dev/null +++ b/config.ini @@ -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" diff --git a/public_html/.htaccess b/public_html/.htaccess new file mode 100644 index 00000000..12afe11e --- /dev/null +++ b/public_html/.htaccess @@ -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 diff --git a/public_html/dispatch.php b/public_html/dispatch.php new file mode 100644 index 00000000..c2ae4340 --- /dev/null +++ b/public_html/dispatch.php @@ -0,0 +1,27 @@ +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()); diff --git a/src/Controllers/AbstractController.php b/src/Controllers/AbstractController.php new file mode 100644 index 00000000..0b4268b8 --- /dev/null +++ b/src/Controllers/AbstractController.php @@ -0,0 +1,49 @@ +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'; + } + } +} diff --git a/src/Controllers/AuthController.php b/src/Controllers/AuthController.php new file mode 100644 index 00000000..c970e9b9 --- /dev/null +++ b/src/Controllers/AuthController.php @@ -0,0 +1,201 @@ +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(); + } + } +} diff --git a/src/Controllers/PostController.php b/src/Controllers/PostController.php new file mode 100644 index 00000000..bf1db644 --- /dev/null +++ b/src/Controllers/PostController.php @@ -0,0 +1,16 @@ +posts = []; + $tp->posts []= 1; + $tp->posts []= 2; + $this->context->transport = $tp; + } +} diff --git a/src/Helpers/InputHelper.php b/src/Helpers/InputHelper.php new file mode 100644 index 00000000..312b36c7 --- /dev/null +++ b/src/Helpers/InputHelper.php @@ -0,0 +1,23 @@ + $value) + { + $token = '{' . $key . '}'; + $text = str_replace($token, $value, $text); + } + return $text; + } +} diff --git a/src/Models/AccessRank.php b/src/Models/AccessRank.php new file mode 100644 index 00000000..9f158e0e --- /dev/null +++ b/src/Models/AccessRank.php @@ -0,0 +1,9 @@ +context->transport->success === true): ?> +

Activation completed successfully.

+ context->transport->adminActivation): ?> +

However, you still need to be approved by admin.

+ + + +context->transport->errorMessage)): ?> +

context->transport->errorMessage ?>

+ diff --git a/src/Views/auth-login.phtml b/src/Views/auth-login.phtml new file mode 100644 index 00000000..eebfa233 --- /dev/null +++ b/src/Views/auth-login.phtml @@ -0,0 +1,19 @@ +
+
+ + +
+ +
+ + +
+ +
+ +
+
+ +context->transport->errorMessage)): ?> +

context->transport->errorMessage ?>

+ diff --git a/src/Views/auth-register.phtml b/src/Views/auth-register.phtml new file mode 100644 index 00000000..b00d2986 --- /dev/null +++ b/src/Views/auth-register.phtml @@ -0,0 +1,39 @@ +context->transport->success === true): ?> +

Congratulations, you are registered.

+ context->transport->emailActivation): ?> +

Please wait for activation e-mail.

+ + context->transport->adminActivation): ?> +

After this, an admin will have to confirm your registration.

+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+ + context->transport->errorMessage)): ?> +

context->transport->errorMessage ?>

+ + diff --git a/src/Views/error-exception.phtml b/src/Views/error-exception.phtml new file mode 100644 index 00000000..cd0c6268 --- /dev/null +++ b/src/Views/error-exception.phtml @@ -0,0 +1,72 @@ +Unhandled exception'; +echo '

'; +printf('%s „%s” thrown at %s:%d', + get_class($this->context->exception), + $this->context->exception->getMessage(), + $this->context->exception->getFile(), + $this->context->exception->getLine()); +echo '

'; + +echo ''; diff --git a/src/Views/layout-json.phtml b/src/Views/layout-json.phtml new file mode 100644 index 00000000..906a16be --- /dev/null +++ b/src/Views/layout-json.phtml @@ -0,0 +1 @@ +context->transport, true) ?> diff --git a/src/Views/layout-normal.phtml b/src/Views/layout-normal.phtml new file mode 100644 index 00000000..2a481a40 --- /dev/null +++ b/src/Views/layout-normal.phtml @@ -0,0 +1,29 @@ + + + + + no title yet... + + + +
+ context->user)): ?> + + login + +  or  + + register + + + logged in as context->user->name ?> +   + + logout + + +
+ + renderView() ?> + + diff --git a/src/Views/post-search.phtml b/src/Views/post-search.phtml new file mode 100644 index 00000000..c5fbe7c7 --- /dev/null +++ b/src/Views/post-search.phtml @@ -0,0 +1 @@ +Todo: view posts