summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Application.php1
-rw-r--r--src/Controllers/AuthController.php86
-rw-r--r--src/Controllers/BaseController.php4
-rw-r--r--src/Controllers/CreditsController.php13
-rw-r--r--src/Controllers/Metrics/Controller.php21
-rw-r--r--src/Controllers/Metrics/MetricsEngine.php18
-rw-r--r--src/Helpers/Authenticator.php93
-rw-r--r--src/Helpers/AuthenticatorServiceProvider.php4
-rw-r--r--src/Helpers/Translation/GettextTranslator.php53
-rw-r--r--src/Helpers/Translation/TranslationNotFound.php9
-rw-r--r--src/Helpers/Translation/TranslationServiceProvider.php86
-rw-r--r--src/Helpers/Translation/Translator.php (renamed from src/Helpers/Translator.php)77
-rw-r--r--src/Helpers/TranslationServiceProvider.php63
-rw-r--r--src/Helpers/Version.php42
-rw-r--r--src/Helpers/VersionServiceProvider.php15
-rw-r--r--src/Http/Exceptions/ValidationException.php37
-rw-r--r--src/Http/Validation/Rules/In.php21
-rw-r--r--src/Http/Validation/Rules/NotIn.php15
-rw-r--r--src/Http/Validation/ValidatesRequest.php37
-rw-r--r--src/Http/Validation/ValidationServiceProvider.php25
-rw-r--r--src/Http/Validation/Validator.php122
-rw-r--r--src/Middleware/ErrorHandler.php47
-rw-r--r--src/Middleware/LegacyMiddleware.php7
-rw-r--r--src/Middleware/SetLocale.php2
-rw-r--r--src/Renderer/Engine.php22
-rw-r--r--src/Renderer/EngineInterface.php10
-rw-r--r--src/Renderer/HtmlEngine.php8
-rw-r--r--src/Renderer/Twig/Extensions/Translation.php2
-rw-r--r--src/Renderer/TwigEngine.php8
-rw-r--r--src/helpers.php2
30 files changed, 815 insertions, 135 deletions
diff --git a/src/Application.php b/src/Application.php
index ac69c20a..99c68231 100644
--- a/src/Application.php
+++ b/src/Application.php
@@ -111,6 +111,7 @@ class Application extends Container
$this->instance('path.lang', $this->get('path.resources') . DIRECTORY_SEPARATOR . 'lang');
$this->instance('path.views', $this->get('path.resources') . DIRECTORY_SEPARATOR . 'views');
$this->instance('path.storage', $appPath . DIRECTORY_SEPARATOR . 'storage');
+ $this->instance('path.storage.app', $this->get('path.storage') . DIRECTORY_SEPARATOR . 'app');
$this->instance('path.cache', $this->get('path.storage') . DIRECTORY_SEPARATOR . 'cache');
$this->instance('path.cache.routes', $this->get('path.cache') . DIRECTORY_SEPARATOR . 'routes.cache.php');
$this->instance('path.cache.views', $this->get('path.cache') . DIRECTORY_SEPARATOR . 'views');
diff --git a/src/Controllers/AuthController.php b/src/Controllers/AuthController.php
index cdaee167..55dd56b0 100644
--- a/src/Controllers/AuthController.php
+++ b/src/Controllers/AuthController.php
@@ -2,8 +2,14 @@
namespace Engelsystem\Controllers;
+use Carbon\Carbon;
+use Engelsystem\Helpers\Authenticator;
+use Engelsystem\Http\Request;
use Engelsystem\Http\Response;
use Engelsystem\Http\UrlGeneratorInterface;
+use Engelsystem\Models\User\User;
+use Illuminate\Support\Arr;
+use Illuminate\Support\Collection;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
class AuthController extends BaseController
@@ -17,17 +23,91 @@ class AuthController extends BaseController
/** @var UrlGeneratorInterface */
protected $url;
- public function __construct(Response $response, SessionInterface $session, UrlGeneratorInterface $url)
- {
+ /** @var Authenticator */
+ protected $auth;
+
+ /** @var array */
+ protected $permissions = [
+ 'login' => 'login',
+ 'postLogin' => 'login',
+ ];
+
+ /**
+ * @param Response $response
+ * @param SessionInterface $session
+ * @param UrlGeneratorInterface $url
+ * @param Authenticator $auth
+ */
+ public function __construct(
+ Response $response,
+ SessionInterface $session,
+ UrlGeneratorInterface $url,
+ Authenticator $auth
+ ) {
$this->response = $response;
$this->session = $session;
$this->url = $url;
+ $this->auth = $auth;
+ }
+
+ /**
+ * @return Response
+ */
+ public function login(): Response
+ {
+ return $this->showLogin();
+ }
+
+ /**
+ * @param bool $showRecovery
+ * @return Response
+ */
+ protected function showLogin($showRecovery = false): Response
+ {
+ $errors = Collection::make(Arr::flatten($this->session->get('errors', [])));
+ $this->session->remove('errors');
+
+ return $this->response->withView(
+ 'pages/login',
+ ['errors' => $errors, 'show_password_recovery' => $showRecovery]
+ );
+ }
+
+ /**
+ * Posted login form
+ *
+ * @param Request $request
+ * @return Response
+ */
+ public function postLogin(Request $request): Response
+ {
+ $data = $this->validate($request, [
+ 'login' => 'required',
+ 'password' => 'required',
+ ]);
+
+ $user = $this->auth->authenticate($data['login'], $data['password']);
+
+ if (!$user instanceof User) {
+ $this->session->set('errors', $this->session->get('errors', []) + ['auth.not-found']);
+
+ return $this->showLogin(true);
+ }
+
+ $this->session->invalidate();
+ $this->session->set('user_id', $user->id);
+ $this->session->set('locale', $user->settings->language);
+
+ $user->last_login_at = new Carbon();
+ $user->save(['touch' => false]);
+
+ return $this->response->redirectTo('news');
}
/**
* @return Response
*/
- public function logout()
+ public function logout(): Response
{
$this->session->invalidate();
diff --git a/src/Controllers/BaseController.php b/src/Controllers/BaseController.php
index cbc00931..655ed759 100644
--- a/src/Controllers/BaseController.php
+++ b/src/Controllers/BaseController.php
@@ -2,8 +2,12 @@
namespace Engelsystem\Controllers;
+use Engelsystem\Http\Validation\ValidatesRequest;
+
abstract class BaseController
{
+ use ValidatesRequest;
+
/** @var string[]|string[][] A list of Permissions required to access the controller or certain pages */
protected $permissions = [];
diff --git a/src/Controllers/CreditsController.php b/src/Controllers/CreditsController.php
index b2805b84..ade97649 100644
--- a/src/Controllers/CreditsController.php
+++ b/src/Controllers/CreditsController.php
@@ -3,6 +3,7 @@
namespace Engelsystem\Controllers;
use Engelsystem\Config\Config;
+use Engelsystem\Helpers\Version;
use Engelsystem\Http\Response;
class CreditsController extends BaseController
@@ -13,14 +14,19 @@ class CreditsController extends BaseController
/** @var Response */
protected $response;
+ /** @var Version */
+ protected $version;
+
/**
* @param Response $response
* @param Config $config
+ * @param Version $version
*/
- public function __construct(Response $response, Config $config)
+ public function __construct(Response $response, Config $config, Version $version)
{
$this->config = $config;
$this->response = $response;
+ $this->version = $version;
}
/**
@@ -30,7 +36,10 @@ class CreditsController extends BaseController
{
return $this->response->withView(
'pages/credits.twig',
- ['credits' => $this->config->get('credits')]
+ [
+ 'credits' => $this->config->get('credits'),
+ 'version' => $this->version->getVersion(),
+ ]
);
}
}
diff --git a/src/Controllers/Metrics/Controller.php b/src/Controllers/Metrics/Controller.php
index f6ea3967..ffb2a41b 100644
--- a/src/Controllers/Metrics/Controller.php
+++ b/src/Controllers/Metrics/Controller.php
@@ -4,6 +4,7 @@ namespace Engelsystem\Controllers\Metrics;
use Engelsystem\Config\Config;
use Engelsystem\Controllers\BaseController;
+use Engelsystem\Helpers\Version;
use Engelsystem\Http\Exceptions\HttpForbidden;
use Engelsystem\Http\Request;
use Engelsystem\Http\Response;
@@ -26,25 +27,31 @@ class Controller extends BaseController
/** @var Stats */
protected $stats;
+ /** @var Version */
+ protected $version;
+
/**
* @param Response $response
* @param MetricsEngine $engine
* @param Config $config
* @param Request $request
* @param Stats $stats
+ * @param Version $version
*/
public function __construct(
Response $response,
MetricsEngine $engine,
Config $config,
Request $request,
- Stats $stats
+ Stats $stats,
+ Version $version
) {
$this->config = $config;
$this->engine = $engine;
$this->request = $request;
$this->response = $response;
$this->stats = $stats;
+ $this->version = $version;
}
/**
@@ -68,6 +75,18 @@ class Controller extends BaseController
$data = [
$this->config->get('app_name') . ' stats',
+ 'info' => [
+ 'type' => 'gauge',
+ 'help' => 'About the environment',
+ [
+ 'labels' => [
+ 'os' => PHP_OS_FAMILY,
+ 'php' => implode('.', [PHP_MAJOR_VERSION, PHP_MINOR_VERSION]),
+ 'version' => $this->version->getVersion(),
+ ],
+ 'value' => 1,
+ ],
+ ],
'users' => [
'type' => 'gauge',
['labels' => ['state' => 'incoming'], 'value' => $this->stats->newUsers()],
diff --git a/src/Controllers/Metrics/MetricsEngine.php b/src/Controllers/Metrics/MetricsEngine.php
index 1e0f6957..21ae8fd0 100644
--- a/src/Controllers/Metrics/MetricsEngine.php
+++ b/src/Controllers/Metrics/MetricsEngine.php
@@ -9,13 +9,13 @@ class MetricsEngine implements EngineInterface
/**
* Render metrics
*
- * @example $data = ['foo' => [['labels' => ['foo'=>'bar'], 'value'=>42]], 'bar'=>123]
- *
* @param string $path
* @param mixed[] $data
* @return string
+ *
+ * @example $data = ['foo' => [['labels' => ['foo'=>'bar'], 'value'=>42]], 'bar'=>123]
*/
- public function get($path, $data = []): string
+ public function get(string $path, array $data = []): string
{
$return = [];
foreach ($data as $name => $list) {
@@ -52,7 +52,7 @@ class MetricsEngine implements EngineInterface
* @param string $path
* @return bool
*/
- public function canRender($path): bool
+ public function canRender(string $path): bool
{
return $path == '/metrics';
}
@@ -60,8 +60,8 @@ class MetricsEngine implements EngineInterface
/**
* @param string $name
* @param array|mixed $row
- * @see https://prometheus.io/docs/instrumenting/exposition_formats/
* @return string
+ * @see https://prometheus.io/docs/instrumenting/exposition_formats/
*/
protected function formatData($name, $row): string
{
@@ -135,4 +135,12 @@ class MetricsEngine implements EngineInterface
$value
);
}
+
+ /**
+ * Does nothing as shared data will onyly result in unexpected behaviour
+ *
+ * @param string|mixed[] $key
+ * @param mixed $value
+ */
+ public function share($key, $value = null) { }
}
diff --git a/src/Helpers/Authenticator.php b/src/Helpers/Authenticator.php
index 61d07980..db33339b 100644
--- a/src/Helpers/Authenticator.php
+++ b/src/Helpers/Authenticator.php
@@ -25,6 +25,9 @@ class Authenticator
/** @var string[] */
protected $permissions;
+ /** @var int */
+ protected $passwordAlgorithm = PASSWORD_DEFAULT;
+
/**
* @param ServerRequestInterface $request
* @param Session $session
@@ -48,7 +51,7 @@ class Authenticator
return $this->user;
}
- $userId = $this->session->get('uid');
+ $userId = $this->session->get('user_id');
if (!$userId) {
return null;
}
@@ -104,17 +107,15 @@ class Authenticator
$abilities = (array)$abilities;
if (empty($this->permissions)) {
- $userId = $this->user ? $this->user->id : $this->session->get('uid');
+ $user = $this->user();
- if ($userId) {
- if ($user = $this->user()) {
- $this->permissions = $this->getPermissionsByUser($user);
+ if ($user) {
+ $this->permissions = $this->getPermissionsByUser($user);
- $user->last_login_at = new Carbon();
- $user->save();
- } else {
- $this->session->remove('uid');
- }
+ $user->last_login_at = new Carbon();
+ $user->save();
+ } elseif ($this->session->get('user_id')) {
+ $this->session->remove('user_id');
}
if (empty($this->permissions)) {
@@ -132,6 +133,78 @@ class Authenticator
}
/**
+ * @param string $login
+ * @param string $password
+ * @return User|null
+ */
+ public function authenticate(string $login, string $password)
+ {
+ /** @var User $user */
+ $user = $this->userRepository->whereName($login)->first();
+ if (!$user) {
+ $user = $this->userRepository->whereEmail($login)->first();
+ }
+
+ if (!$user) {
+ return null;
+ }
+
+ if (!$this->verifyPassword($user, $password)) {
+ return null;
+ }
+
+ return $user;
+ }
+
+ /**
+ * @param User $user
+ * @param string $password
+ * @return bool
+ */
+ public function verifyPassword(User $user, string $password)
+ {
+ $algorithm = $this->passwordAlgorithm;
+
+ if (!password_verify($password, $user->password)) {
+ return false;
+ }
+
+ if (password_needs_rehash($user->password, $algorithm)) {
+ $this->setPassword($user, $password);
+ }
+
+ return true;
+ }
+
+ /**
+ * @param UserRepository $user
+ * @param string $password
+ */
+ public function setPassword(User $user, string $password)
+ {
+ $algorithm = $this->passwordAlgorithm;
+
+ $user->password = password_hash($password, $algorithm);
+ $user->save();
+ }
+
+ /**
+ * @return int
+ */
+ public function getPasswordAlgorithm()
+ {
+ return $this->passwordAlgorithm;
+ }
+
+ /**
+ * @param int $passwordAlgorithm
+ */
+ public function setPasswordAlgorithm(int $passwordAlgorithm)
+ {
+ $this->passwordAlgorithm = $passwordAlgorithm;
+ }
+
+ /**
* @param User $user
* @return array
* @codeCoverageIgnore
diff --git a/src/Helpers/AuthenticatorServiceProvider.php b/src/Helpers/AuthenticatorServiceProvider.php
index 715a592f..f06e635d 100644
--- a/src/Helpers/AuthenticatorServiceProvider.php
+++ b/src/Helpers/AuthenticatorServiceProvider.php
@@ -2,14 +2,18 @@
namespace Engelsystem\Helpers;
+use Engelsystem\Config\Config;
use Engelsystem\Container\ServiceProvider;
class AuthenticatorServiceProvider extends ServiceProvider
{
public function register()
{
+ /** @var Config $config */
+ $config = $this->app->get('config');
/** @var Authenticator $authenticator */
$authenticator = $this->app->make(Authenticator::class);
+ $authenticator->setPasswordAlgorithm($config->get('password_algorithm'));
$this->app->instance(Authenticator::class, $authenticator);
$this->app->instance('authenticator', $authenticator);
diff --git a/src/Helpers/Translation/GettextTranslator.php b/src/Helpers/Translation/GettextTranslator.php
new file mode 100644
index 00000000..7f2299e2
--- /dev/null
+++ b/src/Helpers/Translation/GettextTranslator.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace Engelsystem\Helpers\Translation;
+
+use Gettext\Translator;
+
+class GettextTranslator extends Translator
+{
+ /**
+ * @param string $domain
+ * @param string $context
+ * @param string $original
+ * @return string
+ * @throws TranslationNotFound
+ */
+ public function dpgettext($domain, $context, $original)
+ {
+ $this->assertHasTranslation($domain, $context, $original);
+
+ return parent::dpgettext($domain, $context, $original);
+ }
+
+ /**
+ * @param string $domain
+ * @param string $context
+ * @param string $original
+ * @param string $plural
+ * @param string $value
+ * @return string
+ * @throws TranslationNotFound
+ */
+ public function dnpgettext($domain, $context, $original, $plural, $value)
+ {
+ $this->assertHasTranslation($domain, $context, $original);
+
+ return parent::dnpgettext($domain, $context, $original, $plural, $value);
+ }
+
+ /**
+ * @param string $domain
+ * @param string $context
+ * @param string $original
+ * @throws TranslationNotFound
+ */
+ protected function assertHasTranslation($domain, $context, $original)
+ {
+ if ($this->getTranslation($domain, $context, $original)) {
+ return;
+ }
+
+ throw new TranslationNotFound(implode('/', [$domain, $context, $original]));
+ }
+}
diff --git a/src/Helpers/Translation/TranslationNotFound.php b/src/Helpers/Translation/TranslationNotFound.php
new file mode 100644
index 00000000..1552838b
--- /dev/null
+++ b/src/Helpers/Translation/TranslationNotFound.php
@@ -0,0 +1,9 @@
+<?php
+
+namespace Engelsystem\Helpers\Translation;
+
+use Exception;
+
+class TranslationNotFound extends Exception
+{
+}
diff --git a/src/Helpers/Translation/TranslationServiceProvider.php b/src/Helpers/Translation/TranslationServiceProvider.php
new file mode 100644
index 00000000..09337dad
--- /dev/null
+++ b/src/Helpers/Translation/TranslationServiceProvider.php
@@ -0,0 +1,86 @@
+<?php
+
+namespace Engelsystem\Helpers\Translation;
+
+use Engelsystem\Config\Config;
+use Engelsystem\Container\ServiceProvider;
+use Gettext\Translations;
+use Symfony\Component\HttpFoundation\Session\Session;
+
+class TranslationServiceProvider extends ServiceProvider
+{
+ /** @var GettextTranslator */
+ protected $translators = [];
+
+ public function register(): void
+ {
+ /** @var Config $config */
+ $config = $this->app->get('config');
+ /** @var Session $session */
+ $session = $this->app->get('session');
+
+ $locales = $config->get('locales');
+ $locale = $config->get('default_locale');
+ $fallbackLocale = $config->get('fallback_locale', 'en_US');
+
+ $sessionLocale = $session->get('locale', $locale);
+ if (isset($locales[$sessionLocale])) {
+ $locale = $sessionLocale;
+ }
+
+ $session->set('locale', $locale);
+
+ $translator = $this->app->make(
+ Translator::class,
+ [
+ 'locale' => $locale,
+ 'locales' => $locales,
+ 'fallbackLocale' => $fallbackLocale,
+ 'getTranslatorCallback' => [$this, 'getTranslator'],
+ 'localeChangeCallback' => [$this, 'setLocale'],
+ ]
+ );
+ $this->app->instance(Translator::class, $translator);
+ $this->app->instance('translator', $translator);
+ }
+
+ /**
+ * @param string $locale
+ * @codeCoverageIgnore
+ */
+ public function setLocale(string $locale): void
+ {
+ $locale .= '.UTF-8';
+ // Set the users locale
+ putenv('LC_ALL=' . $locale);
+ setlocale(LC_ALL, $locale);
+
+ // Reset numeric formatting to allow output of floats
+ putenv('LC_NUMERIC=C');
+ setlocale(LC_NUMERIC, 'C');
+ }
+
+ /**
+ * @param string $locale
+ * @return GettextTranslator
+ */
+ public function getTranslator(string $locale): GettextTranslator
+ {
+ if (!isset($this->translators[$locale])) {
+ $file = $this->app->get('path.lang') . '/' . $locale . '/default.mo';
+
+ /** @var GettextTranslator $translator */
+ $translator = $this->app->make(GettextTranslator::class);
+
+ /** @var Translations $translations */
+ $translations = $this->app->make(Translations::class);
+ $translations->addFromMoFile($file);
+
+ $translator->loadTranslations($translations);
+
+ $this->translators[$locale] = $translator;
+ }
+
+ return $this->translators[$locale];
+ }
+}
diff --git a/src/Helpers/Translator.php b/src/Helpers/Translation/Translator.php
index 94fbd795..8b11ecb4 100644
--- a/src/Helpers/Translator.php
+++ b/src/Helpers/Translation/Translator.php
@@ -1,6 +1,6 @@
<?php
-namespace Engelsystem\Helpers;
+namespace Engelsystem\Helpers\Translation;
class Translator
{
@@ -10,6 +10,12 @@ class Translator
/** @var string */
protected $locale;
+ /** @var string */
+ protected $fallbackLocale;
+
+ /** @var callable */
+ protected $getTranslatorCallback;
+
/** @var callable */
protected $localeChangeCallback;
@@ -17,15 +23,24 @@ class Translator
* Translator constructor.
*
* @param string $locale
+ * @param string $fallbackLocale
+ * @param callable $getTranslatorCallback
* @param string[] $locales
* @param callable $localeChangeCallback
*/
- public function __construct(string $locale, array $locales = [], callable $localeChangeCallback = null)
- {
+ public function __construct(
+ string $locale,
+ string $fallbackLocale,
+ callable $getTranslatorCallback,
+ array $locales = [],
+ callable $localeChangeCallback = null
+ ) {
$this->localeChangeCallback = $localeChangeCallback;
+ $this->getTranslatorCallback = $getTranslatorCallback;
$this->setLocale($locale);
- $this->setLocales($locales);
+ $this->fallbackLocale = $fallbackLocale;
+ $this->locales = $locales;
}
/**
@@ -37,9 +52,7 @@ class Translator
*/
public function translate(string $key, array $replace = []): string
{
- $translated = $this->translateGettext($key);
-
- return $this->replaceText($translated, $replace);
+ return $this->translateText('gettext', [$key], $replace);
}
/**
@@ -53,7 +66,29 @@ class Translator
*/
public function translatePlural(string $key, string $pluralKey, int $number, array $replace = []): string
{
- $translated = $this->translateGettextPlural($key, $pluralKey, $number);
+ return $this->translateText('ngettext', [$key, $pluralKey, $number], $replace);
+ }
+
+ /**
+ * @param string $type
+ * @param array $parameters
+ * @param array $replace
+ * @return mixed|string
+ */
+ protected function translateText(string $type, array $parameters, array $replace = [])
+ {
+ $translated = $parameters[0];
+
+ foreach ([$this->locale, $this->fallbackLocale] as $lang) {
+ /** @var GettextTranslator $translator */
+ $translator = call_user_func($this->getTranslatorCallback, $lang);
+
+ try {
+ $translated = call_user_func_array([$translator, $type], $parameters);
+ break;
+ } catch (TranslationNotFound $e) {
+ }
+ }
return $this->replaceText($translated, $replace);
}
@@ -75,32 +110,6 @@ class Translator
}
/**
- * Translate the key via gettext
- *
- * @param string $key
- * @return string
- * @codeCoverageIgnore
- */
- protected function translateGettext(string $key): string
- {
- return gettext($key);
- }
-
- /**
- * Translate the key via gettext
- *
- * @param string $key
- * @param string $keyPlural
- * @param int $number
- * @return string
- * @codeCoverageIgnore
- */
- protected function translateGettextPlural(string $key, string $keyPlural, int $number): string
- {
- return ngettext($key, $keyPlural, $number);
- }
-
- /**
* @return string
*/
public function getLocale(): string
diff --git a/src/Helpers/TranslationServiceProvider.php b/src/Helpers/TranslationServiceProvider.php
deleted file mode 100644
index 4565dfcd..00000000
--- a/src/Helpers/TranslationServiceProvider.php
+++ /dev/null
@@ -1,63 +0,0 @@
-<?php
-
-namespace Engelsystem\Helpers;
-
-use Engelsystem\Config\Config;
-use Engelsystem\Container\ServiceProvider;
-use Symfony\Component\HttpFoundation\Session\Session;
-
-class TranslationServiceProvider extends ServiceProvider
-{
- public function register()
- {
- /** @var Config $config */
- $config = $this->app->get('config');
- /** @var Session $session */
- $session = $this->app->get('session');
-
- $locales = $config->get('locales');
- $locale = $config->get('default_locale');
-
- $sessionLocale = $session->get('locale', $locale);
- if (isset($locales[$sessionLocale])) {
- $locale = $sessionLocale;
- }
-
- $this->initGettext();
- $session->set('locale', $locale);
-
- $translator = $this->app->make(
- Translator::class,
- ['locale' => $locale, 'locales' => $locales, 'localeChangeCallback' => [$this, 'setLocale']]
- );
- $this->app->instance(Translator::class, $translator);
- $this->app->instance('translator', $translator);
- }
-
- /**
- * @param string $textDomain
- * @param string $encoding
- * @codeCoverageIgnore
- */
- protected function initGettext($textDomain = 'default', $encoding = 'UTF-8')
- {
- bindtextdomain($textDomain, $this->app->get('path.lang'));
- bind_textdomain_codeset($textDomain, $encoding);
- textdomain($textDomain);
- }
-
- /**
- * @param string $locale
- * @codeCoverageIgnore
- */
- public function setLocale($locale)
- {
- // Set the users locale
- putenv('LC_ALL=' . $locale);
- setlocale(LC_ALL, $locale);
-
- // Reset numeric formatting to allow output of floats
- putenv('LC_NUMERIC=C');
- setlocale(LC_NUMERIC, 'C');
- }
-}
diff --git a/src/Helpers/Version.php b/src/Helpers/Version.php
new file mode 100644
index 00000000..97fe6ef3
--- /dev/null
+++ b/src/Helpers/Version.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace Engelsystem\Helpers;
+
+use Engelsystem\Config\Config;
+
+class Version
+{
+ /** @var Config */
+ protected $config;
+
+ /** @vat string */
+ protected $storage;
+
+ /** @var string */
+ protected $versionFile = 'VERSION';
+
+ /**
+ * @param string $storage
+ * @param Config $config
+ */
+ public function __construct(string $storage, Config $config)
+ {
+ $this->storage = $storage;
+ $this->config = $config;
+ }
+
+ /**
+ * @return string
+ */
+ public function getVersion()
+ {
+ $file = $this->storage . DIRECTORY_SEPARATOR . $this->versionFile;
+
+ $version = 'n/a';
+ if (file_exists($file)) {
+ $version = trim(file_get_contents($file));
+ }
+
+ return $this->config->get('version', $version);
+ }
+}
diff --git a/src/Helpers/VersionServiceProvider.php b/src/Helpers/VersionServiceProvider.php
new file mode 100644
index 00000000..41e10158
--- /dev/null
+++ b/src/Helpers/VersionServiceProvider.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Engelsystem\Helpers;
+
+use Engelsystem\Container\ServiceProvider;
+
+class VersionServiceProvider extends ServiceProvider
+{
+ public function register()
+ {
+ $this->app->when(Version::class)
+ ->needs('$storage')
+ ->give($this->app->get('path.storage.app'));
+ }
+}
diff --git a/src/Http/Exceptions/ValidationException.php b/src/Http/Exceptions/ValidationException.php
new file mode 100644
index 00000000..e48fb0c3
--- /dev/null
+++ b/src/Http/Exceptions/ValidationException.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Engelsystem\Http\Exceptions;
+
+use Engelsystem\Http\Validation\Validator;
+use RuntimeException;
+use Throwable;
+
+class ValidationException extends RuntimeException
+{
+ /** @var Validator */
+ protected $validator;
+
+ /**
+ * @param Validator $validator
+ * @param string $message
+ * @param int $code
+ * @param Throwable|null $previous
+ */
+ public function __construct(
+ Validator $validator,
+ string $message = '',
+ int $code = 0,
+ Throwable $previous = null
+ ) {
+ $this->validator = $validator;
+ parent::__construct($message, $code, $previous);
+ }
+
+ /**
+ * @return Validator
+ */
+ public function getValidator(): Validator
+ {
+ return $this->validator;
+ }
+}
diff --git a/src/Http/Validation/Rules/In.php b/src/Http/Validation/Rules/In.php
new file mode 100644
index 00000000..d585cc3d
--- /dev/null
+++ b/src/Http/Validation/Rules/In.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace Engelsystem\Http\Validation\Rules;
+
+use Respect\Validation\Rules\In as RespectIn;
+
+class In extends RespectIn
+{
+ /**
+ * @param mixed $haystack
+ * @param bool $compareIdentical
+ */
+ public function __construct($haystack, $compareIdentical = false)
+ {
+ if (!is_array($haystack)) {
+ $haystack = explode(',', $haystack);
+ }
+
+ parent::__construct($haystack, $compareIdentical);
+ }
+}
diff --git a/src/Http/Validation/Rules/NotIn.php b/src/Http/Validation/Rules/NotIn.php
new file mode 100644
index 00000000..7f223c42
--- /dev/null
+++ b/src/Http/Validation/Rules/NotIn.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Engelsystem\Http\Validation\Rules;
+
+class NotIn extends In
+{
+ /**
+ * @param mixed $input
+ * @return bool
+ */
+ public function validate($input)
+ {
+ return !parent::validate($input);
+ }
+}
diff --git a/src/Http/Validation/ValidatesRequest.php b/src/Http/Validation/ValidatesRequest.php
new file mode 100644
index 00000000..33ff76af
--- /dev/null
+++ b/src/Http/Validation/ValidatesRequest.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Engelsystem\Http\Validation;
+
+use Engelsystem\Http\Exceptions\ValidationException;
+use Engelsystem\Http\Request;
+
+trait ValidatesRequest
+{
+ /** @var Validator */
+ protected $validator;
+
+ /**
+ * @param Request $request
+ * @param array $rules
+ * @return array
+ */
+ protected function validate(Request $request, array $rules)
+ {
+ if (!$this->validator->validate(
+ (array)$request->getParsedBody(),
+ $rules
+ )) {
+ throw new ValidationException($this->validator);
+ }
+
+ return $this->validator->getData();
+ }
+
+ /**
+ * @param Validator $validator
+ */
+ public function setValidator(Validator $validator)
+ {
+ $this->validator = $validator;
+ }
+}
diff --git a/src/Http/Validation/ValidationServiceProvider.php b/src/Http/Validation/ValidationServiceProvider.php
new file mode 100644
index 00000000..14530ae6
--- /dev/null
+++ b/src/Http/Validation/ValidationServiceProvider.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Engelsystem\Http\Validation;
+
+use Engelsystem\Application;
+use Engelsystem\Container\ServiceProvider;
+use Engelsystem\Controllers\BaseController;
+
+class ValidationServiceProvider extends ServiceProvider
+{
+ public function register()
+ {
+ $validator = $this->app->make(Validator::class);
+ $this->app->instance(Validator::class, $validator);
+ $this->app->instance('validator', $validator);
+
+ $this->app->afterResolving(function ($object, Application $app) {
+ if (!$object instanceof BaseController) {
+ return;
+ }
+
+ $object->setValidator($app->get(Validator::class));
+ });
+ }
+}
diff --git a/src/Http/Validation/Validator.php b/src/Http/Validation/Validator.php
new file mode 100644
index 00000000..976f5682
--- /dev/null
+++ b/src/Http/Validation/Validator.php
@@ -0,0 +1,122 @@
+<?php
+
+namespace Engelsystem\Http\Validation;
+
+use Illuminate\Support\Str;
+use InvalidArgumentException;
+use Respect\Validation\Exceptions\ComponentException;
+use Respect\Validation\Validator as RespectValidator;
+
+class Validator
+{
+ /** @var string[] */
+ protected $errors = [];
+
+ /** @var array */
+ protected $data = [];
+
+ /** @var array */
+ protected $mapping = [
+ 'accepted' => 'TrueVal',
+ 'int' => 'IntVal',
+ 'required' => 'NotEmpty',
+ ];
+
+ /** @var array */
+ protected $nestedRules = ['optional', 'not'];
+
+ /**
+ * @param array $data
+ * @param array $rules
+ * @return bool
+ */
+ public function validate($data, $rules)
+ {
+ $this->errors = [];
+ $this->data = [];
+
+ foreach ($rules as $key => $values) {
+ $v = new RespectValidator();
+ $v->with('\\Engelsystem\\Http\\Validation\\Rules', true);
+
+ $value = isset($data[$key]) ? $data[$key] : null;
+ $values = explode('|', $values);
+
+ $packing = [];
+ foreach ($this->nestedRules as $rule) {
+ if (in_array($rule, $values)) {
+ $packing[] = $rule;
+ }
+ }
+
+ $values = array_diff($values, $this->nestedRules);
+ foreach ($values as $parameters) {
+ $parameters = explode(':', $parameters);
+ $rule = array_shift($parameters);
+ $rule = Str::camel($rule);
+ $rule = $this->map($rule);
+
+ // To allow rules nesting
+ $w = $v;
+ try {
+ foreach (array_reverse(array_merge($packing, [$rule])) as $rule) {
+ if (!in_array($rule, $this->nestedRules)) {
+ call_user_func_array([$w, $rule], $parameters);
+ continue;
+ }
+
+ $w = call_user_func_array([new RespectValidator(), $rule], [$w]);
+ }
+ } catch (ComponentException $e) {
+ throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
+ }
+
+ if ($w->validate($value)) {
+ $this->data[$key] = $value;
+ } else {
+ $this->errors[$key][] = implode('.', ['validation', $key, $this->mapBack($rule)]);
+ }
+
+ $v->removeRules();
+ }
+ }
+
+ return empty($this->errors);
+ }
+
+ /**
+ * @param string $rule
+ * @return string
+ */
+ protected function map($rule)
+ {
+ return $this->mapping[$rule] ?? $rule;
+ }
+
+ /**
+ * @param string $rule
+ * @return string
+ */
+ protected function mapBack($rule)
+ {
+ $mapping = array_flip($this->mapping);
+
+ return $mapping[$rule] ?? $rule;
+ }
+
+ /**
+ * @return array
+ */
+ public function getData(): array
+ {
+ return $this->data;
+ }
+
+ /**
+ * @return string[]
+ */
+ public function getErrors(): array
+ {
+ return $this->errors;
+ }
+}
diff --git a/src/Middleware/ErrorHandler.php b/src/Middleware/ErrorHandler.php
index 29b1fac1..65e2e609 100644
--- a/src/Middleware/ErrorHandler.php
+++ b/src/Middleware/ErrorHandler.php
@@ -3,7 +3,10 @@
namespace Engelsystem\Middleware;
use Engelsystem\Http\Exceptions\HttpException;
+use Engelsystem\Http\Exceptions\ValidationException;
+use Engelsystem\Http\Request;
use Engelsystem\Http\Response;
+use Illuminate\Support\Arr;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
@@ -19,6 +22,22 @@ class ErrorHandler implements MiddlewareInterface
protected $viewPrefix = 'errors/';
/**
+ * A list of inputs that are not saved from form input
+ *
+ * @var array
+ */
+ protected $formIgnore = [
+ 'password',
+ 'password_confirmation',
+ 'password2',
+ 'new_password',
+ 'new_password2',
+ 'new_pw',
+ 'new_pw2',
+ '_token',
+ ];
+
+ /**
* @param TwigLoader $loader
*/
public function __construct(TwigLoader $loader)
@@ -43,6 +62,21 @@ class ErrorHandler implements MiddlewareInterface
$response = $handler->handle($request);
} catch (HttpException $e) {
$response = $this->createResponse($e->getMessage(), $e->getStatusCode(), $e->getHeaders());
+ } catch (ValidationException $e) {
+ $response = $this->createResponse('', 302, ['Location' => $this->getPreviousUrl($request)]);
+
+ if ($request instanceof Request) {
+ $session = $request->getSession();
+ $session->set(
+ 'errors',
+ array_merge_recursive(
+ $session->get('errors', []),
+ ['validation' => $e->getValidator()->getErrors()]
+ )
+ );
+
+ $session->set('form-data', Arr::except($request->request->all(), $this->formIgnore));
+ }
}
$statusCode = $response->getStatusCode();
@@ -106,4 +140,17 @@ class ErrorHandler implements MiddlewareInterface
{
return response($content, $status, $headers);
}
+
+ /**
+ * @param ServerRequestInterface $request
+ * @return string
+ */
+ protected function getPreviousUrl(ServerRequestInterface $request)
+ {
+ if ($header = $request->getHeader('referer')) {
+ return array_pop($header);
+ }
+
+ return '/';
+ }
}
diff --git a/src/Middleware/LegacyMiddleware.php b/src/Middleware/LegacyMiddleware.php
index af2c6a70..27a15faa 100644
--- a/src/Middleware/LegacyMiddleware.php
+++ b/src/Middleware/LegacyMiddleware.php
@@ -3,7 +3,7 @@
namespace Engelsystem\Middleware;
use Engelsystem\Helpers\Authenticator;
-use Engelsystem\Helpers\Translator;
+use Engelsystem\Helpers\Translation\Translator;
use Engelsystem\Http\Request;
use Engelsystem\Http\Response;
use Psr\Container\ContainerInterface;
@@ -19,7 +19,6 @@ class LegacyMiddleware implements MiddlewareInterface
'angeltypes',
'atom',
'ical',
- 'login',
'public_dashboard',
'rooms',
'shift_entries',
@@ -175,10 +174,6 @@ class LegacyMiddleware implements MiddlewareInterface
$title = settings_title();
$content = user_settings();
return [$title, $content];
- case 'login':
- $title = login_title();
- $content = guest_login();
- return [$title, $content];
case 'register':
$title = register_title();
$content = guest_register();
diff --git a/src/Middleware/SetLocale.php b/src/Middleware/SetLocale.php
index 86fa0b7f..568adbe6 100644
--- a/src/Middleware/SetLocale.php
+++ b/src/Middleware/SetLocale.php
@@ -2,7 +2,7 @@
namespace Engelsystem\Middleware;
-use Engelsystem\Helpers\Translator;
+use Engelsystem\Helpers\Translation\Translator;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
diff --git a/src/Renderer/Engine.php b/src/Renderer/Engine.php
new file mode 100644
index 00000000..60f1d686
--- /dev/null
+++ b/src/Renderer/Engine.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Engelsystem\Renderer;
+
+abstract class Engine implements EngineInterface
+{
+ /** @var array */
+ protected $sharedData = [];
+
+ /**
+ * @param mixed[]|string $key
+ * @param null $value
+ */
+ public function share($key, $value = null)
+ {
+ if (!is_array($key)) {
+ $key = [$key => $value];
+ }
+
+ $this->sharedData = array_replace_recursive($this->sharedData, $key);
+ }
+}
diff --git a/src/Renderer/EngineInterface.php b/src/Renderer/EngineInterface.php
index ca468db5..3bce9c02 100644
--- a/src/Renderer/EngineInterface.php
+++ b/src/Renderer/EngineInterface.php
@@ -11,11 +11,17 @@ interface EngineInterface
* @param mixed[] $data
* @return string
*/
- public function get($path, $data = []);
+ public function get(string $path, array $data = []): string;
/**
* @param string $path
* @return bool
*/
- public function canRender($path);
+ public function canRender(string $path): bool;
+
+ /**
+ * @param string|mixed[] $key
+ * @param mixed $value
+ */
+ public function share($key, $value = null);
}
diff --git a/src/Renderer/HtmlEngine.php b/src/Renderer/HtmlEngine.php
index 1feafcda..0ccffa65 100644
--- a/src/Renderer/HtmlEngine.php
+++ b/src/Renderer/HtmlEngine.php
@@ -2,7 +2,7 @@
namespace Engelsystem\Renderer;
-class HtmlEngine implements EngineInterface
+class HtmlEngine extends Engine
{
/**
* Render a template
@@ -11,9 +11,11 @@ class HtmlEngine implements EngineInterface
* @param mixed[] $data
* @return string
*/
- public function get($path, $data = [])
+ public function get(string $path, array $data = []): string
{
+ $data = array_replace_recursive($this->sharedData, $data);
$template = file_get_contents($path);
+
if (is_array($data)) {
foreach ($data as $name => $content) {
$template = str_replace('%' . $name . '%', $content, $template);
@@ -27,7 +29,7 @@ class HtmlEngine implements EngineInterface
* @param string $path
* @return bool
*/
- public function canRender($path)
+ public function canRender(string $path): bool
{
return mb_strpos($path, '.htm') !== false && file_exists($path);
}
diff --git a/src/Renderer/Twig/Extensions/Translation.php b/src/Renderer/Twig/Extensions/Translation.php
index 41619c19..3e6f30b4 100644
--- a/src/Renderer/Twig/Extensions/Translation.php
+++ b/src/Renderer/Twig/Extensions/Translation.php
@@ -2,7 +2,7 @@
namespace Engelsystem\Renderer\Twig\Extensions;
-use Engelsystem\Helpers\Translator;
+use Engelsystem\Helpers\Translation\Translator;
use Twig_Extension as TwigExtension;
use Twig_Extensions_TokenParser_Trans as TranslationTokenParser;
use Twig_Filter as TwigFilter;
diff --git a/src/Renderer/TwigEngine.php b/src/Renderer/TwigEngine.php
index 55a2e299..aa51a177 100644
--- a/src/Renderer/TwigEngine.php
+++ b/src/Renderer/TwigEngine.php
@@ -7,7 +7,7 @@ use Twig_Error_Loader as LoaderError;
use Twig_Error_Runtime as RuntimeError;
use Twig_Error_Syntax as SyntaxError;
-class TwigEngine implements EngineInterface
+class TwigEngine extends Engine
{
/** @var Twig */
protected $twig;
@@ -25,8 +25,10 @@ class TwigEngine implements EngineInterface
* @return string
* @throws LoaderError|RuntimeError|SyntaxError
*/
- public function get($path, $data = [])
+ public function get(string $path, array $data = []): string
{
+ $data = array_replace_recursive($this->sharedData, $data);
+
return $this->twig->render($path, $data);
}
@@ -34,7 +36,7 @@ class TwigEngine implements EngineInterface
* @param string $path
* @return bool
*/
- public function canRender($path)
+ public function canRender(string $path): bool
{
return $this->twig->getLoader()->exists($path);
}
diff --git a/src/helpers.php b/src/helpers.php
index 111141e4..051b78a3 100644
--- a/src/helpers.php
+++ b/src/helpers.php
@@ -4,7 +4,7 @@
use Engelsystem\Application;
use Engelsystem\Config\Config;
use Engelsystem\Helpers\Authenticator;
-use Engelsystem\Helpers\Translator;
+use Engelsystem\Helpers\Translation\Translator;
use Engelsystem\Http\Request;
use Engelsystem\Http\Response;
use Engelsystem\Http\UrlGeneratorInterface;