mautic / app /bundles /ApiBundle /Controller /CommonApiController.php
chrisbryan17's picture
Upload folder using huggingface_hub
d2897cd verified
<?php
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);
}
}
}