Implement the controller action to view the EntityworkflowSend

This commit is contained in:
Julien Fastré 2024-10-07 15:35:36 +02:00
parent a0b5c208eb
commit 5c0f3cb317
Signed by: julienfastre
GPG Key ID: BDE2190974723FCB
9 changed files with 381 additions and 5 deletions

View File

@ -12,14 +12,69 @@ declare(strict_types=1);
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 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\Routing\Annotation\Route;
use Twig\Environment;
class WorkflowViewSendPublicController
final readonly class WorkflowViewSendPublicController
{
#[Route('/public/main/workflow/send/{uuid}/view/{verificationKey}', methods: ['GET'], name: 'chill_main_workflow_send_view_public')]
public function __invoke(EntityWorkflowSend $workflowSend, string $verificationKey): Response
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,
) {}
#[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
{
return new Response('ok');
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 {
$response = new Response(
$this->entityWorkflowManager->renderPublicView($workflowSend),
);
$view = new EntityWorkflowSendView($workflowSend, $this->clock->now(), $request->getClientIp());
$this->entityManager->persist($view);
$this->entityManager->flush();
return $response;
} catch (HandlerWithPublicViewNotFoundException $e) {
throw new \RuntimeException('Could not render the public view', previous: $e);
}
}
}

View File

@ -119,6 +119,11 @@ class EntityWorkflowSend implements TrackCreationInterface
return $this->privateToken;
}
public function getEntityWorkflowStep(): EntityWorkflowStep
{
return $this->entityWorkflowStep;
}
public function getUuid(): UuidInterface
{
return $this->uuid;

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{ 'workflow.public_link.expired_link_title'|trans }}</title>
</head>
<body>
<h1>{{ 'workflow.public_link.expired_link_title'|trans }}</h1>
<p>{{ 'workflow.public_link.expired_link_explanation'|trans }}</p>
</body>
</html>

View File

@ -0,0 +1,234 @@
<?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\Tests\Controller;
use Chill\MainBundle\Controller\WorkflowViewSendPublicController;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSend;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSendView;
use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface;
use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Chill\MainBundle\Workflow\EntityWorkflowWithPublicViewInterface;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Psr\Log\NullLogger;
use Symfony\Component\Clock\MockClock;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Workflow\Registry;
use Twig\Environment;
/**
* @internal
*
* @coversNothing
*/
class WorkflowViewSendPublicControllerTest extends TestCase
{
use ProphecyTrait;
public function testTooMuchTrials(): void
{
$environment = $this->prophesize(Environment::class);
$entityManager = $this->prophesize(EntityManagerInterface::class);
$entityManager->flush()->shouldNotBeCalled();
$controller = new WorkflowViewSendPublicController($entityManager->reveal(), new NullLogger(), new EntityWorkflowManager([], new Registry()), new MockClock(), $environment->reveal());
self::expectException(AccessDeniedHttpException::class);
$send = $this->buildEntityWorkflowSend();
for ($i = 0; $i < 51; ++$i) {
$send->increaseErrorTrials();
}
$controller($send, $send->getPrivateToken(), new Request());
}
public function testInvalidVerificationKey(): void
{
$environment = $this->prophesize(Environment::class);
$entityManager = $this->prophesize(EntityManagerInterface::class);
$entityManager->flush()->shouldBeCalled();
$controller = new WorkflowViewSendPublicController($entityManager->reveal(), new NullLogger(), new EntityWorkflowManager([], new Registry()), new MockClock(), $environment->reveal());
self::expectException(AccessDeniedHttpException::class);
$send = $this->buildEntityWorkflowSend();
try {
$controller($send, 'some-token', new Request());
} catch (AccessDeniedHttpException $e) {
self::assertEquals(1, $send->getNumberOfErrorTrials());
throw $e;
}
}
public function testExpiredLink(): void
{
$environment = $this->prophesize(Environment::class);
$environment->render('@ChillMain/Workflow/workflow_view_send_public_expired.html.twig')->willReturn('test');
$entityManager = $this->prophesize(EntityManagerInterface::class);
$entityManager->flush()->shouldNotBeCalled();
$controller = new WorkflowViewSendPublicController($entityManager->reveal(), new NullLogger(), new EntityWorkflowManager([], new Registry()), new MockClock('next year'), $environment->reveal());
$send = $this->buildEntityWorkflowSend();
$response = $controller($send, $send->getPrivateToken(), new Request());
self::assertEquals('test', $response->getContent());
self::assertEquals(409, $response->getStatusCode());
}
public function testNoHandlerFound(): void
{
$environment = $this->prophesize(Environment::class);
$entityManager = $this->prophesize(EntityManagerInterface::class);
$entityManager->flush()->shouldNotBeCalled();
$controller = new WorkflowViewSendPublicController(
$entityManager->reveal(),
new NullLogger(),
new EntityWorkflowManager([], new Registry()),
new MockClock(),
$environment->reveal(),
);
self::expectException(\RuntimeException::class);
$send = $this->buildEntityWorkflowSend();
$controller($send, $send->getPrivateToken(), new Request());
}
public function testHappyScenario(): void
{
$send = $this->buildEntityWorkflowSend();
$environment = $this->prophesize(Environment::class);
$entityManager = $this->prophesize(EntityManagerInterface::class);
$entityManager->persist(Argument::that(function (EntityWorkflowSendView $view) use ($send) {
return $send === $view->getSend();
}))->shouldBeCalled();
$entityManager->flush()->shouldBeCalled();
$controller = new WorkflowViewSendPublicController(
$entityManager->reveal(),
new NullLogger(),
new EntityWorkflowManager([
$this->buildFakeHandler(),
], new Registry()),
new MockClock(),
$environment->reveal(),
);
$response = $controller($send, $send->getPrivateToken(), $this->buildRequest());
self::assertEquals(200, $response->getStatusCode());
self::assertEquals('content', $response->getContent());
}
private function buildFakeHandler(): EntityWorkflowHandlerInterface&EntityWorkflowWithPublicViewInterface
{
return new class () implements EntityWorkflowWithPublicViewInterface, EntityWorkflowHandlerInterface {
public function getDeletionRoles(): array
{
throw new \BadMethodCallException('not implemented');
}
public function getEntityData(EntityWorkflow $entityWorkflow, array $options = []): array
{
throw new \BadMethodCallException('not implemented');
}
public function getEntityTitle(EntityWorkflow $entityWorkflow, array $options = []): string
{
throw new \BadMethodCallException('not implemented');
}
public function getRelatedEntity(EntityWorkflow $entityWorkflow): ?object
{
throw new \BadMethodCallException('not implemented');
}
public function getRelatedObjects(object $object): array
{
throw new \BadMethodCallException('not implemented');
}
public function getRoleShow(EntityWorkflow $entityWorkflow): ?string
{
throw new \BadMethodCallException('not implemented');
}
public function getSuggestedUsers(EntityWorkflow $entityWorkflow): array
{
throw new \BadMethodCallException('not implemented');
}
public function getTemplate(EntityWorkflow $entityWorkflow, array $options = []): string
{
throw new \BadMethodCallException('not implemented');
}
public function getTemplateData(EntityWorkflow $entityWorkflow, array $options = []): array
{
throw new \BadMethodCallException('not implemented');
}
public function isObjectSupported(object $object): bool
{
throw new \BadMethodCallException('not implemented');
}
public function supports(EntityWorkflow $entityWorkflow, array $options = []): bool
{
return true;
}
public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool
{
return false;
}
public function findByRelatedEntity(object $object): array
{
throw new \BadMethodCallException('not implemented');
}
public function renderPublicView(EntityWorkflowSend $entityWorkflowSend): string
{
return 'content';
}
};
}
private function buildRequest(): Request
{
return Request::create('/test', server: ['REMOTE_ADDR' => '10.0.0.10']);
}
private function buildEntityWorkflowSend(): EntityWorkflowSend
{
$entityWorkflow = new EntityWorkflow();
$step = $entityWorkflow->getCurrentStep();
return new EntityWorkflowSend($step, new ThirdParty(), new \DateTimeImmutable('next month'));
}
}

