85 lines
2.8 KiB
PHP
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;
|
|
}
|
|
}
|