Release 2.16 (#4780)

This commit is contained in:
Kevin Papst
2024-05-01 14:24:24 +02:00
committed by GitHub
parent 8f8d228fb3
commit 99c296a751
150 changed files with 2498 additions and 1905 deletions

View File

@@ -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;
}

View File

@@ -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');
}

View File

@@ -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);

View File

@@ -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
*/

View File

@@ -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

View File

@@ -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;
/**

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
/**

View File

@@ -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

View File

@@ -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;

View File

@@ -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;
/**

View File

@@ -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;

View File

@@ -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;
/**

View File

@@ -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')]

View File

@@ -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')]

View File

@@ -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;
/**

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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', [

View File

@@ -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;

View File

@@ -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;
/**

View File

@@ -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')]

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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')]

View File

@@ -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')]

View File

@@ -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')]

View File

@@ -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',
];
}

View File

@@ -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',
];
}

View File

@@ -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',
];
}

View File

@@ -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;
/**

View File

@@ -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')]
/**

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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')]

View File

@@ -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')]

View File

@@ -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;
/**

View File

@@ -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;
/**

View File

@@ -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;
/**

View File

@@ -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')]

View File

@@ -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();

View 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()
);
}
}

View File

@@ -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()
);
}
}

View File

@@ -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
*

View File

@@ -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;
}

View 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());
}
}
}

View 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));
}
}

View File

@@ -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

View File

@@ -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]);
}
}

View File

@@ -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

View File

@@ -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');
}

View File

@@ -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');

View File

@@ -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;
}
}

View File

@@ -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(...)),
];
}
}

View File

@@ -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;

View File

@@ -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();