Issues (3627)

ApiBundle/Controller/CommonApiController.php (1 issue)

1
<?php
2
3
/*
4
 * @copyright   2014 Mautic Contributors. All rights reserved
5
 * @author      Mautic
6
 *
7
 * @link        http://mautic.org
8
 *
9
 * @license     GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html
10
 */
11
12
namespace Mautic\ApiBundle\Controller;
13
14
use Doctrine\ORM\Mapping\ClassMetadata;
15
use Doctrine\ORM\Tools\Pagination\Paginator;
16
use FOS\RestBundle\Controller\FOSRestController;
17
use FOS\RestBundle\View\View;
18
use JMS\Serializer\Exclusion\ExclusionStrategyInterface;
19
use Mautic\ApiBundle\ApiEvents;
20
use Mautic\ApiBundle\Event\ApiEntityEvent;
21
use Mautic\ApiBundle\Serializer\Exclusion\ParentChildrenExclusionStrategy;
22
use Mautic\ApiBundle\Serializer\Exclusion\PublishDetailsExclusionStrategy;
23
use Mautic\CategoryBundle\Entity\Category;
24
use Mautic\CoreBundle\Controller\FormErrorMessagesTrait;
25
use Mautic\CoreBundle\Controller\MauticController;
26
use Mautic\CoreBundle\Factory\MauticFactory;
27
use Mautic\CoreBundle\Form\RequestTrait;
28
use Mautic\CoreBundle\Helper\CoreParametersHelper;
29
use Mautic\CoreBundle\Helper\InputHelper;
30
use Mautic\CoreBundle\Model\AbstractCommonModel;
31
use Mautic\CoreBundle\Security\Exception\PermissionException;
32
use Mautic\CoreBundle\Service\FlashBag;
33
use Mautic\UserBundle\Entity\User;
34
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
35
use Symfony\Component\Form\Form;
36
use Symfony\Component\HttpFoundation\RedirectResponse;
37
use Symfony\Component\HttpFoundation\Request;
38
use Symfony\Component\HttpFoundation\Response;
39
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
40
use Symfony\Component\Translation\TranslatorInterface;
41
42
/**
43
 * Class CommonApiController.
44
 */
45
class CommonApiController extends FOSRestController implements MauticController
0 ignored issues
show
Deprecated Code introduced by
The class FOS\RestBundle\Controller\FOSRestController has been deprecated: since FOSRestBundle 2.5, use {@see AbstractFOSRestController} instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

