diff --git a/config/services.yaml b/config/services.yaml index 6516d109..0cbd61c0 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -80,6 +80,11 @@ services: tags: - { name: kernel.event_subscriber } + App\EventListener\LocaleListener: + arguments: + $defaultLocale: "%locale%" + $supportedLocales: "%supported_locales%" + App\Handler\: resource: '../src/Handler/*' public: true diff --git a/features/language.feature b/features/language.feature index 6fabc811..12d4fe66 100644 --- a/features/language.feature +++ b/features/language.feature @@ -13,18 +13,21 @@ Feature: Language detection Scenario: Language detection And I am on "/" - Then I should see text matching "Welcome" + Then I should be on "/en/" + And I should see text matching "Welcome" @language Scenario: Language detection Given set the HTTP-Header "Accept-Language" to "de" And I am on "/" - Then I should see text matching "Willkommen" + Then I should be on "/de/" + And I should see text matching "Willkommen" @language - Scenario: Missing language fallback + Scenario: Language fallback Given set the HTTP-Header "Accept-Language" to "afa" And I am on "/" - Then I should see text matching "Welcome" + Then I should be on "/en/" + And I should see text matching "Welcome" diff --git a/features/login.feature b/features/login.feature index 4b786374..894d3bae 100644 --- a/features/login.feature +++ b/features/login.feature @@ -96,7 +96,7 @@ Feature: Login @logout Scenario: Logout When I am authenticated as "louis@example.org" - And I am on "/logout" + And I am on "/en/logout" When I am on "/admin/dashboard" Then I should be on "/en/login" @@ -104,7 +104,7 @@ Feature: Login @logout Scenario: Logout When I am authenticated as "louis@example.org" - And I am on "/logout" + And I am on "/en/logout" Then I should see text matching "You are now logged out." diff --git a/features/registration.feature b/features/registration.feature index f2d7c094..88e63a76 100644 --- a/features/registration.feature +++ b/features/registration.feature @@ -32,7 +32,7 @@ Feature: registration And I should see text matching "The following recovery token got created for you" And I should see text matching regex "/^[0-9a-f]{8}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{12}+$/" in element with selector ".recovery-token" - When I am on "/logout" + When I am on "/en/logout" Then I should be on "/en/" When I am on "/en/login" diff --git a/src/Controller/DeleteController.php b/src/Controller/DeleteController.php index e82ae675..afac6933 100644 --- a/src/Controller/DeleteController.php +++ b/src/Controller/DeleteController.php @@ -27,7 +27,7 @@ public function __construct(private readonly DeleteHandler $deleteHandler, priva * @param $aliasId * @return Response */ - #[Route(path: '/{_locale<%locales%>}/alias/delete/{aliasId}', name: 'alias_delete')] + #[Route(path: '/{_locale}/alias/delete/{aliasId}', name: 'alias_delete')] public function deleteAlias(Request $request, $aliasId): Response { $user = $this->getUser(); @@ -67,7 +67,7 @@ public function deleteAlias(Request $request, $aliasId): Response * @param Request $request * @return RedirectResponse|Response */ - #[Route(path: '/{_locale<%locales%>}/user/delete', name: 'user_delete')] + #[Route(path: '/{_locale}/user/delete', name: 'user_delete')] public function deleteUser(Request $request): RedirectResponse|Response { $user = $this->getUser(); diff --git a/src/Controller/InitController.php b/src/Controller/InitController.php index 0a90b4cc..1dc74a88 100644 --- a/src/Controller/InitController.php +++ b/src/Controller/InitController.php @@ -28,7 +28,8 @@ public function __construct(private readonly EntityManagerInterface $manager, pr * @return Response * @throws ValidationException */ - #[Route(path: '/{_locale<%locales%>}/init', name: 'init')] + #[Route(path: '/init', name: 'init_no_locale')] + #[Route(path: '/{_locale}/init', name: 'init')] public function index(Request $request): Response { // redirect if already configured @@ -63,7 +64,7 @@ public function index(Request $request): Response * @param Request $request * @return Response */ - #[Route(path: '/{_locale<%locales%>}/init/user', name: 'init_user')] + #[Route(path: '/{_locale}/init/user', name: 'init_user')] public function user(Request $request): Response { // redirect if already configured diff --git a/src/Controller/RecoveryController.php b/src/Controller/RecoveryController.php index e24bbe4d..bf47fdfe 100644 --- a/src/Controller/RecoveryController.php +++ b/src/Controller/RecoveryController.php @@ -42,26 +42,13 @@ public function __construct( { } - /** - * @param Request $request - * @return RedirectResponse - */ - #[Route(path: '/recovery', name: 'recovery_no_locale')] - public function recoveryNoLocale(Request $request): RedirectResponse - { - $supportedLocales = (array)$this->getParameter('supported_locales'); - $preferredLanguage = $request->getPreferredLanguage($supportedLocales); - $locale = $preferredLanguage ?: $request->getLocale(); - - return $this->redirectToRoute('recovery', ['_locale' => $locale]); - } - /** * @param Request $request * @return Response * @throws Exception */ - #[Route(path: '/{_locale<%locales%>}/recovery', name: 'recovery')] + #[Route(path: '/recovery', name: 'recovery_no_locale')] + #[Route(path: '/{_locale}/recovery', name: 'recovery')] public function recoveryProcess(Request $request): Response { $recoveryProcess = new RecoveryProcess(); @@ -123,7 +110,7 @@ public function recoveryProcess(Request $request): Response * @return Response * @throws Exception */ - #[Route(path: '/{_locale<%locales%>}/recovery/reset_password', name: 'recovery_reset_password')] + #[Route(path: '/{_locale}/recovery/reset_password', name: 'recovery_reset_password')] public function recoveryResetPassword(Request $request): Response { $recoveryResetPassword = new RecoveryResetPassword(); @@ -204,7 +191,7 @@ public function recoveryResetPassword(Request $request): Response * @return Response * @throws Exception */ - #[Route(path: '/{_locale<%locales%>}/user/recovery_token', name: 'user_recovery_token')] + #[Route(path: '/{_locale}/user/recovery_token', name: 'user_recovery_token')] public function recoveryToken(Request $request): Response { if (null === $user = $this->getUser()) { @@ -273,7 +260,7 @@ public function recoveryToken(Request $request): Response * @param Request $request * @return Response */ - #[Route(path: '/{_locale<%locales%>}/recovery/recovery_token/ack', name: 'recovery_recovery_token_ack')] + #[Route(path: '/{_locale}/recovery/recovery_token/ack', name: 'recovery_recovery_token_ack')] public function recoveryRecoveryTokenAck(Request $request): Response { $recoveryTokenAck = new RecoveryTokenAck(); @@ -311,7 +298,7 @@ public function recoveryRecoveryTokenAck(Request $request): Response * @param Request $request * @return Response */ - #[Route(path: '/{_locale<%locales%>}/user/recovery_token/ack', name: 'user_recovery_token_ack')] + #[Route(path: '/{_locale}/user/recovery_token/ack', name: 'user_recovery_token_ack')] public function recoveryTokenAck(Request $request): Response { $recoveryTokenAck = new RecoveryTokenAck(); diff --git a/src/Controller/RegistrationController.php b/src/Controller/RegistrationController.php index d2f21d3e..71fc9540 100644 --- a/src/Controller/RegistrationController.php +++ b/src/Controller/RegistrationController.php @@ -27,7 +27,7 @@ public function __construct(private readonly RegistrationHandler $registrationHa * @param Request $request * @return Response */ - #[Route(path: '/{_locale<%locales%>}/register/recovery_token', name: 'register_recovery_token')] + #[Route(path: '/{_locale}/register/recovery_token', name: 'register_recovery_token')] public function registerRecoveryTokenAck(Request $request): Response { $recoveryTokenAck = new RecoveryTokenAck(); @@ -58,7 +58,7 @@ public function registerRecoveryTokenAck(Request $request): Response * @param Request $request * @return Response */ - #[Route(path: '/{_locale<%locales%>}/register/welcome', name: 'register_welcome')] + #[Route(path: '/{_locale}/register/welcome', name: 'register_welcome')] public function welcome(Request $request): Response { $request->getSession()->getFlashBag()->add('success', 'flashes.registration-successful'); @@ -72,8 +72,10 @@ public function welcome(Request $request): Response * @return Response * @throws Exception */ - #[Route(path: '/{_locale<%locales%>}/register', name: 'register')] - #[Route(path: '/{_locale<%locales%>}/register/{voucher}', name: 'register_voucher')] + #[Route(path: '/register', name: 'register_no_locale')] + #[Route(path: '/{_locale}/register', name: 'register')] + #[Route(path: '/register/{voucher}', name: 'register_voucher_no_locale')] + #[Route(path: '/{_locale}/register/{voucher}', name: 'register_voucher')] public function register(Request $request, string $voucher = null): Response { if (!$this->registrationHandler->isRegistrationOpen()) { diff --git a/src/Controller/SecurityController.php b/src/Controller/SecurityController.php index aa49a26c..1ed99b0d 100644 --- a/src/Controller/SecurityController.php +++ b/src/Controller/SecurityController.php @@ -11,25 +11,12 @@ class SecurityController extends AbstractController { - /** - * @param Request $request - * @return RedirectResponse - */ - #[Route(path: '/login', name: 'login_no_locale')] - public function indexNoLocale(Request $request): RedirectResponse - { - $supportedLocales = (array)$this->getParameter('supported_locales'); - $preferredLanguage = $request->getPreferredLanguage($supportedLocales); - $locale = $preferredLanguage ?: $request->getLocale(); - - return $this->redirectToRoute('login', ['_locale' => $locale]); - } - /** * @param AuthenticationUtils $authenticationUtils * @return Response */ - #[Route(path: '/{_locale<%locales%>}/login', name: 'login')] + #[Route(path: '/login', name: 'login_no_locale')] + #[Route(path: '/{_locale}/login', name: 'login')] public function login(AuthenticationUtils $authenticationUtils): Response { // get the login error if there is one @@ -46,7 +33,8 @@ public function login(AuthenticationUtils $authenticationUtils): Response /** * @return void */ - #[Route(path: '/logout', name: 'logout', methods: ['GET'])] + #[Route(path: '/logout', name: 'logout_no_locale', methods: ['GET'])] + #[Route(path: '/{_locale}/logout', name: 'logout', methods: ['GET'])] public function logout(): void { diff --git a/src/Controller/StartController.php b/src/Controller/StartController.php index 83271a9a..5df05510 100644 --- a/src/Controller/StartController.php +++ b/src/Controller/StartController.php @@ -49,24 +49,11 @@ public function __construct( { } - /** - * @param Request $request - * @return RedirectResponse - */ - #[Route(path: '/', name: 'index_no_locale')] - public function indexNoLocale(Request $request): RedirectResponse - { - $supportedLocales = (array)$this->getParameter('supported_locales'); - $preferredLanguage = $request->getPreferredLanguage($supportedLocales); - $locale = $preferredLanguage ?: $request->getLocale(); - - return $this->redirectToRoute('index', ['_locale' => $locale]); - } - /** * @return Response */ - #[Route(path: '/{_locale<%locales%>}/', name: 'index')] + #[Route(path: '/', name: 'index_no_locale')] + #[Route(path: '/{_locale}/', name: 'index')] public function index(): Response { // forward to start if logged in @@ -82,24 +69,11 @@ public function index(): Response return $this->render('Start/index_anonymous.html.twig'); } - /** - * @param Request $request - * @return RedirectResponse - */ - #[Route(path: '/start', name: 'start_no_locale')] - public function startNoLocale(Request $request): RedirectResponse - { - $supportedLocales = (array)$this->getParameter('supported_locales'); - $preferredLanguage = $request->getPreferredLanguage($supportedLocales); - $locale = $preferredLanguage ?: $request->getLocale(); - - return $this->redirectToRoute('start', ['_locale' => $locale]); - } - /** * @return Response */ - #[Route(path: '/{_locale<%locales%>}/start', name: 'start')] + #[Route(path: '/start', name: 'start_no_locale')] + #[Route(path: '/{_locale}/start', name: 'start')] public function start(): Response { if ($this->isGranted(Roles::SPAM)) { @@ -122,7 +96,7 @@ public function start(): Response * @param Request $request * @return Response */ - #[Route(path: '/{_locale<%locales%>}/voucher', name: 'vouchers', requirements: ['_locale' => '%locales%'])] + #[Route(path: '/{_locale}/voucher', name: 'vouchers')] public function voucher(Request $request): Response { /** @var User $user */ @@ -162,7 +136,7 @@ public function voucher(Request $request): Response * @param Request $request * @return Response */ - #[Route(path: '/{_locale<%locales%>}/alias', name: 'aliases')] + #[Route(path: '/{_locale}/alias', name: 'aliases')] public function alias(Request $request): Response { /** @var User $user */ @@ -224,7 +198,7 @@ public function alias(Request $request): Response * @return Response * @throws Exception */ - #[Route(path: '/{_locale<%locales%>}/account', name: 'account')] + #[Route(path: '/{_locale}/account', name: 'account')] public function account(Request $request): Response { /** @var User $user */ @@ -264,7 +238,7 @@ public function account(Request $request): Response * @param Request $request * @return Response|null */ - #[Route(path: '/{_locale<%locales%>}/openpgp', name: 'openpgp')] + #[Route(path: '/{_locale}/openpgp', name: 'openpgp')] public function openPgp(Request $request): ?Response { /** @var User $user */ diff --git a/src/Controller/TwofactorController.php b/src/Controller/TwofactorController.php index ceccf1fc..ac182e20 100644 --- a/src/Controller/TwofactorController.php +++ b/src/Controller/TwofactorController.php @@ -2,19 +2,19 @@ namespace App\Controller; -use Doctrine\ORM\EntityManagerInterface; -use Exception; use App\Form\Model\Twofactor; use App\Form\Model\TwofactorBackupAck; use App\Form\Model\TwofactorConfirm; use App\Form\TwofactorBackupAckType; use App\Form\TwofactorConfirmType; use App\Form\TwofactorType; +use Doctrine\ORM\EntityManagerInterface; use Endroid\QrCode\Builder\Builder; use Endroid\QrCode\Encoding\Encoding; -use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelHigh; -use Endroid\QrCode\RoundBlockSizeMode\RoundBlockSizeModeMargin; +use Endroid\QrCode\ErrorCorrectionLevel; +use Endroid\QrCode\RoundBlockSizeMode; use Endroid\QrCode\Writer\PngWriter; +use RuntimeException; use Scheb\TwoFactorBundle\Model\Totp\TwoFactorInterface; use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Totp\TotpAuthenticatorInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -27,21 +27,20 @@ class TwofactorController extends AbstractController { public function __construct(private readonly EntityManagerInterface $manager) { - } /** * @param Request $request * @param TotpAuthenticatorInterface $totpAuthenticator * @return Response - * @throws Exception + * @throws RuntimeException */ - #[Route(path: '/{_locale<%locales%>}/user/twofactor', name: 'user_twofactor')] + #[Route(path: '/{_locale}/user/twofactor', name: 'user_twofactor')] public function twofactor(Request $request, TotpAuthenticatorInterface $totpAuthenticator): Response { /** @var $user TwoFactorInterface */ if (null === $user = $this->getUser()) { - throw new Exception('User should not be null'); + throw new RuntimeException('User should not be null'); } $form = $this->createForm(TwofactorType::class, new Twofactor()); @@ -99,13 +98,13 @@ public function twofactor(Request $request, TotpAuthenticatorInterface $totpAuth /** * @param Request $request * @return Response - * @throws Exception + * @throws RuntimeException */ - #[Route(path: '/{_locale<%locales%>}/user/twofactor_confirm', name: 'user_twofactor_confirm')] + #[Route(path: '/{_locale}/user/twofactor_confirm', name: 'user_twofactor_confirm')] public function twofactorConfirm(Request $request): Response { if (null === $user = $this->getUser()) { - throw new Exception('User should not be null'); + throw new RuntimeException('User should not be null'); } $confirmForm = $this->createForm(TwofactorConfirmType::class, new TwofactorConfirm()); @@ -175,13 +174,13 @@ public function twofactorConfirm(Request $request): Response /** * @param Request $request * @return Response - * @throws Exception + * @throws RuntimeException */ - #[Route(path: '/{_locale<%locales%>}/user/twofactor_backup_codes', name: 'user_twofactor_backup_ack')] + #[Route(path: '/{_locale}/user/twofactor_backup_codes', name: 'user_twofactor_backup_ack')] public function twofactorBackupAck(Request $request): Response { if (null === $user = $this->getUser()) { - throw new Exception('User should not be null'); + throw new RuntimeException('User should not be null'); } $backupAckForm = $this->createForm( @@ -251,13 +250,13 @@ public function twofactorBackupAck(Request $request): Response /** * @param Request $request * @return Response - * @throws Exception + * @throws RuntimeException */ - #[Route(path: '/{_locale<%locales%>}/user/twofactor_disable', name: 'user_twofactor_disable')] + #[Route(path: '/{_locale}/user/twofactor_disable', name: 'user_twofactor_disable')] public function twofactorDisable(Request $request): Response { if (null === $user = $this->getUser()) { - throw new Exception('User should not be null'); + throw new RuntimeException('User should not be null'); } $disableForm = $this->createForm(TwofactorType::class, new Twofactor()); @@ -301,13 +300,13 @@ public function twofactorDisable(Request $request): Response /** * @param TotpAuthenticatorInterface $totpAuthenticator * @return Response - * @throws Exception + * @throws RuntimeException */ - #[Route(path: '/{_locale<%locales%>}/user/twofactor/qrcode', name: 'user_twofactor_qrcode')] + #[Route(path: '/{_locale}/user/twofactor/qrcode', name: 'user_twofactor_qrcode')] public function displayTotpQrCode(TotpAuthenticatorInterface $totpAuthenticator): Response { if (null === $user = $this->getUser()) { - throw new Exception('User should not be null'); + throw new RuntimeException('User should not be null'); } if (!($user instanceof TwoFactorInterface)) { throw new NotFoundHttpException('Cannot display QR code'); @@ -318,10 +317,10 @@ public function displayTotpQrCode(TotpAuthenticatorInterface $totpAuthenticator) ->writerOptions([]) ->data($totpAuthenticator->getQRContent($user)) ->encoding(new Encoding('UTF-8')) - ->errorCorrectionLevel(new ErrorCorrectionLevelHigh()) + ->errorCorrectionLevel(ErrorCorrectionLevel::High) ->size(320) ->margin(20) - ->roundBlockSizeMode(new RoundBlockSizeModeMargin()) + ->roundBlockSizeMode(RoundBlockSizeMode::Margin) ->build(); return new Response($result->getString(), Response::HTTP_OK, ['Content-Type' => 'image/png']); diff --git a/src/EventListener/LocaleListener.php b/src/EventListener/LocaleListener.php new file mode 100644 index 00000000..c1e61204 --- /dev/null +++ b/src/EventListener/LocaleListener.php @@ -0,0 +1,43 @@ +getRequest(); + // No localisation for admin routes + if (str_starts_with($request->getPathInfo(), '/admin/')) { + return; + } + + // Redirect routes without locale to localized route + if (!$request->attributes->get('_locale')) { + $preferredLanguage = $request->getPreferredLanguage($this->supportedLocales); + $locale = $preferredLanguage ?: $this->defaultLocale; + $event->setResponse(new RedirectResponse('/' . $locale . $request->getPathInfo())); + } + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents(): array + { + // must be registered before (i.e. with a higher priority than) the default Locale listener + return [KernelEvents::REQUEST => [['onKernelRequest', 20]]]; + } +}