Issues (3627)

ApiBundle/Controller/CommonApiController.php (12 issues)

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
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);
0 ignored issues
show
The method deleteEntity() does not exist on Mautic\CoreBundle\Model\AbstractCommonModel. It seems like you code against a sub-type of Mautic\CoreBundle\Model\AbstractCommonModel such as Mautic\CoreBundle\Model\FormModel. ( Ignorable by Annotation )

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

215
            $this->model->/** @scrutinizer ignore-call */ 
216
                          deleteEntity($entity);
Loading history...
216
            $this->getDoctrine()->getManager()->detach($entity);
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\Persistence\ObjectManager::detach() has been deprecated: Detach operation is deprecated and will be removed in Persistence 2.0. Please use {@see ObjectManager::clear()} instead. ( Ignorable by Annotation )

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

216
            /** @scrutinizer ignore-deprecated */ $this->getDoctrine()->getManager()->detach($entity);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
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();
0 ignored issues
show
Are you sure the assignment to $entity is correct as $this->model->getEntity() targeting Mautic\CoreBundle\Model\...ommonModel::getEntity() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
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();
0 ignored issues
show
Are you sure the assignment to $entity is correct as $this->model->getEntity() targeting Mautic\CoreBundle\Model\...ommonModel::getEntity() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
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(
0 ignored issues
show
The method createForm() does not exist on Mautic\CoreBundle\Model\AbstractCommonModel. It seems like you code against a sub-type of Mautic\CoreBundle\Model\AbstractCommonModel such as Mautic\CoreBundle\Model\FormModel. ( Ignorable by Annotation )

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

751
        return $this->model->/** @scrutinizer ignore-call */ createForm(
Loading history...
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;
0 ignored issues
show
$model is of type null, thus it always evaluated to false.
Loading history...
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) {
0 ignored issues
show
$formResponse is always a sub-type of Symfony\Component\HttpFoundation\Response.
Loading history...
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);
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\Persistence\ObjectManager::detach() has been deprecated: Detach operation is deprecated and will be removed in Persistence 2.0. Please use {@see ObjectManager::clear()} instead. ( Ignorable by Annotation )

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

1050
        /** @scrutinizer ignore-deprecated */ $this->getDoctrine()->getManager()->detach($entity);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
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) {
0 ignored issues
show
$submitParams is never a sub-type of Symfony\Component\HttpFoundation\Response.
Loading history...
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);
0 ignored issues
show
Are you sure the assignment to $preSaveError is correct as $this->preSaveEntity($en...$submitParams, $action) targeting Mautic\ApiBundle\Control...roller::preSaveEntity() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
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);
0 ignored issues
show
The method saveEntity() does not exist on Mautic\CoreBundle\Model\AbstractCommonModel. It seems like you code against a sub-type of Mautic\CoreBundle\Model\AbstractCommonModel such as Mautic\CampaignBundle\Model\EventLogModel or Mautic\CoreBundle\Model\FormModel. ( Ignorable by Annotation )

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

1147
            $this->model->/** @scrutinizer ignore-call */ 
1148
                          saveEntity($entity);
Loading history...
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) {
0 ignored issues
show
$entity is of type null, thus it always evaluated to false.
Loading history...
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