Julien Fastré 667e144681
Add PDF signature zone availability checks
Introduce `PDFSignatureZoneAvailable` service to check available PDF signature zones. Updated `WorkflowAddSignatureController` to use the new service. Added unit tests to verify the correctness of the functionality.
2024-09-04 14:09:53 +02:00

416 lines
17 KiB
PHP

<?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\EntityWorkflowStepSignature;
use Chill\MainBundle\Form\WorkflowSignatureMetadataType;
use Chill\MainBundle\Form\WorkflowStepType;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
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 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\Routing\Annotation\Route;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Component\Workflow\Registry;
use Symfony\Component\Workflow\TransitionBlocker;
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,
private readonly ClockInterface $clock,
) {}
#[Route(path: '/{_locale}/main/workflow/create', name: 'chill_main_workflow_create')]
public function create(Request $request): Response
{
if (!$request->query->has('entityClass')) {
throw new BadRequestHttpException('Missing entityClass parameter');
}
if (!$request->query->has('entityId')) {
throw new BadRequestHttpException('missing entityId parameter');
}
if (!$request->query->has('workflow')) {
throw new BadRequestHttpException('missing workflow parameter');
}
$entityWorkflow = new EntityWorkflow();
$entityWorkflow
->setRelatedEntityClass($request->query->get('entityClass'))
->setRelatedEntityId($request->query->getInt('entityId'))
->setWorkflowName($request->query->get('workflow'))
->addSubscriberToFinal($this->security->getUser());
$errors = $this->validator->validate($entityWorkflow, null, ['creation']);
if (\count($errors) > 0) {
$msg = [];
foreach ($errors as $error) {
/* @var \Symfony\Component\Validator\ConstraintViolationInterface $error */
$msg[] = $error->getMessage();
}
return new Response(implode("\n", $msg), Response::HTTP_UNPROCESSABLE_ENTITY);
}
$this->denyAccessUnlessGranted(EntityWorkflowVoter::CREATE, $entityWorkflow);
$em = $this->managerRegistry->getManager();
$em->persist($entityWorkflow);
$em->flush();
return $this->redirectToRoute('chill_main_workflow_show', ['id' => $entityWorkflow->getId()]);
}
#[Route(path: '/{_locale}/main/workflow/{id}/delete', name: 'chill_main_workflow_delete')]
public function delete(EntityWorkflow $entityWorkflow, Request $request): Response
{
$this->denyAccessUnlessGranted(EntityWorkflowVoter::DELETE, $entityWorkflow);
$form = $this->createForm(FormType::class);
$form->add('submit', SubmitType::class, ['label' => 'workflow.Delete workflow']);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->entityManager->remove($entityWorkflow);
$this->entityManager->flush();
$this->addFlash('success', $this->translator->trans('workflow.Workflow deleted with success'));
return $this->redirectToRoute('chill_main_homepage');
}
return $this->render('@ChillMain/Workflow/delete.html.twig', [
'entityWorkflow' => $entityWorkflow,
'delete_form' => $form->createView(),
'handler' => $this->entityWorkflowManager->getHandler($entityWorkflow),
]);
}
#[Route(path: '/{_locale}/main/workflow-step/{id}/access_key', name: 'chill_main_workflow_grant_access_by_key')]
public function getAccessByAccessKey(EntityWorkflowStep $entityWorkflowStep, Request $request): Response
{
if (null === $accessKey = $request->query->get('accessKey', null)) {
throw new BadRequestHttpException('accessKey is missing');
}
if (!$this->getUser() instanceof User) {
throw new AccessDeniedHttpException('Not a valid user');
}
if ($entityWorkflowStep->getAccessKey() !== $accessKey) {
throw new AccessDeniedHttpException('Access key is invalid');
}
if (!$entityWorkflowStep->isWaitingForTransition()) {
$this->addFlash('error', $this->translator->trans('workflow.Steps is not waiting for transition. Maybe someone apply the transition before you ?'));
} else {
$entityWorkflowStep->addDestUserByAccessKey($this->security->getUser());
$this->entityManager->flush();
$this->addFlash('success', $this->translator->trans('workflow.You get access to this step'));
}
return $this->redirectToRoute('chill_main_workflow_show', ['id' => $entityWorkflowStep->getEntityWorkflow()->getId()]);
}
/**
* Previous workflows where the user has applyed a transition.
*/
#[Route(path: '/{_locale}/main/workflow/list/previous_transitionned', name: 'chill_main_workflow_list_previous_transitionned')]
public function myPreviousWorkflowsTransitionned(Request $request): Response
{
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
$total = $this->entityWorkflowRepository->countByPreviousTransitionned($this->security->getUser());
$paginator = $this->paginatorFactory->create($total);
$workflows = $this->entityWorkflowRepository->findByPreviousTransitionned(
$this->security->getUser(),
['createdAt' => 'DESC'],
$paginator->getItemsPerPage(),
$paginator->getCurrentPageFirstItemNumber()
);
return $this->render(
'@ChillMain/Workflow/list.html.twig',
[
'help' => 'workflow.Previous workflow transitionned help',
'workflows' => $this->buildHandler($workflows),
'paginator' => $paginator,
'step' => 'previous_transitionned',
]
);
}
/**
* Previous workflows where the user was mentioned, but did not give any reaction.
*/
#[Route(path: '/{_locale}/main/workflow/list/previous_without_reaction', name: 'chill_main_workflow_list_previous_without_reaction')]
public function myPreviousWorkflowsWithoutReaction(Request $request): Response
{
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
$total = $this->entityWorkflowRepository->countByPreviousDestWithoutReaction($this->security->getUser());
$paginator = $this->paginatorFactory->create($total);
$workflows = $this->entityWorkflowRepository->findByPreviousDestWithoutReaction(
$this->security->getUser(),
['createdAt' => 'DESC'],
$paginator->getItemsPerPage(),
$paginator->getCurrentPageFirstItemNumber()
);
return $this->render(
'@ChillMain/Workflow/list.html.twig',
[
'help' => 'workflow.Previous workflow without reaction help',
'workflows' => $this->buildHandler($workflows),
'paginator' => $paginator,
'step' => 'previous_without_reaction',
]
);
}
#[Route(path: '/{_locale}/main/workflow/list/cc', name: 'chill_main_workflow_list_cc')]
public function myWorkflowsCc(Request $request): Response
{
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
$total = $this->entityWorkflowRepository->countByDest($this->security->getUser());
$paginator = $this->paginatorFactory->create($total);
$workflows = $this->entityWorkflowRepository->findByCc(
$this->security->getUser(),
['createdAt' => 'DESC'],
$paginator->getItemsPerPage(),
$paginator->getCurrentPageFirstItemNumber()
);
return $this->render(
'@ChillMain/Workflow/list.html.twig',
[
'workflows' => $this->buildHandler($workflows),
'paginator' => $paginator,
'step' => 'cc',
]
);
}
#[Route(path: '/{_locale}/main/workflow/list/dest', name: 'chill_main_workflow_list_dest')]
public function myWorkflowsDest(Request $request): Response
{
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
$total = $this->entityWorkflowRepository->countByDest($this->security->getUser());
$paginator = $this->paginatorFactory->create($total);
$workflows = $this->entityWorkflowRepository->findByDest(
$this->security->getUser(),
['createdAt' => 'DESC'],
$paginator->getItemsPerPage(),
$paginator->getCurrentPageFirstItemNumber()
);
return $this->render(
'@ChillMain/Workflow/list.html.twig',
[
'workflows' => $this->buildHandler($workflows),
'paginator' => $paginator,
'step' => 'dest',
]
);
}
#[Route(path: '/{_locale}/main/workflow/list/subscribed', name: 'chill_main_workflow_list_subscribed')]
public function myWorkflowsSubscribed(Request $request): Response
{
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
$total = $this->entityWorkflowRepository->countBySubscriber($this->security->getUser());
$paginator = $this->paginatorFactory->create($total);
$workflows = $this->entityWorkflowRepository->findBySubscriber(
$this->security->getUser(),
['createdAt' => 'DESC'],
$paginator->getItemsPerPage(),
$paginator->getCurrentPageFirstItemNumber()
);
return $this->render(
'@ChillMain/Workflow/list.html.twig',
[
'workflows' => $this->buildHandler($workflows),
'paginator' => $paginator,
'step' => 'subscribed',
]
);
}
#[Route(path: '/{_locale}/main/workflow/{id}/show', name: 'chill_main_workflow_show')]
public function show(EntityWorkflow $entityWorkflow, Request $request): Response
{
$this->denyAccessUnlessGranted(EntityWorkflowVoter::SEE, $entityWorkflow);
$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
$stepDTO = new WorkflowTransitionContextDTO($entityWorkflow);
$usersInvolved = $entityWorkflow->getUsersInvolved();
$currentUserFound = array_search($this->security->getUser(), $usersInvolved, true);
if (false !== $currentUserFound) {
unset($usersInvolved[$currentUserFound]);
}
$transitionForm = $this->createForm(
WorkflowStepType::class,
$stepDTO,
[
'entity_workflow' => $entityWorkflow,
'suggested_users' => $usersInvolved,
]
);
$transitionForm->handleRequest($request);
if ($transitionForm->isSubmitted() && $transitionForm->isValid()) {
if (!$workflow->can($entityWorkflow, $transition = $transitionForm['transition']->getData()->getName())) {
$blockers = $workflow->buildTransitionBlockerList($entityWorkflow, $transition);
$msgs = array_map(fn (TransitionBlocker $tb) => $this->translator->trans(
$tb->getMessage(),
$tb->getParameters()
), iterator_to_array($blockers));
throw $this->createAccessDeniedException(sprintf("not allowed to apply transition {$transition}: %s", implode(', ', $msgs)));
}
$byUser = $this->security->getUser();
$workflow->apply($entityWorkflow, $transition, [
'context' => $stepDTO,
'byUser' => $byUser,
'transition' => $transition,
'transitionAt' => $this->clock->now(),
]);
$this->entityManager->flush();
return $this->redirectToRoute('chill_main_workflow_show', ['id' => $entityWorkflow->getId()]);
}
if ($transitionForm->isSubmitted() && !$transitionForm->isValid()) {
$this->addFlash('error', $this->translator->trans('This form contains errors'));
}
}
return $this->render(
'@ChillMain/Workflow/index.html.twig',
[
'handler' => $handler,
'handler_template' => $handler->getTemplate($entityWorkflow),
'handler_template_data' => $handler->getTemplateData($entityWorkflow),
'transition_form' => isset($transitionForm) ? $transitionForm->createView() : null,
'entity_workflow' => $entityWorkflow,
'transition_form_errors' => $errors,
'signatures' => $signatures,
]
);
}
private function buildHandler(array $workflows): array
{
$lines = [];
foreach ($workflows as $workflow) {
$handler = $this->entityWorkflowManager->getHandler($workflow);
$lines[] = [
'handler' => $handler,
'entity_workflow' => $workflow,
];
}
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->entityManager->getRepository(EntityWorkflowStepSignature::class)->find($signature_id);
if ($signature->getSigner() instanceof User) {
return $this->redirectToRoute('chill_main_workflow_signature_add', ['id' => $signature_id]);
}
$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]);
}
return $this->render(
'@ChillMain/Workflow/_signature_metadata.html.twig',
[
'metadata_form' => $metadataForm->createView(),
'person' => $signature->getSigner(),
]
);
}
}