From f279afe8888c78f34903d62e4867ea2a79ef5e18 Mon Sep 17 00:00:00 2001 From: trendschau Date: Tue, 28 Sep 2021 12:56:29 +0200 Subject: [PATCH] Version 1.4.9: Password recovery and security middleware --- cache/securitylog.txt | 5 + composer.json | 3 +- composer.lock | 218 ++++++++++++- settings/formdata.yaml | 1 + system/Controllers/AuthController.php | 303 +++++++++++++----- system/Controllers/FormController.php | 42 +-- system/Controllers/SettingsController.php | 23 +- system/Extensions/TwigCaptchaExtension.php | 44 +++ system/Middleware/SecurityMiddleware.php | 117 +++++++ .../Middleware/ValidationErrorsMiddleware.php | 7 + ...essController.php => accessMiddleware.php} | 8 +- system/Models/Helpers.php | 77 ++++- system/Models/User.php | 42 ++- system/Models/Validation.php | 22 ++ system/Plugin.php | 17 +- system/Routes/Web.php | 48 +-- system/Settings.php | 5 + system/author/auth/login.twig | 30 +- system/author/auth/recoverpw.twig | 39 +++ system/author/auth/recoverpwnew.twig | 41 +++ system/author/auth/recoverpwsend.twig | 19 ++ system/author/css/style.css | 14 +- system/author/js/vue-meta.js | 9 +- system/author/layouts/layoutAuth.twig | 11 +- system/author/partials/form.twig | 23 +- system/author/settings/system.twig | 108 +++++-- system/system.php | 20 +- 27 files changed, 1027 insertions(+), 269 deletions(-) create mode 100644 cache/securitylog.txt create mode 100644 settings/formdata.yaml create mode 100644 system/Extensions/TwigCaptchaExtension.php create mode 100644 system/Middleware/SecurityMiddleware.php rename system/Middleware/{accessController.php => accessMiddleware.php} (83%) create mode 100644 system/author/auth/recoverpw.twig create mode 100644 system/author/auth/recoverpwnew.twig create mode 100644 system/author/auth/recoverpwsend.twig diff --git a/cache/securitylog.txt b/cache/securitylog.txt new file mode 100644 index 0000000..0123c2b --- /dev/null +++ b/cache/securitylog.txt @@ -0,0 +1,5 @@ +127.0.0.1;2021-09-26 12:01:24;wrong captcha http://localhost/typemill/tm/recoverpw +127.0.0.1;2021-09-26 12:06:16;wrong captcha http://localhost/typemill/tm/recoverpw +127.0.0.1;2021-09-27 23:44:57;wrong captcha http://localhost/typemill/tm/recoverpw +127.0.0.1;2021-09-27 23:51:19;wrong captcha http://localhost/typemill/tm/recoverpw +127.0.0.1;2021-09-27 23:51:30;wrong captcha http://localhost/typemill/tm/recoverpw diff --git a/composer.json b/composer.json index dc2d4b1..bf5529e 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,8 @@ "jbroadway/urlify": "1.1.3", "vlucas/valitron": "dev-master", "laminas/laminas-permissions-acl": "^2.7", - "akrabat/proxy-detection-middleware": "^0.4.0" + "akrabat/proxy-detection-middleware": "^0.4.0", + "gregwar/captcha": "1.*" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index d16a63a..0b41d0f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7539fdddfa1c0b8d030fa5955b45a928", + "content-hash": "bc8436c1ed1a0ea8333205185da466f4", "packages": [ { "name": "akrabat/proxy-detection-middleware", @@ -164,6 +164,63 @@ }, "time": "2019-12-29T11:14:16+00:00" }, + { + "name": "gregwar/captcha", + "version": "v1.1.9", + "source": { + "type": "git", + "url": "https://github.com/Gregwar/Captcha.git", + "reference": "4bb668e6b40e3205a020ca5ee4ca8cff8b8780c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Gregwar/Captcha/zipball/4bb668e6b40e3205a020ca5ee4ca8cff8b8780c5", + "reference": "4bb668e6b40e3205a020ca5ee4ca8cff8b8780c5", + "shasum": "" + }, + "require": { + "ext-gd": "*", + "ext-mbstring": "*", + "php": ">=5.3.0", + "symfony/finder": "*" + }, + "require-dev": { + "phpunit/phpunit": "^6.4" + }, + "type": "captcha", + "autoload": { + "psr-4": { + "Gregwar\\": "src/Gregwar" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Passault", + "email": "g.passault@gmail.com", + "homepage": "http://www.gregwar.com/" + }, + { + "name": "Jeremy Livingston", + "email": "jeremy.j.livingston@gmail.com" + } + ], + "description": "Captcha generator", + "homepage": "https://github.com/Gregwar/Captcha", + "keywords": [ + "bot", + "captcha", + "spam" + ], + "support": { + "issues": "https://github.com/Gregwar/Captcha/issues", + "source": "https://github.com/Gregwar/Captcha/tree/master" + }, + "time": "2020-03-24T14:39:05+00:00" + }, { "name": "jbroadway/urlify", "version": "1.1.3-stable", @@ -291,23 +348,23 @@ }, { "name": "laminas/laminas-zendframework-bridge", - "version": "1.3.0", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-zendframework-bridge.git", - "reference": "13af2502d9bb6f7d33be2de4b51fb68c6cdb476e" + "reference": "bf180a382393e7db5c1e8d0f2ec0c4af9c724baf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-zendframework-bridge/zipball/13af2502d9bb6f7d33be2de4b51fb68c6cdb476e", - "reference": "13af2502d9bb6f7d33be2de4b51fb68c6cdb476e", + "url": "https://api.github.com/repos/laminas/laminas-zendframework-bridge/zipball/bf180a382393e7db5c1e8d0f2ec0c4af9c724baf", + "reference": "bf180a382393e7db5c1e8d0f2ec0c4af9c724baf", "shasum": "" }, "require": { - "php": "^7.3 || ^8.0" + "php": "^7.3 || ~8.0.0 || ~8.1.0" }, "require-dev": { - "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.1 || ^9.3", + "phpunit/phpunit": "^9.3", "psalm/plugin-phpunit": "^0.15.1", "squizlabs/php_codesniffer": "^3.5", "vimeo/psalm": "^4.6" @@ -349,7 +406,7 @@ "type": "community_bridge" } ], - "time": "2021-06-24T12:49:22+00:00" + "time": "2021-09-03T17:53:30+00:00" }, { "name": "nikic/fast-route", @@ -919,6 +976,68 @@ ], "time": "2020-10-24T10:57:07+00:00" }, + { + "name": "symfony/finder", + "version": "v5.3.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "a10000ada1e600d109a6c7632e9ac42e8bf2fb93" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/a10000ada1e600d109a6c7632e9ac42e8bf2fb93", + "reference": "a10000ada1e600d109a6c7632e9ac42e8bf2fb93", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-php80": "^1.16" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v5.3.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-08-04T21:20:46+00:00" + }, { "name": "symfony/polyfill-ctype", "version": "v1.23.0", @@ -998,6 +1117,89 @@ ], "time": "2021-02-19T12:13:01+00:00" }, + { + "name": "symfony/polyfill-php80", + "version": "v1.23.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "1100343ed1a92e3a38f9ae122fc0eb21602547be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/1100343ed1a92e3a38f9ae122fc0eb21602547be", + "reference": "1100343ed1a92e3a38f9ae122fc0eb21602547be", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.23.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-07-28T13:41:28+00:00" + }, { "name": "symfony/yaml", "version": "v2.8.52", diff --git a/settings/formdata.yaml b/settings/formdata.yaml new file mode 100644 index 0000000..9423090 --- /dev/null +++ b/settings/formdata.yaml @@ -0,0 +1 @@ +'' \ No newline at end of file diff --git a/system/Controllers/AuthController.php b/system/Controllers/AuthController.php index 206e83c..83713b9 100644 --- a/system/Controllers/AuthController.php +++ b/system/Controllers/AuthController.php @@ -8,6 +8,7 @@ use Slim\Http\Response; use Typemill\Models\Validation; use Typemill\Models\User; use Typemill\Models\WriteYaml; +use Typemill\Extensions\ParsedownExtension; class AuthController extends Controller { @@ -34,36 +35,10 @@ class AuthController extends Controller */ public function show(Request $request, Response $response, $args) - { - $data = array(); + { + $settings = $this->c->get('settings'); - /* check previous login attemps */ - $yaml = new WriteYaml(); - $logins = $yaml->getYaml('settings/users', '.logins'); - $userIP = $this->getUserIP(); - $userLogins = isset($logins[$userIP]) ? count($logins[$userIP]) : false; - - if($userLogins) - { - /* get the latest */ - $lastLogin = intval($logins[$userIP][$userLogins-1]); - - /* if last login is longer than 60 seconds ago, clear it. */ - if(time() - $lastLogin > 60) - { - unset($logins[$userIP]); - $yaml->updateYaml('settings/users', '.logins', $logins); - } - - /* Did the user made three login attemps that failed? */ - elseif($userLogins >= 3) - { - $timeleft = 60 - (time() - $lastLogin); - $data['messages'] = array('time' => $timeleft, 'error' => array( 'Too many bad logins. Please wait.')); - } - } - - return $this->render($response, '/auth/login.twig', $data); + return $this->render($response, '/auth/login.twig', ['settings' => $settings]); } /** @@ -79,41 +54,13 @@ class AuthController extends Controller if( ( null !== $request->getattribute('csrf_result') ) OR ( $request->getattribute('csrf_result') === false ) ) { $this->c->flash->addMessage('error', 'The form has a timeout, please try again.'); - return $response->withRedirect($this->c->router->pathFor('auth.show')); + return $response->withRedirect($this->c->router->pathFor('auth.show')); } - /* log user attemps to authenticate */ - $yaml = new WriteYaml(); - $logins = $yaml->getYaml('settings/users', '.logins'); - $userIP = $this->getUserIP(); - $userLogins = isset($logins[$userIP]) ? count($logins[$userIP]) : false; - - /* if there have been user logins before. You have to do this again, because user does not always refresh the login page and old login attemps are stored. */ - if($userLogins) - { - /* get the latest */ - $lastLogin = intval($logins[$userIP][$userLogins-1]); - - /* if last login is longer than 60 seconds ago, clear it and add this attempt */ - if(time() - $lastLogin > 60) - { - unset($logins[$userIP]); - $yaml->updateYaml('settings/users', '.logins', $logins); - } - - /* Did the user made three login attemps that failed? */ - elseif($userLogins >= 2) - { - $logins[$userIP][] = time(); - $yaml->updateYaml('settings/users', '.logins', $logins); - - return $response->withRedirect($this->c->router->pathFor('auth.show')); - } - } - - /* authentication */ + /* authentication */ $params = $request->getParams(); $validation = new Validation(); + $settings = $this->c->get('settings'); if($validation->signin($params)) { @@ -131,13 +78,6 @@ class AuthController extends Controller $user->login($userdata['username']); - /* clear the user login attemps */ - if($userLogins) - { - unset($logins[$userIP]); - $yaml->updateYaml('settings/users', '.logins', $logins); - } - # if user is allowed to view content-area if($this->c->acl->hasRole($userdata['userrole']) && $this->c->acl->isAllowed($userdata['userrole'], 'content', 'view')) { @@ -149,11 +89,12 @@ class AuthController extends Controller return $response->withRedirect($this->c->router->pathFor('user.account')); } } - - /* if authentication failed, add attempt to log file */ - $logins[$userIP][] = time(); - $yaml->updateYaml('settings/users', '.logins', $logins); + if(isset($this->settings['securitylog']) && $this->settings['securitylog']) + { + \Typemill\Models\Helpers::addLogEntry('wrong login'); + } + $this->c->flash->addMessage('error', 'Ups, wrong password or username, please try again.'); return $response->withRedirect($this->c->router->pathFor('auth.show')); } @@ -176,25 +117,225 @@ class AuthController extends Controller return $response->withRedirect($this->c->router->pathFor('auth.show')); } - private function getUserIP() + public function showRecoverPassword(Request $request, Response $response, $args) { - $client = @$_SERVER['HTTP_CLIENT_IP']; - $forward = @$_SERVER['HTTP_X_FORWARDED_FOR']; - $remote = $_SERVER['REMOTE_ADDR']; + $data = array(); + + return $this->render($response, '/auth/recoverpw.twig', $data); + } - if(filter_var($client, FILTER_VALIDATE_IP)) + public function recoverPassword(Request $request, Response $response, $args) + { + $params = $request->getParams(); + $validation = new Validation(); + $settings = $this->c->get('settings'); + $uri = $request->getUri()->withUserInfo(''); + $base_url = $uri->getBaseUrl(); + + if(!isset($params['email']) OR filter_var($params['email'], \FILTER_VALIDATE_EMAIL) === false ) { - $ip = $client; + $this->c->flash->addMessage('error', 'Please enter a valid email.'); + return $response->withRedirect($this->c->router->pathFor('auth.recoverpwshow')); } - elseif(filter_var($forward, FILTER_VALIDATE_IP)) + + $user = new User(); + $requiredUser = $user->findUsersByEmail($params['email']); + + if(!$requiredUser) { - $ip = $forward; + $this->c->flash->addMessage('error', 'The email address is unknown.'); + return $response->withRedirect($this->c->router->pathFor('auth.recoverpwshow')); + } + + $requiredUser = $user->getSecureUser($requiredUser[0]); + + $requiredUser['recoverdate'] = date("Y-m-d H:i:s"); + $requiredUser['recovertoken'] = bin2hex(random_bytes(32)); + + $url = $base_url . '/tm/recoverpwnew?username=' . $requiredUser['username'] . '&recovertoken=' . $requiredUser['recovertoken']; + $link = '' . $url . ''; + + # define the headers + $headers = 'Content-Type: text/html; charset=utf-8' . "\r\n"; + $headers .= 'Content-Transfer-Encoding: base64' . "\r\n"; + if(isset($settings['recoverfrom']) && $settings['recoverfrom'] != '') + { + $headers .= 'From: ' . $settings['recoverfrom']; + } + + $subjectline = (isset($settings['recoversubject']) && ($settings['recoversubject'] != '') ) ? $settings['recoversubject'] : 'Recover your password'; + $subject = '=?UTF-8?B?' . base64_encode($subjectline) . '?='; + + $messagetext = "Dear user,

