Merge pull request #622 from MyIgel/controllers

AuthController (login, logout), use templating, replaced gettext, input validation
This commit is contained in:
msquare 2019-07-21 13:32:45 +02:00 committed by GitHub
commit d4d4b409b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
81 changed files with 1935 additions and 525 deletions

View File

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

View File

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

View File

@ -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,6 +25,7 @@ return [
\Engelsystem\Middleware\RouteDispatcherServiceProvider::class,
\Engelsystem\Middleware\RequestHandlerServiceProvider::class,
\Engelsystem\Middleware\SessionHandlerServiceProvider::class,
\Engelsystem\Http\Validation\ValidationServiceProvider::class,
// Additional services
\Engelsystem\Mail\MailerServiceProvider::class,

View File

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

View File

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

View File

@ -35,8 +35,8 @@ RUN rm -f /app/import/* /app/config/config.php
# 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

View File

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

View File

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

View File

@ -47,6 +47,7 @@ function users_controller()
function user_delete_controller()
{
$user = auth()->user();
$auth = auth();
$request = request();
if ($request->has('user_id')) {
@ -68,12 +69,10 @@ 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.'));
}
@ -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'));

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"
@ -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."
#: includes/pages/guest_login.php:451 includes/view/User_view.php:130
#~ msgid "auth.no-nickname"
#~ msgstr "Gib bitte einen Nick an."
#: 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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 logout()
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(): Response
{
$this->session->invalidate();

View File

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

View File

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

View File

@ -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()) {
if ($user) {
$this->permissions = $this->getPermissionsByUser($user);
$user->last_login_at = new Carbon();
$user->save();
} else {
$this->session->remove('uid');
}
} 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,16 @@ 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',
];
/**
* @param TwigLoader $loader
*/
@ -43,6 +56,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 +134,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 '/';
}
}

View File

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

View File

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

22
src/Renderer/Engine.php Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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('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('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');
$response->expects($this->once())
->method('redirectTo')
->with('https://foo.bar/');
$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];
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

View File

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

View File

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

View File

@ -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)
@ -67,6 +53,8 @@ class TranslationServiceProviderTest extends ServiceProviderTest
[
'locale' => $locale,
'locales' => $locales,
'fallbackLocale' => 'en_US',
'getTranslatorCallback' => [$serviceProvider, 'getTranslator'],
'localeChangeCallback' => [$serviceProvider, 'setLocale'],
]
)
@ -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');
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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