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('/]*>/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 = Base::instance()->encode($item['alt']); } $url = Base::instance()->encode($item['url']); return '' . $alt . ''; }, $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('/]*>/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 ''; }, $html) ?? $html; } }