make sure that timezone is properly validated (#2663)

This commit is contained in:
Kevin Papst
2021-07-13 23:23:18 +02:00
committed by GitHub
parent 43a31efeac
commit 7d052eba00
9 changed files with 68 additions and 62 deletions

View File

@@ -9,18 +9,9 @@
namespace App\Validator\Constraints;
use App\Validator\TimesheetBudgetUsedValidator;
final class TimesheetBudgetUsedConstraint extends TimesheetConstraint
final class TimesheetBudgetUsed extends TimesheetConstraint
{
public const BUDGET_SPENT = 'kimai-timesheet-budget-used-01';
// same messages, so we can re-use the validation translation!
public $messageRate = 'The budget is completely used.';
public $messageTime = 'The budget is completely used.';
public function validatedBy()
{
return TimesheetBudgetUsedValidator::class;
}
}

View File

@@ -0,0 +1,255 @@
<?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\Activity\ActivityStatisticService;
use App\Configuration\SystemConfiguration;
use App\Customer\CustomerStatisticService;
use App\Entity\Timesheet;
use App\Project\ProjectStatisticService;
use App\Repository\TimesheetRepository;
use App\Timesheet\RateServiceInterface;
use App\Utils\Duration;
use App\Utils\LocaleHelper;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
final class TimesheetBudgetUsedValidator extends ConstraintValidator
{
private $customerStatisticService;
private $projectStatisticService;
private $activityStatisticService;
private $timesheetRepository;
private $rateService;
private $configuration;
public function __construct(SystemConfiguration $configuration, CustomerStatisticService $customerStatisticService, ProjectStatisticService $projectStatisticService, ActivityStatisticService $activityStatisticService, TimesheetRepository $timesheetRepository, RateServiceInterface $rateService)
{
$this->configuration = $configuration;
$this->customerStatisticService = $customerStatisticService;
$this->projectStatisticService = $projectStatisticService;
$this->activityStatisticService = $activityStatisticService;
$this->timesheetRepository = $timesheetRepository;
$this->rateService = $rateService;
}
/**
* @param Timesheet $timesheet
* @param Constraint $constraint
*/
public function validate($timesheet, Constraint $constraint)
{
if (!($constraint instanceof TimesheetBudgetUsed)) {
throw new UnexpectedTypeException($constraint, TimesheetBudgetUsed::class);
}
if (!\is_object($timesheet) || !($timesheet instanceof Timesheet)) {
throw new UnexpectedTypeException($timesheet, Timesheet::class);
}
if ($this->configuration->isTimesheetAllowOverbookingBudget()) {
return;
}
if ($this->context->getViolations()->count() > 0) {
return;
}
// we can only work with stopped entries
if (null === $timesheet->getEnd() || null === $timesheet->getUser() || null === $timesheet->getProject()) {
return;
}
$duration = $timesheet->getDuration();
if (null === $duration || 0 === $duration) {
$duration = $timesheet->getEnd()->getTimestamp() - $timesheet->getBegin()->getTimestamp();
}
$timeRate = $this->rateService->calculate($timesheet);
$rate = $timeRate->getRate();
$activityDuration = $duration;
$activityRate = $rate;
$projectDuration = $duration;
$projectRate = $rate;
$customerDuration = $duration;
$customerRate = $rate;
if ($timesheet->getId() !== null) {
$rawData = $this->timesheetRepository->getRawData($timesheet);
// if an existing entry was updated, but duration and rate were not changed: do not validate
// this could for example happen if overbooking config was recently activated
if ($duration === $rawData['duration'] && $rate === $rawData['rate']) {
return;
}
// the duration of an existing entry could be increased or lowered
$activityId = (int) $rawData['activity'];
$projectId = (int) $rawData['project'];
$customerId = (int) $rawData['customer'];
if (null !== $timesheet->getActivity() && $activityId === $timesheet->getActivity()->getId()) {
$activityDuration -= $rawData['duration'];
$activityRate -= $rawData['rate'];
}
if ($projectId === $timesheet->getProject()->getId()) {
$projectDuration -= $rawData['duration'];
$projectRate -= $rawData['rate'];
}
if ($customerId === $timesheet->getProject()->getCustomer()->getId()) {
$customerDuration -= $rawData['duration'];
$customerRate -= $rawData['rate'];
}
}
if (null !== $timesheet->getActivity() && $this->checkActivity($constraint, $timesheet, $activityDuration, $activityRate)) {
return;
}
if ($this->checkProject($constraint, $timesheet, $projectDuration, $projectRate)) {
return;
}
if ($this->checkCustomer($constraint, $timesheet, $customerDuration, $customerRate)) {
return;
}
}
private function checkActivity(TimesheetBudgetUsed $constraint, Timesheet $timesheet, int $duration, float $rate): bool
{
$activity = $timesheet->getActivity();
if (!$activity->hasBudget() && !$activity->hasTimeBudget()) {
return false;
}
$stat = $this->activityStatisticService->getActivityStatistics($activity);
$fullRate = ($stat->getRecordRate() + $rate);
if ($activity->hasBudget() && $fullRate > $activity->getBudget()) {
$this->addBudgetViolation($constraint, $timesheet, 'activity', $activity->getBudget(), $stat->getRecordRate());
return true;
}
$fullDuration = ($stat->getRecordDuration() + $duration);
if ($activity->hasTimeBudget() && $fullDuration > $activity->getTimeBudget()) {
$this->addTimeBudgetViolation($constraint, 'activity', $activity->getTimeBudget(), $stat->getRecordDuration());
return true;
}
return false;
}
private function checkProject(TimesheetBudgetUsed $constraint, Timesheet $timesheet, int $duration, float $rate): bool
{
$project = $timesheet->getProject();
if (!$project->hasBudget() && !$project->hasTimeBudget()) {
return false;
}
$stat = $this->projectStatisticService->getProjectStatistics($project);
$fullRate = ($stat->getRecordRate() + $rate);
if ($project->hasBudget() && $fullRate > $project->getBudget()) {
$this->addBudgetViolation($constraint, $timesheet, 'project', $project->getBudget(), $stat->getRecordRate());
return true;
}
$fullDuration = ($stat->getRecordDuration() + $duration);
if ($project->hasTimeBudget() && $fullDuration > $project->getTimeBudget()) {
$this->addTimeBudgetViolation($constraint, 'project', $project->getTimeBudget(), $stat->getRecordDuration());
return true;
}
return false;
}
private function checkCustomer(TimesheetBudgetUsed $constraint, Timesheet $timesheet, int $duration, float $rate): bool
{
$customer = $timesheet->getProject()->getCustomer();
if (!$customer->hasBudget() && !$customer->hasTimeBudget()) {
return false;
}
$stat = $this->customerStatisticService->getCustomerStatistics($customer);
$fullRate = ($stat->getRecordRate() + $rate);
if ($customer->hasBudget() && $fullRate > $customer->getBudget()) {
$this->addBudgetViolation($constraint, $timesheet, 'customer', $customer->getBudget(), $stat->getRecordRate());
return true;
}
$fullDuration = ($stat->getRecordDuration() + $duration);
if ($customer->hasTimeBudget() && $fullDuration > $customer->getTimeBudget()) {
$this->addTimeBudgetViolation($constraint, 'customer', $customer->getTimeBudget(), $stat->getRecordDuration());
return true;
}
return false;
}
private function addBudgetViolation(TimesheetBudgetUsed $constraint, Timesheet $timesheet, string $field, float $budget, float $rate)
{
// using the locale of the assigned user is not the best solution, but allows to be independent from the request stack
$helper = new LocaleHelper($timesheet->getUser()->getLanguage());
$currency = $timesheet->getProject()->getCustomer()->getCurrency();
$free = $budget - $rate;
$free = $free > 0 ? $free : 0;
$this->context->buildViolation($constraint->messageRate)
->atPath($field)
->setTranslationDomain('validators')
->setParameters([
'%used%' => $helper->money($rate, $currency),
'%budget%' => $helper->money($budget, $currency),
'%free%' => $helper->money($free, $currency)
])
->addViolation()
;
}
private function addTimeBudgetViolation(TimesheetBudgetUsed $constraint, string $field, int $budget, int $duration)
{
$durationFormat = new Duration();
$free = $budget - $duration;
$free = $free > 0 ? $free : 0;
$this->context->buildViolation($constraint->messageTime)
->atPath($field)
->setTranslationDomain('validators')
->setParameters([
'%used%' => $durationFormat->format($duration),
'%budget%' => $durationFormat->format($budget),
'%free%' => $durationFormat->format($free)
])
->addViolation()
;
}
}

View File

@@ -11,19 +11,19 @@ namespace App\Validator\Constraints;
use App\Entity\Timesheet as TimesheetEntity;
use App\Timesheet\LockdownService;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
final class TimesheetLockdownValidator extends ConstraintValidator
{
private $auth;
private $lockdownService;
private $security;
public function __construct(AuthorizationCheckerInterface $auth, LockdownService $lockdownService)
public function __construct(Security $security, LockdownService $lockdownService)
{
$this->auth = $auth;
$this->security = $security;
$this->lockdownService = $lockdownService;
}
@@ -50,7 +50,7 @@ final class TimesheetLockdownValidator extends ConstraintValidator
}
// lockdown never takes effect for users with special permission
if ($this->auth->isGranted('lockdown_override_timesheet')) {
if (null !== $this->security->getUser() && $this->security->isGranted('lockdown_override_timesheet')) {
return;
}
@@ -67,7 +67,10 @@ final class TimesheetLockdownValidator extends ConstraintValidator
}
}
$allowEditInGracePeriod = $this->auth->isGranted('lockdown_grace_timesheet');
$allowEditInGracePeriod = false;
if (null !== $this->security->getUser() && $this->security->isGranted('lockdown_grace_timesheet')) {
$allowEditInGracePeriod = true;
}
if ($this->lockdownService->isEditable($timesheet, $now, $allowEditInGracePeriod)) {
return;

View File

@@ -11,26 +11,20 @@ namespace App\Validator\Constraints;
use App\Entity\Timesheet as TimesheetEntity;
use App\Timesheet\TrackingModeService;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
final class TimesheetRestartValidator extends ConstraintValidator
{
/**
* @var TrackingModeService
*/
private $trackingModeService;
/**
* @var AuthorizationCheckerInterface
*/
private $auth;
private $security;
public function __construct(TrackingModeService $service, AuthorizationCheckerInterface $auth)
public function __construct(Security $security, TrackingModeService $service)
{
$this->security = $security;
$this->trackingModeService = $service;
$this->auth = $auth;
}
/**
@@ -58,7 +52,7 @@ final class TimesheetRestartValidator extends ConstraintValidator
return;
}
if ($this->auth->isGranted('start', $timesheet)) {
if (null !== $this->security->getUser() && $this->security->isGranted('start', $timesheet)) {
return;
}
@@ -76,7 +70,5 @@ final class TimesheetRestartValidator extends ConstraintValidator
->setTranslationDomain('validators')
->setCode(TimesheetRestart::START_DISALLOWED)
->addViolation();
return;
}
}