Skip to content

Commit

Permalink
Merge pull request #3587 from MTES-MCT/feature/3580-sanitize-suivi-de…
Browse files Browse the repository at this point in the history
…scription

[BO - Signalement] Bloquer les images copiées/collées en base64 dans le contenu
  • Loading branch information
numew authored Jan 23, 2025
2 parents e136293 + a2d2f84 commit 61d61de
Show file tree
Hide file tree
Showing 11 changed files with 159 additions and 24 deletions.
26 changes: 26 additions & 0 deletions migrations/Version20250120141313.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

final class Version20250120141313 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add is_sanitized column to suivi';
}

public function up(Schema $schema): void
{
$this->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');
}
}
61 changes: 61 additions & 0 deletions src/Command/SanitizeSuivisCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

namespace App\Command;

use App\Manager\HistoryEntryManager;
use App\Repository\SuiviRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HtmlSanitizer\HtmlSanitizerInterface;

#[AsCommand(
name: 'app:sanitize-suivis',
description: 'Sanitize suivis',
)]
class SanitizeSuivisCommand extends Command
{
private const int BATCH_SIZE = 1000;

public function __construct(
private readonly SuiviRepository $suiviRepository,
private readonly EntityManagerInterface $entityManager,
#[Autowire(service: 'html_sanitizer.sanitizer.app.message_sanitizer')]
private readonly HtmlSanitizerInterface $htmlSanitizer,
private readonly HistoryEntryManager $historyEntryManager,
) {
parent::__construct();
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->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;
}
}
21 changes: 20 additions & 1 deletion src/Entity/Suivi.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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');
}
Expand Down Expand Up @@ -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;
}
}
6 changes: 5 additions & 1 deletion src/Manager/SuiviManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
Expand All @@ -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)
Expand Down
12 changes: 12 additions & 0 deletions src/Twig/AppExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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']),
];
}

Expand Down Expand Up @@ -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('&amp;t&#61;___TOKEN___', '/'.$suivi->getSignalement()->getUuid(), $content);
$content = str_replace('?t&#61;___TOKEN___', '/'.$suivi->getSignalement()->getUuid(), $content);
$content = str_replace('?folder&#61;_up', '/'.$suivi->getSignalement()->getUuid().'?variant=resize', $content);

return $content;
}
}
7 changes: 1 addition & 6 deletions templates/back/notifications/index.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,7 @@
({{notification.suivi.signalement.villeOccupant}})
</td>
<td>{{ notification.suivi.createdAt|format_datetime(locale='fr', timezone=territory_timezone, pattern='d MMMM yyyy à HH:mm:ss') }}</td>
<td class="word-wrap-anywhere">{{ 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') }}
</td>
<td class="word-wrap-anywhere">{{ transform_suivi_description(notification.suivi)|raw }}</td>
<td>{{ notification.suivi.createdBy ? notification.suivi.createdBy.nomComplet : notification.signalement.nomOccupant|upper~' '~notification.signalement.prenomOccupant|capitalize }}</td>
<td class="fr-text--right fr-ws-nowrap">
<a href="{{ path('back_signalement_view',{uuid:notification.suivi.signalement.uuid}) }}#suivis"
Expand Down
7 changes: 1 addition & 6 deletions templates/back/signalement/view/suivis.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,7 @@
{% else %}
<div class="fr-col-7 bloc-suivi-content-row fr-pl-3v">
{% 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')
}}
{{ transform_suivi_description(suivi)|raw}}
</div>
<div class="fr-col-2">
{% if suivi.isPublic %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
</strong>
</div>
<div class="message-box-message">
{{ 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 }}
</div>
</div>
{% endfor %}
Expand Down
2 changes: 1 addition & 1 deletion templates/pdf/signalement.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -618,7 +618,7 @@
{% endif %}
<small>{{ suivi.createdAt|date('d/m/Y') }}</small>
</td>
<td style="padding:.5rem;width:85%"> {{ suivi.description|sanitize_html('app.message_sanitizer') }}</td>
<td style="padding:.5rem;width:85%"> {{ suivi.description|raw }}</td>
</tr>
{% endfor %}
</table>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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&#039;y revenir plus tard';
$suivi = $this->suiviRepository->findOneBy(['description' => $description]);

$this->client->request(
Expand Down
37 changes: 30 additions & 7 deletions tests/Functional/Manager/SuiviManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -24,6 +25,8 @@ class SuiviManagerTest extends KernelTestCase
private Security $security;
private UrlGeneratorInterface $urlGenerator;
private DesordreCritereRepository $desordreCritereRepository;
private HtmlSanitizerInterface $htmlSanitizerInterface;
private SuiviManager $suiviManager;

protected function setUp(): void
{
Expand All @@ -33,19 +36,20 @@ 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);
}

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,
$this->security,
$this->desordreCritereRepository,
$this->htmlSanitizerInterface,
Suivi::class,
);
}

