From d61bc9de2728825de8e033f0965ab57fda42ce70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tarmo=20Lepp=C3=A4nen?= Date: Sat, 22 Feb 2020 19:26:53 +0200 Subject: [PATCH] feat: Translation support for application eg. validation messages, etc. (#311) * feat: Translation support for application eg. validation messages, etc. * Added support for translated authentication failure. --- config/packages/framework.yaml | 4 + config/packages/security.yaml | 2 +- config/packages/stof_doctrine_extensions.yaml | 2 +- config/packages/translation.yaml | 3 +- config/services.yaml | 1 + .../AcceptLanguageSubscriber.php | 93 +++++++++++++++++++ src/Rest/Traits/RestResourceBaseMethods.php | 12 ++- ...TranslatedAuthenticationFailureHandler.php | 66 +++++++++++++ .../AcceptLanguageSubscriberTest.php | 74 +++++++++++++++ .../AcceptLanguageSubscriberTest.php | 34 +++++++ translations/security+intl-icu.fi.xlf | 14 +++ 11 files changed, 297 insertions(+), 8 deletions(-) create mode 100644 src/EventSubscriber/AcceptLanguageSubscriber.php create mode 100644 src/Security/Handler/TranslatedAuthenticationFailureHandler.php create mode 100644 tests/Integration/EventSubscriber/AcceptLanguageSubscriberTest.php create mode 100644 tests/Unit/EventSubscriber/AcceptLanguageSubscriberTest.php create mode 100644 translations/security+intl-icu.fi.xlf diff --git a/config/packages/framework.yaml b/config/packages/framework.yaml index 4b9ba40d4..8ae19635a 100644 --- a/config/packages/framework.yaml +++ b/config/packages/framework.yaml @@ -4,6 +4,10 @@ framework: csrf_protection: false #http_method_override: true + default_locale: '%locale%' + translator: + default_path: '%kernel.project_dir%/translations' + serializer: enable_annotations: true diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 6777a0c30..13e372ff2 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -27,7 +27,7 @@ security: provider: security_user_provider check_path: /auth/getToken success_handler: lexik_jwt_authentication.handler.authentication_success - failure_handler: lexik_jwt_authentication.handler.authentication_failure + failure_handler: App\Security\Handler\TranslatedAuthenticationFailureHandler root: pattern: ^/$ stateless: true diff --git a/config/packages/stof_doctrine_extensions.yaml b/config/packages/stof_doctrine_extensions.yaml index 5d09feb54..a4fcb3c80 100644 --- a/config/packages/stof_doctrine_extensions.yaml +++ b/config/packages/stof_doctrine_extensions.yaml @@ -1,7 +1,7 @@ # Read the documentation: https://symfony.com/doc/current/bundles/StofDoctrineExtensionsBundle/index.html # See the official DoctrineExtensions documentation for more details: https://github.com/Atlantic18/DoctrineExtensions/tree/master/doc/ stof_doctrine_extensions: - default_locale: en_US + default_locale: '%locale%' orm: default: timestampable: true diff --git a/config/packages/translation.yaml b/config/packages/translation.yaml index 05a2b3d82..2055e9df9 100644 --- a/config/packages/translation.yaml +++ b/config/packages/translation.yaml @@ -1,6 +1,5 @@ framework: - default_locale: en translator: default_path: '%kernel.project_dir%/translations' fallbacks: - - en + - '%locale%' diff --git a/config/services.yaml b/config/services.yaml index ab5a6c670..75bd241a9 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -26,6 +26,7 @@ services: $projectDir: '%kernel.project_dir%' $environment: '%kernel.environment%' $uuidRegex: '%app.uuid_regex%' + $locale: '%locale%' _instanceof: App\Rest\Interfaces\ControllerInterface: tags: [ 'app.rest.controller' ] diff --git a/src/EventSubscriber/AcceptLanguageSubscriber.php b/src/EventSubscriber/AcceptLanguageSubscriber.php new file mode 100644 index 000000000..1df2aaf15 --- /dev/null +++ b/src/EventSubscriber/AcceptLanguageSubscriber.php @@ -0,0 +1,93 @@ + + */ + +namespace App\EventSubscriber; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpKernel\Event\RequestEvent; +use function in_array; + +/** + * Class LocaleSubscriber + * + * @package App\EventSubscriber + * @author TLe, Tarmo Leppänen + */ +class AcceptLanguageSubscriber implements EventSubscriberInterface +{ + // Supported locales + public const LOCALE_EN = 'en'; + public const LOCALE_FI = 'fi'; + + /** + * @var array + */ + public const SUPPORTED_LOCALES = [ + self::LOCALE_EN, + self::LOCALE_FI, + ]; + + private string $defaultLocale; + + /** + * LocaleSubscriber constructor. + * + * @param string $locale + */ + public function __construct(string $locale) + { + $this->defaultLocale = $locale; + } + + /** + * Returns an array of event names this subscriber wants to listen to. + * + * The array keys are event names and the value can be: + * + * * The method name to call (priority defaults to 0) + * * An array composed of the method name to call and the priority + * * An array of arrays composed of the method names to call and respective + * priorities, or 0 if unset + * + * For instance: + * + * * ['eventName' => 'methodName'] + * * ['eventName' => ['methodName', $priority]] + * * ['eventName' => [['methodName1', $priority], ['methodName2']]] + * + * @return array The event names to listen to + */ + public static function getSubscribedEvents(): array + { + return [ + RequestEvent::class => [ + 'onKernelRequest', + 100, // Note that this needs to at least `100` to get translation messages as expected + ], + ]; + } + + /** + * Method to change used locale according to current request. + * + * @param RequestEvent $event + */ + public function onKernelRequest(RequestEvent $event): void + { + $request = $event->getRequest(); + + $locale = $request->headers->get('Accept-Language', $this->defaultLocale); + + // Ensure that given locale is supported, if not fallback to default. + if (!in_array($locale, self::SUPPORTED_LOCALES, true)) { + $locale = $this->defaultLocale; + } + + $request->setLocale($locale); + } +} diff --git a/src/Rest/Traits/RestResourceBaseMethods.php b/src/Rest/Traits/RestResourceBaseMethods.php index 55175e27b..a26f701b5 100644 --- a/src/Rest/Traits/RestResourceBaseMethods.php +++ b/src/Rest/Traits/RestResourceBaseMethods.php @@ -12,6 +12,7 @@ use App\Entity\Interfaces\EntityInterface; use App\Repository\Interfaces\BaseRepositoryInterface; use App\Utils\JSON; +use JsonException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Validator\ConstraintViolationInterface; use Symfony\Component\Validator\ConstraintViolationListInterface; @@ -19,6 +20,7 @@ use Symfony\Component\Validator\Validator\ValidatorInterface; use Throwable; use function get_class; +use function str_replace; /** * Trait RestResourceBaseMethods @@ -458,7 +460,7 @@ private function validateDto(RestDtoInterface $dto, bool $skipValidation): void // Oh noes, we have some errors if ($errors !== null && $errors->count() > 0) { - $this->createValidatorException($errors); + $this->createValidatorException($errors, get_class($dto)); } } @@ -476,7 +478,7 @@ private function validateEntity(EntityInterface $entity, bool $skipValidation): // Oh noes, we have some errors if ($errors !== null && $errors->count() > 0) { - $this->createValidatorException($errors); + $this->createValidatorException($errors, get_class($entity)); } } @@ -495,10 +497,11 @@ private function createEntity(): EntityInterface /** * @param ConstraintViolationListInterface $errors + * @param string $target * - * @throws Throwable + * @throws JsonException */ - private function createValidatorException(ConstraintViolationListInterface $errors): void + private function createValidatorException(ConstraintViolationListInterface $errors, string $target): void { $output = []; @@ -507,6 +510,7 @@ private function createValidatorException(ConstraintViolationListInterface $erro $output[] = [ 'message' => $error->getMessage(), 'propertyPath' => $error->getPropertyPath(), + 'target' => str_replace('\\', '.', $target), 'code' => $error->getCode(), ]; } diff --git a/src/Security/Handler/TranslatedAuthenticationFailureHandler.php b/src/Security/Handler/TranslatedAuthenticationFailureHandler.php new file mode 100644 index 000000000..1aebe5d8c --- /dev/null +++ b/src/Security/Handler/TranslatedAuthenticationFailureHandler.php @@ -0,0 +1,66 @@ + + */ + +namespace App\Security\Handler; + +use Lexik\Bundle\JWTAuthenticationBundle\Event\AuthenticationFailureEvent; +use Lexik\Bundle\JWTAuthenticationBundle\Response\JWTAuthenticationFailureResponse; +use Lexik\Bundle\JWTAuthenticationBundle\Security\Http\Authentication\AuthenticationFailureHandler; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Contracts\Translation\TranslatorInterface; + +/** + * Class TranslatedAuthenticationFailureHandler + * + * @package App\Security\Handler + * @author TLe, Tarmo Leppänen + */ +class TranslatedAuthenticationFailureHandler extends AuthenticationFailureHandler +{ + private TranslatorInterface $translator; + + /** + * TranslatedAuthenticationFailureHandler constructor. + * + * @param EventDispatcherInterface $dispatcher + * @param TranslatorInterface $translator + */ + public function __construct(EventDispatcherInterface $dispatcher, TranslatorInterface $translator) + { + parent::__construct($dispatcher); + + $this->translator = $translator; + } + + /** + * @inheritDoc + * + * @param Request $request + * @param AuthenticationException $exception + * + * @return Response + * + * @noinspection PhpMissingParentCallCommonInspection + */ + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response + { + $event = new AuthenticationFailureEvent( + $exception, + new JWTAuthenticationFailureResponse( + $this->translator->trans('Invalid credentials.', [], 'security') + ) + ); + + $this->dispatcher->dispatch($event); + + return $event->getResponse(); + } +} diff --git a/tests/Integration/EventSubscriber/AcceptLanguageSubscriberTest.php b/tests/Integration/EventSubscriber/AcceptLanguageSubscriberTest.php new file mode 100644 index 000000000..9e0f56289 --- /dev/null +++ b/tests/Integration/EventSubscriber/AcceptLanguageSubscriberTest.php @@ -0,0 +1,74 @@ + + */ + +namespace App\Tests\Integration\EventSubscriber; + +use App\EventSubscriber\AcceptLanguageSubscriber; +use Generator; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Event\RequestEvent; +use Symfony\Component\HttpKernel\HttpKernelInterface; + +/** + * Class AcceptLanguageSubscriberTest + * + * @package App\Tests\Integration\EventSubscriber + * @author TLe, Tarmo Leppänen + */ +class AcceptLanguageSubscriberTest extends KernelTestCase +{ + public function testThatSpecifiedDefaultLanguageIsSet(): void + { + static::bootKernel(); + + $request = new Request(); + $request->headers->set('Accept-Language', 'foo'); + + $event = new RequestEvent(static::$kernel, $request, HttpKernelInterface::MASTER_REQUEST); + + $subscriber = new AcceptLanguageSubscriber('bar'); + $subscriber->onKernelRequest($event); + + static::assertSame('bar', $request->getLocale()); + } + + /** + * @dataProvider dataProviderTestThatLocaleIsSetAsExpected + * + * @param string $expected + * @param string $default + * @param string $asked + * + * @testdox Test that when default locale is `$default` and when asking `$asked` locale result is `$expected`. + */ + public function testThatLocaleIsSetAsExpected(string $expected, string $default, string $asked): void + { + static::bootKernel(); + + $request = new Request(); + $request->headers->set('Accept-Language', $asked); + + $event = new RequestEvent(static::$kernel, $request, HttpKernelInterface::MASTER_REQUEST); + + $subscriber = new AcceptLanguageSubscriber($default); + $subscriber->onKernelRequest($event); + + static::assertSame($expected, $request->getLocale()); + } + + /** + * @return Generator + */ + public function dataProviderTestThatLocaleIsSetAsExpected(): Generator + { + yield ['fi', 'fi', 'fi']; + yield ['fi', 'fi', 'sv']; + yield ['en', 'fi', 'en']; + } +} diff --git a/tests/Unit/EventSubscriber/AcceptLanguageSubscriberTest.php b/tests/Unit/EventSubscriber/AcceptLanguageSubscriberTest.php new file mode 100644 index 000000000..0ee654cb0 --- /dev/null +++ b/tests/Unit/EventSubscriber/AcceptLanguageSubscriberTest.php @@ -0,0 +1,34 @@ + + */ + +namespace App\Tests\Unit\EventSubscriber; + +use App\EventSubscriber\AcceptLanguageSubscriber; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Symfony\Component\HttpKernel\Event\RequestEvent; + +/** + * Class AcceptLanguageSubscriberTest + * + * @package App\Tests\Unit\EventSubscriber + * @author TLe, Tarmo Leppänen + */ +class AcceptLanguageSubscriberTest extends KernelTestCase +{ + public function testThatGetSubscribedEventsReturnsExpected(): void + { + $expected = [ + RequestEvent::class => [ + 'onKernelRequest', + 100, + ], + ]; + + static::assertSame($expected, AcceptLanguageSubscriber::getSubscribedEvents()); + } +} diff --git a/translations/security+intl-icu.fi.xlf b/translations/security+intl-icu.fi.xlf new file mode 100644 index 000000000..31d0b95bf --- /dev/null +++ b/translations/security+intl-icu.fi.xlf @@ -0,0 +1,14 @@ + + + +
+ +
+ + + Invalid credentials. + Virheelliset käyttäjätunnukset. + + +
+