From 997863826671f1e6ca4df2359beb97f8e0c9fd72 Mon Sep 17 00:00:00 2001 From: trendschau Date: Tue, 26 Dec 2023 04:39:17 +0100 Subject: [PATCH] Add auth code feature, add simpleMail model and refactor system settings --- data/security/securitylog.txt | 10 + .../Controllers/ControllerWebAuth.php | 342 +++++++++++++++--- .../Controllers/ControllerWebRecover.php | 21 +- .../Controllers/ControllerWebSetup.php | 6 +- system/typemill/Models/Settings.php | 14 +- system/typemill/Models/SimpleMail.php | 63 ++++ system/typemill/Models/Validation.php | 26 ++ system/typemill/Plugin.php | 17 - system/typemill/Static/Helpers.php | 1 - system/typemill/author/auth/authcode.twig | 103 ++++++ system/typemill/routes/web.php | 6 + system/typemill/settings/metatabs.yaml | 1 + system/typemill/settings/system.yaml | 146 +++++--- 13 files changed, 616 insertions(+), 140 deletions(-) create mode 100644 data/security/securitylog.txt create mode 100644 system/typemill/Models/SimpleMail.php create mode 100644 system/typemill/author/auth/authcode.twig diff --git a/data/security/securitylog.txt b/data/security/securitylog.txt new file mode 100644 index 0000000..d146afb --- /dev/null +++ b/data/security/securitylog.txt @@ -0,0 +1,10 @@ +127.0.0.1;2023-12-24 10:46:32;login: invalid data +127.0.0.1;2023-12-24 10:47:01;login: invalid data +127.0.0.1;2023-12-24 10:51:05;login: invalid data +127.0.0.1;2023-12-24 10:57:31;login: authcode wrong or outdated. +127.0.0.1;2023-12-24 10:59:47;login: authcode wrong or outdated. +127.0.0.1;2023-12-24 10:59:51;login: wrong password +127.0.0.1;2023-12-24 10:59:59;login: authcode wrong or outdated. +127.0.0.1;2023-12-25 06:20:18;login: authcode wrong or outdated. +127.0.0.1;2023-12-25 06:20:35;login: user not found +127.0.0.1;2023-12-25 09:12:05;login: wrong password diff --git a/system/typemill/Controllers/ControllerWebAuth.php b/system/typemill/Controllers/ControllerWebAuth.php index 07af29a..6727498 100644 --- a/system/typemill/Controllers/ControllerWebAuth.php +++ b/system/typemill/Controllers/ControllerWebAuth.php @@ -7,6 +7,7 @@ use Psr\Http\Message\ResponseInterface as Response; use Slim\Routing\RouteContext; use Typemill\Models\Validation; use Typemill\Models\User; +use Typemill\Models\SimpleMail; use Typemill\Static\Translations; class ControllerWebAuth extends Controller @@ -21,68 +22,215 @@ class ControllerWebAuth extends Controller public function login(Request $request, Response $response) { - /* - 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->withHeader('Location', $this->routeParser->urlFor('auth.show')); - } - */ - $input = $request->getParsedBody(); $validation = new Validation(); -# $settings = $this->c->get('settings'); + $securitylog = $this->settings['securitylog'] ?? false; + $authcodeactive = $this->settings['authcode'] ?? false; - if($validation->signin($input) === true) + if($validation->signin($input) !== true) { - $user = new User(); - - if(!$user->setUserWithPassword($input['username'])) + if($securitylog) { - $this->c->get('flash')->addMessage('error', Translations::translate('Wrong password or username, please try again.')); - - return $response->withHeader('Location', $this->routeParser->urlFor('auth.show'))->withStatus(302); + \Typemill\Static\Helpers::addLogEntry('login: invalid data'); } - $userdata = $user->getUserData(); + $this->c->get('flash')->addMessage('error', Translations::translate('Wrong password or username, please try again.')); - if($userdata && password_verify($input['password'], $userdata['password'])) - { - # check if user has confirmed the account - if(isset($userdata['optintoken']) && $userdata['optintoken']) - { - $this->c->get('flash')->addMessage('error', Translations::translate('Your registration is not confirmed yet. Please check your e-mails and use the confirmation link.')); - return $response->withHeader('Location', $this->routeParser->urlFor('auth.show'))->withStatus(302); - } - - $user->login(); - -# return $response->withHeader('Location', $this->routeParser->urlFor('settings.show'))->withStatus(302); - - # if user is allowed to view content-area - $acl = $this->c->get('acl'); - if($acl->hasRole($userdata['userrole']) && $acl->isAllowed($userdata['userrole'], 'content', 'view')) - { - $editor = (isset($this->settings['editor']) && $this->settings['editor'] == 'visual') ? 'visual' : 'raw'; - - return $response->withHeader('Location', $this->routeParser->urlFor('content.' . $editor))->withStatus(302); - } - - return $response->withHeader('Location', $this->routeParser->urlFor('user.account'))->withStatus(302); - } + return $response->withHeader('Location', $this->routeParser->urlFor('auth.show'))->withStatus(302); } - if(isset($this->settings['securitylog']) && $this->settings['securitylog']) + $user = new User(); + + if(!$user->setUserWithPassword($input['username'])) { - \Typemill\Static\Helpers::addLogEntry('wrong login'); + if($securitylog) + { + \Typemill\Static\Helpers::addLogEntry('login: user not found'); + } + + $this->c->get('flash')->addMessage('error', Translations::translate('Wrong password or username, please try again.')); + + return $response->withHeader('Location', $this->routeParser->urlFor('auth.show'))->withStatus(302); } - $this->c->get('flash')->addMessage('error', Translations::translate('Wrong password or username, please try again.')); + $userdata = $user->getUserData(); + $authcodedata = $this->checkAuthcode($userdata); - return $response->withHeader('Location', $this->routeParser->urlFor('auth.show'))->withStatus(302); + if($userdata && !password_verify($input['password'], $userdata['password'])) + { + if($securitylog) + { + \Typemill\Static\Helpers::addLogEntry('login: wrong password'); + } + + # always show authcode page, so attacker does not know if email or password was wrong or mail was send. + if($authcodeactive && !$authcodedata['valid']) + { + # a bit slower because send mail takes some time usually + usleep(rand(100000, 200000)); + + # show authcode page + return $this->c->get('view')->render($response, 'auth/authcode.twig', [ + 'username' => $userdata['username'], + ]); + } + + $this->c->get('flash')->addMessage('error', Translations::translate('Wrong password or username, please try again.')); + + return $response->withHeader('Location', $this->routeParser->urlFor('auth.show'))->withStatus(302); + } + + # check device fingerprint + if($authcodeactive) + { + $fingerprint = $this->generateDeviceFingerprint(); + if(!$this->findDeviceFingerprint($fingerprint, $userdata)) + { + # invalidate authcodedata so user has to use a new authcode again + $authcodedata['valid'] = false; + $authcodedata['validated'] = 12345; + } + } + + if($authcodeactive && !$authcodedata['valid'] ) + { + # generate new authcode + $authcodevalue = rand(10000, 99999); + + $mail = new SimpleMail($settings); + + $subject = Translations::translate('Your authentication code for Typemill'); + $message = Translations::translate('Use the following authentication code to login into Typemill cms') . ': ' . $authcodevalue; + + $send = $mail->send($userdata['email'], $subject, $message); + + $send = true; + + if(!$send) + { + $title = Translations::translate('Error sending email'); + $message = Translations::translate('Dear ') . $userdata['username'] . ', ' . Translations::translate('we could not send the email with the authentication code to your address. Reason: ') . $mail->error; + } + else + { + # store authcode + $user->setValue('authcodedata', $authcodevalue . ':' . time() . ':' . $authcodedata['validated']); + $user->updateUser(); + } + + # show authcode page + return $this->c->get('view')->render($response, 'auth/authcode.twig', [ + 'username' => $userdata['username'], + ]); + } + + # check if user has confirmed the account + if(isset($userdata['optintoken']) && $userdata['optintoken']) + { + if($securitylog) + { + \Typemill\Static\Helpers::addLogEntry('login: user not confirmed yet.'); + } + + $this->c->get('flash')->addMessage('error', Translations::translate('Your registration is not confirmed yet. Please check your e-mails and use the confirmation link.')); + + return $response->withHeader('Location', $this->routeParser->urlFor('auth.show'))->withStatus(302); + } + + $user->login(); + +# return $response->withHeader('Location', $this->routeParser->urlFor('settings.show'))->withStatus(302); + + # if user is allowed to view content-area + $acl = $this->c->get('acl'); + if($acl->hasRole($userdata['userrole']) && $acl->isAllowed($userdata['userrole'], 'content', 'view')) + { + $editor = (isset($this->settings['editor']) && $this->settings['editor'] == 'visual') ? 'visual' : 'raw'; + + return $response->withHeader('Location', $this->routeParser->urlFor('content.' . $editor))->withStatus(302); + } + + return $response->withHeader('Location', $this->routeParser->urlFor('user.account'))->withStatus(302); } + + # login user with valid authcode + public function loginWithAuthcode(Request $request, Response $response) + { + $input = $request->getParsedBody(); + $validation = new Validation(); + $securitylog = $this->settings['securitylog'] ?? false; + + if($validation->authcode($input) !== true) + { + if($securitylog) + { + \Typemill\Static\Helpers::addLogEntry('login: invalid authcode format'); + } + + $this->c->get('flash')->addMessage('error', Translations::translate('Invalid authcode format, please try again.')); + + return $response->withHeader('Location', $this->routeParser->urlFor('auth.show'))->withStatus(302); + } + + $user = new User(); + + if(!$user->setUserWithPassword($input['username'])) + { + if($securitylog) + { + \Typemill\Static\Helpers::addLogEntry('login: user not found'); + } + + $this->c->get('flash')->addMessage('error', Translations::translate('Wrong password or username, please try again.')); + + return $response->withHeader('Location', $this->routeParser->urlFor('auth.show'))->withStatus(302); + } + + $userdata = $user->getUserData(); + $authcodevalue = $input['code-1'] . $input['code-2'] . $input['code-3'] . $input['code-4'] . $input['code-5']; + $authcodedata = $this->checkAuthcode($userdata); + + if(!$this->validateAuthcode($authcodevalue, $authcodedata)) + { + if($securitylog) + { + \Typemill\Static\Helpers::addLogEntry('login: authcode wrong or outdated.'); + } + + $this->c->get('flash')->addMessage('error', Translations::translate('The authcode was wrong or outdated, please start again.')); + + return $response->withHeader('Location', $this->routeParser->urlFor('auth.show'))->withStatus(302); + } + + # add the device fingerprint if not set yet + $fingerprints = $userdata['fingerprints'] ?? []; + $fingerprint = $this->generateDeviceFingerprint(); + if(!$this->findDeviceFingerprint($fingerprint, $fingerprints)) + { + $fingerprints[] = $fingerprint; + $user->setValue('fingerprints', $fingerprints); + } + + # update authcode lastValidation and store + $user->setValue('authcodedata', $authcodevalue . ':' . $authcodedata['generated'] . ':' . time()); + $user->updateUser(); + + $user->login(); + + # if user is allowed to view content-area + $acl = $this->c->get('acl'); + if($acl->hasRole($userdata['userrole']) && $acl->isAllowed($userdata['userrole'], 'content', 'view')) + { + $editor = (isset($this->settings['editor']) && $this->settings['editor'] == 'visual') ? 'visual' : 'raw'; + + return $response->withHeader('Location', $this->routeParser->urlFor('content.' . $editor))->withStatus(302); + } + + return $response->withHeader('Location', $this->routeParser->urlFor('user.account'))->withStatus(302); + } + + /** * log out a user * @@ -97,4 +245,106 @@ class ControllerWebAuth extends Controller return $response->withHeader('Location', $this->routeParser->urlFor('auth.show'))->withStatus(302); } + + + # check if the stored authcode in userdata is valid and/or fresh + private function checkAuthcode($userdata) + { + # format: 12345:time(generated):time(validated) + + $authcodedata = $userdata['authcodedata'] ?? false; + + if(!$authcodedata) + { + return $authcode = [ + 'value' => 12345, + 'generated' => 12345, + 'validated' => 12345, + 'valid' => false, + 'fresh' => false + ]; + } + + $validation = new Validation(); + $authcodedata = explode(":", $authcodedata); + + # validate format here, do we need it? + + $now = time(); + $lastValidation = 60 * 60 * 24; + $lastGeneration = 60 * 5; + $valid = false; + $fresh = false; + + # if last validation is less than 24 hours old + if($now - $lastValidation < $authcodedata[2]) + { + $valid = true; + } + + # if last generation is less than 5 minutes old + if($now - $lastGeneration < $authcodedata[1]) + { + $fresh = true; + } + + $authcode = [ + 'value' => $authcodedata[0], + 'generated' => $authcodedata[1], + 'validated' => $authcodedata[2], + 'valid' => $valid, + 'fresh' => $fresh + ]; + + return $authcode; + } + + # check if the submitted authcode is the same as the stored authcode + private function validateAuthcode($authcodevalue, $authcodedata) + { + if($authcodedata['valid'] === true) + { + return true; + } + + if($authcodedata['fresh'] === false) + { + return false; + } + + if($authcodevalue == $authcodedata['value']) + { + return true; + } + + return false; + } + + # create a simple device fingerprint + private function generateDeviceFingerprint() + { + $userAgent = $_SERVER['HTTP_USER_AGENT']; + $ipAddress = $_SERVER['REMOTE_ADDR']; + $acceptLanguage = isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? $_SERVER['HTTP_ACCEPT_LANGUAGE'] : ''; + + $fingerprint = md5($userAgent . $ipAddress . $acceptLanguage); + + return $fingerprint; + } + + # create a simple device fingerprint + private function findDeviceFingerprint($fingerprint, $userdata) + { + if(!isset($userdata['fingerprints']) or empty($userdata['fingerprints'])) + { + return false; + } + + if(!in_array($fingerprint, $userdata['fingerprints'])) + { + return false; + } + + return true; + } } \ No newline at end of file diff --git a/system/typemill/Controllers/ControllerWebRecover.php b/system/typemill/Controllers/ControllerWebRecover.php index 905d068..8a52c4e 100644 --- a/system/typemill/Controllers/ControllerWebRecover.php +++ b/system/typemill/Controllers/ControllerWebRecover.php @@ -7,6 +7,7 @@ use Psr\Http\Message\ResponseInterface as Response; use Slim\Routing\RouteContext; use Typemill\Models\User; use Typemill\Models\Validation; +use Typemill\Models\SimpleMail; use Typemill\Static\Translations; use Typemill\Extensions\ParsedownExtension; @@ -50,15 +51,9 @@ class ControllerWebRecover extends Controller $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']; - } + $mail = new SimpleMail($settings); - $subjectline = (isset($settings['recoversubject']) && ($settings['recoversubject'] != '') ) ? $settings['recoversubject'] : 'Recover your password'; - $subject = '=?UTF-8?B?' . base64_encode($subjectline) . '?='; + $subject = (isset($settings['recoversubject']) && ($settings['recoversubject'] != '') ) ? $settings['recoversubject'] : 'Recover your password'; $messagetext = Translations::translate('Dear user'); $messagetext .= ",

"; @@ -72,16 +67,14 @@ class ControllerWebRecover extends Controller $messagetext = $parsedown->markup($contentArray); } - $message = base64_encode($messagetext . "

" . $link); + $message = $messagetext . "

" . $link; - # $send = mail($requiredUser['email'], $subject, $message, $headers); + $send = $mail->send($requiredUser['email'], $subject, $message); - $send = false; - - if($send == 'delete') + if(!$send) { $title = Translations::translate('Error sending email'); - $message = Translations::translate('Dear ') . $requiredUser['username'] . ', ' . Translations::translate('we could not send the email with the password instructions to your address. Please contact the website owner and ask for help.'); + $message = Translations::translate('Dear ') . $requiredUser['username'] . ', ' . Translations::translate('we could not send the email with the password instructions to your address. Reason: ') . $mail->error; } else { diff --git a/system/typemill/Controllers/ControllerWebSetup.php b/system/typemill/Controllers/ControllerWebSetup.php index f9cfa4a..f90e2b3 100644 --- a/system/typemill/Controllers/ControllerWebSetup.php +++ b/system/typemill/Controllers/ControllerWebSetup.php @@ -95,7 +95,11 @@ class ControllerWebSetup extends Controller # create initial settings file $settingsModel = new Settings(); - $settingsModel->createSettings(); + $settingsModel->createSettings([ + 'author' => $params['username'], + 'mailfrom' => $params['email'], + 'mailfromname' => $params['username'] + ]); $urlinfo = $this->c->get('urlinfo'); $route = $urlinfo['baseurl'] . '/tm/system'; diff --git a/system/typemill/Models/Settings.php b/system/typemill/Models/Settings.php index 8fdab5d..2f7bac7 100644 --- a/system/typemill/Models/Settings.php +++ b/system/typemill/Models/Settings.php @@ -224,18 +224,26 @@ class Settings return $settingsDefinitions; } - public function createSettings() + public function createSettings(array $defaultSettings = NULL) { - $language = Translations::whichLanguage(); + $defaults = [ + 'language' => Translations::whichLanguage() + ]; + + if($defaultSettings) + { + $defaults = array_merge($defaults, $defaultSettings); + } $initialSettings = $this->storage->updateYaml('settingsFolder', '', 'settings.yaml', [ - 'language' => $language + $defaults ]); if($initialSettings) { return true; } + return false; } diff --git a/system/typemill/Models/SimpleMail.php b/system/typemill/Models/SimpleMail.php new file mode 100644 index 0000000..d86bb36 --- /dev/null +++ b/system/typemill/Models/SimpleMail.php @@ -0,0 +1,63 @@ +from = trim($settings['mailfrom']); + + if(isset($settings['mailfromname']) && $settings['mailfromname'] != '') + { + $this->from = '=?UTF-8?B?' . base64_encode($settings['mailfromname']) . '?= <' . trim($settings['mailfrom']) . '>'; + } + } + + if(isset($settings['mailreply']) && $settings['mailreply'] != '') + { + $this->reply = trim($settings['mailreply']); + } + } + + public function sendEmail(string $to, string $subject, string $message) + { + if(!$this->from) + { + $this->error = 'You need to add a email address into the settings.'; + + return false; + } + + # 'Reply-To: webmaster@example.com' . "\r\n" . + + $headers = 'Content-Type: text/html; charset=utf-8' . "\r\n"; + $headers .= 'Content-Transfer-Encoding: base64' . "\r\n"; + $headers .= 'From: ' . $this->from . "\r\n"; + if($this->$reply) + { + $headers .= 'Reply-To: base64' . $this->reply . "\r\n"; + } + $headers .= 'X-Mailer: PHP/' . phpversion(); + + $subject = '=?UTF-8?B?' . base64_encode($subject) . '?='; + $message = base64_encode($message); + + $send = mail($to, $subject, $message, $headers); + + if($send !== true) + { + $this->error = error_get_last()['message']; + } + + return $send; + } +} \ No newline at end of file diff --git a/system/typemill/Models/Validation.php b/system/typemill/Models/Validation.php index 5cd7d94..e1be426 100644 --- a/system/typemill/Models/Validation.php +++ b/system/typemill/Models/Validation.php @@ -274,6 +274,32 @@ class Validation return false; } + /** + * validation for authcode confirmation + * + * @param array $params with form data. + * @return obj $v the validation object passed to a result method. + */ + + public function authcode(array $params) + { + $v = new Validator($params); + $v->rule('required', ['username', 'code-1', 'code-2', 'code-3', 'code-4', 'code-5'])->message("Required"); + $v->rule('alphaNum', 'username')->message("Invalid characters"); + $v->rule('regex', 'code-1', '/^[0-9]{1}$/')->message("Must be 1-9"); + $v->rule('regex', 'code-2', '/^[0-9]{1}$/')->message("Must be 1-9"); + $v->rule('regex', 'code-3', '/^[0-9]{1}$/')->message("Must be 1-9"); + $v->rule('regex', 'code-4', '/^[0-9]{1}$/')->message("Must be 1-9"); + $v->rule('regex', 'code-5', '/^[0-9]{1}$/')->message("Must be 1-9"); + + if($v->validate()) + { + return true; + } + + return false; + } + /** * validation for setup user (in backoffice) diff --git a/system/typemill/Plugin.php b/system/typemill/Plugin.php index 68421ca..6102586 100644 --- a/system/typemill/Plugin.php +++ b/system/typemill/Plugin.php @@ -194,23 +194,6 @@ abstract class Plugin implements EventSubscriberInterface $this->container->get('assets')->addJS($JS); } -/* - protected function addEditorJS($JS) - { - $this->container->get('assets')->addEditorJS($JS); - } - - protected function addEditorInlineJS($JS) - { - $this->container->get('assets')->addEditorInlineJS($JS); - } - - protected function addEditorCSS($CSS) - { - $this->container->get('assets')->addEditorCSS($CSS); - } -*/ - protected function addInlineJS($JS) { $this->container->get('assets')->addInlineJS($JS); diff --git a/system/typemill/Static/Helpers.php b/system/typemill/Static/Helpers.php index f3c443d..4adb2c4 100644 --- a/system/typemill/Static/Helpers.php +++ b/system/typemill/Static/Helpers.php @@ -51,7 +51,6 @@ class Helpers{ return $ip; } - public static function addLogEntry($action) { $line = self::getUserIP(); diff --git a/system/typemill/author/auth/authcode.twig b/system/typemill/author/auth/authcode.twig new file mode 100644 index 0000000..44372ec --- /dev/null +++ b/system/typemill/author/auth/authcode.twig @@ -0,0 +1,103 @@ +{% extends 'layouts/layoutAuth.twig' %} + +{% block title %}Login Authentication Code{% endblock %} + +{% block content %} + +
+ +
+
+ +

Authentication Code

+ +

Enter the auth code from the e-mail you got: + +

+ +
+ +
+ + + + + +
+ + + + + + + +
+ +
+
+
+ +
+
+

{{ translate('Auth code missing?') }}

+

{{ translate('If you did not receive an email with an authentication code, then the username or password you entered was wrong. Please try again.') }}

+
+
+ +
+{% endblock %} \ No newline at end of file diff --git a/system/typemill/routes/web.php b/system/typemill/routes/web.php index 18e1c89..4024b0f 100644 --- a/system/typemill/routes/web.php +++ b/system/typemill/routes/web.php @@ -18,6 +18,11 @@ $app->group('/tm', function (RouteCollectorProxy $group) use ($settings) { $group->get('/login', ControllerWebAuth::class . ':show')->setName('auth.show'); $group->post('/login', ControllerWebAuth::class . ':login')->setName('auth.login'); + if(isset($settings['authcode']) && $settings['authcode']) + { + $group->post('/authcode', ControllerWebAuth::class . ':loginWithAuthcode')->setName('auth.authcode'); + } + if(isset($settings['recoverpw']) && $settings['recoverpw']) { $group->get('/recover', ControllerWebRecover::class . ':showRecoverForm')->setName('auth.recoverform'); @@ -50,6 +55,7 @@ $app->group('/tm', function (RouteCollectorProxy $group) use ($routeParser,$acl) $app->redirect('/tm', $routeParser->urlFor('auth.show'), 302); $app->redirect('/tm/', $routeParser->urlFor('auth.show'), 302); +$app->redirect('/tm/authcode', $routeParser->urlFor('auth.show'), 302); # downloads $app->get('/media/files[/{params:.*}]', ControllerWebDownload::class . ':download')->setName('download.file'); diff --git a/system/typemill/settings/metatabs.yaml b/system/typemill/settings/metatabs.yaml index 32f6eea..722bc48 100644 --- a/system/typemill/settings/metatabs.yaml +++ b/system/typemill/settings/metatabs.yaml @@ -85,6 +85,7 @@ meta: reference: type: text label: Reference to page + placeholder: '/path/to/internal/page or https://exgernal-page.org' maxlength: 200 referencetype: type: radio diff --git a/system/typemill/settings/system.yaml b/system/typemill/settings/system.yaml index 81a3e5b..739222a 100644 --- a/system/typemill/settings/system.yaml +++ b/system/typemill/settings/system.yaml @@ -4,19 +4,20 @@ fieldsetsystem: fields: title: type: text - label: Website title + label: 'Website title' maxlength: 60 css: lg:w-half author: type: text - label: Website owner + label: 'Website owner' css: lg:w-half maxlength: 60 copyright: type: select - label: Copyright + label: 'Copyright' css: lg:w-half maxlength: 60 + description: 'Used for copyright and year in footer.' options: '©': '©' 'CC-BY': 'CC-BY' @@ -30,11 +31,13 @@ fieldsetsystem: label: Year css: lg:w-half maxlength: 4 + description: 'Used for copyright and year in footer.' language: type: select - label: Language (author area) + label: 'Language (author area)' css: lg:w-half maxlength: 60 + description: 'Used for translations in author area, themes, and plugins.' options: 'en': 'English' 'ru': 'Russian' @@ -44,15 +47,16 @@ fieldsetsystem: 'fr': 'French' langattr: type: text - label: Language attribute (website) + label: 'Language attribute (website)' css: lg:w-half maxlength: 5 - description: Please use ISO 639-1 codes like "en" + description: 'Used for frontend language attribute. Please use ISO 639-1 codes like "en".' sitemap: type: text - label: Google sitemap (readonly) + label: 'Google sitemap (readonly)' css: lg:w-half disabled: true + description: 'Submit the url above in google search console to support indexing.' fieldsetmedia: type: fieldset legend: Media @@ -63,48 +67,48 @@ fieldsetmedia: favicon: type: image label: Favicon - description: Only PNG format will work. + description: 'Only PNG format will work.' liveimagewidth: type: number - label: Standard width for live pictures + label: 'Standard width for live pictures' placeholder: 820 - description: Default width of live images is 820px. Changes will apply to future uploads. + description: 'Default width of live images is 820px. Changes will apply to future uploads.' css: lg:w-half liveimageheight: type: number - label: Standard height for live pictures - description: If you add a value for the height, then the image will be cropped. + label: 'Standard height for live pictures' + description: 'If you add a value for the height, then the image will be cropped.' css: lg:w-half maximageuploads: type: number - label: Maximum size for image uploads in MB - description: The maximum image size might be limited by your server settings. + label: 'Maximum size for image uploads in MB' + description: 'The maximum image size might be limited by your server settings.' allowsvg: type: checkbox label: Allow svg - checkboxlabel: Allow the upload of svg images + checkboxlabel: 'Allow the upload of svg images' convertwebp: type: checkbox - label: Convert to webp - checkboxlabel: Try to convert uploaded images into the webp-format + label: 'Convert to webp' + checkboxlabel: 'Try to convert uploaded images into the webp-format for better performance.' maxfileuploads: type: number - label: Maximum size for file uploads in MB - description: The maximum file size might be limited by your server settings. + label: 'Maximum size for file uploads in MB' + description: 'The maximum file size might be limited by your server settings.' fieldsetwriting: type: fieldset legend: Writing fields: editor: type: radio - label: Standard editor mode + label: 'Standard editor mode' css: lg:w-half options: 'visual': 'visual editor' 'raw': 'raw editor' formats: type: checkboxlist - label: Format options for visual editor + label: 'Format options for visual editor' css: lg:w-half options: 'markdown': 'markdown' @@ -124,12 +128,12 @@ fieldsetwriting: 'shortcode': 'shortcode' headlineanchors: type: checkbox - label: Headline anchors - checkboxlabel: Show anchors next to headline in frontend + label: 'Headline anchors' + checkboxlabel: 'Show anchors next to headline in frontend' urlschemes: type: text - label: Url schemes - description: Add more url schemes for external links e.g. like dict:// (comma separated list) + label: 'Url schemes' + description: 'Add more url schemes for external links e.g. like dict:// (comma separated list)' maxlength: 60 fieldsetaccess: type: fieldset @@ -137,57 +141,83 @@ fieldsetaccess: fields: access: type: checkbox - label: Website restriction - checkboxlabel: Show the website only to authenticated users and redirect all other users to the login page. + label: 'Website restriction' + checkboxlabel: 'Show the website only to authenticated users and redirect all other users to the login page.' pageaccess: type: checkbox - label: Page restriction - checkboxlabel: Activate individual restrictions for pages in the meta-tab of each page. + label: 'Page restriction' + checkboxlabel: 'Activate individual restrictions for pages in the meta-tab of each page.' hrdelimiter: type: checkbox - label: Content break - checkboxlabel: Cut restricted content after the first hr-element on a page (per default content will be cut after title). + label: 'Content break' + checkboxlabel: 'Cut restricted content after the first hr-element on a page (per default content will be cut after title).' restrictionnotice: type: textarea - label: Restriction notice (use markdown) + label: 'Restriction notice (use markdown)' maxlength: 2000 wraprestrictionnotice: type: checkbox - label: Wrap restriction notice - checkboxlabel: Wrap the restriction notice above into a notice-4 element (which can be designed as special box) + label: 'Wrap restriction notice' + checkboxlabel: 'Wrap the restriction notice above into a notice-4 element (which can be designed as special box)' +fieldsetmail: + type: fieldset + legend: Email + fields: + mailfrom: + type: text + label: 'Mail From (required)' + placeholder: sender@yourmail.org + maxlength: 60 + description: 'Enter an email address that should send emails (sender).' + mailfromname: + type: text + label: 'Mail From Name (optional)' + placeholder: sender name + maxlength: 60 + description: 'Optionally enter a name for the sender address. If not set, the from-address will be visible.' + replyto: + type: text + label: 'Reply To (optional)' + placeholder: noreply@yourmail.org + maxlength: 60 + description: 'Optionally enter a "reply to" address for answers from the receiver. If not set, answers will go to the from-address.' +fieldsetrecover: + type: fieldset + legend: Recover + fields: + recoverpw: + type: checkbox + label: 'Recover password' + checkboxlabel: 'Activate a password recovery in the login form.' + recoversubject: + type: text + label: 'Email subject' + placeholder: 'Recover your password' + maxlength: 60 + recovermessage: + type: textarea + label: 'Text before recover link in email message' + maxlength: 2000 fieldsetsecurity: type: fieldset legend: Security fields: securitylog: type: checkbox - label: Security log - checkboxlabel: Track spam and suspicious actions in a logfile + label: 'Security log' + checkboxlabel: 'Track spam and suspicious actions in a logfile' + authcode: + type: checkbox + label: 'Authentication code (recommended)' + checkboxlabel: 'Send a 5-digit authentication code by email to confirm login.' + description: 'The authentication code will be valid for 5 minutes. Be aware that device fingerprints will be stored in the user accounts. Make sure this complies with privacy legislation in your country.' authcaptcha: type: radio - label: Use captcha in authentication forms + label: 'Use captcha in authentication forms' options: - disabled: Disable - standard: Always show - aftererror: Show after first wrong input - recoverpw: - type: checkbox - label: Recover password - checkboxlabel: Activate the password recovery. - recoverfrom: - type: text - label: Sender email - placeholder: your@email.org - maxlength: 60 - recoversubject: - type: text - label: Email subject - placeholder: Recover your password - maxlength: 60 - recovermessage: - type: textarea - label: Text before recover link in email message - maxlength: 2000 + disabled: 'Disable' + standard: 'Always show' + aftererror: 'Show after first wrong input' fieldsetdeveloper: type: fieldset legend: Developer