599 lines
20 KiB
PHP
Executable File
599 lines
20 KiB
PHP
Executable File
<?php
|
|
|
|
namespace Mautic\ApiBundle\Controller;
|
|
|
|
use Doctrine\Persistence\ManagerRegistry;
|
|
use Mautic\ApiBundle\ApiEvents;
|
|
use Mautic\ApiBundle\Event\ApiEntityEvent;
|
|
use Mautic\ApiBundle\Helper\EntityResultHelper;
|
|
use Mautic\CategoryBundle\Entity\Category;
|
|
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\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,
|
|
) {
|
|
parent::__construct($security, $translator, $entityResultHelper, $appVersion, $requestStack, $doctrine, $modelFactory, $dispatcher, $coreParametersHelper);
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
$lastEntityIndex = -1;
|
|
foreach ($entities as $index => $moreEntities) {
|
|
if ($moreEntities !== $entity) {
|
|
continue;
|
|
}
|
|
|
|
$lastEntityIndex = $index;
|
|
}
|
|
|
|
if (-1 === $lastEntityIndex || $lastEntityIndex === $key) {
|
|
$this->detachEntity($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;
|
|
unset($parameters['publishUp'], $parameters['publishDown']);
|
|
} 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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Entity not to be detached in case of Lead Batch API.
|
|
*/
|
|
protected function detachEntity(object $entity): void
|
|
{
|
|
$this->doctrine->getManager()->detach($entity);
|
|
}
|
|
}
|