Release 2.58 (#5952)
* bump version * fix formatting locale reset after embedded controller sub-requests (#5944) * fix GHSA-c6w6-57jj-62vh * fix GHSA-m492-gv72-xvxj * fix GHSA-jr9p-4h4j-6c58 * make sure to only use JS logic to call API endpoints * fixes GHSA-r8vr-m544-qh4h * make sure to only use JS logic to call API endpoints * fix GHSA-rw46-qg69-vg6h * fix GHSA-pj8j-p4g4-4vw8 - prevent kimai from rendering images via markdown * fix GHSA-pj8j-p4g4-4vw8 - use a safe network client to prevent SSRF via images * fix GHSA-xv4r-4885-gwpg * fix GHSA-pgcc-vfmc-7cw5 - move GET routes to API with POST method to prevent CSRF * fix tooltip survives page reload * updated wizard images * split wizard and password reset subscriber into two classes * relax upper php limit * added zizmor workflow scans and apply findings * user permissions <name>_other_profile now respect teams * move all linting steps to new job * updated docker image version names * use .env.local for storing APP_SECRET * improve build order and use given tag as ref for checkout, not default main branch * improved APP_SECRET handling, see entrypoint.sh * use local code for building the image for more flexibility, added dockerignore
This commit is contained in:
@@ -25,11 +25,11 @@ abstract class AbstractTimesheetSubscriber extends AbstractActionsSubscriber
|
||||
$timesheet = $payload['timesheet'];
|
||||
if ($timesheet->getId() !== null) {
|
||||
if ($timesheet->isRunning() && $this->isGranted('stop', $timesheet)) {
|
||||
$event->addAction('stop', ['url' => $this->path('stop_timesheet', ['id' => $timesheet->getId()]), 'class' => 'api-link dd-ts-stop', 'attr' => ['data-event' => 'kimai.timesheetStop kimai.timesheetUpdate', 'data-method' => 'PATCH', 'data-msg-error' => 'timesheet.stop.error', 'data-msg-success' => 'timesheet.stop.success']]);
|
||||
$event->addAction('stop', ['url' => '#', 'class' => 'api-link dd-ts-stop', 'attr' => ['data-event' => 'kimai.timesheetStop kimai.timesheetUpdate', 'data-href' => $this->path('stop_timesheet', ['id' => $timesheet->getId()]), 'data-method' => 'PATCH', 'data-msg-error' => 'timesheet.stop.error', 'data-msg-success' => 'timesheet.stop.success']]);
|
||||
}
|
||||
|
||||
if (!$timesheet->isRunning() && $this->isGranted('start', $timesheet)) {
|
||||
$event->addAction('repeat', ['title' => 'repeat', 'url' => $this->path('restart_timesheet', ['id' => $timesheet->getId()]), 'class' => 'api-link dd-ts-repeat', 'attr' => ['data-payload' => '{"copy": "all"}', 'data-event' => 'kimai.timesheetStart kimai.timesheetUpdate', 'data-method' => 'PATCH', 'data-msg-error' => 'timesheet.start.error', 'data-msg-success' => 'timesheet.start.success']]);
|
||||
$event->addAction('repeat', ['title' => 'repeat', 'url' => '#', 'class' => 'api-link dd-ts-repeat', 'attr' => ['data-payload' => '{"copy": "all"}', 'data-event' => 'kimai.timesheetStart kimai.timesheetUpdate', 'data-href' => $this->path('restart_timesheet', ['id' => $timesheet->getId()]), 'data-method' => 'PATCH', 'data-msg-error' => 'timesheet.start.error', 'data-msg-success' => 'timesheet.start.success']]);
|
||||
}
|
||||
|
||||
if ($this->isGranted('edit', $timesheet)) {
|
||||
|
||||
70
src/EventSubscriber/PasswordResetSubscriber.php
Normal file
70
src/EventSubscriber/PasswordResetSubscriber.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\EventSubscriber;
|
||||
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
|
||||
class PasswordResetSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly UrlGeneratorInterface $urlGenerator,
|
||||
private readonly AuthorizationCheckerInterface $security,
|
||||
private readonly TokenStorageInterface $storage,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
// higher priority is executed earlier - need to be higher than wizard
|
||||
KernelEvents::REQUEST => ['onKernelRequest', -20]
|
||||
];
|
||||
}
|
||||
|
||||
public function onKernelRequest(RequestEvent $event): void
|
||||
{
|
||||
// ignore sub-requests
|
||||
if (!$event->isMainRequest() || null === ($token = $this->storage->getToken())) {
|
||||
return;
|
||||
}
|
||||
|
||||
$uri = $event->getRequest()->getRequestUri();
|
||||
|
||||
// never trigger password reset on API calls
|
||||
// TODO 3.0 remove /register/
|
||||
if (str_starts_with($uri, '/api/') || stripos($uri, '/register/') !== false || stripos($uri, '/wizard/') !== false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user = $token->getUser();
|
||||
|
||||
if (!($user instanceof User)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->security->isGranted('IS_AUTHENTICATED_FULLY')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$user->requiresPasswordReset()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$response = new RedirectResponse($this->urlGenerator->generate('wizard', ['wizard' => 'password']));
|
||||
$event->setResponse($response);
|
||||
}
|
||||
}
|
||||
@@ -10,11 +10,13 @@
|
||||
namespace App\EventSubscriber;
|
||||
|
||||
use App\Configuration\LocaleService;
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
|
||||
/**
|
||||
* When visiting the homepage, this listener redirects the user to the most
|
||||
@@ -24,7 +26,8 @@ final class RedirectToLocaleSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly UrlGeneratorInterface $urlGenerator,
|
||||
private readonly LocaleService $localeService
|
||||
private readonly LocaleService $localeService,
|
||||
private readonly TokenStorageInterface $storage,
|
||||
)
|
||||
{
|
||||
}
|
||||
@@ -32,7 +35,9 @@ final class RedirectToLocaleSubscriber implements EventSubscriberInterface
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
KernelEvents::REQUEST => ['onKernelRequest']
|
||||
// the higher the priority (default: 0), the earlier it is executed
|
||||
// runs on default priority to make sure we have the correct locale in the URL
|
||||
KernelEvents::REQUEST => ['onKernelRequest', 0]
|
||||
];
|
||||
}
|
||||
|
||||
@@ -52,15 +57,26 @@ final class RedirectToLocaleSubscriber implements EventSubscriberInterface
|
||||
return;
|
||||
}
|
||||
|
||||
$allLanguages = $this->localeService->getTranslatedLocales();
|
||||
$preferredLanguage = null;
|
||||
|
||||
// Add the default locale at the first position of the array, because getPreferredLanguage()
|
||||
// returns the first element when no appropriate language is found
|
||||
array_unshift($allLanguages, 'en');
|
||||
if (null !== ($token = $this->storage->getToken())) {
|
||||
$user = $token->getUser();
|
||||
if ($user instanceof User) {
|
||||
$preferredLanguage = $user->getLanguage();
|
||||
}
|
||||
}
|
||||
|
||||
$preferredLanguage = $request->getPreferredLanguage(array_unique($allLanguages));
|
||||
if ($preferredLanguage === null){
|
||||
$allLanguages = $this->localeService->getTranslatedLocales();
|
||||
|
||||
$response = new RedirectResponse($this->urlGenerator->generate('homepage', ['_locale' => $preferredLanguage]));
|
||||
// Add the default locale at the first position of the array, because getPreferredLanguage()
|
||||
// returns the first element when no appropriate language is found
|
||||
array_unshift($allLanguages, 'en');
|
||||
|
||||
$preferredLanguage = $request->getPreferredLanguage(array_unique($allLanguages));
|
||||
}
|
||||
|
||||
$response = new RedirectResponse($this->urlGenerator->generate('homepage', ['_locale' => $preferredLanguage ?? 'en']));
|
||||
$event->setResponse($response);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace App\EventSubscriber;
|
||||
use App\Entity\User;
|
||||
use App\Twig\LocaleFormatExtensions;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpKernel\Event\FinishRequestEvent;
|
||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
@@ -19,6 +20,8 @@ use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
|
||||
final class UserEnvironmentSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
private ?string $userLocale = null;
|
||||
|
||||
public function __construct(
|
||||
private readonly TokenStorageInterface $tokenStorage,
|
||||
private readonly AuthorizationCheckerInterface $auth,
|
||||
@@ -30,10 +33,30 @@ final class UserEnvironmentSubscriber implements EventSubscriberInterface
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
KernelEvents::REQUEST => ['prepareEnvironment', -100],
|
||||
// runs as first one in Kimai, to make sure we use the correct locales for rendering
|
||||
KernelEvents::REQUEST => ['prepareEnvironment', -10],
|
||||
// don't know why do we use -20
|
||||
KernelEvents::FINISH_REQUEST => ['restoreLocale', -20],
|
||||
];
|
||||
}
|
||||
|
||||
public function restoreLocale(FinishRequestEvent $event): void
|
||||
{
|
||||
if ($event->isMainRequest()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->userLocale === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// LocaleSwitcher (called by LocaleAwareListener) overwrites \Locale::getDefault() with the URL
|
||||
// locale during sub-requests. Restore both the PHP default and the Twig formatter locale to
|
||||
// the user's formatting locale that was saved during the main request.
|
||||
\Locale::setDefault($this->userLocale);
|
||||
$this->localeFormatExtensions->setLocale($this->userLocale);
|
||||
}
|
||||
|
||||
public function prepareEnvironment(RequestEvent $event): void
|
||||
{
|
||||
// ignore sub-requests
|
||||
@@ -55,6 +78,7 @@ final class UserEnvironmentSubscriber implements EventSubscriberInterface
|
||||
}
|
||||
|
||||
// the locale is primarily used for formatting values, so we depend on the user locale if available
|
||||
$this->userLocale = $locale;
|
||||
\Locale::setDefault($locale);
|
||||
$this->localeFormatExtensions->setLocale($locale);
|
||||
}
|
||||
|
||||
@@ -22,35 +22,31 @@ use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
class WizardSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private UrlGeneratorInterface $urlGenerator,
|
||||
private AuthorizationCheckerInterface $security,
|
||||
private TokenStorageInterface $storage,
|
||||
private SystemConfiguration $systemConfiguration
|
||||
private readonly UrlGeneratorInterface $urlGenerator,
|
||||
private readonly AuthorizationCheckerInterface $security,
|
||||
private readonly TokenStorageInterface $storage,
|
||||
private readonly SystemConfiguration $systemConfiguration
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
KernelEvents::REQUEST => ['onKernelRequest']
|
||||
KernelEvents::REQUEST => ['onKernelRequest', -30]
|
||||
];
|
||||
}
|
||||
|
||||
public function onKernelRequest(RequestEvent $event): void
|
||||
{
|
||||
// ignore sub-requests
|
||||
if (!$event->isMainRequest()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ignore events like the toolbar where we do not have a token
|
||||
if (null === ($token = $this->storage->getToken())) {
|
||||
// ignore sub-requests and un-authenticated events
|
||||
if (!$event->isMainRequest() || null === ($token = $this->storage->getToken())) {
|
||||
return;
|
||||
}
|
||||
|
||||
$uri = $event->getRequest()->getRequestUri();
|
||||
|
||||
// never require 2FA on API calls
|
||||
// never trigger wizard on API calls
|
||||
// TODO 3.0 remove /register/
|
||||
if (str_starts_with($uri, '/api/') || stripos($uri, '/register/') !== false || stripos($uri, '/wizard/') !== false) {
|
||||
return;
|
||||
}
|
||||
@@ -65,11 +61,6 @@ class WizardSubscriber implements EventSubscriberInterface
|
||||
return;
|
||||
}
|
||||
|
||||
if ($user->requiresPasswordReset()) {
|
||||
$response = new RedirectResponse($this->urlGenerator->generate('wizard', ['wizard' => 'password']));
|
||||
$event->setResponse($response);
|
||||
}
|
||||
|
||||
if ($user->isRegularUserOnly() && !$this->systemConfiguration->isUserWizardActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user