1
0
mirror of https://github.com/typemill/typemill.git synced 2025-07-29 02:10:20 +02:00

Merge branch 'v2.1.0' into v2.x

This commit is contained in:
trendschau
2024-01-15 17:17:05 +01:00
41 changed files with 1347 additions and 343 deletions

7
.gitignore vendored
View File

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

12
composer.lock generated
View File

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

View File

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

View File

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

View File

@@ -0,0 +1,61 @@
<?php
namespace Typemill\Controllers;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Slim\Routing\RouteContext;
use Typemill\Models\User;
use Typemill\Models\SimpleMail;
use Typemill\Static\Translations;
class ControllerApiTestmail extends Controller
{
public function send(Request $request, Response $response)
{
if(!isset($this->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');
}
}

View File

@@ -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') . ',<br><br>';
$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 .= '<br><br>' . $authcodevalue . '<br><br>';
$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;
}
}

View File

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

View File

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

View File

@@ -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 = '<a href="'. $url . '">' . $url . '</a>';
# define the headers
$headers = 'Content-Type: text/html; charset=utf-8' . "\r\n";
$headers .= 'Content-Transfer-Encoding: base64' . "\r\n";
if(isset($settings['recoverfrom']) && $settings['recoverfrom'] != '')
{
$headers .= 'From: ' . $settings['recoverfrom'];
}
$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 .= ",<br/><br/>";
@@ -72,16 +67,14 @@ class ControllerWebRecover extends Controller
$messagetext = $parsedown->markup($contentArray);
}
$message = base64_encode($messagetext . "<br/><br/>" . $link);
$message = $messagetext . "<br/><br/>" . $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
{

View File

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

View File

@@ -0,0 +1,14 @@
<?php
namespace Typemill\Events;
use Symfony\Component\EventDispatcher\Event;
/**
* Event for the page rendering data.
*/
class OnCspLoaded extends BaseEvent
{
}

View File

@@ -63,6 +63,9 @@ class ApiAuthentication
}
*/
########### WHY NOT USE BASIC AUTH PARAMS FROM URI ?
$params = [];
if (preg_match("/Basic\s+(.*)$/i", $request->getHeaderLine("Authorization"), $matches))

View File

