Merge branch 'master' into docker

main
Igor Scheller 6 years ago committed by GitHub
commit 43fa21f655
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

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

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

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

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

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

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

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

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

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

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

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

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

@ -8,14 +8,6 @@ use Engelsystem\Models\User\Settings;
use Engelsystem\Models\User\State;
use Engelsystem\Models\User\User;
/**
* @return string
*/
function login_title()
{
return __('Login');
}
/**
* @return string
*/
@ -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);
}

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

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

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

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

@ -0,0 +1,5 @@
{% extends "errors/default.twig" %}
{% block title %}{{ __("405: Method not allowed") }}{% endblock %}
{% block content_headline_text %}{{ __("405: Method not allowed") }}{% endblock %}

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

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

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

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

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

@ -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 = [];

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

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

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

@ -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)) {
@ -131,6 +132,78 @@ class Authenticator
return true;
}
/**
* @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

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

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

@ -0,0 +1,9 @@
<?php
namespace Engelsystem\Helpers\Translation;
use Exception;
class TranslationNotFound extends Exception
{
}

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

@ -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);
}
@ -74,32 +109,6 @@ class Translator
return call_user_func_array('sprintf', array_merge([$key], $replace));
}
/**
* 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
*/

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

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

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

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

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

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

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

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

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

@ -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;
@ -18,6 +21,22 @@ class ErrorHandler implements MiddlewareInterface
/** @var string */
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
*/
@ -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 '/';
}
}

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

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

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

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

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

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

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

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

@ -0,0 +1,2 @@
/*
!.gitignore

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

@ -21,5 +21,7 @@ class BaseControllerTest extends TestCase
'dolor',
],
], $controller->getPermissions());
$this->assertTrue(method_exists($controller, 'setValidator'));
}
}

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

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

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

@ -14,12 +14,4 @@ class ControllerImplementation extends BaseController
'dolor',
],
];
/**
* @param array $permissions
*/
public function setPermissions(array $permissions)
{
$this->permissions = $permissions;
}
}

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

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

@ -0,0 +1,3 @@
# Testing content
msgid "foo.bar"
msgstr "Foo Bar!"

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -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);
@ -144,6 +153,67 @@ class ErrorHandlerTest extends TestCase
$this->assertEquals($psrResponse, $return);
}
/**
* @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
*/
@ -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 */

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

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

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

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

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

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

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

Loading…
Cancel
Save