please use the following link to set a new password:"; + if(isset($settings['recovermessage']) && ($settings['recovermessage'] != '')) + { + $parsedown = new ParsedownExtension($base_url); + $parsedown->setSafeMode(true); + + $contentArray = $parsedown->text($settings['recovermessage']); + $messagetext = $parsedown->markup($contentArray); + } + + $message = base64_encode($messagetext . "

" . $link); + + # store user + $user->updateUser($requiredUser); + + $send = mail($requiredUser['email'], $subject, $message, $headers); + + if(!$send) + { + $data = [ + 'title' => 'We could not send the email', + 'message' => 'Dear ' . $requiredUser['username'] . ', we could not send the email with the password instructions to your address. You can try it again but chances are low that it will work next time. Please contact the website owner and ask for help.', + ]; } else { - $ip = $remote; + # store user + $user->updateUser($requiredUser); + + $data = [ + 'title' => 'Please check your inbox', + 'message' => 'Dear ' . $requiredUser['username'] . ', please check the inbox of your email account. We have sent you some short instructions how to recover your password. Do not forget to check your spam-folder if your inbox is empty.', + ]; + } + + return $this->render($response, '/auth/recoverpwsend.twig', $data); + } + + public function showRecoverPasswordNew(Request $request, Response $response, $args) + { + $params = $request->getParams(); + + if(!isset($params['username']) OR !isset($params['recovertoken'])) + { + $this->c->flash->addMessage('error', 'Ups, you tried to open the password recovery page but the link was invalid.'); + return $response->withRedirect($this->c->router->pathFor('auth.show')); } - return $ip; + $user = new user(); + + $requiredUser = $user->getSecureUser($params['username']); + + if(!$requiredUser) + { + $this->c->flash->addMessage('error', 'Ups, you tried to open the password recovery page but the link was invalid.'); + return $response->withRedirect($this->c->router->pathFor('auth.show')); + } + + if(!isset($requiredUser['recovertoken']) OR $requiredUser['recovertoken'] != $params['recovertoken'] ) + { + $this->c->flash->addMessage('error', 'Ups, you tried to open the password recovery page but the link was invalid.'); + return $response->withRedirect($this->c->router->pathFor('auth.show')); + } + + $recoverdate = isset($requiredUser['recoverdate']) ? $requiredUser['recoverdate'] : false; + + if(!$recoverdate ) + { + $user->unsetFromUser($requiredUser['username'], ['recovertoken']); + + $this->c->flash->addMessage('error', 'The link to recover the password was too old. Please create a new one.'); + return $response->withRedirect($this->c->router->pathFor('auth.show')); + } + + $now = new \DateTime('NOW'); + $recoverdate = new \DateTime($recoverdate); + + if(!$recoverdate) + { + $user->unsetFromUser($requiredUser['username'], ['recovertoken', 'recoverdate']); + + $this->c->flash->addMessage('error', 'The link to recover the password was too old. Please create a new one.'); + return $response->withRedirect($this->c->router->pathFor('auth.show')); + } + + $validDate = $recoverdate->add(new \DateInterval('P1D')); + + if($validDate <= $now) + { + $user->unsetFromUser($requiredUser['username'], ['recovertoken', 'recoverdate']); + + $this->c->flash->addMessage('error', 'The link to recover the password was too old. Please create a new one.'); + return $response->withRedirect($this->c->router->pathFor('auth.show')); + } + + return $this->render($response, '/auth/recoverpwnew.twig', ['recovertoken' => $params['recovertoken'],'username' => $requiredUser['username']]); + } + + public function createRecoverPasswordNew(Request $request, Response $response, $args) + { + $params = $request->getParams(); + + if(!isset($params['username']) OR !isset($params['recovertoken'])) + { + $this->c->flash->addMessage('error', 'Ups, you tried to set a new password but username or token was invalid.'); + return $response->withRedirect($this->c->router->pathFor('auth.show')); + } + + $validation = new Validation(); + + if(!$validation->recoverPassword($params)) + { + $this->c->flash->addMessage('error', 'Please check your input.'); + return $response->withRedirect($this->c->router->pathFor('auth.recoverpwshownew',[], ['username' => $params['username'], 'recovertoken' => $params['recovertoken']])); + } + + $user = new user(); + + $requiredUser = $user->getSecureUser($params['username']); + + if(!$requiredUser) + { + $this->c->flash->addMessage('error', 'Ups, you tried to create a new password but the username was invalid.'); + return $response->withRedirect($this->c->router->pathFor('auth.show')); + } + + if(!isset($requiredUser['recovertoken']) OR $requiredUser['recovertoken'] != $params['recovertoken'] ) + { + $this->c->flash->addMessage('error', 'Ups, you tried to create a new password but the token was invalid.'); + return $response->withRedirect($this->c->router->pathFor('auth.show')); + } + + $recoverdate = isset($requiredUser['recoverdate']) ? $requiredUser['recoverdate'] : false; + + if(!$recoverdate ) + { + $user->unsetFromUser($requiredUser['username'], ['recovertoken']); + + $this->c->flash->addMessage('error', 'The date for the password reset was invalid. Please create a new one.'); + return $response->withRedirect($this->c->router->pathFor('auth.show')); + } + + $now = new \DateTime('NOW'); + $recoverdate = new \DateTime($recoverdate); + + if(!$recoverdate) + { + $user->unsetFromUser($requiredUser['username'], ['recovertoken', 'recoverdate']); + + $this->c->flash->addMessage('error', 'The date for the password reset was too old. Please create a new one.'); + return $response->withRedirect($this->c->router->pathFor('auth.show')); + } + + $validDate = $recoverdate->add(new \DateInterval('P1D')); + + if($validDate <= $now) + { + $user->unsetFromUser($requiredUser['username'], ['recovertoken', 'recoverdate']); + + $this->c->flash->addMessage('error', 'The link to recover the password was too old. Please create a new one.'); + return $response->withRedirect($this->c->router->pathFor('auth.show')); + } + + $requiredUser['password'] = $params['password']; + $user->updateUser($requiredUser); + $user->unsetFromUser($requiredUser['username'], ['recovertoken', 'recoverdate']); + + unset($_SESSION['old']); + + $this->c->flash->addMessage('info', 'A new password has been created. Please login.'); + return $response->withRedirect($this->c->router->pathFor('auth.show')); } } \ No newline at end of file diff --git a/system/Controllers/FormController.php b/system/Controllers/FormController.php index 4e77827..a9c4b86 100644 --- a/system/Controllers/FormController.php +++ b/system/Controllers/FormController.php @@ -12,7 +12,7 @@ class FormController extends Controller *************************************/ public function savePublicForm($request, $response, $args) - { + { if($request->isPost()) { $params = $request->getParams(); @@ -26,46 +26,6 @@ class FormController extends Controller $this->c->flash->addMessage('error', 'The form has a timeout. Please try again.'); return $response->withRedirect($referer[0]); } - - # simple bot check with honeypot - if(isset($params[$pluginName]['personal-mail'])) - { - if($params[$pluginName]['personal-mail'] != '') - { - $this->c->flash->addMessage('publicform', 'bot'); - return $response->withRedirect($referer[0]); - } - unset($params[$pluginName]['personal-mail']); - } - - #recaptcha check - if(isset($params['g-recaptcha-response'])) - { - $recaptchaApi = 'https://www.google.com/recaptcha/api/siteverify'; - $settings = $this->c->get('settings'); - $secret = isset($settings['plugins'][$pluginName]['recaptcha_secretkey']) ? $settings['plugins'][$pluginName]['recaptcha_secretkey'] : false; - $recaptchaRequest = ['secret' => $secret, 'response' => $params['g-recaptcha-response']]; - - # use key 'http' even if you send the request to https://... - $options = array( - 'http' => array( - 'header' => "Content-type: application/x-www-form-urlencoded\r\n", - 'method' => 'POST', - 'content' => http_build_query($recaptchaRequest), - 'timeout' => 5 - ) - ); - - $context = stream_context_create($options); - $result = file_get_contents($recaptchaApi, false, $context); - $result = json_decode($result); - - if ($result === FALSE || $result->success === FALSE) - { - $this->c->flash->addMessage('publicform', 'bot'); - return $response->withRedirect($referer[0]); - } - } if(isset($params[$pluginName])) { diff --git a/system/Controllers/SettingsController.php b/system/Controllers/SettingsController.php index 5d32191..f73b4b6 100644 --- a/system/Controllers/SettingsController.php +++ b/system/Controllers/SettingsController.php @@ -113,6 +113,11 @@ class SettingsController extends Controller 'headersoff' => isset($newSettings['headersoff']) ? true : null, 'urlschemes' => $newSettings['urlschemes'], 'svg' => isset($newSettings['svg']) ? true : null, + 'recoverpw' => isset($newSettings['recoverpw']) ? true : null, + 'recoverfrom' => $newSettings['recoverfrom'], + 'recoversubject' => $newSettings['recoversubject'], + 'recovermessage' => $newSettings['recovermessage'], + 'securitylog' => isset($newSettings['securitylog']) ? true : null, ); # https://www.slimframework.com/docs/v3/cookbook/uploading-files.html; @@ -348,15 +353,7 @@ class SettingsController extends Controller /* if the plugin defines forms and fields, so that the user can edit the plugin settings in the frontend */ if(isset($pluginOriginalSettings['forms']['fields'])) - { - # if the plugin defines frontend fields - if(isset($pluginOriginalSettings['public'])) - { - $pluginOriginalSettings['forms']['fields']['recaptcha'] = ['type' => 'checkbox', 'label' => 'Google Recaptcha', 'checkboxlabel' => 'Activate Recaptcha' ]; - $pluginOriginalSettings['forms']['fields']['recaptcha_webkey'] = ['type' => 'text', 'label' => 'Recaptcha Website Key', 'help' => 'Add the recaptcha website key here. You can get the key from the recaptcha website.', 'description' => 'The website key is mandatory if you activate the recaptcha field']; - $pluginOriginalSettings['forms']['fields']['recaptcha_secretkey'] = ['type' => 'text', 'label' => 'Recaptcha Secret Key', 'help' => 'Add the recaptcha secret key here. You can get the key from the recaptcha website.', 'description' => 'The secret key is mandatory if you activate the recaptcha field']; - } - + { /* get all the fields and prefill them with the dafault-data, the user-data or old input data */ $fields = $fieldsModel->getFields($userSettings, 'plugins', $pluginName, $pluginOriginalSettings); @@ -1132,14 +1129,6 @@ class SettingsController extends Controller } } - # if the plugin defines frontend fields - if(isset($originalSettings['public'])) - { - $originalFields['recaptcha'] = ['type' => 'checkbox', 'label' => 'Google Recaptcha', 'checkboxlabel' => 'Activate Recaptcha' ]; - $originalFields['recaptcha_webkey'] = ['type' => 'text', 'label' => 'Recaptcha Website Key', 'help' => 'Add the recaptcha website key here. You can get the key from the recaptcha website.', 'description' => 'The website key is mandatory if you activate the recaptcha field']; - $originalFields['recaptcha_secretkey'] = ['type' => 'text', 'label' => 'Recaptcha Secret Key', 'help' => 'Add the recaptcha secret key here. You can get the key from the recaptcha website.', 'description' => 'The secret key is mandatory if you activate the recaptcha field']; - } - # if plugin is not active, then skip required $skiprequired = false; if($objectType == 'plugins' && !isset($userInput['active'])) diff --git a/system/Extensions/TwigCaptchaExtension.php b/system/Extensions/TwigCaptchaExtension.php new file mode 100644 index 0000000..0dc18cc --- /dev/null +++ b/system/Extensions/TwigCaptchaExtension.php @@ -0,0 +1,44 @@ +build(); + + $error = ''; + if(isset($_SESSION['captcha']) && $_SESSION['captcha'] === 'error') + { + $error = 'The captcha was wrong.'; + } + + $_SESSION['phrase'] = $builder->getPhrase(); + + $_SESSION['captcha'] = true; + + $template = '
' . + '' . + '' . + $error . + '' . + '
'; + + return $template; + } + } +} \ No newline at end of file diff --git a/system/Middleware/SecurityMiddleware.php b/system/Middleware/SecurityMiddleware.php new file mode 100644 index 0000000..b90d91a --- /dev/null +++ b/system/Middleware/SecurityMiddleware.php @@ -0,0 +1,117 @@ +router = $router; + $this->settings = $settings; + $this->flash = $flash; + } + + public function __invoke(Request $request, Response $response, $next) + { + if($request->isPost()) + { + $referer = $request->getHeader('HTTP_REFERER'); + + # check csrf protection + if( $request->getattribute('csrf_result') === false ) + { + $this->flash->addMessage('error', 'The form has a timeout. Please try again.'); + return $response->withRedirect($referer[0]); + } + + # simple bot check with honeypot + if( (null !== $request->getParam('personal-honey-mail') ) && ($request->getParam('personal-honey-mail') != '') ) + { + if(isset($this->settings['securitylog']) && $this->settings['securitylog']) + { + \Typemill\Models\Helpers::addLogEntry('honeypot ' . $referer[0]); + } + + $this->flash->addMessage('notice', 'Hey honey, you made it right!'); + return $response->withRedirect($this->router->pathFor('home')); + } + + # check captcha + if(isset($_SESSION['captcha'])) + { + # if captcha field was filled correctly + if( (null !== $request->getParam('captcha')) && \Gregwar\Captcha\PhraseBuilder::comparePhrases($_SESSION['phrase'], $request->getParam('captcha') ) ) + { + # delete captcha because it is solved and should not show up again + unset($_SESSION['captcha']); + + # delete phrase because can't use twice + unset($_SESSION['phrase']); + } + else + { + # delete phrase because can't use twice, but keep captcha so it shows up again + unset($_SESSION['phrase']); + + # set session to error + $_SESSION['captcha'] = 'error'; + + if(isset($this->settings['securitylog']) && $this->settings['securitylog']) + { + \Typemill\Models\Helpers::addLogEntry('wrong captcha ' . $referer[0]); + } + + # and add message that captcha is empty + $this->flash->addMessage('error', 'Captcha is wrong.'); + return $response->withRedirect($referer[0]); + } + } + + #check google recaptcha + if( null !== $request->getParam('g-recaptcha-response') ) + { + $recaptchaApi = 'https://www.google.com/recaptcha/api/siteverify'; + $settings = $this->c->get('settings'); + $secret = isset($settings['plugins'][$pluginName]['recaptcha_secretkey']) ? $settings['plugins'][$pluginName]['recaptcha_secretkey'] : false; + $recaptchaRequest = ['secret' => $secret, 'response' => $request->getParam('g-recaptcha-response')]; + + # use key 'http' even if you send the request to https://... + $options = array( + 'http' => array( + 'header' => "Content-type: application/x-www-form-urlencoded\r\n", + 'method' => 'POST', + 'content' => http_build_query($recaptchaRequest), + 'timeout' => 5 + ) + ); + + $context = stream_context_create($options); + $result = file_get_contents($recaptchaApi, false, $context); + $result = json_decode($result); + + if ($result === FALSE || $result->success === FALSE) + { + if(isset($this->settings['securitylog']) && $this->settings['securitylog']) + { + \Typemill\Models\Helpers::addLogEntry('wrong google recaptcha ' . $referer[0]); + } + + # and add message that captcha is empty + $this->flash->addMessage('error', 'Captcha is wrong.'); + return $response->withRedirect($referer[0]); + } + } + } + + return $next($request, $response); + } +} \ No newline at end of file diff --git a/system/Middleware/ValidationErrorsMiddleware.php b/system/Middleware/ValidationErrorsMiddleware.php index 9010707..b1db99a 100644 --- a/system/Middleware/ValidationErrorsMiddleware.php +++ b/system/Middleware/ValidationErrorsMiddleware.php @@ -23,6 +23,13 @@ class ValidationErrorsMiddleware unset($_SESSION['errors']); } + + if(isset($_SESSION['phrase'])) + { + $this->view->getEnvironment()->addGlobal('errors', ['captcha' => 'the captcha is wrong, please try again']); + + unset($_SESSION['phrase']); + } return $next($request, $response); } diff --git a/system/Middleware/accessController.php b/system/Middleware/accessMiddleware.php similarity index 83% rename from system/Middleware/accessController.php rename to system/Middleware/accessMiddleware.php index e23ee6d..aff2769 100644 --- a/system/Middleware/accessController.php +++ b/system/Middleware/accessMiddleware.php @@ -6,7 +6,7 @@ use Slim\Interfaces\RouterInterface; use Slim\Http\Request; use Slim\Http\Response; -class accessController +class accessMiddleware { protected $router; @@ -31,6 +31,12 @@ class accessController return $response->withRedirect($this->router->pathFor('auth.show')); } + # make sure logged in users do not have captchas + if(isset($_SESSION['captcha'])) + { + unset($_SESSION['captcha']); + } + if(!$this->acl->hasRole($_SESSION['role'])) { $_SESSION['role'] = 'member'; diff --git a/system/Models/Helpers.php b/system/Models/Helpers.php index 1a189b8..4cbd24d 100644 --- a/system/Models/Helpers.php +++ b/system/Models/Helpers.php @@ -2,27 +2,51 @@ namespace Typemill\Models; +use Typemill\Models\Write; + class Helpers{ - public static function printTimer($timer) + public static function getUserIP() { - $lastTime = NULL; - $table = ''; - $table .= ''; - foreach($timer as $breakpoint => $time) + $client = @$_SERVER['HTTP_CLIENT_IP']; + $forward = @$_SERVER['HTTP_X_FORWARDED_FOR']; + $remote = $_SERVER['REMOTE_ADDR']; + + if(filter_var($client, FILTER_VALIDATE_IP)) { - $duration = $time - $lastTime; - - $table .= ''; - $table .= ''; - $table .= ''; - $table .= ''; - $table .= ''; - - $lastTime = $time; + $ip = $client; } - $table .= '
BreakpointTimeDuration
' . $breakpoint . '' . $time . '' . $duration . '
'; - echo $table; + elseif(filter_var($forward, FILTER_VALIDATE_IP)) + { + $ip = $forward; + } + else + { + $ip = $remote; + } + + return $ip; + } + + public static function addLogEntry($action) + { + $line = self::getUserIP(); + $line .= ';' . date("Y-m-d H:i:s"); + $line .= ';' . $action; + + $write = new Write(); + $logfile = $write->getFile('cache', 'securitylog.txt'); + + if($logfile) + { + $logfile .= $line . PHP_EOL; + } + else + { + $logfile = $line . PHP_EOL; + } + + $write->writeFile('cache', 'securitylog.txt', $logfile); } public static function array_sort($array, $on, $order=SORT_ASC) @@ -59,4 +83,25 @@ class Helpers{ return $new_array; } + + public static function printTimer($timer) + { + $lastTime = NULL; + $table = ''; + $table .= ''; + foreach($timer as $breakpoint => $time) + { + $duration = $time - $lastTime; + + $table .= ''; + $table .= ''; + $table .= ''; + $table .= ''; + $table .= ''; + + $lastTime = $time; + } + $table .= '
BreakpointTimeDuration
' . $breakpoint . '' . $time . '' . $duration . '
'; + echo $table; + } } \ No newline at end of file diff --git a/system/Models/User.php b/system/Models/User.php index 06e4581..ca66671 100644 --- a/system/Models/User.php +++ b/system/Models/User.php @@ -21,12 +21,11 @@ class User extends WriteYaml $usernames[] = str_replace('.yaml', '', $userfile); } - usort($usernames, 'strnatcasecmp'); return $usernames; } - + public function getUser($username) { $user = $this->getYaml('settings/users', $username . '.yaml'); @@ -39,7 +38,7 @@ class User extends WriteYaml unset($user['password']); return $user; } - + public function createUser($params) { $params['password'] = $this->generatePassword($params['password']); @@ -52,6 +51,34 @@ class User extends WriteYaml } return false; } + + public function unsetFromUser($username, $keys) + { + if(empty($keys)) + { + return false; + } + + $userdata = $this->getUser($username); + + if(!$userdata) + { + return false; + } + + foreach($keys as $key) + { + if(isset($userdata[$key])) + { + unset($userdata[$key]); + } + } + + $this->updateYaml('settings/users', $userdata['username'] . '.yaml', $userdata); + + return true; + } + public function updateUser($params) { @@ -75,7 +102,6 @@ class User extends WriteYaml # cleanup data here - $this->updateYaml('settings/users', $userdata['username'] . '.yaml', $update); $this->deleteUserIndex(); @@ -110,12 +136,11 @@ class User extends WriteYaml public function login($username) { - $user = $this->getUser($username); + $user = $this->getSecureUser($username); if($user) { $user['lastlogin'] = time(); - unset($user['password']); $_SESSION['user'] = $user['username']; $_SESSION['role'] = $user['userrole']; @@ -132,6 +157,11 @@ class User extends WriteYaml # update user last login $this->updateUser($user); + + if(isset($user['recovertoken']) OR isset($user['recoverdate'])) + { + $this->unsetFromUser($user['username'], ['recovertoken', 'recoverdate']); + } } } diff --git a/system/Models/Validation.php b/system/Models/Validation.php index 0cde6be..909f2f7 100644 --- a/system/Models/Validation.php +++ b/system/Models/Validation.php @@ -261,6 +261,23 @@ class Validation return $this->validationResult($v); } + /** + * validation for password recovery + * + * @param array $params with form data. + * @return obj $v the validation object passed to a result method. + */ + + public function recoverPassword(array $params) + { + $v = new Validator($params); + $v->rule('required', ['password', 'passwordrepeat']); + $v->rule('lengthBetween', 'password', 5, 20); + $v->rule('equals', 'passwordrepeat', 'password'); + + return $this->validationResult($v); + } + /** * validation for system settings * @@ -285,6 +302,11 @@ class Validation $v->rule('in', 'copyright', $copyright); $v->rule('noHTML', 'restrictionnotice'); $v->rule('lengthBetween', 'restrictionnotice', 2, 1000 ); + $v->rule('email', 'recoverfrom'); + $v->rule('noHTML', 'recoversubject'); + $v->rule('lengthBetween', 'recoversubject', 2, 80 ); + $v->rule('noHTML', 'recovermessage'); + $v->rule('lengthBetween', 'recovermessage', 2, 1000 ); $v->rule('iplist', 'trustedproxies'); return $this->validationResult($v, $name); diff --git a/system/Plugin.php b/system/Plugin.php index eb908c4..9c91b24 100644 --- a/system/Plugin.php +++ b/system/Plugin.php @@ -203,13 +203,11 @@ abstract class Plugin implements EventSubscriberInterface $pluginDefinitions = \Typemill\Settings::getObjectSettings('plugins', $pluginName); $settings = $this->getSettings(); $buttonlabel = isset($settings['plugins'][$pluginName]['button_label']) ? $settings['plugins'][$pluginName]['button_label'] : false; + $captchaoptions = isset($settings['plugins'][$pluginName]['captchaoptions']) ? $settings['plugins'][$pluginName]['captchaoptions'] : false; $recaptcha = isset($settings['plugins'][$pluginName]['recaptcha']) ? $settings['plugins'][$pluginName]['recaptcha_webkey'] : false; - + if(isset($pluginDefinitions['public']['fields'])) - { - # add simple honeypot spam protection - $pluginDefinitions['public']['fields']['personal-mail'] = ['type' => 'text', 'class' => 'personal-mail']; - + { # get all the fields and prefill them with the dafault-data, the user-data or old input data $fields = $fieldsModel->getFields($settings, 'plugins', $pluginName, $pluginDefinitions, 'public'); @@ -217,7 +215,14 @@ abstract class Plugin implements EventSubscriberInterface $twig = $this->getTwig(); # render each field and add it to the form - $form = $twig->fetch('/partials/form.twig', ['fields' => $fields, 'itemName' => $pluginName, 'object' => 'plugins', 'recaptcha_webkey' => $recaptcha, 'buttonlabel' => $buttonlabel]); + $form = $twig->fetch('/partials/form.twig', [ + 'fields' => $fields, + 'itemName' => $pluginName, + 'object' => 'plugins', + 'buttonlabel' => $buttonlabel, + 'captchaoptions' => $captchaoptions, + 'recaptcha_webkey' => $recaptcha, + ]); } return $form; diff --git a/system/Routes/Web.php b/system/Routes/Web.php index dee11e3..58b826f 100644 --- a/system/Routes/Web.php +++ b/system/Routes/Web.php @@ -9,7 +9,7 @@ use Typemill\Controllers\ContentBackendController; use Typemill\Middleware\RedirectIfUnauthenticated; use Typemill\Middleware\RedirectIfAuthenticated; use Typemill\Middleware\RedirectIfNoAdmin; -use Typemill\Middleware\accessController; +use Typemill\Middleware\accessMiddleware; if($settings['settings']['setup']) { @@ -36,26 +36,34 @@ $app->get('/tm/login', AuthController::class . ':show')->setName('auth.show')->a $app->post('/tm/login', AuthController::class . ':login')->setName('auth.login')->add(new RedirectIfAuthenticated($container['router'], $container['settings'])); $app->get('/tm/logout', AuthController::class . ':logout')->setName('auth.logout')->add(new RedirectIfUnauthenticated($container['router'], $container['flash'])); -$app->get('/tm/settings', SettingsController::class . ':showSettings')->setName('settings.show')->add(new accessController($container['router'], $container['acl'], 'system', 'view')); -$app->post('/tm/settings', SettingsController::class . ':saveSettings')->setName('settings.save')->add(new accessController($container['router'], $container['acl'], 'system', 'update')); +if(isset($settings['settings']['recoverpw']) && $settings['settings']['recoverpw']) +{ + $app->get('/tm/recoverpw', AuthController::class . ':showrecoverpassword')->setName('auth.recoverpwshow')->add(new RedirectIfAuthenticated($container['router'], $container['settings'])); + $app->post('/tm/recoverpw', AuthController::class . ':recoverpassword')->setName('auth.recoverpw')->add(new RedirectIfAuthenticated($container['router'], $container['settings'])); + $app->get('/tm/recoverpwnew', AuthController::class . ':showrecoverpasswordnew')->setName('auth.recoverpwshownew')->add(new RedirectIfAuthenticated($container['router'], $container['settings'])); + $app->post('/tm/recoverpwnew', AuthController::class . ':createrecoverpasswordnew')->setName('auth.recoverpwnew')->add(new RedirectIfAuthenticated($container['router'], $container['settings'])); +} -$app->get('/tm/themes', SettingsController::class . ':showThemes')->setName('themes.show')->add(new accessController($container['router'], $container['acl'], 'system', 'view')); -$app->post('/tm/themes', SettingsController::class . ':saveThemes')->setName('themes.save')->add(new accessController($container['router'], $container['acl'], 'system', 'update')); +$app->get('/tm/settings', SettingsController::class . ':showSettings')->setName('settings.show')->add(new accessMiddleware($container['router'], $container['acl'], 'system', 'view')); +$app->post('/tm/settings', SettingsController::class . ':saveSettings')->setName('settings.save')->add(new accessMiddleware($container['router'], $container['acl'], 'system', 'update')); -$app->get('/tm/plugins', SettingsController::class . ':showPlugins')->setName('plugins.show')->add(new accessController($container['router'], $container['acl'], 'system', 'view')); -$app->post('/tm/plugins', SettingsController::class . ':savePlugins')->setName('plugins.save')->add(new accessController($container['router'], $container['acl'], 'system', 'update')); +$app->get('/tm/themes', SettingsController::class . ':showThemes')->setName('themes.show')->add(new accessMiddleware($container['router'], $container['acl'], 'system', 'view')); +$app->post('/tm/themes', SettingsController::class . ':saveThemes')->setName('themes.save')->add(new accessMiddleware($container['router'], $container['acl'], 'system', 'update')); -$app->get('/tm/account', SettingsController::class . ':showAccount')->setName('user.account')->add(new accessController($container['router'], $container['acl'], 'user', 'view')); -$app->get('/tm/user/new', SettingsController::class . ':newUser')->setName('user.new')->add(new accessController($container['router'], $container['acl'], 'user', 'create')); -$app->post('/tm/user/create', SettingsController::class . ':createUser')->setName('user.create')->add(new accessController($container['router'], $container['acl'], 'user', 'create')); -$app->post('/tm/user/update', SettingsController::class . ':updateUser')->setName('user.update')->add(new accessController($container['router'], $container['acl'], 'user', 'update')); -$app->post('/tm/user/delete', SettingsController::class . ':deleteUser')->setName('user.delete')->add(new accessController($container['router'], $container['acl'], 'user', 'delete')); -$app->get('/tm/user/{username}', SettingsController::class . ':showUser')->setName('user.show')->add(new accessController($container['router'], $container['acl'], 'user', 'view')); -$app->get('/tm/users', SettingsController::class . ':listUser')->setName('user.list')->add(new accessController($container['router'], $container['acl'], 'userlist', 'view')); +$app->get('/tm/plugins', SettingsController::class . ':showPlugins')->setName('plugins.show')->add(new accessMiddleware($container['router'], $container['acl'], 'system', 'view')); +$app->post('/tm/plugins', SettingsController::class . ':savePlugins')->setName('plugins.save')->add(new accessMiddleware($container['router'], $container['acl'], 'system', 'update')); -$app->get('/tm/content/raw[/{params:.*}]', ContentBackendController::class . ':showContent')->setName('content.raw')->add(new accessController($container['router'], $container['acl'], 'content', 'view')); -$app->get('/tm/content/visual[/{params:.*}]', ContentBackendController::class . ':showBlox')->setName('content.visual')->add(new accessController($container['router'], $container['acl'], 'content', 'view')); -$app->get('/tm/content[/{params:.*}]', ContentBackendController::class . ':showEmpty')->setName('content.empty')->add(new accessController($container['router'], $container['acl'], 'content', 'view')); +$app->get('/tm/account', SettingsController::class . ':showAccount')->setName('user.account')->add(new accessMiddleware($container['router'], $container['acl'], 'user', 'view')); +$app->get('/tm/user/new', SettingsController::class . ':newUser')->setName('user.new')->add(new accessMiddleware($container['router'], $container['acl'], 'user', 'create')); +$app->post('/tm/user/create', SettingsController::class . ':createUser')->setName('user.create')->add(new accessMiddleware($container['router'], $container['acl'], 'user', 'create')); +$app->post('/tm/user/update', SettingsController::class . ':updateUser')->setName('user.update')->add(new accessMiddleware($container['router'], $container['acl'], 'user', 'update')); +$app->post('/tm/user/delete', SettingsController::class . ':deleteUser')->setName('user.delete')->add(new accessMiddleware($container['router'], $container['acl'], 'user', 'delete')); +$app->get('/tm/user/{username}', SettingsController::class . ':showUser')->setName('user.show')->add(new accessMiddleware($container['router'], $container['acl'], 'user', 'view')); +$app->get('/tm/users', SettingsController::class . ':listUser')->setName('user.list')->add(new accessMiddleware($container['router'], $container['acl'], 'userlist', 'view')); + +$app->get('/tm/content/raw[/{params:.*}]', ContentBackendController::class . ':showContent')->setName('content.raw')->add(new accessMiddleware($container['router'], $container['acl'], 'content', 'view')); +$app->get('/tm/content/visual[/{params:.*}]', ContentBackendController::class . ':showBlox')->setName('content.visual')->add(new accessMiddleware($container['router'], $container['acl'], 'content', 'view')); +$app->get('/tm/content[/{params:.*}]', ContentBackendController::class . ':showEmpty')->setName('content.empty')->add(new accessMiddleware($container['router'], $container['acl'], 'content', 'view')); foreach($routes as $pluginRoute) { @@ -67,11 +75,11 @@ foreach($routes as $pluginRoute) if(isset($pluginRoute['name'])) { - $app->{$method}($route, $class)->setName($pluginRoute['name'])->add(new accessController($container['router'], $container['acl'], $resource, $privilege)); + $app->{$method}($route, $class)->setName($pluginRoute['name'])->add(new accessMiddleware($container['router'], $container['acl'], $resource, $privilege)); } else { - $app->{$method}($route, $class)->add(new accessController($container['router'], $container['acl'], $resource, $privilege)); + $app->{$method}($route, $class)->add(new accessMiddleware($container['router'], $container['acl'], $resource, $privilege)); } } @@ -81,7 +89,7 @@ if($settings['settings']['setup']) } elseif(isset($settings['settings']['access']) && $settings['settings']['access'] != '') { - $app->get('/[{params:.*}]', PageController::class . ':index')->setName('home')->add(new accessController($container['router'], $container['acl'], 'user', 'view')); + $app->get('/[{params:.*}]', PageController::class . ':index')->setName('home')->add(new accessMiddleware($container['router'], $container['acl'], 'user', 'view')); } else { diff --git a/system/Settings.php b/system/Settings.php index 554f961..248c433 100644 --- a/system/Settings.php +++ b/system/Settings.php @@ -186,6 +186,11 @@ class Settings 'headersoff' => true, 'urlschemes' => true, 'svg' => true, + 'recoverpw' => true, + 'recoversubject' => true, + 'recovermessage' => true, + 'recoverfrom' => true, + 'securitylog' => true, ]; # cleanup the existing usersettings diff --git a/system/author/auth/login.twig b/system/author/auth/login.twig index 476db36..92b28fd 100644 --- a/system/author/auth/login.twig +++ b/system/author/auth/login.twig @@ -26,22 +26,34 @@ {{ errors.password | first }} {% endif %} +
+ + +
+ + {{ captcha(old) }} +
- {{ csrf_field() | raw }} - - {% if messages.time %} -
{{ __('wait') }} {{ messages.time }} sec
- - {% endif %} + {{ csrf_field() | raw }}
-
-

{{ __('back to startpage') }}

-
+ + {% if settings.recoverpw %} + +

{{ __('back to startpage') }}{{ __('forgot password') }}

+ + {% else %} + +
+

{{ __('back to startpage') }}

+
+ + {% endif %} + {% endblock %} \ No newline at end of file diff --git a/system/author/auth/recoverpw.twig b/system/author/auth/recoverpw.twig new file mode 100644 index 0000000..e8b2685 --- /dev/null +++ b/system/author/auth/recoverpw.twig @@ -0,0 +1,39 @@ +{% extends 'layouts/layoutAuth.twig' %} +{% block title %}Login{% endblock %} + +{% block content %} + +
+ +
+
+ +
+
+ + + {% if errors.email %} + {{ errors.email | first }} + {% endif %} +
+
+ + +
+ + {{ captcha(true) }} + +
+ +
+ + {{ csrf_field() | raw }} +
+
+
+ +
+ +{% endblock %} \ No newline at end of file diff --git a/system/author/auth/recoverpwnew.twig b/system/author/auth/recoverpwnew.twig new file mode 100644 index 0000000..b671e19 --- /dev/null +++ b/system/author/auth/recoverpwnew.twig @@ -0,0 +1,41 @@ +{% extends 'layouts/layoutAuth.twig' %} +{% block title %}Login{% endblock %} + +{% block content %} + +
+ +
+
+ +
+
+ + + {% if errors.password %} + {{ errors.password | first }} + {% endif %} +
+
+ + + {% if errors.passwordrepeat %} + {{ errors.passwordrepeat | first }} + {% endif %} +
+
+ +
+ + {{ csrf_field() | raw }} + + +
+
+
+ +
+ +{% endblock %} \ No newline at end of file diff --git a/system/author/auth/recoverpwsend.twig b/system/author/auth/recoverpwsend.twig new file mode 100644 index 0000000..75df2f3 --- /dev/null +++ b/system/author/auth/recoverpwsend.twig @@ -0,0 +1,19 @@ +{% extends 'layouts/layoutAuth.twig' %} +{% block title %}Login{% endblock %} + +{% block content %} + +
+ +
+ +

{{ title }}

+

{{ message }}

+ +
+ +
+ +{% endblock %} \ No newline at end of file diff --git a/system/author/css/style.css b/system/author/css/style.css index ab080e9..cb41b45 100644 --- a/system/author/css/style.css +++ b/system/author/css/style.css @@ -639,6 +639,9 @@ footer{ margin-left: auto; margin-right: auto; } +.setupWrapper.extrawidth{ + max-width: 500px; +} .setupWrapper input[type="submit"] { border-radius: 0px; @@ -653,6 +656,13 @@ footer{ margin: 0 0 30px 0; } +/* honeypot field */ +.personal-mail{ display:none; } + +/* captcha image */ +img.captcha{ margin: 10px 0; } + + /************************ * OPEN-CLOSE BUTTON * ************************/ @@ -2688,12 +2698,12 @@ footer a:focus, footer a:hover, footer a:active { text-decoration: underline; } -.setupContent a, .setupContent a:link, .setupContent a:visited +.setupWrapper a, .setupWrapper a:link, .setupWrapper a:visited { text-decoration: none; color: #444; } -.setupContent a:focus, .setupContent a:hover, .setupContent a:active +.setupWrapper a:focus, .setupWrapper a:hover, .setupWrapper a:active { color: #e0474c; } diff --git a/system/author/js/vue-meta.js b/system/author/js/vue-meta.js index 4ff125b..ccb6267 100644 --- a/system/author/js/vue-meta.js +++ b/system/author/js/vue-meta.js @@ -22,7 +22,7 @@ Vue.component('tab-meta', { } }, template: '
' + - '
' + + '
' + '' + '
{{ slugerror }}
' + '
' + @@ -54,8 +54,11 @@ Vue.component('tab-meta', { '
', mounted: function() { - this.slug = this.$parent.item.slug; - this.originalSlug = this.slug; + if(this.$parent.item.slug != '') + { + this.slug = this.$parent.item.slug; + this.originalSlug = this.slug; + } }, methods: { selectComponent: function(field) diff --git a/system/author/layouts/layoutAuth.twig b/system/author/layouts/layoutAuth.twig index 8e502ef..4c6f495 100644 --- a/system/author/layouts/layoutAuth.twig +++ b/system/author/layouts/layoutAuth.twig @@ -23,14 +23,21 @@ - {% include 'partials/symbols.twig' %} + + + + bookmark-o + + + + {% include 'partials/flash.twig' %} +
{% block content %}{% endblock %}
- \ No newline at end of file diff --git a/system/author/partials/form.twig b/system/author/partials/form.twig index da95e6a..de90336 100644 --- a/system/author/partials/form.twig +++ b/system/author/partials/form.twig @@ -27,13 +27,30 @@ {% endif %} {% endfor %} - + +
+ + +
+ {{ csrf_field() | raw }} - + + {% if captchaoptions == 'disabled' %} + + {% elseif captchaoptions == 'aftererror' %} + + {{ captcha(old) }} + + {% else %} + + {{ captcha(true) }} + + {% endif %} + {% if recaptcha_webkey %}

{% endif %} - + diff --git a/system/author/settings/system.twig b/system/author/settings/system.twig index 1adf078..933a64c 100644 --- a/system/author/settings/system.twig +++ b/system/author/settings/system.twig @@ -74,7 +74,7 @@
-

{{ __('General Presentation') }}

+

{{ __('Media') }}

- -
+
+ + +
{{ __('This applies only for future images in the content area.') }}
+ {% if errors.settings.images.live.width %} + {{ errors.settings.images.live.width | first }} + {% endif %} +
+
+ + +
{{ __('If you add a value for the height, then the image will be cropped.') }}
+ {% if errors.settings.images.live.height %} + {{ errors.settings.images.live.height | first }} + {% endif %} +
+
+ +
@@ -139,6 +156,18 @@ {% endfor %} +
+ + +
+ + + {% if errors.settings.urlschemes %} + {{ errors.settings.urlschemes | first }} + {% endif %}

@@ -182,6 +211,38 @@
+
+

{{ __('Password Recovery') }}

+

{{ __('Activate the password recovery only if it is really needed. If possible use your password manager instead.') }}

+
+
+ + +
+ + + {% if errors.settings.recoverfrom %} + {{ errors.settings.recoverfrom | first }} + {% endif %} +
+
+ + + {% if errors.settings.recoversubject %} + {{ errors.settings.recoversubject | first }} + {% endif %} +
+
+ + + {% if errors.settings.recovermessage %} + {{ errors.settings.recovermessage | first }} + {% endif %} +
+

{{ __('Developer') }}

{{ __('The following options are only for developers') }}

@@ -193,6 +254,13 @@ +
+ + +
-
- - -
{{ __('This applies only for future images in the content area.') }}
- {% if errors.settings.images.live.width %} - {{ errors.settings.images.live.width | first }} - {% endif %} -
-
- - -
{{ __('If you add a value for the height, then the image will be cropped.') }}
- {% if errors.settings.images.live.height %} - {{ errors.settings.images.live.height | first }} - {% endif %} -
-
- - -
-
- - - {% if errors.settings.urlschemes %} - {{ errors.settings.urlschemes | first }} - {% endif %} -
diff --git a/system/system.php b/system/system.php index 1fdd891..62829c7 100644 --- a/system/system.php +++ b/system/system.php @@ -249,9 +249,6 @@ $container['view'] = function ($container) use ($uri) # Instantiate and add Slim specific extension $router = $container->get('router'); -# $basePath = rtrim(str_ireplace('index.php', '', $uri->getBasePath()), '/'); -# $view->addExtension(new Slim\Views\TwigExtension($container['router'], $basePath)); - $view->addExtension(new Slim\Views\TwigExtension($router, $uri)); $view->addExtension(new Twig_Extension_Debug()); $view->addExtension(new Typemill\Extensions\TwigUserExtension()); @@ -259,15 +256,12 @@ $container['view'] = function ($container) use ($uri) $view->addExtension(new Typemill\Extensions\TwigMetaExtension()); $view->addExtension(new Typemill\Extensions\TwigPagelistExtension()); - # use {{ base_url() }} in twig templates -# $view['base_url'] = $uri->getBaseUrl(); -# $view['current_url'] = $uri->getPath(); - # if session route, add flash messages and csrf-protection if($container['flash']) { $view->getEnvironment()->addGlobal('flash', $container->flash); $view->addExtension(new Typemill\Extensions\TwigCsrfExtension($container['csrf'])); + $view->addExtension(new Typemill\Extensions\TwigCaptchaExtension()); } /****************************** @@ -289,17 +283,6 @@ $container['view'] = function ($container) use ($uri) return $view; }; -# $container->dispatcher->dispatch('onTwigLoaded'); - -/*************************** -* ADD NOT FOUND HANDLER * -***************************/ - -$container['notFoundHandler'] = function($c) -{ - return new \Typemill\Handlers\NotFoundHandler($c['view']); -}; - /************************ * ADD MIDDLEWARE * ************************/ @@ -317,6 +300,7 @@ foreach($middleware as $pluginMiddleware) if($container['flash']) { $app->add(new \Typemill\Middleware\ValidationErrorsMiddleware($container['view'])); + $app->add(new \Typemill\Middleware\SecurityMiddleware($container['router'], $container['settings'], $container['flash'])); $app->add(new \Typemill\Middleware\OldInputMiddleware($container['view'])); $app->add($container->get('csrf')); }