src/Services/Core/ImageExtractorService.php line 268

Open in your IDE?
  1. <?php
  2. namespace App\Services\Core;
  3. use App\Entity\Core\Uploads;
  4. use App\Services\Core\ExternalImageDownloader;
  5. use Doctrine\ORM\EntityManagerInterface;
  6. use Symfony\Component\HttpFoundation\RequestStack;
  7. use Vich\UploaderBundle\Templating\Helper\UploaderHelper;
  8. class ImageExtractorService
  9. {
  10. private ExternalImageDownloader $imageDownloader;
  11. private EntityManagerInterface $entityManager;
  12. private string $currentDomain;
  13. private UploaderHelper $uploaderHelper;
  14. public function __construct(
  15. ExternalImageDownloader $imageDownloader,
  16. EntityManagerInterface $entityManager,
  17. RequestStack $requestStack = null,
  18. UploaderHelper $uploaderHelper = null
  19. ) {
  20. $this->imageDownloader = $imageDownloader;
  21. $this->entityManager = $entityManager;
  22. $this->uploaderHelper = $uploaderHelper;
  23. // Déterminer le domaine actuel
  24. $request = $requestStack?->getCurrentRequest();
  25. if ($request) {
  26. $this->currentDomain = $request->getSchemeAndHttpHost();
  27. } else {
  28. // Fallback - vous devez définir votre domaine
  29. $this->currentDomain = $_ENV['APP_URL'] ?? 'https://hiringnotes.com';
  30. }
  31. }
  32. /**
  33. * Extrait toutes les URLs d'images externes d'un texte HTML/markdown
  34. * et les remplace par des liens vers les images uploadées
  35. */
  36. public function processImagesInText(string $text): string
  37. {
  38. // Regex améliorées pour trouver les URLs d'images dans différents formats
  39. $patterns = [
  40. // Balises img HTML avec tous types d'attributs: <img src="url" /> ou <img alt="..." src="url" class="...">
  41. '/<img[^>]*src\s*=\s*["\']([^"\']+)["\'][^>]*>/i',
  42. // Markdown: ![alt](url)
  43. '/!\[([^\]]*)\]\(([^)]+)\)/',
  44. // URLs directes d'images (avec ou sans extension)
  45. '/https?:\/\/[^\s<>"]+\.(?:jpg|jpeg|png|gif|webp|svg|bmp)(?:\?[^\s<>"]*)?/i',
  46. // URLs de CDN ou services d'images (sans extension évidente)
  47. '/https?:\/\/[^\s<>"]*(?:cdn|image|img|photo)[^\s<>"]*(?:\?[^\s<>"]*)?/i'
  48. ];
  49. $processedText = $text;
  50. foreach ($patterns as $pattern) {
  51. $processedText = preg_replace_callback($pattern, function($matches) use ($text) {
  52. return $this->processImageMatch($matches);
  53. }, $processedText);
  54. }
  55. return $processedText;
  56. }
  57. /**
  58. * Traite une correspondance d'image trouvée dans le texte
  59. */
  60. private function processImageMatch(array $matches): string
  61. {
  62. // Identifier l'URL selon le pattern
  63. if (str_contains($matches[0], '<img')) {
  64. // Balise HTML img
  65. $imageUrl = $matches[1];
  66. $fullMatch = $matches[0];
  67. } elseif (str_contains($matches[0], '![')) {
  68. // Format Markdown
  69. $imageUrl = $matches[2];
  70. $altText = $matches[1];
  71. $fullMatch = $matches[0];
  72. } else {
  73. // URL directe
  74. $imageUrl = $matches[0];
  75. $fullMatch = $matches[0];
  76. }
  77. // Vérifier si c'est une image externe
  78. if (!$this->isExternalImage($imageUrl)) {
  79. return $fullMatch; // Retourner tel quel si pas externe
  80. }
  81. // Télécharger et uploader l'image
  82. $uploadedFile = $this->uploadExternalImage($imageUrl);
  83. if (!$uploadedFile) {
  84. return $fullMatch; // Retourner tel quel si échec
  85. }
  86. // Remplacer l'URL dans le texte original
  87. if (str_contains($fullMatch, '<img')) {
  88. // Remplacer dans la balise HTML
  89. return str_replace($imageUrl, $this->getUploadedImageUrl($uploadedFile), $fullMatch);
  90. } elseif (str_contains($fullMatch, '![')) {
  91. // Remplacer dans le markdown
  92. return str_replace($imageUrl, $this->getUploadedImageUrl($uploadedFile), $fullMatch);
  93. } else {
  94. // URL directe
  95. return $this->getUploadedImageUrl($uploadedFile);
  96. }
  97. }
  98. /**
  99. * Upload une image externe et retourne l'entité Uploads
  100. */
  101. public function uploadExternalImage(string $imageUrl, string $title = null, string $description = null): ?Uploads
  102. {
  103. try {
  104. // Télécharger l'image
  105. $downloadedFile = $this->imageDownloader->downloadImageFromUrl($imageUrl);
  106. if (!$downloadedFile) {
  107. error_log("Impossible de télécharger l'image: " . $imageUrl);
  108. return null;
  109. }
  110. // Créer une nouvelle entité Uploads
  111. $upload = new Uploads();
  112. $upload->setImageFile($downloadedFile);
  113. // Définir titre et description
  114. if ($title) {
  115. $upload->setTitle($title);
  116. } else {
  117. // Générer un titre depuis l'URL
  118. $basename = pathinfo(parse_url($imageUrl, PHP_URL_PATH), PATHINFO_FILENAME);
  119. $upload->setTitle($basename ?: 'Image externe');
  120. }
  121. if ($description) {
  122. $upload->setDescription($description);
  123. } else {
  124. $upload->setDescription('Image téléchargée depuis: ' . $imageUrl);
  125. }
  126. // Sauvegarder en base
  127. $this->entityManager->persist($upload);
  128. $this->entityManager->flush();
  129. error_log("Image uploadée avec succès: " . $imageUrl . " -> ID: " . $upload->getId());
  130. return $upload;
  131. } catch (\Exception $e) {
  132. error_log("Erreur lors de l'upload de l'image " . $imageUrl . ": " . $e->getMessage());
  133. return null;
  134. }
  135. }
  136. /**
  137. * Vérifie si une URL d'image est externe au domaine actuel
  138. */
  139. private function isExternalImage(string $url): bool
  140. {
  141. // Vérifier si c'est une URL absolue
  142. if (!filter_var($url, FILTER_VALIDATE_URL)) {
  143. return false; // URL relative ou invalide
  144. }
  145. // Vérifier si c'est un domaine externe
  146. $urlHost = parse_url($url, PHP_URL_HOST);
  147. $currentHost = parse_url($this->currentDomain, PHP_URL_HOST);
  148. if ($urlHost === $currentHost) {
  149. return false; // Même domaine
  150. }
  151. // Vérifier si c'est une image par extension
  152. $path = parse_url($url, PHP_URL_PATH);
  153. $extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
  154. $imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp'];
  155. if (in_array($extension, $imageExtensions)) {
  156. return true;
  157. }
  158. // Vérifier si l'URL contient des indices d'être une image (CDN, etc.)
  159. if ($this->isLikelyImageUrl($url)) {
  160. return true;
  161. }
  162. return false;
  163. }
  164. /**
  165. * Détermine si une URL est probablement une image même sans extension
  166. */
  167. private function isLikelyImageUrl(string $url): bool
  168. {
  169. // Patterns pour détecter les CDN d'images et autres services
  170. $imageUrlPatterns = [
  171. // CDN génériques
  172. '/cdn.*\.(jpg|jpeg|png|gif|webp|svg|bmp)/i',
  173. '/.*cdn.*\?/i', // URLs avec cdn et paramètres de query
  174. // Services spécifiques
  175. '/neuroncdn\.com/i',
  176. '/cloudfront\.net/i',
  177. '/amazonaws\.com.*\.(jpg|jpeg|png|gif|webp|svg|bmp)/i',
  178. '/imgur\.com/i',
  179. '/cloudinary\.com/i',
  180. '/unsplash\.com/i',
  181. '/pexels\.com/i',
  182. // Patterns avec hash/ID (comme votre exemple)
  183. '/\/[a-f0-9]{32,}/i', // Hash MD5 ou plus long
  184. '/\/[a-f0-9]{8,}\?/i', // Hash avec paramètres
  185. // Patterns avec paramètres d'image
  186. '/[\?&](w|width|h|height|size|format|quality)=/i',
  187. '/[\?&]ts=\d+/i', // timestamp comme dans votre exemple
  188. // Domaines d'images connus
  189. '/\.(imagekit|imgix|fastly|keycdn|maxcdn)\..*/',
  190. ];
  191. foreach ($imageUrlPatterns as $pattern) {
  192. if (preg_match($pattern, $url)) {
  193. return true;
  194. }
  195. }
  196. return false;
  197. }
  198. /**
  199. * Génère l'URL publique d'une image uploadée
  200. */
  201. private function getUploadedImageUrl(Uploads $upload): string
  202. {
  203. // Utiliser VichUploaderHelper si disponible
  204. if ($this->uploaderHelper) {
  205. try {
  206. $url = $this->uploaderHelper->asset($upload, 'imageFile');
  207. if ($url) {
  208. return $url;
  209. }
  210. } catch (\Exception $e) {
  211. error_log('Erreur UploaderHelper: ' . $e->getMessage());
  212. }
  213. }
  214. // Fallback manuel avec EmbeddedFile
  215. $image = $upload->getImage();
  216. if ($image && $image->getName()) {
  217. // Adapter le chemin selon votre configuration vich_uploader.yaml
  218. // Le mapping "uploads_files" doit correspondre à votre configuration
  219. return '/ups/' . $image->getName();
  220. }
  221. // Fallback si pas d'image
  222. return '';
  223. }
  224. /**
  225. * Upload une image externe et retourne l'URL locale
  226. */
  227. public function uploadExternalImageAndGetUrl(string $imageUrl, string $title = null, string $description = null): ?string
  228. {
  229. $uploadedImage = $this->uploadExternalImage($imageUrl, $title, $description);
  230. if ($uploadedImage) {
  231. // Forcer le flush pour que l'EmbeddedFile soit bien persisté
  232. $this->entityManager->flush();
  233. return $this->getUploadedImageUrl($uploadedImage);
  234. }
  235. return null;
  236. }
  237. /**
  238. * Extrait uniquement les URLs d'images externes sans les traiter
  239. */
  240. public function extractExternalImageUrls(string $text): array
  241. {
  242. $urls = [];
  243. $patterns = [
  244. // Balises img HTML avec tous types d'attributs
  245. '/<img[^>]*src\s*=\s*["\']([^"\']+)["\'][^>]*>/i',
  246. // Markdown: ![alt](url)
  247. '/!\[([^\]]*)\]\(([^)]+)\)/',
  248. // URLs directes d'images (avec extension)
  249. '/https?:\/\/[^\s<>"]+\.(?:jpg|jpeg|png|gif|webp|svg|bmp)(?:\?[^\s<>"]*)?/i',
  250. // URLs de CDN ou services d'images (sans extension évidente)
  251. '/https?:\/\/[^\s<>"]*(?:cdn|image|img|photo)[^\s<>"]*(?:\?[^\s<>"]*)?/i'
  252. ];
  253. foreach ($patterns as $patternIndex => $pattern) {
  254. preg_match_all($pattern, $text, $matches);
  255. if ($patternIndex === 0) {
  256. // Balises img HTML
  257. $foundUrls = $matches[1];
  258. } elseif ($patternIndex === 1) {
  259. // Markdown
  260. $foundUrls = $matches[2];
  261. } else {
  262. // URLs directes
  263. $foundUrls = $matches[0];
  264. }
  265. foreach ($foundUrls as $url) {
  266. if ($this->isExternalImage($url)) {
  267. $urls[] = $url;
  268. }
  269. }
  270. }
  271. return array_unique($urls);
  272. }
  273. }