From 8f8130634e40f6a24295b7bab449a43ed7c5aa80 Mon Sep 17 00:00:00 2001 From: Igor Scheller Date: Tue, 8 Oct 2019 15:23:46 +0200 Subject: Show normal login page after registration, added msg() template function --- resources/views/pages/login.twig | 1 + 1 file changed, 1 insertion(+) (limited to 'resources') diff --git a/resources/views/pages/login.twig b/resources/views/pages/login.twig index 88326429..6160508f 100644 --- a/resources/views/pages/login.twig +++ b/resources/views/pages/login.twig @@ -32,6 +32,7 @@
+ {{ msg() }} {% for message in errors|default([]) %} {{ m.alert(__(message), 'danger') }} {% endfor %} -- cgit v1.2.3-54-g00ecf From dd0366296893a0e8da8ae0365387dd4823d53451 Mon Sep 17 00:00:00 2001 From: Igor Scheller Date: Tue, 8 Oct 2019 16:17:06 +0200 Subject: Rebuild password reset --- config/routes.php | 6 + includes/controller/users_controller.php | 115 --------- includes/model/User_model.php | 19 -- includes/view/User_view.php | 35 --- resources/lang/de_DE/default.mo | Bin 46206 -> 46693 bytes resources/lang/de_DE/default.po | 22 +- resources/lang/en_US/default.mo | Bin 770 -> 1240 bytes resources/lang/en_US/default.po | 18 ++ resources/lang/pt_BR/default.mo | Bin 41129 -> 41132 bytes resources/lang/pt_BR/default.po | 2 +- resources/views/emails/mail.twig | 8 +- resources/views/emails/password-reset.twig | 3 + resources/views/macros/form.twig | 18 ++ resources/views/pages/login.twig | 2 +- resources/views/pages/password/reset-form.twig | 18 ++ resources/views/pages/password/reset-success.twig | 12 + resources/views/pages/password/reset.twig | 32 +++ src/Controllers/PasswordResetController.php | 167 +++++++++++++ src/Middleware/LegacyMiddleware.php | 6 - .../Controllers/PasswordResetControllerTest.php | 266 +++++++++++++++++++++ 20 files changed, 566 insertions(+), 183 deletions(-) create mode 100644 resources/views/emails/password-reset.twig create mode 100644 resources/views/macros/form.twig create mode 100644 resources/views/pages/password/reset-form.twig create mode 100644 resources/views/pages/password/reset-success.twig create mode 100644 resources/views/pages/password/reset.twig create mode 100644 src/Controllers/PasswordResetController.php create mode 100644 tests/Unit/Controllers/PasswordResetControllerTest.php (limited to 'resources') diff --git a/config/routes.php b/config/routes.php index 02fd3abd..e57d3079 100644 --- a/config/routes.php +++ b/config/routes.php @@ -13,6 +13,12 @@ $route->get('/login', 'AuthController@login'); $route->post('/login', 'AuthController@postLogin'); $route->get('/logout', 'AuthController@logout'); +// Password recovery +$route->get('/password/reset', 'PasswordResetController@reset'); +$route->post('/password/reset', 'PasswordResetController@postReset'); +$route->get('/password/reset/{token:.+}', 'PasswordResetController@resetPassword'); +$route->post('/password/reset/{token:.+}', 'PasswordResetController@postResetPassword'); + // Stats $route->get('/metrics', 'Metrics\\Controller@metrics'); $route->get('/stats', 'Metrics\\Controller@stats'); diff --git a/includes/controller/users_controller.php b/includes/controller/users_controller.php index 892089e7..3ad2ffd9 100644 --- a/includes/controller/users_controller.php +++ b/includes/controller/users_controller.php @@ -1,7 +1,6 @@ input('token'))->first(); - if (!$passwordReset) { - error(__('Token is not correct.')); - redirect(page_link_to('login')); - } - - if ($request->hasPostData('submit')) { - $valid = true; - - if ( - $request->has('password') - && strlen($request->postData('password')) >= config('min_password_length') - ) { - if ($request->postData('password') != $request->postData('password2')) { - $valid = false; - error(__('Your passwords don\'t match.')); - } - } else { - $valid = false; - error(__('Your password is to short (please use at least 6 characters).')); - } - - if ($valid) { - auth()->setPassword($passwordReset->user, $request->postData('password')); - success(__('Password saved.')); - $passwordReset->delete(); - redirect(page_link_to('login')); - } - } - - return User_password_set_view(); -} - -/** - * First step of password recovery: display a form that asks for your email and send email with recovery link - * - * @return string - */ -function user_password_recovery_start_controller() -{ - $request = request(); - if ($request->hasPostData('submit')) { - $valid = true; - - $user_source = null; - if ($request->has('email') && strlen(strip_request_item('email')) > 0) { - $email = strip_request_item('email'); - if (check_email($email)) { - /** @var User $user_source */ - $user_source = User::whereEmail($email)->first(); - if (!$user_source) { - $valid = false; - error(__('E-mail address is not correct.')); - } - } else { - $valid = false; - error(__('E-mail address is not correct.')); - } - } else { - $valid = false; - error(__('Please enter your e-mail.')); - } - - if ($valid) { - $token = User_generate_password_recovery_token($user_source); - engelsystem_email_to_user( - $user_source, - __('Password recovery'), - sprintf( - __('Please visit %s to recover your password.'), - page_link_to('user_password_recovery', ['token' => $token]) - ) - ); - success(__('We sent an email containing your password recovery link.')); - redirect(page_link_to('login')); - } - } - - return User_password_recovery_view(); -} - -/** - * User password recovery in 2 steps. - * (By email) - * - * @return string - */ -function user_password_recovery_controller() -{ - if (request()->has('token')) { - return user_password_recovery_set_new_controller(); - } - - return user_password_recovery_start_controller(); -} - -/** - * Menu title for password recovery. - * - * @return string - */ -function user_password_recovery_title() -{ - return __('Password recovery'); -} - /** * Loads a user from param user_id. * diff --git a/includes/model/User_model.php b/includes/model/User_model.php index 1994bc47..681e70aa 100644 --- a/includes/model/User_model.php +++ b/includes/model/User_model.php @@ -2,7 +2,6 @@ use Carbon\Carbon; use Engelsystem\Database\DB; -use Engelsystem\Models\User\PasswordReset; use Engelsystem\Models\User\User; use Engelsystem\ValidationResult; use Illuminate\Database\Query\JoinClause; @@ -227,24 +226,6 @@ function User_reset_api_key($user, $log = true) } } -/** - * Generates a new password recovery token for given user. - * - * @param User $user - * @return string - */ -function User_generate_password_recovery_token($user) -{ - $reset = PasswordReset::findOrNew($user->id); - $reset->user_id = $user->id; - $reset->token = md5($user->name . time() . rand()); - $reset->save(); - - engelsystem_log('Password recovery for ' . User_Nick_render($user, true) . ' started.'); - - return $reset->token; -} - /** * @param User $user * @return float diff --git a/includes/view/User_view.php b/includes/view/User_view.php index b38a5062..95ecb626 100644 --- a/includes/view/User_view.php +++ b/includes/view/User_view.php @@ -759,41 +759,6 @@ function User_view_state_admin($freeloader, $user_source) return $state; } -/** - * View for password recovery step 1: E-Mail - * - * @return string - */ -function User_password_recovery_view() -{ - return page_with_title(user_password_recovery_title(), [ - msg(), - __('We will send you an e-mail with a password recovery link. Please use the email address you used for registration.'), - form([ - form_text('email', __('E-Mail'), ''), - form_submit('submit', __('Recover')) - ]) - ]); -} - -/** - * View for password recovery step 2: New password - * - * @return string - */ -function User_password_set_view() -{ - return page_with_title(user_password_recovery_title(), [ - msg(), - __('Please enter a new password.'), - form([ - form_password('password', __('Password')), - form_password('password2', __('Confirm password')), - form_submit('submit', __('Save')) - ]) - ]); -} - /** * @param array[] $user_angeltypes * @return string diff --git a/resources/lang/de_DE/default.mo b/resources/lang/de_DE/default.mo index fb93d590..d4b7885b 100644 Binary files a/resources/lang/de_DE/default.mo and b/resources/lang/de_DE/default.mo differ diff --git a/resources/lang/de_DE/default.po b/resources/lang/de_DE/default.po index 091e1114..d6dc38bb 100644 --- a/resources/lang/de_DE/default.po +++ b/resources/lang/de_DE/default.po @@ -619,9 +619,9 @@ msgid "Please visit %s to recover your password." msgstr "Bitte besuche %s, um Dein Passwort zurückzusetzen" #: includes/controller/users_controller.php:394 -msgid "We sent an email containing your password recovery link." +msgid "We sent you an email containing your password recovery link." msgstr "" -"Wir haben eine eMail mit einem Link zum Passwort-zurücksetzen geschickt." +"Wir haben dir eine eMail mit einem Link zum Passwort-zurücksetzen geschickt." #: includes/helper/email_helper.php:41 #, php-format @@ -2769,3 +2769,21 @@ msgstr "Bitte gib ein Passwort an." msgid "validation.login.required" msgstr "Bitte gib einen Loginnamen an." + +msgid "form.submit" +msgstr "Absenden" + +msgid "validation.email.required" +msgstr "Bitte gib eine E-Mail-Adresse an." + +msgid "validation.email.email" +msgstr "Die E-Mail-Adresse ist nicht gültig." + +msgid "validation.password.min" +msgstr "Dein angegebenes Passwort ist zu kurz." + +msgid "validation.password.confirmed" +msgstr "Deine Passwörter stimmen nicht überein." + +msgid "validation.password_confirmation.required" +msgstr "Du musst dein Passwort bestätigen." diff --git a/resources/lang/en_US/default.mo b/resources/lang/en_US/default.mo index 7ef9c3b2..30dd375d 100644 Binary files a/resources/lang/en_US/default.mo and b/resources/lang/en_US/default.mo differ diff --git a/resources/lang/en_US/default.po b/resources/lang/en_US/default.po index 1ff16c83..a2d56fd1 100644 --- a/resources/lang/en_US/default.po +++ b/resources/lang/en_US/default.po @@ -30,3 +30,21 @@ msgstr "The password is required." msgid "validation.login.required" msgstr "The login name is required." + +msgid "form.submit" +msgstr "Submit" + +msgid "validation.email.required" +msgstr "The email address is required." + +msgid "validation.email.email" +msgstr "This email address is not valid." + +msgid "validation.password.min" +msgstr "Your password is too short." + +msgid "validation.password.confirmed" +msgstr "Your passwords are not equal." + +msgid "validation.password_confirmation.required" +msgstr "You have to confirm your password." diff --git a/resources/lang/pt_BR/default.mo b/resources/lang/pt_BR/default.mo index 8b864156..d15826e6 100644 Binary files a/resources/lang/pt_BR/default.mo and b/resources/lang/pt_BR/default.mo differ diff --git a/resources/lang/pt_BR/default.po b/resources/lang/pt_BR/default.po index b9bf420d..e5e5371e 100644 --- a/resources/lang/pt_BR/default.po +++ b/resources/lang/pt_BR/default.po @@ -551,7 +551,7 @@ msgid "Please visit %s to recover your password." msgstr "Por favor visite %s para recuperar sua senha" #: includes/controller/users_controller.php:271 -msgid "We sent an email containing your password recovery link." +msgid "We sent you an email containing your password recovery link." msgstr "Nós enviamos um email com o link para recuperação da sua senha." #: includes/helper/email_helper.php:12 diff --git a/resources/views/emails/mail.twig b/resources/views/emails/mail.twig index ec70f594..e0ad7b64 100644 --- a/resources/views/emails/mail.twig +++ b/resources/views/emails/mail.twig @@ -1,6 +1,6 @@ -{{ __('Hi %s,', [username]) }} +{% block title %}{{ __('Hi %s,', [username]) }}{% endblock %} -{{ __('here is a message for you from the %s:', [config('app_name')]) }} -{{ message|raw }} +{% block introduction %}{{ __('here is a message for you from the %s:', [config('app_name')]) }}{% endblock %} +{% block message %}{{ message|raw }}{% endblock %} -{{ __('This email is autogenerated and has not been signed. You got this email because you are registered in the %s.', [config('app_name')]) }} +{% block footer %}{{ __('This email is autogenerated and has not been signed. You got this email because you are registered in the %s.', [config('app_name')]) }}{% endblock %} diff --git a/resources/views/emails/password-reset.twig b/resources/views/emails/password-reset.twig new file mode 100644 index 00000000..30b613b4 --- /dev/null +++ b/resources/views/emails/password-reset.twig @@ -0,0 +1,3 @@ +{% extends "emails/mail.twig" %} + +{% block message %}{{ __('Please visit %s to recover your password.', [url('/password/reset/') ~ reset.token]) }}{% endblock %} diff --git a/resources/views/macros/form.twig b/resources/views/macros/form.twig new file mode 100644 index 00000000..5d41b085 --- /dev/null +++ b/resources/views/macros/form.twig @@ -0,0 +1,18 @@ +{% macro input(name, label, type, required) %} +
+ {% if label %} + + {% endif %} + +
+{% endmacro %} + +{% macro hidden(name, value) %} + +{% endmacro %} + +{% macro submit(label) %} + +{% endmacro %} diff --git a/resources/views/pages/login.twig b/resources/views/pages/login.twig index 6160508f..34dbd63f 100644 --- a/resources/views/pages/login.twig +++ b/resources/views/pages/login.twig @@ -62,7 +62,7 @@
diff --git a/resources/views/pages/password/reset-form.twig b/resources/views/pages/password/reset-form.twig new file mode 100644 index 00000000..60eb2499 --- /dev/null +++ b/resources/views/pages/password/reset-form.twig @@ -0,0 +1,18 @@ +{% extends "pages/password/reset.twig" %} +{% import 'macros/base.twig' as m %} +{% import 'macros/form.twig' as f %} + +{% block row_content %} +
+
+ {{ csrf() }} + + {{ f.input('password', __('Password'), 'password', true) }} + {{ f.input('password_confirmation', __('Confirm password'), 'password', true) }} + +
+ {{ f.submit(__('Save')) }} +
+
+
+{% endblock %} diff --git a/resources/views/pages/password/reset-success.twig b/resources/views/pages/password/reset-success.twig new file mode 100644 index 00000000..19b8a93e --- /dev/null +++ b/resources/views/pages/password/reset-success.twig @@ -0,0 +1,12 @@ +{% extends "pages/password/reset.twig" %} +{% import 'macros/base.twig' as m %} + +{% block row_content %} +
+ {% if type == 'email' %} + {{ m.alert(__('We sent you an email containing your password recovery link.'), 'info') }} + {% elseif type == 'reset' %} + {{ m.alert(__('Password saved.'), 'success') }} + {% endif %} +
+{% endblock %} diff --git a/resources/views/pages/password/reset.twig b/resources/views/pages/password/reset.twig new file mode 100644 index 00000000..289152ea --- /dev/null +++ b/resources/views/pages/password/reset.twig @@ -0,0 +1,32 @@ +{% extends 'layouts/app.twig' %} +{% import 'macros/base.twig' as m %} +{% import 'macros/form.twig' as f %} + +{% block title %}{{ __('Password recovery') }}{% endblock %} + +{% block content %} +
+

