Release 2.36.0 (#5514)

This commit is contained in:
Kevin Papst
2025-06-02 16:07:48 +02:00
committed by GitHub
parent 594385f741
commit 2f2ebd6293
27 changed files with 466 additions and 257 deletions

View File

@@ -63,24 +63,6 @@ final class ValidationFailedExceptionErrorHandler implements SubscribingHandlerI
}
public function serializeValidationExceptionToJson(JsonSerializationVisitor $visitor, ValidationFailedException $exception, array $type, Context $context)
{
$errors = [];
/** @var ConstraintViolationInterface $error */
foreach (iterator_to_array($exception->getViolations()) as $error) {
$errors[$error->getPropertyPath()]['errors'][] = $this->getErrorMessage($error);
}
return [
'code' => '400',
'message' => $this->translator->trans($exception->getMessage(), [], 'validators'),
'errors' => [
'children' => $errors
],
];
}
private function getErrorMessage(ConstraintViolationInterface $error): string
{
$locale = \Locale::getDefault();
/** @var User $user */
@@ -90,6 +72,24 @@ final class ValidationFailedExceptionErrorHandler implements SubscribingHandlerI
$locale = $user->getLanguage();
}
$errors = [];
/** @var ConstraintViolationInterface $error */
foreach (iterator_to_array($exception->getViolations()) as $error) {
$errors[$error->getPropertyPath()]['errors'][] = $this->getErrorMessage($error, $locale);
}
return [
'code' => '400',
'message' => $this->translator->trans($exception->getMessage(), [], 'validators', $locale),
'errors' => [
'children' => $errors
],
];
}
private function getErrorMessage(ConstraintViolationInterface $error, string $locale): string
{
if (null !== $error->getPlural()) {
return $this->translator->trans($error->getMessageTemplate(), ['%count%' => $error->getPlural()] + $error->getParameters(), 'validators', $locale);
}

View File

@@ -315,7 +315,7 @@ final class TimesheetController extends BaseApiController
if ($form->isValid()) {
try {
$this->service->saveNewTimesheet($timesheet);
$this->service->saveTimesheet($timesheet);
$view = new View($timesheet, 200);
@@ -372,7 +372,7 @@ final class TimesheetController extends BaseApiController
return $this->viewHandler->handle($view);
}
$this->service->updateTimesheet($timesheet);
$this->service->saveTimesheet($timesheet);
$view = new View($timesheet, Response::HTTP_OK);
$view->getContext()->setGroups(self::GROUPS_ENTITY);
@@ -547,7 +547,7 @@ final class TimesheetController extends BaseApiController
$copyTimesheet = clone $timesheet;
$this->dispatcher->dispatch(new TimesheetDuplicatePreEvent($copyTimesheet, $timesheet));
$this->service->saveNewTimesheet($copyTimesheet);
$this->service->saveTimesheet($copyTimesheet);
$this->dispatcher->dispatch(new TimesheetDuplicatePostEvent($copyTimesheet, $timesheet));
$view = new View($copyTimesheet, 200);
@@ -571,7 +571,7 @@ final class TimesheetController extends BaseApiController
$timesheet->setExported(!$timesheet->isExported());
$this->service->updateTimesheet($timesheet);
$this->service->saveTimesheet($timesheet);
$view = new View($timesheet, 200);
$view->getContext()->setGroups(self::GROUPS_ENTITY);
@@ -601,7 +601,7 @@ final class TimesheetController extends BaseApiController
$meta->setValue($paramFetcher->get('value'));
$this->service->updateTimesheet($timesheet);
$this->service->saveTimesheet($timesheet);
$view = new View($timesheet, 200);
$view->getContext()->setGroups(self::GROUPS_ENTITY);

View File

@@ -97,7 +97,7 @@ class ActivityService
$errors = $this->validator->validate($activity, null, $groups);
if ($errors->count() > 0) {
throw new ValidationFailedException($errors, 'Validation Failed');
throw new ValidationFailedException($errors);
}
}

View File

@@ -17,11 +17,11 @@ final class Constants
/**
* The current release version
*/
public const VERSION = '2.35.1';
public const VERSION = '2.36.0';
/**
* The current release: major * 10000 + minor * 100 + patch
*/
public const VERSION_ID = 23501;
public const VERSION_ID = 23600;
/**
* The software name
*/

View File

@@ -152,7 +152,7 @@ abstract class TimesheetAbstractController extends AbstractController
if ($editForm->isSubmitted() && $editForm->isValid()) {
try {
$this->service->updateTimesheet($entry);
$this->service->saveTimesheet($entry);
$this->flashSuccess('action.update.success');
return $this->redirectToRoute($this->getTimesheetRoute());
@@ -184,7 +184,7 @@ abstract class TimesheetAbstractController extends AbstractController
if ($createForm->isSubmitted() && $createForm->isValid()) {
try {
$this->service->saveNewTimesheet($entry);
$this->service->saveTimesheet($entry);
$this->flashSuccess('action.update.success');
return $this->redirectToRoute($this->getTimesheetRoute());
@@ -216,7 +216,7 @@ abstract class TimesheetAbstractController extends AbstractController
if ($form->isSubmitted() && $form->isValid()) {
try {
$this->dispatcher->dispatch(new TimesheetDuplicatePreEvent($copyTimesheet, $timesheet));
$this->service->saveNewTimesheet($copyTimesheet);
$this->service->saveTimesheet($copyTimesheet);
$this->dispatcher->dispatch(new TimesheetDuplicatePostEvent($copyTimesheet, $timesheet));
$this->flashSuccess('action.update.success');

View File

@@ -126,7 +126,7 @@ final class TimesheetTeamController extends TimesheetAbstractController
}
foreach ($newTimesheets as $newTimesheet) {
$this->service->saveNewTimesheet($newTimesheet);
$this->service->saveTimesheet($newTimesheet);
}
$this->flashSuccess('action.update.success');

View File

@@ -101,7 +101,7 @@ final class CustomerService
$errors = $this->validator->validate($customer, null, $groups);
if ($errors->count() > 0) {
throw new ValidationFailedException($errors, 'Validation Failed');
throw new ValidationFailedException($errors);
}
}

View File

@@ -59,8 +59,7 @@ class TimesheetEditForm extends AbstractType
$timezone = $options['timezone'];
$isNew = true;
if (isset($options['data'])) {
/** @var Timesheet $entry */
if (isset($options['data']) && $options['data'] instanceof Timesheet) {
$entry = $options['data'];
$activity = $entry->getActivity();
@@ -338,7 +337,7 @@ class TimesheetEditForm extends AbstractType
function (FormEvent $event) {
/** @var Timesheet|null $timesheet */
$timesheet = $event->getData();
if (null === $timesheet || $timesheet->isRunning()) {
if (null === $timesheet || ($timesheet instanceof Timesheet && $timesheet->isRunning())) {
$event->getForm()->get('duration')->setData(null);
}
}

View File

@@ -218,9 +218,7 @@ final class InvoiceModel
}
/**
* Returns the user who is currently creating the invoice.
*
* @return User|null
* Returns the user currently creating the invoice.
*/
public function getUser(): ?User
{

View File

@@ -105,7 +105,7 @@ final class ProjectService
$errors = $this->validator->validate($project, null, $groups);
if ($errors->count() > 0) {
throw new ValidationFailedException($errors, 'Validation Failed');
throw new ValidationFailedException($errors);
}
}

View File

@@ -12,6 +12,7 @@ namespace App\Saml;
use App\Configuration\SamlConfigurationInterface;
use App\Saml\Security\SamlAuthenticationFailureHandler;
use App\Saml\Security\SamlAuthenticationSuccessHandler;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
@@ -40,7 +41,8 @@ class SamlAuthenticator extends AbstractAuthenticator
private readonly SamlAuthenticationFailureHandler $failureHandler,
private readonly SamlAuthFactory $samlAuthFactory,
private readonly SamlProvider $samlProvider,
private readonly SamlConfigurationInterface $configuration
private readonly SamlConfigurationInterface $configuration,
private readonly LoggerInterface $logger
) {
}
@@ -86,6 +88,7 @@ class SamlAuthenticator extends AbstractAuthenticator
// file_put_contents(__DIR__ . '/../../var/log/saml.xml', $oneLoginAuth->getLastResponseXML());
if (\count($oneLoginAuth->getErrors()) > 0) {
$this->logger->critical('SAML login failed: ' . $oneLoginAuth->getLastErrorReason());
throw new AuthenticationException($oneLoginAuth->getLastErrorReason());
}
@@ -102,7 +105,9 @@ class SamlAuthenticator extends AbstractAuthenticator
if (isset($this->options['username_attribute'])) {
if (!\array_key_exists($this->options['username_attribute'], $attributes)) {
throw new \Exception(\sprintf("Attribute '%s' not found in SAML data", $this->options['username_attribute']));
$errorMessage = \sprintf("Attribute '%s' not found in SAML data", $this->options['username_attribute']);
$this->logger->critical($errorMessage);
throw new \Exception($errorMessage);
}
$username = $attributes[$this->options['username_attribute']][0];

View File

@@ -45,12 +45,12 @@ final class TimesheetService
private array $doNotValidateCodes = [];
public function __construct(
private SystemConfiguration $configuration,
private TimesheetRepository $repository,
private TrackingModeService $trackingModeService,
private EventDispatcherInterface $dispatcher,
private AuthorizationCheckerInterface $auth,
private ValidatorInterface $validator
private readonly SystemConfiguration $configuration,
private readonly TimesheetRepository $repository,
private readonly TrackingModeService $trackingModeService,
private readonly EventDispatcherInterface $dispatcher,
private readonly AuthorizationCheckerInterface $auth,
private readonly ValidatorInterface $validator
) {
}
@@ -101,7 +101,7 @@ final class TimesheetService
public function restartTimesheet(Timesheet $timesheet, Timesheet $copyFrom): Timesheet
{
$this->dispatcher->dispatch(new TimesheetRestartPreEvent($timesheet, $copyFrom));
$this->saveNewTimesheet($timesheet);
$this->saveNewTimesheet($timesheet); // @phpstan-ignore method.deprecated
$this->dispatcher->dispatch(new TimesheetRestartPostEvent($timesheet, $copyFrom));
return $timesheet;
@@ -111,6 +111,7 @@ final class TimesheetService
* @throws ValidationFailedException for invalid timesheets or running timesheets that should be stopped
* @throws InvalidArgumentException for already persisted timesheets
* @throws AccessDeniedException if user is not allowed to start timesheet
* @deprecated since 2.36.0 - use saveTimesheet() instead
*/
public function saveNewTimesheet(Timesheet $timesheet): Timesheet
{
@@ -150,12 +151,19 @@ final class TimesheetService
return $timesheet;
}
public function saveTimesheet(Timesheet $timesheet): Timesheet
{
if ($timesheet->getId() === null) {
return $this->saveNewTimesheet($timesheet); // @phpstan-ignore method.deprecated
} else {
return $this->updateTimesheet($timesheet); // @phpstan-ignore method.deprecated
}
}
/**
* Does NOT validate the given timesheet!
* Does NOT validate the given timesheet.
*
* @param Timesheet $timesheet
* @return Timesheet
* @throws \Exception
* @deprecated since 2.36.0 - use saveTimesheet() instead
*/
public function updateTimesheet(Timesheet $timesheet): Timesheet
{
@@ -245,7 +253,7 @@ final class TimesheetService
continue;
}
throw new ValidationFailedException($errors, 'Validation Failed');
throw new ValidationFailedException($errors);
}
}
}

View File

@@ -112,7 +112,7 @@ class UserService
$errors = $this->validator->validate($user, null, $groups);
if ($errors->count() > 0) {
throw new ValidationFailedException($errors, 'Validation Failed');
throw new ValidationFailedException($errors);
}
}

View File

@@ -0,0 +1,26 @@
<?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\Validator\Constraints;
final class TimesheetNegativeDuration extends TimesheetConstraint
{
public const NEGATIVE_DURATION_ERROR = 'kimai-timesheet-negative-duration-01';
protected const ERROR_NAMES = [
self::NEGATIVE_DURATION_ERROR => 'Duration cannot be negative.',
];
public string $message = 'Duration cannot be negative.';
public function getTargets(): string
{
return self::CLASS_CONSTRAINT;
}
}

View File

@@ -0,0 +1,43 @@
<?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\Validator\Constraints;
use App\Entity\Timesheet as TimesheetEntity;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
final class TimesheetNegativeDurationValidator extends ConstraintValidator
{
public function validate(mixed $value, Constraint $constraint): void
{
if (!($constraint instanceof TimesheetNegativeDuration)) {
throw new UnexpectedTypeException($constraint, TimesheetNegativeDuration::class);
}
if (!\is_object($value) || !($value instanceof TimesheetEntity)) {
throw new UnexpectedTypeException($value, TimesheetEntity::class);
}
if ($value->isRunning()) {
return;
}
$duration = $value->getCalculatedDuration();
if ($duration !== null && $duration < 0) {
$this->context->buildViolation($constraint->message)
->atPath('duration')
->setTranslationDomain('validators')
->setCode(TimesheetNegativeDuration::NEGATIVE_DURATION_ERROR)
->addViolation();
}
}
}

View File

@@ -44,7 +44,7 @@ final class TimesheetZeroDurationValidator extends ConstraintValidator
$duration = $value->getCalculatedDuration();
}
if ($duration <= 0) {
if ($duration === 0) {
$this->context->buildViolation($constraint->message)
->atPath('duration')
->setTranslationDomain('validators')

View File

@@ -14,7 +14,7 @@ final class ValidationException extends \RuntimeException
public function __construct(string $message = null)
{
if ($message === null) {
$message = 'Validation failed';
$message = 'Validation Failed';
}
parent::__construct($message, 400);
}

View File

@@ -13,10 +13,10 @@ use Symfony\Component\Validator\ConstraintViolationListInterface;
final class ValidationFailedException extends \RuntimeException
{
public function __construct(private ConstraintViolationListInterface $violations, ?string $message = null)
public function __construct(private readonly ConstraintViolationListInterface $violations, ?string $message = null)
{
if ($message === null) {
$message = 'Validation failed';
$message = 'Validation Failed';
}
parent::__construct($message, 400);
}