Merge branch 'signature-app-master' into 'master'

Signature app master

Closes #307

See merge request Chill-Projet/chill-bundles!743
This commit is contained in:
2024-11-12 20:30:00 +00:00
426 changed files with 22236 additions and 2253 deletions

View File

@@ -96,7 +96,7 @@ class SearchController extends AbstractController
return $this->render('@ChillMain/Search/choose_list.html.twig');
}
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/search.{_format}', name: 'chill_main_search', requirements: ['_format' => 'html|json'], defaults: ['_format' => 'html'])]
#[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/search.{_format}', name: 'chill_main_search', requirements: ['_format' => 'html|json', '_locale' => '[a-z]{1,3}'], defaults: ['_format' => 'html'])]
public function searchAction(Request $request, mixed $_format)
{
$pattern = trim((string) $request->query->get('q', ''));

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Controller;
use Chill\MainBundle\CRUD\Controller\CRUDController;
use Chill\MainBundle\Pagination\PaginatorInterface;
use Symfony\Component\HttpFoundation\Request;
class UserGroupAdminController extends CRUDController
{
protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator)
{
$query->addSelect('JSON_EXTRACT(e.label, :lang) AS HIDDEN labeli18n')
->setParameter('lang', $request->getLocale());
$query->addOrderBy('labeli18n', 'ASC');
return $query;
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController;
class UserGroupApiController extends ApiController {}

View File

@@ -0,0 +1,177 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Controller;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\UserGroup;
use Chill\MainBundle\Form\Type\PickUserDynamicType;
use Chill\MainBundle\Pagination\PaginatorFactoryInterface;
use Chill\MainBundle\Repository\UserGroupRepositoryInterface;
use Chill\MainBundle\Routing\ChillUrlGeneratorInterface;
use Chill\MainBundle\Security\Authorization\UserGroupVoter;
use Chill\MainBundle\Templating\Entity\ChillEntityRenderManagerInterface;
use Doctrine\ORM\EntityManagerInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Translation\TranslatableMessage;
use Twig\Environment;
/**
* Controller to see and manage user groups.
*/
final readonly class UserGroupController
{
public function __construct(
private UserGroupRepositoryInterface $userGroupRepository,
private Security $security,
private PaginatorFactoryInterface $paginatorFactory,
private Environment $twig,
private FormFactoryInterface $formFactory,
private ChillUrlGeneratorInterface $chillUrlGenerator,
private EntityManagerInterface $objectManager,
private ChillEntityRenderManagerInterface $chillEntityRenderManager,
) {}
#[Route('/{_locale}/main/user-groups/my', name: 'chill_main_user_groups_my')]
public function myUserGroups(): Response
{
if (!$this->security->isGranted('ROLE_USER')) {
throw new AccessDeniedHttpException();
}
$user = $this->security->getUser();
if (!$user instanceof User) {
throw new AccessDeniedHttpException();
}
$nb = $this->userGroupRepository->countByUser($user);
$paginator = $this->paginatorFactory->create($nb);
$groups = $this->userGroupRepository->findByUser($user, true, $paginator->getItemsPerPage(), $paginator->getCurrentPageFirstItemNumber());
$forms = new \SplObjectStorage();
foreach ($groups as $group) {
$forms->attach($group, $this->createFormAppendUserForGroup($group)?->createView());
}
return new Response($this->twig->render('@ChillMain/UserGroup/my_user_groups.html.twig', [
'groups' => $groups,
'paginator' => $paginator,
'forms' => $forms,
]));
}
#[Route('/{_locale}/main/user-groups/{id}/append', name: 'chill_main_user_groups_append_users')]
public function appendUsersToGroup(UserGroup $userGroup, Request $request, Session $session): Response
{
if (!$this->security->isGranted(UserGroupVoter::APPEND_TO_GROUP, $userGroup)) {
throw new AccessDeniedHttpException();
}
$form = $this->createFormAppendUserForGroup($userGroup);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
foreach ($form['users']->getData() as $user) {
$userGroup->addUser($user);
$session->getFlashBag()->add(
'success',
new TranslatableMessage(
'user_group.user_added',
[
'user_group' => $this->chillEntityRenderManager->renderString($userGroup, []),
'user' => $this->chillEntityRenderManager->renderString($user, []),
]
)
);
}
$this->objectManager->flush();
return new RedirectResponse(
$this->chillUrlGenerator->returnPathOr('chill_main_user_groups_my')
);
}
if ($form->isSubmitted()) {
$errors = [];
foreach ($form->getErrors() as $error) {
$errors[] = $error->getMessage();
}
return new Response(implode(', ', $errors));
}
return new RedirectResponse(
$this->chillUrlGenerator->returnPathOr('chill_main_user_groups_my')
);
}
/**
* @ParamConverter("user", class=User::class, options={"id" = "userId"})
*/
#[Route('/{_locale}/main/user-group/{id}/user/{userId}/remove', name: 'chill_main_user_groups_remove_user')]
public function removeUserToGroup(UserGroup $userGroup, User $user, Session $session): Response
{
if (!$this->security->isGranted(UserGroupVoter::APPEND_TO_GROUP, $userGroup)) {
throw new AccessDeniedHttpException();
}
$userGroup->removeUser($user);
$this->objectManager->flush();
$session->getFlashBag()->add(
'success',
new TranslatableMessage(
'user_group.user_removed',
[
'user_group' => $this->chillEntityRenderManager->renderString($userGroup, []),
'user' => $this->chillEntityRenderManager->renderString($user, []),
]
)
);
return new RedirectResponse(
$this->chillUrlGenerator->returnPathOr('chill_main_user_groups_my')
);
}
private function createFormAppendUserForGroup(UserGroup $group): ?FormInterface
{
if (!$this->security->isGranted(UserGroupVoter::APPEND_TO_GROUP, $group)) {
return null;
}
$builder = $this->formFactory->createBuilder(FormType::class, ['users' => []], [
'action' => $this->chillUrlGenerator->generateWithReturnPath('chill_main_user_groups_append_users', ['id' => $group->getId()]),
]);
$builder->add('users', PickUserDynamicType::class, [
'submit_on_adding_new_entity' => true,
'label' => 'user_group.append_users',
'mapped' => false,
'multiple' => true,
]);
return $builder->getForm();
}
}

View File

@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Controller;
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZoneAvailable;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
use Chill\MainBundle\Security\Authorization\EntityWorkflowStepSignatureVoter;
use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Twig\Environment;
final readonly class WorkflowAddSignatureController
{
public function __construct(
private EntityWorkflowManager $entityWorkflowManager,
private PDFSignatureZoneAvailable $PDFSignatureZoneAvailable,
private NormalizerInterface $normalizer,
private Environment $twig,
private UrlGeneratorInterface $urlGenerator,
private Security $security,
) {}
#[Route(path: '/{_locale}/main/workflow/signature/{id}/sign', name: 'chill_main_workflow_signature_add', methods: 'GET')]
public function __invoke(EntityWorkflowStepSignature $signature, Request $request): Response
{
if (!$this->security->isGranted(EntityWorkflowStepSignatureVoter::SIGN, $signature)) {
throw new AccessDeniedHttpException('not authorized to sign this step');
}
$entityWorkflow = $signature->getStep()->getEntityWorkflow();
if (EntityWorkflowSignatureStateEnum::PENDING !== $signature->getState()) {
if ($request->query->has('returnPath')) {
return new RedirectResponse($request->query->get('returnPath'));
}
return new RedirectResponse(
$this->urlGenerator->generate('chill_main_workflow_show', ['id' => $entityWorkflow->getId()])
);
}
$storedObject = $this->entityWorkflowManager->getAssociatedStoredObject($entityWorkflow);
if (null === $storedObject) {
throw new NotFoundHttpException('No stored object found');
}
$zones = $this->PDFSignatureZoneAvailable->getAvailableSignatureZones($entityWorkflow);
$signatureClient = [];
$signatureClient['id'] = $signature->getId();
$signatureClient['storedObject'] = $this->normalizer->normalize($storedObject, 'json');
$signatureClient['zones'] = $zones;
return new Response(
$this->twig->render(
'@ChillMain/Workflow/signature_sign.html.twig',
['signature' => $signatureClient]
)
);
}
}

View File

@@ -13,23 +13,27 @@ namespace Chill\MainBundle\Controller;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowComment;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
use Chill\MainBundle\Form\EntityWorkflowCommentType;
use Chill\MainBundle\Form\WorkflowSignatureMetadataType;
use Chill\MainBundle\Form\WorkflowStepType;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowStepSignatureRepository;
use Chill\MainBundle\Security\Authorization\EntityWorkflowVoter;
use Chill\MainBundle\Security\ChillSecurity;
use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\NonUniqueResultException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Clock\ClockInterface;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Component\Workflow\Registry;
@@ -38,7 +42,19 @@ use Symfony\Contracts\Translation\TranslatorInterface;
class WorkflowController extends AbstractController
{
public function __construct(private readonly EntityWorkflowManager $entityWorkflowManager, private readonly EntityWorkflowRepository $entityWorkflowRepository, private readonly ValidatorInterface $validator, private readonly PaginatorFactory $paginatorFactory, private readonly Registry $registry, private readonly EntityManagerInterface $entityManager, private readonly TranslatorInterface $translator, private readonly ChillSecurity $security, private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry) {}
public function __construct(
private readonly EntityWorkflowManager $entityWorkflowManager,
private readonly EntityWorkflowRepository $entityWorkflowRepository,
private readonly ValidatorInterface $validator,
private readonly PaginatorFactory $paginatorFactory,
private readonly Registry $registry,
private readonly EntityManagerInterface $entityManager,
private readonly TranslatorInterface $translator,
private readonly ChillSecurity $security,
private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry,
private readonly ClockInterface $clock,
private readonly EntityWorkflowStepSignatureRepository $entityWorkflowStepSignatureRepository,
) {}
#[Route(path: '/{_locale}/main/workflow/create', name: 'chill_main_workflow_create')]
public function create(Request $request): Response
@@ -268,6 +284,9 @@ class WorkflowController extends AbstractController
);
}
/**
* @throws NonUniqueResultException
*/
#[Route(path: '/{_locale}/main/workflow/{id}/show', name: 'chill_main_workflow_show')]
public function show(EntityWorkflow $entityWorkflow, Request $request): Response
{
@@ -276,24 +295,17 @@ class WorkflowController extends AbstractController
$handler = $this->entityWorkflowManager->getHandler($entityWorkflow);
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
$errors = [];
$signatures = $entityWorkflow->getCurrentStep()->getSignatures();
if (\count($workflow->getEnabledTransitions($entityWorkflow)) > 0) {
// possible transition
$usersInvolved = $entityWorkflow->getUsersInvolved();
$currentUserFound = array_search($this->security->getUser(), $usersInvolved, true);
if (false !== $currentUserFound) {
unset($usersInvolved[$currentUserFound]);
}
$stepDTO = new WorkflowTransitionContextDTO($entityWorkflow);
$transitionForm = $this->createForm(
WorkflowStepType::class,
$entityWorkflow->getCurrentStep(),
$stepDTO,
[
'transition' => true,
'entity_workflow' => $entityWorkflow,
'suggested_users' => $usersInvolved,
]
);
@@ -310,12 +322,14 @@ class WorkflowController extends AbstractController
throw $this->createAccessDeniedException(sprintf("not allowed to apply transition {$transition}: %s", implode(', ', $msgs)));
}
// TODO symfony 5: add those "future" on context ($workflow->apply($entityWorkflow, $transition, $context)
$entityWorkflow->futureCcUsers = $transitionForm['future_cc_users']->getData() ?? [];
$entityWorkflow->futureDestUsers = $transitionForm['future_dest_users']->getData() ?? [];
$entityWorkflow->futureDestEmails = $transitionForm['future_dest_emails']->getData() ?? [];
$byUser = $this->security->getUser();
$workflow->apply($entityWorkflow, $transition);
$workflow->apply($entityWorkflow, $transition, [
'context' => $stepDTO,
'byUser' => $byUser,
'transition' => $transition,
'transitionAt' => $this->clock->now(),
]);
$this->entityManager->flush();
@@ -327,22 +341,6 @@ class WorkflowController extends AbstractController
}
}
/*
$commentForm = $this->createForm(EntityWorkflowCommentType::class, $newComment = new EntityWorkflowComment());
$commentForm->handleRequest($request);
if ($commentForm->isSubmitted() && $commentForm->isValid()) {
$this->entityManager->persist($newComment);
$this->entityManager->flush();
$this->addFlash('success', $this->translator->trans('workflow.Comment added'));
return $this->redirectToRoute('chill_main_workflow_show', ['id' => $entityWorkflow->getId()]);
} elseif ($commentForm->isSubmitted() && !$commentForm->isValid()) {
$this->addFlash('error', $this->translator->trans('This form contains errors'));
}
*/
return $this->render(
'@ChillMain/Workflow/index.html.twig',
[
@@ -352,7 +350,7 @@ class WorkflowController extends AbstractController
'transition_form' => isset($transitionForm) ? $transitionForm->createView() : null,
'entity_workflow' => $entityWorkflow,
'transition_form_errors' => $errors,
// 'comment_form' => $commentForm->createView(),
'signatures' => $signatures,
]
);
}
@@ -371,4 +369,65 @@ class WorkflowController extends AbstractController
return $lines;
}
#[Route(path: '/{_locale}/main/workflow/signature/{signature_id}/metadata', name: 'chill_main_workflow_signature_metadata')]
public function addSignatureMetadata(int $signature_id, Request $request): Response
{
$signature = $this->entityWorkflowStepSignatureRepository->find($signature_id);
if (null === $signature) {
throw new NotFoundHttpException('signature not found');
}
if ($signature->isSigned()) {
$this->addFlash(
'notice',
$this->translator->trans('workflow.signature_zone.already_signed_alert')
);
return $this->redirectToRoute('chill_main_workflow_show', ['id' => $signature->getStep()->getEntityWorkflow()->getId()]);
}
if ($signature->getSigner() instanceof User) {
return $this->redirectToRoute('chill_main_workflow_signature_add', [
'id' => $signature_id,
'returnPath' => $request->query->get('returnPath', null),
]);
}
$metadataForm = $this->createForm(WorkflowSignatureMetadataType::class);
$metadataForm->add('submit', SubmitType::class, ['label' => $this->translator->trans('Save')]);
$metadataForm->handleRequest($request);
if ($metadataForm->isSubmitted() && $metadataForm->isValid()) {
$data = $metadataForm->getData();
$signature->setSignatureMetadata(
[
'base_signer' => [
'document_type' => $data['documentType'],
'document_number' => $data['documentNumber'],
'expiration_date' => $data['expirationDate'],
],
]
);
$this->entityManager->persist($signature);
$this->entityManager->flush();
return $this->redirectToRoute('chill_main_workflow_signature_add', [
'id' => $signature_id,
'returnPath' => $request->query->get('returnPath', null),
]);
}
return $this->render(
'@ChillMain/Workflow/signature_metadata.html.twig',
[
'metadata_form' => $metadataForm->createView(),
'person' => $signature->getSigner(),
]
);
}
}

