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

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