public function testCreateSuivi(): void
{
/** @var Signalement $signalement */
$signalement = $this->managerRegistry->getRepository(Signalement::class)->findOneBy(
['reference' => self::REF_SIGNALEMENT]
Expand All @@ -61,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),
Expand All @@ -74,9 +78,28 @@ 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 <br><strong>Non décence</strong><br><strong>Desc. : </strong>Lorem ipsum suivi sit amet, consectetur adipiscing elit.';
$desc = 'Le signalement a été cloturé pour test avec le motif suivant <br /><strong>Non décence</strong><br /><strong>Desc. : </strong>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());
}

public function testCreateSuiviWithImageBase64(): void
{
/** @var Signalement $signalement */
$signalement = $this->managerRegistry->getRepository(Signalement::class)->findOneBy(
['reference' => self::REF_SIGNALEMENT]
);

$desc = 'Salut ma poule <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAApgAAAKYB3X3/OAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAANCSURBVEiJtZZPbBtFFMZ/M7ubXdtdb1xSFyeilBapySVU8h8OoFaooFSqiihIVIpQBKci6KEg9Q6H9kovIHoCIVQJJCKE1ENFjnAgcaSGC6rEnxBwA04Tx43t2FnvDAfjkNibxgHxnWb2e/u992bee7tCa00YFsffekFY+nUzFtjW0LrvjRXrCDIAaPLlW0nHL0SsZtVoaF98mLrx3pdhOqLtYPHChahZcYYO7KvPFxvRl5XPp1sN3adWiD1ZAqD6XYK1b/dvE5IWryTt2udLFedwc1+9kLp+vbbpoDh+6TklxBeAi9TL0taeWpdmZzQDry0AcO+jQ12RyohqqoYoo8RDwJrU+qXkjWtfi8Xxt58BdQuwQs9qC/afLwCw8tnQbqYAPsgxE1S6F3EAIXux2oQFKm0ihMsOF71dHYx+f3NND68ghCu1YIoePPQN1pGRABkJ6Bus96CutRZMydTl+TvuiRW1m3n0eDl0vRPcEysqdXn+jsQPsrHMquGeXEaY4Yk4wxWcY5V/9scqOMOVUFthatyTy8QyqwZ+kDURKoMWxNKr2EeqVKcTNOajqKoBgOE28U4tdQl5p5bwCw7BWquaZSzAPlwjlithJtp3pTImSqQRrb2Z8PHGigD4RZuNX6JYj6wj7O4TFLbCO/Mn/m8R+h6rYSUb3ekokRY6f/YukArN979jcW+V/S8g0eT/N3VN3kTqWbQ428m9/8k0P/1aIhF36PccEl6EhOcAUCrXKZXXWS3XKd2vc/TRBG9O5ELC17MmWubD2nKhUKZa26Ba2+D3P+4/MNCFwg59oWVeYhkzgN/JDR8deKBoD7Y+ljEjGZ0sosXVTvbc6RHirr2reNy1OXd6pJsQ+gqjk8VWFYmHrwBzW/n+uMPFiRwHB2I7ih8ciHFxIkd/3Omk5tCDV1t+2nNu5sxxpDFNx+huNhVT3/zMDz8usXC3ddaHBj1GHj/As08fwTS7Kt1HBTmyN29vdwAw+/wbwLVOJ3uAD1wi/dUH7Qei66PfyuRj4Ik9is+hglfbkbfR3cnZm7chlUWLdwmprtCohX4HUtlOcQjLYCu+fzGJH2QRKvP3UNz8bWk1qMxjGTOMThZ3kvgLI5AzFfo379UAAAAASUVORK5CYII=">';
$descSanitized = 'Salut ma poule ';

$suivi = $this->suiviManager->createSuivi(
signalement : $signalement,
description : $desc,
type : Suivi::TYPE_USAGER
);
$this->assertEquals($descSanitized, $suivi->getDescription());
}
}

0 comments on commit 61d61de

Please sign in to comment.