45
class CommonApiController extends /** @scrutinizer ignore-deprecated */ FOSRestController implements MauticController
Loading history...
46
{
47
    use RequestTrait;
48
    use FormErrorMessagesTrait;
49
50
    /**
51
     * @var CoreParametersHelper
52
     */
53
    protected $coreParametersHelper;
54
55
    /**
56
     * If set to true, serializer will not return null values.
57
     *
58
     * @var bool
59
     */
60
    protected $customSelectRequested = false;
61
62
    /**
63
     * @var array
64
     */
65
    protected $dataInputMasks = [];
66
67
    /**
68
     * @var EventDispatcherInterface
69
     */
70
    protected $dispatcher;
71
72
    /**
73
     * Class for the entity.
74
     *
75
     * @var string
76
     */
77
    protected $entityClass;
78
79
    /**
80
     * Key to return for entity lists.
81
     *
82
     * @var string
83
     */
84
    protected $entityNameMulti;
85
86
    /**
87
     * Key to return for a single entity.
88
     *
89
     * @var string
90
     */
91
    protected $entityNameOne;
92
93
    /**
94
     * Custom JMS strategies to add to the view's context.
95
     *
96
     * @var array
97
     */
98
    protected $exclusionStrategies = [];
99
100
    /**
101
     * Pass to the model's getEntities() method.
102
     *
103
     * @var array
104
     */
105
    protected $extraGetEntitiesArguments = [];
106
107
    /**
108
     * @var MauticFactory
109
     */
110
    protected $factory;
111
112
    /**
113
     * @var bool
114
     */
115
    protected $inBatchMode = false;
116
117
    /**
118
     * Used to set default filters for entity lists such as restricting to owning user.
119
     *
120
     * @var array
121
     */
122
    protected $listFilters = [];
123
124
    /**
125
     * Model object for processing the entity.
126
     *
127
     * @var \Mautic\CoreBundle\Model\AbstractCommonModel
128
     */
129
    protected $model;
130
131
    /**
132
     * The level parent/children should stop loading if applicable.
133
     *
134
     * @var int
135
     */
136
    protected $parentChildrenLevelDepth = 3;
137
138
    /**
139
     * Permission base for the entity such as page:pages.
140
     *
141
     * @var string
142
     */
143
    protected $permissionBase;
144
145
    /**
146
     * @var Request
147
     */
148
    protected $request;
149
150
    /**
151
     * @var array
152
     */
153
    protected $routeParams = [];
154
155
    /**
156
     * @var \Mautic\CoreBundle\Security\Permissions\CorePermissions
157
     */
158
    protected $security;
159
160
    /**
161
     * @var array
162
     */
163
    protected $serializerGroups = [];
164
165
    /**
166
     * @var TranslatorInterface
167
     */
168
    protected $translator;
169
170
    /**
171
     * @var User
172
     */
173
    protected $user;
174
175
    /**
176
     * @var array
177
     */
178
    protected $entityRequestParameters = [];
179
180
    /**
181
     * Delete a batch of entities.
182
     *
183
     * @return array|Response
184
     */
185
    public function deleteEntitiesAction()
186
    {
187
        $parameters = $this->request->query->all();
188
189
        $valid = $this->validateBatchPayload($parameters);
190
        if ($valid instanceof Response) {
191
            return $valid;
192
        }
193
194
        $errors            = [];
195
        $entities          = $this->getBatchEntities($parameters, $errors, true);
196
        $this->inBatchMode = true;
197
198
        // Generate the view before deleting so that the IDs are still populated before Doctrine removes them
199
        $payload = [$this->entityNameMulti => $entities];
200
        $view    = $this->view($payload, Response::HTTP_OK);
201
        $this->setSerializationContext($view);
202
        $response = $this->handleView($view);
203
204
        foreach ($entities as $key => $entity) {
205
            if (null === $entity || !$entity->getId()) {
206
                $this->setBatchError($key, 'mautic.core.error.notfound', Response::HTTP_NOT_FOUND, $errors, $entities, $entity);
207
                continue;
208
            }
209
210
            if (!$this->checkEntityAccess($entity, 'delete')) {
211
                $this->setBatchError($key, 'mautic.core.error.accessdenied', Response::HTTP_FORBIDDEN, $errors, $entities, $entity);
212
                continue;
213
            }
214
215
            $this->model->deleteEntity($entity);
216
            $this->getDoctrine()->getManager()->detach($entity);
217
        }
218
219
        if (!empty($errors)) {
220
            $content           = json_decode($response->getContent(), true);
221
            $content['errors'] = $errors;
222
            $response->setContent(json_encode($content));
223
        }
224
225
        return $response;
226
    }
227
228
    /**
229
     * Deletes an entity.
230
     *
231
     * @param int $id Entity ID
232
     *
233
     * @return Response
234
     */
235
    public function deleteEntityAction($id)
236
    {
237
        $entity = $this->model->getEntity($id);
238
        if (null !== $entity) {
239
            if (!$this->checkEntityAccess($entity, 'delete')) {
240
                return $this->accessDenied();
241
            }
242
243
            $this->model->deleteEntity($entity);
244
245
            $this->preSerializeEntity($entity);
246
            $view = $this->view([$this->entityNameOne => $entity], Response::HTTP_OK);
247
            $this->setSerializationContext($view);
248
249
            return $this->handleView($view);
250
        }
251
252
        return $this->notFound();
253
    }
254
255
    /**
256
     * Edit a batch of entities.
257
     *
258
     * @return array|Response
259
     */
260
    public function editEntitiesAction()
261
    {
262
        $parameters = $this->request->request->all();
263
264
        $valid = $this->validateBatchPayload($parameters);
265
        if ($valid instanceof Response) {
266
            return $valid;
267
        }
268
269
        $errors      = [];
270
        $statusCodes = [];
271
        $entities    = $this->getBatchEntities($parameters, $errors);
272
273
        foreach ($parameters as $key => $params) {
274
            $method = $this->request->getMethod();
275
            $entity = (isset($entities[$key])) ? $entities[$key] : null;
276
277
            $statusCode = Response::HTTP_OK;
278
            if (null === $entity || !$entity->getId()) {
279
                if ('PATCH' === $method) {
280
                    //PATCH requires that an entity exists
281
                    $this->setBatchError($key, 'mautic.core.error.notfound', Response::HTTP_NOT_FOUND, $errors, $entities, $entity);
282
                    $statusCodes[$key] = Response::HTTP_NOT_FOUND;
283
                    continue;
284
                }
285
286
                //PUT can create a new entity if it doesn't exist
287
                $entity = $this->model->getEntity();
288
                if (!$this->checkEntityAccess($entity, 'create')) {
289
                    $this->setBatchError($key, 'mautic.core.error.accessdenied', Response::HTTP_FORBIDDEN, $errors, $entities, $entity);
290
                    $statusCodes[$key] = Response::HTTP_FORBIDDEN;
291
                    continue;
292
                }
293
294
                $statusCode = Response::HTTP_CREATED;
295
            }
296
297
            if (!$this->checkEntityAccess($entity, 'edit')) {
298
                $this->setBatchError($key, 'mautic.core.error.accessdenied', Response::HTTP_FORBIDDEN, $errors, $entities, $entity);
299
                $statusCodes[$key] = Response::HTTP_FORBIDDEN;
300
                continue;
301
            }
302
303
            $this->processBatchForm($key, $entity, $params, $method, $errors, $entities);
304
305
            if (isset($errors[$key])) {
306
                $statusCodes[$key] = $errors[$key]['code'];
307
            } else {
308
                $statusCodes[$key] = $statusCode;
309
            }
310
        }
311
312
        $payload = [
313
            $this->entityNameMulti => $entities,
314
            'statusCodes'          => $statusCodes,
315
        ];
316
317
        if (!empty($errors)) {
318
            $payload['errors'] = $errors;
319
        }
320
321
        $view = $this->view($payload, Response::HTTP_OK);
322
        $this->setSerializationContext($view);
323
324
        return $this->handleView($view);
325
    }
326
327
    /**
328
     * Edits an existing entity or creates one on PUT if it doesn't exist.
329
     *
330
     * @param int $id Entity ID
331
     *
332
     * @return Response
333
     */
334
    public function editEntityAction($id)
335
    {
336
        $entity     = $this->model->getEntity($id);
337
        $parameters = $this->request->request->all();
338
        $method     = $this->request->getMethod();
339
340
        if (null === $entity || !$entity->getId()) {
341
            if ('PATCH' === $method) {
342
                //PATCH requires that an entity exists
343
                return $this->notFound();
344
            }
345
346
            //PUT can create a new entity if it doesn't exist
347
            $entity = $this->model->getEntity();
348
            if (!$this->checkEntityAccess($entity, 'create')) {
349
                return $this->accessDenied();
350
            }
351
        }
352
353
        if (!$this->checkEntityAccess($entity, 'edit')) {
354
            return $this->accessDenied();
355
        }
356
357
        return $this->processForm($entity, $parameters, $method);
358
    }
359
360
    /**
361
     * Obtains a list of entities as defined by the API URL.
362
     *
363
     * @return Response
364
     */
365
    public function getEntitiesAction()
366
    {
367
        $repo          = $this->model->getRepository();
368
        $tableAlias    = $repo->getTableAlias();
369
        $publishedOnly = $this->request->get('published', 0);
370
        $minimal       = $this->request->get('minimal', 0);
371
372
        try {
373
            if (!$this->security->isGranted($this->permissionBase.':view')) {
374
                return $this->accessDenied();
375
            }
376
        } catch (PermissionException $e) {
377
            return $this->accessDenied($e->getMessage());
378
        }
379
380
        if ($this->security->checkPermissionExists($this->permissionBase.':viewother')
381
            && !$this->security->isGranted($this->permissionBase.':viewother')
382
        ) {
383
            $this->listFilters[] = [
384
                'column' => $tableAlias.'.createdBy',
385
                'expr'   => 'eq',
386
                'value'  => $this->user->getId(),
387
            ];
388
        }
389
390
        if ($publishedOnly) {
391
            $this->listFilters[] = [
392
                'column' => $tableAlias.'.isPublished',
393
                'expr'   => 'eq',
394
                'value'  => true,
395
            ];
396
        }
397
398
        if ($minimal) {
399
            if (isset($this->serializerGroups[0])) {
400
                $this->serializerGroups[0] = str_replace('Details', 'List', $this->serializerGroups[0]);
401
            }
402
        }
403
404
        $args = array_merge(
405
            [
406
                'start'  => $this->request->query->get('start', 0),
407
                'limit'  => $this->request->query->get('limit', $this->coreParametersHelper->get('default_pagelimit')),
408
                'filter' => [
409
                    'string' => $this->request->query->get('search', ''),
410
                    'force'  => $this->listFilters,
411
                ],
412
                'orderBy'        => $this->addAliasIfNotPresent($this->request->query->get('orderBy', ''), $tableAlias),
413
                'orderByDir'     => $this->request->query->get('orderByDir', 'ASC'),
414
                'withTotalCount' => true, //for repositories that break free of Paginator
415
            ],
416
            $this->extraGetEntitiesArguments
417
        );
418
419
        if ($select = InputHelper::cleanArray($this->request->get('select', []))) {
420
            $args['select']              = $select;
421
            $this->customSelectRequested = true;
422
        }
423
424
        if ($where = $this->getWhereFromRequest()) {
425
            $args['filter']['where'] = $where;
426
        }
427
428
        if ($order = $this->getOrderFromRequest()) {
429
            $args['filter']['order'] = $order;
430
        }
431
432
        $results = $this->model->getEntities($args);
433
434
        [$entities, $totalCount] = $this->prepareEntitiesForView($results);
435
436
        $view = $this->view(
437
            [
438
                'total'                => $totalCount,
439
                $this->entityNameMulti => $entities,
440
            ],
441
            Response::HTTP_OK
442
        );
443
        $this->setSerializationContext($view);
444
445
        return $this->handleView($view);
446
    }
447
448
    /**
449
     * Sanitizes and returns an array of where statements from the request.
450
     *
451
     * @return array
452
     */
453
    protected function getWhereFromRequest()
454
    {
455
        $where = InputHelper::cleanArray($this->request->get('where', []));
456
457
        $this->sanitizeWhereClauseArrayFromRequest($where);
458
459
        return $where;
460
    }
461
462
    /**
463
     * Sanitizes and returns an array of ORDER statements from the request.
464
     *
465
     * @return array
466
     */
467
    protected function getOrderFromRequest()
468
    {
469
        return InputHelper::cleanArray($this->request->get('order', []));
470
    }
471
472
    /**
473
     * Adds the repository alias to the column name if it doesn't exist.
474
     *
475
     * @return string $column name with alias prefix
476
     */
477
    protected function addAliasIfNotPresent($columns, $alias)
478
    {
479
        if (!$columns) {
480
            return $columns;
481
        }
482
483
        $columns = explode(',', trim($columns));
484
        $prefix  = $alias.'.';
485
486
        array_walk(
487
            $columns,
488
            function (&$column, $key, $prefix) {
489
                $column = trim($column);
490
                if (1 === count(explode('.', $column))) {
491
                    $column = $prefix.$column;
492
                }
493
            },
494
            $prefix
495
        );
496
497
        return implode(',', $columns);
498
    }
499
500
    /**
501
     * Obtains a specific entity as defined by the API URL.
502
     *
503
     * @param int $id Entity ID
504
     *
505
     * @return Response
506
     */
507
    public function getEntityAction($id)
508
    {
509
        $args = [];
510
        if ($select = InputHelper::cleanArray($this->request->get('select', []))) {
511
            $args['select']              = $select;
512
            $this->customSelectRequested = true;
513
        }
514
515
        if (!empty($args)) {
516
            $args['id'] = $id;
517
            $entity     = $this->model->getEntity($args);
518
        } else {
519
            $entity = $this->model->getEntity($id);
520
        }
521
522
        if (!$entity instanceof $this->entityClass) {
523
            return $this->notFound();
524
        }
525
526
        if (!$this->checkEntityAccess($entity)) {
527
            return $this->accessDenied();
528
        }
529
530
        $this->preSerializeEntity($entity);
531
        $view = $this->view([$this->entityNameOne => $entity], Response::HTTP_OK);
532
        $this->setSerializationContext($view);
533
534
        return $this->handleView($view);
535
    }
536
537
    /**
538
     * Initialize some variables.
539
     */
540
    public function initialize(FilterControllerEvent $event)
541
    {
542
        $this->security = $this->get('mautic.security');
543
544
        if ($this->model && !$this->permissionBase && method_exists($this->model, 'getPermissionBase')) {
545
            $this->permissionBase = $this->model->getPermissionBase();
546
        }
547
    }
548
549
    /**
550
     * Creates new entity from provided params.
551
     *
552
     * @return object
553
     */
554
    public function getNewEntity(array $params)
555
    {
556
        return $this->model->getEntity();
557
    }
558
559
    /**
560
     * Create a batch of new entities.
561
     *
562
     * @return array|Response
563
     */
564
    public function newEntitiesAction()
565
    {
566
        $entity = $this->model->getEntity();
567
568
        if (!$this->checkEntityAccess($entity, 'create')) {
569
            return $this->accessDenied();
570
        }
571
572
        $parameters = $this->request->request->all();
573
574
        $valid = $this->validateBatchPayload($parameters);
575
        if ($valid instanceof Response) {
576
            return $valid;
577
        }
578
579
        $this->inBatchMode = true;
580
        $entities          = [];
581
        $errors            = [];
582
        $statusCodes       = [];
583
        foreach ($parameters as $key => $params) {
584
            // Can be new or an existing on based on params
585
            $entity       = $this->getNewEntity($params);
586
            $entityExists = false;
587
            $method       = 'POST';
588
            if ($entity->getId()) {
589
                $entityExists = true;
590
                $method       = 'PATCH';
591
                if (!$this->checkEntityAccess($entity, 'edit')) {
592
                    $this->setBatchError($key, 'mautic.core.error.accessdenied', Response::HTTP_FORBIDDEN, $errors, $entities, $entity);
593
                    $statusCodes[$key] = Response::HTTP_FORBIDDEN;
594
                    continue;
595
                }
596
            }
597
            $this->processBatchForm($key, $entity, $params, $method, $errors, $entities);
598
599
            if (isset($errors[$key])) {
600
                $statusCodes[$key] = $errors[$key]['code'];
601
            } elseif ($entityExists) {
602
                $statusCodes[$key] = Response::HTTP_OK;
603
            } else {
604
                $statusCodes[$key] = Response::HTTP_CREATED;
605
            }
606
        }
607
608
        $payload = [
609
            $this->entityNameMulti => $entities,
610
            'statusCodes'          => $statusCodes,
611
        ];
612
613
        if (!empty($errors)) {
614
            $payload['errors'] = $errors;
615
        }
616
617
        $view = $this->view($payload, Response::HTTP_CREATED);
618
        $this->setSerializationContext($view);
619
620
        return $this->handleView($view);
621
    }
622
623
    /**
624
     * Creates a new entity.
625
     *
626
     * @return Response
627
     */
628
    public function newEntityAction()
629
    {
630
        $parameters = $this->request->request->all();
631
        $entity     = $this->getNewEntity($parameters);
632
633
        if (!$this->checkEntityAccess($entity, 'create')) {
634
            return $this->accessDenied();
635
        }
636
637
        return $this->processForm($entity, $parameters, 'POST');
638
    }
639
640
    public function setCoreParametersHelper(CoreParametersHelper $coreParametersHelper)
641
    {
642
        $this->coreParametersHelper = $coreParametersHelper;
643
    }
644
645
    public function setDispatcher(EventDispatcherInterface $dispatcher)
646
    {
647
        $this->dispatcher = $dispatcher;
648
    }
649
650
    public function setFactory(MauticFactory $factory)
651
    {
652
        $this->factory = $factory;
653
    }
654
655
    public function setRequest(Request $request)
656
    {
657
        $this->request = $request;
658
    }
659
660
    public function setTranslator(TranslatorInterface $translator)
661
    {
662
        $this->translator = $translator;
663
    }
664
665
    public function setFlashBag(FlashBag $flashBag)
666
    {
667
        // @see \Mautic\CoreBundle\EventListener\CoreSubscriber::onKernelController()
668
    }
669
670
    public function setUser(User $user)
671
    {
672
        $this->user = $user;
673
    }
674
675
    /**
676
     * Alias for notFound method. It's used in the LeadAccessTrait.
677
     *
678
     * @param array $args
679
     *
680
     * @return Response
681
     */
682
    public function postActionRedirect($args = [])
683
    {
684
        return $this->notFound('mautic.contact.error.notfound');
685
    }
686
687
    /**
688
     * Returns a 403 Access Denied.
689
     *
690
     * @param string $msg
691
     *
692
     * @return Response
693
     */
694
    protected function accessDenied($msg = 'mautic.core.error.accessdenied')
695
    {
696
        return $this->returnError($msg, Response::HTTP_FORBIDDEN);
697
    }
698
699
    protected function addExclusionStrategy(ExclusionStrategyInterface $strategy)
700
    {
701
        $this->exclusionStrategies[] = $strategy;
702
    }
703
704
    /**
705
     * Returns a 400 Bad Request.
706
     *
707
     * @param string $msg
708
     *
709
     * @return Response
710
     */
711
    protected function badRequest($msg = 'mautic.core.error.badrequest')
712
    {
713
        return $this->returnError($msg, Response::HTTP_BAD_REQUEST);
714
    }
715
716
    /**
717
     * Checks if user has permission to access retrieved entity.
718
     *
719
     * @param mixed  $entity
720
     * @param string $action view|create|edit|publish|delete
721
     *
722
     * @return bool|Response
723
     */
724
    protected function checkEntityAccess($entity, $action = 'view')
725
    {
726
        if ('create' != $action && method_exists($entity, 'getCreatedBy')) {
727
            $ownPerm   = "{$this->permissionBase}:{$action}own";
728
            $otherPerm = "{$this->permissionBase}:{$action}other";
729
730
            $owner = (method_exists($entity, 'getPermissionUser')) ? $entity->getPermissionUser() : $entity->getCreatedBy();
731
732
            return $this->security->hasEntityAccess($ownPerm, $otherPerm, $owner);
733
        }
734
735
        try {
736
            return $this->security->isGranted("{$this->permissionBase}:{$action}");
737
        } catch (PermissionException $e) {
738
            return $this->accessDenied($e->getMessage());
739
        }
740
    }
741
742
    /**
743
     * Creates the form instance.
744
     *
745
     * @param $entity
746
     *
747
     * @return Form
748
     */
749
    protected function createEntityForm($entity)
750
    {
751
        return $this->model->createForm(
752
            $entity,
753
            $this->get('form.factory'),
754
            null,
755
            array_merge(
756
                [
757
                    'csrf_protection'    => false,
758
                    'allow_extra_fields' => true,
759
                ],
760
                $this->getEntityFormOptions()
761
            )
762
        );
763
    }
764
765
    /**
766
     * @param        $parameters
767
     * @param        $errors
768
     * @param bool   $prepareForSerialization
769
     * @param string $requestIdColumn
770
     * @param null   $model
771
     * @param bool   $returnWithOriginalKeys
772
     *
773
     * @return array|mixed
774
     */
775
    protected function getBatchEntities($parameters, &$errors, $prepareForSerialization = false, $requestIdColumn = 'id', $model = null, $returnWithOriginalKeys = true)
776
    {
777
        $ids = [];
778
        if (isset($parameters['ids'])) {
779
            foreach ($parameters['ids'] as $key => $id) {
780
                $ids[(int) $id] = $key;
781
            }
782
        } else {
783
            foreach ($parameters as $key => $params) {
784
                if (is_array($params) && !isset($params[$requestIdColumn])) {
785
                    $this->setBatchError($key, 'mautic.api.call.id_missing', Response::HTTP_BAD_REQUEST, $errors);
786
                    continue;
787
                }
788
789
                $id       = (is_array($params)) ? (int) $params[$requestIdColumn] : (int) $params;
790
                $ids[$id] = $key;
791
            }
792
        }
793
        $return = [];
794
        if (!empty($ids)) {
795
            $model    = ($model) ? $model : $this->model;
796
            $entities = $model->getEntities(
797
                [
798
                    'filter' => [
799
                        'force' => [
800
                            [
801
                                'column' => $model->getRepository()->getTableAlias().'.id',
802
                                'expr'   => 'in',
803
                                'value'  => array_keys($ids),
804
                            ],
805
                        ],
806
                    ],
807
                    'ignore_paginator' => true,
808
                ]
809
            );
810
811
            [$entities, $total] = $prepareForSerialization
812
                ?
813
                $this->prepareEntitiesForView($entities)
814
                :
815
                $this->prepareEntityResultsToArray($entities);
816
817
            foreach ($entities as $entity) {
818
                if ($returnWithOriginalKeys) {
819
                    // Ensure same keys as params
820
                    $return[$ids[$entity->getId()]] = $entity;
821
                } else {
822
                    $return[$entity->getId()] = $entity;
823
                }
824
            }
825
        }
826
827
        return $return;
828
    }
829
830
    /**
831
     * Get the default properties of an entity and parents.
832
     *
833
     * @param $entity
834
     *
835
     * @return array
836
     */
837
    protected function getEntityDefaultProperties($entity)
838
    {
839
        $class         = get_class($entity);
840
        $chain         = array_reverse(class_parents($entity), true) + [$class => $class];
841
        $defaultValues = [];
842
843
        $classMetdata = new ClassMetadata($class);
844
        foreach ($chain as $class) {
845
            if (method_exists($class, 'loadMetadata')) {
846
                $class::loadMetadata($classMetdata);
847
            }
848
            $defaultValues += (new \ReflectionClass($class))->getDefaultProperties();
849
        }
850
851
        // These are the mapped columns
852
        $fields = $classMetdata->getFieldNames();
853
854
        // Merge values in with $fields
855
        $properties = [];
856
        foreach ($fields as $field) {
857
            $properties[$field] = $defaultValues[$field];
858
        }
859
860
        return $properties;
861
    }
862
863
    /**
864
     * Append options to the form.
865
     *
866
     * @return array
867
     */
868
    protected function getEntityFormOptions()
869
    {
870
        return [];
871
    }
872
873
    /**
874
     * Get a model instance from the service container.
875
     *
876
     * @param $modelNameKey
877
     *
878
     * @return AbstractCommonModel
879
     */
880
    protected function getModel($modelNameKey)
881
    {
882
        return $this->get('mautic.model.factory')->getModel($modelNameKey);
883
    }
884
885
    /**
886
     * Returns a 404 Not Found.
887
     *
888
     * @param string $msg
889
     *
890
     * @return Response
891
     */
892
    protected function notFound($msg = 'mautic.core.error.notfound')
893
    {
894
        return $this->returnError($msg, Response::HTTP_NOT_FOUND);
895
    }
896
897
    /**
898
     * Gives child controllers opportunity to analyze and do whatever to an entity before populating the form.
899
     *
900
     * @param        $entity
901
     * @param        $parameters
902
     * @param string $action
903
     *
904
     * @return mixed
905
     */
906
    protected function prePopulateForm(&$entity, $parameters, $action = 'edit')
907
    {
908
    }
909
910
    /**
911
     * Give the controller an opportunity to process the entity before persisting.
912
     *
913
     * @param $entity
914
     * @param $form
915
     * @param $parameters
916
     * @param $action
917
     *
918
     * @return mixed
919
     */
920
    protected function preSaveEntity(&$entity, $form, $parameters, $action = 'edit')
921
    {
922
    }
923
924
    /**
925
     * Gives child controllers opportunity to analyze and do whatever to an entity before going through serializer.
926
     *
927
     * @param        $entity
928
     * @param string $action
929
     *
930
     * @return mixed
931
     */
932
    protected function preSerializeEntity(&$entity, $action = 'view')
933
    {
934
    }
935
936
    /**
937
     * Prepares entities returned from repository getEntities().
938
     *
939
     * @param $results
940
     *
941
     * @return array($entities, $totalCount)
942
     */
943
    protected function prepareEntitiesForView($results)
944
    {
945
        return $this->prepareEntityResultsToArray(
946
            $results,
947
            function ($entity) {
948
                $this->preSerializeEntity($entity);
949
            }
950
        );
951
    }
952
953
    /**
954
     * @param      $results
955
     * @param null $callback
956
     *
957
     * @return array($entities, $totalCount)
958
     */
959
    protected function prepareEntityResultsToArray($results, $callback = null)
960
    {
961
        if ($results instanceof Paginator) {
962
            $totalCount = count($results);
963
        } elseif (isset($results['count'])) {
964
            $totalCount = $results['count'];
965
            $results    = $results['results'];
966
        } else {
967
            $totalCount = count($results);
968
        }
969
970
        //we have to convert them from paginated proxy functions to entities in order for them to be
971
        //returned by the serializer/rest bundle
972
        $entities = [];
973
        foreach ($results as $key => $r) {
974
            if (is_array($r) && isset($r[0])) {
975
                //entity has some extra something something tacked onto the entities
976
                if (is_object($r[0])) {
977
                    foreach ($r as $k => $v) {
978
                        if (0 === $k) {
979
                            continue;
980
                        }
981
982
                        $r[0]->$k = $v;
983
                    }
984
                    $entities[$key] = $r[0];
985
                } elseif (is_array($r[0])) {
986
                    foreach ($r[0] as $k => $v) {
987
                        $r[$k] = $v;
988
                    }
989
                    unset($r[0]);
990
                    $entities[$key] = $r;
991
                }
992
            } else {
993
                $entities[$key] = $r;
994
            }
995
996
            if (is_callable($callback)) {
997
                $callback($entities[$key]);
998
            }
999
        }
1000
1001
        return [$entities, $totalCount];
1002
    }
1003
1004
    /**
1005
     * Convert posted parameters into what the form needs in order to successfully bind.
1006
     *
1007
     * @param $parameters
1008
     * @param $entity
1009
     * @param $action
1010
     *
1011
     * @return mixed
1012
     */
1013
    protected function prepareParametersForBinding($parameters, $entity, $action)
1014
    {
1015
        return $parameters;
1016
    }
1017
1018
    /**
1019
     * @param $key
1020
     * @param $entity
1021
     * @param $params
1022
     * @param $method
1023
     * @param $errors
1024
     * @param $entities
1025
     */
1026
    protected function processBatchForm($key, $entity, $params, $method, &$errors, &$entities)
1027
    {
1028
        $this->inBatchMode = true;
1029
        $formResponse      = $this->processForm($entity, $params, $method);
1030
        if ($formResponse instanceof Response) {
1031
            if (!$formResponse instanceof RedirectResponse) {
1032
                // Assume an error
1033
                $this->setBatchError(
1034
                    $key,
1035
                    InputHelper::string($formResponse->getContent()),
1036
                    $formResponse->getStatusCode(),
1037
                    $errors,
1038
                    $entities,
1039
                    $entity
1040
                );
1041
            }
1042
        } elseif (is_object($formResponse) && get_class($formResponse) === get_class($entity)) {
1043
            // Success
1044
            $entities[$key] = $formResponse;
1045
        } elseif (is_array($formResponse) && isset($formResponse['code'], $formResponse['message'])) {
1046
            // There was an error
1047
            $errors[$key] = $formResponse;
1048
        }
1049
1050
        $this->getDoctrine()->getManager()->detach($entity);
1051
1052
        $this->inBatchMode = false;
1053
    }
1054
1055
    /**
1056
     * Processes API Form.
1057
     *
1058
     * @param        $entity
1059
     * @param null   $parameters
1060
     * @param string $method
1061
     *
1062
     * @return mixed
1063
     */
1064
    protected function processForm($entity, $parameters = null, $method = 'PUT')
1065
    {
1066
        $categoryId = null;
1067
1068
        if (null === $parameters) {
1069
            //get from request
1070
            $parameters = $this->request->request->all();
1071
        }
1072
1073
        // Store the original parameters from the request so that callbacks can have access to them as needed
1074
        $this->entityRequestParameters = $parameters;
1075
1076
        //unset the ID in the parameters if set as this will cause the form to fail
1077
        if (isset($parameters['id'])) {
1078
            unset($parameters['id']);
1079
        }
1080
1081
        //is an entity being updated or created?
1082
        if ($entity->getId()) {
1083
            $statusCode = Response::HTTP_OK;
1084
            $action     = 'edit';
1085
        } else {
1086
            $statusCode = Response::HTTP_CREATED;
1087
            $action     = 'new';
1088
1089
            // All the properties have to be defined in order for validation to work
1090
            // Bug reported https://github.com/symfony/symfony/issues/19788
1091
            $defaultProperties = $this->getEntityDefaultProperties($entity);
1092
            $parameters        = array_merge($defaultProperties, $parameters);
1093
        }
1094
1095
        // Check if user has access to publish
1096
        if (
1097
            (
1098
                array_key_exists('isPublished', $parameters) ||
1099
                array_key_exists('publishUp', $parameters) ||
1100
                array_key_exists('publishDown', $parameters)
1101
            ) &&
1102
            $this->security->checkPermissionExists($this->permissionBase.':publish')) {
1103
            if ($this->security->checkPermissionExists($this->permissionBase.':publishown')) {
1104
                if (!$this->checkEntityAccess($entity, 'publish')) {
1105
                    if ('new' === $action) {
1106
                        $parameters['isPublished'] = 0;
1107
                    } else {
1108
                        unset($parameters['isPublished'], $parameters['publishUp'], $parameters['publishDown']);
1109
                    }
1110
                }
1111
            }
1112
        }
1113
1114
        $form         = $this->createEntityForm($entity);
1115
        $submitParams = $this->prepareParametersForBinding($parameters, $entity, $action);
1116
1117
        if ($submitParams instanceof Response) {
1118
            return $submitParams;
1119
        }
1120
1121
        // Remove category from the payload because it will cause form validation error.
1122
        if (isset($submitParams['category'])) {
1123
            $categoryId = (int) $submitParams['category'];
1124
            unset($submitParams['category']);
1125
        }
1126
1127
        $this->prepareParametersFromRequest($form, $submitParams, $entity, $this->dataInputMasks);
1128
1129
        $form->submit($submitParams, 'PATCH' !== $method);
1130
1131
        if ($form->isValid()) {
1132
            $this->setCategory($entity, $categoryId);
1133
            $preSaveError = $this->preSaveEntity($entity, $form, $submitParams, $action);
1134
1135
            if ($preSaveError instanceof Response) {
1136
                return $preSaveError;
1137
            }
1138
1139
            try {
1140
                if ($this->dispatcher->hasListeners(ApiEvents::API_ON_ENTITY_PRE_SAVE)) {
1141
                    $this->dispatcher->dispatch(ApiEvents::API_ON_ENTITY_PRE_SAVE, new ApiEntityEvent($entity, $this->entityRequestParameters, $this->request));
1142
                }
1143
            } catch (\Exception $e) {
1144
                return $this->returnError($e->getMessage(), $e->getCode());
1145
            }
1146
1147
            $this->model->saveEntity($entity);
1148
            $headers = [];
1149
            //return the newly created entities location if applicable
1150
            if (Response::HTTP_CREATED === $statusCode) {
1151
                $route = (null !== $this->get('router')->getRouteCollection()->get('mautic_api_'.$this->entityNameMulti.'_getone'))
1152
                    ? 'mautic_api_'.$this->entityNameMulti.'_getone' : 'mautic_api_get'.$this->entityNameOne;
1153
                $headers['Location'] = $this->generateUrl(
1154
                    $route,
1155
                    array_merge(['id' => $entity->getId()], $this->routeParams),
1156
                    true
1157
                );
1158
            }
1159
1160
            try {
1161
                if ($this->dispatcher->hasListeners(ApiEvents::API_ON_ENTITY_POST_SAVE)) {
1162
                    $this->dispatcher->dispatch(ApiEvents::API_ON_ENTITY_POST_SAVE, new ApiEntityEvent($entity, $this->entityRequestParameters, $this->request));
1163
                }
1164
            } catch (\Exception $e) {
1165
                return $this->returnError($e->getMessage(), $e->getCode());
1166
            }
1167
1168
            $this->preSerializeEntity($entity, $action);
1169
1170
            if ($this->inBatchMode) {
1171
                return $entity;
1172
            } else {
1173
                $view = $this->view([$this->entityNameOne => $entity], $statusCode, $headers);
1174
            }
1175
1176
            $this->setSerializationContext($view);
1177
        } else {
1178
            $formErrors = $this->getFormErrorMessages($form);
1179
            $msg        = $this->getFormErrorMessage($formErrors);
1180
1181
            if (!$msg) {
1182
                $msg = $this->translator->trans('mautic.core.error.badrequest', [], 'flashes');
1183
            }
1184
1185
            return $this->returnError($msg, Response::HTTP_BAD_REQUEST, $formErrors);
1186
        }
1187
1188
        return $this->handleView($view);
1189
    }
1190
1191
    /**
1192
     * Returns an error.
1193
     *
1194
     * @param string $msg
1195
     * @param int    $code
1196
     * @param array  $details
1197
     *
1198
     * @return Response|array
1199
     */
1200
    protected function returnError($msg, $code = Response::HTTP_INTERNAL_SERVER_ERROR, $details = [])
1201
    {
1202
        if ($this->get('translator')->hasId($msg, 'flashes')) {
1203
            $msg = $this->get('translator')->trans($msg, [], 'flashes');
1204
        } elseif ($this->get('translator')->hasId($msg, 'messages')) {
1205
            $msg = $this->get('translator')->trans($msg, [], 'messages');
1206
        }
1207
1208
        $error = [
1209
            'code'    => $code,
1210
            'message' => $msg,
1211
            'details' => $details,
1212
            'type'    => null,
1213
        ];
1214
1215
        if ($this->inBatchMode) {
1216
            return $error;
1217
        }
1218
1219
        $view = $this->view(
1220
            [
1221
                'errors' => [
1222
                    $error,
1223
                ],
1224
            ],
1225
            $code
1226
        );
1227
1228
        return $this->handleView($view);
1229
    }
1230
1231
    /**
1232
     * @param $where
1233
     */
1234
    protected function sanitizeWhereClauseArrayFromRequest(&$where)
1235
    {
1236
        foreach ($where as $key => $statement) {
1237
            if (isset($statement['internal'])) {
1238
                unset($where[$key]);
1239
            } elseif (in_array($statement['expr'], ['andX', 'orX'])) {
1240
                $this->sanitizeWhereClauseArrayFromRequest($statement['val']);
1241
            }
1242
        }
1243
    }
1244
1245
    /**
1246
     * @param object $entity
1247
     * @param int    $categoryId
1248
     *
1249
     * @throws \UnexpectedValueException
1250
     */
1251
    protected function setCategory($entity, $categoryId)
1252
    {
1253
        if (!empty($categoryId) && method_exists($entity, 'setCategory')) {
1254
            $category = $this->getDoctrine()->getManager()->find(Category::class, $categoryId);
1255
1256
            if (null === $category) {
1257
                throw new \UnexpectedValueException("Category $categoryId does not exist");
1258
            }
1259
1260
            $entity->setCategory($category);
1261
        }
1262
    }
1263
1264
    /**
1265
     * @param       $key
1266
     * @param       $msg
1267
     * @param       $code
1268
     * @param       $errors
1269
     * @param array $entities
1270
     * @param null  $entity
1271
     */
1272
    protected function setBatchError($key, $msg, $code, &$errors, &$entities = [], $entity = null)
1273
    {
1274
        unset($entities[$key]);
1275
        if ($entity) {
1276
            $this->getDoctrine()->getManager()->detach($entity);
1277
        }
1278
1279
        $errors[$key] = [
1280
            'message' => $this->get('translator')->hasId($msg, 'flashes') ? $this->get('translator')->trans($msg, [], 'flashes') : $msg,
1281
            'code'    => $code,
1282
            'type'    => 'api',
1283
        ];
1284
    }
1285
1286
    /**
1287
     * Set serialization groups and exclusion strategies.
1288
     *
1289
     * @param View $view
1290
     */
1291
    protected function setSerializationContext($view)
1292
    {
1293
        $context = $view->getContext();
1294
        if (!empty($this->serializerGroups)) {
1295
            $context->setGroups($this->serializerGroups);
1296
        }
1297
1298
        // Only include FormEntity properties for the top level entity and not the associated entities
1299
        $context->addExclusionStrategy(
1300
            new PublishDetailsExclusionStrategy()
1301
        );
1302
1303
        // Only include first level of children/parents
1304
        if ($this->parentChildrenLevelDepth) {
1305
            $context->addExclusionStrategy(
1306
                new ParentChildrenExclusionStrategy($this->parentChildrenLevelDepth)
1307
            );
1308
        }
1309
1310
        // Add custom exclusion strategies
1311
        foreach ($this->exclusionStrategies as $strategy) {
1312
            $context->addExclusionStrategy($strategy);
1313
        }
1314
1315
        // Include null values if a custom select has not been given
1316
        if (!$this->customSelectRequested) {
1317
            $context->setSerializeNull(true);
1318
        }
1319
1320
        $view->setContext($context);
1321
    }
1322
1323
    /**
1324
     * @param $parameters
1325
     *
1326
     * @return array|bool|Response
1327
     */
1328
    protected function validateBatchPayload($parameters)
1329
    {
1330
        $batchLimit = (int) $this->get('mautic.config')->getParameter('api_batch_max_limit', 200);
1331
        if (count($parameters) > $batchLimit) {
1332
            return $this->returnError($this->get('translator')->trans('mautic.api.call.batch_exception', ['%limit%' => $batchLimit]));
1333
        }
1334
1335
        return true;
1336
    }
1337
1338
    /**
1339
     * {@inheritdoc}
1340
     *
1341
     * @param null $data
1342
     * @param null $statusCode
1343
     */
1344
    protected function view($data = null, $statusCode = null, array $headers = [])
1345
    {
1346
        if ($data instanceof Paginator) {
1347
            // Get iterator out of Paginator class so that the entities are properly serialized by the serializer
1348
            $data = $data->getIterator()->getArrayCopy();
1349
        }
1350
1351
        $headers['Mautic-Version'] = $this->get('kernel')->getVersion();
1352
1353
        return parent::view($data, $statusCode, $headers);
1354
    }
1355
}
1356