1
0
mirror of https://github.com/typemill/typemill.git synced 2025-07-30 19:00:32 +02:00

Version 1.4.9: Password recovery and security middleware

This commit is contained in:
trendschau
2021-09-28 12:56:29 +02:00
parent eb16fe52a4
commit f279afe888
27 changed files with 1027 additions and 269 deletions

5
cache/securitylog.txt vendored Normal file
View File

@@ -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

View File

@@ -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": {

218
composer.lock generated
View File

@@ -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",

1
settings/formdata.yaml Normal file
View File

@@ -0,0 +1 @@
''

View File

@@ -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 = '<a href="'. $url . '">' . $url . '</a>';
# 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,<br/><br/>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 . "<br/><br/>" . $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'));
}
}

View File

@@ -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]))
{

View File

@@ -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']))

View File

@@ -0,0 +1,44 @@
<?php
namespace Typemill\Extensions;
use Gregwar\Captcha\CaptchaBuilder;
class TwigCaptchaExtension extends \Twig_Extension
{
public function getFunctions()
{
return [
new \Twig_SimpleFunction('captcha', array($this, 'captchaImage' ))
];
}
public function captchaImage($initialize = false)
{
if(isset($_SESSION['captcha']) OR $initialize)
{
$builder = new CaptchaBuilder;
$builder->build();
$error = '';
if(isset($_SESSION['captcha']) && $_SESSION['captcha'] === 'error')
{
$error = '<span class="error">The captcha was wrong.</span>';
}
$_SESSION['phrase'] = $builder->getPhrase();
$_SESSION['captcha'] = true;
$template = '<div class="formElement">' .
'<label for="captcha">Captcha</label>' .
'<input type="text" name="captcha">' .
$error .
'<img class="captcha" src="' . $builder->inline() . '" />' .
'</div>';
return $template;
}
}
}

View File

@@ -0,0 +1,117 @@
<?php
namespace Typemill\Middleware;
use Slim\Interfaces\RouterInterface;
use Slim\Http\Request;
use Slim\Http\Response;
use Gregwar\Captcha\CaptchaBuilder;
class securityMiddleware
{
protected $router;
protected $settings;
protected $flash;
public function __construct(RouterInterface $router, $settings, $flash)
{
$this->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);
}
}

View File

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

View File

@@ -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';

View File

@@ -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 = '<html><body><table>';
$table .= '<tr><th>Breakpoint</th><th>Time</th><th>Duration</th></tr>';
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 .= '<tr>';
$table .= '<td>' . $breakpoint . '</td>';
$table .= '<td>' . $time . '</td>';
$table .= '<td>' . $duration . '</td>';
$table .= '</tr>';
$lastTime = $time;
$ip = $client;
}
$table .= '</table></body></html>';
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 = '<html><body><table>';
$table .= '<tr><th>Breakpoint</th><th>Time</th><th>Duration</th></tr>';
foreach($timer as $breakpoint => $time)
{
$duration = $time - $lastTime;
$table .= '<tr>';
$table .= '<td>' . $breakpoint . '</td>';
$table .= '<td>' . $time . '</td>';
$table .= '<td>' . $duration . '</td>';
$table .= '</tr>';
$lastTime = $time;
}
$table .= '</table></body></html>';
echo $table;
}
}

View File

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

View File

@@ -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);

View File

@@ -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;

View File

