summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIgor Scheller <igor.scheller@igorshp.de>2019-07-21 13:42:19 +0200
committerGitHub <noreply@github.com>2019-07-21 13:42:19 +0200
commit43fa21f6553bb9e46f00d0768eab56e74c842bb4 (patch)
tree00df255fcd7c3777855bee27967117440c4add8c
parent4f60daa29568a43ab1da761d89124308dc7b37e7 (diff)
parent1e16f4c47ec15ffea41e1fa612d205b8db2a16d1 (diff)
Merge branch 'master' into docker
-rw-r--r--.gitlab-ci.yml6
-rw-r--r--README.md1
-rw-r--r--composer.json3
-rw-r--r--config/app.php4
-rw-r--r--config/config.default.php15
-rw-r--r--config/routes.php2
-rw-r--r--contrib/Dockerfile7
-rw-r--r--db/migrations/2018_10_01_000000_create_users_tables.php2
-rw-r--r--db/migrations/2019_06_12_000000_fix_user_languages.php34
-rw-r--r--includes/controller/users_controller.php13
-rw-r--r--includes/helper/email_helper.php3
-rw-r--r--includes/pages/admin_user.php2
-rw-r--r--includes/pages/guest_login.php119
-rw-r--r--includes/pages/user_settings.php5
-rw-r--r--includes/sys_auth.php68
-rw-r--r--includes/view/AngelTypes_view.php2
-rw-r--r--includes/view/User_view.php2
-rw-r--r--resources/lang/de_DE.UTF-8/LC_MESSAGES/default.mobin46271 -> 0 bytes
-rw-r--r--resources/lang/de_DE/default.mobin0 -> 46206 bytes
-rw-r--r--resources/lang/de_DE/default.po (renamed from resources/lang/de_DE.UTF-8/LC_MESSAGES/default.po)33
-rw-r--r--resources/lang/en_US/default.mobin0 -> 770 bytes
-rw-r--r--resources/lang/en_US/default.po32
-rw-r--r--resources/lang/pt_BR.UTF.8/LC_MESSAGES/pt_BR.mobin41256 -> 0 bytes
-rw-r--r--resources/lang/pt_BR/default.mobin0 -> 41129 bytes
-rw-r--r--resources/lang/pt_BR/default.po (renamed from resources/lang/pt_BR.UTF.8/LC_MESSAGES/pt_BR.po)14
-rw-r--r--resources/views/errors/405.twig5
-rw-r--r--resources/views/macros/base.twig11
-rw-r--r--resources/views/pages/credits.twig1
-rw-r--r--resources/views/pages/login.twig104
-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
-rw-r--r--storage/app/.gitignore2
-rw-r--r--tests/Unit/Controllers/AuthControllerTest.php142
-rw-r--r--tests/Unit/Controllers/BaseControllerTest.php2
-rw-r--r--tests/Unit/Controllers/CreditsControllerTest.php16
-rw-r--r--tests/Unit/Controllers/Metrics/ControllerTest.php22
-rw-r--r--tests/Unit/Controllers/Metrics/MetricsEngineTest.php11
-rw-r--r--tests/Unit/Controllers/Stub/ControllerImplementation.php8
-rw-r--r--tests/Unit/Helpers/AuthenticatorServiceProviderTest.php9
-rw-r--r--tests/Unit/Helpers/AuthenticatorTest.php125
-rw-r--r--tests/Unit/Helpers/Stub/files/VERSION1
-rw-r--r--tests/Unit/Helpers/Translation/Assets/fo_OO/default.mobin0 -> 73 bytes
-rw-r--r--tests/Unit/Helpers/Translation/Assets/fo_OO/default.po3
-rw-r--r--tests/Unit/Helpers/Translation/GettextTranslatorTest.php67
-rw-r--r--tests/Unit/Helpers/Translation/TranslationServiceProviderTest.php (renamed from tests/Unit/Helpers/TranslationServiceProviderTest.php)62
-rw-r--r--tests/Unit/Helpers/Translation/TranslatorTest.php134
-rw-r--r--tests/Unit/Helpers/TranslatorTest.php90
-rw-r--r--tests/Unit/Helpers/VersionServiceProviderTest.php25
-rw-r--r--tests/Unit/Helpers/VersionTest.php28
-rw-r--r--tests/Unit/HelpersTest.php2
-rw-r--r--tests/Unit/Http/Exceptions/ValidationExceptionTest.php25
-rw-r--r--tests/Unit/Http/UrlGeneratorServiceProviderTest.php5
-rw-r--r--tests/Unit/Http/Validation/Rules/InTest.php19
-rw-r--r--tests/Unit/Http/Validation/Rules/NotInTest.php20
-rw-r--r--tests/Unit/Http/Validation/Stub/ValidatesRequestImplementation.php27
-rw-r--r--tests/Unit/Http/Validation/ValidatesRequestTest.php46
-rw-r--r--tests/Unit/Http/Validation/ValidationServiceProviderTest.php34
-rw-r--r--tests/Unit/Http/Validation/ValidatorTest.php142
-rw-r--r--tests/Unit/Middleware/ErrorHandlerTest.php74
-rw-r--r--tests/Unit/Middleware/LegacyMiddlewareTest.php2
-rw-r--r--tests/Unit/Middleware/SetLocaleTest.php2
-rw-r--r--tests/Unit/Renderer/EngineTest.php25
-rw-r--r--tests/Unit/Renderer/HtmlEngineTest.php5
-rw-r--r--tests/Unit/Renderer/Stub/EngineImplementation.php32
-rw-r--r--tests/Unit/Renderer/Twig/Extensions/TranslationTest.php2
-rw-r--r--tests/Unit/Renderer/TwigEngineTest.php12
94 files changed, 2119 insertions, 540 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 7b0a8043..4e8bd7d9 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -46,7 +46,9 @@ build-image:
<<: *docker_definition
stage: build
script:
- - docker build --pull -t "${TEST_IMAGE}" -f contrib/Dockerfile .
+ - apk -q add git
+ - VERSION="$(git describe --abbrev=0 --tags)-${CI_COMMIT_REF_NAME}+${CI_PIPELINE_ID}.${CI_COMMIT_SHORT_SHA}"
+ - docker build --pull --build-arg VERSION="${VERSION}" -t "${TEST_IMAGE}" -f contrib/Dockerfile .
- docker push "${TEST_IMAGE}"
test:
@@ -65,7 +67,7 @@ test:
junit: ./unittests.xml
coverage: '/^\s*Lines:\s*(\d+(?:\.\d+)?%)/'
before_script:
- - apk add ${PHPIZE_DEPS} && pecl install xdebug-beta && docker-php-ext-enable xdebug
+ - apk add ${PHPIZE_DEPS} && pecl install xdebug && docker-php-ext-enable xdebug
- curl -sS https://getcomposer.org/installer | php -- --no-ansi --install-dir /usr/local/bin/ --filename composer
- cp -R tests/ phpunit.xml "${DOCROOT}"
- HOMEDIR=$(pwd)
diff --git a/README.md b/README.md
index ce47ef6a..02508157 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,6 @@ To report bugs use [engelsystem/issues](https://github.com/engelsystem/engelsyst
* PHP >= 7.1
* Required modules:
* dom
- * gettext
* json
* mbstring
* PDO
diff --git a/composer.json b/composer.json
index 3e50226a..a1f2101b 100644
--- a/composer.json
+++ b/composer.json
@@ -15,7 +15,6 @@
],
"require": {
"php": ">=7.1.0",
- "ext-gettext": "*",
"ext-json": "*",
"ext-libxml": "*",
"ext-mbstring": "*",
@@ -24,6 +23,7 @@
"ext-xml": "*",
"doctrine/dbal": "^2.9",
"erusev/parsedown": "^1.7",
+ "gettext/gettext": "^4.6",
"illuminate/container": "5.8.*",
"illuminate/database": "5.8.*",
"illuminate/support": "5.8.*",
@@ -32,6 +32,7 @@
"psr/container": "^1.0",
"psr/http-server-middleware": "^1.0",
"psr/log": "^1.1",
+ "respect/validation": "^1.1",
"swiftmailer/swiftmailer": "^6.2",
"symfony/http-foundation": "^4.3",
"symfony/psr-http-message-bridge": "^1.2",
diff --git a/config/app.php b/config/app.php
index 17fdee11..bfb66cf3 100644
--- a/config/app.php
+++ b/config/app.php
@@ -17,7 +17,7 @@ return [
\Engelsystem\Database\DatabaseServiceProvider::class,
\Engelsystem\Http\RequestServiceProvider::class,
\Engelsystem\Http\SessionServiceProvider::class,
- \Engelsystem\Helpers\TranslationServiceProvider::class,
+ \Engelsystem\Helpers\Translation\TranslationServiceProvider::class,
\Engelsystem\Http\ResponseServiceProvider::class,
\Engelsystem\Http\Psr7ServiceProvider::class,
\Engelsystem\Helpers\AuthenticatorServiceProvider::class,
@@ -25,8 +25,10 @@ return [
\Engelsystem\Middleware\RouteDispatcherServiceProvider::class,
\Engelsystem\Middleware\RequestHandlerServiceProvider::class,
\Engelsystem\Middleware\SessionHandlerServiceProvider::class,
+ \Engelsystem\Http\Validation\ValidationServiceProvider::class,
// Additional services
+ \Engelsystem\Helpers\VersionServiceProvider::class,
\Engelsystem\Mail\MailerServiceProvider::class,
],
diff --git a/config/config.default.php b/config/config.default.php
index 306eabff..ba343cf4 100644
--- a/config/config.default.php
+++ b/config/config.default.php
@@ -99,13 +99,10 @@ return [
// Number of hours that an angel has to sign out own shifts
'last_unsubscribe' => 3,
- // Define the algorithm to use for `crypt()` of passwords
+ // Define the algorithm to use for `password_verify()`
// If the user uses an old algorithm the password will be converted to the new format
- // MD5 '$1'
- // Blowfish '$2y$13'
- // SHA-256 '$5$rounds=5000'
- // SHA-512 '$6$rounds=5000'
- 'crypt_alg' => '$6$rounds=5000',
+ // See https://secure.php.net/manual/en/password.constants.php for a complete list
+ 'password_algorithm' => PASSWORD_DEFAULT,
// The minimum length for passwords
'min_password_length' => 8,
@@ -141,12 +138,12 @@ return [
// Available locales in /locale/
'locales' => [
- 'de_DE.UTF-8' => 'Deutsch',
- 'en_US.UTF-8' => 'English',
+ 'de_DE' => 'Deutsch',
+ 'en_US' => 'English',
],
// The default locale to use
- 'default_locale' => env('DEFAULT_LOCALE', 'en_US.UTF-8'),
+ 'default_locale' => env('DEFAULT_LOCALE', 'en_US'),
// Available T-Shirt sizes, set value to null if not available
'tshirt_sizes' => [
diff --git a/config/routes.php b/config/routes.php
index e999d026..02fd3abd 100644
--- a/config/routes.php
+++ b/config/routes.php
@@ -9,6 +9,8 @@ $route->get('/', 'HomeController@index');
$route->get('/credits', 'CreditsController@index');
// Authentication
+$route->get('/login', 'AuthController@login');
+$route->post('/login', 'AuthController@postLogin');
$route->get('/logout', 'AuthController@logout');
// Stats
diff --git a/contrib/Dockerfile b/contrib/Dockerfile
index 00e21689..be6818fe 100644
--- a/contrib/Dockerfile
+++ b/contrib/Dockerfile
@@ -25,11 +25,14 @@ COPY --from=composer /app/composer.lock /app/
RUN find /app/storage/ -type f -not -name .gitignore -exec rm {} \;
RUN rm -f /app/import/* /app/config/config.php
+ARG VERSION
+RUN if [[ ! -f /app/storage/app/VERSION ]] && [[ ! -z "${VERSION}" ]]; then echo -n "${VERSION}" > /app/storage/app/VERSION; fi
+
# Build the PHP container
FROM php:7-fpm-alpine
WORKDIR /var/www
-RUN apk add --no-cache icu-dev gettext-dev && \
- docker-php-ext-install intl gettext pdo_mysql
+RUN apk add --no-cache icu-dev && \
+ docker-php-ext-install intl pdo_mysql
COPY --from=data /app/ /var/www
RUN chown -R www-data:www-data /var/www/import/ /var/www/storage/ && \
rm -r /var/www/html
diff --git a/db/migrations/2018_10_01_000000_create_users_tables.php b/db/migrations/2018_10_01_000000_create_users_tables.php
index d8422ca0..52b3658f 100644
--- a/db/migrations/2018_10_01_000000_create_users_tables.php
+++ b/db/migrations/2018_10_01_000000_create_users_tables.php
@@ -28,7 +28,7 @@ class CreateUsersTables extends Migration
$table->string('name', 24)->unique();
$table->string('email', 254)->unique();
- $table->string('password', 128);
+ $table->string('password', 255);
$table->string('api_key', 32);
$table->dateTime('last_login_at')->nullable();
diff --git a/db/migrations/2019_06_12_000000_fix_user_languages.php b/db/migrations/2019_06_12_000000_fix_user_languages.php
new file mode 100644
index 00000000..c7d1474c
--- /dev/null
+++ b/db/migrations/2019_06_12_000000_fix_user_languages.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Engelsystem\Migrations;
+
+use Engelsystem\Database\Migration\Migration;
+
+class FixUserLanguages extends Migration
+{
+ /**
+ * Run the migration
+ */
+ public function up()
+ {
+ $connection = $this->schema->getConnection();
+ $connection
+ ->table('users_settings')
+ ->update([
+ 'language' => $connection->raw('REPLACE(language, ".UTF-8", "")')
+ ]);
+ }
+
+ /**
+ * Reverse the migration
+ */
+ public function down()
+ {
+ $connection = $this->schema->getConnection();
+ $connection
+ ->table('users_settings')
+ ->update([
+ 'language' => $connection->raw('CONCAT(language, ".UTF-8")')
+ ]);
+ }
+}
diff --git a/includes/controller/users_controller.php b/includes/controller/users_controller.php
index 7c6bde02..214998dc 100644
--- a/includes/controller/users_controller.php
+++ b/includes/controller/users_controller.php
@@ -47,6 +47,7 @@ function users_controller()
function user_delete_controller()
{
$user = auth()->user();
+ $auth = auth();
$request = request();
if ($request->has('user_id')) {
@@ -68,14 +69,12 @@ function user_delete_controller()
if ($request->hasPostData('submit')) {
$valid = true;
- if (
- !(
+ if (!(
$request->has('password')
- && verify_password($request->postData('password'), $user->password, $user->id)
- )
- ) {
+ && $auth->verifyPassword($user, $request->postData('password'))
+ )) {
$valid = false;
- error(__('Your password is incorrect. Please try it again.'));
+ error(__('Your password is incorrect. Please try it again.'));
}
if ($valid) {
@@ -341,7 +340,7 @@ function user_password_recovery_set_new_controller()
}
if ($valid) {
- set_password($passwordReset->user->id, $request->postData('password'));
+ auth()->setPassword($passwordReset->user, $request->postData('password'));
success(__('Password saved.'));
$passwordReset->delete();
redirect(page_link_to('login'));
diff --git a/includes/helper/email_helper.php b/includes/helper/email_helper.php
index bad0d539..3012d5ce 100644
--- a/includes/helper/email_helper.php
+++ b/includes/helper/email_helper.php
@@ -1,5 +1,6 @@
<?php
+use Engelsystem\Helpers\Translation\Translator;
use Engelsystem\Mail\EngelsystemMailer;
use Engelsystem\Models\User\User;
use Psr\Log\LogLevel;
@@ -17,7 +18,7 @@ function engelsystem_email_to_user($recipientUser, $title, $message, $notIfItsMe
return true;
}
- /** @var \Engelsystem\Helpers\Translator $translator */
+ /** @var Translator $translator */
$translator = app()->get('translator');
$locale = $translator->getLocale();
diff --git a/includes/pages/admin_user.php b/includes/pages/admin_user.php
index e6f94180..8482dea5 100644
--- a/includes/pages/admin_user.php
+++ b/includes/pages/admin_user.php
@@ -291,8 +291,8 @@ function admin_user()
$request->postData('new_pw') != ''
&& $request->postData('new_pw') == $request->postData('new_pw2')
) {
- set_password($user_id, $request->postData('new_pw'));
$user_source = User::find($user_id);
+ auth()->setPassword($user_source, $request->postData('new_pw'));
engelsystem_log('Set new password for ' . User_Nick_render($user_source, true));
$html .= success('Passwort neu gesetzt.', true);
} else {
diff --git a/includes/pages/guest_login.php b/includes/pages/guest_login.php
index d152a092..3bc10fc3 100644
--- a/includes/pages/guest_login.php
+++ b/includes/pages/guest_login.php
@@ -11,14 +11,6 @@ use Engelsystem\Models\User\User;
/**
* @return string
*/
-function login_title()
-{
- return __('Login');
-}
-
-/**
- * @return string
- */
function register_title()
{
return __('Register');
@@ -226,7 +218,7 @@ function guest_register()
// Assign user-group and set password
DB::insert('INSERT INTO `UserGroups` (`uid`, `group_id`) VALUES (?, -20)', [$user->id]);
- set_password($user->id, $request->postData('password'));
+ auth()->setPassword($user, $request->postData('password'));
// Assign angel-types
$user_angel_types_info = [];
@@ -369,112 +361,3 @@ function entry_required()
{
return '<span class="text-info glyphicon glyphicon-warning-sign"></span>';
}
-
-/**
- * @return string
- */
-function guest_login()
-{
- $nick = '';
- $request = request();
- $session = session();
- $valid = true;
-
- $session->remove('uid');
-
- if ($request->hasPostData('submit')) {
- if ($request->has('nick') && !empty($request->input('nick'))) {
- $nickValidation = User_validate_Nick($request->input('nick'));
- $nick = $nickValidation->getValue();
- $login_user = User::whereName($nickValidation->getValue())->first();
- if ($login_user) {
- if ($request->has('password')) {
- if (!verify_password($request->postData('password'), $login_user->password, $login_user->id)) {
- $valid = false;
- error(__('Your password is incorrect. Please try it again.'));
- }
- } else {
- $valid = false;
- error(__('Please enter a password.'));
- }
- } else {
- $valid = false;
- error(__('No user was found with that Nickname. Please try again. If you are still having problems, ask a Dispatcher.'));
- }
- } else {
- $valid = false;
- error(__('Please enter a nickname.'));
- }
-
- if ($valid && $login_user) {
- $session->set('uid', $login_user->id);
- $session->set('locale', $login_user->settings->language);
-
- redirect(page_link_to(config('home_site')));
- }
- }
-
- return page([
- div('col-md-12', [
- div('row', [
- EventConfig_countdown_page()
- ]),
- div('row', [
- div('col-sm-6 col-sm-offset-3 col-md-4 col-md-offset-4', [
- div('panel panel-primary first', [
- div('panel-heading', [
- '<span class="icon-icon_angel"></span> ' . __('Login')
- ]),
- div('panel-body', [
- msg(),
- form([
- form_text_placeholder('nick', __('Nick'), $nick),
- form_password_placeholder('password', __('Password')),
- form_submit('submit', __('Login')),
- !$valid ? buttons([
- button(page_link_to('user_password_recovery'), __('I forgot my password'))
- ]) : ''
- ])
- ]),
- div('panel-footer', [
- glyph('info-sign') . __('Please note: You have to activate cookies!')
- ])
- ])
- ])
- ]),
- div('row', [
- div('col-sm-6 text-center', [
- heading(register_title(), 2),
- get_register_hint()
- ]),
- div('col-sm-6 text-center', [
- heading(__('What can I do?'), 2),
- '<p>' . __('Please read about the jobs you can do to help us.') . '</p>',
- buttons([
- button(
- page_link_to('angeltypes', ['action' => 'about']),
- __('Teams/Job description') . ' &raquo;'
- )
- ])
- ])
- ])
- ])
- ]);
-}
-
-/**
- * @return string
- */
-function get_register_hint()
-{
- if (auth()->can('register') && config('registration_enabled')) {
- return join('', [
- '<p>' . __('Please sign up, if you want to help us!') . '</p>',
- buttons([
- button(page_link_to('register'), register_title() . ' &raquo;')
- ])
- ]);
- }
-
- return error(__('Registration is disabled.'), true);
-}
diff --git a/includes/pages/user_settings.php b/includes/pages/user_settings.php
index ae29e4d8..f6853191 100644
--- a/includes/pages/user_settings.php
+++ b/includes/pages/user_settings.php
@@ -101,9 +101,10 @@ function user_settings_main($user_source, $enable_tshirt_size, $tshirt_sizes)
function user_settings_password($user_source)
{
$request = request();
+ $auth = auth();
if (
!$request->has('password')
- || !verify_password($request->postData('password'), $user_source->password, $user_source->id)
+ || !$auth->verifyPassword($user_source, $request->postData('password'))
) {
error(__('-> not OK. Please try again.'));
} elseif (strlen($request->postData('new_password')) < config('min_password_length')) {
@@ -111,7 +112,7 @@ function user_settings_password($user_source)
} elseif ($request->postData('new_password') != $request->postData('new_password2')) {
error(__('Your passwords don\'t match.'));
} else {
- set_password($user_source->id, $request->postData('new_password'));
+ $auth->setPassword($user_source, $request->postData('new_password'));
success(__('Password saved.'));
}
redirect(page_link_to('user_settings'));
diff --git a/includes/sys_auth.php b/includes/sys_auth.php
index 520b13eb..f0485495 100644
--- a/includes/sys_auth.php
+++ b/includes/sys_auth.php
@@ -1,74 +1,6 @@
<?php
use Engelsystem\Database\DB;
-use Engelsystem\Models\User\User;
-
-/**
- * generate a salt (random string) of arbitrary length suitable for the use with crypt()
- *
- * @param int $length
- * @return string
- */
-function generate_salt($length = 16)
-{
- $alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
- $salt = '';
- for ($i = 0; $i < $length; $i++) {
- $salt .= $alphabet[rand(0, strlen($alphabet) - 1)];
- }
- return $salt;
-}
-
-/**
- * set the password of a user
- *
- * @param int $uid
- * @param string $password
- */
-function set_password($uid, $password)
-{
- $user = User::find($uid);
- $user->password = crypt($password, config('crypt_alg') . '$' . generate_salt(16) . '$');
- $user->save();
-}
-
-/**
- * verify a password given a precomputed salt.
- * if $uid is given and $salt is an old-style salt (plain md5), we convert it automatically
- *
- * @param string $password
- * @param string $salt
- * @param int $uid
- * @return bool
- */
-function verify_password($password, $salt, $uid = null)
-{
- $crypt_alg = config('crypt_alg');
- $correct = false;
- if (substr($salt, 0, 1) == '$') {
- // new-style crypt()
- $correct = crypt($password, $salt) == $salt;
- } elseif (substr($salt, 0, 7) == '{crypt}') {
- // old-style crypt() with DES and static salt - not used anymore
- $correct = crypt($password, '77') == $salt;
- } elseif (strlen($salt) == 32) {
- // old-style md5 without salt - not used anymore
- $correct = md5($password) == $salt;
- }
-
- if ($correct && substr($salt, 0, strlen($crypt_alg)) != $crypt_alg && intval($uid)) {
- // this password is stored in another format than we want it to be.
- // let's update it!
- // we duplicate the query from the above set_password() function to have the extra safety of checking
- // the old hash
- $user = User::find($uid);
- if ($user->password == $salt) {
- $user->password = crypt($password, $crypt_alg . '$' . generate_salt() . '$');
- $user->save();
- }
- }
- return $correct;
-}
/**
* @param int $user_id
diff --git a/includes/view/AngelTypes_view.php b/includes/view/AngelTypes_view.php
index f5434e8f..9f9bd736 100644
--- a/includes/view/AngelTypes_view.php
+++ b/includes/view/AngelTypes_view.php
@@ -578,7 +578,7 @@ function AngelTypes_about_view($angeltypes, $user_logged_in)
$buttons[] = button(page_link_to('register'), register_title());
}
- $buttons[] = button(page_link_to('login'), login_title());
+ $buttons[] = button(page_link_to('login'), __('Login'));
}
$faqUrl = config('faq_url');
diff --git a/includes/view/User_view.php b/includes/view/User_view.php
index 949bba87..21be0c9f 100644
--- a/includes/view/User_view.php
+++ b/includes/view/User_view.php
@@ -126,7 +126,7 @@ function User_registration_success_view($event_welcome_message)
div('col-md-4', [
'<h2>' . __('Login') . '</h2>',
form([
- form_text('nick', __('Nick'), ''),
+ form_text('login', __('Nick'), ''),
form_password('password', __('Password')),
form_submit('submit', __('Login')),
buttons([
diff --git a/resources/lang/de_DE.UTF-8/LC_MESSAGES/default.mo b/resources/lang/de_DE.UTF-8/LC_MESSAGES/default.mo
deleted file mode 100644
index 35ad80b7..00000000
--- a/resources/lang/de_DE.UTF-8/LC_MESSAGES/default.mo
+++ /dev/null
Binary files differ
diff --git a/resources/lang/de_DE/default.mo b/resources/lang/de_DE/default.mo
new file mode 100644
index 00000000..fb93d590
--- /dev/null
+++ b/resources/lang/de_DE/default.mo
Binary files differ
diff --git a/resources/lang/de_DE.UTF-8/LC_MESSAGES/default.po b/resources/lang/de_DE/default.po
index d5a7b993..1f0372af 100644
--- a/resources/lang/de_DE.UTF-8/LC_MESSAGES/default.po
+++ b/resources/lang/de_DE/default.po
@@ -2,7 +2,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Engelsystem\n"
"POT-Creation-Date: 2019-04-28 15:23+0200\n"
-"PO-Revision-Date: 2019-06-12 16:07+0200\n"
+"PO-Revision-Date: 2019-06-13 11:54+0200\n"
"Last-Translator: msquare <msquare@notrademark.de>\n"
"Language-Team: \n"
"Language: de_DE\n"
@@ -10,7 +10,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 1.8.11\n"
-"X-Poedit-KeywordsList: __;_e;translate;translatePlural;gettext;gettext_noop\n"
+"X-Poedit-KeywordsList: __;_e;translate;translatePlural\n"
"X-Poedit-Basepath: ../../../..\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Poedit-SourceCharset: UTF-8\n"
@@ -541,7 +541,7 @@ msgstr "Du kannst Dich nicht selber löschen."
#: includes/controller/users_controller.php:78
#: includes/pages/guest_login.php:410
-msgid "Your password is incorrect. Please try it again."
+msgid "Your password is incorrect. Please try it again."
msgstr "Dein Passwort stimmt nicht. Bitte probiere es nochmal."
#: includes/controller/users_controller.php:87
@@ -1529,19 +1529,20 @@ msgstr "Nachname"
msgid "Entry required!"
msgstr "Pflichtfeld!"
-#: includes/pages/guest_login.php:414
-msgid "Please enter a password."
-msgstr "Gib bitte ein Passwort ein."
+#~ msgid "auth.no-password"
+#~ msgstr "Gib bitte ein Passwort ein."
#: includes/pages/guest_login.php:418
-msgid ""
-"No user was found with that Nickname. Please try again. If you are still "
-"having problems, ask a Dispatcher."
+msgid "auth.not-found"
msgstr ""
-"Es wurde kein Engel mit diesem Namen gefunden. Probiere es bitte noch "
-"einmal. Wenn das Problem weiterhin besteht, frage einen Dispatcher."
+"Es wurde kein Engel gefunden. Probiere es bitte noch einmal. Wenn das Problem "
+"weiterhin besteht, melde dich im Himmel."
+
+#~ msgid "auth.no-nickname"
+#~ msgstr "Gib bitte einen Nick an."
-#: includes/pages/guest_login.php:451 includes/view/User_view.php:130
+#: includes/pages/guest_login.php:481
+#: includes/view/User_view.php:122
msgid "I forgot my password"
msgstr "Passwort vergessen"
@@ -2357,7 +2358,7 @@ msgid ""
"I have my own car with me and am willing to use it for the event (You'll get "
"reimbursed for fuel)"
msgstr ""
-"Ich habe mein eigenes Auto dabei und möchte würde es zum Fahren für das "
+"Ich habe mein eigenes Auto dabei und möchte es zum Fahren für das "
"Event verwenden (Du wirst für Spritkosten entschädigt)"
#: includes/view/UserDriverLicenses_view.php:30
@@ -2762,3 +2763,9 @@ msgid ""
msgstr ""
"Diese Seite existiert nicht oder Du hast keinen Zugriff. Melde Dich an um "
"Zugriff zu erhalten!"
+
+msgid "validation.password.required"
+msgstr "Bitte gib ein Passwort an."
+
+msgid "validation.login.required"
+msgstr "Bitte gib einen Loginnamen an."
diff --git a/resources/lang/en_US/default.mo b/resources/lang/en_US/default.mo
new file mode 100644
index 00000000..7ef9c3b2
--- /dev/null
+++ b/resources/lang/en_US/default.mo
Binary files differ
diff --git a/resources/lang/en_US/default.po b/resources/lang/en_US/default.po
new file mode 100644
index 00000000..54847e61
--- /dev/null
+++ b/resources/lang/en_US/default.po
@@ -0,0 +1,32 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: Engelsystem 2.0\n"
+"POT-Creation-Date: 2017-12-29 19:01+0100\n"
+"PO-Revision-Date: 2019-06-04 23:41+0200\n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 1.8.11\n"
+"X-Poedit-KeywordsList: _;gettext;gettext_noop\n"
+"X-Poedit-Basepath: .\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Poedit-SourceCharset: UTF-8\n"
+"Last-Translator: \n"
+"Language: en_US\n"
+"X-Poedit-SearchPath-0: .\n"
+
+#~ msgid "auth.no-nickname"
+#~ msgstr "Please enter a nickname."
+
+#~ msgid "auth.no-password"
+#~ msgstr "Please enter a password."
+
+msgid "auth.not-found"
+msgstr "No user was found. Please try again. If you are still having problems, ask Heaven."
+
+msgid "validation.password.required"
+msgstr "The password is required."
+
+msgid "validation.login.required"
+msgstr "The login name is required."
diff --git a/resources/lang/pt_BR.UTF.8/LC_MESSAGES/pt_BR.mo b/resources/lang/pt_BR.UTF.8/LC_MESSAGES/pt_BR.mo
deleted file mode 100644
index 95251feb..00000000
--- a/resources/lang/pt_BR.UTF.8/LC_MESSAGES/pt_BR.mo
+++ /dev/null
Binary files differ
diff --git a/resources/lang/pt_BR/default.mo b/resources/lang/pt_BR/default.mo
new file mode 100644
index 00000000..8b864156
--- /dev/null
+++ b/resources/lang/pt_BR/default.mo
Binary files differ
diff --git a/resources/lang/pt_BR.UTF.8/LC_MESSAGES/pt_BR.po b/resources/lang/pt_BR/default.po
index e7307e5d..b9bf420d 100644
--- a/resources/lang/pt_BR.UTF.8/LC_MESSAGES/pt_BR.po
+++ b/resources/lang/pt_BR/default.po
@@ -2,7 +2,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Engelsystem 2.0\n"
"POT-Creation-Date: 2017-04-25 05:23+0200\n"
-"PO-Revision-Date: 2018-10-05 15:35+0200\n"
+"PO-Revision-Date: 2018-11-27 00:29+0100\n"
"Last-Translator: samba <samba@autistici.org>\n"
"Language-Team: \n"
"Language: pt_BR\n"
@@ -477,7 +477,7 @@ msgstr "Você não pode se deletar."
#: includes/controller/users_controller.php:61
#: includes/pages/guest_login.php:315
-msgid "Your password is incorrect. Please try it again."
+msgid "Your password is incorrect. Please try it again."
msgstr "Sua senha está incorreta. Por favor, tente novamente."
#: includes/controller/users_controller.php:71
@@ -1420,19 +1420,17 @@ msgid "Entry required!"
msgstr "Campo necessário!"
#: includes/pages/guest_login.php:319
-msgid "Please enter a password."
+msgid "auth.no-password"
msgstr "Por favor digite uma senha."
#: includes/pages/guest_login.php:323
-msgid ""
-"No user was found with that Nickname. Please try again. If you are still "
-"having problems, ask a Dispatcher."
+msgid "auth.not-found"
msgstr ""
-"Nenhum usuário com esse apelido foi encontrado. Por favor tente novamente. \n"
+"Nenhum usuário foi encontrado. Por favor tente novamente. \n"
"Se você continuar com problemas, pergunte a um Dispatcher."
#: includes/pages/guest_login.php:327
-msgid "Please enter a nickname."
+msgid "auth.no-nickname"
msgstr "Por favor digite um apelido."
#: includes/pages/guest_login.php:358 includes/view/User_view.php:101
diff --git a/resources/views/errors/405.twig b/resources/views/errors/405.twig
new file mode 100644
index 00000000..cbbb94ea
--- /dev/null
+++ b/resources/views/errors/405.twig
@@ -0,0 +1,5 @@
+{% extends "errors/default.twig" %}
+
+{% block title %}{{ __("405: Method not allowed") }}{% endblock %}
+
+{% block content_headline_text %}{{ __("405: Method not allowed") }}{% endblock %}
diff --git a/resources/views/macros/base.twig b/resources/views/macros/base.twig
new file mode 100644
index 00000000..94287bd4
--- /dev/null
+++ b/resources/views/macros/base.twig
@@ -0,0 +1,11 @@
+{% macro angel() %}
+ <span class="icon-icon_angel"></span>
+{% endmacro %}
+
+{% macro glyphicon(glyph) %}
+ <span class="glyphicon glyphicon-{{ glyph }}"></span>
+{% endmacro %}
+
+{% macro alert(message, type) %}
+ <div class="alert alert-{{ type|default('info') }}">{{ message }}</div>
+{% endmacro %}
diff --git a/resources/views/pages/credits.twig b/resources/views/pages/credits.twig
index eb98c7e7..3bb04895 100644
--- a/resources/views/pages/credits.twig
+++ b/resources/views/pages/credits.twig
@@ -15,6 +15,7 @@
<div class="col-md-4">
<h2>Source code</h2>
+ <p>Version: <i>{{ version }}</i></p>
<p>
The original engelsystem was written by
<a href="https://github.com/cookieBerlin/engelsystem">cookie</a>.
diff --git a/resources/views/pages/login.twig b/resources/views/pages/login.twig
new file mode 100644
index 00000000..75b98aa1
--- /dev/null
+++ b/resources/views/pages/login.twig
@@ -0,0 +1,104 @@
+{% extends "layouts/app.twig" %}
+{% import 'macros/base.twig' as m %}
+
+{% block title %}{{ __('Login') }}{% endblock %}
+
+{% block content %}
+ <div class="col-md-12">
+ <div class="row">
+ <div class="col-sm-12 text-center">
+ <h2>{{ __('Welcome to the %s!', [config('name') ~ m.angel() ~ (config('app_name')|upper) ])|raw }}</h2>
+ </div>
+ </div>
+
+ <div class="row">
+ {% for name,date in {
+ (__('Buildup starts')): config('buildup_start'),
+ (__('Event starts')): config('event_start'),
+ (__('Event ends')): config('event_end'),
+ (__('Teardown ends')): config('teardown_end')
+ } if date %}
+ {% if date > date() %}
+ <div class="col-sm-3 text-center hidden-xs">
+ <h4>{{ name }}</h4>
+ <span class="moment-countdown text-big" data-timestamp="{{ date.getTimestamp }}">%c</span>
+ <small>{{ date.format(__('Y-m-d')) }}</small>
+ </div>
+ {% endif %}
+ {% endfor %}
+ </div>
+
+ <div class="row">
+ <div class="col-sm-6 col-sm-offset-3 col-md-4 col-md-offset-4">
+ <div class="panel panel-primary first">
+
+ <div class="panel-heading">{{ m.angel }} {{ __('Login') }}</div>
+
+ <div class="panel-body">
+ {% for message in errors|default([]) %}
+ {{ m.alert(__(message), 'danger') }}
+ {% endfor %}
+
+ <form action="" enctype="multipart/form-data" method="post">
+ {{ csrf() }}
+ <div class="form-group">
+ <input class="form-control" id="form_nick"
+ type="text" name="login" value="" placeholder="{{ __('Nick') }}">
+ </div>
+
+ <div class="form-group">
+ <input class="form-control" id="form_password"
+ type="password" name="password" value="" placeholder="{{ __('Password') }}">
+ </div>
+
+ <div class="form-group">
+ <div class="btn-group">
+ <button class="btn btn-primary" type="submit" name="submit">
+ {{ __('Login') }}
+ </button>
+
+ {% if show_password_recovery|default(false) %}
+ <a href="{{ url('user-password-recovery') }}" class="btn btn-default ">
+ {{ __('I forgot my password') }}
+ </a>
+ {% endif %}
+ </div>
+ </div>
+
+ </form>
+ </div>
+
+ <div class="panel-footer">
+ {{ m.glyphicon('info-sign') }} {{ __('Please note: You have to activate cookies!') }}
+ </div>
+
+ </div>
+ </div>
+ </div>
+
+ <div class="row">
+ <div class="col-sm-6 text-center">
+ <h2>{{ __('Register') }}</h2>
+ {% if has_permission_to('register') and config('registration_enabled') %}
+ <p>{{ __('Please sign up, if you want to help us!') }}</p>
+ <div class="form-group">
+ <a href="{{ url('register') }}" class="btn btn-default">{{ __('Register') }} &raquo;</a>
+ </div>
+ {% else %}
+ {{ m.alert(__('Registration is disabled.'), 'danger') }}
+ {% endif %}
+ </div>
+
+ <div class="col-sm-6 text-center">
+ <h2>{{ __('What can I do?') }}</h2>
+ <p>{{ __('Please read about the jobs you can do to help us.') }}</p>
+ <div class="form-group">
+ <a href="{{ url('angeltypes', {'action': 'about'}) }}" class="btn btn-default">
+ {{ __('Teams/Job description') }} &raquo;
+ </a>
+ </div>
+ </div>
+ </div>
+
+ </div>
+{% endblock %}
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;
diff --git a/storage/app/.gitignore b/storage/app/.gitignore
new file mode 100644
index 00000000..78d91016
--- /dev/null
+++ b/storage/app/.gitignore
@@ -0,0 +1,2 @@
+/*
+!.gitignore
diff --git a/tests/Unit/Controllers/AuthControllerTest.php b/tests/Unit/Controllers/AuthControllerTest.php
index c5349cda..c3d9659c 100644
--- a/tests/Unit/Controllers/AuthControllerTest.php
+++ b/tests/Unit/Controllers/AuthControllerTest.php
@@ -3,40 +3,166 @@
namespace Engelsystem\Test\Unit\Controllers;
use Engelsystem\Controllers\AuthController;
+use Engelsystem\Helpers\Authenticator;
+use Engelsystem\Http\Exceptions\ValidationException;
+use Engelsystem\Http\Request;
use Engelsystem\Http\Response;
use Engelsystem\Http\UrlGeneratorInterface;
+use Engelsystem\Http\Validation\Validator;
+use Engelsystem\Models\User\Settings;
+use Engelsystem\Models\User\User;
+use Engelsystem\Test\Unit\HasDatabase;
+use Illuminate\Support\Collection;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
+use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
+use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
class AuthControllerTest extends TestCase
{
+ use HasDatabase;
+
/**
* @covers \Engelsystem\Controllers\AuthController::__construct
- * @covers \Engelsystem\Controllers\AuthController::logout
+ * @covers \Engelsystem\Controllers\AuthController::login
+ * @covers \Engelsystem\Controllers\AuthController::showLogin
*/
- public function testLogout()
+ public function testLogin()
{
/** @var Response|MockObject $response */
$response = $this->createMock(Response::class);
/** @var SessionInterface|MockObject $session */
- $session = $this->getMockForAbstractClass(SessionInterface::class);
/** @var UrlGeneratorInterface|MockObject $url */
- $url = $this->getMockForAbstractClass(UrlGeneratorInterface::class);
+ /** @var Authenticator|MockObject $auth */
+ list(, $session, $url, $auth) = $this->getMocks();
$session->expects($this->once())
- ->method('invalidate');
+ ->method('get')
+ ->with('errors', [])
+ ->willReturn(['foo' => 'bar']);
+ $response->expects($this->once())
+ ->method('withView')
+ ->with('pages/login')
+ ->willReturn($response);
+
+ $controller = new AuthController($response, $session, $url, $auth);
+ $controller->login();
+ }
+
+ /**
+ * @covers \Engelsystem\Controllers\AuthController::postLogin
+ */
+ public function testPostLogin()
+ {
+ $this->initDatabase();
+ $request = new Request();
+ /** @var Response|MockObject $response */
+ $response = $this->createMock(Response::class);
+ /** @var UrlGeneratorInterface|MockObject $url */
+ /** @var Authenticator|MockObject $auth */
+ list(, , $url, $auth) = $this->getMocks();
+ $session = new Session(new MockArraySessionStorage());
+ /** @var Validator|MockObject $validator */
+ $validator = new Validator();
+
+ $user = new User([
+ 'name' => 'foo',
+ 'password' => '',
+ 'email' => '',
+ 'api_key' => '',
+ 'last_login_at' => null,
+ ]);
+ $user->forceFill(['id' => 42]);
+ $user->save();
+
+ $settings = new Settings(['language' => 'de_DE', 'theme' => '']);
+ $settings->user()
+ ->associate($user)
+ ->save();
+
+ $auth->expects($this->exactly(2))
+ ->method('authenticate')
+ ->with('foo', 'bar')
+ ->willReturnOnConsecutiveCalls(null, $user);
+
+ $response->expects($this->once())
+ ->method('withView')
+ ->with('pages/login', ['errors' => Collection::make(['auth.not-found']), 'show_password_recovery' => true])
+ ->willReturn($response);
$response->expects($this->once())
->method('redirectTo')
- ->with('https://foo.bar/');
+ ->with('news')
+ ->willReturn($response);
+
+ // No credentials
+ $controller = new AuthController($response, $session, $url, $auth);
+ $controller->setValidator($validator);
+ try {
+ $controller->postLogin($request);
+ $this->fail('Login without credentials possible');
+ } catch (ValidationException $e) {
+ }
+
+ // Missing password
+ $request = new Request([], ['login' => 'foo']);
+ try {
+ $controller->postLogin($request);
+ $this->fail('Login without password possible');
+ } catch (ValidationException $e) {
+ }
+
+ // No user found
+ $request = new Request([], ['login' => 'foo', 'password' => 'bar']);
+ $controller->postLogin($request);
+ $this->assertEquals([], $session->all());
+
+ // Authenticated user
+ $controller->postLogin($request);
+
+ $this->assertNotNull($user->last_login_at);
+ $this->assertEquals(['user_id' => 42, 'locale' => 'de_DE'], $session->all());
+ }
+
+ /**
+ * @covers \Engelsystem\Controllers\AuthController::logout
+ */
+ public function testLogout()
+ {
+ /** @var Response $response */
+ /** @var SessionInterface|MockObject $session */
+ /** @var UrlGeneratorInterface|MockObject $url */
+ /** @var Authenticator|MockObject $auth */
+ list($response, $session, $url, $auth) = $this->getMocks();
+
+ $session->expects($this->once())
+ ->method('invalidate');
$url->expects($this->once())
->method('to')
->with('/')
->willReturn('https://foo.bar/');
- $controller = new AuthController($response, $session, $url);
- $controller->logout();
+ $controller = new AuthController($response, $session, $url, $auth);
+ $return = $controller->logout();
+
+ $this->assertEquals(['https://foo.bar/'], $return->getHeader('location'));
+ }
+
+ /**
+ * @return array
+ */
+ protected function getMocks()
+ {
+ $response = new Response();
+ /** @var SessionInterface|MockObject $session */
+ $session = $this->getMockForAbstractClass(SessionInterface::class);
+ /** @var UrlGeneratorInterface|MockObject $url */
+ $url = $this->getMockForAbstractClass(UrlGeneratorInterface::class);
+ /** @var Authenticator|MockObject $auth */
+ $auth = $this->createMock(Authenticator::class);
+
+ return [$response, $session, $url, $auth];
}
}
diff --git a/tests/Unit/Controllers/BaseControllerTest.php b/tests/Unit/Controllers/BaseControllerTest.php
index 738b538f..2adc9dc7 100644
--- a/tests/Unit/Controllers/BaseControllerTest.php
+++ b/tests/Unit/Controllers/BaseControllerTest.php
@@ -21,5 +21,7 @@ class BaseControllerTest extends TestCase
'dolor',
],
], $controller->getPermissions());
+
+ $this->assertTrue(method_exists($controller, 'setValidator'));
}
}
diff --git a/tests/Unit/Controllers/CreditsControllerTest.php b/tests/Unit/Controllers/CreditsControllerTest.php
index 42ea4ea1..303bf60e 100644
--- a/tests/Unit/Controllers/CreditsControllerTest.php
+++ b/tests/Unit/Controllers/CreditsControllerTest.php
@@ -4,9 +4,10 @@ namespace Engelsystem\Test\Unit\Controllers;
use Engelsystem\Config\Config;
use Engelsystem\Controllers\CreditsController;
+use Engelsystem\Helpers\Version;
use Engelsystem\Http\Response;
+use Engelsystem\Test\Unit\TestCase;
use PHPUnit\Framework\MockObject\MockObject;
-use PHPUnit\Framework\TestCase;
class CreditsControllerTest extends TestCase
{
@@ -19,12 +20,17 @@ class CreditsControllerTest extends TestCase
/** @var Response|MockObject $response */
$response = $this->createMock(Response::class);
$config = new Config(['foo' => 'bar', 'credits' => ['lor' => 'em']]);
+ /** @var Version|MockObject $version */
+ $version = $this->createMock(Version::class);
- $response->expects($this->once())
- ->method('withView')
- ->with('pages/credits.twig', ['credits' => ['lor' => 'em']]);
+ $this->setExpects(
+ $response,
+ 'withView',
+ ['pages/credits.twig', ['credits' => ['lor' => 'em'], 'version' => '42.1.0-test']]
+ );
+ $this->setExpects($version, 'getVersion', [], '42.1.0-test');
- $controller = new CreditsController($response, $config);
+ $controller = new CreditsController($response, $config, $version);
$controller->index();
}
}
diff --git a/tests/Unit/Controllers/Metrics/ControllerTest.php b/tests/Unit/Controllers/Metrics/ControllerTest.php
index 18daa96a..f203200c 100644
--- a/tests/Unit/Controllers/Metrics/ControllerTest.php
+++ b/tests/Unit/Controllers/Metrics/ControllerTest.php
@@ -6,6 +6,7 @@ use Engelsystem\Config\Config;
use Engelsystem\Controllers\Metrics\Controller;
use Engelsystem\Controllers\Metrics\MetricsEngine;
use Engelsystem\Controllers\Metrics\Stats;
+use Engelsystem\Helpers\Version;
use Engelsystem\Http\Exceptions\HttpForbidden;
use Engelsystem\Http\Request;
use Engelsystem\Http\Response;
@@ -28,7 +29,8 @@ class ControllerTest extends TestCase
/** @var MetricsEngine|MockObject $engine */
/** @var Stats|MockObject $stats */
/** @var Config $config */
- list($response, $request, $engine, $stats, $config) = $this->getMocks();
+ /** @var Version|MockObject $version */
+ list($response, $request, $engine, $stats, $config, $version) = $this->getMocks();
$request->server = new ServerBag();
$request->server->set('REQUEST_TIME_FLOAT', 0.0123456789);
@@ -37,6 +39,7 @@ class ControllerTest extends TestCase
->method('get')
->willReturnCallback(function ($path, $data) use ($response) {
$this->assertEquals('/metrics', $path);
+ $this->assertArrayHasKey('info', $data);
$this->assertArrayHasKey('users', $data);
$this->assertArrayHasKey('licenses', $data);
$this->assertArrayHasKey('users_working', $data);
@@ -122,7 +125,9 @@ class ControllerTest extends TestCase
'XL' => 'X Large',
]);
- $controller = new Controller($response, $engine, $config, $request, $stats);
+ $this->setExpects($version, 'getVersion', [], '0.42.42');
+
+ $controller = new Controller($response, $engine, $config, $request, $stats, $version);
$controller->metrics();
}
@@ -137,7 +142,8 @@ class ControllerTest extends TestCase
/** @var MetricsEngine|MockObject $engine */
/** @var Stats|MockObject $stats */
/** @var Config $config */
- list($response, $request, $engine, $stats, $config) = $this->getMocks();
+ /** @var Version|MockObject $version */
+ list($response, $request, $engine, $stats, $config, $version) = $this->getMocks();
$response->expects($this->once())
->method('withHeader')
@@ -168,7 +174,7 @@ class ControllerTest extends TestCase
$this->setExpects($stats, 'arrivedUsers', null, 10, $this->exactly(2));
$this->setExpects($stats, 'currentlyWorkingUsers', null, 5);
- $controller = new Controller($response, $engine, $config, $request, $stats);
+ $controller = new Controller($response, $engine, $config, $request, $stats, $version);
$controller->stats();
}
@@ -182,7 +188,8 @@ class ControllerTest extends TestCase
/** @var MetricsEngine|MockObject $engine */
/** @var Stats|MockObject $stats */
/** @var Config $config */
- list($response, $request, $engine, $stats, $config) = $this->getMocks();
+ /** @var Version|MockObject $version */
+ list($response, $request, $engine, $stats, $config, $version) = $this->getMocks();
$request->expects($this->once())
->method('get')
@@ -191,7 +198,7 @@ class ControllerTest extends TestCase
$config->set('api_key', 'fooBar!');
- $controller = new Controller($response, $engine, $config, $request, $stats);
+ $controller = new Controller($response, $engine, $config, $request, $stats, $version);
$this->expectException(HttpForbidden::class);
$this->expectExceptionMessage(json_encode(['error' => 'The api_key is invalid']));
@@ -212,7 +219,8 @@ class ControllerTest extends TestCase
/** @var Stats|MockObject $stats */
$stats = $this->createMock(Stats::class);
$config = new Config();
+ $version = $this->createMock(Version::class);
- return [$response, $request, $engine, $stats, $config];
+ return [$response, $request, $engine, $stats, $config, $version];
}
}
diff --git a/tests/Unit/Controllers/Metrics/MetricsEngineTest.php b/tests/Unit/Controllers/Metrics/MetricsEngineTest.php
index 38817b36..87a7dc88 100644
--- a/tests/Unit/Controllers/Metrics/MetricsEngineTest.php
+++ b/tests/Unit/Controllers/Metrics/MetricsEngineTest.php
@@ -66,4 +66,15 @@ class MetricsEngineTest extends TestCase
$this->assertFalse($engine->canRender('/metrics.foo'));
$this->assertTrue($engine->canRender('/metrics'));
}
+
+ /**
+ * @covers \Engelsystem\Controllers\Metrics\MetricsEngine::share
+ */
+ public function testShare()
+ {
+ $engine = new MetricsEngine();
+
+ $engine->share('foo', 42);
+ $this->assertEquals('', $engine->get('/metrics'));
+ }
}
diff --git a/tests/Unit/Controllers/Stub/ControllerImplementation.php b/tests/Unit/Controllers/Stub/ControllerImplementation.php
index 01d9f250..a8bf538c 100644
--- a/tests/Unit/Controllers/Stub/ControllerImplementation.php
+++ b/tests/Unit/Controllers/Stub/ControllerImplementation.php
@@ -14,12 +14,4 @@ class ControllerImplementation extends BaseController
'dolor',
],
];
-
- /**
- * @param array $permissions
- */
- public function setPermissions(array $permissions)
- {
- $this->permissions = $permissions;
- }
}
diff --git a/tests/Unit/Helpers/AuthenticatorServiceProviderTest.php b/tests/Unit/Helpers/AuthenticatorServiceProviderTest.php
index b1767ebc..ab9b23ec 100644
--- a/tests/Unit/Helpers/AuthenticatorServiceProviderTest.php
+++ b/tests/Unit/Helpers/AuthenticatorServiceProviderTest.php
@@ -3,6 +3,7 @@
namespace Engelsystem\Test\Unit\Helpers;
use Engelsystem\Application;
+use Engelsystem\Config\Config;
use Engelsystem\Helpers\Authenticator;
use Engelsystem\Helpers\AuthenticatorServiceProvider;
use Engelsystem\Http\Request;
@@ -19,11 +20,19 @@ class AuthenticatorServiceProviderTest extends ServiceProviderTest
$app = new Application();
$app->bind(ServerRequestInterface::class, Request::class);
+ $config = new Config();
+ $config->set('password_algorithm', PASSWORD_DEFAULT);
+ $app->instance('config', $config);
+
$serviceProvider = new AuthenticatorServiceProvider($app);
$serviceProvider->register();
$this->assertInstanceOf(Authenticator::class, $app->get(Authenticator::class));
$this->assertInstanceOf(Authenticator::class, $app->get('authenticator'));
$this->assertInstanceOf(Authenticator::class, $app->get('auth'));
+
+ /** @var Authenticator $auth */
+ $auth = $app->get(Authenticator::class);
+ $this->assertEquals(PASSWORD_DEFAULT, $auth->getPasswordAlgorithm());
}
}
diff --git a/tests/Unit/Helpers/AuthenticatorTest.php b/tests/Unit/Helpers/AuthenticatorTest.php
index 400278f2..83dc72ad 100644
--- a/tests/Unit/Helpers/AuthenticatorTest.php
+++ b/tests/Unit/Helpers/AuthenticatorTest.php
@@ -4,6 +4,7 @@ namespace Engelsystem\Test\Unit\Helpers;
use Engelsystem\Helpers\Authenticator;
use Engelsystem\Models\User\User;
+use Engelsystem\Test\Unit\HasDatabase;
use Engelsystem\Test\Unit\Helpers\Stub\UserModelImplementation;
use Engelsystem\Test\Unit\ServiceProviderTest;
use PHPUnit\Framework\MockObject\MockObject;
@@ -12,6 +13,8 @@ use Symfony\Component\HttpFoundation\Session\Session;
class AuthenticatorTest extends ServiceProviderTest
{
+ use HasDatabase;
+
/**
* @covers \Engelsystem\Helpers\Authenticator::__construct(
* @covers \Engelsystem\Helpers\Authenticator::user
@@ -29,7 +32,7 @@ class AuthenticatorTest extends ServiceProviderTest
$session->expects($this->exactly(3))
->method('get')
- ->with('uid')
+ ->with('user_id')
->willReturnOnConsecutiveCalls(
null,
42,
@@ -114,16 +117,13 @@ class AuthenticatorTest extends ServiceProviderTest
/** @var User|MockObject $user */
$user = $this->createMock(User::class);
- $user->expects($this->once())
- ->method('save');
-
- $session->expects($this->exactly(2))
+ $session->expects($this->once())
->method('get')
- ->with('uid')
+ ->with('user_id')
->willReturn(42);
$session->expects($this->once())
->method('remove')
- ->with('uid');
+ ->with('user_id');
/** @var Authenticator|MockObject $auth */
$auth = $this->getMockBuilder(Authenticator::class)
@@ -151,4 +151,115 @@ class AuthenticatorTest extends ServiceProviderTest
// Permissions cached
$this->assertTrue($auth->can('bar'));
}
+
+ /**
+ * @covers \Engelsystem\Helpers\Authenticator::authenticate
+ */
+ public function testAuthenticate()
+ {
+ $this->initDatabase();
+
+ /** @var ServerRequestInterface|MockObject $request */
+ $request = $this->getMockForAbstractClass(ServerRequestInterface::class);
+ /** @var Session|MockObject $session */
+ $session = $this->createMock(Session::class);
+ $userRepository = new User();
+
+ (new User([
+ 'name' => 'lorem',
+ 'password' => password_hash('testing', PASSWORD_DEFAULT),
+ 'email' => 'lorem@foo.bar',
+ 'api_key' => '',
+ ]))->save();
+ (new User([
+ 'name' => 'ipsum',
+ 'password' => '',
+ 'email' => 'ipsum@foo.bar',
+ 'api_key' => '',
+ ]))->save();
+
+ $auth = new Authenticator($request, $session, $userRepository);
+ $this->assertNull($auth->authenticate('not-existing', 'foo'));
+ $this->assertNull($auth->authenticate('ipsum', 'wrong-password'));
+ $this->assertInstanceOf(User::class, $auth->authenticate('lorem', 'testing'));
+ $this->assertInstanceOf(User::class, $auth->authenticate('lorem@foo.bar', 'testing'));
+ }
+
+ /**
+ * @covers \Engelsystem\Helpers\Authenticator::verifyPassword
+ */
+ public function testVerifyPassword()
+ {
+ $this->initDatabase();
+ $password = password_hash('testing', PASSWORD_ARGON2I);
+ $user = new User([
+ 'name' => 'lorem',
+ 'password' => $password,
+ 'email' => 'lorem@foo.bar',
+ 'api_key' => '',
+ ]);
+ $user->save();
+
+ /** @var Authenticator|MockObject $auth */
+ $auth = $this->getMockBuilder(Authenticator::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['setPassword'])
+ ->getMock();
+
+ $auth->expects($this->once())
+ ->method('setPassword')
+ ->with($user, 'testing');
+ $auth->setPasswordAlgorithm(PASSWORD_BCRYPT);
+
+ $this->assertFalse($auth->verifyPassword($user, 'randomStuff'));
+ $this->assertTrue($auth->verifyPassword($user, 'testing'));
+ }
+
+ /**
+ * @covers \Engelsystem\Helpers\Authenticator::setPassword
+ */
+ public function testSetPassword()
+ {
+ $this->initDatabase();
+ $user = new User([
+ 'name' => 'ipsum',
+ 'password' => '',
+ 'email' => 'ipsum@foo.bar',
+ 'api_key' => '',
+ ]);
+ $user->save();
+
+ $auth = $this->getAuthenticator();
+ $auth->setPasswordAlgorithm(PASSWORD_ARGON2I);
+
+ $auth->setPassword($user, 'FooBar');
+ $this->assertTrue($user->isClean());
+
+ $this->assertTrue(password_verify('FooBar', $user->password));
+ $this->assertFalse(password_needs_rehash($user->password, PASSWORD_ARGON2I));
+ }
+
+ /**
+ * @covers \Engelsystem\Helpers\Authenticator::setPasswordAlgorithm
+ * @covers \Engelsystem\Helpers\Authenticator::getPasswordAlgorithm
+ */
+ public function testPasswordAlgorithm()
+ {
+ $auth = $this->getAuthenticator();
+
+ $auth->setPasswordAlgorithm(PASSWORD_ARGON2I);
+ $this->assertEquals(PASSWORD_ARGON2I, $auth->getPasswordAlgorithm());
+ }
+
+ /**
+ * @return Authenticator
+ */
+ protected function getAuthenticator()
+ {
+ return new class extends Authenticator
+ {
+ /** @noinspection PhpMissingParentConstructorInspection */
+ public function __construct() { }
+ };
+ }
}
diff --git a/tests/Unit/Helpers/Stub/files/VERSION b/tests/Unit/Helpers/Stub/files/VERSION
new file mode 100644
index 00000000..749a96f3
--- /dev/null
+++ b/tests/Unit/Helpers/Stub/files/VERSION
@@ -0,0 +1 @@
+0.42.0-testing
diff --git a/tests/Unit/Helpers/Translation/Assets/fo_OO/default.mo b/tests/Unit/Helpers/Translation/Assets/fo_OO/default.mo
new file mode 100644
index 00000000..96f1f3ca
--- /dev/null
+++ b/tests/Unit/Helpers/Translation/Assets/fo_OO/default.mo
Binary files differ
diff --git a/tests/Unit/Helpers/Translation/Assets/fo_OO/default.po b/tests/Unit/Helpers/Translation/Assets/fo_OO/default.po
new file mode 100644
index 00000000..015bc36d
--- /dev/null
+++ b/tests/Unit/Helpers/Translation/Assets/fo_OO/default.po
@@ -0,0 +1,3 @@
+# Testing content
+msgid "foo.bar"
+msgstr "Foo Bar!"
diff --git a/tests/Unit/Helpers/Translation/GettextTranslatorTest.php b/tests/Unit/Helpers/Translation/GettextTranslatorTest.php
new file mode 100644
index 00000000..825cf5b7
--- /dev/null
+++ b/tests/Unit/Helpers/Translation/GettextTranslatorTest.php
@@ -0,0 +1,67 @@
+<?php
+
+namespace Engelsystem\Test\Unit\Helpers\Translation;
+
+use Engelsystem\Helpers\Translation\GettextTranslator;
+use Engelsystem\Helpers\Translation\TranslationNotFound;
+use Engelsystem\Test\Unit\ServiceProviderTest;
+use Gettext\Translation;
+use Gettext\Translations;
+
+class GettextTranslatorTest extends ServiceProviderTest
+{
+ /**
+ * @covers \Engelsystem\Helpers\Translation\GettextTranslator::assertHasTranslation()
+ */
+ public function testNoTranslation()
+ {
+ $translations = $this->getTranslations();
+
+ $translator = new GettextTranslator();
+ $translator->loadTranslations($translations);
+
+ $this->assertEquals('Translation!', $translator->gettext('test.value'));
+
+ $this->expectException(TranslationNotFound::class);
+ $this->expectExceptionMessage('//foo.bar');
+
+ $translator->gettext('foo.bar');
+ }
+
+ /**
+ * @covers \Engelsystem\Helpers\Translation\GettextTranslator::dpgettext()
+ */
+ public function testDpgettext()
+ {
+ $translations = $this->getTranslations();
+
+ $translator = new GettextTranslator();
+ $translator->loadTranslations($translations);
+
+ $this->assertEquals('Translation!', $translator->dpgettext(null, null, 'test.value'));
+ }
+
+ /**
+ * @covers \Engelsystem\Helpers\Translation\GettextTranslator::dnpgettext()
+ */
+ public function testDnpgettext()
+ {
+ $translations = $this->getTranslations();
+
+ $translator = new GettextTranslator();
+ $translator->loadTranslations($translations);
+
+ $this->assertEquals('Translations!', $translator->dnpgettext(null, null, 'test.value', 'test.values', 2));
+ }
+
+ protected function getTranslations(): Translations
+ {
+ $translations = new Translations();
+ $translations[] =
+ (new Translation(null, 'test.value', 'test.values'))
+ ->setTranslation('Translation!')
+ ->setPluralTranslations(['Translations!']);
+
+ return $translations;
+ }
+}
diff --git a/tests/Unit/Helpers/TranslationServiceProviderTest.php b/tests/Unit/Helpers/Translation/TranslationServiceProviderTest.php
index 41c08aa5..91307bdd 100644
--- a/tests/Unit/Helpers/TranslationServiceProviderTest.php
+++ b/tests/Unit/Helpers/Translation/TranslationServiceProviderTest.php
@@ -1,10 +1,10 @@
<?php
-namespace Engelsystem\Test\Unit\Helpers;
+namespace Engelsystem\Test\Unit\Helpers\Translation;
use Engelsystem\Config\Config;
-use Engelsystem\Helpers\TranslationServiceProvider;
-use Engelsystem\Helpers\Translator;
+use Engelsystem\Helpers\Translation\TranslationServiceProvider;
+use Engelsystem\Helpers\Translation\Translator;
use Engelsystem\Test\Unit\ServiceProviderTest;
use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Component\HttpFoundation\Session\Session;
@@ -12,13 +12,16 @@ use Symfony\Component\HttpFoundation\Session\Session;
class TranslationServiceProviderTest extends ServiceProviderTest
{
/**
- * @covers \Engelsystem\Helpers\TranslationServiceProvider::register()
+ * @covers \Engelsystem\Helpers\Translation\TranslationServiceProvider::register()
*/
- public function testRegister()
+ public function testRegister(): void
{
+ $defaultLocale = 'fo_OO';
+ $locale = 'te_ST.WTF-9';
+ $locales = ['fo_OO' => 'Foo', 'fo_OO.BAR' => 'Foo (Bar)', 'te_ST.WTF-9' => 'WTF\'s Testing?'];
+ $config = new Config(['locales' => $locales, 'default_locale' => $defaultLocale]);
+
$app = $this->getApp(['make', 'instance', 'get']);
- /** @var Config|MockObject $config */
- $config = $this->createMock(Config::class);
/** @var Session|MockObject $session */
$session = $this->createMock(Session::class);
/** @var Translator|MockObject $translator */
@@ -27,31 +30,14 @@ class TranslationServiceProviderTest extends ServiceProviderTest
/** @var TranslationServiceProvider|MockObject $serviceProvider */
$serviceProvider = $this->getMockBuilder(TranslationServiceProvider::class)
->setConstructorArgs([$app])
- ->setMethods(['initGettext', 'setLocale'])
+ ->setMethods(['setLocale'])
->getMock();
- $serviceProvider->expects($this->once())
- ->method('initGettext');
-
$app->expects($this->exactly(2))
->method('get')
->withConsecutive(['config'], ['session'])
->willReturnOnConsecutiveCalls($config, $session);
- $defaultLocale = 'fo_OO';
- $locale = 'te_ST.WTF-9';
- $locales = ['fo_OO' => 'Foo', 'fo_OO.BAR' => 'Foo (Bar)', 'te_ST.WTF-9' => 'WTF\'s Testing?'];
- $config->expects($this->exactly(2))
- ->method('get')
- ->withConsecutive(
- ['locales'],
- ['default_locale']
- )
- ->willReturnOnConsecutiveCalls(
- $locales,
- $defaultLocale
- );
-
$session->expects($this->once())
->method('get')
->with('locale', $defaultLocale)
@@ -65,9 +51,11 @@ class TranslationServiceProviderTest extends ServiceProviderTest
->with(
Translator::class,
[
- 'locale' => $locale,
- 'locales' => $locales,
- 'localeChangeCallback' => [$serviceProvider, 'setLocale'],
+ 'locale' => $locale,
+ 'locales' => $locales,
+ 'fallbackLocale' => 'en_US',
+ 'getTranslatorCallback' => [$serviceProvider, 'getTranslator'],
+ 'localeChangeCallback' => [$serviceProvider, 'setLocale'],
]
)
->willReturn($translator);
@@ -81,4 +69,22 @@ class TranslationServiceProviderTest extends ServiceProviderTest
$serviceProvider->register();
}
+
+ /**
+ * @covers \Engelsystem\Helpers\Translation\TranslationServiceProvider::getTranslator()
+ */
+ public function testGetTranslator(): void
+ {
+ $app = $this->getApp(['get']);
+ $serviceProvider = new TranslationServiceProvider($app);
+
+ $this->setExpects($app, 'get', ['path.lang'], __DIR__ . '/Assets');
+
+ // Get translator
+ $translator = $serviceProvider->getTranslator('fo_OO');
+ $this->assertEquals('Foo Bar!', $translator->gettext('foo.bar'));
+
+ // Retry from cache
+ $serviceProvider->getTranslator('fo_OO');
+ }
}
diff --git a/tests/Unit/Helpers/Translation/TranslatorTest.php b/tests/Unit/Helpers/Translation/TranslatorTest.php
new file mode 100644
index 00000000..c173209a
--- /dev/null
+++ b/tests/Unit/Helpers/Translation/TranslatorTest.php
@@ -0,0 +1,134 @@
+<?php
+
+namespace Engelsystem\Test\Unit\Helpers\Translation;
+
+use Engelsystem\Helpers\Translation\GettextTranslator;
+use Engelsystem\Helpers\Translation\TranslationNotFound;
+use Engelsystem\Helpers\Translation\Translator;
+use Engelsystem\Test\Unit\ServiceProviderTest;
+use PHPUnit\Framework\MockObject\MockObject;
+use stdClass;
+
+class TranslatorTest extends ServiceProviderTest
+{
+ /**
+ * @covers \Engelsystem\Helpers\Translation\Translator::__construct
+ * @covers \Engelsystem\Helpers\Translation\Translator::getLocale
+ * @covers \Engelsystem\Helpers\Translation\Translator::getLocales
+ * @covers \Engelsystem\Helpers\Translation\Translator::hasLocale
+ * @covers \Engelsystem\Helpers\Translation\Translator::setLocale
+ * @covers \Engelsystem\Helpers\Translation\Translator::setLocales
+ */
+ public function testInit()
+ {
+ $locales = ['te_ST' => 'Tests', 'fo_OO' => 'SomeFOO'];
+ $locale = 'te_ST';
+
+ /** @var callable|MockObject $localeChange */
+ $localeChange = $this->getMockBuilder(stdClass::class)
+ ->setMethods(['__invoke'])
+ ->getMock();
+ $localeChange->expects($this->exactly(2))
+ ->method('__invoke')
+ ->withConsecutive(['te_ST'], ['fo_OO']);
+
+ $translator = new Translator($locale, 'fo_OO', function () { }, $locales, $localeChange);
+
+ $this->assertEquals($locales, $translator->getLocales());
+ $this->assertEquals($locale, $translator->getLocale());
+
+ $translator->setLocale('fo_OO');
+ $this->assertEquals('fo_OO', $translator->getLocale());
+
+ $newLocales = ['lo_RM' => 'Lorem', 'ip_SU-M' => 'Ipsum'];
+ $translator->setLocales($newLocales);
+ $this->assertEquals($newLocales, $translator->getLocales());
+
+ $this->assertTrue($translator->hasLocale('ip_SU-M'));
+ $this->assertFalse($translator->hasLocale('te_ST'));
+ }
+
+ /**
+ * @covers \Engelsystem\Helpers\Translation\Translator::translate
+ */
+ public function testTranslate()
+ {
+ /** @var Translator|MockObject $translator */
+ $translator = $this->getMockBuilder(Translator::class)
+ ->setConstructorArgs(['de_DE', 'en_US', function () { }, ['de_DE' => 'Deutsch']])
+ ->setMethods(['translateText'])
+ ->getMock();
+ $translator->expects($this->exactly(2))
+ ->method('translateText')
+ ->withConsecutive(['gettext', ['Hello!'], []], ['gettext', ['My favourite number is %u!'], [3]])
+ ->willReturnOnConsecutiveCalls('Hallo!', 'Meine Lieblingszahl ist die 3!');
+
+ $return = $translator->translate('Hello!');
+ $this->assertEquals('Hallo!', $return);
+
+ $return = $translator->translate('My favourite number is %u!', [3]);
+ $this->assertEquals('Meine Lieblingszahl ist die 3!', $return);
+ }
+
+ /**
+ * @covers \Engelsystem\Helpers\Translation\Translator::translatePlural
+ */
+ public function testTranslatePlural()
+ {
+ /** @var Translator|MockObject $translator */
+ $translator = $this->getMockBuilder(Translator::class)
+ ->setConstructorArgs(['de_DE', 'en_US', function () { }, ['de_DE' => 'Deutsch']])
+ ->setMethods(['translateText'])
+ ->getMock();
+ $translator->expects($this->once())
+ ->method('translateText')
+ ->with('ngettext', ['%s apple', '%s apples', 2], [2])
+ ->willReturn('2 Äpfel');
+
+ $return = $translator->translatePlural('%s apple', '%s apples', 2, [2]);
+ $this->assertEquals('2 Äpfel', $return);
+ }
+
+ /**
+ * @covers \Engelsystem\Helpers\Translation\Translator::translatePlural
+ * @covers \Engelsystem\Helpers\Translation\Translator::translateText
+ * @covers \Engelsystem\Helpers\Translation\Translator::replaceText
+ */
+ public function testReplaceText()
+ {
+ /** @var GettextTranslator|MockObject $gtt */
+ $gtt = $this->createMock(GettextTranslator::class);
+ /** @var callable|MockObject $getTranslator */
+ $getTranslator = $this->getMockBuilder(stdClass::class)
+ ->setMethods(['__invoke'])
+ ->getMock();
+ $getTranslator->expects($this->exactly(5))
+ ->method('__invoke')
+ ->withConsecutive(['te_ST'], ['fo_OO'], ['te_ST'], ['fo_OO'], ['te_ST'])
+ ->willReturn($gtt);
+
+ $i = 0;
+ $gtt->expects($this->exactly(4))
+ ->method('gettext')
+ ->willReturnCallback(function () use (&$i) {
+ $i++;
+ if ($i != 4) {
+ throw new TranslationNotFound();
+ }
+
+ return 'Lorem %s???';
+ });
+ $this->setExpects($gtt, 'ngettext', ['foo.barf'], 'Lorem %s!');
+
+ $translator = new Translator('te_ST', 'fo_OO', $getTranslator, ['te_ST' => 'Test', 'fo_OO' => 'Foo']);
+
+ // No translation
+ $this->assertEquals('foo.bar', $translator->translate('foo.bar'));
+
+ // Fallback translation
+ $this->assertEquals('Lorem test2???', $translator->translate('foo.batz', ['test2']));
+
+ // Successful translation
+ $this->assertEquals('Lorem test3!', $translator->translatePlural('foo.barf', 'foo.bar2', 3, ['test3']));
+ }
+}
diff --git a/tests/Unit/Helpers/TranslatorTest.php b/tests/Unit/Helpers/TranslatorTest.php
deleted file mode 100644
index 45ca769b..00000000
--- a/tests/Unit/Helpers/TranslatorTest.php
+++ /dev/null
@@ -1,90 +0,0 @@
-<?php
-
-namespace Engelsystem\Test\Unit\Helpers;
-
-use Engelsystem\Helpers\Translator;
-use Engelsystem\Test\Unit\ServiceProviderTest;
-use PHPUnit\Framework\MockObject\MockObject;
-use stdClass;
-
-class TranslatorTest extends ServiceProviderTest
-{
- /**
- * @covers \Engelsystem\Helpers\Translator::__construct
- * @covers \Engelsystem\Helpers\Translator::getLocale
- * @covers \Engelsystem\Helpers\Translator::getLocales
- * @covers \Engelsystem\Helpers\Translator::hasLocale
- * @covers \Engelsystem\Helpers\Translator::setLocale
- * @covers \Engelsystem\Helpers\Translator::setLocales
- */
- public function testInit()
- {
- $locales = ['te_ST.ER-01' => 'Tests', 'fo_OO' => 'SomeFOO'];
- $locale = 'te_ST.ER-01';
-
- /** @var callable|MockObject $callable */
- $callable = $this->getMockBuilder(stdClass::class)
- ->setMethods(['__invoke'])
- ->getMock();
- $callable->expects($this->exactly(2))
- ->method('__invoke')
- ->withConsecutive(['te_ST.ER-01'], ['fo_OO']);
-
- $translator = new Translator($locale, $locales, $callable);
-
- $this->assertEquals($locales, $translator->getLocales());
- $this->assertEquals($locale, $translator->getLocale());
-
- $translator->setLocale('fo_OO');
- $this->assertEquals('fo_OO', $translator->getLocale());
-
- $newLocales = ['lo_RM' => 'Lorem', 'ip_SU-M' => 'Ipsum'];
- $translator->setLocales($newLocales);
- $this->assertEquals($newLocales, $translator->getLocales());
-
- $this->assertTrue($translator->hasLocale('ip_SU-M'));
- $this->assertFalse($translator->hasLocale('te_ST.ER-01'));
- }
-
- /**
- * @covers \Engelsystem\Helpers\Translator::replaceText
- * @covers \Engelsystem\Helpers\Translator::translate
- */
- public function testTranslate()
- {
- /** @var Translator|MockObject $translator */
- $translator = $this->getMockBuilder(Translator::class)
- ->setConstructorArgs(['de_DE.UTF-8', ['de_DE.UTF-8' => 'Deutsch']])
- ->setMethods(['translateGettext'])
- ->getMock();
- $translator->expects($this->exactly(2))
- ->method('translateGettext')
- ->withConsecutive(['Hello!'], ['My favourite number is %u!'])
- ->willReturnOnConsecutiveCalls('Hallo!', 'Meine Lieblingszahl ist die %u!');
-
- $return = $translator->translate('Hello!');
- $this->assertEquals('Hallo!', $return);
-
- $return = $translator->translate('My favourite number is %u!', [3]);
- $this->assertEquals('Meine Lieblingszahl ist die 3!', $return);
- }
-
- /**
- * @covers \Engelsystem\Helpers\Translator::translatePlural
- */
- public function testTranslatePlural()
- {
- /** @var Translator|MockObject $translator */
- $translator = $this->getMockBuilder(Translator::class)
- ->setConstructorArgs(['de_DE.UTF-8', ['de_DE.UTF-8' => 'Deutsch']])
- ->setMethods(['translateGettextPlural'])
- ->getMock();
- $translator->expects($this->once())
- ->method('translateGettextPlural')
- ->with('%s apple', '%s apples', 2)
- ->willReturn('2 Äpfel');
-
- $return = $translator->translatePlural('%s apple', '%s apples', 2, [2]);
- $this->assertEquals('2 Äpfel', $return);
- }
-}
diff --git a/tests/Unit/Helpers/VersionServiceProviderTest.php b/tests/Unit/Helpers/VersionServiceProviderTest.php
new file mode 100644
index 00000000..609c649d
--- /dev/null
+++ b/tests/Unit/Helpers/VersionServiceProviderTest.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Engelsystem\Test\Unit\Helpers;
+
+use Engelsystem\Application;
+use Engelsystem\Helpers\Version;
+use Engelsystem\Helpers\VersionServiceProvider;
+use Engelsystem\Test\Unit\ServiceProviderTest;
+
+class VersionServiceProviderTest extends ServiceProviderTest
+{
+ /**
+ * @covers \Engelsystem\Helpers\VersionServiceProvider::register
+ */
+ public function testRegister()
+ {
+ $app = new Application();
+ $app->instance('path.storage.app', '/tmp');
+
+ $serviceProvider = new VersionServiceProvider($app);
+ $serviceProvider->register();
+
+ $this->assertArrayHasKey(Version::class, $app->contextual);
+ }
+}
diff --git a/tests/Unit/Helpers/VersionTest.php b/tests/Unit/Helpers/VersionTest.php
new file mode 100644
index 00000000..40569abb
--- /dev/null
+++ b/tests/Unit/Helpers/VersionTest.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Engelsystem\Test\Unit\Helpers;
+
+use Engelsystem\Config\Config;
+use Engelsystem\Helpers\Version;
+use Engelsystem\Test\Unit\ServiceProviderTest;
+
+class VersionTest extends ServiceProviderTest
+{
+ /**
+ * @covers \Engelsystem\Helpers\Version::__construct
+ * @covers \Engelsystem\Helpers\Version::getVersion
+ */
+ public function testGetVersion()
+ {
+ $config = new Config();
+ $version = new Version(__DIR__ . '/Stub', $config);
+
+ $this->assertEquals('n/a', $version->getVersion());
+
+ $version = new Version(__DIR__ . '/Stub/files', $config);
+ $this->assertEquals('0.42.0-testing', $version->getVersion());
+
+ $config->set('version', '1.2.3-dev');
+ $this->assertEquals('1.2.3-dev', $version->getVersion());
+ }
+}
diff --git a/tests/Unit/HelpersTest.php b/tests/Unit/HelpersTest.php
index ad677cb3..09362a90 100644
--- a/tests/Unit/HelpersTest.php
+++ b/tests/Unit/HelpersTest.php
@@ -6,7 +6,7 @@ use Engelsystem\Application;
use Engelsystem\Config\Config;
use Engelsystem\Container\Container;
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;
diff --git a/tests/Unit/Http/Exceptions/ValidationExceptionTest.php b/tests/Unit/Http/Exceptions/ValidationExceptionTest.php
new file mode 100644
index 00000000..c5a38b5a
--- /dev/null
+++ b/tests/Unit/Http/Exceptions/ValidationExceptionTest.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Engelsystem\Test\Unit\Http\Exceptions;
+
+use Engelsystem\Http\Exceptions\ValidationException;
+use Engelsystem\Http\Validation\Validator;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+
+class ValidationExceptionTest extends TestCase
+{
+ /**
+ * @covers \Engelsystem\Http\Exceptions\ValidationException::__construct
+ * @covers \Engelsystem\Http\Exceptions\ValidationException::getValidator
+ */
+ public function testConstruct()
+ {
+ /** @var Validator|MockObject $validator */
+ $validator = $this->createMock(Validator::class);
+
+ $exception = new ValidationException($validator);
+
+ $this->assertEquals($validator, $exception->getValidator());
+ }
+}
diff --git a/tests/Unit/Http/UrlGeneratorServiceProviderTest.php b/tests/Unit/Http/UrlGeneratorServiceProviderTest.php
index 61bf3e7c..6d18f160 100644
--- a/tests/Unit/Http/UrlGeneratorServiceProviderTest.php
+++ b/tests/Unit/Http/UrlGeneratorServiceProviderTest.php
@@ -19,7 +19,7 @@ class UrlGeneratorServiceProviderTest extends ServiceProviderTest
$urlGenerator = $this->getMockBuilder(UrlGenerator::class)
->getMock();
- $app = $this->getApp();
+ $app = $this->getApp(['make', 'instance', 'bind']);
$this->setExpects($app, 'make', [UrlGenerator::class], $urlGenerator);
$app->expects($this->exactly(2))
@@ -29,6 +29,9 @@ class UrlGeneratorServiceProviderTest extends ServiceProviderTest
['http.urlGenerator', $urlGenerator],
[UrlGeneratorInterface::class, $urlGenerator]
);
+ $app->expects($this->once())
+ ->method('bind')
+ ->with(UrlGeneratorInterface::class, UrlGenerator::class);
$serviceProvider = new UrlGeneratorServiceProvider($app);
$serviceProvider->register();
diff --git a/tests/Unit/Http/Validation/Rules/InTest.php b/tests/Unit/Http/Validation/Rules/InTest.php
new file mode 100644
index 00000000..e5688d90
--- /dev/null
+++ b/tests/Unit/Http/Validation/Rules/InTest.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace Engelsystem\Test\Unit\Http\Validation\Rules;
+
+use Engelsystem\Http\Validation\Rules\In;
+use Engelsystem\Test\Unit\TestCase;
+
+class InTest extends TestCase
+{
+ /**
+ * @covers \Engelsystem\Http\Validation\Rules\In::__construct
+ */
+ public function testConstruct()
+ {
+ $rule = new In('foo,bar');
+
+ $this->assertEquals(['foo', 'bar'], $rule->haystack);
+ }
+}
diff --git a/tests/Unit/Http/Validation/Rules/NotInTest.php b/tests/Unit/Http/Validation/Rules/NotInTest.php
new file mode 100644
index 00000000..9be12336
--- /dev/null
+++ b/tests/Unit/Http/Validation/Rules/NotInTest.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace Engelsystem\Test\Unit\Http\Validation\Rules;
+
+use Engelsystem\Http\Validation\Rules\NotIn;
+use Engelsystem\Test\Unit\TestCase;
+
+class NotInTest extends TestCase
+{
+ /**
+ * @covers \Engelsystem\Http\Validation\Rules\NotIn::validate
+ */
+ public function testConstruct()
+ {
+ $rule = new NotIn('foo,bar');
+
+ $this->assertTrue($rule->validate('lorem'));
+ $this->assertFalse($rule->validate('foo'));
+ }
+}
diff --git a/tests/Unit/Http/Validation/Stub/ValidatesRequestImplementation.php b/tests/Unit/Http/Validation/Stub/ValidatesRequestImplementation.php
new file mode 100644
index 00000000..772b1dc9
--- /dev/null
+++ b/tests/Unit/Http/Validation/Stub/ValidatesRequestImplementation.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Engelsystem\Test\Unit\Http\Validation\Stub;
+
+use Engelsystem\Controllers\BaseController;
+use Engelsystem\Http\Request;
+
+class ValidatesRequestImplementation extends BaseController
+{
+ /**
+ * @param Request $request
+ * @param array $rules
+ * @return array
+ */
+ public function validateData(Request $request, array $rules)
+ {
+ return $this->validate($request, $rules);
+ }
+
+ /**
+ * @return bool
+ */
+ public function hasValidator()
+ {
+ return !is_null($this->validator);
+ }
+}
diff --git a/tests/Unit/Http/Validation/ValidatesRequestTest.php b/tests/Unit/Http/Validation/ValidatesRequestTest.php
new file mode 100644
index 00000000..8011bd03
--- /dev/null
+++ b/tests/Unit/Http/Validation/ValidatesRequestTest.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Engelsystem\Test\Unit\Http\Validation;
+
+use Engelsystem\Http\Exceptions\ValidationException;
+use Engelsystem\Http\Request;
+use Engelsystem\Http\Validation\Validator;
+use Engelsystem\Test\Unit\Http\Validation\Stub\ValidatesRequestImplementation;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+
+class ValidatesRequestTest extends TestCase
+{
+ /**
+ * @covers \Engelsystem\Http\Validation\ValidatesRequest::validate
+ * @covers \Engelsystem\Http\Validation\ValidatesRequest::setValidator
+ */
+ public function testValidate()
+ {
+ /** @var Validator|MockObject $validator */
+ $validator = $this->createMock(Validator::class);
+ $validator->expects($this->exactly(2))
+ ->method('validate')
+ ->withConsecutive(
+ [['foo' => 'bar'], ['foo' => 'required']],
+ [[], ['foo' => 'required']]
+ )
+ ->willReturnOnConsecutiveCalls(
+ true,
+ false
+ );
+ $validator->expects($this->once())
+ ->method('getData')
+ ->willReturn(['foo' => 'bar']);
+
+ $implementation = new ValidatesRequestImplementation();
+ $implementation->setValidator($validator);
+
+ $return = $implementation->validateData(new Request([], ['foo' => 'bar']), ['foo' => 'required']);
+
+ $this->assertEquals(['foo' => 'bar'], $return);
+
+ $this->expectException(ValidationException::class);
+ $implementation->validateData(new Request([], []), ['foo' => 'required']);
+ }
+}
diff --git a/tests/Unit/Http/Validation/ValidationServiceProviderTest.php b/tests/Unit/Http/Validation/ValidationServiceProviderTest.php
new file mode 100644
index 00000000..969f4351
--- /dev/null
+++ b/tests/Unit/Http/Validation/ValidationServiceProviderTest.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Engelsystem\Test\Unit\Http\Validation;
+
+use Engelsystem\Application;
+use Engelsystem\Http\Validation\ValidationServiceProvider;
+use Engelsystem\Http\Validation\Validator;
+use Engelsystem\Test\Unit\Http\Validation\Stub\ValidatesRequestImplementation;
+use Engelsystem\Test\Unit\ServiceProviderTest;
+use stdClass;
+
+class ValidationServiceProviderTest extends ServiceProviderTest
+{
+ /**
+ * @covers \Engelsystem\Http\Validation\ValidationServiceProvider::register
+ */
+ public function testRegister()
+ {
+ $app = new Application();
+
+ $serviceProvider = new ValidationServiceProvider($app);
+ $serviceProvider->register();
+
+ $this->assertTrue($app->has(Validator::class));
+ $this->assertTrue($app->has('validator'));
+
+ /** @var ValidatesRequestImplementation $validatesRequest */
+ $validatesRequest = $app->make(ValidatesRequestImplementation::class);
+ $this->assertTrue($validatesRequest->hasValidator());
+
+ // Test afterResolving early return
+ $app->make(stdClass::class);
+ }
+}
diff --git a/tests/Unit/Http/Validation/ValidatorTest.php b/tests/Unit/Http/Validation/ValidatorTest.php
new file mode 100644
index 00000000..450e5d4e
--- /dev/null
+++ b/tests/Unit/Http/Validation/ValidatorTest.php
@@ -0,0 +1,142 @@
+<?php
+
+namespace Engelsystem\Test\Unit\Http\Validation;
+
+use Engelsystem\Http\Validation\Validator;
+use InvalidArgumentException;
+use PHPUnit\Framework\TestCase;
+
+class ValidatorTest extends TestCase
+{
+ /**
+ * @covers \Engelsystem\Http\Validation\Validator::validate
+ * @covers \Engelsystem\Http\Validation\Validator::getData
+ * @covers \Engelsystem\Http\Validation\Validator::getErrors
+ */
+ public function testValidate()
+ {
+ $val = new Validator();
+
+ $this->assertTrue($val->validate(
+ ['foo' => 'bar', 'lorem' => 'on', 'dolor' => 'bla'],
+ ['lorem' => 'accepted']
+ ));
+ $this->assertEquals(['lorem' => 'on'], $val->getData());
+
+ $this->assertFalse($val->validate(
+ [],
+ ['lorem' => 'required|min:3']
+ ));
+ $this->assertEquals(
+ ['lorem' => ['validation.lorem.required', 'validation.lorem.min']],
+ $val->getErrors()
+ );
+ }
+
+ /**
+ * @covers \Engelsystem\Http\Validation\Validator::validate
+ */
+ public function testValidateChaining()
+ {
+ $val = new Validator();
+
+ $this->assertTrue($val->validate(
+ ['lorem' => 10],
+ ['lorem' => 'required|min:3|max:10']
+ ));
+ $this->assertTrue($val->validate(
+ ['lorem' => 3],
+ ['lorem' => 'required|min:3|max:10']
+ ));
+
+ $this->assertFalse($val->validate(
+ ['lorem' => 2],
+ ['lorem' => 'required|min:3|max:10']
+ ));
+ $this->assertFalse($val->validate(
+ ['lorem' => 42],
+ ['lorem' => 'required|min:3|max:10']
+ ));
+ }
+
+ /**
+ * @covers \Engelsystem\Http\Validation\Validator::validate
+ */
+ public function testValidateNotImplemented()
+ {
+ $val = new Validator();
+
+ $this->expectException(InvalidArgumentException::class);
+
+ $val->validate(
+ ['lorem' => 'bar'],
+ ['foo' => 'never_implemented']
+ );
+ }
+
+ /**
+ * @covers \Engelsystem\Http\Validation\Validator::map
+ * @covers \Engelsystem\Http\Validation\Validator::mapBack
+ */
+ public function testValidateMapping()
+ {
+ $val = new Validator();
+
+ $this->assertTrue($val->validate(
+ ['foo' => 'bar'],
+ ['foo' => 'required']
+ ));
+ $this->assertTrue($val->validate(
+ ['foo' => '0'],
+ ['foo' => 'int']
+ ));
+ $this->assertTrue($val->validate(
+ ['foo' => 'on'],
+ ['foo' => 'accepted']
+ ));
+
+ $this->assertFalse($val->validate(
+ [],
+ ['lorem' => 'required']
+ ));
+ $this->assertEquals(
+ ['lorem' => ['validation.lorem.required']],
+ $val->getErrors()
+ );
+ }
+
+ /**
+ * @covers \Engelsystem\Http\Validation\Validator::validate
+ */
+ public function testValidateNesting()
+ {
+ $val = new Validator();
+
+ $this->assertTrue($val->validate(
+ [],
+ ['foo' => 'not|required']
+ ));
+
+ $this->assertTrue($val->validate(
+ ['foo' => 'foo'],
+ ['foo' => 'not|int']
+ ));
+ $this->assertFalse($val->validate(
+ ['foo' => 1],
+ ['foo' => 'not|int']
+ ));
+
+ $this->assertTrue($val->validate(
+ [],
+ ['foo' => 'optional|int']
+ ));
+ $this->assertTrue($val->validate(
+ ['foo' => '33'],
+ ['foo' => 'optional|int']
+ ));
+ $this->assertFalse($val->validate(
+ ['foo' => 'T'],
+ ['foo' => 'optional|int']
+ ));
+ }
+}
diff --git a/tests/Unit/Middleware/ErrorHandlerTest.php b/tests/Unit/Middleware/ErrorHandlerTest.php
index 6c37b651..a9fdd71a 100644
--- a/tests/Unit/Middleware/ErrorHandlerTest.php
+++ b/tests/Unit/Middleware/ErrorHandlerTest.php
@@ -2,14 +2,23 @@
namespace Engelsystem\Test\Unit\Middleware;
+use Engelsystem\Application;
use Engelsystem\Http\Exceptions\HttpException;
+use Engelsystem\Http\Exceptions\ValidationException;
+use Engelsystem\Http\Psr7ServiceProvider;
+use Engelsystem\Http\Request;
use Engelsystem\Http\Response;
+use Engelsystem\Http\ResponseServiceProvider;
+use Engelsystem\Http\Validation\Validator;
use Engelsystem\Middleware\ErrorHandler;
use Engelsystem\Test\Unit\Middleware\Stub\ReturnResponseMiddlewareHandler;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+use Symfony\Component\HttpFoundation\Session\Session;
+use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
use Twig_LoaderInterface as TwigLoader;
class ErrorHandlerTest extends TestCase
@@ -104,7 +113,7 @@ class ErrorHandlerTest extends TestCase
/**
* @covers \Engelsystem\Middleware\ErrorHandler::process
*/
- public function testProcessException()
+ public function testProcessHttpException()
{
/** @var ServerRequestInterface|MockObject $request */
$request = $this->createMock(ServerRequestInterface::class);
@@ -146,6 +155,67 @@ class ErrorHandlerTest extends TestCase
/**
* @covers \Engelsystem\Middleware\ErrorHandler::process
+ * @covers \Engelsystem\Middleware\ErrorHandler::getPreviousUrl
+ */
+ public function testProcessValidationException()
+ {
+ /** @var TwigLoader|MockObject $twigLoader */
+ $twigLoader = $this->createMock(TwigLoader::class);
+ $handler = $this->getMockForAbstractClass(RequestHandlerInterface::class);
+ $validator = $this->createMock(Validator::class);
+
+ $handler->expects($this->exactly(2))
+ ->method('handle')
+ ->willReturnCallback(function () use ($validator) {
+ throw new ValidationException($validator);
+ });
+
+ $validator->expects($this->exactly(2))
+ ->method('getErrors')
+ ->willReturn(['foo' => ['validation.foo.numeric']]);
+
+ $session = new Session(new MockArraySessionStorage());
+ $session->set('errors', ['validation' => ['foo' => ['validation.foo.required']]]);
+ $request = Request::create(
+ '/foo/bar',
+ 'POST',
+ ['foo' => 'bar', 'password' => 'Test123', 'password_confirmation' => 'Test1234']
+ );
+ $request->setSession($session);
+
+ /** @var Application $app */
+ $app = app();
+ (new ResponseServiceProvider($app))->register();
+ (new Psr7ServiceProvider($app))->register();
+
+ $errorHandler = new ErrorHandler($twigLoader);
+
+ $return = $errorHandler->process($request, $handler);
+
+ $this->assertEquals(302, $return->getStatusCode());
+ $this->assertEquals('/', $return->getHeaderLine('location'));
+ $this->assertEquals([
+ 'errors' => [
+ 'validation' => [
+ 'foo' => [
+ 'validation.foo.required',
+ 'validation.foo.numeric',
+ ],
+ ],
+ ],
+ 'form-data' => [
+ 'foo' => 'bar',
+ ],
+ ], $session->all());
+
+ $request = $request->withAddedHeader('referer', '/foo/batz');
+ $return = $errorHandler->process($request, $handler);
+
+ $this->assertEquals('/foo/batz', $return->getHeaderLine('location'));
+ }
+
+ /**
+ * @covers \Engelsystem\Middleware\ErrorHandler::process
*/
public function testProcessContentTypeSniffer()
{
@@ -153,7 +223,7 @@ class ErrorHandlerTest extends TestCase
$request = $this->createMock(ServerRequestInterface::class);
/** @var TwigLoader|MockObject $twigLoader */
$twigLoader = $this->createMock(TwigLoader::class);
- $response = new Response('<!DOCTYPE html><html><body><h1>Hi!</h1></body></html>', 500);
+ $response = new Response('<!DOCTYPE html><html lang="en"><body><h1>Hi!</h1></body></html>', 500);
$returnResponseHandler = new ReturnResponseMiddlewareHandler($response);
/** @var ErrorHandler|MockObject $errorHandler */
diff --git a/tests/Unit/Middleware/LegacyMiddlewareTest.php b/tests/Unit/Middleware/LegacyMiddlewareTest.php
index f14a38ed..cce7371a 100644
--- a/tests/Unit/Middleware/LegacyMiddlewareTest.php
+++ b/tests/Unit/Middleware/LegacyMiddlewareTest.php
@@ -3,7 +3,7 @@
namespace Engelsystem\Test\Unit\Middleware;
use Engelsystem\Helpers\Authenticator;
-use Engelsystem\Helpers\Translator;
+use Engelsystem\Helpers\Translation\Translator;
use Engelsystem\Http\Request;
use Engelsystem\Middleware\LegacyMiddleware;
use PHPUnit\Framework\MockObject\MockObject;
diff --git a/tests/Unit/Middleware/SetLocaleTest.php b/tests/Unit/Middleware/SetLocaleTest.php
index dc68d83a..a586f6b7 100644
--- a/tests/Unit/Middleware/SetLocaleTest.php
+++ b/tests/Unit/Middleware/SetLocaleTest.php
@@ -2,7 +2,7 @@
namespace Engelsystem\Test\Unit\Middleware;
-use Engelsystem\Helpers\Translator;
+use Engelsystem\Helpers\Translation\Translator;
use Engelsystem\Middleware\SetLocale;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
diff --git a/tests/Unit/Renderer/EngineTest.php b/tests/Unit/Renderer/EngineTest.php
new file mode 100644
index 00000000..659d85c5
--- /dev/null
+++ b/tests/Unit/Renderer/EngineTest.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Engelsystem\Test\Unit\Renderer;
+
+use Engelsystem\Test\Unit\Renderer\Stub\EngineImplementation;
+use PHPUnit\Framework\TestCase;
+
+class EngineTest extends TestCase
+{
+ /**
+ * @covers \Engelsystem\Renderer\Engine::share
+ */
+ public function testShare()
+ {
+ $engine = new EngineImplementation();
+ $engine->share(['foo' => ['bar' => 'baz', 'lorem' => 'ipsum']]);
+ $engine->share(['foo' => ['lorem' => 'dolor']]);
+ $engine->share('key', 'value');
+
+ $this->assertEquals(
+ ['foo' => ['bar' => 'baz', 'lorem' => 'dolor'], 'key' => 'value'],
+ $engine->getSharedData()
+ );
+ }
+}
diff --git a/tests/Unit/Renderer/HtmlEngineTest.php b/tests/Unit/Renderer/HtmlEngineTest.php
index 4a31e4bc..f76e7528 100644
--- a/tests/Unit/Renderer/HtmlEngineTest.php
+++ b/tests/Unit/Renderer/HtmlEngineTest.php
@@ -16,11 +16,12 @@ class HtmlEngineTest extends TestCase
public function testGet()
{
$engine = new HtmlEngine();
+ $engine->share('shared_data', 'tester');
- $file = $this->createTempFile('<div>%main_content%</div>');
+ $file = $this->createTempFile('<div>%main_content% is a %shared_data%</div>');
$data = $engine->get($file, ['main_content' => 'Lorem ipsum dolor sit']);
- $this->assertEquals('<div>Lorem ipsum dolor sit</div>', $data);
+ $this->assertEquals('<div>Lorem ipsum dolor sit is a tester</div>', $data);
}
/**
diff --git a/tests/Unit/Renderer/Stub/EngineImplementation.php b/tests/Unit/Renderer/Stub/EngineImplementation.php
new file mode 100644
index 00000000..fc436569
--- /dev/null
+++ b/tests/Unit/Renderer/Stub/EngineImplementation.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace Engelsystem\Test\Unit\Renderer\Stub;
+
+use Engelsystem\Renderer\Engine;
+
+class EngineImplementation extends Engine
+{
+ /**
+ * @inheritdoc
+ */
+ public function get(string $path, array $data = []): string
+ {
+ return '';
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function canRender(string $path): bool
+ {
+ return true;
+ }
+
+ /**
+ * @return array
+ */
+ public function getSharedData(): array
+ {
+ return $this->sharedData;
+ }
+}
diff --git a/tests/Unit/Renderer/Twig/Extensions/TranslationTest.php b/tests/Unit/Renderer/Twig/Extensions/TranslationTest.php
index 18705683..0b055c44 100644
--- a/tests/Unit/Renderer/Twig/Extensions/TranslationTest.php
+++ b/tests/Unit/Renderer/Twig/Extensions/TranslationTest.php
@@ -2,7 +2,7 @@
namespace Engelsystem\Test\Unit\Renderer\Twig\Extensions;
-use Engelsystem\Helpers\Translator;
+use Engelsystem\Helpers\Translation\Translator;
use Engelsystem\Renderer\Twig\Extensions\Translation;
use PHPUnit\Framework\MockObject\MockObject;
use Twig_Extensions_TokenParser_Trans as TranslationTokenParser;
diff --git a/tests/Unit/Renderer/TwigEngineTest.php b/tests/Unit/Renderer/TwigEngineTest.php
index 9d0618f1..5e5e59d9 100644
--- a/tests/Unit/Renderer/TwigEngineTest.php
+++ b/tests/Unit/Renderer/TwigEngineTest.php
@@ -20,16 +20,16 @@ class TwigEngineTest extends TestCase
$twig = $this->createMock(Twig::class);
$path = 'foo.twig';
- $data = ['lorem' => 'ipsum'];
-
$twig->expects($this->once())
->method('render')
- ->with($path, $data)
- ->willReturn('LoremIpsum!');
+ ->with($path, ['lorem' => 'ipsum', 'shared' => 'data'])
+ ->willReturn('LoremIpsum data!');
$engine = new TwigEngine($twig);
- $return = $engine->get($path, $data);
- $this->assertEquals('LoremIpsum!', $return);
+ $engine->share('shared', 'data');
+
+ $return = $engine->get($path, ['lorem' => 'ipsum']);
+ $this->assertEquals('LoremIpsum data!', $return);
}