@@ -0,0 +1,62 @@
<?php
namespace Typemill\Middleware;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Psr7\Response;
class CorsHeadersMiddleware implements MiddlewareInterface
{
protected $settings;
protected $urlinfo;
public function __construct($settings, $urlinfo)
{
$this->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;
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace Typemill\Middleware;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Psr7\Response;
class CspHeadersMiddleware implements MiddlewareInterface
{
protected $settings;
protected $cspFromPlugins;
protected $cspFromTheme;
public function __construct($settings, $cspFromPlugins, $cspFromTheme)
{
$this->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;
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Typemill\Middleware;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Psr7\Response;
class CustomHeadersMiddleware implements MiddlewareInterface
{
protected $settings;
public function __construct($settings)
{
$this->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;
}
}

View File

@@ -8,7 +8,7 @@ use Slim\Flash\Messages;
class FlashMessages
{
private $container;
public $container;
public function __construct($container)
{

View File

@@ -0,0 +1,28 @@
<?php
namespace Typemill\Middleware;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Psr7\Response;
class RemoveCredentialsMiddleware implements MiddlewareInterface
{
public function process(Request $request, RequestHandler $handler) :response
{
$uri = $request->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;
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,65 @@
<?php
namespace Typemill\Models;
use Typemill\Static\Translations;
class SimpleMail
{
private $from = false;
private $reply = false;
public $error;
public function __construct($settings)
{
if(isset($settings['mailfrom']) && $settings['mailfrom'] != '')
{
$this->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;
}
}

View File

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

View File

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

View File

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

View File

@@ -43,9 +43,7 @@ class License
}
elseif($licenseStatus === true)
{
echo '<pre>';
print_r($licensedata);
die();
die('Static License licenceStatus is true');
}
else
{

View File

@@ -0,0 +1,106 @@
<?php
namespace Typemill\Static;
## MOVE TO MIDDLEWARE IN 2.2.0
class Urlinfo
{
# we need to get urlinfos to use in frontend and to inject into assets and container before middleware starts and request-object is available.
public static function getUrlInfo($basepath, $uri, $settings)
{
$uri = $uri->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;
}
}

View File

@@ -0,0 +1,104 @@
{% extends 'layouts/layoutAuth.twig' %}
{% block title %}Login Verification Code{% endblock %}
{% block content %}
<div class="lg:flex-row flex flex-col-reverse justify-end">
<div class="lg:w-1/2 bg-teal-600 text-white min-h-screen flex justify-center items-center">
<div class="max-w-md content-center">
<h1 class="text-4xl py-5">Login Verification Code</h1>
<p>{{ translate('Enter the verification code from your email:') }}</a>
<form method="POST" action="{{ url_for("auth.authcode") }}" autocomplete="off">
<fieldset>
<div class="flex justify-between my-2">
<input
type="text"
name="code-1"
pattern="[0-9]"
maxlength="1"
oninput="moveToNextField(this)"
class="mr-2 form-control block w-full px-3 py-3 text-xl text-center font-bold text-gray-700 bg-white bg-clip-padding border border-solid border-gray-300 transition ease-in-out m-0 focus:text-gray-700 focus:bg-white focus:border-blue-600 focus:outline-non"
required>
<input
type="text"
name="code-2"
pattern="[0-9]"
maxlength="1"
oninput="moveToNextField(this)"
class="mr-2 form-control block w-full px-3 py-3 text-xl text-center font-bold text-gray-700 bg-white bg-clip-padding border border-solid border-gray-300 transition ease-in-out m-0 focus:text-gray-700 focus:bg-white focus:border-blue-600 focus:outline-non"
required>
<input
type="text"
name="code-3"
pattern="[0-9]"
maxlength="1"
oninput="moveToNextField(this)"
class="mr-2 form-control block w-full px-3 py-3 text-xl text-center font-bold text-gray-700 bg-white bg-clip-padding border border-solid border-gray-300 transition ease-in-out m-0 focus:text-gray-700 focus:bg-white focus:border-blue-600 focus:outline-non"
required>
<input
type="text"
name="code-4"
pattern="[0-9]"
maxlength="1"
oninput="moveToNextField(this)"
class="mr-2 form-control block w-full px-3 py-3 text-xl text-center font-bold text-gray-700 bg-white bg-clip-padding border border-solid border-gray-300 transition ease-in-out m-0 focus:text-gray-700 focus:bg-white focus:border-blue-600 focus:outline-non"
required>
<input
type="text"
name="code-5"
pattern="[0-9]"
maxlength="1"
oninput="moveToNextField(this)"
class="form-control block w-full px-3 py-3 text-xl text-center font-bold text-gray-700 bg-white bg-clip-padding border border-solid border-gray-300 transition ease-in-out m-0 focus:text-gray-700 focus:bg-white focus:border-blue-600 focus:outline-non"
required>
</div>
<input type="hidden" value="{{username}}" name="username">
<div class="personal-mail hidden">
<label>Personal Mail</label>
<input type="text" name="personal-honey-mail">
</div>
<input
type="submit"
value="{{ translate('Confirm') }}"
class="block w-full mt-6 px-3 py-3 border-0 font-medium text-xs leading-tight uppercase border-2 border-stone-50 text-stone-50 pointer hover:bg-stone-50 hover:text-stone-800 cursor-pointer focus:outline-none focus:ring-0 transition duration-100 ease-in-out"
/>
</fieldset>
<script>
function moveToNextField(currentField) {
var inputValue = currentField.value;
var inputLength = inputValue.length;
if (inputLength === 1)
{
var nextField = currentField.nextElementSibling;
if (nextField)
{
nextField.focus();
}
}
}
</script>
</form>
</div>
</div>
<div class="lg:w-1/2 lg:bg-white lg:text-black p-5 bg-teal-600 text-white border-b border-white content-center flex justify-center items-center">
<div class="max-w-md content-center">
<h2 class="text-4xl py-5">{{ authtitle }}</h2>
<p class="py-2">{{ authtext }}</p>
<a class="text-teal-600 py-2" href="{{ url_for('auth.show') }}">&#x2192; {{ translate('Back to login') }}</a>
</div>
</div>
</div>
{% endblock %}

View File

@@ -32,14 +32,13 @@ app.component('component-textarea', {
<textarea rows="8" class="w-full border border-stone-300 text-stone-900 bg-stone-200 px-2 py-3"
:id="id"
:class="errors[name] ? ' border-red-500 bg-red-100' : ' border-stone-300 bg-stone-200'"
:class="css"
:readonly="readonly"
:required="required"
:disabled="disabled"
:name="name"
:placeholder="placeholder"
:value="value"
@input="update($event, name)"></textarea>
@input="update($event, name)"></textarea><slot></slot>
<p v-if="errors[name]" class="text-xs text-red-500">{{ errors[name] }}</p>
<p v-else class="text-xs">{{ $filters.translate(description) }}</p>
</div>`,
@@ -81,8 +80,7 @@ app.component('component-codearea', {
:name="name"
:placeholder="placeholder"
:value="value"
@input="update($event, name)">
</textarea>
@input="update($event, name)"></textarea><slot></slot>
<pre aria-hidden="true" class="highlight hljs"><code data-el="highlight" v-html="highlighted"></code></pre>
</div>
<p v-if="errors[name]" class="text-xs text-red-500">{{ errors[name] }}</p>
@@ -141,7 +139,7 @@ app.component('component-select', {
@change="update($event,name)">
<option disabled value="">Please select</option>
<option v-for="option,optionkey in options" v-bind:value="optionkey">{{option}}</option>
</select>
</select><slot></slot>
<p v-if="errors[name]" class="text-xs text-red-500">{{ errors[name] }}</p>
<p v-else class="text-xs">{{ $filters.translate(description) }}</p>
</div>`,
@@ -173,7 +171,7 @@ app.component('component-checkbox', {
v-model="checked"
@change="update(checked, name)">
<span class="ml-2 text-sm">{{ $filters.translate(checkboxlabel) }}</span>
</label>
</label><slot></slot>
<p v-if="errors[name]" class="text-xs text-red-500">{{ errors[name] }}</p>
<p v-else class="text-xs">{{ $filters.translate(description) }}</p>
</div>`,
@@ -209,7 +207,7 @@ app.component('component-checkboxlist', {
v-model="checkedoptions"
@change="update(checkedoptions, name)">
<span class="ml-2 text-sm">{{ $filters.translate(option) }}</span>
</label>
</label><slot></slot>
<p v-if="errors[name]" class="text-xs text-red-500">{{ errors[name] }}</p>
<p v-else class="text-xs">{{ $filters.translate(description) }}</p>
</div>`,
@@ -249,7 +247,7 @@ app.component('component-radio', {
v-model="picked"
@change="update(picked, name)">
<span class="ml-2 text-sm">{{ $filters.translate(option) }}</span>
</label>
</label><slot></slot>
<p v-if="errors[name]" class="text-xs text-red-500">{{ errors[name] }}</p>
<p v-else class="text-xs">{{ $filters.translate(description) }}</p>
</div>`,
@@ -277,7 +275,7 @@ app.component('component-number', {
:name="name"
:placeholder="placeholder"
:value="value"
@input="update($event, name)">
@input="update($event, name)"><slot></slot>
<p v-if="errors[name]" class="text-xs text-red-500">{{ errors[name] }}</p>
<p v-else class="text-xs">{{ $filters.translate(description) }}</p>
</div>`,
@@ -308,7 +306,7 @@ app.component('component-date', {
:name="name"
:placeholder="placeholder"
:value="value"
@input="update($event, name)">
@input="update($event, name)"><slot></slot>
</div>
<p v-if="errors[name]" class="text-xs text-red-500">{{ errors[name] }}</p>
<p v-else class="text-xs">{{ $filters.translate(description) }}</p>
@@ -342,7 +340,7 @@ app.component('component-email', {
:name="name"
:placeholder="placeholder"
:value="value"
@input="update($event, name)">
@input="update($event, name)"><slot></slot>
</div>
<p v-if="errors[name]" class="text-xs text-red-500">{{ errors[name] }}</p>
<p v-else class="text-xs">{{ $filters.translate(description) }}</p>
@@ -375,7 +373,7 @@ app.component('component-tel', {
:name="name"
:placeholder="placeholder"
:value="value"
@input="update($event, name)">
@input="update($event, name)"><slot></slot>
</div>
<p v-if="errors[name]" class="text-xs text-red-500">{{ errors[name] }}</p>
<p v-else class="text-xs">{{ $filters.translate(description) }}</p>
@@ -409,7 +407,7 @@ app.component('component-url', {
:name="name"
:placeholder="placeholder"
:value="value"
@input="update($event, name)">
@input="update($event, name)"><slot></slot>
</div>
<p v-if="errors[name]" class="text-xs text-red-500">{{ errors[name] }}</p>
<p v-else class="text-xs">{{ $filters.translate(description) }}</p>
@@ -442,7 +440,7 @@ app.component('component-color', {
:name="name"
:placeholder="placeholder"
:value="value"
@input="update($event, name)">
@input="update($event, name)"><slot></slot>
</div>
<p v-if="errors[name]" class="text-xs text-red-500">{{ errors[name] }}</p>
<p v-else class="text-xs">{{ $filters.translate(description) }}</p>

View File

@@ -43,35 +43,6 @@ const app = Vue.createApp({
</div>
</div>
</div>
<!--
<div v-for="(fieldDefinition, fieldname) in formDefinitions">
<fieldset class="flex flex-wrap justify-between border-2 border-stone-200 p-4 my-8" v-if="fieldDefinition.type == 'fieldset'">
<legend class="text-lg font-medium">{{ fieldDefinition.legend }}</legend>
<component v-for="(subfieldDefinition, subfieldname) in fieldDefinition.fields"
:key="subfieldname"
:is="selectComponent(subfieldDefinition.type)"
:errors="errors"
:name="subfieldname"
:userroles="userroles"
:value="formData[subfieldname]"
v-bind="subfieldDefinition">
</component>
</fieldset>
<component v-else
:key="fieldname"
:is="selectComponent(fieldDefinition.type)"
:errors="errors"
:name="fieldname"
:userroles="userroles"
:value="formData[fieldname]"
v-bind="fieldDefinition">
</component>
</div>
<div class="my-5">
<div :class="messageClass" class="block w-full h-8 px-3 py-1 my-1 text-white transition duration-100">{{ message }}</div>
<input type="submit" @click.prevent="save()" value="save" class="w-full p-3 my-1 bg-stone-700 hover:bg-stone-900 text-white cursor-pointer transition duration-100">
</div>
-->
</form>
</Transition>`,
data() {

View File

@@ -17,6 +17,13 @@ const app = Vue.createApp({
:userroles="userroles"
:value="formData[fieldname]"
v-bind="subfieldDefinition">
<slot v-if="fieldname == 'mailfrom'">
<button
class = "absolute px-2 py-3 ml-2 text-stone-50 bg-stone-700 hover:bg-stone-900 hover:text-white transition duration-100 cursor-pointer"
style = "right:0px; width:200px;"
@click.prevent = "testmail()"
>send testmail</button>
</slot>
</component>
</fieldset>
</div>
@@ -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()
{

View File

@@ -17,11 +17,4 @@
app.config.globalProperties.$filters = translatefilter;
app.mount('#license');
</script>
<script src="https://cdn.paddle.com/paddle/paddle.js"></script>
<script type="text/javascript">
Paddle.Environment.set('sandbox');
Paddle.Setup({ vendor: 10629 });
</script>
{% endblock %}

View File

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

View File

@@ -1,3 +1,5 @@
# English
# Author: Sebastian Schuermanns
'-LICENSE_AND_YOUR_WEBSITE_MUST_RUN_UNDER_THE_DOMAIN_OF_YOUR_LICENSE.': ''
'ACCESS_&_RIGHTS': ''
ACCESS_FOR: ''

View File

@@ -1,3 +1,5 @@
# French (Français)
# Author:
'-LICENSE_AND_YOUR_WEBSITE_MUST_RUN_UNDER_THE_DOMAIN_OF_YOUR_LICENSE.': ''
'ACCESS_&_RIGHTS': ''
ACCESS_FOR: ''

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
version: '2.0.3'
version: '2.1.0'
title: 'Typemill'
author: 'Unknown'
copyright: false

View File

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

View File

@@ -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)
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."

View File

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