@@ -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
{

View File

@@ -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

View File

@@ -26,22 +26,34 @@
<span class="error">{{ errors.password | first }}</span>
{% endif %}
</div>
<div class="personal-mail">
<label>Personal Mail</label>
<input type="text" name="personal-honey-mail">
</div>
{{ captcha(old) }}
</fieldset>
<div class="loginarea" id="loginarea">
<input type="submit" value="{{ __('Login') }}" id="loginbutton" class="loginbutton" />
{{ csrf_field() | raw }}
{% if messages.time %}
<div id="counter" class="counter">{{ __('wait') }} <span id="wait">{{ messages.time }}</span> sec</div>
<div class="forgotpw"><a href="https://typemill.net/writers/forgot-password" target="_blank">{{ __('Forgot password') }}?</a></div>
{% endif %}
{{ csrf_field() | raw }}
</div>
</form>
</div>
<div class="setupContent">
<p><a href="{{ base_url() }}">{{ __('back to startpage') }}</a></p>
</div>
{% if settings.recoverpw %}
<p><span class="left"><a href="{{ base_url() }}">{{ __('back to startpage') }}</a></span><span class="right"><a href="{{ base_url() }}/tm/recoverpw">{{ __('forgot password') }}</a></span></p>
{% else %}
<div class="setupContent">
<p><a href="{{ base_url() }}">{{ __('back to startpage') }}</a></p>
</div>
{% endif %}
</div>
<footer></footer>
{% endblock %}

View File

@@ -0,0 +1,39 @@
{% extends 'layouts/layoutAuth.twig' %}
{% block title %}Login{% endblock %}
{% block content %}
<div class="setupWrapper">
<div class="authformWrapper">
<form method="POST" action="{{ path_for("auth.recoverpw") }}" autocomplete="off">
<fieldset class="auth">
<div class="formElement{{ errors.email ? ' errors' : '' }}">
<label for="email">{{ __('email') }} <abbr title="{{ __('required') }}">*</abbr></label>
<input type="text" name="email" value="{{ old.email }}" required>
{% if errors.email %}
<span class="error">{{ errors.email | first }}</span>
{% endif %}
</div>
<div class="personal-mail">
<label>Personal Mail</label>
<input type="text" name="personal-honey-mail">
</div>
{{ captcha(true) }}
</fieldset>
<div class="loginarea" id="loginarea">
<input type="submit" value="{{ __('Recover password') }}" id="loginbutton" class="loginbutton" />
{{ csrf_field() | raw }}
</div>
</form>
</div>
<div class="setupContent">
<p><a href="{{ base_url() }}/tm/login">{{ __('back to login') }}</a></p>
</div>
</div>
<footer></footer>
{% endblock %}

View File

@@ -0,0 +1,41 @@
{% extends 'layouts/layoutAuth.twig' %}
{% block title %}Login{% endblock %}
{% block content %}
<div class="setupWrapper">
<div class="authformWrapper">
<form method="POST" action="{{ path_for("auth.recoverpwnew") }}" autocomplete="off">
<fieldset class="auth">
<div class="formElement{{ errors.password ? ' errors' : '' }}">
<label for="password">{{ __('New password') }} <abbr title="{{ __('required') }}">*</abbr></label>
<input type="password" name="password" value="" required>
{% if errors.password %}
<span class="error">{{ errors.password | first }}</span>
{% endif %}
</div>
<div class="formElement{{ errors.passwordrepeat ? ' errors' : '' }}">
<label for="passwordrepeat">{{ __('Repeat password') }} <abbr title="{{ __('required') }}">*</abbr></label>
<input type="password" name="passwordrepeat" value="" required>
{% if errors.passwordrepeat %}
<span class="error">{{ errors.passwordrepeat | first }}</span>
{% endif %}
</div>
</fieldset>
<div class="loginarea" id="loginarea">
<input type="submit" value="{{ __('Create new password') }}" id="loginbutton" class="loginbutton" />
{{ csrf_field() | raw }}
<input type="hidden" name="recovertoken" value="{{recovertoken}}">
<input type="hidden" name="username" value="{{username}}">
</div>
</form>
</div>
<div class="setupContent">
<p><a href="{{ base_url() }}/tm/login">{{ __('back to login') }}</a></p>
</div>
</div>
<footer></footer>
{% endblock %}

View File

@@ -0,0 +1,19 @@
{% extends 'layouts/layoutAuth.twig' %}
{% block title %}Login{% endblock %}
{% block content %}
<div class="setupWrapper extrawidth">
<div class="authformWrapper">
<h1>{{ title }}</h1>
<p>{{ message }}</p>
</div>
<div class="setupContent">
<p><a href="{{ base_url() }}/tm/login">{{ __('back to login') }}</a></p>
</div>
</div>
<footer></footer>
{% endblock %}

View File

@@ -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;
}

View File

