<?php declare(strict_types=1);
namespace Shopware\Core\Content\Media\Subscriber;
use Doctrine\DBAL\Connection;
use League\Flysystem\AdapterInterface;
use Shopware\Core\Content\Media\Aggregate\MediaFolder\MediaFolderDefinition;
use Shopware\Core\Content\Media\Aggregate\MediaThumbnail\MediaThumbnailCollection;
use Shopware\Core\Content\Media\Aggregate\MediaThumbnail\MediaThumbnailDefinition;
use Shopware\Core\Content\Media\Event\MediaThumbnailDeletedEvent;
use Shopware\Core\Content\Media\MediaDefinition;
use Shopware\Core\Content\Media\MediaEntity;
use Shopware\Core\Content\Media\Message\DeleteFileHandler;
use Shopware\Core\Content\Media\Message\DeleteFileMessage;
use Shopware\Core\Content\Media\Pathname\UrlGeneratorInterface;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
use Shopware\Core\Framework\DataAbstractionLayer\Event\BeforeDeleteEvent;
use Shopware\Core\Framework\DataAbstractionLayer\Event\EntitySearchedEvent;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\MultiFilter;
use Shopware\Core\Framework\Uuid\Uuid;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\MessageBusInterface;
class MediaDeletionSubscriber implements EventSubscriberInterface
{
public const SYNCHRONE_FILE_DELETE = 'synchrone-file-delete';
private Connection $connection;
private UrlGeneratorInterface $urlGenerator;
private EventDispatcherInterface $dispatcher;
private EntityRepositoryInterface $thumbnailRepository;
private MessageBusInterface $messageBus;
private DeleteFileHandler $deleteFileHandler;
private EntityRepositoryInterface $mediaRepository;
/**
* @internal
*/
public function __construct(
UrlGeneratorInterface $urlGenerator,
EventDispatcherInterface $dispatcher,
EntityRepositoryInterface $thumbnailRepository,
MessageBusInterface $messageBus,
DeleteFileHandler $deleteFileHandler,
Connection $connection,
EntityRepositoryInterface $mediaRepository
) {
$this->urlGenerator = $urlGenerator;
$this->dispatcher = $dispatcher;
$this->thumbnailRepository = $thumbnailRepository;
$this->messageBus = $messageBus;
$this->deleteFileHandler = $deleteFileHandler;
$this->connection = $connection;
$this->mediaRepository = $mediaRepository;
}
public static function getSubscribedEvents(): array
{
return [
BeforeDeleteEvent::class => 'beforeDelete',
EntitySearchedEvent::class => 'securePrivateFolders',
];
}
public function securePrivateFolders(EntitySearchedEvent $event): void
{
if ($event->getContext()->getScope() === Context::SYSTEM_SCOPE) {
return;
}
if ($event->getDefinition()->getEntityName() === MediaFolderDefinition::ENTITY_NAME) {
$event->getCriteria()->addFilter(
new MultiFilter('OR', [
new EqualsFilter('media_folder.configuration.private', false),
new EqualsFilter('media_folder.configuration.private', null),
])
);
return;
}
if ($event->getDefinition()->getEntityName() === MediaDefinition::ENTITY_NAME) {
$event->getCriteria()->addFilter(new EqualsFilter('private', false));
return;
}
}
public function beforeDelete(BeforeDeleteEvent $event): void
{
$affected = $event->getIds(MediaThumbnailDefinition::ENTITY_NAME);
if (!empty($affected)) {
$this->handleThumbnailDeletion($event, $affected, $event->getContext());
}
$affected = $event->getIds(MediaFolderDefinition::ENTITY_NAME);
if (!empty($affected)) {
$this->handleFolderDeletion($affected, $event->getContext());
}
$affected = $event->getIds(MediaDefinition::ENTITY_NAME);
if (!empty($affected)) {
$this->handleMediaDeletion($affected, $event->getContext());
}
}
private function handleMediaDeletion(array $affected, Context $context): void
{
$media = $context->scope(Context::SYSTEM_SCOPE, function (Context $context) use ($affected) {
return $this->mediaRepository->search(new Criteria($affected), $context);
});
$privatePaths = [];
$publicPaths = [];
$thumbnails = [];
/** @var MediaEntity $mediaEntity */
foreach ($media as $mediaEntity) {
if (!$mediaEntity->hasFile()) {
continue;
}
if ($mediaEntity->isPrivate()) {
$privatePaths[] = $this->urlGenerator->getRelativeMediaUrl($mediaEntity);
} else {
$publicPaths[] = $this->urlGenerator->getRelativeMediaUrl($mediaEntity);
}
if (!$mediaEntity->getThumbnails()) {
continue;
}
foreach ($mediaEntity->getThumbnails()->getIds() as $id) {
$thumbnails[] = ['id' => $id];
}
}
$this->performFileDelete($context, $publicPaths, AdapterInterface::VISIBILITY_PUBLIC);
$this->performFileDelete($context, $privatePaths, AdapterInterface::VISIBILITY_PRIVATE);
$this->thumbnailRepository->delete($thumbnails, $context);
}
private function handleFolderDeletion(array $affected, Context $context): void
{
$ids = $this->fetchChildrenIds($affected);
if (empty($ids)) {
return;
}
$media = $this->connection->fetchAllAssociative(
'SELECT LOWER(HEX(id)) as id FROM media WHERE media_folder_id IN (:ids)',
['ids' => Uuid::fromHexToBytesList($ids)],
['ids' => Connection::PARAM_STR_ARRAY]
);
if (empty($media)) {
return;
}
$this->mediaRepository->delete($media, $context);
}
private function fetchChildrenIds(array $ids): array
{
$children = $this->connection->fetchFirstColumn(
'SELECT LOWER(HEX(id)) FROM media_folder WHERE parent_id IN (:ids)',
['ids' => Uuid::fromHexToBytesList($ids)],
['ids' => Connection::PARAM_STR_ARRAY]
);
if (empty($children)) {
return \array_merge($ids, $children);
}
$nested = $this->fetchChildrenIds($children);
$children = \array_merge($children, $nested);
return \array_merge($ids, $children, $nested);
}
private function handleThumbnailDeletion(BeforeDeleteEvent $event, array $affected, Context $context): void
{
$privatePaths = [];
$publicPaths = [];
$thumbnails = $this->getThumbnails($affected, $context);
foreach ($thumbnails as $thumbnail) {
if ($thumbnail->getMedia() === null) {
continue;
}
if ($thumbnail->getMedia()->isPrivate()) {
$privatePaths[] = $this->urlGenerator->getRelativeThumbnailUrl($thumbnail->getMedia(), $thumbnail);
} else {
$publicPaths[] = $this->urlGenerator->getRelativeThumbnailUrl($thumbnail->getMedia(), $thumbnail);
}
}
$this->performFileDelete($context, $privatePaths, AdapterInterface::VISIBILITY_PRIVATE);
$this->performFileDelete($context, $publicPaths, AdapterInterface::VISIBILITY_PUBLIC);
$event->addSuccess(function () use ($thumbnails, $context): void {
$this->dispatcher->dispatch(new MediaThumbnailDeletedEvent($thumbnails, $context), MediaThumbnailDeletedEvent::EVENT_NAME);
});
}
private function getThumbnails(array $ids, Context $context): MediaThumbnailCollection
{
$criteria = new Criteria();
$criteria->addAssociation('media');
$criteria->addFilter(new EqualsAnyFilter('media_thumbnail.id', $ids));
$thumbnailsSearch = $this->thumbnailRepository->search($criteria, $context);
/** @var MediaThumbnailCollection $thumbnails */
$thumbnails = $thumbnailsSearch->getEntities();
return $thumbnails;
}
private function performFileDelete(Context $context, array $paths, string $visibility): void
{
if (\count($paths) <= 0) {
return;
}
if ($context->hasState(self::SYNCHRONE_FILE_DELETE)) {
$this->deleteFileHandler->handle(new DeleteFileMessage($paths, $visibility));
return;
}
$this->messageBus->dispatch(new DeleteFileMessage($paths, $visibility));
}
}