From faf74150e9481ad9338eb6cc2428d02b24e9fc43 Mon Sep 17 00:00:00 2001 From: Igor Scheller Date: Sun, 6 Oct 2019 21:03:20 +0200 Subject: Mailer: Use users locale for translation --- .../Translation/TranslationServiceProvider.php | 6 ++- src/Mail/EngelsystemMailer.php | 59 +++++++++++++++++++--- 2 files changed, 57 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/Helpers/Translation/TranslationServiceProvider.php b/src/Helpers/Translation/TranslationServiceProvider.php index 09337dad..05e782ec 100644 --- a/src/Helpers/Translation/TranslationServiceProvider.php +++ b/src/Helpers/Translation/TranslationServiceProvider.php @@ -40,8 +40,10 @@ class TranslationServiceProvider extends ServiceProvider 'localeChangeCallback' => [$this, 'setLocale'], ] ); - $this->app->instance(Translator::class, $translator); - $this->app->instance('translator', $translator); + $this->app->singleton(Translator::class, function () use ($translator) { + return $translator; + }); + $this->app->alias(Translator::class, 'translator'); } /** diff --git a/src/Mail/EngelsystemMailer.php b/src/Mail/EngelsystemMailer.php index 81660681..87915d67 100644 --- a/src/Mail/EngelsystemMailer.php +++ b/src/Mail/EngelsystemMailer.php @@ -2,6 +2,8 @@ namespace Engelsystem\Mail; +use Engelsystem\Helpers\Translation\Translator; +use Engelsystem\Models\User\User; use Engelsystem\Renderer\Renderer; use Swift_Mailer as SwiftMailer; @@ -10,30 +12,75 @@ class EngelsystemMailer extends Mailer /** @var Renderer|null */ protected $view; + /** @var Translator|null */ + protected $translation; + /** @var string */ protected $subjectPrefix = null; /** * @param SwiftMailer $mailer * @param Renderer $view + * @param Translator $translation */ - public function __construct(SwiftMailer $mailer, Renderer $view = null) + public function __construct(SwiftMailer $mailer, Renderer $view = null, Translator $translation = null) { parent::__construct($mailer); + $this->translation = $translation; $this->view = $view; } + /** + * @param string|string[]|User $to + * @param string $subject + * @param string $template + * @param array $data + * @param string|null $locale + * @return int + */ + public function sendViewTranslated( + $to, + string $subject, + string $template, + array $data = [], + ?string $locale = null + ): int { + if ($to instanceof User) { + $locale = $locale ?: $to->settings->language; + $to = $to->contact->email ? $to->contact->email : $to->email; + } + + $activeLocale = null; + if ( + $locale + && $this->translation + && isset($this->translation->getLocales()[$locale]) + ) { + $activeLocale = $this->translation->getLocale(); + $this->translation->setLocale($locale); + } + + $subject = $this->translation ? $this->translation->translate($subject) : $subject; + $sentMails = $this->sendView($to, $subject, $template, $data); + + if ($activeLocale) { + $this->translation->setLocale($activeLocale); + } + + return $sentMails; + } + /** * Send a template * - * @param string $to - * @param string $subject - * @param string $template - * @param array $data + * @param string|string[] $to + * @param string $subject + * @param string $template + * @param array $data * @return int */ - public function sendView($to, $subject, $template, $data = []): int + public function sendView($to, string $subject, string $template, array $data = []): int { $body = $this->view->render($template, $data); -- cgit v1.2.3-70-g09d2 From 8d090438b659b641dd0f6cbc99193f3b48b2fc4b Mon Sep 17 00:00:00 2001 From: Igor Scheller Date: Mon, 7 Oct 2019 21:59:40 +0200 Subject: Validation rules: min/max/between: Use string length to compare strings --- src/Http/Validation/Rules/Between.php | 10 +++++ src/Http/Validation/Rules/Max.php | 10 +++++ src/Http/Validation/Rules/Min.php | 10 +++++ src/Http/Validation/Rules/StringInputLength.php | 44 ++++++++++++++++++++++ tests/Unit/Http/Validation/Rules/BetweenTest.php | 28 ++++++++++++++ tests/Unit/Http/Validation/Rules/MaxTest.php | 26 +++++++++++++ tests/Unit/Http/Validation/Rules/MinTest.php | 26 +++++++++++++ .../Validation/Rules/StringInputLengthTest.php | 37 ++++++++++++++++++ .../Rules/Stub/ParentClassImplementation.php | 23 +++++++++++ .../Rules/Stub/UsesStringInputLength.php | 10 +++++ tests/Unit/Http/Validation/ValidatorTest.php | 5 ++- 11 files changed, 227 insertions(+), 2 deletions(-) create mode 100644 src/Http/Validation/Rules/Between.php create mode 100644 src/Http/Validation/Rules/Max.php create mode 100644 src/Http/Validation/Rules/Min.php create mode 100644 src/Http/Validation/Rules/StringInputLength.php create mode 100644 tests/Unit/Http/Validation/Rules/BetweenTest.php create mode 100644 tests/Unit/Http/Validation/Rules/MaxTest.php create mode 100644 tests/Unit/Http/Validation/Rules/MinTest.php create mode 100644 tests/Unit/Http/Validation/Rules/StringInputLengthTest.php create mode 100644 tests/Unit/Http/Validation/Rules/Stub/ParentClassImplementation.php create mode 100644 tests/Unit/Http/Validation/Rules/Stub/UsesStringInputLength.php (limited to 'src') diff --git a/src/Http/Validation/Rules/Between.php b/src/Http/Validation/Rules/Between.php new file mode 100644 index 00000000..106a93ac --- /dev/null +++ b/src/Http/Validation/Rules/Between.php @@ -0,0 +1,10 @@ +isDateTime($input) + ) { + $input = Str::length($input); + } + + return parent::validate($input); + } + + /** + * @param mixed $input + * @return bool + */ + protected function isDateTime($input): bool + { + try { + new DateTime($input); + } catch (Throwable $e) { + return false; + } + + return true; + } +} diff --git a/tests/Unit/Http/Validation/Rules/BetweenTest.php b/tests/Unit/Http/Validation/Rules/BetweenTest.php new file mode 100644 index 00000000..130d2f93 --- /dev/null +++ b/tests/Unit/Http/Validation/Rules/BetweenTest.php @@ -0,0 +1,28 @@ +assertFalse($rule->validate(1)); + $this->assertFalse($rule->validate('11')); + $this->assertTrue($rule->validate(5)); + $this->assertFalse($rule->validate('AS')); + $this->assertFalse($rule->validate('TestContentThatCounts')); + $this->assertTrue($rule->validate('TESTING')); + + $rule = new Between('2042-01-01', '2042-10-10'); + $this->assertFalse($rule->validate('2000-01-01')); + $this->assertFalse($rule->validate('3000-01-01')); + $this->assertTrue($rule->validate('2042-05-11')); + } +} diff --git a/tests/Unit/Http/Validation/Rules/MaxTest.php b/tests/Unit/Http/Validation/Rules/MaxTest.php new file mode 100644 index 00000000..3f4d9516 --- /dev/null +++ b/tests/Unit/Http/Validation/Rules/MaxTest.php @@ -0,0 +1,26 @@ +assertFalse($rule->validate(10)); + $this->assertFalse($rule->validate('22')); + $this->assertTrue($rule->validate(3)); + $this->assertFalse($rule->validate('TEST')); + $this->assertTrue($rule->validate('AS')); + + $rule = new Max('2042-01-01'); + $this->assertFalse($rule->validate('2100-01-01')); + $this->assertTrue($rule->validate('2000-01-01')); + } +} diff --git a/tests/Unit/Http/Validation/Rules/MinTest.php b/tests/Unit/Http/Validation/Rules/MinTest.php new file mode 100644 index 00000000..56350802 --- /dev/null +++ b/tests/Unit/Http/Validation/Rules/MinTest.php @@ -0,0 +1,26 @@ +assertFalse($rule->validate(1)); + $this->assertFalse($rule->validate('2')); + $this->assertTrue($rule->validate(3)); + $this->assertFalse($rule->validate('AS')); + $this->assertTrue($rule->validate('TEST')); + + $rule = new Min('2042-01-01'); + $this->assertFalse($rule->validate('2000-01-01')); + $this->assertTrue($rule->validate('2345-01-01')); + } +} diff --git a/tests/Unit/Http/Validation/Rules/StringInputLengthTest.php b/tests/Unit/Http/Validation/Rules/StringInputLengthTest.php new file mode 100644 index 00000000..5c4dc512 --- /dev/null +++ b/tests/Unit/Http/Validation/Rules/StringInputLengthTest.php @@ -0,0 +1,37 @@ +validate($input); + + $this->assertEquals($expectedInput, $rule->lastInput); + } + + /** + * @return array[] + */ + public function validateProvider() + { + return [ + ['TEST', 4], + ['?', 1], + ['2042-01-01 00:00', '2042-01-01 00:00'], + ['3', '3'], + ]; + } +} diff --git a/tests/Unit/Http/Validation/Rules/Stub/ParentClassImplementation.php b/tests/Unit/Http/Validation/Rules/Stub/ParentClassImplementation.php new file mode 100644 index 00000000..1b6aaaf5 --- /dev/null +++ b/tests/Unit/Http/Validation/Rules/Stub/ParentClassImplementation.php @@ -0,0 +1,23 @@ +lastInput = $input; + + return $this->validateResult; + } +} diff --git a/tests/Unit/Http/Validation/Rules/Stub/UsesStringInputLength.php b/tests/Unit/Http/Validation/Rules/Stub/UsesStringInputLength.php new file mode 100644 index 00000000..3522304c --- /dev/null +++ b/tests/Unit/Http/Validation/Rules/Stub/UsesStringInputLength.php @@ -0,0 +1,10 @@ +assertFalse($val->validate( - ['lorem' => 2], - ['lorem' => 'required|min:3|max:10'] + ['lorem' => 'OMG'], + ['lorem' => 'required|min:4|max:10'] )); + $this->assertEquals(['lorem' => ['validation.lorem.min']], $val->getErrors()); $this->assertFalse($val->validate( ['lorem' => 42], ['lorem' => 'required|min:3|max:10'] -- cgit v1.2.3-70-g09d2 From 1b3e3b1d65126da909d0013c9c0a0cb3f41639c1 Mon Sep 17 00:00:00 2001 From: Igor Scheller Date: Mon, 7 Oct 2019 22:02:28 +0200 Subject: Exceptions: Added HttpNotFound exception --- src/Http/Exceptions/HttpNotFound.php | 23 +++++++++++++++++++++++ tests/Unit/Http/Exceptions/HttpNotFoundTest.php | 22 ++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 src/Http/Exceptions/HttpNotFound.php create mode 100644 tests/Unit/Http/Exceptions/HttpNotFoundTest.php (limited to 'src') diff --git a/src/Http/Exceptions/HttpNotFound.php b/src/Http/Exceptions/HttpNotFound.php new file mode 100644 index 00000000..324adaf9 --- /dev/null +++ b/src/Http/Exceptions/HttpNotFound.php @@ -0,0 +1,23 @@ +assertEquals(404, $exception->getStatusCode()); + $this->assertEquals('', $exception->getMessage()); + + $exception = new HttpNotFound('Nothing to see here!'); + $this->assertEquals('Nothing to see here!', $exception->getMessage()); + } +} -- cgit v1.2.3-70-g09d2 From ae0816ce8de46c38cd6f4b6d95abb17a6703d280 Mon Sep 17 00:00:00 2001 From: Igor Scheller Date: Tue, 8 Oct 2019 14:07:28 +0200 Subject: Response: Fixed naming to use renderer instead of view and added setter --- src/Http/Response.php | 27 +++++++++++++++++++-------- tests/Unit/Http/ResponseTest.php | 12 ++++++++++++ 2 files changed, 31 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/Http/Response.php b/src/Http/Response.php index 1a7c8209..a6b4ab74 100644 --- a/src/Http/Response.php +++ b/src/Http/Response.php @@ -3,6 +3,7 @@ namespace Engelsystem\Http; use Engelsystem\Renderer\Renderer; +use InvalidArgumentException; use Psr\Http\Message\ResponseInterface; use Symfony\Component\HttpFoundation\Response as SymfonyResponse; @@ -11,21 +12,21 @@ class Response extends SymfonyResponse implements ResponseInterface use MessageTrait; /** @var Renderer */ - protected $view; + protected $renderer; /** * @param string $content * @param int $status * @param array $headers - * @param Renderer $view + * @param Renderer $renderer */ public function __construct( $content = '', int $status = 200, array $headers = [], - Renderer $view = null + Renderer $renderer = null ) { - $this->view = $view; + $this->renderer = $renderer; parent::__construct($content, $status, $headers); } @@ -47,7 +48,7 @@ class Response extends SymfonyResponse implements ResponseInterface * provided status code; if none is provided, implementations MAY * use the defaults as suggested in the HTTP specification. * @return static - * @throws \InvalidArgumentException For invalid status code arguments. + * @throws InvalidArgumentException For invalid status code arguments. */ public function withStatus($code, $reasonPhrase = '') { @@ -107,12 +108,12 @@ class Response extends SymfonyResponse implements ResponseInterface */ public function withView($view, $data = [], $status = 200, $headers = []) { - if (!$this->view instanceof Renderer) { - throw new \InvalidArgumentException('Renderer not defined'); + if (!$this->renderer instanceof Renderer) { + throw new InvalidArgumentException('Renderer not defined'); } $new = clone $this; - $new->setContent($this->view->render($view, $data)); + $new->setContent($this->renderer->render($view, $data)); $new->setStatusCode($status, ($status == $this->getStatusCode() ? $this->statusText : null)); foreach ($headers as $key => $values) { @@ -144,4 +145,14 @@ class Response extends SymfonyResponse implements ResponseInterface return $response; } + + /** + * Set the renderer to use + * + * @param Renderer $renderer + */ + public function setRenderer(Renderer $renderer) + { + $this->renderer = $renderer; + } } diff --git a/tests/Unit/Http/ResponseTest.php b/tests/Unit/Http/ResponseTest.php index 34f76513..b8e6e527 100644 --- a/tests/Unit/Http/ResponseTest.php +++ b/tests/Unit/Http/ResponseTest.php @@ -55,6 +55,7 @@ class ResponseTest extends TestCase /** * @covers \Engelsystem\Http\Response::withView + * @covers \Engelsystem\Http\Response::setRenderer */ public function testWithView() { @@ -73,6 +74,17 @@ class ResponseTest extends TestCase $this->assertEquals('Foo ipsum!', $newResponse->getContent()); $this->assertEquals(505, $newResponse->getStatusCode()); $this->assertArraySubset(['test' => ['er']], $newResponse->getHeaders()); + + /** @var REnderer|MockObject $renderer */ + $anotherRenderer = $this->createMock(Renderer::class); + $anotherRenderer->expects($this->once()) + ->method('render') + ->with('bar') + ->willReturn('Stuff'); + + $response->setRenderer($anotherRenderer); + $response = $response->withView('bar'); + $this->assertEquals('Stuff', $response->getContent()); } /** -- cgit v1.2.3-70-g09d2 From 810068dcf811b17805ee9958b877a827bf8125ed Mon Sep 17 00:00:00 2001 From: Igor Scheller Date: Tue, 8 Oct 2019 14:18:51 +0200 Subject: Fixes: AuthController session error merging and StatsTest --- src/Controllers/AuthController.php | 2 +- tests/Unit/Controllers/AuthControllerTest.php | 4 ++-- tests/Unit/Controllers/Metrics/StatsTest.php | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/Controllers/AuthController.php b/src/Controllers/AuthController.php index c69c2377..7892064b 100644 --- a/src/Controllers/AuthController.php +++ b/src/Controllers/AuthController.php @@ -88,7 +88,7 @@ class AuthController extends BaseController $user = $this->auth->authenticate($data['login'], $data['password']); if (!$user instanceof User) { - $this->session->set('errors', $this->session->get('errors', []) + ['auth.not-found']); + $this->session->set('errors', array_merge($this->session->get('errors', []), ['auth.not-found'])); return $this->showLogin(); } diff --git a/tests/Unit/Controllers/AuthControllerTest.php b/tests/Unit/Controllers/AuthControllerTest.php index 50ce014d..a12ed6d6 100644 --- a/tests/Unit/Controllers/AuthControllerTest.php +++ b/tests/Unit/Controllers/AuthControllerTest.php @@ -13,7 +13,6 @@ use Engelsystem\Models\User\Settings; use Engelsystem\Models\User\User; use Engelsystem\Test\Unit\HasDatabase; use Engelsystem\Test\Unit\TestCase; -use Illuminate\Support\Collection; use PHPUnit\Framework\MockObject\MockObject; use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\HttpFoundation\Session\SessionInterface; @@ -66,6 +65,7 @@ class AuthControllerTest extends TestCase $session = new Session(new MockArraySessionStorage()); /** @var Validator|MockObject $validator */ $validator = new Validator(); + $session->set('errors', [['bar' => 'some.bar.error']]); $user = new User([ 'name' => 'foo', @@ -89,7 +89,7 @@ class AuthControllerTest extends TestCase $response->expects($this->once()) ->method('withView') - ->with('pages/login', ['errors' => Collection::make(['auth.not-found'])]) + ->with('pages/login', ['errors' => collect(['some.bar.error', 'auth.not-found'])]) ->willReturn($response); $response->expects($this->once()) ->method('redirectTo') diff --git a/tests/Unit/Controllers/Metrics/StatsTest.php b/tests/Unit/Controllers/Metrics/StatsTest.php index fa78d8c3..9204f7db 100644 --- a/tests/Unit/Controllers/Metrics/StatsTest.php +++ b/tests/Unit/Controllers/Metrics/StatsTest.php @@ -155,8 +155,8 @@ class StatsTest extends TestCase $this->initDatabase(); $this->addUsers(); - (new PasswordReset(['use_id' => 1, 'token' => 'loremIpsum123']))->save(); - (new PasswordReset(['use_id' => 3, 'token' => '5omeR4nd0mTok3N']))->save(); + (new PasswordReset(['user_id' => 1, 'token' => 'loremIpsum123']))->save(); + (new PasswordReset(['user_id' => 3, 'token' => '5omeR4nd0mTok3N']))->save(); $stats = new Stats($this->database); $this->assertEquals(2, $stats->passwordResets()); -- cgit v1.2.3-70-g09d2 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 --- includes/pages/guest_login.php | 4 +- includes/view/User_view.php | 58 +++++----------------- resources/views/pages/login.twig | 1 + src/Renderer/Twig/Extensions/Legacy.php | 1 + tests/Unit/Renderer/Twig/Extensions/LegacyTest.php | 1 + 5 files changed, 18 insertions(+), 47 deletions(-) (limited to 'src') diff --git a/includes/pages/guest_login.php b/includes/pages/guest_login.php index 280743e5..170572e4 100644 --- a/includes/pages/guest_login.php +++ b/includes/pages/guest_login.php @@ -242,9 +242,9 @@ function guest_register() redirect(page_link_to('register')); } - // If a welcome message is present, display registration success page. + // If a welcome message is present, display it on the next page if ($message = $config->get('welcome_msg')) { - return User_registration_success_view($message); + info((new Parsedown())->text($message)); } redirect(page_link_to('/')); diff --git a/includes/view/User_view.php b/includes/view/User_view.php index bfe7e02c..b38a5062 100644 --- a/includes/view/User_view.php +++ b/includes/view/User_view.php @@ -107,46 +107,6 @@ function User_settings_view( ]); } -/** - * Displays the welcome message to the user and shows a login form. - * - * @param string $event_welcome_message - * @return string - */ -function User_registration_success_view($event_welcome_message) -{ - $parsedown = new Parsedown(); - $event_welcome_message = $parsedown->text($event_welcome_message); - - return page_with_title(__('Registration successful'), [ - msg(), - div('row', [ - div('col-md-4', [ - $event_welcome_message - ]), - div('col-md-4', [ - '

' . __('Login') . '

', - form([ - form_text('login', __('Nick'), ''), - form_password('password', __('Password')), - form_submit('submit', __('Login')), - buttons([ - button(page_link_to('user_password_recovery'), __('I forgot my password')) - ]), - info(__('Please note: You have to activate cookies!'), true) - ], page_link_to('login')) - ]), - div('col-md-4', [ - '

' . __('What can I do?') . '

', - '

' . __('Please read about the jobs you can do to help us.') . '

', - buttons([ - button(page_link_to('angeltypes', ['action' => 'about']), __('Teams/Job description') . ' »') - ]) - ]) - ]) - ]); -} - /** * Gui for deleting user with password field. * @@ -255,13 +215,13 @@ function Users_view( ]; $user_table_headers = [ - 'name' => Users_table_header_link('name', __('Nick'), $order_by) + 'name' => Users_table_header_link('name', __('Nick'), $order_by) ]; - if(config('enable_user_name')) { + if (config('enable_user_name')) { $user_table_headers['first_name'] = Users_table_header_link('first_name', __('Prename'), $order_by); $user_table_headers['last_name'] = Users_table_header_link('last_name', __('Name'), $order_by); } - if(config('enable_dect')) { + if (config('enable_dect')) { $user_table_headers['dect'] = Users_table_header_link('dect', __('DECT'), $order_by); } $user_table_headers['arrived'] = Users_table_header_link('arrived', __('Arrived'), $order_by); @@ -271,8 +231,16 @@ function Users_view( $user_table_headers['force_active'] = Users_table_header_link('force_active', __('Forced'), $order_by); $user_table_headers['got_shirt'] = Users_table_header_link('got_shirt', __('T-Shirt'), $order_by); $user_table_headers['shirt_size'] = Users_table_header_link('shirt_size', __('Size'), $order_by); - $user_table_headers['arrival_date'] = Users_table_header_link('planned_arrival_date', __('Planned arrival'), $order_by); - $user_table_headers['departure_date'] = Users_table_header_link('planned_departure_date', __('Planned departure'), $order_by); + $user_table_headers['arrival_date'] = Users_table_header_link( + 'planned_arrival_date', + __('Planned arrival'), + $order_by + ); + $user_table_headers['departure_date'] = Users_table_header_link( + 'planned_departure_date', + __('Planned departure'), + $order_by + ); $user_table_headers['last_login_at'] = Users_table_header_link('last_login_at', __('Last login'), $order_by); $user_table_headers['actions'] = ''; 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 %} diff --git a/src/Renderer/Twig/Extensions/Legacy.php b/src/Renderer/Twig/Extensions/Legacy.php index 79de32cb..55c095fc 100644 --- a/src/Renderer/Twig/Extensions/Legacy.php +++ b/src/Renderer/Twig/Extensions/Legacy.php @@ -32,6 +32,7 @@ class Legacy extends TwigExtension new TwigFunction('menuUserHints', 'header_render_hints', $isSafeHtml), new TwigFunction('menuUserSubmenu', 'make_user_submenu', $isSafeHtml), new TwigFunction('page', [$this, 'getPage']), + new TwigFunction('msg', 'msg', $isSafeHtml), ]; } diff --git a/tests/Unit/Renderer/Twig/Extensions/LegacyTest.php b/tests/Unit/Renderer/Twig/Extensions/LegacyTest.php index b6c19d14..7190c979 100644 --- a/tests/Unit/Renderer/Twig/Extensions/LegacyTest.php +++ b/tests/Unit/Renderer/Twig/Extensions/LegacyTest.php @@ -26,6 +26,7 @@ class LegacyTest extends ExtensionTest $this->assertExtensionExists('menuUserHints', 'header_render_hints', $functions, $isSafeHtml); $this->assertExtensionExists('menuUserSubmenu', 'make_user_submenu', $functions, $isSafeHtml); $this->assertExtensionExists('page', [$extension, 'getPage'], $functions); + $this->assertExtensionExists('msg', 'msg', $functions, $isSafeHtml); } /** -- cgit v1.2.3-70-g09d2 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 'src') 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-70-g09d2 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 'src') 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-70-g09d2