Various corrections
This commit is contained in:
@@ -16,7 +16,8 @@
|
|||||||
"twig/twig": "*",
|
"twig/twig": "*",
|
||||||
"slim/twig-view": "^3.4",
|
"slim/twig-view": "^3.4",
|
||||||
"catfan/medoo": "2.*",
|
"catfan/medoo": "2.*",
|
||||||
"vlucas/phpdotenv": "^5.6"
|
"vlucas/phpdotenv": "^5.6",
|
||||||
|
"ezyang/htmlpurifier": "^4.19"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
|
|||||||
@@ -71,6 +71,9 @@ $db->pdo->exec("
|
|||||||
)
|
)
|
||||||
");
|
");
|
||||||
|
|
||||||
|
// Sanitizer HTML
|
||||||
|
$htmlSanitizer = new \App\Services\HtmlSanitizer();
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// Slim App
|
// Slim App
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -83,7 +86,7 @@ $app->add(TwigMiddleware::create($app, $twig));
|
|||||||
// Routes
|
// Routes
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
$controller = new PostController($twig, $db);
|
$controller = new PostController($twig, $db, $htmlSanitizer);
|
||||||
|
|
||||||
$app->get('/', [$controller, 'index']);
|
$app->get('/', [$controller, 'index']);
|
||||||
$app->get('/article/{slug}', [$controller, 'show']);
|
$app->get('/article/{slug}', [$controller, 'show']);
|
||||||
|
|||||||
@@ -15,8 +15,11 @@ use App\Models\Post;
|
|||||||
*/
|
*/
|
||||||
class PostController
|
class PostController
|
||||||
{
|
{
|
||||||
public function __construct(private Twig $view, private Medoo $db)
|
public function __construct(
|
||||||
{
|
private Twig $view,
|
||||||
|
private Medoo $db,
|
||||||
|
private \App\Services\HtmlSanitizer $sanitizer
|
||||||
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -36,7 +39,6 @@ class PostController
|
|||||||
public function show(Request $req, Response $res, array $args): Response
|
public function show(Request $req, Response $res, array $args): Response
|
||||||
{
|
{
|
||||||
$slug = (string)($args['slug'] ?? '');
|
$slug = (string)($args['slug'] ?? '');
|
||||||
|
|
||||||
$row = $this->db->get('post', '*', ['slug' => $slug]);
|
$row = $this->db->get('post', '*', ['slug' => $slug]);
|
||||||
|
|
||||||
if (!$row) {
|
if (!$row) {
|
||||||
@@ -45,6 +47,9 @@ class PostController
|
|||||||
}
|
}
|
||||||
|
|
||||||
$post = Post::fromArray($row);
|
$post = Post::fromArray($row);
|
||||||
|
// Nettoyer le contenu avant de l'envoyer au template
|
||||||
|
$post = $post->withSanitizedContent($this->sanitizer->sanitize($post->getContent()));
|
||||||
|
|
||||||
return $this->view->render($res, 'pages/post_detail.twig', ['post' => $post]);
|
return $this->view->render($res, 'pages/post_detail.twig', ['post' => $post]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,12 +114,19 @@ class PostController
|
|||||||
return $res->withHeader('Location', '/admin')->withStatus(302);
|
return $res->withHeader('Location', '/admin')->withStatus(302);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function ensureUniqueSlug(string $slug): string
|
private function ensureUniqueSlug(string $slug, ?int $excludeId = null): string
|
||||||
{
|
{
|
||||||
$original = $slug;
|
$original = $slug;
|
||||||
$counter = 1;
|
$counter = 1;
|
||||||
|
|
||||||
while ($this->db->get('post', 'id', ['slug' => $slug])) {
|
while (true) {
|
||||||
|
$existing = $this->db->get('post', 'id', ['slug' => $slug]);
|
||||||
|
|
||||||
|
// Si pas de collision, ou si c'est le même post, le slug est unique
|
||||||
|
if (!$existing || ($excludeId && $existing === $excludeId)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
$slug = $original . '-' . $counter;
|
$slug = $original . '-' . $counter;
|
||||||
$counter++;
|
$counter++;
|
||||||
}
|
}
|
||||||
@@ -147,10 +159,15 @@ class PostController
|
|||||||
return $res->withHeader('Location', '/admin')->withStatus(302);
|
return $res->withHeader('Location', '/admin')->withStatus(302);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculer le nouveau slug
|
||||||
|
$newSlug = $post->getSlug();
|
||||||
|
$newSlug = $this->ensureUniqueSlug($newSlug, $id); // Passer l'ID pour exclure le post actuel
|
||||||
|
|
||||||
// Persister en DB
|
// Persister en DB
|
||||||
$this->db->update('post', [
|
$this->db->update('post', [
|
||||||
'title' => $post->getTitle(),
|
'title' => $post->getTitle(),
|
||||||
'content' => $post->getContent(),
|
'content' => $post->getContent(),
|
||||||
|
'slug' => $newSlug,
|
||||||
'updated_at' => date('Y-m-d H:i:s'),
|
'updated_at' => date('Y-m-d H:i:s'),
|
||||||
], ['id' => $id]);
|
], ['id' => $id]);
|
||||||
|
|
||||||
|
|||||||
@@ -118,11 +118,15 @@ final class Post
|
|||||||
public function getExcerpt(int $length = 150): string
|
public function getExcerpt(int $length = 150): string
|
||||||
{
|
{
|
||||||
$text = strip_tags($this->content);
|
$text = strip_tags($this->content);
|
||||||
return mb_strlen($text) > $length
|
$excerpt = mb_strlen($text) > $length
|
||||||
? mb_substr($text, 0, $length) . '...'
|
? mb_substr($text, 0, $length) . '...'
|
||||||
: $text;
|
: $text;
|
||||||
|
|
||||||
|
// Échapper les caractères HTML restants
|
||||||
|
return htmlspecialchars($excerpt, ENT_QUOTES, 'UTF-8');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retourne un slug (URL-friendly) du titre.
|
* Retourne un slug (URL-friendly) du titre.
|
||||||
*
|
*
|
||||||
@@ -147,4 +151,10 @@ final class Post
|
|||||||
'content' => $this->content,
|
'content' => $this->content,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function withSanitizedContent(string $content): self
|
||||||
|
{
|
||||||
|
return new self($this->id, $this->title, $content, $this->createdAt, $this->updatedAt);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
32
src/Services/HtmlSanitizer.php
Normal file
32
src/Services/HtmlSanitizer.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use HTMLPurifier;
|
||||||
|
use HTMLPurifier_Config;
|
||||||
|
|
||||||
|
final class HtmlSanitizer
|
||||||
|
{
|
||||||
|
private HTMLPurifier $purifier;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$config = HTMLPurifier_Config::createDefault();
|
||||||
|
// Autoriser les balises courantes de formatage
|
||||||
|
$config->set('HTML.Allowed', 'p,br,strong,em,u,h1,h2,h3,h4,h5,h6,ul,ol,li,blockquote,a[href],img[src|alt|width|height]');
|
||||||
|
// Désactiver les attributs dangereux
|
||||||
|
$config->set('HTML.AllowedAttributes', 'href,src,alt,width,height,title');
|
||||||
|
// Activer le cache
|
||||||
|
$config->set('Cache.DefinitionImpl', 'Serializer');
|
||||||
|
$config->set('Cache.SerializerPath', __DIR__ . '/../../var/cache/htmlpurifier');
|
||||||
|
|
||||||
|
$this->purifier = new HTMLPurifier($config);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sanitize(string $html): string
|
||||||
|
{
|
||||||
|
return $this->purifier->purify($html);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="post-content">
|
<div class="post-content">
|
||||||
{{ post.content|raw }}
|
{{ post.content }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|||||||
Reference in New Issue
Block a user