Release 2.16 (#4780)
This commit is contained in:
@@ -27,7 +27,8 @@ final class ApiRequestMatcher implements RequestMatcherInterface
|
||||
}
|
||||
|
||||
// let's use this firewall if a Bearer token is set in the header
|
||||
if ($request->headers->has('Authorization')) {
|
||||
// other cases like "bearer" are rejected earlier
|
||||
if (($auth = $request->headers->get('Authorization')) !== null && str_starts_with($auth, 'Bearer ')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -232,7 +232,7 @@ final class UserController extends BaseApiController
|
||||
throw $this->createAccessDeniedException('User has no access to API tokens');
|
||||
}
|
||||
|
||||
if ($accessToken->getUser() !== $user) {
|
||||
if (!$this->isGranted('api-token', $accessToken->getUser())) {
|
||||
throw $this->createAccessDeniedException('You are not allowed to delete this access token');
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,12 @@ final class RegenerateLocalesCommand extends Command
|
||||
* @var string[]
|
||||
*/
|
||||
private array $noRegionCode = ['ar', 'id', 'pa', 'sl'];
|
||||
/**
|
||||
* A list of locales that will be activated, not matter if translation files exist for them.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
private array $addLocaleToList = ['zh_Hant_TW'];
|
||||
|
||||
public function __construct(
|
||||
private readonly string $projectDirectory,
|
||||
@@ -74,8 +80,8 @@ final class RegenerateLocalesCommand extends Command
|
||||
foreach ($translationFilenames as $file) {
|
||||
$firstLevelLocales[] = explode('.', basename($file))[1];
|
||||
}
|
||||
$firstLevelLocales = array_unique($firstLevelLocales);
|
||||
$io->title('Locales found from translation files');
|
||||
$firstLevelLocales = array_unique(array_merge($firstLevelLocales, $this->addLocaleToList));
|
||||
$io->title('First level locales found');
|
||||
$io->writeln(implode('|', $firstLevelLocales));
|
||||
|
||||
$secondLevel = [];
|
||||
@@ -95,7 +101,7 @@ final class RegenerateLocalesCommand extends Command
|
||||
sort($firstLevelLocales);
|
||||
sort($secondLevel);
|
||||
|
||||
// keep the locales that have translation filesat the begin
|
||||
// keep the locales that have translation files at the beginning
|
||||
// the config is than easier to read and the locales will be sorted in the UI anyway
|
||||
$locales = array_merge($firstLevelLocales, $secondLevel);
|
||||
|
||||
|
||||
@@ -17,11 +17,11 @@ class Constants
|
||||
/**
|
||||
* The current release version
|
||||
*/
|
||||
public const VERSION = '2.15.0';
|
||||
public const VERSION = '2.16.0';
|
||||
/**
|
||||
* The current release: major * 10000 + minor * 100 + patch
|
||||
*/
|
||||
public const VERSION_ID = 21500;
|
||||
public const VERSION_ID = 21600;
|
||||
/**
|
||||
* The software name
|
||||
*/
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace App\Controller;
|
||||
use App\Constants;
|
||||
use App\Utils\PageSetup;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
#[Route(path: '/about')]
|
||||
final class AboutController extends AbstractController
|
||||
|
||||
@@ -42,7 +42,7 @@ use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,7 +14,7 @@ use App\Saml\SamlAuthFactory;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Security\Http\SecurityRequestAttributes;
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ use App\Repository\BookmarkRepository;
|
||||
use App\Utils\ProfileManager;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Core\Exception\RuntimeException;
|
||||
use Symfony\Component\Security\Csrf\CsrfToken;
|
||||
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||||
|
||||
@@ -17,7 +17,7 @@ use App\Timesheet\TrackingModeService;
|
||||
use App\Utils\PageSetup;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,7 +19,7 @@ use App\WorkingTime\WorkingTimeService;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
/**
|
||||
* Users can control their working time statistics
|
||||
|
||||
@@ -43,7 +43,7 @@ use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Csrf\CsrfToken;
|
||||
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
@@ -21,7 +21,7 @@ use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,7 +15,7 @@ use App\Utils\PageSetup;
|
||||
use App\Utils\ReleaseVersion;
|
||||
use Composer\InstalledVersions;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Csrf\CsrfToken;
|
||||
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
@@ -19,7 +19,7 @@ use App\Utils\PageSetup;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace App\Controller;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Timesheet\FavoriteRecordService;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[Route(path: '/favorite')]
|
||||
|
||||
@@ -19,7 +19,7 @@ use Pagerfanta\Adapter\ArrayAdapter;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Intl\Locales;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[Route(path: '/help')]
|
||||
|
||||
@@ -16,7 +16,7 @@ use App\Repository\UserRepository;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
/**
|
||||
|
||||
@@ -45,7 +45,7 @@ use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Csrf\CsrfToken;
|
||||
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
@@ -27,7 +27,7 @@ use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace App\Controller;
|
||||
use App\Plugin\PluginManager;
|
||||
use App\Utils\PageSetup;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
use Symfony\Contracts\Cache\ItemInterface;
|
||||
|
||||
@@ -43,7 +43,7 @@ use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
/**
|
||||
@@ -173,7 +173,7 @@ final class ProfileController extends AbstractController
|
||||
$this->flashSuccess('action.update.success');
|
||||
$request->getSession()->set('_show_access_token', $accessToken->getId());
|
||||
|
||||
return new Response();
|
||||
return $this->redirectToRoute('user_profile_api_token', ['username' => $profile->getUserIdentifier()]);
|
||||
}
|
||||
|
||||
return $this->render('user/access-token.html.twig', [
|
||||
|
||||
@@ -47,7 +47,7 @@ use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Csrf\CsrfToken;
|
||||
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
@@ -19,7 +19,7 @@ use App\Timesheet\TimesheetService;
|
||||
use App\Utils\PageSetup;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,7 +21,7 @@ use App\Repository\UserRepository;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Html;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[Route(path: '/reporting/customer/monthly_projects')]
|
||||
|
||||
@@ -18,7 +18,7 @@ use App\Reporting\ProjectDateRange\ProjectDateRangeQuery;
|
||||
use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
final class ProjectDateRangeController extends AbstractController
|
||||
|
||||
@@ -17,7 +17,7 @@ use App\Utils\PageSetup;
|
||||
use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
final class ProjectDetailsController extends AbstractController
|
||||
|
||||
@@ -16,7 +16,7 @@ use App\Reporting\ProjectInactive\ProjectInactiveQuery;
|
||||
use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
final class ProjectInactiveController extends AbstractController
|
||||
|
||||
@@ -16,7 +16,7 @@ use App\Reporting\ProjectView\ProjectViewQuery;
|
||||
use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
final class ProjectViewController extends AbstractController
|
||||
|
||||
@@ -23,7 +23,7 @@ use App\Timesheet\TimesheetStatisticService;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Html;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[Route(path: '/reporting/users')]
|
||||
|
||||
@@ -23,7 +23,7 @@ use App\Timesheet\TimesheetStatisticService;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Html;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[Route(path: '/reporting/users')]
|
||||
|
||||
@@ -24,7 +24,7 @@ use App\Timesheet\TimesheetStatisticService;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Html;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[Route(path: '/reporting/users')]
|
||||
|
||||
@@ -19,7 +19,7 @@ use Exception;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Html;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
@@ -113,6 +113,8 @@ final class UserMonthController extends AbstractUserReportController
|
||||
'current' => $start,
|
||||
'next' => $nextMonth,
|
||||
'previous' => $previousMonth,
|
||||
'begin' => $start,
|
||||
'end' => $end,
|
||||
'export_route' => 'report_user_month_export',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ use Exception;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Html;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
@@ -106,6 +106,8 @@ final class UserWeekController extends AbstractUserReportController
|
||||
'current' => $start,
|
||||
'next' => $next,
|
||||
'previous' => $previous,
|
||||
'begin' => $start,
|
||||
'end' => $end,
|
||||
'export_route' => 'report_user_week_export',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ use Exception;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Html;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
@@ -122,6 +122,8 @@ final class UserYearController extends AbstractUserReportController
|
||||
'current' => $start,
|
||||
'next' => $next,
|
||||
'previous' => $previous,
|
||||
'begin' => $start,
|
||||
'end' => $end,
|
||||
'export_route' => 'report_user_year_export',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace App\Controller;
|
||||
use App\Reporting\ReportingService;
|
||||
use App\Utils\PageSetup;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace App\Controller\Security;
|
||||
|
||||
use App\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
#[Route(path: '/auth/link')]
|
||||
/**
|
||||
|
||||
@@ -24,7 +24,7 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Mime\Address;
|
||||
use Symfony\Component\Mime\Email;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace App\Controller\Security;
|
||||
use App\Configuration\SamlConfigurationInterface;
|
||||
use App\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||
use Symfony\Component\Mime\Address;
|
||||
use Symfony\Component\Mime\Email;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
@@ -45,7 +45,7 @@ use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use Symfony\Component\Validator\Constraints\GreaterThanOrEqual;
|
||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||
|
||||
@@ -21,7 +21,7 @@ use App\Utils\PageSetup;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[Route(path: '/admin/tags')]
|
||||
|
||||
@@ -22,7 +22,7 @@ use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[Route(path: '/admin/teams')]
|
||||
|
||||
@@ -16,7 +16,7 @@ use App\Form\TimesheetEditForm;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
/**
|
||||
|
||||
@@ -24,7 +24,7 @@ use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
/**
|
||||
|
||||
@@ -30,7 +30,7 @@ use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
namespace App\Controller;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[Route(path: '/widgets')]
|
||||
|
||||
@@ -20,7 +20,7 @@ use App\User\UserService;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[Route(path: '/wizard')]
|
||||
@@ -28,7 +28,6 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
final class WizardController extends AbstractController
|
||||
{
|
||||
#[Route(path: '/{wizard}', name: 'wizard', methods: ['GET', 'POST'])]
|
||||
#[IsGranted('view_own_timesheet')]
|
||||
public function wizard(Request $request, UserService $userService, string $wizard): Response
|
||||
{
|
||||
$user = $this->getUser();
|
||||
|
||||
77
src/Doctrine/UTCDateTimeImmutableType.php
Normal file
77
src/Doctrine/UTCDateTimeImmutableType.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?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\Doctrine;
|
||||
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use Doctrine\DBAL\Types\ConversionException;
|
||||
use Doctrine\DBAL\Types\DateTimeImmutableType;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
|
||||
final class UTCDateTimeImmutableType extends DateTimeImmutableType
|
||||
{
|
||||
/**
|
||||
* @var \DateTimeZone|null
|
||||
*/
|
||||
private static ?\DateTimeZone $utc = null;
|
||||
|
||||
/**
|
||||
* @param T $value
|
||||
* @return (T is null ? null : string)
|
||||
* @template T<\DateTimeImmutable>
|
||||
* @throws ConversionException
|
||||
*/
|
||||
public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string
|
||||
{
|
||||
if ($value instanceof \DateTimeImmutable) {
|
||||
$value = clone $value;
|
||||
$value = $value->setTimezone(self::getUtc());
|
||||
}
|
||||
|
||||
return parent::convertToDatabaseValue($value, $platform);
|
||||
}
|
||||
|
||||
public static function getUtc(): \DateTimeZone
|
||||
{
|
||||
if (self::$utc === null) {
|
||||
self::$utc = new \DateTimeZone('UTC');
|
||||
}
|
||||
|
||||
return self::$utc;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @throws ConversionException
|
||||
*/
|
||||
public function convertToPHPValue($value, AbstractPlatform $platform): ?\DateTimeImmutable
|
||||
{
|
||||
if (null === $value || $value instanceof \DateTimeImmutable) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (\is_string($value)) {
|
||||
$converted = \DateTimeImmutable::createFromFormat(
|
||||
$platform->getDateTimeFormatString(),
|
||||
$value,
|
||||
self::getUtc()
|
||||
);
|
||||
|
||||
if ($converted !== false) {
|
||||
return $converted;
|
||||
}
|
||||
}
|
||||
|
||||
throw ConversionException::conversionFailedFormat(
|
||||
$value,
|
||||
Types::DATETIME_IMMUTABLE,
|
||||
$platform->getDateTimeFormatString()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -49,8 +49,6 @@ final class UTCDateTimeType extends DateTimeType
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @param AbstractPlatform $platform
|
||||
* @return null|\DateTime
|
||||
* @throws ConversionException
|
||||
*/
|
||||
public function convertToPHPValue($value, AbstractPlatform $platform): ?\DateTime
|
||||
@@ -59,20 +57,22 @@ final class UTCDateTimeType extends DateTimeType
|
||||
return $value;
|
||||
}
|
||||
|
||||
$converted = \DateTime::createFromFormat(
|
||||
$platform->getDateTimeFormatString(),
|
||||
$value,
|
||||
self::getUtc()
|
||||
);
|
||||
|
||||
if (!$converted) {
|
||||
throw ConversionException::conversionFailedFormat(
|
||||
if (\is_string($value)) {
|
||||
$converted = \DateTime::createFromFormat(
|
||||
$platform->getDateTimeFormatString(),
|
||||
$value,
|
||||
Types::DATETIME_MUTABLE,
|
||||
$platform->getDateTimeFormatString()
|
||||
self::getUtc()
|
||||
);
|
||||
|
||||
if ($converted !== false) {
|
||||
return $converted;
|
||||
}
|
||||
}
|
||||
|
||||
return $converted;
|
||||
throw ConversionException::conversionFailedFormat(
|
||||
$value,
|
||||
Types::DATETIME_MUTABLE,
|
||||
$platform->getDateTimeFormatString()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt
|
||||
#[Assert\NotNull]
|
||||
private ?string $category = self::WORK;
|
||||
#[ORM\Column(name: 'modified_at', type: 'datetime_immutable', nullable: true)]
|
||||
private \DateTimeImmutable $modifiedAt; // @phpstan-ignore-line - create migration and update all null values and then make it not null
|
||||
private \DateTimeImmutable $modifiedAt;
|
||||
/**
|
||||
* Tags
|
||||
*
|
||||
|
||||
@@ -16,15 +16,11 @@ interface MetaDisplayEventInterface
|
||||
{
|
||||
/**
|
||||
* If you want to filter where your meta-field will be displayed, use the query settings.
|
||||
*
|
||||
* @return BaseQuery
|
||||
*/
|
||||
public function getQuery(): BaseQuery;
|
||||
|
||||
/**
|
||||
* If you want to filter where your meta-field will be displayed, check the current location.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLocation(): string;
|
||||
|
||||
@@ -35,9 +31,6 @@ interface MetaDisplayEventInterface
|
||||
|
||||
/**
|
||||
* Adds a field that should be displayed.
|
||||
*
|
||||
* @param MetaTableTypeInterface $meta
|
||||
* @return void
|
||||
*/
|
||||
public function addField(MetaTableTypeInterface $meta) /* : void */;
|
||||
public function addField(MetaTableTypeInterface $meta): void;
|
||||
}
|
||||
|
||||
41
src/EventSubscriber/LogoutSubscriber.php
Normal file
41
src/EventSubscriber/LogoutSubscriber.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?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\Security\SessionHandler;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Security\Http\Event\LogoutEvent;
|
||||
|
||||
final class LogoutSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SessionHandler $sessionHandler,
|
||||
private readonly LoggerInterface $logger
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
LogoutEvent::class => 'onLogout',
|
||||
];
|
||||
}
|
||||
|
||||
public function onLogout(LogoutEvent $event): void
|
||||
{
|
||||
try {
|
||||
$this->sessionHandler->garbageCollection();
|
||||
} catch (\Exception $exception) {
|
||||
$this->logger->error('Failed removing expired session: ' . $exception->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
32
src/Export/Spreadsheet/CellFormatter/StringFormatter.php
Normal file
32
src/Export/Spreadsheet/CellFormatter/StringFormatter.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?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\Export\Spreadsheet\CellFormatter;
|
||||
|
||||
use App\Utils\StringHelper;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\CellAddress;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
|
||||
final class StringFormatter implements CellFormatterInterface
|
||||
{
|
||||
public function setFormattedValue(Worksheet $sheet, int $column, int $row, $value): void
|
||||
{
|
||||
if (null === $value) {
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), '');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!\is_string($value)) {
|
||||
throw new \InvalidArgumentException('Unsupported value given, only string is supported');
|
||||
}
|
||||
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), StringHelper::sanitizeDDE($value));
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ use App\Export\Spreadsheet\CellFormatter\CellFormatterInterface;
|
||||
use App\Export\Spreadsheet\CellFormatter\DateFormatter;
|
||||
use App\Export\Spreadsheet\CellFormatter\DateTimeFormatter;
|
||||
use App\Export\Spreadsheet\CellFormatter\DurationFormatter;
|
||||
use App\Export\Spreadsheet\CellFormatter\StringFormatter;
|
||||
use App\Export\Spreadsheet\CellFormatter\TimeFormatter;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\CellAddress;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
@@ -30,7 +31,7 @@ class SpreadsheetExporter
|
||||
*/
|
||||
private array $formatter = [];
|
||||
|
||||
public function __construct(private TranslatorInterface $translator)
|
||||
public function __construct(private readonly TranslatorInterface $translator)
|
||||
{
|
||||
$this->registerCellFormatter('datetime', new DateTimeFormatter());
|
||||
$this->registerCellFormatter('date', new DateFormatter());
|
||||
@@ -38,6 +39,7 @@ class SpreadsheetExporter
|
||||
$this->registerCellFormatter('duration', new DurationFormatter());
|
||||
$this->registerCellFormatter('boolean', new BooleanFormatter());
|
||||
$this->registerCellFormatter('array', new ArrayFormatter());
|
||||
$this->registerCellFormatter('string', new StringFormatter());
|
||||
}
|
||||
|
||||
public function registerCellFormatter(string $type, CellFormatterInterface $formatter): void
|
||||
|
||||
@@ -15,6 +15,7 @@ use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Validator\Constraints\GreaterThan;
|
||||
|
||||
final class AccessTokenForm extends AbstractType
|
||||
{
|
||||
@@ -27,6 +28,11 @@ final class AccessTokenForm extends AbstractType
|
||||
->add('expiresAt', DatePickerType::class, [
|
||||
'label' => 'expires',
|
||||
'required' => false,
|
||||
'force_time' => 'end',
|
||||
'min_day' => $options['min_date'],
|
||||
'constraints' => [
|
||||
new GreaterThan($options['min_date'])
|
||||
],
|
||||
])
|
||||
;
|
||||
}
|
||||
@@ -41,6 +47,8 @@ final class AccessTokenForm extends AbstractType
|
||||
'attr' => [
|
||||
'data-form-event' => 'kimai.accessToken'
|
||||
],
|
||||
'min_date' => new \DateTimeImmutable('today 00:00:00'),
|
||||
]);
|
||||
$resolver->setAllowedTypes('min_date', [\DateTimeInterface::class]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,12 +61,12 @@ class DatePickerType extends AbstractType
|
||||
{
|
||||
if ($options['min_day'] !== null) {
|
||||
$view->vars['attr'] = array_merge($view->vars['attr'], [
|
||||
'min' => $options['min_day'],
|
||||
'min' => (\is_string($options['min_day']) ? $options['min_day'] : $options['min_day']->format('Y-m-d')), // @phpstan-ignore-line
|
||||
]);
|
||||
}
|
||||
if ($options['max_day'] !== null) {
|
||||
$view->vars['attr'] = array_merge($view->vars['attr'], [
|
||||
'max' => $options['max_day'],
|
||||
'max' => (\is_string($options['max_day']) ? $options['max_day'] : $options['max_day']->format('Y-m-d')), // @phpstan-ignore-line
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -84,10 +84,13 @@ class DatePickerType extends AbstractType
|
||||
'format' => $formFormat,
|
||||
'model_timezone' => date_default_timezone_get(),
|
||||
'view_timezone' => date_default_timezone_get(),
|
||||
'force_time' => null, // one of: string (start, end) or a string to as argument for DateTime->modify() or null
|
||||
'force_time' => null, // one of: null, string (start, end) or a string working as argument for DateTime->modify() or null
|
||||
'min_day' => null,
|
||||
'max_day' => null,
|
||||
]);
|
||||
|
||||
$resolver->addAllowedTypes('min_day', ['null', 'string', \DateTimeInterface::class]);
|
||||
$resolver->addAllowedTypes('max_day', ['null', 'string', \DateTimeInterface::class]);
|
||||
}
|
||||
|
||||
public function getParent(): string
|
||||
|
||||
@@ -49,6 +49,9 @@ final class DateRangeType extends AbstractType
|
||||
'max_day' => null,
|
||||
'locale' => \Locale::getDefault(),
|
||||
]);
|
||||
$resolver->addAllowedTypes('timezone', ['string']);
|
||||
$resolver->addAllowedTypes('min_day', ['null', 'string', \DateTimeInterface::class]);
|
||||
$resolver->addAllowedTypes('max_day', ['null', 'string', \DateTimeInterface::class]);
|
||||
|
||||
$resolver->setDefault('format', function (Options $options): string {
|
||||
$format = $this->localeService->getDateFormat($options['locale']);
|
||||
@@ -101,13 +104,13 @@ final class DateRangeType extends AbstractType
|
||||
|
||||
if ($options['min_day'] !== null) {
|
||||
$view->vars['attr'] = array_merge($view->vars['attr'], [
|
||||
'min' => $options['min_day'],
|
||||
'min' => (\is_string($options['min_day']) ? $options['min_day'] : $options['min_day']->format('Y-m-d')), // @phpstan-ignore-line
|
||||
]);
|
||||
}
|
||||
|
||||
if ($options['max_day'] !== null) {
|
||||
$view->vars['attr'] = array_merge($view->vars['attr'], [
|
||||
'max' => $options['max_day'],
|
||||
'max' => (\is_string($options['max_day']) ? $options['max_day'] : $options['max_day']->format('Y-m-d')), // @phpstan-ignore-line
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -160,6 +163,18 @@ final class DateRangeType extends AbstractType
|
||||
$formatDate
|
||||
);
|
||||
|
||||
$fallbackFormat = 'yyyy-MM-dd';
|
||||
$fallbackPattern = (new FormFormatConverter())->convertToPattern($fallbackFormat . $separator . $fallbackFormat, false);
|
||||
|
||||
$fallbackTransformer = new DateTimeToLocalizedStringTransformer(
|
||||
$timezone,
|
||||
$timezone,
|
||||
IntlDateFormatter::MEDIUM,
|
||||
IntlDateFormatter::MEDIUM,
|
||||
IntlDateFormatter::GREGORIAN,
|
||||
$fallbackFormat
|
||||
);
|
||||
|
||||
$range = new DateRange();
|
||||
|
||||
if (empty($dates) && $allowEmpty) {
|
||||
@@ -170,7 +185,7 @@ final class DateRangeType extends AbstractType
|
||||
throw new TransformationFailedException('Date range missing');
|
||||
}
|
||||
|
||||
if (preg_match($pattern, $dates) !== 1) {
|
||||
if (preg_match($pattern, $dates) !== 1 && preg_match($fallbackPattern, $dates) !== 1) {
|
||||
throw new TransformationFailedException('Invalid date range given');
|
||||
}
|
||||
$values = explode($separator, $dates);
|
||||
@@ -179,13 +194,23 @@ final class DateRangeType extends AbstractType
|
||||
throw new TransformationFailedException('Invalid date range given');
|
||||
}
|
||||
|
||||
$begin = $transformer->reverseTransform($values[0]);
|
||||
try {
|
||||
$begin = $transformer->reverseTransform($values[0]);
|
||||
} catch (TransformationFailedException $e) {
|
||||
// we always allow english format to simplify cross-linking
|
||||
$begin = $fallbackTransformer->reverseTransform($values[0]);
|
||||
}
|
||||
if ($begin === null) {
|
||||
throw new TransformationFailedException('Invalid begin date given');
|
||||
}
|
||||
$range->setBegin($begin);
|
||||
|
||||
$end = $transformer->reverseTransform($values[1]);
|
||||
try {
|
||||
$end = $transformer->reverseTransform($values[1]);
|
||||
} catch (TransformationFailedException $e) {
|
||||
// we always allow english format to simplify cross-linking
|
||||
$end = $fallbackTransformer->reverseTransform($values[1]);
|
||||
}
|
||||
if ($end === null) {
|
||||
throw new TransformationFailedException('Invalid end date given');
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ class Kernel extends BaseKernel
|
||||
return $plugins;
|
||||
}
|
||||
|
||||
protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader)
|
||||
protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader): void
|
||||
{
|
||||
$container->registerExtension(new AppExtension());
|
||||
|
||||
@@ -130,11 +130,12 @@ class Kernel extends BaseKernel
|
||||
$confDir = $this->getProjectDir() . '/config';
|
||||
|
||||
// using this one instead of $loader->load($confDir . '/packages/*' . self::CONFIG_EXTS, 'glob');
|
||||
// to get rid of the local.yaml from the list, we load it afterward explicit
|
||||
// to get rid of the local.yaml from the list: we load it afterward explicit
|
||||
$finder = (new Finder())
|
||||
->files()
|
||||
->in([$confDir . '/packages/'])
|
||||
->name('*' . self::CONFIG_EXTS)
|
||||
->notName('local.yaml')
|
||||
->sortByName()
|
||||
->followLinks()
|
||||
;
|
||||
@@ -145,7 +146,7 @@ class Kernel extends BaseKernel
|
||||
}
|
||||
|
||||
if (is_file($confDir . '/packages/local.yaml')) {
|
||||
$loader->load($confDir . '/packages/local.yaml', 'glob');
|
||||
$loader->load($confDir . '/packages/local.yaml');
|
||||
}
|
||||
$loader->load($confDir . '/services' . self::CONFIG_EXTS, 'glob');
|
||||
$loader->load($confDir . '/services_' . $this->environment . self::CONFIG_EXTS, 'glob');
|
||||
|
||||
@@ -10,11 +10,18 @@
|
||||
namespace App\Security;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\RateLimiter\RateLimiterFactory;
|
||||
|
||||
final class SessionHandler extends PdoSessionHandler
|
||||
{
|
||||
public function __construct(Connection $connection)
|
||||
public function __construct(
|
||||
Connection $connection,
|
||||
private readonly RateLimiterFactory $sessionPredictionLimiter,
|
||||
private readonly RequestStack $requestStack
|
||||
)
|
||||
{
|
||||
parent::__construct($connection->getNativeConnection(), [
|
||||
'db_table' => 'kimai2_sessions',
|
||||
@@ -25,4 +32,29 @@ final class SessionHandler extends PdoSessionHandler
|
||||
'lock_mode' => PdoSessionHandler::LOCK_ADVISORY,
|
||||
]);
|
||||
}
|
||||
|
||||
public function garbageCollection(): void
|
||||
{
|
||||
$connection = $this->getConnection();
|
||||
$sql = 'DELETE FROM kimai2_sessions WHERE lifetime < :time';
|
||||
$stmt = $connection->prepare($sql);
|
||||
$stmt->bindValue(':time', time(), \PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
}
|
||||
|
||||
public function validateId(#[\SensitiveParameter] string $sessionId): bool
|
||||
{
|
||||
$result = parent::validateId($sessionId);
|
||||
|
||||
if ($result === false) {
|
||||
$limiter = $this->sessionPredictionLimiter->create($this->requestStack->getMainRequest()?->getClientIp());
|
||||
$limit = $limiter->consume();
|
||||
|
||||
if (false === $limit->isAccepted()) {
|
||||
throw new BadRequestHttpException('Too many requests with invalid Session ID. Prediction attack?');
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ use App\Twig\Runtime\QrCodeExtension;
|
||||
use App\Twig\Runtime\ThemeExtension;
|
||||
use App\Twig\Runtime\TimesheetExtension;
|
||||
use App\Twig\Runtime\WidgetExtension;
|
||||
use App\Utils\StringHelper;
|
||||
use KevinPapst\TablerBundle\Twig\RuntimeExtension;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFilter;
|
||||
@@ -51,6 +52,7 @@ final class RuntimeExtensions extends AbstractExtension
|
||||
new TwigFilter('comment1line', [MarkdownExtension::class, 'commentOneLiner'], ['pre_escape' => 'html', 'is_safe' => ['html']]),
|
||||
new TwigFilter('colorize', [ThemeExtension::class, 'colorize']),
|
||||
new TwigFilter('icon', [RuntimeExtension::class, 'icon']),
|
||||
new TwigFilter('sanitize_dde', StringHelper::sanitizeDDE(...)),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,12 +16,13 @@ final class DateTimeFormat extends Constraint
|
||||
public const INVALID_FORMAT = 'kimai-datetime-00';
|
||||
|
||||
protected const ERROR_NAMES = [
|
||||
self::INVALID_FORMAT => 'The given value is not a valid datetime format.',
|
||||
self::INVALID_FORMAT => 'This value is not a valid datetime.',
|
||||
];
|
||||
|
||||
public ?string $separator = null;
|
||||
public ?string $message = 'This datetime format is invalid.';
|
||||
public ?string $message = 'This value is not a valid datetime.';
|
||||
|
||||
// Before: The given value is not a valid datetime format.
|
||||
public function getTargets(): string
|
||||
{
|
||||
return self::PROPERTY_CONSTRAINT;
|
||||
|
||||
@@ -62,7 +62,7 @@ final class DateTimeFormatValidator extends ConstraintValidator
|
||||
}
|
||||
|
||||
if (false === $valid) {
|
||||
$this->context->buildViolation('The given value is not a valid datetime format.')
|
||||
$this->context->buildViolation(DateTimeFormat::getErrorName(DateTimeFormat::INVALID_FORMAT))
|
||||
->setTranslationDomain('validators')
|
||||
->setCode(DateTimeFormat::INVALID_FORMAT)
|
||||
->addViolation();
|
||||
|
||||
Reference in New Issue
Block a user