diff --git a/.gitignore b/.gitignore
index 51f80f8..bb3ebe2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,10 +1,11 @@
cache/sitemap.xml
content/index.yaml
content/00-welcome/index.yaml
-content/00-welcome/00-setup.yaml
+content/00-welcome/00-setup-your-website.yaml
content/00-welcome/01-write-content.yaml
-content/00-welcome/02-get-help.yaml
-content/00-welcome/03-markdown-test.yaml
+content/00-welcome/02-manage-access.yaml
+content/00-welcome/03-get-help.yaml
+content/00-welcome/04-markdown-test.yaml
content/01-cyanine-theme/index.yaml
content/01-cyanine-theme/00-landingpage.yaml
content/01-cyanine-theme/01-colors-and-fonts.yaml
diff --git a/composer.lock b/composer.lock
index f970e95..e7f012d 100644
--- a/composer.lock
+++ b/composer.lock
@@ -1582,16 +1582,16 @@
},
{
"name": "symfony/event-dispatcher",
- "version": "v5.4.26",
+ "version": "v5.4.34",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher.git",
- "reference": "5dcc00e03413f05c1e7900090927bb7247cb0aac"
+ "reference": "e3bca343efeb613f843c254e7718ef17c9bdf7a3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/5dcc00e03413f05c1e7900090927bb7247cb0aac",
- "reference": "5dcc00e03413f05c1e7900090927bb7247cb0aac",
+ "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/e3bca343efeb613f843c254e7718ef17c9bdf7a3",
+ "reference": "e3bca343efeb613f843c254e7718ef17c9bdf7a3",
"shasum": ""
},
"require": {
@@ -1647,7 +1647,7 @@
"description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/event-dispatcher/tree/v5.4.26"
+ "source": "https://github.com/symfony/event-dispatcher/tree/v5.4.34"
},
"funding": [
{
@@ -1663,7 +1663,7 @@
"type": "tidelift"
}
],
- "time": "2023-07-06T06:34:20+00:00"
+ "time": "2023-12-27T21:12:56+00:00"
},
{
"name": "symfony/event-dispatcher-contracts",
diff --git a/system/typemill/Controllers/ControllerApiFile.php b/system/typemill/Controllers/ControllerApiFile.php
index f2c1f34..7a64121 100644
--- a/system/typemill/Controllers/ControllerApiFile.php
+++ b/system/typemill/Controllers/ControllerApiFile.php
@@ -221,17 +221,19 @@ class ControllerApiFile extends Controller
# if the previous check of the mtype with the base64 string failed, then do it now again with the temporary file
if(!$mtype)
{
- $fullPath = $this->settings['rootPath'] . $filePath;
+ $filePath = str_replace('media/files', 'media/tmp', $fileinfo['url']);
+ $filePath = str_replace('/', DIRECTORY_SEPARATOR, $filePath);
+ $fullPath = $this->settings['rootPath'] . DIRECTORY_SEPARATOR . $filePath;
$finfo = finfo_open( FILEINFO_MIME_TYPE );
$mtype = @finfo_file( $finfo, $fullPath );
finfo_close($finfo);
if(!$mtype OR !$this->checkAllowedMimeTypes($mtype, $extension))
{
- $media->clearTempFolder();
+ $media->clearTempFolder($immediate = true);
$response->getBody()->write(json_encode([
- 'message' => Translations::translate('The mime-type is missing, not allowed or does not fit to the file extension.')
+ 'message' => Translations::translate('The mime-type is missing, not allowed or does not fit to the file extension.') . ' mtype: ' . $mtype . ', ext: ' . $extension
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
diff --git a/system/typemill/Controllers/ControllerApiGlobals.php b/system/typemill/Controllers/ControllerApiGlobals.php
index b66f4ac..a154450 100644
--- a/system/typemill/Controllers/ControllerApiGlobals.php
+++ b/system/typemill/Controllers/ControllerApiGlobals.php
@@ -12,10 +12,11 @@ class ControllerApiGlobals extends Controller
{
$navigation = new Navigation();
$systemNavigation = $navigation->getSystemNavigation(
- $userrole = $request->getAttribute('c_userrole'),
- $acl = $this->c->get('acl'),
- $urlinfo = $this->c->get('urlinfo'),
- $dispatcher = $this->c->get('dispatcher')
+ $userrole = $request->getAttribute('c_userrole'),
+ $acl = $this->c->get('acl'),
+ $urlinfo = $this->c->get('urlinfo'),
+ $dispatcher = $this->c->get('dispatcher'),
+ $parser = $this->routeParser
);
# won't work because api has no session, instead you have to pass user
diff --git a/system/typemill/Controllers/ControllerApiTestmail.php b/system/typemill/Controllers/ControllerApiTestmail.php
new file mode 100644
index 0000000..e68c1b1
--- /dev/null
+++ b/system/typemill/Controllers/ControllerApiTestmail.php
@@ -0,0 +1,61 @@
+settings['mailfrom']) or !filter_var($this->settings['mailfrom'], FILTER_VALIDATE_EMAIL))
+ {
+ $response->getBody()->write(json_encode([
+ 'message' => Translations::translate('The from mail is missing or it is not a valid e-mail address.')
+ ]));
+
+ return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
+ }
+
+ $user = new User();
+ $username = $request->getAttribute('c_username');
+
+ if(!$user->setUser($username))
+ {
+ $response->getBody()->write(json_encode([
+ 'message' => Translations::translate('We did not find the a user or usermail.')
+ ]));
+
+ return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
+ }
+
+ $userdata = $user->getUserData();
+
+ $mail = new SimpleMail($this->settings);
+
+ $subject = Translations::translate('Testmail from Typemill');
+ $message = Translations::translate('This is a testmail from Typemill and if you read this e-mail, then everything works fine.');
+
+ $send = $mail->send($userdata['email'], $subject, $message);
+
+ if(!$send)
+ {
+ $response->getBody()->write(json_encode([
+ 'message' => Translations::translate('We could not send the testmail to your e-mail address. Reason: ') . $mail->error
+ ]));
+
+ return $response->withHeader('Content-Type', 'application/json')->withStatus(500);
+ }
+
+ $response->getBody()->write(json_encode([
+ 'message' => Translations::translate('The testmail has been send, please check your inbox and your spam-folder to varify that you received the mail.')
+ ]));
+
+ return $response->withHeader('Content-Type', 'application/json');
+ }
+}
\ No newline at end of file
diff --git a/system/typemill/Controllers/ControllerWebAuth.php b/system/typemill/Controllers/ControllerWebAuth.php
index 07af29a..2815b46 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,80 +22,363 @@ 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');
-
- if($validation->signin($input) === true)
- {
- $user = new User();
+ $securitylog = $this->settings['securitylog'] ?? false;
+ $authcodeactive = $this->isAuthcodeActive($this->settings);
+ $authtitle = Translations::translate('Verification code missing?');
+ $authtext = Translations::translate('If you did not receive an email with the verification code, then the username or password you entered was wrong. Please try again.');
- if(!$user->setUserWithPassword($input['username']))
+ if($validation->signin($input) !== true)
+ {
+ if($securitylog)
+ {
+ \Typemill\Static\Helpers::addLogEntry('login: invalid data');
+ }
+
+ if($this->c->get('flash'))
{
$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();
-
- 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');
+ }
+
+ if($this->c->get('flash'))
+ {
+ $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'],
+ 'authtitle' => $authtitle,
+ 'authtext' => $authtext
+ ]);
+ }
+
+ if($this->c->get('flash'))
+ {
+ $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($this->settings);
+
+ $subject = Translations::translate('Your Typemill verification code');
+ $message = Translations::translate('Dear user') . ',
';
+ $message .= Translations::translate('Someone tried to log in to your Typemill website and we want to make sure it is you. Enter the following verification code to finish your login. The code will be valid for 5 minutes.');
+ $message .= '
' . $authcodevalue . '
';
+ $message .= Translations::translate('If you did not make this login attempt, please reset your password immediately.');
+
+ $send = $mail->send($userdata['email'], $subject, $message);
+
+ if(!$send)
+ {
+ $authtitle = Translations::translate('Error sending email');
+ $authtext = Translations::translate('We could not send the email with the verification 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'],
+ 'authtitle' => $authtitle,
+ 'authtext' => $authtext
+ ]);
+ }
+
+ # check if user has confirmed the account
+ if(isset($userdata['optintoken']) && $userdata['optintoken'])
+ {
+ if($securitylog)
+ {
+ \Typemill\Static\Helpers::addLogEntry('login: user not confirmed yet.');
+ }
+
+ if($this->c->get('flash'))
+ {
+ $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();
+
+ # 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
- *
- * @param obj $request the slim request object
- * @param obj $response the slim response object
- * @return obje $response with redirect to route
- */
-
+
+ private function isAuthcodeActive($settings)
+ {
+ if(
+ isset($settings['authcode']) &&
+ $settings['authcode'] &&
+ isset($settings['mailfrom']) &&
+ filter_var($settings['mailfrom'], FILTER_VALIDATE_EMAIL)
+ )
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ # login a 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 verification code format');
+ }
+
+ if($this->c->get('flash'))
+ {
+ $this->c->get('flash')->addMessage('error', Translations::translate('Invalid verification code 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');
+ }
+
+ if($this->c->get('flash'))
+ {
+ $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: verification code wrong or outdated.');
+ }
+
+ if($this->c->get('flash'))
+ {
+ $this->c->get('flash')->addMessage('error', Translations::translate('The verification 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, $userdata))
+ {
+ $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
public function logout(Request $request, Response $response)
{
\Typemill\Static\Session::stopSession();
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/ControllerWebDownload.php b/system/typemill/Controllers/ControllerWebDownload.php
index 4ed8a84..a81d75a 100644
--- a/system/typemill/Controllers/ControllerWebDownload.php
+++ b/system/typemill/Controllers/ControllerWebDownload.php
@@ -2,18 +2,20 @@
namespace Typemill\Controllers;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Psr\Http\Message\ResponseInterface as Response;
use Typemill\Models\StorageWrapper;
use Typemill\Static\Translations;
class ControllerWebDownload extends Controller
{
- public function download($request, $response, $args)
+ public function download(Request $request, Response $response, $args)
{
$filename = isset($args['params']) ? $args['params'] : false;
if(!$filename)
{
- $response->getBody()->write(Translations::translate('the requested file does not exist.'))->withStatus(404);
- return $response;
+ $response->getBody()->write(Translations::translate('the requested file does not exist.'));
+ return $response->withStatus(404);
}
$storage = new StorageWrapper('\Typemill\Models\Storage');
@@ -26,8 +28,8 @@ class ControllerWebDownload extends Controller
$allowedFiletypes = [];
if(!$this->validate($filepath, $filename, $allowedFiletypes))
{
- $response->getBody()->write(Translations::translate('the requested filetype does not exist.'))->withStatus(404);
- return $response;
+ $response->getBody()->write(Translations::translate('the requested filetype does not exist.'));
+ return $response->withStatus(404);
}
if($restrictions && isset($restrictions[$filefolder . $filename]))
diff --git a/system/typemill/Controllers/ControllerWebFrontend.php b/system/typemill/Controllers/ControllerWebFrontend.php
index c58f730..3facbac 100644
--- a/system/typemill/Controllers/ControllerWebFrontend.php
+++ b/system/typemill/Controllers/ControllerWebFrontend.php
@@ -134,6 +134,44 @@ class ControllerWebFrontend extends Controller
$metadata = $this->c->get('dispatcher')->dispatch(new OnMetaLoaded($metadata),'onMetaLoaded')->getData();
+ # REFERENCE FEATURE
+ if(isset($metadata['meta']['referencetype']) && $metadata['meta']['referencetype'] != 'disable')
+ {
+ $referenceurl = rtrim($urlinfo['baseurl'], '/') . '/' . trim($metadata['meta']['reference'], '/');
+
+ switch ($metadata['meta']['referencetype']) {
+ case 'redirect301':
+ return $response->withHeader('Location', $referenceurl)->withStatus(301);
+ break;
+ case 'redirect302':
+ return $response->withHeader('Location', $referenceurl)->withStatus(302);
+ break;
+ case 'outlink':
+ return $response->withHeader('Location', $metadata['meta']['reference'])->withStatus(301);
+ break;
+ case 'copy':
+ $refpageinfo = $extendedNavigation[$metadata['meta']['reference']] ?? false;
+ if(!$refpageinfo)
+ {
+ return $this->c->get('view')->render($response->withStatus(404), '404.twig', [
+ 'title' => 'Referenced page not found',
+ 'description' => 'We did not find the page that has been referenced. Please inform the website owner to fix it in meta reference.'
+ ]);
+ }
+
+ $refKeyPathArray = explode(".", $refpageinfo['keyPath']);
+ $refItem = $navigation->getItemWithKeyPath($draftNavigation, $refKeyPathArray);
+
+ # GET THE CONTENT FROM REFENCED PAGE
+ $liveMarkdown = $content->getLiveMarkdown($refItem);
+ $liveMarkdown = $this->c->get('dispatcher')->dispatch(new OnMarkdownLoaded($liveMarkdown), 'onMarkdownLoaded')->getData();
+ $markdownArray = $content->markdownTextToArray($liveMarkdown);
+
+ break;
+ }
+ }
+
+
# CHECK ACCESS RESTRICTIONS
$restricted = $this->checkRestrictions($metadata['meta'], $username, $userrole);
if($restricted)
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/Events/OnCspLoaded.php b/system/typemill/Events/OnCspLoaded.php
new file mode 100644
index 0000000..cdae7ec
--- /dev/null
+++ b/system/typemill/Events/OnCspLoaded.php
@@ -0,0 +1,14 @@
+getHeaderLine("Authorization"), $matches))
diff --git a/system/typemill/Middleware/CorsHeadersMiddleware.php b/system/typemill/Middleware/CorsHeadersMiddleware.php
new file mode 100644
index 0000000..062db4a
--- /dev/null
+++ b/system/typemill/Middleware/CorsHeadersMiddleware.php
@@ -0,0 +1,62 @@
+settings = $settings;
+
+ $this->urlinfo = $urlinfo;
+ }
+
+ public function process(Request $request, RequestHandler $handler) :response
+ {
+ # add the custom headers to the response after everything is processed
+ $response = $handler->handle($request);
+
+ ###################
+ # CORS HEADER #
+ ###################
+
+ $origin = $request->getHeaderLine('Origin');
+ $corsdomains = isset($this->settings['corsdomains']) ? trim($this->settings['corsdomains']) : false;
+ $whitelist = [];
+
+ if($corsdomains && $corsdomains != '')
+ {
+ $corsdomains = explode(",", $corsdomains);
+ foreach($corsdomains as $domain)
+ {
+ $domain = trim($domain);
+ if($domain != '')
+ {
+ $whitelist[] = $domain;
+ }
+ }
+ }
+
+ if(!$origin OR $origin == '' OR !isset($whitelist[$origin]))
+ {
+ # set current website as default origin and block all cross origin calls
+ $origin = $this->urlinfo['baseurl'];
+ }
+
+ $response = $response->withHeader('Access-Control-Allow-Origin', $origin)
+ ->withHeader('Access-Control-Allow-Headers', 'X-Requested-With, Content-Type, Accept, Origin, Authorization')
+ ->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS')
+ ->withHeader('Access-Control-Allow-Credentials', 'true');
+
+ return $response;
+ }
+}
\ No newline at end of file
diff --git a/system/typemill/Middleware/CspHeadersMiddleware.php b/system/typemill/Middleware/CspHeadersMiddleware.php
new file mode 100644
index 0000000..661fc9d
--- /dev/null
+++ b/system/typemill/Middleware/CspHeadersMiddleware.php
@@ -0,0 +1,84 @@
+settings = $settings;
+
+ $this->cspFromPlugins = $cspFromPlugins;
+
+ $this->cspFromTheme = $cspFromTheme;
+ }
+
+ public function process(Request $request, RequestHandler $handler) :response
+ {
+ # add the custom headers to the response after everything is processed
+ $response = $handler->handle($request);
+
+ if(isset($this->settings['cspdisabled']) && $this->settings['cspdisabled'])
+ {
+ return $response;
+ }
+
+ $whitelist = ["'unsafe-inline'", "'unsafe-eval'", "'self'", "data:", "*.youtube-nocookie.com", "*.youtube.com"];
+
+ $cspdomains = isset($this->settings['cspdomains']) ? trim($this->settings['cspdomains']) : false;
+
+ if($cspdomains && $cspdomains != '')
+ {
+ $cspdomains = explode(",", $cspdomains);
+ foreach($cspdomains as $cspdomain)
+ {
+ $cspdomain = trim($cspdomain);
+ if($cspdomain != '')
+ {
+ $whitelist[] = $cspdomain;
+ }
+ }
+ }
+
+ # add csp from plugins
+ if($this->cspFromPlugins && is_array($this->cspFromPlugins) && !empty($this->cspFromPlugins))
+ {
+ $whitelist = array_merge($whitelist, $this->cspFromPlugins);
+ }
+
+ # add csp from current theme
+ if($this->cspFromTheme && is_array($this->cspFromTheme) && !empty($this->cspFromTheme))
+ {
+ $whitelist = array_merge($whitelist, $this->cspFromTheme);
+ }
+
+ $whitelist = array_unique($whitelist);
+
+ # do not add csp header if disabled-flag is found
+ if(in_array("disable", $whitelist))
+ {
+ return $response;
+ }
+
+ $whitelist = implode(' ', $whitelist);
+
+ # Define the Content Security Policy header
+ $cspHeader = "default-src " . $whitelist . ";";
+
+ # Add the Content Security Policy header to the response
+ $response = $response->withHeader('Content-Security-Policy', $cspHeader);
+
+ return $response;
+ }
+}
\ No newline at end of file
diff --git a/system/typemill/Middleware/CustomHeadersMiddleware.php b/system/typemill/Middleware/CustomHeadersMiddleware.php
new file mode 100644
index 0000000..98a993f
--- /dev/null
+++ b/system/typemill/Middleware/CustomHeadersMiddleware.php
@@ -0,0 +1,47 @@
+settings = $settings;
+ }
+
+ public function process(Request $request, RequestHandler $handler) :response
+ {
+ $scheme = $request->getUri()->getScheme();
+
+ # add the custom headers to the response after everything is processed
+ $response = $handler->handle($request);
+
+ $response = $response->withoutHeader('Server');
+ $response = $response->withHeader('X-Powered-By', 'Typemill');
+
+ $headersOff = $this->settings['headersoff'] ?? false;
+
+ if(!$headersOff)
+ {
+ $response = $response
+ ->withHeader('X-Content-Type-Options', 'nosniff')
+ ->withHeader('X-Frame-Options', 'SAMEORIGIN')
+ ->withHeader('X-XSS-Protection', '1;mode=block')
+ ->withHeader('Referrer-Policy', 'no-referrer-when-downgrade');
+
+ if($scheme == 'https')
+ {
+ $response = $response->withHeader('Strict-Transport-Security', 'max-age=63072000');
+ }
+ }
+
+ return $response;
+ }
+}
\ No newline at end of file
diff --git a/system/typemill/Middleware/FlashMessages.php b/system/typemill/Middleware/FlashMessages.php
index 9bcb7bc..65eb88e 100644
--- a/system/typemill/Middleware/FlashMessages.php
+++ b/system/typemill/Middleware/FlashMessages.php
@@ -8,7 +8,7 @@ use Slim\Flash\Messages;
class FlashMessages
{
- private $container;
+ public $container;
public function __construct($container)
{
diff --git a/system/typemill/Middleware/RemoveCredentialsMiddleware.php b/system/typemill/Middleware/RemoveCredentialsMiddleware.php
new file mode 100644
index 0000000..a0fe634
--- /dev/null
+++ b/system/typemill/Middleware/RemoveCredentialsMiddleware.php
@@ -0,0 +1,28 @@
+getUri();
+
+ # Remove user information (username:password) from the URI
+ $uri = $uri->withUserInfo('');
+
+ # Create a new request with the modified URI
+ $request = $request->withUri($uri);
+
+ # we could add basic auth credentials to request for later usage
+
+ $response = $handler->handle($request);
+
+ return $response;
+ }
+}
\ No newline at end of file
diff --git a/system/typemill/Middleware/SessionMiddleware.php b/system/typemill/Middleware/SessionMiddleware.php
index bb21251..14afca3 100644
--- a/system/typemill/Middleware/SessionMiddleware.php
+++ b/system/typemill/Middleware/SessionMiddleware.php
@@ -11,54 +11,52 @@ use Typemill\Models\User;
class SessionMiddleware implements MiddlewareInterface
{
- protected $segments;
+ protected $segments;
- protected $route;
+ protected $route;
+
+ public function __construct($segments, $route)
+ {
+ $this->segments = $segments;
- protected $uri;
-
- public function __construct($segments, $route, $uri)
- {
- $this->segments = $segments;
-
- $this->route = $route;
-
- $this->uri = $uri;
- }
-
+ $this->route = $route;
+ }
+
public function process(Request $request, RequestHandler $handler) :response
- {
- $scheme = $request->getUri()->getScheme();
-
- # start session
- Session::startSessionForSegments($this->segments, $this->route, $scheme);
+ {
+ $uri = $request->getUri();
- $authenticated = (
- (isset($_SESSION['username'])) &&
- (isset($_SESSION['login']))
- )
- ? true : false;
+ $scheme = $request->getUri()->getScheme();
+
+ # start session
+ Session::startSessionForSegments($this->segments, $this->route, $scheme);
- if($authenticated)
- {
- # add userdata to the request for later use
- $user = new User();
+ $authenticated = (
+ (isset($_SESSION['username'])) &&
+ (isset($_SESSION['login']))
+ )
+ ? true : false;
- if($user->setUser($_SESSION['username']))
- {
- $userdata = $user->getUserData();
+ if($authenticated)
+ {
+ # add userdata to the request for later use
+ $user = new User();
- $request = $request->withAttribute('c_username', $userdata['username']);
- $request = $request->withAttribute('c_userrole', $userdata['userrole']);
- if(isset($userdata['darkmode']))
- {
- $request = $request->withAttribute('c_darkmode', $userdata['darkmode']);
- }
- }
- }
+ if($user->setUser($_SESSION['username']))
+ {
+ $userdata = $user->getUserData();
+
+ $request = $request->withAttribute('c_username', $userdata['username']);
+ $request = $request->withAttribute('c_userrole', $userdata['userrole']);
+ if(isset($userdata['darkmode']))
+ {
+ $request = $request->withAttribute('c_darkmode', $userdata['darkmode']);
+ }
+ }
+ }
$response = $handler->handle($request);
return $response;
- }
+ }
}
\ No newline at end of file
diff --git a/system/typemill/Models/Media.php b/system/typemill/Models/Media.php
index d9d6f1c..75a8505 100644
--- a/system/typemill/Models/Media.php
+++ b/system/typemill/Models/Media.php
@@ -41,7 +41,7 @@ class Media
$this->tmpFolder = $this->basepath . 'media' . DIRECTORY_SEPARATOR . 'tmp' . DIRECTORY_SEPARATOR;
}
- public function clearTempFolder()
+ public function clearTempFolder($immediate = NULL)
{
$files = scandir($this->tmpFolder);
$now = time();
@@ -54,13 +54,15 @@ class Media
$filelink = $this->tmpFolder . $file;
if(file_exists($filelink))
{
- $filetime = filemtime($filelink);
- if($now - $filetime > 1800)
+ $filetime = filemtime($filelink);
+ $delete = $immediate ? $immediate : ($now - $filetime > 1800);
+
+ if($delete)
{
if(!unlink($filelink))
{
$result = false;
- }
+ }
}
}
}
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..ae96a58
--- /dev/null
+++ b/system/typemill/Models/SimpleMail.php
@@ -0,0 +1,65 @@
+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 send(string $to, string $subject, string $message)
+ {
+ if(!$this->from)
+ {
+ $this->error = Translations::translate('Email address in system settings is missing.');
+
+ 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 75ef329..04b4fe9 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)
@@ -786,7 +812,7 @@ class Validation
break;
case "text":
$v->rule('noHTML', $fieldName);
- $v->rule('lengthMax', $fieldName, 500);
+ $v->rule('lengthMax', $fieldName, 1000);
# $v->rule('regex', $fieldName, '/^[\pL0-9_ \-\.\?\!\/\:]*$/u');
break;
case "textarea":
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..818602f 100644
--- a/system/typemill/Static/Helpers.php
+++ b/system/typemill/Static/Helpers.php
@@ -6,29 +6,6 @@ use Typemill\Models\StorageWrapper;
class Helpers{
- public static function urlInfo($uri)
- {
- $basepath = preg_replace('/(.*)\/.*/', '$1', $_SERVER['SCRIPT_NAME']);
- $currentpath = $uri->getPath();
- $route = str_replace($basepath, '', $currentpath);
- $scheme = $uri->getScheme();
- $authority = $uri->getAuthority();
- $protocol = ($scheme ? $scheme . ':' : '') . ($authority ? '//' . $authority : '');
- $baseurl = $protocol . $basepath;
- $currenturl = $protocol . $currentpath;
-
- return [
- 'basepath' => $basepath,
- 'currentpath' => $currentpath,
- 'route' => $route,
- 'scheme' => $scheme,
- 'authority' => $authority,
- 'protocol' => $protocol,
- 'baseurl' => $baseurl,
- 'currenturl' => $currenturl
- ];
- }
-
public static function getUserIP()
{
$client = @$_SERVER['HTTP_CLIENT_IP'];
@@ -51,7 +28,6 @@ class Helpers{
return $ip;
}
-
public static function addLogEntry($action)
{
$line = self::getUserIP();
diff --git a/system/typemill/Static/License.php b/system/typemill/Static/License.php
index 7676ce6..0a9f050 100644
--- a/system/typemill/Static/License.php
+++ b/system/typemill/Static/License.php
@@ -43,9 +43,7 @@ class License
}
elseif($licenseStatus === true)
{
- echo '
'; - print_r($licensedata); - die(); + die('Static License licenceStatus is true'); } else { diff --git a/system/typemill/Static/Urlinfo.php b/system/typemill/Static/Urlinfo.php new file mode 100644 index 0000000..c704dfa --- /dev/null +++ b/system/typemill/Static/Urlinfo.php @@ -0,0 +1,106 @@ +withUserInfo(''); + $uri = $uri->withPort(null); + + $currentpath = $uri->getPath(); + $route = str_replace($basepath, '', $currentpath); + + $query = $uri->getQuery(); + parse_str($query, $params); + + # proxy detection + if(isset($settings['proxy']) && $settings['proxy'] && isset($_SERVER['HTTP_X_FORWARDED_HOST'])) + { + $trustedProxies = ( isset($settings['trustedproxies']) && !empty($settings['trustedproxies']) ) ? explode(",", $settings['trustedproxies']) : []; + + $proxyuri = self::updateUri($uri, $trustedProxies); + + if($proxyuri) + { + # use uri from proxy + $uri = $proxyuri; + + # standard basepath is empty + $basepath = ""; + + # if proxy has basepath, then + if (isset($_SERVER['HTTP_X_FORWARDED_PREFIX'])) + { + # Use X-Forwarded-Prefix if available + $basepath = rtrim($_SERVER['HTTP_X_FORWARDED_PREFIX'], '/') . '/'; + } + } + } + + $scheme = $uri->getScheme(); + $authority = $uri->getAuthority(); + $protocol = ($scheme ? $scheme . ':' : '') . ($authority ? '//' . $authority : ''); + $baseurl = $protocol . $basepath; + $currenturl = $baseurl . $route; + + return [ + 'basepath' => $basepath, + 'currentpath' => $currentpath, + 'route' => $route, + 'scheme' => $scheme, + 'authority' => $authority, + 'protocol' => $protocol, + 'baseurl' => $baseurl, + 'baseurlWithoutProxy' => false, # add the base url without proxy maybe needed for license? + 'currenturl' => $currenturl, + 'params' => $params + ]; + } + + private static function updateUri($uri, $trustedProxies) + { + # optionally check trusted proxies + $ipAddress = $_SERVER['REMOTE_ADDR'] ?? null; + if ( + $ipAddress + && !empty($trustedProxies) + && !in_array($ipAddress, $trustedProxies) + ) + { + return false; + } + + # get scheme from proxy + $scheme = $_SERVER['HTTP_X_FORWARDED_PROTO'] ?? null; + if ( + $scheme + && in_array($scheme, ['http', 'https']) + ) + { + $uri = $uri->withScheme($scheme); + } + + # get host from proxy + $host = $_SERVER['HTTP_X_FORWARDED_HOST'] ?? null; + if ( + $host + ) + { + $host = trim(current(explode(',', $host))); + + $pos = strpos($host, ':'); + if ($pos !== false) + { + $host = strstr($host, ':', true); + } + $uri = $uri->withHost($host); + } + + return $uri; + } +} \ No newline at end of file diff --git a/system/typemill/author/auth/authcode.twig b/system/typemill/author/auth/authcode.twig new file mode 100644 index 0000000..90dc73f --- /dev/null +++ b/system/typemill/author/auth/authcode.twig @@ -0,0 +1,104 @@ +{% extends 'layouts/layoutAuth.twig' %} + +{% block title %}Login Verification Code{% endblock %} + +{% block content %} + ++ ++{% endblock %} \ No newline at end of file diff --git a/system/typemill/author/js/vue-forms.js b/system/typemill/author/js/vue-forms.js index 5e4765f..b686dd3 100644 --- a/system/typemill/author/js/vue-forms.js +++ b/system/typemill/author/js/vue-forms.js @@ -32,14 +32,13 @@ app.component('component-textarea', { + @input="update($event, name)">++ ++ ++Login Verification Code
+ +{{ translate('Enter the verification code from your email:') }} + +
++ ++ +{{ errors[name] }}
{{ $filters.translate(description) }}
`, @@ -81,8 +80,7 @@ app.component('component-codearea', { :name="name" :placeholder="placeholder" :value="value" - @input="update($event, name)"> - + @input="update($event, name)">{{ errors[name] }}
@@ -141,7 +139,7 @@ app.component('component-select', { @change="update($event,name)"> - +{{ errors[name] }}
{{ $filters.translate(description) }}
`, @@ -173,7 +171,7 @@ app.component('component-checkbox', { v-model="checked" @change="update(checked, name)"> {{ $filters.translate(checkboxlabel) }} - +{{ errors[name] }}
{{ $filters.translate(description) }}
`, @@ -209,7 +207,7 @@ app.component('component-checkboxlist', { v-model="checkedoptions" @change="update(checkedoptions, name)"> {{ $filters.translate(option) }} - +{{ errors[name] }}
{{ $filters.translate(description) }}
`, @@ -249,7 +247,7 @@ app.component('component-radio', { v-model="picked" @change="update(picked, name)"> {{ $filters.translate(option) }} - +{{ errors[name] }}
{{ $filters.translate(description) }}
`, @@ -277,7 +275,7 @@ app.component('component-number', { :name="name" :placeholder="placeholder" :value="value" - @input="update($event, name)"> + @input="update($event, name)">{{ errors[name] }}
{{ $filters.translate(description) }}
`, @@ -308,7 +306,7 @@ app.component('component-date', { :name="name" :placeholder="placeholder" :value="value" - @input="update($event, name)"> + @input="update($event, name)">{{ errors[name] }}
{{ $filters.translate(description) }}
@@ -342,7 +340,7 @@ app.component('component-email', { :name="name" :placeholder="placeholder" :value="value" - @input="update($event, name)"> + @input="update($event, name)">{{ errors[name] }}
{{ $filters.translate(description) }}
@@ -375,7 +373,7 @@ app.component('component-tel', { :name="name" :placeholder="placeholder" :value="value" - @input="update($event, name)"> + @input="update($event, name)">{{ errors[name] }}
{{ $filters.translate(description) }}
@@ -409,7 +407,7 @@ app.component('component-url', { :name="name" :placeholder="placeholder" :value="value" - @input="update($event, name)"> + @input="update($event, name)">{{ errors[name] }}
{{ $filters.translate(description) }}
@@ -442,7 +440,7 @@ app.component('component-color', { :name="name" :placeholder="placeholder" :value="value" - @input="update($event, name)"> + @input="update($event, name)">{{ errors[name] }}
{{ $filters.translate(description) }}
diff --git a/system/typemill/author/js/vue-license.js b/system/typemill/author/js/vue-license.js index 6b077e4..4b178d0 100644 --- a/system/typemill/author/js/vue-license.js +++ b/system/typemill/author/js/vue-license.js @@ -43,35 +43,6 @@ const app = Vue.createApp({ - `, data() { diff --git a/system/typemill/author/js/vue-system.js b/system/typemill/author/js/vue-system.js index da87e5f..7a5b276 100644 --- a/system/typemill/author/js/vue-system.js +++ b/system/typemill/author/js/vue-system.js @@ -17,6 +17,13 @@ const app = Vue.createApp({ :userroles="userroles" :value="formData[fieldname]" v-bind="subfieldDefinition"> ++ + @@ -85,6 +92,32 @@ const app = Vue.createApp({ this.currentTab = tab; this.reset(); }, + testmail: function() + { + this.reset(); + var self = this; + + tmaxios.post('/api/v1/testmail',{ + 'url': data.urlinfo.route, + }) + .then(function (response) + { + self.messageClass = 'bg-teal-500'; + self.message = response.data.message; + }) + .catch(function (error) + { + if(error.response) + { + self.message = handleErrorMessage(error); + self.messageClass = 'bg-rose-500'; + if(error.response.data.errors !== undefined) + { + self.errors = error.response.data.errors; + } + } + }); + }, save: function() { this.reset(); @@ -109,7 +142,7 @@ const app = Vue.createApp({ self.errors = error.response.data.errors; } } - }); + }); }, reset: function() { diff --git a/system/typemill/author/system/license.twig b/system/typemill/author/system/license.twig index c381395..f92624d 100644 --- a/system/typemill/author/system/license.twig +++ b/system/typemill/author/system/license.twig @@ -17,11 +17,4 @@ app.config.globalProperties.$filters = translatefilter; app.mount('#license'); - - {% endblock %} \ No newline at end of file diff --git a/system/typemill/author/translations/de.yaml b/system/typemill/author/translations/de.yaml index 9c6b71b..a2854b7 100644 --- a/system/typemill/author/translations/de.yaml +++ b/system/typemill/author/translations/de.yaml @@ -1,3 +1,5 @@ +# German (Deutsche) +# Author: Sebastian Schürmanns '-LICENSE_AND_YOUR_WEBSITE_MUST_RUN_UNDER_THE_DOMAIN_OF_YOUR_LICENSE.': '-Lizenz und die Webseite muss unter der Domain von der Lizenz laufen.' 'ACCESS_&_RIGHTS': 'Zugang & Rechte' ACCESS_FOR: 'Zugang für' diff --git a/system/typemill/author/translations/en.yaml b/system/typemill/author/translations/en.yaml index f48e9b6..cdef239 100644 --- a/system/typemill/author/translations/en.yaml +++ b/system/typemill/author/translations/en.yaml @@ -1,3 +1,5 @@ +# English +# Author: Sebastian Schuermanns '-LICENSE_AND_YOUR_WEBSITE_MUST_RUN_UNDER_THE_DOMAIN_OF_YOUR_LICENSE.': '' 'ACCESS_&_RIGHTS': '' ACCESS_FOR: '' diff --git a/system/typemill/author/translations/fr.yaml b/system/typemill/author/translations/fr.yaml index 927aaa2..a3cbb34 100644 --- a/system/typemill/author/translations/fr.yaml +++ b/system/typemill/author/translations/fr.yaml @@ -1,3 +1,5 @@ +# French (Français) +# Author: '-LICENSE_AND_YOUR_WEBSITE_MUST_RUN_UNDER_THE_DOMAIN_OF_YOUR_LICENSE.': '' 'ACCESS_&_RIGHTS': '' ACCESS_FOR: '' diff --git a/system/typemill/author/translations/it.yaml b/system/typemill/author/translations/it.yaml index b940d41..bc42361 100644 --- a/system/typemill/author/translations/it.yaml +++ b/system/typemill/author/translations/it.yaml @@ -1,3 +1,5 @@ +# Italian (Italiano) pls ignore autoupdates +# Author: Severo Iuliano (https://github.com/iusvar) '-LICENSE_AND_YOUR_WEBSITE_MUST_RUN_UNDER_THE_DOMAIN_OF_YOUR_LICENSE.': '' 'ACCESS_&_RIGHTS': '' ACCESS_FOR: '' diff --git a/system/typemill/routes/api.php b/system/typemill/routes/api.php index 05bd1bb..7abf57c 100644 --- a/system/typemill/routes/api.php +++ b/system/typemill/routes/api.php @@ -3,6 +3,7 @@ use Slim\Routing\RouteCollectorProxy; use Typemill\Middleware\ApiAuthentication; use Typemill\Middleware\ApiAuthorization; +use Typemill\Middleware\CorsHeadersMiddleware; use Typemill\Controllers\ControllerApiGlobals; use Typemill\Controllers\ControllerApiMedia; use Typemill\Controllers\ControllerApiSystemSettings; @@ -18,6 +19,7 @@ use Typemill\Controllers\ControllerApiAuthorArticle; use Typemill\Controllers\ControllerApiAuthorBlock; use Typemill\Controllers\ControllerApiAuthorMeta; use Typemill\Controllers\ControllerApiAuthorShortcode; +use Typemill\Controllers\ControllerApiTestmail; $app->group('/api/v1', function (RouteCollectorProxy $group) use ($acl) { @@ -34,6 +36,7 @@ $app->group('/api/v1', function (RouteCollectorProxy $group) use ($acl) { $group->post('/plugin', ControllerApiSystemPlugins::class . ':updatePlugin')->setName('api.plugin.set')->add(new ApiAuthorization($acl, 'system', 'update')); # admin $group->post('/extensions', ControllerApiSystemExtensions::class . ':activateExtension')->setName('api.extension.activate')->add(new ApiAuthorization($acl, 'system', 'update')); # admin $group->post('/versioncheck', ControllerApiSystemVersions::class . ':checkVersions')->setName('api.versioncheck')->add(new ApiAuthorization($acl, 'system', 'update')); # admin + $group->post('/testmail', ControllerApiTestmail::class . ':send')->setName('api.testmail')->add(new ApiAuthorization($acl, 'system', 'update')); # admin $group->get('/users/getbynames', ControllerApiSystemUsers::class . ':getUsersByNames')->setName('api.usersbynames')->add(new ApiAuthorization($acl, 'user', 'update')); # admin $group->get('/users/getbyemail', ControllerApiSystemUsers::class . ':getUsersByEmail')->setName('api.usersbyemail')->add(new ApiAuthorization($acl, 'user', 'update')); # admin $group->get('/users/getbyrole', ControllerApiSystemUsers::class . ':getUsersByRole')->setName('api.usersbyrole')->add(new ApiAuthorization($acl, 'user', 'update')); # admin @@ -85,13 +88,13 @@ $app->group('/api/v1', function (RouteCollectorProxy $group) use ($acl) { $group->get('/meta', ControllerApiAuthorMeta::class . ':getMeta')->setName('api.meta.get')->add(new ApiAuthorization($acl, 'mycontent', 'view')); $group->post('/meta', ControllerApiAuthorMeta::class . ':updateMeta')->setName('api.metadata.update')->add(new ApiAuthorization($acl, 'mycontent', 'update')); -})->add(new ApiAuthentication()); +})->add(new CorsHeadersMiddleware($settings, $urlinfo))->add(new ApiAuthentication()); # api-routes from plugins if(isset($routes['api']) && !empty($routes['api'])) { foreach($routes['api'] as $pluginRoute) - { + { $method = $pluginRoute['httpMethod'] ?? false; $route = $pluginRoute['route'] ?? false; $class = $pluginRoute['class'] ?? false; @@ -102,12 +105,12 @@ if(isset($routes['api']) && !empty($routes['api'])) if($resources && $privilege) { # protected api requires authentication and authorization - $app->{$method}($route, $class)->setName($name)->add(new ApiAuthorization($acl, $resource, $privilege))->add(new ApiAuthentication()); + $app->{$method}($route, $class)->setName($name)->add(new ApiAuthorization($acl, $resource, $privilege))->add(new CorsHeadersMiddleware($settings, $urlinfo))->add(new ApiAuthentication()); } else { # public api routes - $app->{$method}($route, $class)->setName($name); + $app->{$method}($route, $class)->setName($name)->add(new CorsHeadersMiddleware($settings, $urlinfo)); } } } \ No newline at end of file diff --git a/system/typemill/routes/web.php b/system/typemill/routes/web.php index 18e1c89..037269d 100644 --- a/system/typemill/routes/web.php +++ b/system/typemill/routes/web.php @@ -4,6 +4,7 @@ use Slim\Routing\RouteCollectorProxy; use Typemill\Middleware\WebRedirectIfAuthenticated; use Typemill\Middleware\WebRedirectIfUnauthenticated; use Typemill\Middleware\WebAuthorization; +use Typemill\Middleware\CspHeadersMiddleware; use Typemill\Controllers\ControllerWebSetup; use Typemill\Controllers\ControllerWebAuth; use Typemill\Controllers\ControllerWebRecover; @@ -18,6 +19,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'); @@ -26,7 +32,7 @@ $app->group('/tm', function (RouteCollectorProxy $group) use ($settings) { $group->post('/reset', ControllerWebRecover::class . ':resetPassword')->setName('auth.reset'); } -})->add(new WebRedirectIfAuthenticated($routeParser, $settings)); +})->add(new CspHeadersMiddleware($settings, $cspFromPlugins, $cspFromTheme))->add(new WebRedirectIfAuthenticated($routeParser, $settings)); # AUTHOR AREA (requires authentication) $app->group('/tm', function (RouteCollectorProxy $group) use ($routeParser,$acl) { @@ -46,10 +52,11 @@ $app->group('/tm', function (RouteCollectorProxy $group) use ($routeParser,$acl) $group->get('/content/visual[/{route:.*}]', ControllerWebAuthor::class . ':showBlox')->setName('content.visual')->add(new WebAuthorization($routeParser, $acl, 'mycontent', 'view')); $group->get('/content/raw[/{route:.*}]', ControllerWebAuthor::class . ':showRaw')->setName('content.raw')->add(new WebAuthorization($routeParser, $acl, 'mycontent', 'view')); -})->add(new WebRedirectIfUnauthenticated($routeParser)); +})->add(new CspHeadersMiddleware($settings, $cspFromPlugins, $cspFromTheme))->add(new WebRedirectIfUnauthenticated($routeParser)); $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'); @@ -68,11 +75,11 @@ if(isset($routes['web']) && !empty($routes['web'])) if($resources && $privilege) { - $app->{$method}($route, $class)->setName($name)->add(new WebAuthorization($routeParser, $acl, $resource, $privilege))->add(new WebRedirectIfUnauthenticated($routeParser)); + $app->{$method}($route, $class)->setName($name)->add(new CspHeadersMiddleware($settings, $cspFromPlugins, $cspFromTheme))->add(new WebAuthorization($routeParser, $acl, $resource, $privilege))->add(new WebRedirectIfUnauthenticated($routeParser)); } else { - $app->{$method}($route, $class)->setName($name); + $app->{$method}($route, $class)->setName($name)->add(new CspHeadersMiddleware($settings, $cspFromPlugins, $cspFromTheme)); } } } @@ -80,10 +87,10 @@ if(isset($routes['web']) && !empty($routes['web'])) if(isset($settings['access']) && $settings['access'] != '') { # if access for website is restricted - $app->get('/[{route:.*}]', ControllerWebFrontend::class . ':index')->setName('home')->add(new WebAuthorization($routeParser, $acl, 'account', 'view')); + $app->get('/[{route:.*}]', ControllerWebFrontend::class . ':index')->setName('home')->add(new CspHeadersMiddleware($settings, $cspFromPlugins, $cspFromTheme))->add(new WebAuthorization($routeParser, $acl, 'account', 'view')); } else { # if access is not restricted - $app->get('/[{route:.*}]', ControllerWebFrontend::class . ':index')->setName('home'); + $app->get('/[{route:.*}]', ControllerWebFrontend::class . ':index')->setName('home')->add(new CspHeadersMiddleware($settings, $cspFromPlugins, $cspFromTheme)); } diff --git a/system/typemill/settings/defaults.yaml b/system/typemill/settings/defaults.yaml index 6123000..3fd0912 100644 --- a/system/typemill/settings/defaults.yaml +++ b/system/typemill/settings/defaults.yaml @@ -1,4 +1,4 @@ -version: '2.0.3' +version: '2.1.0' title: 'Typemill' author: 'Unknown' copyright: false diff --git a/system/typemill/settings/metatabs.yaml b/system/typemill/settings/metatabs.yaml index be5ee00..722bc48 100644 --- a/system/typemill/settings/metatabs.yaml +++ b/system/typemill/settings/metatabs.yaml @@ -78,20 +78,24 @@ meta: hidden: true css: hidden pattern: '[0-9][0-9]-[0-9][0-9]-[0-9][0-9]' -# fieldsetreference: -# type: fieldset -# legend: Reference -# fields: -# reference: -# type: text -# label: Reference to page -# maxlength: 60 -# referencetype: -# type: radio -# label: Type of reference -# options: -# copy: Copy (copy the content of the referenced page) -# redirect: Redirect (redirect the user to the referenced page) + fieldsetreference: + type: fieldset + legend: Reference + fields: + reference: + type: text + label: Reference to page + placeholder: '/path/to/internal/page or https://exgernal-page.org' + maxlength: 200 + referencetype: + type: radio + label: Type of reference + options: + disable: Disable + redirect301: PERMANENT REDIRECT (301) the user to the referenced internal page + redirect302: TEMPORARY REDIRECT (302) the user to the referenced internal page + copy: COPY the content of the referenced internal page + outlink: LINK to an external page fieldsetvisibility: type: fieldset legend: Visibility diff --git a/system/typemill/settings/system.yaml b/system/typemill/settings/system.yaml index 81a3e5b..f495185 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,73 +141,120 @@ 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: email + label: 'Mail From (required)' + placeholder: sender@yourmail.org + maxlength: 100 + description: 'Enter an email address that sends the e-mails (sender). The e-mail-feature will be used for recovery and verification e-mails. Send a testmail to your user-account to verify that you receive the e-mails.' + mailfromname: + type: text + label: 'Mail From Name (optional)' + placeholder: sender name + maxlength: 100 + 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: 100 + 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.' + description: "From mail is required for this feature. Send a testmail before you use this feature." + recoversubject: + type: text + label: 'Email subject' + placeholder: 'Recover your password' + maxlength: 60 + recovermessage: + type: textarea + label: 'Text before recover link in email message' + descriptiion: 'The recover-link will be active for 24 hours.' + maxlength: 2000 fieldsetsecurity: type: fieldset legend: Security fields: - securitylog: + authcode: type: checkbox - label: Security log - checkboxlabel: Track spam and suspicious actions in a logfile + label: 'Login Verification (recommended)' + checkboxlabel: 'Verify your login with a 5-digit code send by email.' + description: 'From mail is required for this feature. Send a testmail before you use this feature. Make sure you have ftp-access to disable the feature in settings.yaml on failure. The verification code will be valid for 5 minutes. Be aware that device fingerprints will be stored in the user accounts.' 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: + disabled: 'Disable' + standard: 'Always show' + aftererror: 'Show after first wrong input' + securitylog: 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 + label: 'Security log' + checkboxlabel: 'Track spam and suspicious actions in a logfile' fieldsetdeveloper: type: fieldset - legend: Developer + legend: "Developer" fields: displayErrorDetails: type: checkbox - label: Error reporting - checkboxlabel: Display application errors + label: "Error reporting" + checkboxlabel: "Display application errors" twigcache: type: checkbox - label: Twig cache - checkboxlabel: Activate the cache for twig templates + label: "Twig cache" + checkboxlabel: "Activate the cache for twig templates" proxy: type: checkbox - label: Proxy - checkboxlabel: Use x-forwarded-header. + label: "Proxy" + checkboxlabel: "Use x-forwarded-header" trustedproxies: type: text - label: Trusted IPs for proxies (comma separated) \ No newline at end of file + label: "Trusted IPs for proxies (comma separated)" + headersoff: + type: checkbox + label: "Disable Custom Headers" + checkboxlabel: "Disable all custom headers of Typemill (except cors) and send your own headers instead." + cspdisabled: + type: checkbox + label: "Disable CSP Headers" + checkboxlabel: "Disable all csp headers (content security policy) for this website." + cspdomains: + type: textarea + label: "Allowed Domains for Content on Typemill (CSP-Headers)" + placeholder: 'https://www.google.com,*google.com' + description: "List all domains, separated by commas, to allow content integration, such as iframes, on your Typemill website. Domains will be added to the csp-header. Usually done with plugins and themes, but add manually if something is blocked." + corsdomains: + type: textarea + label: "Allowed Domains for API-Access (CORS-Headers)" + placeholder: 'https://my-website-that-uses-the-api.org,https://another-website-using-the-api.org' + description: "List all domains, separated by comma, that should have access to the Typemill API. Domains will be added to the cors-header." + \ No newline at end of file diff --git a/system/typemill/system.php b/system/typemill/system.php index 157b64a..aa06a0c 100644 --- a/system/typemill/system.php +++ b/system/typemill/system.php @@ -16,11 +16,14 @@ use Typemill\Static\Plugins; use Typemill\Static\Translations; use Typemill\Static\Permissions; use Typemill\Static\Helpers; +use Typemill\Static\Urlinfo; use Typemill\Events\OnSettingsLoaded; use Typemill\Events\OnPluginsLoaded; use Typemill\Events\OnSessionSegmentsLoaded; use Typemill\Events\OnRolesPermissionsLoaded; use Typemill\Events\OnResourcesLoaded; +use Typemill\Events\OnCspLoaded; +use Typemill\Middleware\RemoveCredentialsMiddleware; use Typemill\Middleware\SessionMiddleware; use Typemill\Middleware\OldInputMiddleware; use Typemill\Middleware\ValidationErrorsMiddleware; @@ -28,6 +31,7 @@ use Typemill\Middleware\JsonBodyParser; use Typemill\Middleware\FlashMessages; use Typemill\Middleware\AssetMiddleware; use Typemill\Middleware\SecurityMiddleware; +use Typemill\Middleware\CustomHeadersMiddleware; use Typemill\Extensions\TwigCsrfExtension; use Typemill\Extensions\TwigUrlExtension; use Typemill\Extensions\TwigUserExtension; @@ -48,14 +52,16 @@ ini_set('display_errors', 0); ini_set('display_startup_errors', 0); error_reporting(E_ALL); + /**************************** -* LOAD SETTINGS * +* LOAD SETTINGS * ****************************/ $settingsModel = new Settings(); $settings = $settingsModel->loadSettings(); + /**************************** * HANDLE DISPLAY ERRORS * ****************************/ @@ -65,19 +71,9 @@ if(isset($settings['displayErrorDetails']) && $settings['displayErrorDetails']) # ini_set('display_startup_errors', 1); } -/**************************** -* ADD PATH-INFOS FOR LATER * -****************************/ - -# ADD THEM TO THE SETTINGS AND YOU HAVE THEM EVERYWHERE?? -$uriFactory = new UriFactory(); -$uri = $uriFactory->createFromGlobals($_SERVER); -$urlinfo = Helpers::urlInfo($uri); - -$timer['settings'] = microtime(true); /**************************** -* CREATE CONTAINER * +* CREATE CONTAINER + APP * ****************************/ # https://www.slimframework.com/docs/v4/start/upgrade.html#changes-to-container @@ -93,11 +89,40 @@ $routeParser = $app->getRouteCollector()->getRouteParser(); # add route parser to container to use named routes in controller $container->set('routeParser', $routeParser); -# set urlinfo -$container->set('urlinfo', $urlinfo); + +/******************************* + * Basepath * + ******************************/ # in slim 4 you alsways have to set application basepath -$app->setBasePath($urlinfo['basepath']); +$basepath = preg_replace('/(.*)\/.*/', '$1', $_SERVER['SCRIPT_NAME']); +$app->setBasePath($basepath); + + +/**************** +* URLINFO * +****************/ + +# WE DO NOT NEED IT HERE? +# WE CAN ADD IT TO CONTAINER IN MIDDLEWARE AFTER PROXY DETECTION + +# WE NEED FOR +# - LICENSE (base url) +# - Each plugin to add container +# - SESSION SEGMEŃTS (route) +# - TRANSLATIONS (route) +# - ASSETS (route) +# - TWIG URL EXTENSION +# - SESSION MIDDLEWARE + +$uriFactory = new UriFactory(); +$uri = $uriFactory->createFromGlobals($_SERVER); +$urlinfo = Urlinfo::getUrlInfo($basepath, $uri, $settings); + +$timer['settings'] = microtime(true); + +# set urlinfo +$container->set('urlinfo', $urlinfo); $timer['container'] = microtime(true); @@ -304,6 +329,8 @@ foreach($middleware as $pluginMiddleware) } } +$app->add(new CustomHeadersMiddleware($settings)); + $app->add(new AssetMiddleware($assets, $container->get('view'))); $app->add(new ValidationErrorsMiddleware($container->get('view'))); @@ -312,11 +339,11 @@ $app->add(new SecurityMiddleware($routeParser, $container->get('settings'))); $app->add(new OldInputMiddleware($container->get('view'))); -$app->add(new FlashMessages($container)); - # Add Twig-View Middleware $app->add(TwigMiddleware::createFromContainer($app)); +$app->add(new FlashMessages($container)); + # add JsonBodyParser Middleware $app->add(new JsonBodyParser()); @@ -343,7 +370,9 @@ $errorMiddleware->setErrorHandler(HttpNotFoundException::class, function ($reque $app->add($errorMiddleware); -$app->add(new SessionMiddleware($session_segments, $urlinfo['route'], $uri)); +$app->add(new SessionMiddleware($session_segments, $urlinfo['route'])); + +$app->add(new RemoveCredentialsMiddleware()); if(isset($settings['proxy']) && $settings['proxy']) { @@ -355,9 +384,25 @@ if(isset($settings['proxy']) && $settings['proxy']) $timer['middleware'] = microtime(true); +/****************************** +* GET CSP FROM PLUGINS/THEMES * +******************************/ + +# get additional csp headers from plugins +$cspFromPlugins = $dispatcher->dispatch(new OnCspLoaded([]), 'onCspLoaded')->getData(); + +# get additional csp headers from theme +$cspFromTheme = []; +$themeSettings = $settingsModel->getObjectSettings('themes', $settings['theme']); +if(isset($themeSettings['csp']) && is_array($themeSettings['csp']) && !empty($themeSettings['csp']) ) +{ + $cspFromTheme = $themeSettings['csp']; +} + /************************ * ADD ROUTES * ************************/ + if(isset($settings['setup']) && $settings['setup'] == true) { require __DIR__ . '/routes/setup.php'; @@ -370,6 +415,7 @@ else $timer['routes'] = microtime(true); + /************************ * RUN APP * ************************/