Spaces:
No application file
No application file
| namespace Mautic\ApiBundle\Controller; | |
| use Doctrine\Persistence\ManagerRegistry; | |
| use FOS\RestBundle\View\View; | |
| use Mautic\ApiBundle\ApiEvents; | |
| use Mautic\ApiBundle\Event\ApiEntityEvent; | |
| use Mautic\ApiBundle\Helper\EntityResultHelper; | |
| use Mautic\CategoryBundle\Entity\Category; | |
| use Mautic\CoreBundle\Factory\MauticFactory; | |
| use Mautic\CoreBundle\Factory\ModelFactory; | |
| use Mautic\CoreBundle\Helper\AppVersion; | |
| use Mautic\CoreBundle\Helper\CoreParametersHelper; | |
| use Mautic\CoreBundle\Helper\InputHelper; | |
| use Mautic\CoreBundle\Model\FormModel; | |
| use Mautic\CoreBundle\Security\Permissions\CorePermissions; | |
| use Mautic\CoreBundle\Translation\Translator; | |
| use Symfony\Component\EventDispatcher\EventDispatcherInterface; | |
| use Symfony\Component\Form\Form; | |
| use Symfony\Component\Form\FormFactoryInterface; | |
| use Symfony\Component\Form\FormInterface; | |
| use Symfony\Component\HttpFoundation\RedirectResponse; | |
| use Symfony\Component\HttpFoundation\Request; | |
| use Symfony\Component\HttpFoundation\RequestStack; | |
| use Symfony\Component\HttpFoundation\Response; | |
| use Symfony\Component\Routing\RouterInterface; | |
| /** | |
| * @template E of object | |
| * | |
| * @extends FetchCommonApiController<E> | |
| */ | |
| class CommonApiController extends FetchCommonApiController | |
| { | |
| /** | |
| * @var array | |
| */ | |
| protected $dataInputMasks = []; | |
| /** | |
| * Model object for processing the entity. | |
| * | |
| * @var FormModel<E>|null | |
| */ | |
| protected $model; | |
| /** | |
| * @var array | |
| */ | |
| protected $routeParams = []; | |
| /** | |
| * @var array | |
| */ | |
| protected $entityRequestParameters = []; | |
| public function __construct( | |
| CorePermissions $security, | |
| Translator $translator, | |
| EntityResultHelper $entityResultHelper, | |
| protected RouterInterface $router, | |
| protected FormFactoryInterface $formFactory, | |
| AppVersion $appVersion, | |
| RequestStack $requestStack, | |
| ManagerRegistry $doctrine, | |
| ModelFactory $modelFactory, | |
| EventDispatcherInterface $dispatcher, | |
| CoreParametersHelper $coreParametersHelper, | |
| MauticFactory $factory | |
| ) { | |
| parent::__construct($security, $translator, $entityResultHelper, $appVersion, $requestStack, $doctrine, $modelFactory, $dispatcher, $coreParametersHelper, $factory); | |
| } | |
| /** | |
| * Delete a batch of entities. | |
| * | |
| * @return array|Response | |
| */ | |
| public function deleteEntitiesAction(Request $request) | |
| { | |
| $parameters = $request->query->all(); | |
| $valid = $this->validateBatchPayload($parameters); | |
| if ($valid instanceof Response) { | |
| return $valid; | |
| } | |
| $errors = []; | |
| $entities = $this->getBatchEntities($parameters, $errors, true); | |
| $this->inBatchMode = true; | |
| // Generate the view before deleting so that the IDs are still populated before Doctrine removes them | |
| $payload = [$this->entityNameMulti => $entities]; | |
| $view = $this->view($payload, Response::HTTP_OK); | |
| $this->setSerializationContext($view); | |
| $response = $this->handleView($view); | |
| foreach ($entities as $key => $entity) { | |
| if (null === $entity || !$entity->getId()) { | |
| $this->setBatchError($key, 'mautic.core.error.notfound', Response::HTTP_NOT_FOUND, $errors, $entities, $entity); | |
| continue; | |
| } | |
| if (!$this->checkEntityAccess($entity, 'delete')) { | |
| $this->setBatchError($key, 'mautic.core.error.accessdenied', Response::HTTP_FORBIDDEN, $errors, $entities, $entity); | |
| continue; | |
| } | |
| $this->model->deleteEntity($entity); | |
| $this->doctrine->getManager()->detach($entity); | |
| } | |
| if (!empty($errors)) { | |
| $content = json_decode($response->getContent(), true); | |
| $content['errors'] = $errors; | |
| $response->setContent(json_encode($content)); | |
| } | |
| return $response; | |
| } | |
| /** | |
| * Deletes an entity. | |
| * | |
| * @param int $id Entity ID | |
| * | |
| * @return Response | |
| */ | |
| public function deleteEntityAction($id) | |
| { | |
| $entity = $this->model->getEntity($id); | |
| if (null !== $entity) { | |
| if (!$this->checkEntityAccess($entity, 'delete')) { | |
| return $this->accessDenied(); | |
| } | |
| $this->model->deleteEntity($entity); | |
| $this->preSerializeEntity($entity); | |
| $view = $this->view([$this->entityNameOne => $entity], Response::HTTP_OK); | |
| $this->setSerializationContext($view); | |
| return $this->handleView($view); | |
| } | |
| return $this->notFound(); | |
| } | |
| /** | |
| * Edit a batch of entities. | |
| * | |
| * @return array|Response | |
| */ | |
| public function editEntitiesAction(Request $request) | |
| { | |
| $parameters = $request->request->all(); | |
| $valid = $this->validateBatchPayload($parameters); | |
| if ($valid instanceof Response) { | |
| return $valid; | |
| } | |
| $errors = []; | |
| $statusCodes = []; | |
| $entities = $this->getBatchEntities($parameters, $errors); | |
| foreach ($parameters as $key => $params) { | |
| $method = $request->getMethod(); | |
| $entity = $entities[$key] ?? null; | |
| $statusCode = Response::HTTP_OK; | |
| if (null === $entity || !$entity->getId()) { | |
| if ('PATCH' === $method) { | |
| // PATCH requires that an entity exists | |
| $this->setBatchError($key, 'mautic.core.error.notfound', Response::HTTP_NOT_FOUND, $errors, $entities, $entity); | |
| $statusCodes[$key] = Response::HTTP_NOT_FOUND; | |
| continue; | |
| } | |
| // PUT can create a new entity if it doesn't exist | |
| $entity = $this->model->getEntity(); | |
| if (!$this->checkEntityAccess($entity, 'create')) { | |
| $this->setBatchError($key, 'mautic.core.error.accessdenied', Response::HTTP_FORBIDDEN, $errors, $entities, $entity); | |
| $statusCodes[$key] = Response::HTTP_FORBIDDEN; | |
| continue; | |
| } | |
| $statusCode = Response::HTTP_CREATED; | |
| } | |
| if (!$this->checkEntityAccess($entity, 'edit')) { | |
| $this->setBatchError($key, 'mautic.core.error.accessdenied', Response::HTTP_FORBIDDEN, $errors, $entities, $entity); | |
| $statusCodes[$key] = Response::HTTP_FORBIDDEN; | |
| continue; | |
| } | |
| $this->processBatchForm($request, $key, $entity, $params, $method, $errors, $entities); | |
| if (isset($errors[$key])) { | |
| $statusCodes[$key] = $errors[$key]['code']; | |
| } else { | |
| $statusCodes[$key] = $statusCode; | |
| } | |
| } | |
| $payload = [ | |
| $this->entityNameMulti => $entities, | |
| 'statusCodes' => $statusCodes, | |
| ]; | |
| if (!empty($errors)) { | |
| $payload['errors'] = $errors; | |
| } | |
| $view = $this->view($payload, Response::HTTP_OK); | |
| $this->setSerializationContext($view); | |
| return $this->handleView($view); | |
| } | |
| /** | |
| * Edits an existing entity or creates one on PUT if it doesn't exist. | |
| * | |
| * @param int $id Entity ID | |
| * | |
| * @return Response | |
| */ | |
| public function editEntityAction(Request $request, $id) | |
| { | |
| $entity = $this->model->getEntity($id); | |
| $parameters = $request->request->all(); | |
| $method = $request->getMethod(); | |
| if (null === $entity || !$entity->getId()) { | |
| if ('PATCH' === $method) { | |
| // PATCH requires that an entity exists | |
| return $this->notFound(); | |
| } | |
| // PUT can create a new entity if it doesn't exist | |
| $entity = $this->model->getEntity(); | |
| if (!$this->checkEntityAccess($entity, 'create')) { | |
| return $this->accessDenied(); | |
| } | |
| } | |
| if (!$this->checkEntityAccess($entity, 'edit')) { | |
| return $this->accessDenied(); | |
| } | |
| return $this->processForm($request, $entity, $parameters, $method); | |
| } | |
| /** | |
| * Create a batch of new entities. | |
| * | |
| * @return array|Response | |
| */ | |
| public function newEntitiesAction(Request $request) | |
| { | |
| $entity = $this->model->getEntity(); | |
| if (!$this->checkEntityAccess($entity, 'create')) { | |
| return $this->accessDenied(); | |
| } | |
| $parameters = $request->request->all(); | |
| $valid = $this->validateBatchPayload($parameters); | |
| if ($valid instanceof Response) { | |
| return $valid; | |
| } | |
| $this->inBatchMode = true; | |
| $entities = []; | |
| $errors = []; | |
| $statusCodes = []; | |
| foreach ($parameters as $key => $params) { | |
| // Can be new or an existing on based on params | |
| $entity = $this->getNewEntity($params); | |
| $entityExists = false; | |
| $method = 'POST'; | |
| if ($entity->getId()) { | |
| $entityExists = true; | |
| $method = 'PATCH'; | |
| if (!$this->checkEntityAccess($entity, 'edit')) { | |
| $this->setBatchError($key, 'mautic.core.error.accessdenied', Response::HTTP_FORBIDDEN, $errors, $entities, $entity); | |
| $statusCodes[$key] = Response::HTTP_FORBIDDEN; | |
| continue; | |
| } | |
| } | |
| $this->processBatchForm($request, $key, $entity, $params, $method, $errors, $entities); | |
| if (isset($errors[$key])) { | |
| $statusCodes[$key] = $errors[$key]['code']; | |
| } elseif ($entityExists) { | |
| $statusCodes[$key] = Response::HTTP_OK; | |
| } else { | |
| $statusCodes[$key] = Response::HTTP_CREATED; | |
| } | |
| } | |
| $payload = [ | |
| $this->entityNameMulti => $entities, | |
| 'statusCodes' => $statusCodes, | |
| ]; | |
| if (!empty($errors)) { | |
| $payload['errors'] = $errors; | |
| } | |
| $view = $this->view($payload, Response::HTTP_CREATED); | |
| $this->setSerializationContext($view); | |
| return $this->handleView($view); | |
| } | |
| /** | |
| * Creates a new entity. | |
| * | |
| * @return Response | |
| */ | |
| public function newEntityAction(Request $request) | |
| { | |
| $parameters = $request->request->all(); | |
| $entity = $this->getNewEntity($parameters); | |
| if (!$this->checkEntityAccess($entity, 'create')) { | |
| return $this->accessDenied(); | |
| } | |
| return $this->processForm($request, $entity, $parameters, 'POST'); | |
| } | |
| /** | |
| * @return FormInterface<mixed> | |
| */ | |
| protected function createEntityForm($entity): FormInterface | |
| { | |
| return $this->model->createForm( | |
| $entity, | |
| $this->formFactory, | |
| null, | |
| array_merge( | |
| [ | |
| 'csrf_protection' => false, | |
| 'allow_extra_fields' => true, | |
| ], | |
| $this->getEntityFormOptions() | |
| ) | |
| ); | |
| } | |
| /** | |
| * Gives child controllers opportunity to analyze and do whatever to an entity before populating the form. | |
| * | |
| * @param string $action | |
| * | |
| * @return mixed | |
| */ | |
| protected function prePopulateForm(&$entity, $parameters, $action = 'edit') | |
| { | |
| } | |
| /** | |
| * Give the controller an opportunity to process the entity before persisting. | |
| * | |
| * @return mixed | |
| */ | |
| protected function preSaveEntity(&$entity, $form, $parameters, $action = 'edit') | |
| { | |
| } | |
| /** | |
| * Convert posted parameters into what the form needs in order to successfully bind. | |
| * | |
| * @param mixed[] $parameters | |
| * @param object $entity | |
| * @param string $action | |
| * | |
| * @return mixed | |
| */ | |
| protected function prepareParametersForBinding(Request $request, $parameters, $entity, $action) | |
| { | |
| return $parameters; | |
| } | |
| protected function processBatchForm(Request $request, $key, $entity, $params, $method, &$errors, &$entities) | |
| { | |
| $this->inBatchMode = true; | |
| $formResponse = $this->processForm($request, $entity, $params, $method); | |
| if ($formResponse instanceof Response) { | |
| if (!$formResponse instanceof RedirectResponse) { | |
| // Assume an error | |
| $this->setBatchError( | |
| $key, | |
| InputHelper::string($formResponse->getContent()), | |
| $formResponse->getStatusCode(), | |
| $errors, | |
| $entities, | |
| $entity | |
| ); | |
| } | |
| } elseif (is_object($formResponse) && $formResponse::class === $entity::class) { | |
| // Success | |
| $entities[$key] = $formResponse; | |
| } elseif (is_array($formResponse) && isset($formResponse['code'], $formResponse['message'])) { | |
| // There was an error | |
| $errors[$key] = $formResponse; | |
| } | |
| $this->doctrine->getManager()->detach($entity); | |
| $this->inBatchMode = false; | |
| } | |
| /** | |
| * Processes API Form. | |
| * | |
| * @param array<mixed>|null $parameters | |
| * @param string $method | |
| * | |
| * @return mixed | |
| */ | |
| protected function processForm(Request $request, $entity, $parameters = null, $method = 'PUT') | |
| { | |
| $categoryId = null; | |
| if (null === $parameters) { | |
| // get from request | |
| $parameters = $request->request->all(); | |
| } | |
| // Store the original parameters from the request so that callbacks can have access to them as needed | |
| $this->entityRequestParameters = $parameters; | |
| // unset the ID in the parameters if set as this will cause the form to fail | |
| if (isset($parameters['id'])) { | |
| unset($parameters['id']); | |
| } | |
| // is an entity being updated or created? | |
| if ($entity->getId()) { | |
| $statusCode = Response::HTTP_OK; | |
| $action = 'edit'; | |
| } else { | |
| $statusCode = Response::HTTP_CREATED; | |
| $action = 'new'; | |
| // All the properties have to be defined in order for validation to work | |
| // Bug reported https://github.com/symfony/symfony/issues/19788 | |
| $defaultProperties = $this->getEntityDefaultProperties($entity); | |
| $parameters = array_merge($defaultProperties, $parameters); | |
| } | |
| // Check if user has access to publish | |
| if ( | |
| ( | |
| array_key_exists('isPublished', $parameters) | |
| || array_key_exists('publishUp', $parameters) | |
| || array_key_exists('publishDown', $parameters) | |
| ) | |
| && $this->security->checkPermissionExists($this->permissionBase.':publish')) { | |
| if ($this->security->checkPermissionExists($this->permissionBase.':publishown')) { | |
| if (!$this->checkEntityAccess($entity, 'publish')) { | |
| if ('new' === $action) { | |
| $parameters['isPublished'] = 0; | |
| } else { | |
| unset($parameters['isPublished'], $parameters['publishUp'], $parameters['publishDown']); | |
| } | |
| } | |
| } | |
| } | |
| $form = $this->createEntityForm($entity); | |
| $submitParams = $this->prepareParametersForBinding($request, $parameters, $entity, $action); | |
| if ($submitParams instanceof Response) { | |
| return $submitParams; | |
| } | |
| // Remove category from the payload because it will cause form validation error. | |
| if (isset($submitParams['category'])) { | |
| $categoryId = (int) $submitParams['category']; | |
| unset($submitParams['category']); | |
| } | |
| $this->prepareParametersFromRequest($form, $submitParams, $entity, $this->dataInputMasks); | |
| $form->submit($submitParams, 'PATCH' !== $method); | |
| if ($form->isSubmitted() && $form->isValid()) { | |
| $this->setCategory($entity, $categoryId); | |
| $preSaveError = $this->preSaveEntity($entity, $form, $submitParams, $action); | |
| if ($preSaveError instanceof Response) { | |
| return $preSaveError; | |
| } | |
| try { | |
| if ($this->dispatcher->hasListeners(ApiEvents::API_ON_ENTITY_PRE_SAVE)) { | |
| $this->dispatcher->dispatch(new ApiEntityEvent($entity, $this->entityRequestParameters, $request), ApiEvents::API_ON_ENTITY_PRE_SAVE); | |
| } | |
| } catch (\Exception $e) { | |
| return $this->returnError($e->getMessage(), $e->getCode()); | |
| } | |
| $statusCode = $this->saveEntity($entity, $statusCode); | |
| $headers = []; | |
| // return the newly created entities location if applicable | |
| if (in_array($statusCode, [Response::HTTP_CREATED, Response::HTTP_ACCEPTED])) { | |
| $route = (null !== $this->router->getRouteCollection()->get('mautic_api_'.$this->entityNameMulti.'_getone')) | |
| ? 'mautic_api_'.$this->entityNameMulti.'_getone' : 'mautic_api_get'.$this->entityNameOne; | |
| $headers['Location'] = $this->generateUrl( | |
| $route, | |
| array_merge(['id' => $entity->getId()], $this->routeParams), | |
| true | |
| ); | |
| } | |
| try { | |
| if ($this->dispatcher->hasListeners(ApiEvents::API_ON_ENTITY_POST_SAVE)) { | |
| $this->dispatcher->dispatch(new ApiEntityEvent($entity, $this->entityRequestParameters, $request), ApiEvents::API_ON_ENTITY_POST_SAVE); | |
| } | |
| } catch (\Exception $e) { | |
| return $this->returnError($e->getMessage(), $e->getCode()); | |
| } | |
| $this->preSerializeEntity($entity, $action); | |
| if ($this->inBatchMode) { | |
| return $entity; | |
| } else { | |
| $view = $this->view([$this->entityNameOne => $entity], $statusCode, $headers); | |
| } | |
| $this->setSerializationContext($view); | |
| } else { | |
| $formErrors = $this->getFormErrorMessages($form); | |
| $formErrorCodes = $this->getFormErrorCodes($form); | |
| $msg = $this->getFormErrorMessage($formErrors); | |
| if (!$msg) { | |
| $msg = $this->translator->trans('mautic.core.error.badrequest', [], 'flashes'); | |
| } | |
| $responseCode = in_array(Response::HTTP_UNPROCESSABLE_ENTITY, $formErrorCodes) ? Response::HTTP_UNPROCESSABLE_ENTITY : Response::HTTP_BAD_REQUEST; | |
| return $this->returnError($msg, $responseCode, $formErrors); | |
| } | |
| return $this->handleView($view); | |
| } | |
| protected function saveEntity($entity, int $statusCode): int | |
| { | |
| $this->model->saveEntity($entity); | |
| return $statusCode; | |
| } | |
| /** | |
| * @param object $entity | |
| * @param int $categoryId | |
| * | |
| * @throws \UnexpectedValueException | |
| */ | |
| protected function setCategory($entity, $categoryId) | |
| { | |
| if (!empty($categoryId) && method_exists($entity, 'setCategory')) { | |
| $category = $this->doctrine->getManager()->find(Category::class, $categoryId); | |
| if (null === $category) { | |
| throw new \UnexpectedValueException("Category $categoryId does not exist"); | |
| } | |
| $entity->setCategory($category); | |
| } | |
| } | |
| } | |