View File

@@ -0,0 +1,98 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Controller;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepHold;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Workflow\Registry;
class WorkflowOnHoldController
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly Security $security,
private readonly Registry $registry,
private readonly UrlGeneratorInterface $urlGenerator,
) {}
#[Route(path: '/{_locale}/main/workflow/{id}/hold', name: 'chill_main_workflow_on_hold')]
public function putOnHold(EntityWorkflow $entityWorkflow, Request $request): Response
{
$currentStep = $entityWorkflow->getCurrentStep();
$currentUser = $this->security->getUser();
if (!$currentUser instanceof User) {
throw new AccessDeniedHttpException('only user can put a workflow on hold');
}
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
$enabledTransitions = $workflow->getEnabledTransitions($entityWorkflow);
if (0 === count($enabledTransitions)) {
throw new AccessDeniedHttpException('You are not allowed to apply any transitions to this workflow, therefore you cannot toggle the hold status.');
}
$stepHold = new EntityWorkflowStepHold($currentStep, $currentUser);
$this->entityManager->persist($stepHold);
$this->entityManager->flush();
return new RedirectResponse(
$this->urlGenerator->generate(
'chill_main_workflow_show',
['id' => $entityWorkflow->getId()]
)
);
}
#[Route(path: '/{_locale}/main/workflow/{id}/remove_hold', name: 'chill_main_workflow_remove_hold')]
public function removeOnHold(EntityWorkflowStep $entityWorkflowStep): Response
{
$user = $this->security->getUser();
if (!$user instanceof User) {
throw new AccessDeniedHttpException('only user can remove workflow on hold');
}
if (!$entityWorkflowStep->isOnHoldByUser($user)) {
throw new AccessDeniedHttpException('You are not allowed to remove workflow on hold');
}
$hold = $entityWorkflowStep->getHoldsOnStep()->findFirst(fn (int $index, EntityWorkflowStepHold $entityWorkflowStepHold) => $user === $entityWorkflowStepHold->getByUser());
if (null === $hold) {
// this should not happens...
throw new NotFoundHttpException();
}
$this->entityManager->remove($hold);
$this->entityManager->flush();
return new RedirectResponse(
$this->urlGenerator->generate(
'chill_main_workflow_show',
['id' => $entityWorkflowStep->getEntityWorkflow()->getId()]
)
);
}
}