@@ -22,7 +22,7 @@ Vue.component('tab-meta', {
}
},
template: '<section><form>' +
'<div><div class="large relative">' +
'<div v-if="slug"><div class="large relative">' +
'<label>Slug / Name in URL</label><input type="text" v-model="slug" @input="changeSlug()"><button @click.prevent="storeSlug()" :disabled="disabled" class="button slugbutton bn br2 bg-tm-green white absolute">change slug</button>' +
'<div v-if="slugerror" class="f6 tm-red mt1">{{ slugerror }}</div>' +
'</div></div>' +
@@ -54,8 +54,11 @@ Vue.component('tab-meta', {
'</form></section>',
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)

View File

@@ -23,14 +23,21 @@
</head>
<body>
{% include 'partials/symbols.twig' %}
<svg style="position: absolute; width: 0; height: 0; overflow: hidden" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<symbol id="icon-bookmark-o" viewBox="0 0 20 28">
<title>bookmark-o</title>
<path d="M18 4h-16v19.406l8-7.672 1.391 1.328 6.609 6.344v-19.406zM18.188 2c0.234 0 0.469 0.047 0.688 0.141 0.688 0.266 1.125 0.906 1.125 1.609v20.141c0 0.703-0.438 1.344-1.125 1.609-0.219 0.094-0.453 0.125-0.688 0.125-0.484 0-0.938-0.172-1.297-0.5l-6.891-6.625-6.891 6.625c-0.359 0.328-0.812 0.516-1.297 0.516-0.234 0-0.469-0.047-0.688-0.141-0.688-0.266-1.125-0.906-1.125-1.609v-20.141c0-0.703 0.438-1.344 1.125-1.609 0.219-0.094 0.453-0.141 0.688-0.141h16.375z"></path>
</symbol>
</defs>
</svg>
{% include 'partials/flash.twig' %}
<div class="main">
{% block content %}{% endblock %}
</div>
<script src="{{ base_url }}/system/author/js/auth.js?20210821"></script>
</body>
</html>

View File

@@ -27,13 +27,30 @@
{% endif %}
{% endfor %}
<div class="personal-mail">
<label>Personal Mail</label>
<input type="text" name="personal-honey-mail">
</div>
{{ csrf_field() | raw }}
{% if captchaoptions == 'disabled' %}
{% elseif captchaoptions == 'aftererror' %}
{{ captcha(old) }}
{% else %}
{{ captcha(true) }}
{% endif %}
{% if recaptcha_webkey %}
<p><div class="g-recaptcha" data-sitekey="{{ recaptcha_webkey }}"></div></p>
{% endif %}
<input type="submit" value="{{ buttonlabel ? buttonlabel : 'send' }}" />
<style>.personal-mail{display:none}</style>

View File

@@ -74,7 +74,7 @@
</div>
<hr>
<header class="headline">
<h2>{{ __('General Presentation') }}</h2>
<h2>{{ __('Media') }}</h2>
</header>
<div class="medium{{ errors.settings.logo ? ' error' : '' }}">
<label for="settings[logo]">{{ __('Logo') }} <small>(jpg,jpeg,png,svg)</small></label>
@@ -104,10 +104,27 @@
{% if errors.settings.favicon %}
<span class="error">{{ errors.settings.favicon | first }}</span>
{% endif %}
</div><div class="medium{{ errors.settings.headlineanchors ? ' error' : '' }}">
<label for="settings[headlineanchors]">{{ __('Headline Anchors') }} *</label>
<label class="control-group">{{ __('Show anchors next to headlines') }}
<input name="settings[headlineanchors]" type="checkbox" {% if (settings.headlineanchors or old.settings.headlineanchors) %} checked {% endif %}>
</div>
<div class="medium{{ errors.settings.images.live.width ? ' error' : '' }}">
<label for="imagewidth">{{ __('Standard width for images') }}</label>
<input type="text" name="settings[images][live][width]" id="imagewidth" value="{{ old.settings.images.live.width ? old.settings.images.live.width : settings.images.live.width }}" title="{{ __('Use a valid number') }}" />
<div class="description">{{ __('This applies only for future images in the content area.') }}</div>
{% if errors.settings.images.live.width %}
<span class="error">{{ errors.settings.images.live.width | first }}</span>
{% endif %}
</div>
<div class="medium{{ errors.settings.images.live.height ? ' error' : '' }}">
<label for="imageheight">{{ __('Standard height for images') }}</label>
<input type="text" name="settings[images][live][height]" id="imageheight" value="{{ old.settings.images.live.height ? old.settings.images.live.height : settings.images.live.height }}" title="{{ __('Use a valid number') }}" />
<div class="description">{{ __('If you add a value for the height, then the image will be cropped.') }}</div>
{% if errors.settings.images.live.height %}
<span class="error">{{ errors.settings.images.live.height | first }}</span>
{% endif %}
</div>
<div class="large{{ errors.settings.svg ? ' error' : '' }}">
<label for="settings[svg]">{{ __('Upload svg images') }}</label>
<label class="control-group">{{ __('Allow upload of svg images (on your own risk, has security implications)') }}
<input name="settings[svg]" type="checkbox" {% if (settings.svg or old.settings.svg) %} checked {% endif %}>
<span class="checkmark"></span>
</label>
</div>
@@ -139,6 +156,18 @@
</label>
{% endfor %}
</div><div class="medium{{ errors.settings.headlineanchors ? ' error' : '' }}">
<label for="settings[headlineanchors]">{{ __('Headline Anchors') }} *</label>
<label class="control-group">{{ __('Show anchors next to headlines in frontend') }}
<input name="settings[headlineanchors]" type="checkbox" {% if (settings.headlineanchors or old.settings.headlineanchors) %} checked {% endif %}>
<span class="checkmark"></span>
</label>
</div><div class="large{{ errors.settings.urlschemes ? ' error' : '' }}">
<label for="urlschemes">{{ __('Add more url schemes for external links e.g. like dict:// (comma separated list)') }}</label>
<input type="text" name="settings[urlschemes]" id="urlschemes" placeholder="dict://" value="{{ old.settings.urlschemes ? old.settings.urlschemes : settings.urlschemes }}" title="{{ __('Comma separated list additional schemes') }}" />
{% if errors.settings.urlschemes %}
<span class="error">{{ errors.settings.urlschemes | first }}</span>
{% endif %}
</div>
<hr>
<header class="headline">
@@ -182,6 +211,38 @@
</label>
</div>
<hr>
<header class="headline">
<h2>{{ __('Password Recovery') }}</h2>
<p>{{ __('Activate the password recovery only if it is really needed. If possible use your password manager instead.') }}</p>
</header>
<div class="large{{ errors.settings.recoverpw ? ' error' : '' }}">
<label for="settings[recoverpw]">{{ __('Recover Passwords') }}</label>
<label class="control-group">{{ __('Activate the recover password function') }}
<input name="settings[recoverpw]" type="checkbox" {% if (settings.recoverpw or old.settings.recoverpw) %} checked {% endif %}>
<span class="checkmark"></span>
</label>
</div><div class="large{{ errors.settings.recoverfrom ? ' error' : '' }}">
<label for="recoverfrom">{{ __('Sender email') }}</label>
<input type="text" name="settings[recoverfrom]" id="recoverfrom" placeholder="your@email.com" value="{{ old.settings.recoverfrom ? old.settings.recoverfrom : settings.recoverfrom }}" />
{% if errors.settings.recoverfrom %}
<span class="error">{{ errors.settings.recoverfrom | first }}</span>
{% endif %}
</div>
<div class="large{{ errors.settings.recoversubject ? ' error' : '' }}">
<label for="recoversubject">{{ __('Subject') }}</label>
<input type="text" name="settings[recoversubject]" id="recoversubject" placeholder="Recover your password" value="{{ old.settings.recoversubject ? old.settings.recoversubject : settings.recoversubject }}" />
{% if errors.settings.recoversubject %}
<span class="error">{{ errors.settings.recoversubject | first }}</span>
{% endif %}
</div>
<div class="large{{ errors.settings.recovermessage ? ' error' : '' }}">
<label for="settings[recovermessage]">{{ __('Text before the recover link in the email message') }} <small>({{ __('use markdown') }})</small></label>
<textarea id="recovermessage" rows="8" name="settings[recovermessage]" placeholder="Dear user,&#10;&#10;please use the following link to create a new password. The link is valid for 24 hours: ">{{ old.settings.recovermessage ? old.settings.recovermessage : settings.recovermessage }}</textarea>
{% if errors.settings.recovermessage %}
<span class="error">{{ errors.settings.recovermessage | first }}</span>
{% endif %}
</div>
<hr>
<header class="headline">
<h2>{{ __('Developer') }}</h2>
<p>{{ __('The following options are only for developers') }}</p>
@@ -193,6 +254,13 @@
<span class="checkmark"></span>
</label>
</div>
<div class="large{{ errors.settings.securitylog ? ' error' : '' }}">
<label for="settings[securitylog]">{{ __('Security Log') }}</label>
<label class="control-group">{{ __('Create a logfile in the cache folder to track spam and suspicious actions') }}
<input name="settings[securitylog]" type="checkbox" {% if (settings.securitylog or old.settings.securitylog) %} checked {% endif %}>
<span class="checkmark"></span>
</label>
</div>
<div class="medium{{ errors.settings.twigcache ? ' error' : '' }}">
<label for="settings[twigcache]">{{ __('Twig Cache') }}</label>
<label class="control-group">{{ __('Activate Cache for Twig Templates') }}
@@ -204,29 +272,6 @@
<div class="label">{{ __('Recreate cached files') }}</div>
<button id="clearcache" class="link bg-tm-green white dim bn br1 ph3 pv2 f6">{{ __('Recreate Cache') }}</button><div id="cacheresult" class="dib ph3 pv2"></div>
</div>
<div class="medium{{ errors.settings.images.live.width ? ' error' : '' }}">
<label for="imagewidth">{{ __('Standard width for images') }}</label>
<input type="text" name="settings[images][live][width]" id="imagewidth" value="{{ old.settings.images.live.width ? old.settings.images.live.width : settings.images.live.width }}" title="{{ __('Use a valid number') }}" />
<div class="description">{{ __('This applies only for future images in the content area.') }}</div>
{% if errors.settings.images.live.width %}
<span class="error">{{ errors.settings.images.live.width | first }}</span>
{% endif %}
</div>
<div class="medium{{ errors.settings.images.live.height ? ' error' : '' }}">
<label for="imageheight">{{ __('Standard height for images') }}</label>
<input type="text" name="settings[images][live][height]" id="imageheight" value="{{ old.settings.images.live.height ? old.settings.images.live.height : settings.images.live.height }}" title="{{ __('Use a valid number') }}" />
<div class="description">{{ __('If you add a value for the height, then the image will be cropped.') }}</div>
{% if errors.settings.images.live.height %}
<span class="error">{{ errors.settings.images.live.height | first }}</span>
{% endif %}
</div>
<div class="large{{ errors.settings.svg ? ' error' : '' }}">
<label for="settings[svg]">{{ __('Upload svg images') }}</label>
<label class="control-group">{{ __('Allow upload of svg images (on your own risk, has security implications)') }}
<input name="settings[svg]" type="checkbox" {% if (settings.svg or old.settings.svg) %} checked {% endif %}>
<span class="checkmark"></span>
</label>
</div>
<div class="medium{{ errors.settings.proxy ? ' error' : '' }}">
<label for="settings[proxy]">{{ __('Proxy') }}</label>
<label class="control-group">{{ __('Use X-Forwarded Headers') }}
@@ -248,13 +293,6 @@
<span class="checkmark"></span>
</label>
</div>
<div class="large{{ errors.settings.urlschemes ? ' error' : '' }}">
<label for="urlschemes">{{ __('Add more url schemes for external links e.g. like dict:// (comma separated list)') }}</label>
<input type="text" name="settings[urlschemes]" id="urlschemes" value="{{ old.settings.urlschemes ? old.settings.urlschemes : settings.urlschemes }}" title="{{ __('Comma separated list additional schemes') }}" />
{% if errors.settings.urlschemes %}
<span class="error">{{ errors.settings.urlschemes | first }}</span>
{% endif %}
</div>
</fieldset>
</section>
<input type="submit" value="{{ __('Save All Settings') }}" />

View File

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