<?php declare(strict_types=1); 
 
namespace Shopware\Core\Framework\DataAbstractionLayer\EntityProtection; 
 
use Shopware\Core\Framework\Context; 
use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition; 
use Shopware\Core\Framework\DataAbstractionLayer\Event\EntitySearchedEvent; 
use Shopware\Core\Framework\DataAbstractionLayer\Field\AssociationField; 
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria; 
use Shopware\Core\Framework\DataAbstractionLayer\Write\Validation\PreWriteValidationEvent; 
use Symfony\Component\EventDispatcher\EventSubscriberInterface; 
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; 
 
class EntityProtectionValidator implements EventSubscriberInterface 
{ 
    /** 
     * @return array<string, string|array{0: string, 1: int}|list<array{0: string, 1?: int}>> 
     */ 
    public static function getSubscribedEvents() 
    { 
        return [ 
            PreWriteValidationEvent::class => 'validateWriteCommands', 
            EntitySearchedEvent::class => 'validateEntitySearch', 
        ]; 
    } 
 
    /** 
     * @param array<string> $protections FQCN of the protections that need to be validated 
     */ 
    public function validateEntityPath(array $pathSegments, array $protections, Context $context): void 
    { 
        foreach ($pathSegments as $pathSegment) { 
            /** @var EntityDefinition $definition */ 
            $definition = $pathSegment['definition']; 
 
            foreach ($protections as $protection) { 
                $protectionInstance = $definition->getProtections()->get($protection); 
                if (!$protectionInstance || $protectionInstance->isAllowed($context->getScope())) { 
                    continue; 
                } 
 
                throw new AccessDeniedHttpException( 
                    sprintf('API access for entity "%s" not allowed.', $pathSegment['entity']) 
                ); 
            } 
        } 
    } 
 
    public function validateEntitySearch(EntitySearchedEvent $event): void 
    { 
        $definition = $event->getDefinition(); 
        $readProtection = $definition->getProtections()->get(ReadProtection::class); 
        $context = $event->getContext(); 
 
        if ($readProtection && !$readProtection->isAllowed($context->getScope())) { 
            throw new AccessDeniedHttpException( 
                sprintf( 
                    'Read access to entity "%s" not allowed for scope "%s".', 
                    $definition->getEntityName(), 
                    $context->getScope() 
                ) 
            ); 
        } 
 
        $this->validateCriteriaAssociation( 
            $definition, 
            $event->getCriteria()->getAssociations(), 
            $context 
        ); 
    } 
 
    public function validateWriteCommands(PreWriteValidationEvent $event): void 
    { 
        foreach ($event->getCommands() as $command) { 
            // Don't validate commands that fake operations on DB level, e.g. cascade deletes 
            if (!$command->isValid()) { 
                continue; 
            } 
 
            $writeProtection = $command->getDefinition()->getProtections()->get(WriteProtection::class); 
            if ($writeProtection && !$writeProtection->isAllowed($event->getContext()->getScope())) { 
                throw new AccessDeniedHttpException( 
                    sprintf( 
                        'Write access to entity "%s" are not allowed in scope "%s".', 
                        $command->getDefinition()->getEntityName(), 
                        $event->getContext()->getScope() 
                    ) 
                ); 
            } 
        } 
    } 
 
    private function validateCriteriaAssociation(EntityDefinition $definition, array $associations, Context $context): void 
    { 
        /** @var Criteria $criteria */ 
        foreach ($associations as $associationName => $criteria) { 
            $field = $definition->getField($associationName); 
            if (!$field instanceof AssociationField) { 
                continue; 
            } 
 
            $associationDefinition = $field->getReferenceDefinition(); 
            $readProtection = $associationDefinition->getProtections()->get(ReadProtection::class); 
            if ($readProtection && !$readProtection->isAllowed($context->getScope())) { 
                throw new AccessDeniedHttpException( 
                    sprintf( 
                        'Read access to nested association "%s" on entity "%s" not allowed for scope "%s".', 
                        $associationName, 
                        $definition->getEntityName(), 
                        $context->getScope() 
                    ) 
                ); 
            } 
 
            $this->validateCriteriaAssociation($associationDefinition, $criteria->getAssociations(), $context); 
        } 
    } 
}