vendor/shopware/core/Framework/Api/Controller/ApiController.php line 439

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\Framework\Api\Controller;
  3. use OpenApi\Annotations as OA;
  4. use Shopware\Core\Defaults;
  5. use Shopware\Core\Framework\Api\Acl\AclCriteriaValidator;
  6. use Shopware\Core\Framework\Api\Acl\Role\AclRoleDefinition;
  7. use Shopware\Core\Framework\Api\Converter\ApiVersionConverter;
  8. use Shopware\Core\Framework\Api\Converter\Exceptions\ApiConversionException;
  9. use Shopware\Core\Framework\Api\Exception\InvalidVersionNameException;
  10. use Shopware\Core\Framework\Api\Exception\LiveVersionDeleteException;
  11. use Shopware\Core\Framework\Api\Exception\MissingPrivilegeException;
  12. use Shopware\Core\Framework\Api\Exception\NoEntityClonedException;
  13. use Shopware\Core\Framework\Api\Exception\ResourceNotFoundException;
  14. use Shopware\Core\Framework\Api\Response\ResponseFactoryInterface;
  15. use Shopware\Core\Framework\Context;
  16. use Shopware\Core\Framework\DataAbstractionLayer\DefinitionInstanceRegistry;
  17. use Shopware\Core\Framework\DataAbstractionLayer\Entity;
  18. use Shopware\Core\Framework\DataAbstractionLayer\EntityCollection;
  19. use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition;
  20. use Shopware\Core\Framework\DataAbstractionLayer\EntityProtection\EntityProtectionValidator;
  21. use Shopware\Core\Framework\DataAbstractionLayer\EntityProtection\ReadProtection;
  22. use Shopware\Core\Framework\DataAbstractionLayer\EntityProtection\WriteProtection;
  23. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
  24. use Shopware\Core\Framework\DataAbstractionLayer\EntityTranslationDefinition;
  25. use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenContainerEvent;
  26. use Shopware\Core\Framework\DataAbstractionLayer\Exception\DefinitionNotFoundException;
  27. use Shopware\Core\Framework\DataAbstractionLayer\Exception\MissingReverseAssociation;
  28. use Shopware\Core\Framework\DataAbstractionLayer\Field\AssociationField;
  29. use Shopware\Core\Framework\DataAbstractionLayer\Field\Field;
  30. use Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToManyAssociationField;
  31. use Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToOneAssociationField;
  32. use Shopware\Core\Framework\DataAbstractionLayer\Field\OneToManyAssociationField;
  33. use Shopware\Core\Framework\DataAbstractionLayer\Field\OneToOneAssociationField;
  34. use Shopware\Core\Framework\DataAbstractionLayer\Field\TranslationsAssociationField;
  35. use Shopware\Core\Framework\DataAbstractionLayer\FieldCollection;
  36. use Shopware\Core\Framework\DataAbstractionLayer\MappingEntityDefinition;
  37. use Shopware\Core\Framework\DataAbstractionLayer\Search\CompositeEntitySearcher;
  38. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  39. use Shopware\Core\Framework\DataAbstractionLayer\Search\EntitySearchResult;
  40. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  41. use Shopware\Core\Framework\DataAbstractionLayer\Search\IdSearchResult;
  42. use Shopware\Core\Framework\DataAbstractionLayer\Search\RequestCriteriaBuilder;
  43. use Shopware\Core\Framework\DataAbstractionLayer\Write\CloneBehavior;
  44. use Shopware\Core\Framework\Feature;
  45. use Shopware\Core\Framework\Routing\Annotation\RouteScope;
  46. use Shopware\Core\Framework\Routing\Annotation\Since;
  47. use Shopware\Core\Framework\Routing\Exception\MissingRequestParameterException;
  48. use Shopware\Core\Framework\Uuid\Exception\InvalidUuidException;
  49. use Shopware\Core\Framework\Uuid\Uuid;
  50. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  51. use Symfony\Component\HttpFoundation\JsonResponse;
  52. use Symfony\Component\HttpFoundation\Request;
  53. use Symfony\Component\HttpFoundation\Response;
  54. use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
  55. use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
  56. use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
  57. use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
  58. use Symfony\Component\Routing\Annotation\Route;
  59. use Symfony\Component\Serializer\Exception\InvalidArgumentException;
  60. use Symfony\Component\Serializer\Exception\UnexpectedValueException;
  61. use Symfony\Component\Serializer\Serializer;
  62. /**
  63.  * @Route(defaults={"_routeScope"={"api"}})
  64.  */
  65. class ApiController extends AbstractController
  66. {
  67.     public const WRITE_UPDATE 'update';
  68.     public const WRITE_CREATE 'create';
  69.     public const WRITE_DELETE 'delete';
  70.     /**
  71.      * @var DefinitionInstanceRegistry
  72.      */
  73.     private $definitionRegistry;
  74.     /**
  75.      * @var Serializer
  76.      */
  77.     private $serializer;
  78.     /**
  79.      * @var RequestCriteriaBuilder
  80.      */
  81.     private $criteriaBuilder;
  82.     /**
  83.      * @var CompositeEntitySearcher
  84.      */
  85.     private $compositeEntitySearcher;
  86.     /**
  87.      * @var ApiVersionConverter
  88.      */
  89.     private $apiVersionConverter;
  90.     /**
  91.      * @var EntityProtectionValidator
  92.      */
  93.     private $entityProtectionValidator;
  94.     /**
  95.      * @var AclCriteriaValidator
  96.      */
  97.     private $criteriaValidator;
  98.     /**
  99.      * @internal
  100.      */
  101.     public function __construct(
  102.         DefinitionInstanceRegistry $definitionRegistry,
  103.         Serializer $serializer,
  104.         RequestCriteriaBuilder $criteriaBuilder,
  105.         CompositeEntitySearcher $compositeEntitySearcher,
  106.         ApiVersionConverter $apiVersionConverter,
  107.         EntityProtectionValidator $entityProtectionValidator,
  108.         AclCriteriaValidator $criteriaValidator
  109.     ) {
  110.         $this->definitionRegistry $definitionRegistry;
  111.         $this->serializer $serializer;
  112.         $this->criteriaBuilder $criteriaBuilder;
  113.         $this->compositeEntitySearcher $compositeEntitySearcher;
  114.         $this->apiVersionConverter $apiVersionConverter;
  115.         $this->entityProtectionValidator $entityProtectionValidator;
  116.         $this->criteriaValidator $criteriaValidator;
  117.     }
  118.     /**
  119.      * @Since("6.0.0.0")
  120.      * @OA\Get(
  121.      *      path="/_search",
  122.      *      summary="Search for multiple entites by a given term",
  123.      *      operationId="compositeSearch",
  124.      *      tags={"Admin Api"},
  125.      *      deprecated=true,
  126.      *      @OA\Parameter(
  127.      *          name="limit",
  128.      *          in="query",
  129.      *          description="Max amount of resources per entity",
  130.      *          @OA\Schema(type="integer"),
  131.      *      ),
  132.      *      @OA\Parameter(
  133.      *          name="term",
  134.      *          in="query",
  135.      *          description="The term to search for",
  136.      *          required=true,
  137.      *          @OA\Schema(type="string")
  138.      *      ),
  139.      *      @OA\Response(
  140.      *          response="200",
  141.      *          description="The list of found entities",
  142.      *          @OA\JsonContent(
  143.      *              type="array",
  144.      *              @OA\Items(
  145.      *                  type="object",
  146.      *                  @OA\Property(
  147.      *                      property="entity",
  148.      *                      type="string",
  149.      *                      description="The name of the entity",
  150.      *                  ),
  151.      *                  @OA\Property(
  152.      *                      property="total",
  153.      *                      type="integer",
  154.      *                      description="The total amount of search results for this entity",
  155.      *                  ),
  156.      *                  @OA\Property(
  157.      *                      property="entities",
  158.      *                      type="array",
  159.      *                      description="The found entities",
  160.      *                      @OA\Items(type="object", additionalProperties=true),
  161.      *                  ),
  162.      *              ),
  163.      *          ),
  164.      *      ),
  165.      *      @OA\Response(
  166.      *          response="400",
  167.      *          ref="#/components/responses/400"
  168.      *      ),
  169.      *     @OA\Response(
  170.      *          response="401",
  171.      *          ref="#/components/responses/401"
  172.      *      )
  173.      * )
  174.      * @Route("/api/_search", name="api.composite.search", methods={"GET","POST"})
  175.      *
  176.      * @deprecated tag:v6.5.0 - Will be removed in the next major
  177.      */
  178.     public function compositeSearch(Request $requestContext $context): JsonResponse
  179.     {
  180.         Feature::triggerDeprecationOrThrow(
  181.             'v6.5.0.0',
  182.             Feature::deprecatedMethodMessage(__CLASS____METHOD__'v6.5.0.0''Shopware\Administration\Controller\AdminSearchController::search()')
  183.         );
  184.         $term = (string) $request->query->get('term');
  185.         if ($term === '') {
  186.             throw new MissingRequestParameterException('term');
  187.         }
  188.         $limit $request->query->getInt('limit'5);
  189.         $results $this->compositeEntitySearcher->search($term$limit$context);
  190.         foreach ($results as &$result) {
  191.             $definition $this->definitionRegistry->getByEntityName($result['entity']);
  192.             /** @var EntityCollection $entityCollection */
  193.             $entityCollection $result['entities'];
  194.             $entities = [];
  195.             foreach ($entityCollection->getElements() as $key => $entity) {
  196.                 $entities[$key] = $this->apiVersionConverter->convertEntity($definition$entity);
  197.             }
  198.             $result['entities'] = $entities;
  199.         }
  200.         return new JsonResponse(['data' => $results]);
  201.     }
  202.     /**
  203.      * @Since("6.0.0.0")
  204.      * @Route("/api/_action/clone/{entity}/{id}", name="api.clone", methods={"POST"}, requirements={
  205.      *     "version"="\d+", "entity"="[a-zA-Z-]+", "id"="[0-9a-f]{32}"
  206.      * })
  207.      */
  208.     public function clone(Context $contextstring $entitystring $idRequest $request): JsonResponse
  209.     {
  210.         $behavior = new CloneBehavior(
  211.             $request->request->all('overwrites'),
  212.             $request->request->getBoolean('cloneChildren'true)
  213.         );
  214.         $entity $this->urlToSnakeCase($entity);
  215.         $definition $this->definitionRegistry->getByEntityName($entity);
  216.         $missing $this->validateAclPermissions($context$definitionAclRoleDefinition::PRIVILEGE_CREATE);
  217.         if ($missing) {
  218.             throw new MissingPrivilegeException([$missing]);
  219.         }
  220.         $eventContainer $context->scope(Context::CRUD_API_SCOPE, function (Context $context) use ($definition$id$behavior): EntityWrittenContainerEvent {
  221.             /** @var EntityRepository $entityRepo */
  222.             $entityRepo $this->definitionRegistry->getRepository($definition->getEntityName());
  223.             return $entityRepo->clone($id$contextnull$behavior);
  224.         });
  225.         $event $eventContainer->getEventByEntityName($definition->getEntityName());
  226.         if (!$event) {
  227.             throw new NoEntityClonedException($entity$id);
  228.         }
  229.         $ids $event->getIds();
  230.         $newId array_shift($ids);
  231.         return new JsonResponse(['id' => $newId]);
  232.     }
  233.     /**
  234.      * @Since("6.0.0.0")
  235.      * @Route("/api/_action/version/{entity}/{id}", name="api.createVersion", methods={"POST"},
  236.      *     requirements={"version"="\d+", "entity"="[a-zA-Z-]+", "id"="[0-9a-f]{32}"
  237.      * })
  238.      */
  239.     public function createVersion(Request $requestContext $contextstring $entitystring $id): Response
  240.     {
  241.         $entity $this->urlToSnakeCase($entity);
  242.         $versionId $request->request->has('versionId') ? (string) $request->request->get('versionId') : null;
  243.         $versionName $request->request->has('versionName') ? (string) $request->request->get('versionName') : null;
  244.         if ($versionId !== null && !Uuid::isValid($versionId)) {
  245.             throw new InvalidUuidException($versionId);
  246.         }
  247.         if ($versionName !== null && !ctype_alnum($versionName)) {
  248.             throw new InvalidVersionNameException();
  249.         }
  250.         try {
  251.             $entityDefinition $this->definitionRegistry->getByEntityName($entity);
  252.         } catch (DefinitionNotFoundException $e) {
  253.             throw new NotFoundHttpException($e->getMessage(), $e);
  254.         }
  255.         $versionId $context->scope(Context::CRUD_API_SCOPE, function (Context $context) use ($entityDefinition$id$versionName$versionId): string {
  256.             return $this->definitionRegistry->getRepository($entityDefinition->getEntityName())->createVersion($id$context$versionName$versionId);
  257.         });
  258.         return new JsonResponse([
  259.             'versionId' => $versionId,
  260.             'versionName' => $versionName,
  261.             'id' => $id,
  262.             'entity' => $entity,
  263.         ]);
  264.     }
  265.     /**
  266.      * @Since("6.0.0.0")
  267.      * @Route("/api/_action/version/merge/{entity}/{versionId}", name="api.mergeVersion", methods={"POST"},
  268.      *     requirements={"version"="\d+", "entity"="[a-zA-Z-]+", "versionId"="[0-9a-f]{32}"
  269.      * })
  270.      */
  271.     public function mergeVersion(Context $contextstring $entitystring $versionId): JsonResponse
  272.     {
  273.         $entity $this->urlToSnakeCase($entity);
  274.         if (!Uuid::isValid($versionId)) {
  275.             throw new InvalidUuidException($versionId);
  276.         }
  277.         $entityDefinition $this->getEntityDefinition($entity);
  278.         $repository $this->definitionRegistry->getRepository($entityDefinition->getEntityName());
  279.         // change scope to be able to update write protected fields
  280.         $context->scope(Context::SYSTEM_SCOPE, function (Context $context) use ($repository$versionId): void {
  281.             $repository->merge($versionId$context);
  282.         });
  283.         return new JsonResponse(nullResponse::HTTP_NO_CONTENT);
  284.     }
  285.     /**
  286.      * @Since("6.0.0.0")
  287.      * @Route("/api/_action/version/{versionId}/{entity}/{entityId}", name="api.deleteVersion", methods={"POST"},
  288.      *     requirements={"version"="\d+", "entity"="[a-zA-Z-]+", "id"="[0-9a-f]{32}"
  289.      * })
  290.      */
  291.     public function deleteVersion(Context $contextstring $entitystring $entityIdstring $versionId): JsonResponse
  292.     {
  293.         if ($versionId !== null && !Uuid::isValid($versionId)) {
  294.             throw new InvalidUuidException($versionId);
  295.         }
  296.         if ($versionId === Defaults::LIVE_VERSION) {
  297.             throw new LiveVersionDeleteException();
  298.         }
  299.         if ($entityId !== null && !Uuid::isValid($entityId)) {
  300.             throw new InvalidUuidException($entityId);
  301.         }
  302.         try {
  303.             $entityDefinition $this->definitionRegistry->getByEntityName($this->urlToSnakeCase($entity));
  304.         } catch (DefinitionNotFoundException $e) {
  305.             throw new NotFoundHttpException($e->getMessage(), $e);
  306.         }
  307.         $versionContext $context->createWithVersionId($versionId);
  308.         $entityRepository $this->definitionRegistry->getRepository($entityDefinition->getEntityName());
  309.         $versionContext->scope(Context::CRUD_API_SCOPE, function (Context $versionContext) use ($entityId$entityRepository): void {
  310.             $entityRepository->delete([['id' => $entityId]], $versionContext);
  311.         });
  312.         $versionRepository $this->definitionRegistry->getRepository('version');
  313.         $versionRepository->delete([['id' => $versionId]], $context);
  314.         return new JsonResponse();
  315.     }
  316.     public function detail(Request $requestContext $contextResponseFactoryInterface $responseFactorystring $entityNamestring $path): Response
  317.     {
  318.         $pathSegments $this->buildEntityPath($entityName$path$context);
  319.         $permissions $this->validatePathSegments($context$pathSegmentsAclRoleDefinition::PRIVILEGE_READ);
  320.         $root $pathSegments[0]['entity'];
  321.         $id $pathSegments[\count($pathSegments) - 1]['value'];
  322.         $definition $this->definitionRegistry->getByEntityName($root);
  323.         $associations array_column($pathSegments'entity');
  324.         array_shift($associations);
  325.         if (empty($associations)) {
  326.             $repository $this->definitionRegistry->getRepository($definition->getEntityName());
  327.         } else {
  328.             $field $this->getAssociation($definition->getFields(), $associations);
  329.             $definition $field->getReferenceDefinition();
  330.             if ($field instanceof ManyToManyAssociationField) {
  331.                 $definition $field->getToManyReferenceDefinition();
  332.             }
  333.             $repository $this->definitionRegistry->getRepository($definition->getEntityName());
  334.         }
  335.         $criteria = new Criteria();
  336.         $criteria $this->criteriaBuilder->handleRequest($request$criteria$definition$context);
  337.         $criteria->setIds([$id]);
  338.         // trigger acl validation
  339.         $missing $this->criteriaValidator->validate($definition->getEntityName(), $criteria$context);
  340.         $permissions array_unique(array_filter(array_merge($permissions$missing)));
  341.         if (!empty($permissions)) {
  342.             throw new MissingPrivilegeException($permissions);
  343.         }
  344.         $entity $context->scope(Context::CRUD_API_SCOPE, function (Context $context) use ($repository$criteria$id): ?Entity {
  345.             return $repository->search($criteria$context)->get($id);
  346.         });
  347.         if ($entity === null) {
  348.             throw new ResourceNotFoundException($definition->getEntityName(), ['id' => $id]);
  349.         }
  350.         return $responseFactory->createDetailResponse($criteria$entity$definition$request$context);
  351.     }
  352.     public function searchIds(Request $requestContext $contextResponseFactoryInterface $responseFactorystring $entityNamestring $path): Response
  353.     {
  354.         [$criteria$repository] = $this->resolveSearch($request$context$entityName$path);
  355.         $result $context->scope(Context::CRUD_API_SCOPE, function (Context $context) use ($repository$criteria): IdSearchResult {
  356.             return $repository->searchIds($criteria$context);
  357.         });
  358.         return new JsonResponse([
  359.             'total' => $result->getTotal(),
  360.             'data' => array_values($result->getIds()),
  361.         ]);
  362.     }
  363.     public function search(Request $requestContext $contextResponseFactoryInterface $responseFactorystring $entityNamestring $path): Response
  364.     {
  365.         [$criteria$repository] = $this->resolveSearch($request$context$entityName$path);
  366.         $result $context->scope(Context::CRUD_API_SCOPE, function (Context $context) use ($repository$criteria): EntitySearchResult {
  367.             return $repository->search($criteria$context);
  368.         });
  369.         $definition $this->getDefinitionOfPath($entityName$path$context);
  370.         return $responseFactory->createListingResponse($criteria$result$definition$request$context);
  371.     }
  372.     public function list(Request $requestContext $contextResponseFactoryInterface $responseFactorystring $entityNamestring $path): Response
  373.     {
  374.         [$criteria$repository] = $this->resolveSearch($request$context$entityName$path);
  375.         $result $context->scope(Context::CRUD_API_SCOPE, function (Context $context) use ($repository$criteria): EntitySearchResult {
  376.             return $repository->search($criteria$context);
  377.         });
  378.         $definition $this->getDefinitionOfPath($entityName$path$context);
  379.         return $responseFactory->createListingResponse($criteria$result$definition$request$context);
  380.     }
  381.     public function create(Request $requestContext $contextResponseFactoryInterface $responseFactorystring $entityNamestring $path): Response
  382.     {
  383.         return $this->write($request$context$responseFactory$entityName$pathself::WRITE_CREATE);
  384.     }
  385.     public function update(Request $requestContext $contextResponseFactoryInterface $responseFactorystring $entityNamestring $path): Response
  386.     {
  387.         return $this->write($request$context$responseFactory$entityName$pathself::WRITE_UPDATE);
  388.     }
  389.     public function delete(Request $requestContext $contextResponseFactoryInterface $responseFactorystring $entityNamestring $path): Response
  390.     {
  391.         $pathSegments $this->buildEntityPath($entityName$path$context, [WriteProtection::class]);
  392.         $last $pathSegments[\count($pathSegments) - 1];
  393.         $id $last['value'];
  394.         $first array_shift($pathSegments);
  395.         if (\count($pathSegments) === 0) {
  396.             //first api level call /product/{id}
  397.             $definition $first['definition'];
  398.             $this->executeWriteOperation($definition, ['id' => $id], $contextself::WRITE_DELETE);
  399.             return $responseFactory->createRedirectResponse($definition$id$request$context);
  400.         }
  401.         $child array_pop($pathSegments);
  402.         $parent $first;
  403.         if (!empty($pathSegments)) {
  404.             $parent array_pop($pathSegments);
  405.         }
  406.         $definition $child['definition'];
  407.         /** @var AssociationField $association */
  408.         $association $child['field'];
  409.         // DELETE api/product/{id}/manufacturer/{id}
  410.         if ($association instanceof ManyToOneAssociationField || $association instanceof OneToOneAssociationField) {
  411.             $this->executeWriteOperation($definition, ['id' => $id], $contextself::WRITE_DELETE);
  412.             return $responseFactory->createRedirectResponse($definition$id$request$context);
  413.         }
  414.         // DELETE api/product/{id}/category/{id}
  415.         if ($association instanceof ManyToManyAssociationField) {
  416.             $local $definition->getFields()->getByStorageName(
  417.                 $association->getMappingLocalColumn()
  418.             );
  419.             $reference $definition->getFields()->getByStorageName(
  420.                 $association->getMappingReferenceColumn()
  421.             );
  422.             $mapping = [
  423.                 $local->getPropertyName() => $parent['value'],
  424.                 $reference->getPropertyName() => $id,
  425.             ];
  426.             /** @var EntityDefinition $parentDefinition */
  427.             $parentDefinition $parent['definition'];
  428.             if ($parentDefinition->isVersionAware()) {
  429.                 $versionField $parentDefinition->getEntityName() . 'VersionId';
  430.                 $mapping[$versionField] = $context->getVersionId();
  431.             }
  432.             if ($association->getToManyReferenceDefinition()->isVersionAware()) {
  433.                 $versionField $association->getToManyReferenceDefinition()->getEntityName() . 'VersionId';
  434.                 $mapping[$versionField] = Defaults::LIVE_VERSION;
  435.             }
  436.             $this->executeWriteOperation($definition$mapping$contextself::WRITE_DELETE);
  437.             return $responseFactory->createRedirectResponse($definition$id$request$context);
  438.         }
  439.         if ($association instanceof TranslationsAssociationField) {
  440.             /** @var EntityTranslationDefinition $refClass */
  441.             $refClass $association->getReferenceDefinition();
  442.             $refPropName $refClass->getFields()->getByStorageName($association->getReferenceField())->getPropertyName();
  443.             $refLanguagePropName $refClass->getPrimaryKeys()->getByStorageName($association->getLanguageField())->getPropertyName();
  444.             $mapping = [
  445.                 $refPropName => $parent['value'],
  446.                 $refLanguagePropName => $id,
  447.             ];
  448.             $this->executeWriteOperation($definition$mapping$contextself::WRITE_DELETE);
  449.             return $responseFactory->createRedirectResponse($definition$id$request$context);
  450.         }
  451.         if ($association instanceof OneToManyAssociationField) {
  452.             $this->executeWriteOperation($definition, ['id' => $id], $contextself::WRITE_DELETE);
  453.             return $responseFactory->createRedirectResponse($definition$id$request$context);
  454.         }
  455.         throw new \RuntimeException(sprintf('Unsupported association for field %s'$association->getPropertyName()));
  456.     }
  457.     private function resolveSearch(Request $requestContext $contextstring $entityNamestring $path): array
  458.     {
  459.         $pathSegments $this->buildEntityPath($entityName$path$context);
  460.         $permissions $this->validatePathSegments($context$pathSegmentsAclRoleDefinition::PRIVILEGE_READ);
  461.         $first array_shift($pathSegments);
  462.         /** @var EntityDefinition|string $definition */
  463.         $definition $first['definition'];
  464.         if (!$definition) {
  465.             throw new NotFoundHttpException('The requested entity does not exist.');
  466.         }
  467.         $repository $this->definitionRegistry->getRepository($definition->getEntityName());
  468.         $criteria = new Criteria();
  469.         if (empty($pathSegments)) {
  470.             $criteria $this->criteriaBuilder->handleRequest($request$criteria$definition$context);
  471.             // trigger acl validation
  472.             $nested $this->criteriaValidator->validate($definition->getEntityName(), $criteria$context);
  473.             $permissions array_unique(array_filter(array_merge($permissions$nested)));
  474.             if (!empty($permissions)) {
  475.                 throw new MissingPrivilegeException($permissions);
  476.             }
  477.             return [$criteria$repository];
  478.         }
  479.         $child array_pop($pathSegments);
  480.         $parent $first;
  481.         if (!empty($pathSegments)) {
  482.             $parent array_pop($pathSegments);
  483.         }
  484.         $association $child['field'];
  485.         $parentDefinition $parent['definition'];
  486.         $definition $child['definition'];
  487.         if ($association instanceof ManyToManyAssociationField) {
  488.             $definition $association->getToManyReferenceDefinition();
  489.         }
  490.         $criteria $this->criteriaBuilder->handleRequest($request$criteria$definition$context);
  491.         if ($association instanceof ManyToManyAssociationField) {
  492.             //fetch inverse association definition for filter
  493.             $reverse $definition->getFields()->filter(
  494.                 function (Field $field) use ($association) {
  495.                     return $field instanceof ManyToManyAssociationField && $association->getMappingDefinition() === $field->getMappingDefinition();
  496.                 }
  497.             );
  498.             //contains now the inverse side association: category.products
  499.             $reverse $reverse->first();
  500.             if (!$reverse) {
  501.                 throw new MissingReverseAssociation($definition->getEntityName(), $parentDefinition);
  502.             }
  503.             $criteria->addFilter(
  504.                 new EqualsFilter(
  505.                     sprintf('%s.%s.id'$definition->getEntityName(), $reverse->getPropertyName()),
  506.                     $parent['value']
  507.                 )
  508.             );
  509.             /** @var EntityDefinition $parentDefinition */
  510.             if ($parentDefinition->isVersionAware()) {
  511.                 $criteria->addFilter(
  512.                     new EqualsFilter(
  513.                         sprintf('%s.%s.versionId'$definition->getEntityName(), $reverse->getPropertyName()),
  514.                         $context->getVersionId()
  515.                     )
  516.                 );
  517.             }
  518.         } elseif ($association instanceof OneToManyAssociationField) {
  519.             /*
  520.              * Example
  521.              * Route:           /api/product/SW1/prices
  522.              * $definition:     \Shopware\Core\Content\Product\Definition\ProductPriceDefinition
  523.              */
  524.             //get foreign key definition of reference
  525.             $foreignKey $definition->getFields()->getByStorageName(
  526.                 $association->getReferenceField()
  527.             );
  528.             $criteria->addFilter(
  529.                 new EqualsFilter(
  530.                 //add filter to parent value: prices.productId = SW1
  531.                     $definition->getEntityName() . '.' $foreignKey->getPropertyName(),
  532.                     $parent['value']
  533.                 )
  534.             );
  535.         } elseif ($association instanceof ManyToOneAssociationField) {
  536.             /*
  537.              * Example
  538.              * Route:           /api/product/SW1/manufacturer
  539.              * $definition:     \Shopware\Core\Content\Product\Aggregate\ProductManufacturer\ProductManufacturerDefinition
  540.              */
  541.             //get inverse association to filter to parent value
  542.             $reverse $definition->getFields()->filter(
  543.                 function (Field $field) use ($parentDefinition) {
  544.                     return $field instanceof AssociationField && $parentDefinition === $field->getReferenceDefinition();
  545.                 }
  546.             );
  547.             $reverse $reverse->first();
  548.             if (!$reverse) {
  549.                 throw new MissingReverseAssociation($definition->getEntityName(), $parentDefinition);
  550.             }
  551.             $criteria->addFilter(
  552.                 new EqualsFilter(
  553.                 //filter inverse association to parent value:  manufacturer.products.id = SW1
  554.                     sprintf('%s.%s.id'$definition->getEntityName(), $reverse->getPropertyName()),
  555.                     $parent['value']
  556.                 )
  557.             );
  558.         } elseif ($association instanceof OneToOneAssociationField) {
  559.             /*
  560.              * Example
  561.              * Route:           /api/order/xxxx/orderCustomer
  562.              * $definition:     \Shopware\Core\Checkout\Order\Aggregate\OrderCustomer\OrderCustomerDefinition
  563.              */
  564.             //get inverse association to filter to parent value
  565.             $reverse $definition->getFields()->filter(
  566.                 function (Field $field) use ($parentDefinition) {
  567.                     return $field instanceof OneToOneAssociationField && $parentDefinition === $field->getReferenceDefinition();
  568.                 }
  569.             );
  570.             $reverse $reverse->first();
  571.             if (!$reverse) {
  572.                 throw new MissingReverseAssociation($definition->getEntityName(), $parentDefinition);
  573.             }
  574.             $criteria->addFilter(
  575.                 new EqualsFilter(
  576.                 //filter inverse association to parent value:  order_customer.order_id = xxxx
  577.                     sprintf('%s.%s.id'$definition->getEntityName(), $reverse->getPropertyName()),
  578.                     $parent['value']
  579.                 )
  580.             );
  581.         }
  582.         $repository $this->definitionRegistry->getRepository($definition->getEntityName());
  583.         $nested $this->criteriaValidator->validate($definition->getEntityName(), $criteria$context);
  584.         $permissions array_unique(array_filter(array_merge($permissions$nested)));
  585.         if (!empty($permissions)) {
  586.             throw new MissingPrivilegeException($permissions);
  587.         }
  588.         return [$criteria$repository];
  589.     }
  590.     private function getDefinitionOfPath(string $entityNamestring $pathContext $context): EntityDefinition
  591.     {
  592.         $pathSegments $this->buildEntityPath($entityName$path$context);
  593.         $first array_shift($pathSegments);
  594.         /** @var EntityDefinition|string $definition */
  595.         $definition $first['definition'];
  596.         if (empty($pathSegments)) {
  597.             return $definition;
  598.         }
  599.         $child array_pop($pathSegments);
  600.         $association $child['field'];
  601.         if ($association instanceof ManyToManyAssociationField) {
  602.             /*
  603.              * Example:
  604.              * route:           /api/product/SW1/categories
  605.              * $definition:     \Shopware\Core\Content\Category\CategoryDefinition
  606.              */
  607.             return $association->getToManyReferenceDefinition();
  608.         }
  609.         return $child['definition'];
  610.     }
  611.     private function write(Request $requestContext $contextResponseFactoryInterface $responseFactorystring $entityNamestring $pathstring $type): Response
  612.     {
  613.         $payload $this->getRequestBody($request);
  614.         $noContent = !$request->query->has('_response');
  615.         // safari bug prevents us from using the location header
  616.         $appendLocationHeader false;
  617.         if ($this->isCollection($payload)) {
  618.             throw new BadRequestHttpException('Only single write operations are supported. Please send the entities one by one or use the /sync api endpoint.');
  619.         }
  620.         $pathSegments $this->buildEntityPath($entityName$path$context, [WriteProtection::class]);
  621.         $last $pathSegments[\count($pathSegments) - 1];
  622.         if ($type === self::WRITE_CREATE && !empty($last['value'])) {
  623.             $methods = ['GET''PATCH''DELETE'];
  624.             throw new MethodNotAllowedHttpException($methodssprintf('No route found for "%s %s": Method Not Allowed (Allow: %s)'$request->getMethod(), $request->getPathInfo(), implode(', '$methods)));
  625.         }
  626.         if ($type === self::WRITE_UPDATE && isset($last['value'])) {
  627.             $payload['id'] = $last['value'];
  628.         }
  629.         $first array_shift($pathSegments);
  630.         if (\count($pathSegments) === 0) {
  631.             $definition $first['definition'];
  632.             $events $this->executeWriteOperation($definition$payload$context$type);
  633.             $event $events->getEventByEntityName($definition->getEntityName());
  634.             $eventIds $event->getIds();
  635.             $entityId array_pop($eventIds);
  636.             if ($definition instanceof MappingEntityDefinition) {
  637.                 return new Response(nullResponse::HTTP_NO_CONTENT);
  638.             }
  639.             if ($noContent) {
  640.                 return $responseFactory->createRedirectResponse($definition$entityId$request$context);
  641.             }
  642.             $repository $this->definitionRegistry->getRepository($definition->getEntityName());
  643.             $criteria = new Criteria($event->getIds());
  644.             $entities $repository->search($criteria$context);
  645.             return $responseFactory->createDetailResponse($criteria$entities->first(), $definition$request$context$appendLocationHeader);
  646.         }
  647.         $child array_pop($pathSegments);
  648.         $parent $first;
  649.         if (!empty($pathSegments)) {
  650.             $parent array_pop($pathSegments);
  651.         }
  652.         /** @var EntityDefinition $definition */
  653.         $definition $child['definition'];
  654.         $association $child['field'];
  655.         $parentDefinition $parent['definition'];
  656.         if ($association instanceof OneToManyAssociationField) {
  657.             $foreignKey $definition->getFields()
  658.                 ->getByStorageName($association->getReferenceField());
  659.             $payload[$foreignKey->getPropertyName()] = $parent['value'];
  660.             $events $this->executeWriteOperation($definition$payload$context$type);
  661.             if ($noContent) {
  662.                 return $responseFactory->createRedirectResponse($definition$parent['value'], $request$context);
  663.             }
  664.             $event $events->getEventByEntityName($definition->getEntityName());
  665.             $repository $this->definitionRegistry->getRepository($definition->getEntityName());
  666.             $criteria = new Criteria($event->getIds());
  667.             $entities $repository->search($criteria$context);
  668.             return $responseFactory->createDetailResponse($criteria$entities->first(), $definition$request$context$appendLocationHeader);
  669.         }
  670.         if ($association instanceof ManyToOneAssociationField || $association instanceof OneToOneAssociationField) {
  671.             $events $this->executeWriteOperation($definition$payload$context$type);
  672.             $event $events->getEventByEntityName($definition->getEntityName());
  673.             $entityIds $event->getIds();
  674.             $entityId array_pop($entityIds);
  675.             $foreignKey $parentDefinition->getFields()->getByStorageName($association->getStorageName());
  676.             $payload = [
  677.                 'id' => $parent['value'],
  678.                 $foreignKey->getPropertyName() => $entityId,
  679.             ];
  680.             $repository $this->definitionRegistry->getRepository($parentDefinition->getEntityName());
  681.             $repository->update([$payload], $context);
  682.             if ($noContent) {
  683.                 return $responseFactory->createRedirectResponse($definition$entityId$request$context);
  684.             }
  685.             $criteria = new Criteria($event->getIds());
  686.             $entities $repository->search($criteria$context);
  687.             return $responseFactory->createDetailResponse($criteria$entities->first(), $definition$request$context$appendLocationHeader);
  688.         }
  689.         /** @var ManyToManyAssociationField $manyToManyAssociation */
  690.         $manyToManyAssociation $association;
  691.         /** @var EntityDefinition|string $reference */
  692.         $reference $manyToManyAssociation->getToManyReferenceDefinition();
  693.         // check if we need to create the entity first
  694.         if (\count($payload) > || !\array_key_exists('id'$payload)) {
  695.             $events $this->executeWriteOperation($reference$payload$context$type);
  696.             $event $events->getEventByEntityName($reference->getEntityName());
  697.             $ids $event->getIds();
  698.             $id array_shift($ids);
  699.         } else {
  700.             // only id provided - add assignment
  701.             $id $payload['id'];
  702.         }
  703.         $payload = [
  704.             'id' => $parent['value'],
  705.             $manyToManyAssociation->getPropertyName() => [
  706.                 ['id' => $id],
  707.             ],
  708.         ];
  709.         $repository $this->definitionRegistry->getRepository($parentDefinition->getEntityName());
  710.         $repository->update([$payload], $context);
  711.         $repository $this->definitionRegistry->getRepository($reference->getEntityName());
  712.         $criteria = new Criteria([$id]);
  713.         $entities $repository->search($criteria$context);
  714.         $entity $entities->first();
  715.         if ($noContent) {
  716.             return $responseFactory->createRedirectResponse($reference$entity->getId(), $request$context);
  717.         }
  718.         return $responseFactory->createDetailResponse($criteria$entity$definition$request$context$appendLocationHeader);
  719.     }
  720.     private function executeWriteOperation(
  721.         EntityDefinition $entity,
  722.         array $payload,
  723.         Context $context,
  724.         string $type
  725.     ): EntityWrittenContainerEvent {
  726.         $repository $this->definitionRegistry->getRepository($entity->getEntityName());
  727.         $conversionException = new ApiConversionException();
  728.         $payload $this->apiVersionConverter->convertPayload($entity$payload$conversionException);
  729.         $conversionException->tryToThrow();
  730.         $event $context->scope(Context::CRUD_API_SCOPE, function (Context $context) use ($repository$payload$entity$type): ?EntityWrittenContainerEvent {
  731.             if ($type === self::WRITE_CREATE) {
  732.                 return $repository->create([$payload], $context);
  733.             }
  734.             if ($type === self::WRITE_UPDATE) {
  735.                 return $repository->update([$payload], $context);
  736.             }
  737.             if ($type === self::WRITE_DELETE) {
  738.                 $event $repository->delete([$payload], $context);
  739.                 if (!empty($event->getErrors())) {
  740.                     throw new ResourceNotFoundException($entity->getEntityName(), $payload);
  741.                 }
  742.                 return $event;
  743.             }
  744.             return null;
  745.         });
  746.         if (!$event) {
  747.             throw new \RuntimeException('Unsupported write operation.');
  748.         }
  749.         return $event;
  750.     }
  751.     private function getAssociation(FieldCollection $fields, array $keys): AssociationField
  752.     {
  753.         $key array_shift($keys);
  754.         /** @var AssociationField $field */
  755.         $field $fields->get($key);
  756.         if (empty($keys)) {
  757.             return $field;
  758.         }
  759.         $reference $field->getReferenceDefinition();
  760.         $nested $reference->getFields();
  761.         return $this->getAssociation($nested$keys);
  762.     }
  763.     private function buildEntityPath(
  764.         string $entityName,
  765.         string $pathInfo,
  766.         Context $context,
  767.         array $protections = [ReadProtection::class]
  768.     ): array {
  769.         $pathInfo str_replace('/extensions/''/'$pathInfo);
  770.         $exploded explode('/'$entityName '/' ltrim($pathInfo'/'));
  771.         $parts = [];
  772.         foreach ($exploded as $index => $part) {
  773.             if ($index 2) {
  774.                 continue;
  775.             }
  776.             if (empty($part)) {
  777.                 continue;
  778.             }
  779.             $value $exploded[$index 1] ?? null;
  780.             if (empty($parts)) {
  781.                 $part $this->urlToSnakeCase($part);
  782.             } else {
  783.                 $part $this->urlToCamelCase($part);
  784.             }
  785.             $parts[] = [
  786.                 'entity' => $part,
  787.                 'value' => $value,
  788.             ];
  789.         }
  790.         /** @var array{'entity': string, 'value': string|null} $first */
  791.         $first array_shift($parts);
  792.         try {
  793.             $root $this->definitionRegistry->getByEntityName($first['entity']);
  794.         } catch (DefinitionNotFoundException $e) {
  795.             throw new NotFoundHttpException($e->getMessage(), $e);
  796.         }
  797.         $entities = [
  798.             [
  799.                 'entity' => $first['entity'],
  800.                 'value' => $first['value'],
  801.                 'definition' => $root,
  802.                 'field' => null,
  803.             ],
  804.         ];
  805.         foreach ($parts as $part) {
  806.             /** @var AssociationField|null $field */
  807.             $field $root->getFields()->get($part['entity']);
  808.             if (!$field) {
  809.                 $path implode('.'array_column($entities'entity')) . '.' $part['entity'];
  810.                 throw new NotFoundHttpException(sprintf('Resource at path "%s" is not an existing relation.'$path));
  811.             }
  812.             if ($field instanceof ManyToManyAssociationField) {
  813.                 $root $field->getToManyReferenceDefinition();
  814.             } else {
  815.                 $root $field->getReferenceDefinition();
  816.             }
  817.             $entities[] = [
  818.                 'entity' => $part['entity'],
  819.                 'value' => $part['value'],
  820.                 'definition' => $field->getReferenceDefinition(),
  821.                 'field' => $field,
  822.             ];
  823.         }
  824.         $context->scope(Context::CRUD_API_SCOPE, function (Context $context) use ($entities$protections): void {
  825.             $this->entityProtectionValidator->validateEntityPath($entities$protections$context);
  826.         });
  827.         return $entities;
  828.     }
  829.     private function urlToSnakeCase(string $name): string
  830.     {
  831.         return str_replace('-''_'$name);
  832.     }
  833.     private function urlToCamelCase(string $name): string
  834.     {
  835.         $parts explode('-'$name);
  836.         $parts array_map('ucfirst'$parts);
  837.         return lcfirst(implode(''$parts));
  838.     }
  839.     /**
  840.      * Return a nested array structure of based on the content-type
  841.      */
  842.     private function getRequestBody(Request $request): array
  843.     {
  844.         $contentType $request->headers->get('CONTENT_TYPE''');
  845.         $semicolonPosition mb_strpos($contentType';');
  846.         if ($semicolonPosition !== false) {
  847.             $contentType mb_substr($contentType0$semicolonPosition);
  848.         }
  849.         try {
  850.             switch ($contentType) {
  851.                 case 'application/vnd.api+json':
  852.                     return $this->serializer->decode($request->getContent(), 'jsonapi');
  853.                 case 'application/json':
  854.                     return $request->request->all();
  855.             }
  856.         } catch (InvalidArgumentException UnexpectedValueException $exception) {
  857.             throw new BadRequestHttpException($exception->getMessage());
  858.         }
  859.         throw new UnsupportedMediaTypeHttpException(sprintf('The Content-Type "%s" is unsupported.'$contentType));
  860.     }
  861.     private function isCollection(array $array): bool
  862.     {
  863.         return array_keys($array) === range(0\count($array) - 1);
  864.     }
  865.     private function getEntityDefinition(string $entityName): EntityDefinition
  866.     {
  867.         try {
  868.             $entityDefinition $this->definitionRegistry->getByEntityName($entityName);
  869.         } catch (DefinitionNotFoundException $e) {
  870.             throw new NotFoundHttpException($e->getMessage(), $e);
  871.         }
  872.         return $entityDefinition;
  873.     }
  874.     private function validateAclPermissions(Context $contextEntityDefinition $entitystring $privilege): ?string
  875.     {
  876.         $resource $entity->getEntityName();
  877.         if ($entity instanceof EntityTranslationDefinition) {
  878.             $resource $entity->getParentDefinition()->getEntityName();
  879.         }
  880.         if (!$context->isAllowed($resource ':' $privilege)) {
  881.             return $resource ':' $privilege;
  882.         }
  883.         return null;
  884.     }
  885.     private function validatePathSegments(Context $context, array $pathSegmentsstring $privilege): array
  886.     {
  887.         $child array_pop($pathSegments);
  888.         $missing = [];
  889.         foreach ($pathSegments as $segment) {
  890.             // you need detail privileges for every parent entity
  891.             $missing[] = $this->validateAclPermissions(
  892.                 $context,
  893.                 $this->getDefinitionForPathSegment($segment),
  894.                 AclRoleDefinition::PRIVILEGE_READ
  895.             );
  896.         }
  897.         $missing[] = $this->validateAclPermissions($context$this->getDefinitionForPathSegment($child), $privilege);
  898.         return array_unique(array_filter($missing));
  899.     }
  900.     private function getDefinitionForPathSegment(array $segment): EntityDefinition
  901.     {
  902.         $definition $segment['definition'];
  903.         if ($segment['field'] instanceof ManyToManyAssociationField) {
  904.             $definition $segment['field']->getToManyReferenceDefinition();
  905.         }
  906.         return $definition;
  907.     }
  908. }