Improve create and start permission handling (#613)

This commit is contained in:
Kevin Papst
2019-03-06 10:24:27 +01:00
committed by GitHub
parent bce85b04d6
commit e260dd84ad
31 changed files with 258 additions and 86 deletions

View File

@@ -211,17 +211,6 @@ class TimesheetController extends BaseApiController
return new Response('You are not allowed to start this timesheet record', Response::HTTP_BAD_REQUEST);
}
if ($form->has('duration')) {
$duration = $form->get('duration')->getData();
if ($duration > 0) {
/** @var Timesheet $record */
$record = $form->getData();
$end = clone $record->getBegin();
$end->modify('+ ' . $duration . 'seconds');
$record->setEnd($end);
}
}
if (null === $timesheet->getEnd()) {
$this->repository->stopActiveEntries(
$timesheet->getUser(),
@@ -289,17 +278,6 @@ class TimesheetController extends BaseApiController
return $this->viewHandler->handle($view);
}
if ($form->has('duration')) {
$duration = $form->get('duration')->getData();
if ($duration > 0) {
/** @var Timesheet $record */
$record = $form->getData();
$end = clone $record->getBegin();
$end->modify('+ ' . $duration . 'seconds');
$record->setEnd($end);
}
}
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($timesheet);
$entityManager->flush();

View File

@@ -97,18 +97,6 @@ trait TimesheetControllerTrait
$editForm->handleRequest($request);
if ($editForm->isSubmitted() && $editForm->isValid()) {
if ($editForm->has('duration')) {
/** @var Timesheet $record */
$record = $editForm->getData();
$duration = $editForm->get('duration')->getData();
$end = null;
if ($duration > 0) {
$end = clone $record->getBegin();
$end->modify('+ ' . $duration . 'seconds');
}
$record->setEnd($end);
}
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($entry);
$entityManager->flush();
@@ -174,17 +162,6 @@ trait TimesheetControllerTrait
$createForm->handleRequest($request);
if ($createForm->isSubmitted() && $createForm->isValid()) {
if ($createForm->has('duration')) {
$duration = $createForm->get('duration')->getData();
if ($duration > 0) {
/** @var Timesheet $record */
$record = $createForm->getData();
$end = clone $record->getBegin();
$end->modify('+ ' . $duration . 'seconds');
$record->setEnd($end);
}
}
$entityManager = $this->getDoctrine()->getManager();
try {

View File

@@ -90,8 +90,10 @@ class Configuration implements ConfigurationInterface
$class = 'App\\Timesheet\\Rounding\\' . ucfirst($value) . 'Rounding';
if (class_exists($class)) {
$rounding = new $class();
return !($rounding instanceof RoundingInterface);
}
return false;
})
->thenInvalid('Chosen rounding mode is invalid')

View File

@@ -117,7 +117,38 @@ class TimesheetEditForm extends AbstractType
if ($options['duration_only']) {
$builder->add('duration', DurationType::class, [
'required' => false,
'docu_chapter' => 'timesheet.html#duration-format',
'attr' => [
'placeholder' => '00:00',
]
]);
$builder->addEventListener(
FormEvents::POST_SET_DATA,
function (FormEvent $event) {
/** @var Timesheet $data */
$data = $event->getData();
if (null === $data->getEnd()) {
$event->getForm()->get('duration')->setData(null);
}
}
);
// make sure that duration is mapped back to end field
$builder->addEventListener(
FormEvents::SUBMIT,
function (FormEvent $event) {
/** @var Timesheet $data */
$data = $event->getData();
$duration = $data->getDuration();
$end = null;
if (null !== $duration) {
$end = clone $data->getBegin();
$end->modify('+ ' . $duration . 'seconds');
}
$data->setEnd($end);
}
);
} else {
$builder->add('end', DateTimePickerType::class, [
'label' => 'label.end',
@@ -270,7 +301,7 @@ class TimesheetEditForm extends AbstractType
'include_user' => false,
'include_exported' => false,
'include_rate' => true,
'docu_chapter' => 'timesheet',
'docu_chapter' => 'timesheet.html',
'method' => 'POST',
]);
}

View File

@@ -73,12 +73,18 @@ class DurationType extends AbstractType
}
},
function ($formatToInt) use ($formatter, $pattern) {
if (null === $formatToInt) {
return null;
}
if (empty($formatToInt)) {
return 0;
}
if (!preg_match($pattern, $formatToInt)) {
throw new TransformationFailedException('Invalid duration format given');
}
try {
return $formatter->parseDurationString($formatToInt);
} catch (\Exception $e) {

View File

@@ -55,7 +55,7 @@ class InvoiceRendererType extends AbstractType
return $choiceValue;
},
'translation_domain' => 'invoice-renderer',
'docu_chapter' => 'invoices',
'docu_chapter' => 'invoices.html',
]);
}

View File

@@ -0,0 +1,55 @@
<?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\Security;
use App\Entity\User;
use App\Repository\UserRepository;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
class CurrentUser
{
/**
* @var TokenStorageInterface
*/
protected $storage;
/**
* @var UserRepository
*/
protected $repository;
/**
* @param TokenStorageInterface $storage
* @param UserRepository $repository
*/
public function __construct(TokenStorageInterface $storage, UserRepository $repository)
{
$this->storage = $storage;
$this->repository = $repository;
}
/**
* @return User|null
*/
public function getUser()
{
if (null === $this->storage->getToken()) {
return null;
}
/** @var User $user */
$user = $this->storage->getToken()->getUser();
if (!($user instanceof User)) {
return null;
}
return $this->repository->getById($user->getId());
}
}