View File

@@ -0,0 +1,98 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Controller;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
use Chill\MainBundle\Routing\ChillUrlGeneratorInterface;
use Chill\MainBundle\Security\Authorization\EntityWorkflowStepSignatureVoter;
use Chill\MainBundle\Workflow\SignatureStepStateChanger;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
use Twig\Environment;
final readonly class WorkflowSignatureCancelController
{
public function __construct(
private EntityManagerInterface $entityManager,
private Security $security,
private FormFactoryInterface $formFactory,
private Environment $twig,
private SignatureStepStateChanger $signatureStepStateChanger,
private ChillUrlGeneratorInterface $chillUrlGenerator,
) {}
#[Route('/{_locale}/main/workflow/signature/{id}/cancel', name: 'chill_main_workflow_signature_cancel')]
public function cancelSignature(EntityWorkflowStepSignature $signature, Request $request): Response
{
return $this->markSignatureAction(
$signature,
$request,
EntityWorkflowStepSignatureVoter::CANCEL,
function (EntityWorkflowStepSignature $signature) {$this->signatureStepStateChanger->markSignatureAsCanceled($signature); },
'@ChillMain/WorkflowSignature/cancel.html.twig',
);
}
#[Route('/{_locale}/main/workflow/signature/{id}/reject', name: 'chill_main_workflow_signature_reject')]
public function rejectSignature(EntityWorkflowStepSignature $signature, Request $request): Response
{
return $this->markSignatureAction(
$signature,
$request,
EntityWorkflowStepSignatureVoter::REJECT,
function (EntityWorkflowStepSignature $signature) {$this->signatureStepStateChanger->markSignatureAsRejected($signature); },
'@ChillMain/WorkflowSignature/reject.html.twig',
);
}
private function markSignatureAction(
EntityWorkflowStepSignature $signature,
Request $request,
string $permissionAttribute,
callable $markSignature,
string $template,
): Response {
if (!$this->security->isGranted($permissionAttribute, $signature)) {
throw new AccessDeniedHttpException('not allowed to cancel this signature');
}
$form = $this->formFactory->create();
$form->add('confirm', SubmitType::class, ['label' => 'Confirm']);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$markSignature($signature);
$this->entityManager->flush();
return new RedirectResponse(
$this->chillUrlGenerator->returnPathOr('chill_main_workflow_show', ['id' => $signature->getStep()->getEntityWorkflow()->getId()])
);
}
return
new Response(
$this->twig->render(
$template,
['form' => $form->createView(), 'signature' => $signature]
)
);
}
}