{{ __('Password recovery') }}

+ + {% for message in errors|default([]) %} + {{ m.alert(__(message), 'danger') }} + {% endfor %} + +
+ {% block row_content %} +
+
+ {{ csrf() }} + + {{ __('We will send you an e-mail with a password recovery link. Please use the email address you used for registration.') }} + {{ f.input('email', __('E-Mail'), 'email', true) }} + +
+ {{ f.submit(__('Recover')) }} +
+
+
+ {% endblock %} +
+
+{% endblock %} diff --git a/src/Controllers/PasswordResetController.php b/src/Controllers/PasswordResetController.php new file mode 100644 index 00000000..505ed8eb --- /dev/null +++ b/src/Controllers/PasswordResetController.php @@ -0,0 +1,167 @@ + 'login', + 'postReset' => 'login', + 'resetPassword' => 'login', + 'postResetPassword' => 'login', + ]; + + /** + * @param Response $response + * @param SessionInterface $session + * @param EngelsystemMailer $mail + * @param LoggerInterface $log + */ + public function __construct( + Response $response, + SessionInterface $session, + EngelsystemMailer $mail, + LoggerInterface $log + ) { + $this->log = $log; + $this->mail = $mail; + $this->response = $response; + $this->session = $session; + } + + /** + * @return Response + */ + public function reset(): Response + { + return $this->showView('pages/password/reset'); + } + + /** + * @param Request $request + * @return Response + */ + public function postReset(Request $request): Response + { + $data = $this->validate($request, [ + 'email' => 'required|email', + ]); + + /** @var User $user */ + $user = User::whereEmail($data['email'])->first(); + if ($user) { + $reset = PasswordReset::findOrNew($user->id); + $reset->user_id = $user->id; + $reset->token = md5(random_bytes(64)); + $reset->save(); + + $this->log->info( + sprintf('Password recovery for %s (%u)', $user->name, $user->id), + ['user' => $user->toJson()] + ); + + $this->mail->sendViewTranslated( + $user, + 'Password recovery', + 'emails/password-reset', + ['username' => $user->name, 'reset' => $reset] + ); + } + + return $this->showView('pages/password/reset-success', ['type' => 'email']); + } + + /** + * @param Request $request + * @return Response + */ + public function resetPassword(Request $request): Response + { + $this->requireToken($request); + + return $this->showView('pages/password/reset-form'); + } + + /** + * @param Request $request + * @return Response + */ + public function postResetPassword(Request $request): Response + { + $reset = $this->requireToken($request); + + $data = $this->validate($request, [ + 'password' => 'required|min:' . config('min_password_length'), + 'password_confirmation' => 'required', + ]); + + if ($data['password'] !== $data['password_confirmation']) { + $this->session->set('errors', + array_merge($this->session->get('errors', []), ['validation.password.confirmed'])); + + return $this->showView('pages/password/reset-form'); + } + + auth()->setPassword($reset->user, $data['password']); + $reset->delete(); + + return $this->showView('pages/password/reset-success', ['type' => 'reset']); + } + + /** + * @param string $view + * @param array $data + * @return Response + */ + protected function showView($view = 'pages/password/reset', $data = []): Response + { + $errors = Collection::make(Arr::flatten($this->session->get('errors', []))); + $this->session->remove('errors'); + + return $this->response->withView( + $view, + array_merge_recursive(['errors' => $errors], $data) + ); + } + + /** + * @param Request $request + * @return PasswordReset + */ + protected function requireToken(Request $request): PasswordReset + { + $token = $request->getAttribute('token'); + /** @var PasswordReset|null $reset */ + $reset = PasswordReset::whereToken($token)->first(); + + if (!$reset) { + throw new HttpNotFound(); + } + + return $reset; + } +} diff --git a/src/Middleware/LegacyMiddleware.php b/src/Middleware/LegacyMiddleware.php index 27a15faa..11508e1c 100644 --- a/src/Middleware/LegacyMiddleware.php +++ b/src/Middleware/LegacyMiddleware.php @@ -26,7 +26,6 @@ class LegacyMiddleware implements MiddlewareInterface 'shifts_json_export', 'users', 'user_driver_licenses', - 'user_password_recovery', 'user_worklog', ]; @@ -112,11 +111,6 @@ class LegacyMiddleware implements MiddlewareInterface case 'shifts_json_export': require_once realpath(__DIR__ . '/../../includes/controller/shifts_controller.php'); shifts_json_export_controller(); - case 'user_password_recovery': - require_once realpath(__DIR__ . '/../../includes/controller/users_controller.php'); - $title = user_password_recovery_title(); - $content = user_password_recovery_controller(); - return [$title, $content]; case 'public_dashboard': return public_dashboard_controller(); case 'angeltypes': diff --git a/tests/Unit/Controllers/PasswordResetControllerTest.php b/tests/Unit/Controllers/PasswordResetControllerTest.php new file mode 100644 index 00000000..54046cef --- /dev/null +++ b/tests/Unit/Controllers/PasswordResetControllerTest.php @@ -0,0 +1,266 @@ +getController('pages/password/reset'); + $response = $controller->reset(); + + $this->assertEquals(200, $response->getStatusCode()); + } + + /** + * @covers \Engelsystem\Controllers\PasswordResetController::postReset + */ + public function testPostReset(): void + { + $this->initDatabase(); + $request = new Request([], ['email' => 'foo@bar.batz']); + $user = $this->createUser(); + + $controller = $this->getController( + 'pages/password/reset-success', + ['type' => 'email', 'errors' => collect()] + ); + /** @var TestLogger $log */ + $log = $this->args['log']; + /** @var EngelsystemMailer|MockObject $mailer */ + $mailer = $this->args['mailer']; + $this->setExpects($mailer, 'sendViewTranslated'); + + $controller->postReset($request); + + $this->assertNotEmpty(PasswordReset::find($user->id)->first()); + $this->assertTrue($log->hasInfoThatContains($user->name)); + } + + /** + * @covers \Engelsystem\Controllers\PasswordResetController::postReset + */ + public function testPostResetInvalidRequest(): void + { + $request = new Request(); + + $controller = $this->getController(); + + $this->expectException(ValidationException::class); + $controller->postReset($request); + } + + /** + * @covers \Engelsystem\Controllers\PasswordResetController::postReset + */ + public function testPostResetNoUser(): void + { + $this->initDatabase(); + $request = new Request([], ['email' => 'foo@bar.batz']); + + $controller = $this->getController( + 'pages/password/reset-success', + ['type' => 'email', 'errors' => collect()] + ); + + $controller->postReset($request); + } + + /** + * @covers \Engelsystem\Controllers\PasswordResetController::resetPassword + * @covers \Engelsystem\Controllers\PasswordResetController::requireToken + */ + public function testResetPassword(): void + { + $this->initDatabase(); + + $user = $this->createUser(); + $token = $this->createToken($user); + $request = new Request([], [], ['token' => $token->token]); + + $controller = $this->getController('pages/password/reset-form'); + + $controller->resetPassword($request); + } + + /** + * @covers \Engelsystem\Controllers\PasswordResetController::resetPassword + * @covers \Engelsystem\Controllers\PasswordResetController::requireToken + */ + public function testResetPasswordNoToken(): void + { + $this->initDatabase(); + $controller = $this->getController(); + + $this->expectException(HttpNotFound::class); + $controller->resetPassword(new Request()); + } + + /** + * @covers \Engelsystem\Controllers\PasswordResetController::postResetPassword + */ + public function testPostResetPassword(): void + { + $this->initDatabase(); + + $this->app->instance('config', new Config(['min_password_length' => 3])); + $user = $this->createUser(); + $token = $this->createToken($user); + $password = 'SomeRandomPasswordForAmazingSecurity'; + $request = new Request( + [], + ['password' => $password, 'password_confirmation' => $password], + ['token' => $token->token] + ); + + $controller = $this->getController( + 'pages/password/reset-success', + ['type' => 'reset', 'errors' => collect()] + ); + + $auth = new Authenticator($request, $this->args['session'], $user); + $this->app->instance('authenticator', $auth); + + $response = $controller->postResetPassword($request); + $this->assertEquals(200, $response->getStatusCode()); + + $this->assertEmpty(PasswordReset::find($user->id)); + $this->assertNotNull(auth()->authenticate($user->name, $password)); + } + + /** + * @covers \Engelsystem\Controllers\PasswordResetController::postResetPassword + * @covers \Engelsystem\Controllers\PasswordResetController::showView + */ + public function testPostResetPasswordNotMatching(): void + { + $this->initDatabase(); + + $this->app->instance('config', new Config(['min_password_length' => 3])); + $user = $this->createUser(); + $token = $this->createToken($user); + $password = 'SomeRandomPasswordForAmazingSecurity'; + $request = new Request( + [], + ['password' => $password, 'password_confirmation' => $password . 'OrNot'], + ['token' => $token->token] + ); + + $controller = $this->getController( + 'pages/password/reset-form', + ['errors' => collect(['some.other.error', 'validation.password.confirmed'])] + ); + /** @var Session $session */ + $session = $this->args['session']; + $session->set('errors', ['foo' => ['bar' => 'some.other.error']]); + + $controller->postResetPassword($request); + $this->assertEmpty($session->get('errors')); + } + + /** + * @return array + */ + protected function getControllerArgs(): array + { + $response = new Response(); + $session = new Session(new MockArraySessionStorage()); + /** @var EngelsystemMailer|MockObject $mailer */ + $mailer = $this->createMock(EngelsystemMailer::class); + $log = new TestLogger(); + $renderer = $this->createMock(Renderer::class); + $response->setRenderer($renderer); + + return $this->args = [ + 'response' => $response, + 'session' => $session, + 'mailer' => $mailer, + 'log' => $log, + 'renderer' => $renderer + ]; + } + + /** + * @param string $view + * @param array $data + * @return PasswordResetController + */ + protected function getController(?string $view = null, ?array $data = null): PasswordResetController + { + /** @var Response $response */ + /** @var Session $session */ + /** @var EngelsystemMailer|MockObject $mailer */ + /** @var TestLogger $log */ + /** @var Renderer|MockObject $renderer */ + list($response, $session, $mailer, $log, $renderer) = array_values($this->getControllerArgs()); + $controller = new PasswordResetController($response, $session, $mailer, $log); + $controller->setValidator(new Validator()); + + if ($view) { + $args = [$view]; + if ($data) { + $args[] = $data; + } + + $this->setExpects($renderer, 'render', $args, 'Foo'); + } + + return $controller; + } + + /** + * @return User + */ + protected function createUser(): User + { + $user = new User([ + 'name' => 'foo', + 'password' => '', + 'email' => 'foo@bar.batz', + 'api_key' => '', + ]); + $user->save(); + + return $user; + } + + /** + * @param User $user + * @return PasswordReset + */ + protected function createToken(User $user): PasswordReset + { + $reset = new PasswordReset(['user_id' => $user->id, 'token' => 'SomeTestToken123']); + $reset->save(); + + return $reset; + } +} -- cgit v1.2.3-54-g00ecf From c9ebaa972cb2a16e16ffc78080f03342eae5d874 Mon Sep 17 00:00:00 2001 From: Igor Scheller Date: Fri, 6 Sep 2019 03:45:56 +0200 Subject: Shifts view: Persist hidden filters --- resources/assets/js/forms.js | 35 ++++++++++++++++++++++++++++++---- resources/views/pages/user-shifts.html | 8 ++++---- 2 files changed, 35 insertions(+), 8 deletions(-) (limited to 'resources') diff --git a/resources/assets/js/forms.js b/resources/assets/js/forms.js index f5818e97..9970b907 100644 --- a/resources/assets/js/forms.js +++ b/resources/assets/js/forms.js @@ -16,7 +16,7 @@ global.checkAll = (id, checked) => { * Sets the checkboxes according to the given type * * @param {string} id The elements ID - * @param {list} shifts_list A list of numbers + * @param {list} shiftsList A list of numbers */ global.checkOwnTypes = (id, shiftsList) => { $('#' + id + ' input[type="checkbox"]').each(function () { @@ -144,10 +144,10 @@ $(function () { elem.children('input').on('click', function (ev) { ev.stopImmediatePropagation(); if (typeof elem.data('DateTimePicker') === 'undefined') { - elem.datetimepicker(opts); - elem.data('DateTimePicker').show(); + elem.datetimepicker(opts); + elem.data('DateTimePicker').show(); } else { - elem.data('DateTimePicker').toggle(); + elem.data('DateTimePicker').toggle(); } }); }); @@ -173,3 +173,30 @@ $(function () { }); }); }); + +/** + * Set the filter selects to latest state + * + * Uses DOMContentLoaded to prevent flickering + */ +window.addEventListener('DOMContentLoaded', () => { + const filter = document.getElementById('collapseShiftsFilterSelect'); + if (!filter || localStorage.getItem('collapseShiftsFilterSelect') !== 'hidden') { + return; + } + + filter.classList.remove('in'); +}); +$(() => { + if (typeof (localStorage) === 'undefined') { + return; + } + + const onChange = (e) => { + localStorage.setItem('collapseShiftsFilterSelect', e.type); + }; + + $('#collapseShiftsFilterSelect') + .on('hidden.bs.collapse', onChange) + .on('shown.bs.collapse', onChange); +}); diff --git a/resources/views/pages/user-shifts.html b/resources/views/pages/user-shifts.html index 9ac501da..d5a98f80 100644 --- a/resources/views/pages/user-shifts.html +++ b/resources/views/pages/user-shifts.html @@ -55,12 +55,12 @@
-
+
%room_select%
%type_select%
@@ -79,5 +79,5 @@ %shifts_table%
-%ical_text% + %ical_text%
\ No newline at end of file -- cgit v1.2.3-54-g00ecf From 7c9910677e776aafe744ecbf41ade5e5b795c3a7 Mon Sep 17 00:00:00 2001 From: Igor Scheller Date: Sun, 6 Oct 2019 16:53:10 +0200 Subject: Frontend: Show language selects on every page --- resources/views/layouts/parts/navbar.twig | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) (limited to 'resources') diff --git a/resources/views/layouts/parts/navbar.twig b/resources/views/layouts/parts/navbar.twig index 61c6a10b..0b2eee63 100644 --- a/resources/views/layouts/parts/navbar.twig +++ b/resources/views/layouts/parts/navbar.twig @@ -55,16 +55,14 @@ {{ elements.toolbar_item(user.name, url('users', {'action': 'view'}), 'users', 'icon icon-icon_angel') }} {% endif %} - {% if has_permission_to('user_settings') or has_permission_to('logout') %} - - {% endif %} + {% endblock %} -- cgit v1.2.3-54-g00ecf From 973c108b153fb1d8be3576935c58e92865d19e7a Mon Sep 17 00:00:00 2001 From: Igor Scheller Date: Sun, 6 Oct 2019 17:38:23 +0200 Subject: credits: Make them translatable and use markdown --- resources/views/pages/credits.twig | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) (limited to 'resources') diff --git a/resources/views/pages/credits.twig b/resources/views/pages/credits.twig index 3bb04895..8bd1f694 100644 --- a/resources/views/pages/credits.twig +++ b/resources/views/pages/credits.twig @@ -4,28 +4,28 @@ {% block content %}
-

