From 36ace4f2672fa82782f1365b3202bd6bbfed01a4 Mon Sep 17 00:00:00 2001 From: numew Date: Mon, 20 Jan 2025 17:55:32 +0100 Subject: [PATCH 1/3] sanitize suivi description #3580 --- migrations/Version20250120141313.php | 26 ++++++++ src/Command/SanitizeSuivisCommand.php | 61 +++++++++++++++++++ src/Entity/Suivi.php | 21 ++++++- src/Manager/SuiviManager.php | 6 +- templates/back/notifications/index.html.twig | 8 +-- .../back/signalement/view/suivis.html.twig | 8 +-- .../_suivi_signalement_tab_suivi.html.twig | 2 +- templates/pdf/signalement.html.twig | 2 +- .../Back/SignalementActionControllerTest.php | 2 +- tests/Functional/Manager/SuiviManagerTest.php | 7 ++- 10 files changed, 129 insertions(+), 14 deletions(-) create mode 100644 migrations/Version20250120141313.php create mode 100644 src/Command/SanitizeSuivisCommand.php diff --git a/migrations/Version20250120141313.php b/migrations/Version20250120141313.php new file mode 100644 index 000000000..df4bc3fc2 --- /dev/null +++ b/migrations/Version20250120141313.php @@ -0,0 +1,26 @@ +addSql('ALTER TABLE suivi ADD is_sanitized TINYINT(1) NOT NULL'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE suivi DROP is_sanitized'); + } +} diff --git a/src/Command/SanitizeSuivisCommand.php b/src/Command/SanitizeSuivisCommand.php new file mode 100644 index 000000000..16a722f09 --- /dev/null +++ b/src/Command/SanitizeSuivisCommand.php @@ -0,0 +1,61 @@ +historyEntryManager->removeEntityListeners(); + $io = new SymfonyStyle($input, $output); + $countAll = $this->suiviRepository->count(['isSanitized' => false]); + $io->info(sprintf('Found %s suivis to sanitize', $countAll)); + $suivis = $this->suiviRepository->findBy(['isSanitized' => false], ['createdAt' => 'DESC'], 50000); + $i = 0; + $progressBar = new ProgressBar($output, \count($suivis)); + $progressBar->start(); + foreach ($suivis as $suivi) { + $suivi->setDescription($this->htmlSanitizer->sanitize($suivi->getDescription(false, false))); + $suivi->setIsSanitized(true); + ++$i; + $progressBar->advance(); + if (0 === $i % self::BATCH_SIZE) { + $this->entityManager->flush(); + } + } + $this->entityManager->flush(); + $progressBar->finish(); + $io->newLine(); + $io->success(sprintf('Sanitized %s/%s suivis', $i, $countAll)); + + return Command::SUCCESS; + } +} diff --git a/src/Entity/Suivi.php b/src/Entity/Suivi.php index 91fb10977..52f4b6136 100644 --- a/src/Entity/Suivi.php +++ b/src/Entity/Suivi.php @@ -73,10 +73,14 @@ class Suivi implements EntityHistoryInterface #[ORM\Column(nullable: true)] private ?array $originalData = null; + #[ORM\Column] + private ?bool $isSanitized = null; + public function __construct() { $this->createdAt = new \DateTimeImmutable(); $this->isPublic = false; + $this->isSanitized = true; } public function getId(): ?int @@ -142,8 +146,11 @@ public function getCreatedByLabel(): ?string return 'OCCUPANT : '.strtoupper($this->getSignalement()->getNomOccupant()).' '.ucfirst($this->getSignalement()->getPrenomOccupant()); } - public function getDescription($transformHtml = true): ?string + public function getDescription($transformHtml = true, $originalData = false): ?string { + if ($originalData) { + return $this->description; + } if (null !== $this->deletedAt) { return self::DESCRIPTION_DELETED.' '.$this->deletedAt->format('d/m/Y'); } @@ -262,4 +269,16 @@ public function getHistoryRegisteredEvent(): array { return [HistoryEntryEvent::CREATE, HistoryEntryEvent::UPDATE, HistoryEntryEvent::DELETE]; } + + public function getIsSanitized(): ?bool + { + return $this->isSanitized; + } + + public function setIsSanitized(bool $isSanitized): static + { + $this->isSanitized = $isSanitized; + + return $this; + } } diff --git a/src/Manager/SuiviManager.php b/src/Manager/SuiviManager.php index b04265c3f..e3c5c4b4a 100644 --- a/src/Manager/SuiviManager.php +++ b/src/Manager/SuiviManager.php @@ -14,6 +14,8 @@ use App\Service\Sanitizer; use Doctrine\Persistence\ManagerRegistry; use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\HtmlSanitizer\HtmlSanitizerInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; class SuiviManager extends Manager @@ -24,6 +26,8 @@ public function __construct( private readonly SignalementUpdatedListener $signalementUpdatedListener, private readonly Security $security, private readonly DesordreCritereRepository $desordreCritereRepository, + #[Autowire(service: 'html_sanitizer.sanitizer.app.message_sanitizer')] + private readonly HtmlSanitizerInterface $htmlSanitizer, string $entityName = Suivi::class, ) { parent::__construct($managerRegistry, $entityName); @@ -42,7 +46,7 @@ public function createSuivi( $suivi = (new Suivi()) ->setCreatedBy($user) ->setSignalement($signalement) - ->setDescription($description) + ->setDescription($this->htmlSanitizer->sanitize($description)) ->setType($type) ->setIsPublic($isPublic) ->setContext($context) diff --git a/templates/back/notifications/index.html.twig b/templates/back/notifications/index.html.twig index f917206e1..e65ca2720 100755 --- a/templates/back/notifications/index.html.twig +++ b/templates/back/notifications/index.html.twig @@ -69,10 +69,10 @@ {{ notification.suivi.createdAt|format_datetime(locale='fr', timezone=territory_timezone, pattern='d MMMM yyyy à HH:mm:ss') }} {{ notification.suivi.description - |replace({'&t=___TOKEN___':'/'~notification.signalement.uuid}) - |replace({'?t=___TOKEN___':'/'~notification.signalement.uuid}) - |replace({'?folder=_up':'/'~notification.signalement.uuid~'?variant=resize'}) - |sanitize_html('app.message_sanitizer') }} + |replace({'&t=___TOKEN___':'/'~notification.signalement.uuid}) + |replace({'?t=___TOKEN___':'/'~notification.signalement.uuid}) + |replace({'?folder=_up':'/'~notification.signalement.uuid~'?variant=resize'}) + |raw }} {{ notification.suivi.createdBy ? notification.suivi.createdBy.nomComplet : notification.signalement.nomOccupant|upper~' '~notification.signalement.prenomOccupant|capitalize }} diff --git a/templates/back/signalement/view/suivis.html.twig b/templates/back/signalement/view/suivis.html.twig index 718aa5b60..005c880bf 100755 --- a/templates/back/signalement/view/suivis.html.twig +++ b/templates/back/signalement/view/suivis.html.twig @@ -62,10 +62,10 @@
{% endif %} {{ suivi.description - |replace({'&t=___TOKEN___':'/'~signalement.uuid}) - |replace({'?t=___TOKEN___':'/'~signalement.uuid}) - |replace({'?folder=_up':'/'~signalement.uuid~'?variant=resize'}) - |sanitize_html('app.message_sanitizer') + |replace({'&t=___TOKEN___':'/'~signalement.uuid}) + |replace({'?t=___TOKEN___':'/'~signalement.uuid}) + |replace({'?foldert=_up':'/'~signalement.uuid~'?variant=resize'}) + |raw }}
diff --git a/templates/front/_partials/_suivi_signalement_tab_suivi.html.twig b/templates/front/_partials/_suivi_signalement_tab_suivi.html.twig index 3b660782e..ac9858fc0 100755 --- a/templates/front/_partials/_suivi_signalement_tab_suivi.html.twig +++ b/templates/front/_partials/_suivi_signalement_tab_suivi.html.twig @@ -24,7 +24,7 @@
- {{ suivi.description|replace({'___TOKEN___':csrf_token('suivi_signalement_ext_file_view')})|sanitize_html('app.message_sanitizer') }} + {{ suivi.description|replace({'___TOKEN___':csrf_token('suivi_signalement_ext_file_view')})|raw }}
{% endfor %} diff --git a/templates/pdf/signalement.html.twig b/templates/pdf/signalement.html.twig index f87bf50a0..c9148e5ec 100755 --- a/templates/pdf/signalement.html.twig +++ b/templates/pdf/signalement.html.twig @@ -618,7 +618,7 @@ {% endif %} {{ suivi.createdAt|date('d/m/Y') }} - {{ suivi.description|sanitize_html('app.message_sanitizer') }} + {{ suivi.description|raw }} {% endfor %} diff --git a/tests/Functional/Controller/Back/SignalementActionControllerTest.php b/tests/Functional/Controller/Back/SignalementActionControllerTest.php index 54039d9db..3ab6e3f3e 100644 --- a/tests/Functional/Controller/Back/SignalementActionControllerTest.php +++ b/tests/Functional/Controller/Back/SignalementActionControllerTest.php @@ -124,7 +124,7 @@ public function testDeleteSuivi(): void $route = $this->router->generate('back_signalement_delete_suivi', ['uuid' => $signalement->getUuid()]); $this->client->request('GET', $route); - $description = 'Un petit message de rappel afin d\'y revenir plus tard'; + $description = 'Un petit message de rappel afin d'y revenir plus tard'; $suivi = $this->suiviRepository->findOneBy(['description' => $description]); $this->client->request( diff --git a/tests/Functional/Manager/SuiviManagerTest.php b/tests/Functional/Manager/SuiviManagerTest.php index 937d79a8c..8d25ca046 100644 --- a/tests/Functional/Manager/SuiviManagerTest.php +++ b/tests/Functional/Manager/SuiviManagerTest.php @@ -13,6 +13,7 @@ use Doctrine\Persistence\ManagerRegistry; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\HtmlSanitizer\HtmlSanitizerInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Security\Core\User\UserInterface; @@ -24,6 +25,7 @@ class SuiviManagerTest extends KernelTestCase private Security $security; private UrlGeneratorInterface $urlGenerator; private DesordreCritereRepository $desordreCritereRepository; + private HtmlSanitizerInterface $htmlSanitizerInterface; protected function setUp(): void { @@ -33,6 +35,7 @@ protected function setUp(): void $this->security = static::getContainer()->get(Security::class); $this->urlGenerator = static::getContainer()->get(UrlGeneratorInterface::class); $this->desordreCritereRepository = static::getContainer()->get(DesordreCritereRepository::class); + $this->htmlSanitizerInterface = static::getContainer()->get(HtmlSanitizerInterface::class); } public function testCreateSuivi(): void @@ -43,6 +46,7 @@ public function testCreateSuivi(): void $this->signalementUpdatedListener, $this->security, $this->desordreCritereRepository, + $this->htmlSanitizerInterface, Suivi::class, ); @@ -74,9 +78,10 @@ public function testCreateSuivi(): void $this->assertEquals(Suivi::TYPE_PARTNER, $suivi->getType()); $this->assertNotEquals($countSuivisBeforeCreate, $countSuivisAfterCreate); $this->assertInstanceOf(Suivi::class, $suivi); - $desc = 'Le signalement a été cloturé pour test avec le motif suivant
Non décence
Desc. : Lorem ipsum suivi sit amet, consectetur adipiscing elit.'; + $desc = 'Le signalement a été cloturé pour test avec le motif suivant
Non décence
Desc. : Lorem ipsum suivi sit amet, consectetur adipiscing elit.'; $this->assertEquals($desc, $suivi->getDescription()); $this->assertTrue($suivi->getIsPublic()); + $this->assertTrue($suivi->getIsSanitized()); $this->assertInstanceOf(UserInterface::class, $suivi->getCreatedBy()); } } From 125d29d2111bcae939701058eaee57c2cdd91b66 Mon Sep 17 00:00:00 2001 From: numew Date: Tue, 21 Jan 2025 14:43:40 +0100 Subject: [PATCH 2/3] change based on comments #3580 --- src/Twig/AppExtension.php | 12 +++++++ tests/Functional/Manager/SuiviManagerTest.php | 32 +++++++++++++++---- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/src/Twig/AppExtension.php b/src/Twig/AppExtension.php index 47eb0542e..b64cba85c 100644 --- a/src/Twig/AppExtension.php +++ b/src/Twig/AppExtension.php @@ -6,6 +6,7 @@ use App\Entity\Enum\OccupantLink; use App\Entity\Enum\QualificationStatus; use App\Entity\File; +use App\Entity\Suivi; use App\Service\Files\ImageBase64Encoder; use App\Service\Notification\NotificationCounter; use App\Service\Signalement\Qualification\QualificationStatusService; @@ -154,6 +155,7 @@ public function getFunctions(): array new TwigFunction('show_email_alert', [$this, 'showEmailAlert']), new TwigFunction('user_avatar_or_placeholder', [UserAvatar::class, 'userAvatarOrPlaceholder'], ['is_safe' => ['html']]), new TwigFunction('singular_or_plural', [$this, 'displaySingularOrPlural']), + new TwigFunction('transform_suivi_description', [$this, 'transformSuiviDescription']), ]; } @@ -182,4 +184,14 @@ public function displaySingularOrPlural(?int $count, string $strIfSingular, stri return $count.' '.$strIfSingular; } + + public function transformSuiviDescription(Suivi $suivi): string + { + $content = $suivi->getDescription(); + $content = str_replace('&t=___TOKEN___', '/'.$suivi->getSignalement()->getUuid(), $content); + $content = str_replace('?t=___TOKEN___', '/'.$suivi->getSignalement()->getUuid(), $content); + $content = str_replace('?folder=_up', '/'.$suivi->getSignalement()->getUuid().'?variant=resize', $content); + + return $content; + } } diff --git a/tests/Functional/Manager/SuiviManagerTest.php b/tests/Functional/Manager/SuiviManagerTest.php index 8d25ca046..570d0dbd4 100644 --- a/tests/Functional/Manager/SuiviManagerTest.php +++ b/tests/Functional/Manager/SuiviManagerTest.php @@ -26,6 +26,7 @@ class SuiviManagerTest extends KernelTestCase private UrlGeneratorInterface $urlGenerator; private DesordreCritereRepository $desordreCritereRepository; private HtmlSanitizerInterface $htmlSanitizerInterface; + private SuiviManager $suiviManager; protected function setUp(): void { @@ -35,12 +36,8 @@ protected function setUp(): void $this->security = static::getContainer()->get(Security::class); $this->urlGenerator = static::getContainer()->get(UrlGeneratorInterface::class); $this->desordreCritereRepository = static::getContainer()->get(DesordreCritereRepository::class); - $this->htmlSanitizerInterface = static::getContainer()->get(HtmlSanitizerInterface::class); - } - - public function testCreateSuivi(): void - { - $suiviManager = new SuiviManager( + $this->htmlSanitizerInterface = self::getContainer()->get('html_sanitizer.sanitizer.app.message_sanitizer'); + $this->suiviManager = new SuiviManager( $this->managerRegistry, $this->urlGenerator, $this->signalementUpdatedListener, @@ -49,7 +46,10 @@ public function testCreateSuivi(): void $this->htmlSanitizerInterface, Suivi::class, ); + } + public function testCreateSuivi(): void + { /** @var Signalement $signalement */ $signalement = $this->managerRegistry->getRepository(Signalement::class)->findOneBy( ['reference' => self::REF_SIGNALEMENT] @@ -65,7 +65,7 @@ public function testCreateSuivi(): void 'motif_cloture' => MotifCloture::tryFrom('NON_DECENCE'), 'subject' => 'test', ]; - $suivi = $suiviManager->createSuivi( + $suivi = $this->suiviManager->createSuivi( user : $user, signalement : $signalement, description : SuiviManager::buildDescriptionClotureSignalement($params), @@ -84,4 +84,22 @@ public function testCreateSuivi(): void $this->assertTrue($suivi->getIsSanitized()); $this->assertInstanceOf(UserInterface::class, $suivi->getCreatedBy()); } + + public function testCreateSuiviWithImageBase64(): void + { + /** @var Signalement $signalement */ + $signalement = $this->managerRegistry->getRepository(Signalement::class)->findOneBy( + ['reference' => self::REF_SIGNALEMENT] + ); + + $desc = 'Salut ma poule '; + $descSanitized = 'Salut ma poule '; + + $suivi = $this->suiviManager->createSuivi( + signalement : $signalement, + description : $desc, + type : Suivi::TYPE_USAGER + ); + $this->assertEquals($descSanitized, $suivi->getDescription()); + } } From a2d2f842f7e2262b98c87cd602d7f31a2939ed66 Mon Sep 17 00:00:00 2001 From: numew Date: Tue, 21 Jan 2025 16:12:37 +0100 Subject: [PATCH 3/3] use twig function #3580 --- templates/back/notifications/index.html.twig | 7 +------ templates/back/signalement/view/suivis.html.twig | 7 +------ 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/templates/back/notifications/index.html.twig b/templates/back/notifications/index.html.twig index e65ca2720..a69bbc4fc 100755 --- a/templates/back/notifications/index.html.twig +++ b/templates/back/notifications/index.html.twig @@ -68,12 +68,7 @@ ({{notification.suivi.signalement.villeOccupant}}) {{ notification.suivi.createdAt|format_datetime(locale='fr', timezone=territory_timezone, pattern='d MMMM yyyy à HH:mm:ss') }} - {{ notification.suivi.description - |replace({'&t=___TOKEN___':'/'~notification.signalement.uuid}) - |replace({'?t=___TOKEN___':'/'~notification.signalement.uuid}) - |replace({'?folder=_up':'/'~notification.signalement.uuid~'?variant=resize'}) - |raw }} - + {{ transform_suivi_description(notification.suivi)|raw }} {{ notification.suivi.createdBy ? notification.suivi.createdBy.nomComplet : notification.signalement.nomOccupant|upper~' '~notification.signalement.prenomOccupant|capitalize }} {% endif %} - {{ suivi.description - |replace({'&t=___TOKEN___':'/'~signalement.uuid}) - |replace({'?t=___TOKEN___':'/'~signalement.uuid}) - |replace({'?foldert=_up':'/'~signalement.uuid~'?variant=resize'}) - |raw - }} + {{ transform_suivi_description(suivi)|raw}}
{% if suivi.isPublic %}