View File

@ -62,7 +62,7 @@ class PostSendExternalMessageHandlerTest extends TestCase
private function buildCheckAddressCallback(string $emailToCheck): callable
{
return fn(TemplatedEmail $email): bool => in_array($emailToCheck, array_map(fn (Address $addr) => $addr->getAddress(), $email->getTo()), true);
return fn (TemplatedEmail $email): bool => in_array($emailToCheck, array_map(fn (Address $addr) => $addr->getAddress(), $email->getTo()), true);
}
private function buildEntityWorkflow(): EntityWorkflow

View File

@ -13,9 +13,16 @@ namespace Chill\MainBundle\Workflow;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSend;
use Chill\MainBundle\Workflow\Exception\HandlerNotFoundException;
use Chill\MainBundle\Workflow\Exception\HandlerWithPublicViewNotFoundException;
use Symfony\Component\Workflow\Registry;
/**
* Manage the handler and performs some operation on handlers.
*
* Each handler must implement @{EntityWorkflowHandlerInterface::class}.
*/
class EntityWorkflowManager
{
/**
@ -63,4 +70,26 @@ class EntityWorkflowManager
return [];
}
/**
* Renders the public view for the given entity workflow send.
*
* @param EntityWorkflowSend $entityWorkflowSend the entity workflow send object
*
* @return string the rendered public view
*
* @throws HandlerWithPublicViewNotFoundException if no handler with public view is found
*/
public function renderPublicView(EntityWorkflowSend $entityWorkflowSend): string
{
$entityWorkflow = $entityWorkflowSend->getEntityWorkflowStep()->getEntityWorkflow();
foreach ($this->handlers as $handler) {
if ($handler instanceof EntityWorkflowWithPublicViewInterface && $handler->supports($entityWorkflow)) {
return $handler->renderPublicView($entityWorkflowSend);
}
}
throw new HandlerWithPublicViewNotFoundException();
}
}

View File

@ -0,0 +1,24 @@
<?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\Workflow;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSend;
interface EntityWorkflowWithPublicViewInterface
{
/**
* Render the public view for EntityWorkflowSend.
*
* The public view must be a safe html string
*/
public function renderPublicView(EntityWorkflowSend $entityWorkflowSend): string;
}

View File

@ -0,0 +1,14 @@
<?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\Workflow\Exception;
class HandlerWithPublicViewNotFoundException extends \RuntimeException {}

View File

@ -565,6 +565,9 @@ workflow:
transition_destinee_remove_emails: Supprimer
transition_destinee_emails_help: Le lien sécurisé sera envoyé à chaque adresse indiquée
public_link:
expired_link_title: Lien expiré
expired_link_explanation: Le lien a expiré, vous ne pouvez plus visualiser ce document.