vendor/shopware/core/Content/Media/Subscriber/MediaDeletionSubscriber.php line 91

  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\Content\Media\Subscriber;
  3. use Doctrine\DBAL\ArrayParameterType;
  4. use Doctrine\DBAL\Connection;
  5. use League\Flysystem\Visibility;
  6. use Shopware\Core\Content\Media\Aggregate\MediaFolder\MediaFolderDefinition;
  7. use Shopware\Core\Content\Media\Aggregate\MediaThumbnail\MediaThumbnailCollection;
  8. use Shopware\Core\Content\Media\Aggregate\MediaThumbnail\MediaThumbnailDefinition;
  9. use Shopware\Core\Content\Media\Event\MediaThumbnailDeletedEvent;
  10. use Shopware\Core\Content\Media\MediaDefinition;
  11. use Shopware\Core\Content\Media\MediaEntity;
  12. use Shopware\Core\Content\Media\Message\DeleteFileHandler;
  13. use Shopware\Core\Content\Media\Message\DeleteFileMessage;
  14. use Shopware\Core\Content\Media\Pathname\UrlGeneratorInterface;
  15. use Shopware\Core\Framework\Context;
  16. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
  17. use Shopware\Core\Framework\DataAbstractionLayer\Event\BeforeDeleteEvent;
  18. use Shopware\Core\Framework\DataAbstractionLayer\Event\EntitySearchedEvent;
  19. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  20. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;
  21. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  22. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\MultiFilter;
  23. use Shopware\Core\Framework\Log\Package;
  24. use Shopware\Core\Framework\Uuid\Uuid;
  25. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  26. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  27. use Symfony\Component\Messenger\MessageBusInterface;
  28. /**
  29.  * @internal
  30.  */
  31. #[Package('content')]
  32. class MediaDeletionSubscriber implements EventSubscriberInterface
  33. {
  34.     final public const SYNCHRONE_FILE_DELETE 'synchrone-file-delete';
  35.     /**
  36.      * @internal
  37.      */
  38.     public function __construct(
  39.         private readonly UrlGeneratorInterface $urlGenerator,
  40.         private readonly EventDispatcherInterface $dispatcher,
  41.         private readonly EntityRepository $thumbnailRepository,
  42.         private readonly MessageBusInterface $messageBus,
  43.         private readonly DeleteFileHandler $deleteFileHandler,
  44.         private readonly Connection $connection,
  45.         private readonly EntityRepository $mediaRepository
  46.     ) {
  47.     }
  48.     public static function getSubscribedEvents(): array
  49.     {
  50.         return [
  51.             BeforeDeleteEvent::class => 'beforeDelete',
  52.             EntitySearchedEvent::class => 'securePrivateFolders',
  53.         ];
  54.     }
  55.     public function securePrivateFolders(EntitySearchedEvent $event): void
  56.     {
  57.         if ($event->getContext()->getScope() === Context::SYSTEM_SCOPE) {
  58.             return;
  59.         }
  60.         if ($event->getDefinition()->getEntityName() === MediaFolderDefinition::ENTITY_NAME) {
  61.             $event->getCriteria()->addFilter(
  62.                 new MultiFilter('OR', [
  63.                     new EqualsFilter('media_folder.configuration.private'false),
  64.                     new EqualsFilter('media_folder.configuration.private'null),
  65.                 ])
  66.             );
  67.             return;
  68.         }
  69.         if ($event->getDefinition()->getEntityName() === MediaDefinition::ENTITY_NAME) {
  70.             $event->getCriteria()->addFilter(
  71.                 new MultiFilter('OR', [
  72.                     new EqualsFilter('private'false),
  73.                     new MultiFilter('AND', [
  74.                         new EqualsFilter('private'true),
  75.                         new EqualsFilter('mediaFolder.defaultFolder.entity''product_download'),
  76.                     ]),
  77.                 ])
  78.             );
  79.         }
  80.     }
  81.     public function beforeDelete(BeforeDeleteEvent $event): void
  82.     {
  83.         $affected array_values($event->getIds(MediaThumbnailDefinition::ENTITY_NAME));
  84.         if (!empty($affected)) {
  85.             $this->handleThumbnailDeletion($event$affected$event->getContext());
  86.         }
  87.         $affected array_values($event->getIds(MediaFolderDefinition::ENTITY_NAME));
  88.         if (!empty($affected)) {
  89.             $this->handleFolderDeletion($affected$event->getContext());
  90.         }
  91.         $affected array_values($event->getIds(MediaDefinition::ENTITY_NAME));
  92.         if (!empty($affected)) {
  93.             $this->handleMediaDeletion($affected$event->getContext());
  94.         }
  95.     }
  96.     /**
  97.      * @param list<string> $affected
  98.      */
  99.     private function handleMediaDeletion(array $affectedContext $context): void
  100.     {
  101.         $media $context->scope(Context::SYSTEM_SCOPE, fn (Context $context) => $this->mediaRepository->search(new Criteria($affected), $context));
  102.         $privatePaths = [];
  103.         $publicPaths = [];
  104.         $thumbnails = [];
  105.         /** @var MediaEntity $mediaEntity */
  106.         foreach ($media as $mediaEntity) {
  107.             if (!$mediaEntity->hasFile()) {
  108.                 continue;
  109.             }
  110.             if ($mediaEntity->isPrivate()) {
  111.                 $privatePaths[] = $this->urlGenerator->getRelativeMediaUrl($mediaEntity);
  112.             } else {
  113.                 $publicPaths[] = $this->urlGenerator->getRelativeMediaUrl($mediaEntity);
  114.             }
  115.             if (!$mediaEntity->getThumbnails()) {
  116.                 continue;
  117.             }
  118.             foreach ($mediaEntity->getThumbnails()->getIds() as $id) {
  119.                 $thumbnails[] = ['id' => $id];
  120.             }
  121.         }
  122.         $this->performFileDelete($context$publicPathsVisibility::PUBLIC);
  123.         $this->performFileDelete($context$privatePathsVisibility::PRIVATE);
  124.         $this->thumbnailRepository->delete($thumbnails$context);
  125.     }
  126.     /**
  127.      * @param list<string> $affected
  128.      */
  129.     private function handleFolderDeletion(array $affectedContext $context): void
  130.     {
  131.         $ids $this->fetchChildrenIds($affected);
  132.         if (empty($ids)) {
  133.             return;
  134.         }
  135.         $media $this->connection->fetchAllAssociative(
  136.             'SELECT LOWER(HEX(id)) as id FROM media WHERE media_folder_id IN (:ids)',
  137.             ['ids' => Uuid::fromHexToBytesList($ids)],
  138.             ['ids' => ArrayParameterType::STRING]
  139.         );
  140.         if (empty($media)) {
  141.             return;
  142.         }
  143.         $this->mediaRepository->delete($media$context);
  144.     }
  145.     /**
  146.      * @param list<string> $ids
  147.      *
  148.      * @return list<string>
  149.      */
  150.     private function fetchChildrenIds(array $ids): array
  151.     {
  152.         $children $this->connection->fetchFirstColumn(
  153.             'SELECT LOWER(HEX(id)) FROM media_folder WHERE parent_id IN (:ids)',
  154.             ['ids' => Uuid::fromHexToBytesList($ids)],
  155.             ['ids' => ArrayParameterType::STRING]
  156.         );
  157.         if (empty($children)) {
  158.             return \array_merge($ids$children);
  159.         }
  160.         $nested $this->fetchChildrenIds($children);
  161.         $children = [...$children, ...$nested];
  162.         return [...$ids, ...$children, ...$nested];
  163.     }
  164.     /**
  165.      * @param list<string> $affected
  166.      */
  167.     private function handleThumbnailDeletion(BeforeDeleteEvent $event, array $affectedContext $context): void
  168.     {
  169.         $privatePaths = [];
  170.         $publicPaths = [];
  171.         $thumbnails $this->getThumbnails($affected$context);
  172.         foreach ($thumbnails as $thumbnail) {
  173.             if ($thumbnail->getMedia() === null) {
  174.                 continue;
  175.             }
  176.             if ($thumbnail->getMedia()->isPrivate()) {
  177.                 $privatePaths[] = $this->urlGenerator->getRelativeThumbnailUrl($thumbnail->getMedia(), $thumbnail);
  178.             } else {
  179.                 $publicPaths[] = $this->urlGenerator->getRelativeThumbnailUrl($thumbnail->getMedia(), $thumbnail);
  180.             }
  181.         }
  182.         $this->performFileDelete($context$privatePathsVisibility::PRIVATE);
  183.         $this->performFileDelete($context$publicPathsVisibility::PUBLIC);
  184.         $event->addSuccess(function () use ($thumbnails$context): void {
  185.             $this->dispatcher->dispatch(new MediaThumbnailDeletedEvent($thumbnails$context), MediaThumbnailDeletedEvent::EVENT_NAME);
  186.         });
  187.     }
  188.     /**
  189.      * @param list<string> $ids
  190.      */
  191.     private function getThumbnails(array $idsContext $context): MediaThumbnailCollection
  192.     {
  193.         $criteria = new Criteria();
  194.         $criteria->addAssociation('media');
  195.         $criteria->addFilter(new EqualsAnyFilter('media_thumbnail.id'$ids));
  196.         $thumbnailsSearch $this->thumbnailRepository->search($criteria$context);
  197.         /** @var MediaThumbnailCollection $thumbnails */
  198.         $thumbnails $thumbnailsSearch->getEntities();
  199.         return $thumbnails;
  200.     }
  201.     /**
  202.      * @param list<string> $paths
  203.      */
  204.     private function performFileDelete(Context $context, array $pathsstring $visibility): void
  205.     {
  206.         if (\count($paths) <= 0) {
  207.             return;
  208.         }
  209.         if ($context->hasState(self::SYNCHRONE_FILE_DELETE)) {
  210.             $this->deleteFileHandler->__invoke(new DeleteFileMessage($paths$visibility));
  211.             return;
  212.         }
  213.         $this->messageBus->dispatch(new DeleteFileMessage($paths$visibility));
  214.     }
  215. }