src/Core/Content/Media/Subscriber/MediaDeletionSubscriber.php line 90

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