Credits

+

{{ __('Credits') }}

{% for title, credit in credits %}
-

{{ title }}

- {{ credit|markdown }} +

{{ __(title) }}

+ {{ __(credit)|markdown }}
{% endfor %}
-

Source code

-

Version: {{ version }}

+

{{ __('Source code') }}

+

{{ __('Version: _%s_', [version])|markdown }}

- The original engelsystem was written by - cookie. - It was then completely rewritten and enhanced by - msquare (maintainer) and - MyIgel. + {{ __('The original engelsystem was written by +[cookie](https://github.com/cookieBerlin/engelsystem). +It was then completely rewritten and enhanced by [msquare](https://notrademark.de) (maintainer) and +[MyIgel](https://myigel.name).')|markdown }}

- Please look at the - contributor list on GitHub for a complete list. + {{ __('Please have a look at the +[contributors list on GitHub](https://github.com/engelsystem/engelsystem/graphs/contributors) +for a complete list.')|markdown }}

-- cgit v1.2.3-54-g00ecf From fa35187795734ad4f4dc33f74cc0fe020dd9ff32 Mon Sep 17 00:00:00 2001 From: Igor Scheller Date: Wed, 9 Oct 2019 13:54:04 +0200 Subject: Removed .mo translation files from version control, use .po as fallback --- .gitignore | 1 + README.md | 5 +++++ resources/lang/de_DE/default.mo | Bin 46693 -> 0 bytes resources/lang/en_US/default.mo | Bin 1240 -> 0 bytes resources/lang/pt_BR/default.mo | Bin 41132 -> 0 bytes .../Translation/TranslationServiceProvider.php | 25 +++++++++++++++++++-- .../Helpers/Translation/Assets/ba_RR/default.po | 3 +++ .../Translation/TranslationServiceProviderTest.php | 22 +++++++++++++++--- 8 files changed, 51 insertions(+), 5 deletions(-) delete mode 100644 resources/lang/de_DE/default.mo delete mode 100644 resources/lang/en_US/default.mo delete mode 100644 resources/lang/pt_BR/default.mo create mode 100644 tests/Unit/Helpers/Translation/Assets/ba_RR/default.po (limited to 'resources') diff --git a/.gitignore b/.gitignore index 77a62c1e..cda5cf92 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ _vimrc_local.vim /public/coverage /coverage /unittests.xml +/resources/lang/*/*.mo # Composer files /vendor/ diff --git a/README.md b/README.md index 02508157..594011b7 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,11 @@ The following instructions explain how to get, build and run the latest engelsys ```bash yarn build ``` + * Optionally (for better performance) + * Generate translation files + ```bash + find resources/lang/ -type f -name '*.po' -exec sh -c 'file="{}"; msgfmt "${file%.*}.po" -o "${file%.*}.mo"' \; + ``` ### Configuration and Setup * The webserver must have write access to the ```import``` and ```storage``` directories and read access for all other directories diff --git a/resources/lang/de_DE/default.mo b/resources/lang/de_DE/default.mo deleted file mode 100644 index d4b7885b..00000000 Binary files a/resources/lang/de_DE/default.mo and /dev/null differ diff --git a/resources/lang/en_US/default.mo b/resources/lang/en_US/default.mo deleted file mode 100644 index 30dd375d..00000000 Binary files a/resources/lang/en_US/default.mo and /dev/null differ diff --git a/resources/lang/pt_BR/default.mo b/resources/lang/pt_BR/default.mo deleted file mode 100644 index d15826e6..00000000 Binary files a/resources/lang/pt_BR/default.mo and /dev/null differ diff --git a/src/Helpers/Translation/TranslationServiceProvider.php b/src/Helpers/Translation/TranslationServiceProvider.php index 05e782ec..6df9b0fe 100644 --- a/src/Helpers/Translation/TranslationServiceProvider.php +++ b/src/Helpers/Translation/TranslationServiceProvider.php @@ -5,6 +5,7 @@ namespace Engelsystem\Helpers\Translation; use Engelsystem\Config\Config; use Engelsystem\Container\ServiceProvider; use Gettext\Translations; +use Illuminate\Support\Str; use Symfony\Component\HttpFoundation\Session\Session; class TranslationServiceProvider extends ServiceProvider @@ -69,14 +70,18 @@ class TranslationServiceProvider extends ServiceProvider public function getTranslator(string $locale): GettextTranslator { if (!isset($this->translators[$locale])) { - $file = $this->app->get('path.lang') . '/' . $locale . '/default.mo'; + $file = $this->getFile($locale); /** @var GettextTranslator $translator */ $translator = $this->app->make(GettextTranslator::class); /** @var Translations $translations */ $translations = $this->app->make(Translations::class); - $translations->addFromMoFile($file); + if (Str::endsWith($file, '.mo')) { + $translations->addFromMoFile($file); + } else { + $translations->addFromPoFile($file); + } $translator->loadTranslations($translations); @@ -85,4 +90,20 @@ class TranslationServiceProvider extends ServiceProvider return $this->translators[$locale]; } + + /** + * @param string $locale + * @return string + */ + protected function getFile(string $locale): string + { + $filepath = $file = $this->app->get('path.lang') . '/' . $locale . '/default'; + $file = $filepath . '.mo'; + + if (!file_exists($file)) { + $file = $filepath . '.po'; + } + + return $file; + } } diff --git a/tests/Unit/Helpers/Translation/Assets/ba_RR/default.po b/tests/Unit/Helpers/Translation/Assets/ba_RR/default.po new file mode 100644 index 00000000..887e2daa --- /dev/null +++ b/tests/Unit/Helpers/Translation/Assets/ba_RR/default.po @@ -0,0 +1,3 @@ +# Testing content +msgid "foo.bar" +msgstr "B Arr!" diff --git a/tests/Unit/Helpers/Translation/TranslationServiceProviderTest.php b/tests/Unit/Helpers/Translation/TranslationServiceProviderTest.php index 1822f353..e55fdf02 100644 --- a/tests/Unit/Helpers/Translation/TranslationServiceProviderTest.php +++ b/tests/Unit/Helpers/Translation/TranslationServiceProviderTest.php @@ -12,7 +12,7 @@ use Symfony\Component\HttpFoundation\Session\Session; class TranslationServiceProviderTest extends ServiceProviderTest { /** - * @covers \Engelsystem\Helpers\Translation\TranslationServiceProvider::register() + * @covers \Engelsystem\Helpers\Translation\TranslationServiceProvider::register */ public function testRegister(): void { @@ -30,7 +30,7 @@ class TranslationServiceProviderTest extends ServiceProviderTest /** @var TranslationServiceProvider|MockObject $serviceProvider */ $serviceProvider = $this->getMockBuilder(TranslationServiceProvider::class) ->setConstructorArgs([$app]) - ->setMethods(['setLocale']) + ->onlyMethods(['setLocale']) ->getMock(); $app->expects($this->exactly(2)) @@ -75,7 +75,7 @@ class TranslationServiceProviderTest extends ServiceProviderTest } /** - * @covers \Engelsystem\Helpers\Translation\TranslationServiceProvider::getTranslator() + * @covers \Engelsystem\Helpers\Translation\TranslationServiceProvider::getTranslator */ public function testGetTranslator(): void { @@ -91,4 +91,20 @@ class TranslationServiceProviderTest extends ServiceProviderTest // Retry from cache $serviceProvider->getTranslator('fo_OO'); } + + /** + * @covers \Engelsystem\Helpers\Translation\TranslationServiceProvider::getTranslator + * @covers \Engelsystem\Helpers\Translation\TranslationServiceProvider::getFile + */ + public function testGetTranslatorFromPo(): void + { + $app = $this->getApp(['get']); + $this->setExpects($app, 'get', ['path.lang'], __DIR__ . '/Assets'); + + $serviceProvider = new TranslationServiceProvider($app); + + // Get translator using a .po file + $translator = $serviceProvider->getTranslator('ba_RR'); + $this->assertEquals('B Arr!', $translator->gettext('foo.bar')); + } } -- cgit v1.2.3-54-g00ecf From 75f04507aedc6cea020da93fc579351cfb0f4f2c Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Fri, 11 Oct 2019 21:16:24 +0200 Subject: Makes Monday first day of the week even if English language is selected --- resources/assets/js/vendor.js | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'resources') diff --git a/resources/assets/js/vendor.js b/resources/assets/js/vendor.js index bf3807f7..b4b6487d 100644 --- a/resources/assets/js/vendor.js +++ b/resources/assets/js/vendor.js @@ -10,6 +10,13 @@ require('./forms'); require('./sticky-headers'); require('./moment-countdown'); +moment.updateLocale('en', { + week : { + dow : 1, // Monday is the first day of the week. + doy : 4 // The week that contains Jan 4th is the first week of the year. + } +}); + $.ajaxSetup({ headers: {'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')} }); -- cgit v1.2.3-54-g00ecf