View File

@@ -33,5 +33,4 @@ interface RoundingInterface
* @param $minutes
*/
public function roundDuration(Timesheet $record, $minutes);
}

View File

@@ -10,7 +10,7 @@
namespace App\Timesheet;
use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use App\Security\CurrentUser;
class UserDateTimeFactory
{
@@ -20,18 +20,13 @@ class UserDateTimeFactory
protected $timezone;
/**
* @param TokenStorageInterface $tokenStorage
* @param CurrentUser $user
*/
public function __construct(TokenStorageInterface $tokenStorage)
public function __construct(CurrentUser $user)
{
if (null === $tokenStorage->getToken()) {
return;
}
/* @var $user User */
$user = $tokenStorage->getToken()->getUser();
$timezone = date_default_timezone_get();
$user = $user->getUser();
if ($user instanceof User && null !== $user->getPreferenceValue('timezone')) {
$timezone = $user->getPreferenceValue('timezone');
}

View File

@@ -9,6 +9,7 @@
namespace App\Twig;
use App\Constants;
use App\Entity\Timesheet;
use App\Utils\Duration;
use App\Utils\LocaleSettings;
@@ -121,6 +122,7 @@ class Extensions extends \Twig_Extension
new TwigFilter('currency', [$this, 'currency']),
new TwigFilter('country', [$this, 'country']),
new TwigFilter('icon', [$this, 'icon']),
new TwigFilter('docu_link', [$this, 'documentationLink']),
];
}
@@ -247,6 +249,15 @@ class Extensions extends \Twig_Extension
return self::$icons[$name] ?? $default;
}
/**
* @param string $url
* @return string
*/
public function documentationLink($url = '')
{
return Constants::HOMEPAGE . '/documentation/' . $url;
}
/**
* @param float $amount
* @param string $currency

View File

@@ -27,6 +27,7 @@ class Timesheet extends Constraint
public const DISABLED_ACTIVITY_ERROR = 'xd5hffg-dsfef3-426a-83d7-1f2d33hs5d87';
public const DISABLED_PROJECT_ERROR = 'xd5hffg-dsfef3-426a-83d7-1f2d33hs5d88';
public const DISABLED_CUSTOMER_ERROR = 'xd5hffg-dsfef3-426a-83d7-1f2d33hs5d89';
public const START_DISALLOWED = 'xd5hffg-dsfef3-426a-83d7-1f2d33hs5d90';
protected static $errorNames = [
self::MISSING_BEGIN_ERROR => 'You must submit a begin date.',
@@ -38,6 +39,7 @@ class Timesheet extends Constraint
self::DISABLED_ACTIVITY_ERROR => 'Cannot start a disabled activity.',
self::DISABLED_PROJECT_ERROR => 'Cannot start a disabled project.',
self::DISABLED_CUSTOMER_ERROR => 'Cannot start a disabled customer.',
self::START_DISALLOWED => 'You are not allowed to start this timesheet record.',
];
public $message = 'This timesheet has invalid settings.';

View File

@@ -11,6 +11,7 @@ namespace App\Validator\Constraints;
use App\Entity\Timesheet;
use App\Validator\Constraints\Timesheet as TimesheetConstraint;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
@@ -18,17 +19,29 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
class TimesheetValidator extends ConstraintValidator
{
/**
* @var AuthorizationCheckerInterface
*/
protected $auth;
/**
* @var array
*/
protected $rules = [];
/**
* @var bool
*/
protected $durationOnly = false;
/**
* @param AuthorizationCheckerInterface $auth
* @param array $ruleset
* @param bool $durationOnly
*/
public function __construct(array $ruleset)
public function __construct(AuthorizationCheckerInterface $auth, array $ruleset, bool $durationOnly)
{
$this->auth = $auth;
$this->rules = $ruleset;
$this->durationOnly = $durationOnly;
}
/**
@@ -61,6 +74,31 @@ class TimesheetValidator extends ConstraintValidator
$this->validateBeginAndEnd($value, $this->context);
$this->validateActivityAndProject($value, $this->context);
$this->validatePermissions($value, $this->context);
}
/**
* @param Timesheet $timesheet
* @param ExecutionContextInterface $context
*/
protected function validatePermissions(Timesheet $timesheet, ExecutionContextInterface $context)
{
// special case that would otherwise need to be validated in several controllers:
// an entry is edited and the end date is removed (or duration deleted) would restart the record,
// which might be disallowed for the current user
if ($context->getViolations()->count() == 0 && null === $timesheet->getEnd()) {
if (!$this->auth->isGranted('start', $timesheet)) {
$context->buildViolation('You are not allowed to start this timesheet record.')
->atPath($this->durationOnly ? 'duration' : 'end')
->setTranslationDomain('validators')
->setCode(TimesheetConstraint::START_DISALLOWED)
->addViolation();
return;
}
}
// TODO check active entries against hard_limit
}
/**