<?phpnamespace App\Services\Core;use App\Entity\Core\Uploads;use App\Services\Core\ExternalImageDownloader;use Doctrine\ORM\EntityManagerInterface;use Symfony\Component\HttpFoundation\RequestStack;use Vich\UploaderBundle\Templating\Helper\UploaderHelper;class ImageExtractorService{ private ExternalImageDownloader $imageDownloader; private EntityManagerInterface $entityManager; private string $currentDomain; private UploaderHelper $uploaderHelper; public function __construct( ExternalImageDownloader $imageDownloader, EntityManagerInterface $entityManager, RequestStack $requestStack = null, UploaderHelper $uploaderHelper = null ) { $this->imageDownloader = $imageDownloader; $this->entityManager = $entityManager; $this->uploaderHelper = $uploaderHelper; // Déterminer le domaine actuel $request = $requestStack?->getCurrentRequest(); if ($request) { $this->currentDomain = $request->getSchemeAndHttpHost(); } else { // Fallback - vous devez définir votre domaine $this->currentDomain = $_ENV['APP_URL'] ?? 'https://hiringnotes.com'; } } /** * Extrait toutes les URLs d'images externes d'un texte HTML/markdown * et les remplace par des liens vers les images uploadées */ public function processImagesInText(string $text): string { // Regex améliorées pour trouver les URLs d'images dans différents formats $patterns = [ // Balises img HTML avec tous types d'attributs: <img src="url" /> ou <img alt="..." src="url" class="..."> '/<img[^>]*src\s*=\s*["\']([^"\']+)["\'][^>]*>/i', // Markdown:  '/!\[([^\]]*)\]\(([^)]+)\)/', // URLs directes d'images (avec ou sans extension) '/https?:\/\/[^\s<>"]+\.(?:jpg|jpeg|png|gif|webp|svg|bmp)(?:\?[^\s<>"]*)?/i', // URLs de CDN ou services d'images (sans extension évidente) '/https?:\/\/[^\s<>"]*(?:cdn|image|img|photo)[^\s<>"]*(?:\?[^\s<>"]*)?/i' ]; $processedText = $text; foreach ($patterns as $pattern) { $processedText = preg_replace_callback($pattern, function($matches) use ($text) { return $this->processImageMatch($matches); }, $processedText); } return $processedText; } /** * Traite une correspondance d'image trouvée dans le texte */ private function processImageMatch(array $matches): string { // Identifier l'URL selon le pattern if (str_contains($matches[0], '<img')) { // Balise HTML img $imageUrl = $matches[1]; $fullMatch = $matches[0]; } elseif (str_contains($matches[0], '![')) { // Format Markdown $imageUrl = $matches[2]; $altText = $matches[1]; $fullMatch = $matches[0]; } else { // URL directe $imageUrl = $matches[0]; $fullMatch = $matches[0]; } // Vérifier si c'est une image externe if (!$this->isExternalImage($imageUrl)) { return $fullMatch; // Retourner tel quel si pas externe } // Télécharger et uploader l'image $uploadedFile = $this->uploadExternalImage($imageUrl); if (!$uploadedFile) { return $fullMatch; // Retourner tel quel si échec } // Remplacer l'URL dans le texte original if (str_contains($fullMatch, '<img')) { // Remplacer dans la balise HTML return str_replace($imageUrl, $this->getUploadedImageUrl($uploadedFile), $fullMatch); } elseif (str_contains($fullMatch, '![')) { // Remplacer dans le markdown return str_replace($imageUrl, $this->getUploadedImageUrl($uploadedFile), $fullMatch); } else { // URL directe return $this->getUploadedImageUrl($uploadedFile); } } /** * Upload une image externe et retourne l'entité Uploads */ public function uploadExternalImage(string $imageUrl, string $title = null, string $description = null): ?Uploads { try { // Télécharger l'image $downloadedFile = $this->imageDownloader->downloadImageFromUrl($imageUrl); if (!$downloadedFile) { error_log("Impossible de télécharger l'image: " . $imageUrl); return null; } // Créer une nouvelle entité Uploads $upload = new Uploads(); $upload->setImageFile($downloadedFile); // Définir titre et description if ($title) { $upload->setTitle($title); } else { // Générer un titre depuis l'URL $basename = pathinfo(parse_url($imageUrl, PHP_URL_PATH), PATHINFO_FILENAME); $upload->setTitle($basename ?: 'Image externe'); } if ($description) { $upload->setDescription($description); } else { $upload->setDescription('Image téléchargée depuis: ' . $imageUrl); } // Sauvegarder en base $this->entityManager->persist($upload); $this->entityManager->flush(); error_log("Image uploadée avec succès: " . $imageUrl . " -> ID: " . $upload->getId()); return $upload; } catch (\Exception $e) { error_log("Erreur lors de l'upload de l'image " . $imageUrl . ": " . $e->getMessage()); return null; } } /** * Vérifie si une URL d'image est externe au domaine actuel */ private function isExternalImage(string $url): bool { // Vérifier si c'est une URL absolue if (!filter_var($url, FILTER_VALIDATE_URL)) { return false; // URL relative ou invalide } // Vérifier si c'est un domaine externe $urlHost = parse_url($url, PHP_URL_HOST); $currentHost = parse_url($this->currentDomain, PHP_URL_HOST); if ($urlHost === $currentHost) { return false; // Même domaine } // Vérifier si c'est une image par extension $path = parse_url($url, PHP_URL_PATH); $extension = strtolower(pathinfo($path, PATHINFO_EXTENSION)); $imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp']; if (in_array($extension, $imageExtensions)) { return true; } // Vérifier si l'URL contient des indices d'être une image (CDN, etc.) if ($this->isLikelyImageUrl($url)) { return true; } return false; } /** * Détermine si une URL est probablement une image même sans extension */ private function isLikelyImageUrl(string $url): bool { // Patterns pour détecter les CDN d'images et autres services $imageUrlPatterns = [ // CDN génériques '/cdn.*\.(jpg|jpeg|png|gif|webp|svg|bmp)/i', '/.*cdn.*\?/i', // URLs avec cdn et paramètres de query // Services spécifiques '/neuroncdn\.com/i', '/cloudfront\.net/i', '/amazonaws\.com.*\.(jpg|jpeg|png|gif|webp|svg|bmp)/i', '/imgur\.com/i', '/cloudinary\.com/i', '/unsplash\.com/i', '/pexels\.com/i', // Patterns avec hash/ID (comme votre exemple) '/\/[a-f0-9]{32,}/i', // Hash MD5 ou plus long '/\/[a-f0-9]{8,}\?/i', // Hash avec paramètres // Patterns avec paramètres d'image '/[\?&](w|width|h|height|size|format|quality)=/i', '/[\?&]ts=\d+/i', // timestamp comme dans votre exemple // Domaines d'images connus '/\.(imagekit|imgix|fastly|keycdn|maxcdn)\..*/', ]; foreach ($imageUrlPatterns as $pattern) { if (preg_match($pattern, $url)) { return true; } } return false; } /** * Génère l'URL publique d'une image uploadée */ private function getUploadedImageUrl(Uploads $upload): string { // Utiliser VichUploaderHelper si disponible if ($this->uploaderHelper) { try { $url = $this->uploaderHelper->asset($upload, 'imageFile'); if ($url) { return $url; } } catch (\Exception $e) { error_log('Erreur UploaderHelper: ' . $e->getMessage()); } } // Fallback manuel avec EmbeddedFile $image = $upload->getImage(); if ($image && $image->getName()) { // Adapter le chemin selon votre configuration vich_uploader.yaml // Le mapping "uploads_files" doit correspondre à votre configuration return '/ups/' . $image->getName(); } // Fallback si pas d'image return ''; } /** * Upload une image externe et retourne l'URL locale */ public function uploadExternalImageAndGetUrl(string $imageUrl, string $title = null, string $description = null): ?string { $uploadedImage = $this->uploadExternalImage($imageUrl, $title, $description); if ($uploadedImage) { // Forcer le flush pour que l'EmbeddedFile soit bien persisté $this->entityManager->flush(); return $this->getUploadedImageUrl($uploadedImage); } return null; } /** * Extrait uniquement les URLs d'images externes sans les traiter */ public function extractExternalImageUrls(string $text): array { $urls = []; $patterns = [ // Balises img HTML avec tous types d'attributs '/<img[^>]*src\s*=\s*["\']([^"\']+)["\'][^>]*>/i', // Markdown:  '/!\[([^\]]*)\]\(([^)]+)\)/', // URLs directes d'images (avec extension) '/https?:\/\/[^\s<>"]+\.(?:jpg|jpeg|png|gif|webp|svg|bmp)(?:\?[^\s<>"]*)?/i', // URLs de CDN ou services d'images (sans extension évidente) '/https?:\/\/[^\s<>"]*(?:cdn|image|img|photo)[^\s<>"]*(?:\?[^\s<>"]*)?/i' ]; foreach ($patterns as $patternIndex => $pattern) { preg_match_all($pattern, $text, $matches); if ($patternIndex === 0) { // Balises img HTML $foundUrls = $matches[1]; } elseif ($patternIndex === 1) { // Markdown $foundUrls = $matches[2]; } else { // URLs directes $foundUrls = $matches[0]; } foreach ($foundUrls as $url) { if ($this->isExternalImage($url)) { $urls[] = $url; } } } return array_unique($urls); }}