View File

@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Controller;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSend;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSendView;
use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Chill\MainBundle\Workflow\Exception\HandlerWithPublicViewNotFoundException;
use Chill\MainBundle\Workflow\Messenger\PostPublicViewMessage;
use Chill\MainBundle\Workflow\Templating\EntityWorkflowViewMetadataDTO;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Clock\ClockInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Routing\Annotation\Route;
use Twig\Environment;
final readonly class WorkflowViewSendPublicController
{
public const LOG_PREFIX = '[workflow-view-send-public-controller] ';
public function __construct(
private EntityManagerInterface $entityManager,
private LoggerInterface $chillLogger,
private EntityWorkflowManager $entityWorkflowManager,
private ClockInterface $clock,
private Environment $environment,
private MessageBusInterface $messageBus,
) {}
#[Route('/public/main/workflow/send/{uuid}/view/{verificationKey}', name: 'chill_main_workflow_send_view_public', methods: ['GET'])]
public function __invoke(EntityWorkflowSend $workflowSend, string $verificationKey, Request $request): Response
{
if (50 < $workflowSend->getNumberOfErrorTrials()) {
throw new AccessDeniedHttpException('number of trials exceeded, no more access allowed');
}
if ($verificationKey !== $workflowSend->getPrivateToken()) {
$this->chillLogger->info(self::LOG_PREFIX.'Invalid trial for this send', ['client_ip' => $request->getClientIp()]);
$workflowSend->increaseErrorTrials();
$this->entityManager->flush();
throw new AccessDeniedHttpException('invalid verification key');
}
if ($this->clock->now() > $workflowSend->getExpireAt()) {
return new Response(
$this->environment->render('@ChillMain/Workflow/workflow_view_send_public_expired.html.twig'),
409
);
}
if (100 < $workflowSend->getViews()->count()) {
$this->chillLogger->info(self::LOG_PREFIX.'100 view reached, not allowed to see it again');
throw new AccessDeniedHttpException('100 views reached, not allowed to see it again');
}
try {
$metadata = new EntityWorkflowViewMetadataDTO(
$workflowSend->getViews()->count(),
100 - $workflowSend->getViews()->count(),
);
$response = new Response(
$this->entityWorkflowManager->renderPublicView($workflowSend, $metadata),
);
$view = new EntityWorkflowSendView($workflowSend, $this->clock->now(), $request->getClientIp());
$this->entityManager->persist($view);
$this->messageBus->dispatch(new PostPublicViewMessage($view->getId()));
$this->entityManager->flush();
return $response;
} catch (HandlerWithPublicViewNotFoundException $e) {
throw new \RuntimeException('Could not render the public view', previous: $e);
}
}
}