Files
f3-simple-blog/app/Services/MarkdownService.php
2026-03-28 17:37:22 +01:00

85 lines
2.8 KiB
PHP

<?php
declare(strict_types=1);
class MarkdownService extends Prefab
{
private const ALLOWED_TAGS = [
'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'ul', 'ol', 'li', 'blockquote', 'pre', 'code',
'strong', 'em', 'a', 'img', 'hr', 'br',
];
public function compile(string $markdown, Media $media): string
{
$markdown = trim($markdown);
if ($markdown === '') {
throw new RuntimeException('Ajoute du contenu avant de publier.');
}
$html = Markdown::instance()->convert($markdown);
$html = strip_tags($html, self::ALLOWED_TAGS);
$html = self::resolveImages($html, $media);
$html = self::secureLinks($html);
return trim($html);
}
// Résout les images media:filename et supprime les images externes.
private static function resolveImages(string $html, Media $media): string
{
return preg_replace_callback('/<img\s[^>]*>/i', function (array $m) use ($media): string {
if (!preg_match('/src="([^"]*)"/', $m[0], $s) || !str_starts_with($s[1], 'media:')) {
return '';
}
$fileName = substr($s[1], 6);
if ($fileName === '') {
return '';
}
$item = $media->findByFileName($fileName);
if ($item === null) {
throw new RuntimeException('Une image utilisée dans le Markdown est introuvable.');
}
// L'alt du Markdown est déjà échappé par le parser F3.
// Le fallback vers l'alt de la base nécessite un échappement.
$alt = '';
if (preg_match('/alt="([^"]*)"/', $m[0], $a)) {
$alt = $a[1];
}
if ($alt === '') {
$alt = htmlspecialchars($item['alt'], ENT_QUOTES, 'UTF-8');
}
$url = htmlspecialchars($item['url'], ENT_QUOTES, 'UTF-8');
return '<img src="' . $url . '" alt="' . $alt . '" loading="lazy" decoding="async">';
}, $html) ?? $html;
}
// Sécurise les liens : rel="noopener noreferrer" sur tous,
// target="_blank" sur les liens externes uniquement.
private static function secureLinks(string $html): string
{
return preg_replace_callback('/<a\s[^>]*>/i', function (array $m): string {
if (!preg_match('/href="([^"]*)"/', $m[0], $h)) {
return $m[0];
}
$attrs = 'href="' . $h[1] . '" rel="noopener noreferrer"';
if (preg_match('~^https?://~i', $h[1])) {
$attrs .= ' target="_blank"';
}
if (preg_match('/title="([^"]*)"/', $m[0], $t)) {
$attrs .= ' title="' . $t[1] . '"';
}
return '<a ' . $attrs . '>';
}, $html) ?? $html;
}
}