Completed
Push — 3.x ( 3e834f...38b337 )
by Grégoire
03:36
created

src/Controller/CRUDController.php (6 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the Sonata Project package.
7
 *
8
 * (c) Thomas Rabaix <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Sonata\AdminBundle\Controller;
15
16
use Doctrine\Inflector\InflectorFactory;
17
use Psr\Log\LoggerInterface;
18
use Psr\Log\NullLogger;
19
use Sonata\AdminBundle\Admin\AdminInterface;
20
use Sonata\AdminBundle\Admin\FieldDescriptionCollection;
21
use Sonata\AdminBundle\Datagrid\ProxyQueryInterface;
22
use Sonata\AdminBundle\Exception\LockException;
23
use Sonata\AdminBundle\Exception\ModelManagerException;
24
use Sonata\AdminBundle\Templating\TemplateRegistryInterface;
25
use Sonata\AdminBundle\Util\AdminObjectAclData;
26
use Sonata\AdminBundle\Util\AdminObjectAclManipulator;
27
use Symfony\Bundle\FrameworkBundle\Controller\ControllerTrait;
28
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
29
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
30
use Symfony\Component\DependencyInjection\ContainerInterface;
31
use Symfony\Component\Form\FormInterface;
32
use Symfony\Component\Form\FormRenderer;
33
use Symfony\Component\Form\FormView;
34
use Symfony\Component\HttpFoundation\JsonResponse;
35
use Symfony\Component\HttpFoundation\RedirectResponse;
36
use Symfony\Component\HttpFoundation\Request;
37
use Symfony\Component\HttpFoundation\Response;
38
use Symfony\Component\HttpKernel\Exception\HttpException;
39
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
40
use Symfony\Component\PropertyAccess\PropertyAccess;
41
use Symfony\Component\PropertyAccess\PropertyPath;
42
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
43
use Symfony\Component\Security\Csrf\CsrfToken;
44
45
/**
46
 * @author Thomas Rabaix <[email protected]>
47
 */
48
class CRUDController implements ContainerAwareInterface
49
{
50
    // NEXT_MAJOR: Don't use these traits anymore (inherit from Controller instead)
51
    use ControllerTrait, ContainerAwareTrait {
52
        ControllerTrait::render as originalRender;
53
    }
54
55
    /**
56
     * The related Admin class.
57
     *
58
     * @var AdminInterface
59
     */
60
    protected $admin;
61
62
    /**
63
     * The template registry of the related Admin class.
64
     *
65
     * @var TemplateRegistryInterface
66
     */
67
    private $templateRegistry;
68
69
    public function setContainer(?ContainerInterface $container = null)
70
    {
71
        $this->container = $container;
72
73
        $this->configure();
74
    }
75
76
    /**
77
     * NEXT_MAJOR: Remove this method.
78
     *
79
     * @see renderWithExtraParams()
80
     *
81
     * @param string               $view       The view name
82
     * @param array<string, mixed> $parameters An array of parameters to pass to the view
83
     *
84
     * @return Response A Response instance
85
     *
86
     * @deprecated since sonata-project/admin-bundle 3.27, to be removed in 4.0. Use Sonata\AdminBundle\Controller\CRUDController::renderWithExtraParams() instead.
87
     */
88
    public function render($view, array $parameters = [], ?Response $response = null)
89
    {
90
        @trigger_error(
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
91
            'Method '.__CLASS__.'::render has been renamed to '.__CLASS__.'::renderWithExtraParams.',
92
            E_USER_DEPRECATED
93
        );
94
95
        return $this->renderWithExtraParams($view, $parameters, $response);
96
    }
97
98
    /**
99
     * Renders a view while passing mandatory parameters on to the template.
100
     *
101
     * @param string               $view       The view name
102
     * @param array<string, mixed> $parameters An array of parameters to pass to the view
103
     *
104
     * @return Response A Response instance
105
     */
106
    public function renderWithExtraParams($view, array $parameters = [], ?Response $response = null)
107
    {
108
        //NEXT_MAJOR: Remove method alias and use $this->render() directly.
109
        return $this->originalRender($view, $this->addRenderExtraParams($parameters), $response);
110
    }
111
112
    /**
113
     * List action.
114
     *
115
     * @throws AccessDeniedException If access is not granted
116
     *
117
     * @return Response
118
     */
119
    public function listAction()
120
    {
121
        $request = $this->getRequest();
122
123
        $this->admin->checkAccess('list');
124
125
        $preResponse = $this->preList($request);
126
        if (null !== $preResponse) {
127
            return $preResponse;
128
        }
129
130
        if ($listMode = $request->get('_list_mode')) {
131
            $this->admin->setListMode($listMode);
132
        }
133
134
        $datagrid = $this->admin->getDatagrid();
135
        $formView = $datagrid->getForm()->createView();
136
137
        // set the theme for the current Admin Form
138
        $this->setFormTheme($formView, $this->admin->getFilterTheme());
139
140
        // NEXT_MAJOR: Remove this line and use commented line below it instead
141
        $template = $this->admin->getTemplate('list');
142
        // $template = $this->templateRegistry->getTemplate('list');
143
144
        return $this->renderWithExtraParams($template, [
145
            'action' => 'list',
146
            'form' => $formView,
147
            'datagrid' => $datagrid,
148
            'csrf_token' => $this->getCsrfToken('sonata.batch'),
149
            'export_formats' => $this->has('sonata.admin.admin_exporter') ?
150
                $this->get('sonata.admin.admin_exporter')->getAvailableFormats($this->admin) :
151
                $this->admin->getExportFormats(),
152
        ], null);
153
    }
154
155
    /**
156
     * Execute a batch delete.
157
     *
158
     * @throws AccessDeniedException If access is not granted
159
     *
160
     * @return RedirectResponse
161
     */
162
    public function batchActionDelete(ProxyQueryInterface $query)
163
    {
164
        $this->admin->checkAccess('batchDelete');
165
166
        $modelManager = $this->admin->getModelManager();
167
168
        try {
169
            $modelManager->batchDelete($this->admin->getClass(), $query);
170
            $this->addFlash(
171
                'sonata_flash_success',
172
                $this->trans('flash_batch_delete_success', [], 'SonataAdminBundle')
173
            );
174
        } catch (ModelManagerException $e) {
175
            $this->handleModelManagerException($e);
176
            $this->addFlash(
177
                'sonata_flash_error',
178
                $this->trans('flash_batch_delete_error', [], 'SonataAdminBundle')
179
            );
180
        }
181
182
        return $this->redirectToList();
183
    }
184
185
    /**
186
     * Delete action.
187
     *
188
     * @param int|string|null $id
189
     *
190
     * @throws NotFoundHttpException If the object does not exist
191
     * @throws AccessDeniedException If access is not granted
192
     *
193
     * @return Response|RedirectResponse
194
     */
195
    public function deleteAction($id) // NEXT_MAJOR: Remove the unused $id parameter
196
    {
197
        $request = $this->getRequest();
198
        $id = $request->get($this->admin->getIdParameter());
199
        $object = $this->admin->getObject($id);
200
201
        if (!$object) {
202
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
203
        }
204
205
        $this->checkParentChildAssociation($request, $object);
206
207
        $this->admin->checkAccess('delete', $object);
208
209
        $preResponse = $this->preDelete($request, $object);
210
        if (null !== $preResponse) {
211
            return $preResponse;
212
        }
213
214
        if (Request::METHOD_DELETE === $this->getRestMethod()) {
215
            // check the csrf token
216
            $this->validateCsrfToken('sonata.delete');
217
218
            $objectName = $this->admin->toString($object);
219
220
            try {
221
                $this->admin->delete($object);
222
223
                if ($this->isXmlHttpRequest()) {
224
                    return $this->renderJson(['result' => 'ok'], Response::HTTP_OK, []);
225
                }
226
227
                $this->addFlash(
228
                    'sonata_flash_success',
229
                    $this->trans(
230
                        'flash_delete_success',
231
                        ['%name%' => $this->escapeHtml($objectName)],
232
                        'SonataAdminBundle'
233
                    )
234
                );
235
            } catch (ModelManagerException $e) {
236
                $this->handleModelManagerException($e);
237
238
                if ($this->isXmlHttpRequest()) {
239
                    return $this->renderJson(['result' => 'error'], Response::HTTP_OK, []);
240
                }
241
242
                $this->addFlash(
243
                    'sonata_flash_error',
244
                    $this->trans(
245
                        'flash_delete_error',
246
                        ['%name%' => $this->escapeHtml($objectName)],
247
                        'SonataAdminBundle'
248
                    )
249
                );
250
            }
251
252
            return $this->redirectTo($object);
253
        }
254
255
        // NEXT_MAJOR: Remove this line and use commented line below it instead
256
        $template = $this->admin->getTemplate('delete');
257
        // $template = $this->templateRegistry->getTemplate('delete');
258
259
        return $this->renderWithExtraParams($template, [
260
            'object' => $object,
261
            'action' => 'delete',
262
            'csrf_token' => $this->getCsrfToken('sonata.delete'),
263
        ], null);
264
    }
265
266
    /**
267
     * Edit action.
268
     *
269
     * @param int|string|null $deprecatedId
270
     *
271
     * @throws NotFoundHttpException If the object does not exist
272
     * @throws AccessDeniedException If access is not granted
273
     *
274
     * @return Response|RedirectResponse
275
     */
276
    public function editAction($deprecatedId = null) // NEXT_MAJOR: Remove the unused $id parameter
277
    {
278
        if (isset(\func_get_args()[0])) {
279
            @trigger_error(
280
                sprintf(
281
                    'Support for the "id" route param as argument 1 at `%s()` is deprecated since sonata-project/admin-bundle 3.62 and will be removed in 4.0, use `AdminInterface::getIdParameter()` instead.',
282
                    __METHOD__
283
                ),
284
                E_USER_DEPRECATED
285
            );
286
        }
287
288
        // the key used to lookup the template
289
        $templateKey = 'edit';
290
291
        $request = $this->getRequest();
292
        $id = $request->get($this->admin->getIdParameter());
293
        $existingObject = $this->admin->getObject($id);
294
295
        if (!$existingObject) {
296
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
297
        }
298
299
        $this->checkParentChildAssociation($request, $existingObject);
300
301
        $this->admin->checkAccess('edit', $existingObject);
302
303
        $preResponse = $this->preEdit($request, $existingObject);
304
        if (null !== $preResponse) {
305
            return $preResponse;
306
        }
307
308
        $this->admin->setSubject($existingObject);
309
        $objectId = $this->admin->getNormalizedIdentifier($existingObject);
310
311
        $form = $this->admin->getForm();
312
313
        $form->setData($existingObject);
314
        $form->handleRequest($request);
315
316
        if ($form->isSubmitted()) {
317
            $isFormValid = $form->isValid();
318
319
            // persist if the form was valid and if in preview mode the preview was approved
320
            if ($isFormValid && (!$this->isInPreviewMode() || $this->isPreviewApproved())) {
321
                $submittedObject = $form->getData();
322
                $this->admin->setSubject($submittedObject);
323
324
                try {
325
                    $existingObject = $this->admin->update($submittedObject);
326
327
                    if ($this->isXmlHttpRequest()) {
328
                        return $this->handleXmlHttpRequestSuccessResponse($request, $existingObject);
329
                    }
330
331
                    $this->addFlash(
332
                        'sonata_flash_success',
333
                        $this->trans(
334
                            'flash_edit_success',
335
                            ['%name%' => $this->escapeHtml($this->admin->toString($existingObject))],
336
                            'SonataAdminBundle'
337
                        )
338
                    );
339
340
                    // redirect to edit mode
341
                    return $this->redirectTo($existingObject);
342
                } catch (ModelManagerException $e) {
343
                    $this->handleModelManagerException($e);
344
345
                    $isFormValid = false;
346
                } catch (LockException $e) {
347
                    $this->addFlash('sonata_flash_error', $this->trans('flash_lock_error', [
348
                        '%name%' => $this->escapeHtml($this->admin->toString($existingObject)),
349
                        '%link_start%' => '<a href="'.$this->admin->generateObjectUrl('edit', $existingObject).'">',
350
                        '%link_end%' => '</a>',
351
                    ], 'SonataAdminBundle'));
352
                }
353
            }
354
355
            // show an error message if the form failed validation
356
            if (!$isFormValid) {
357
                if ($this->isXmlHttpRequest() && null !== ($response = $this->handleXmlHttpRequestErrorResponse($request, $form))) {
358
                    return $response;
359
                }
360
361
                $this->addFlash(
362
                    'sonata_flash_error',
363
                    $this->trans(
364
                        'flash_edit_error',
365
                        ['%name%' => $this->escapeHtml($this->admin->toString($existingObject))],
366
                        'SonataAdminBundle'
367
                    )
368
                );
369
            } elseif ($this->isPreviewRequested()) {
370
                // enable the preview template if the form was valid and preview was requested
371
                $templateKey = 'preview';
372
                $this->admin->getShow();
373
            }
374
        }
375
376
        $formView = $form->createView();
377
        // set the theme for the current Admin Form
378
        $this->setFormTheme($formView, $this->admin->getFormTheme());
379
380
        // NEXT_MAJOR: Remove this line and use commented line below it instead
381
        $template = $this->admin->getTemplate($templateKey);
382
        // $template = $this->templateRegistry->getTemplate($templateKey);
383
384
        return $this->renderWithExtraParams($template, [
385
            'action' => 'edit',
386
            'form' => $formView,
387
            'object' => $existingObject,
388
            'objectId' => $objectId,
389
        ], null);
390
    }
391
392
    /**
393
     * Batch action.
394
     *
395
     * @throws NotFoundHttpException If the HTTP method is not POST
396
     * @throws \RuntimeException     If the batch action is not defined
397
     *
398
     * @return Response|RedirectResponse
399
     */
400
    public function batchAction()
401
    {
402
        $request = $this->getRequest();
403
        $restMethod = $this->getRestMethod();
404
405
        if (Request::METHOD_POST !== $restMethod) {
406
            throw $this->createNotFoundException(sprintf('Invalid request method given "%s", %s expected', $restMethod, Request::METHOD_POST));
407
        }
408
409
        // check the csrf token
410
        $this->validateCsrfToken('sonata.batch');
411
412
        $confirmation = $request->get('confirmation', false);
413
414
        if ($data = json_decode((string) $request->get('data'), true)) {
415
            $action = $data['action'];
416
            $idx = $data['idx'];
417
            $allElements = $data['all_elements'];
418
            $request->request->replace(array_merge($request->request->all(), $data));
419
        } else {
420
            $request->request->set('idx', $request->get('idx', []));
421
            $request->request->set('all_elements', $request->get('all_elements', false));
422
423
            $action = $request->get('action');
424
            $idx = $request->get('idx');
425
            $allElements = $request->get('all_elements');
426
            $data = $request->request->all();
427
428
            unset($data['_sonata_csrf_token']);
429
        }
430
431
        // NEXT_MAJOR: Remove reflection check.
432
        $reflector = new \ReflectionMethod($this->admin, 'getBatchActions');
433
        if ($reflector->getDeclaringClass()->getName() === \get_class($this->admin)) {
434
            @trigger_error(
435
                'Override Sonata\AdminBundle\Admin\AbstractAdmin::getBatchActions method'
436
                .' is deprecated since version 3.2.'
437
                .' Use Sonata\AdminBundle\Admin\AbstractAdmin::configureBatchActions instead.'
438
                .' The method will be final in 4.0.',
439
                E_USER_DEPRECATED
440
            );
441
        }
442
        $batchActions = $this->admin->getBatchActions();
443
        if (!\array_key_exists($action, $batchActions)) {
444
            throw new \RuntimeException(sprintf('The `%s` batch action is not defined', $action));
445
        }
446
447
        $camelizedAction = InflectorFactory::create()->build()->classify($action);
448
        $isRelevantAction = sprintf('batchAction%sIsRelevant', $camelizedAction);
449
450
        if (method_exists($this, $isRelevantAction)) {
451
            $nonRelevantMessage = $this->{$isRelevantAction}($idx, $allElements, $request);
452
        } else {
453
            $nonRelevantMessage = 0 !== \count($idx) || $allElements; // at least one item is selected
454
        }
455
456
        if (!$nonRelevantMessage) { // default non relevant message (if false of null)
457
            $nonRelevantMessage = 'flash_batch_empty';
458
        }
459
460
        $datagrid = $this->admin->getDatagrid();
461
        $datagrid->buildPager();
462
463
        if (true !== $nonRelevantMessage) {
464
            $this->addFlash(
465
                'sonata_flash_info',
466
                $this->trans($nonRelevantMessage, [], 'SonataAdminBundle')
467
            );
468
469
            return $this->redirectToList();
470
        }
471
472
        $askConfirmation = $batchActions[$action]['ask_confirmation'] ??
473
            true;
474
475
        if ($askConfirmation && 'ok' !== $confirmation) {
476
            $actionLabel = $batchActions[$action]['label'];
477
            $batchTranslationDomain = $batchActions[$action]['translation_domain'] ??
478
                $this->admin->getTranslationDomain();
479
480
            $formView = $datagrid->getForm()->createView();
481
            $this->setFormTheme($formView, $this->admin->getFilterTheme());
482
483
            // NEXT_MAJOR: Remove these lines and use commented lines below them instead
484
            $template = !empty($batchActions[$action]['template']) ?
485
                $batchActions[$action]['template'] :
486
                $this->admin->getTemplate('batch_confirmation');
487
            // $template = !empty($batchActions[$action]['template']) ?
488
            //     $batchActions[$action]['template'] :
489
            //     $this->templateRegistry->getTemplate('batch_confirmation');
490
491
            return $this->renderWithExtraParams($template, [
492
                'action' => 'list',
493
                'action_label' => $actionLabel,
494
                'batch_translation_domain' => $batchTranslationDomain,
495
                'datagrid' => $datagrid,
496
                'form' => $formView,
497
                'data' => $data,
498
                'csrf_token' => $this->getCsrfToken('sonata.batch'),
499
            ], null);
500
        }
501
502
        // execute the action, batchActionXxxxx
503
        $finalAction = sprintf('batchAction%s', $camelizedAction);
504
        if (!method_exists($this, $finalAction)) {
505
            throw new \RuntimeException(sprintf('A `%s::%s` method must be callable', static::class, $finalAction));
506
        }
507
508
        $query = $datagrid->getQuery();
509
510
        $query->setFirstResult(null);
511
        $query->setMaxResults(null);
512
513
        $this->admin->preBatchAction($action, $query, $idx, $allElements);
514
515
        if (\count($idx) > 0) {
516
            $this->admin->getModelManager()->addIdentifiersToQuery($this->admin->getClass(), $query, $idx);
517
        } elseif (!$allElements) {
518
            $this->addFlash(
519
                'sonata_flash_info',
520
                $this->trans('flash_batch_no_elements_processed', [], 'SonataAdminBundle')
521
            );
522
523
            return $this->redirectToList();
524
        }
525
526
        return $this->{$finalAction}($query, $request);
527
    }
528
529
    /**
530
     * Create action.
531
     *
532
     * @throws AccessDeniedException If access is not granted
533
     *
534
     * @return Response
535
     */
536
    public function createAction()
537
    {
538
        $request = $this->getRequest();
539
        // the key used to lookup the template
540
        $templateKey = 'edit';
541
542
        $this->admin->checkAccess('create');
543
544
        $class = new \ReflectionClass($this->admin->hasActiveSubClass() ? $this->admin->getActiveSubClass() : $this->admin->getClass());
545
546
        if ($class->isAbstract()) {
547
            return $this->renderWithExtraParams(
548
                '@SonataAdmin/CRUD/select_subclass.html.twig',
549
                [
550
                    'base_template' => $this->getBaseTemplate(),
551
                    'admin' => $this->admin,
552
                    'action' => 'create',
553
                ],
554
                null
555
            );
556
        }
557
558
        $newObject = $this->admin->getNewInstance();
559
560
        $preResponse = $this->preCreate($request, $newObject);
561
        if (null !== $preResponse) {
562
            return $preResponse;
563
        }
564
565
        $this->admin->setSubject($newObject);
566
567
        $form = $this->admin->getForm();
568
569
        $form->setData($newObject);
570
        $form->handleRequest($request);
571
572
        if ($form->isSubmitted()) {
573
            $isFormValid = $form->isValid();
574
575
            // persist if the form was valid and if in preview mode the preview was approved
576
            if ($isFormValid && (!$this->isInPreviewMode() || $this->isPreviewApproved())) {
577
                $submittedObject = $form->getData();
578
                $this->admin->setSubject($submittedObject);
579
                $this->admin->checkAccess('create', $submittedObject);
580
581
                try {
582
                    $newObject = $this->admin->create($submittedObject);
583
584
                    if ($this->isXmlHttpRequest()) {
585
                        return $this->handleXmlHttpRequestSuccessResponse($request, $newObject);
586
                    }
587
588
                    $this->addFlash(
589
                        'sonata_flash_success',
590
                        $this->trans(
591
                            'flash_create_success',
592
                            ['%name%' => $this->escapeHtml($this->admin->toString($newObject))],
593
                            'SonataAdminBundle'
594
                        )
595
                    );
596
597
                    // redirect to edit mode
598
                    return $this->redirectTo($newObject);
599
                } catch (ModelManagerException $e) {
600
                    $this->handleModelManagerException($e);
601
602
                    $isFormValid = false;
603
                }
604
            }
605
606
            // show an error message if the form failed validation
607
            if (!$isFormValid) {
608
                if ($this->isXmlHttpRequest() && null !== ($response = $this->handleXmlHttpRequestErrorResponse($request, $form))) {
609
                    return $response;
610
                }
611
612
                $this->addFlash(
613
                    'sonata_flash_error',
614
                    $this->trans(
615
                        'flash_create_error',
616
                        ['%name%' => $this->escapeHtml($this->admin->toString($newObject))],
617
                        'SonataAdminBundle'
618
                    )
619
                );
620
            } elseif ($this->isPreviewRequested()) {
621
                // pick the preview template if the form was valid and preview was requested
622
                $templateKey = 'preview';
623
                $this->admin->getShow();
624
            }
625
        }
626
627
        $formView = $form->createView();
628
        // set the theme for the current Admin Form
629
        $this->setFormTheme($formView, $this->admin->getFormTheme());
630
631
        // NEXT_MAJOR: Remove this line and use commented line below it instead
632
        $template = $this->admin->getTemplate($templateKey);
633
        // $template = $this->templateRegistry->getTemplate($templateKey);
634
635
        return $this->renderWithExtraParams($template, [
636
            'action' => 'create',
637
            'form' => $formView,
638
            'object' => $newObject,
639
            'objectId' => null,
640
        ], null);
641
    }
642
643
    /**
644
     * Show action.
645
     *
646
     * @param int|string|null $deprecatedId
647
     *
648
     * @throws NotFoundHttpException If the object does not exist
649
     * @throws AccessDeniedException If access is not granted
650
     *
651
     * @return Response
652
     */
653
    public function showAction($deprecatedId = null) // NEXT_MAJOR: Remove the unused $id parameter
654
    {
655
        if (isset(\func_get_args()[0])) {
656
            @trigger_error(
657
                sprintf(
658
                    'Support for the "id" route param as argument 1 at `%s()` is deprecated since sonata-project/admin-bundle 3.62 and will be removed in 4.0, use `AdminInterface::getIdParameter()` instead.',
659
                    __METHOD__
660
                ),
661
                E_USER_DEPRECATED
662
            );
663
        }
664
665
        $request = $this->getRequest();
666
        $id = $request->get($this->admin->getIdParameter());
667
        $object = $this->admin->getObject($id);
668
669
        if (!$object) {
670
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
671
        }
672
673
        $this->checkParentChildAssociation($request, $object);
674
675
        $this->admin->checkAccess('show', $object);
676
677
        $preResponse = $this->preShow($request, $object);
678
        if (null !== $preResponse) {
679
            return $preResponse;
680
        }
681
682
        $this->admin->setSubject($object);
683
684
        $fields = $this->admin->getShow();
685
        \assert($fields instanceof FieldDescriptionCollection);
686
687
        // NEXT_MAJOR: Remove this line and use commented line below it instead
688
        $template = $this->admin->getTemplate('show');
689
        //$template = $this->templateRegistry->getTemplate('show');
690
691
        return $this->renderWithExtraParams($template, [
692
            'action' => 'show',
693
            'object' => $object,
694
            'elements' => $fields,
695
        ], null);
696
    }
697
698
    /**
699
     * Show history revisions for object.
700
     *
701
     * @param int|string|null $deprecatedId
702
     *
703
     * @throws AccessDeniedException If access is not granted
704
     * @throws NotFoundHttpException If the object does not exist or the audit reader is not available
705
     *
706
     * @return Response
707
     */
708
    public function historyAction($deprecatedId = null) // NEXT_MAJOR: Remove the unused $id parameter
709
    {
710
        if (isset(\func_get_args()[0])) {
711
            @trigger_error(
712
                sprintf(
713
                    'Support for the "id" route param as argument 1 at `%s()` is deprecated since sonata-project/admin-bundle 3.62 and will be removed in 4.0, use `AdminInterface::getIdParameter()` instead.',
714
                    __METHOD__
715
                ),
716
                E_USER_DEPRECATED
717
            );
718
        }
719
720
        $request = $this->getRequest();
721
        $id = $request->get($this->admin->getIdParameter());
722
        $object = $this->admin->getObject($id);
723
724
        if (!$object) {
725
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
726
        }
727
728
        $this->admin->checkAccess('history', $object);
729
730
        $manager = $this->get('sonata.admin.audit.manager');
731
732
        if (!$manager->hasReader($this->admin->getClass())) {
733
            throw $this->createNotFoundException(
734
                sprintf(
735
                    'unable to find the audit reader for class : %s',
736
                    $this->admin->getClass()
737
                )
738
            );
739
        }
740
741
        $reader = $manager->getReader($this->admin->getClass());
742
743
        $revisions = $reader->findRevisions($this->admin->getClass(), $id);
744
745
        // NEXT_MAJOR: Remove this line and use commented line below it instead
746
        $template = $this->admin->getTemplate('history');
747
        // $template = $this->templateRegistry->getTemplate('history');
748
749
        return $this->renderWithExtraParams($template, [
750
            'action' => 'history',
751
            'object' => $object,
752
            'revisions' => $revisions,
753
            'currentRevision' => $revisions ? current($revisions) : false,
754
        ], null);
755
    }
756
757
    /**
758
     * View history revision of object.
759
     *
760
     * @param int|string|null $id
761
     * @param string|null     $revision
762
     *
763
     * @throws AccessDeniedException If access is not granted
764
     * @throws NotFoundHttpException If the object or revision does not exist or the audit reader is not available
765
     *
766
     * @return Response
767
     */
768
    public function historyViewRevisionAction($id = null, $revision = null) // NEXT_MAJOR: Remove the unused $id parameter
769
    {
770
        $request = $this->getRequest();
771
        $id = $request->get($this->admin->getIdParameter());
772
        $object = $this->admin->getObject($id);
773
774
        if (!$object) {
775
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
776
        }
777
778
        $this->admin->checkAccess('historyViewRevision', $object);
779
780
        $manager = $this->get('sonata.admin.audit.manager');
781
782
        if (!$manager->hasReader($this->admin->getClass())) {
783
            throw $this->createNotFoundException(
784
                sprintf(
785
                    'unable to find the audit reader for class : %s',
786
                    $this->admin->getClass()
787
                )
788
            );
789
        }
790
791
        $reader = $manager->getReader($this->admin->getClass());
792
793
        // retrieve the revisioned object
794
        $object = $reader->find($this->admin->getClass(), $id, $revision);
795
796
        if (!$object) {
797
            throw $this->createNotFoundException(
798
                sprintf(
799
                    'unable to find the targeted object `%s` from the revision `%s` with classname : `%s`',
800
                    $id,
801
                    $revision,
802
                    $this->admin->getClass()
803
                )
804
            );
805
        }
806
807
        $this->admin->setSubject($object);
808
809
        // NEXT_MAJOR: Remove this line and use commented line below it instead
810
        $template = $this->admin->getTemplate('show');
811
        // $template = $this->templateRegistry->getTemplate('show');
812
813
        return $this->renderWithExtraParams($template, [
814
            'action' => 'show',
815
            'object' => $object,
816
            'elements' => $this->admin->getShow(),
817
        ], null);
818
    }
819
820
    /**
821
     * Compare history revisions of object.
822
     *
823
     * @param int|string|null $id
824
     * @param int|string|null $base_revision
825
     * @param int|string|null $compare_revision
826
     *
827
     * @throws AccessDeniedException If access is not granted
828
     * @throws NotFoundHttpException If the object or revision does not exist or the audit reader is not available
829
     *
830
     * @return Response
831
     */
832
    public function historyCompareRevisionsAction($id = null, $base_revision = null, $compare_revision = null) // NEXT_MAJOR: Remove the unused $id parameter
833
    {
834
        $this->admin->checkAccess('historyCompareRevisions');
835
836
        $request = $this->getRequest();
837
        $id = $request->get($this->admin->getIdParameter());
838
        $object = $this->admin->getObject($id);
839
840
        if (!$object) {
841
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
842
        }
843
844
        $manager = $this->get('sonata.admin.audit.manager');
845
846
        if (!$manager->hasReader($this->admin->getClass())) {
847
            throw $this->createNotFoundException(
848
                sprintf(
849
                    'unable to find the audit reader for class : %s',
850
                    $this->admin->getClass()
851
                )
852
            );
853
        }
854
855
        $reader = $manager->getReader($this->admin->getClass());
856
857
        // retrieve the base revision
858
        $base_object = $reader->find($this->admin->getClass(), $id, $base_revision);
859
        if (!$base_object) {
860
            throw $this->createNotFoundException(
861
                sprintf(
862
                    'unable to find the targeted object `%s` from the revision `%s` with classname : `%s`',
863
                    $id,
864
                    $base_revision,
865
                    $this->admin->getClass()
866
                )
867
            );
868
        }
869
870
        // retrieve the compare revision
871
        $compare_object = $reader->find($this->admin->getClass(), $id, $compare_revision);
872
        if (!$compare_object) {
873
            throw $this->createNotFoundException(
874
                sprintf(
875
                    'unable to find the targeted object `%s` from the revision `%s` with classname : `%s`',
876
                    $id,
877
                    $compare_revision,
878
                    $this->admin->getClass()
879
                )
880
            );
881
        }
882
883
        $this->admin->setSubject($base_object);
884
885
        // NEXT_MAJOR: Remove this line and use commented line below it instead
886
        $template = $this->admin->getTemplate('show_compare');
887
        // $template = $this->templateRegistry->getTemplate('show_compare');
888
889
        return $this->renderWithExtraParams($template, [
890
            'action' => 'show',
891
            'object' => $base_object,
892
            'object_compare' => $compare_object,
893
            'elements' => $this->admin->getShow(),
894
        ], null);
895
    }
896
897
    /**
898
     * Export data to specified format.
899
     *
900
     * @throws AccessDeniedException If access is not granted
901
     * @throws \RuntimeException     If the export format is invalid
902
     *
903
     * @return Response
904
     */
905
    public function exportAction(Request $request)
906
    {
907
        $this->admin->checkAccess('export');
908
909
        $format = $request->get('format');
910
911
        // NEXT_MAJOR: remove the check
912
        if (!$this->has('sonata.admin.admin_exporter')) {
913
            @trigger_error(
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
914
                'Not registering the exporter bundle is deprecated since version 3.14.'
915
                .' You must register it to be able to use the export action in 4.0.',
916
                E_USER_DEPRECATED
917
            );
918
            $allowedExportFormats = (array) $this->admin->getExportFormats();
919
920
            $class = (string) $this->admin->getClass();
921
            $filename = sprintf(
922
                'export_%s_%s.%s',
923
                strtolower((string) substr($class, strripos($class, '\\') + 1)),
924
                date('Y_m_d_H_i_s', strtotime('now')),
925
                $format
926
            );
927
            $exporter = $this->get('sonata.admin.exporter');
928
        } else {
929
            $adminExporter = $this->get('sonata.admin.admin_exporter');
930
            $allowedExportFormats = $adminExporter->getAvailableFormats($this->admin);
931
            $filename = $adminExporter->getExportFilename($this->admin, $format);
932
            $exporter = $this->get('sonata.exporter.exporter');
933
        }
934
935
        if (!\in_array($format, $allowedExportFormats, true)) {
936
            throw new \RuntimeException(
937
                sprintf(
938
                    'Export in format `%s` is not allowed for class: `%s`. Allowed formats are: `%s`',
939
                    $format,
940
                    $this->admin->getClass(),
941
                    implode(', ', $allowedExportFormats)
942
                )
943
            );
944
        }
945
946
        return $exporter->getResponse(
947
            $format,
948
            $filename,
949
            $this->admin->getDataSourceIterator()
950
        );
951
    }
952
953
    /**
954
     * Returns the Response object associated to the acl action.
955
     *
956
     * @param int|string|null $deprecatedId
957
     *
958
     * @throws AccessDeniedException If access is not granted
959
     * @throws NotFoundHttpException If the object does not exist or the ACL is not enabled
960
     *
961
     * @return Response|RedirectResponse
962
     */
963
    public function aclAction($deprecatedId = null) // NEXT_MAJOR: Remove the unused $id parameter
964
    {
965
        if (isset(\func_get_args()[0])) {
966
            @trigger_error(
967
                sprintf(
968
                    'Support for the "id" route param as argument 1 at `%s()` is deprecated since sonata-project/admin-bundle 3.62 and will be removed in 4.0, use `AdminInterface::getIdParameter()` instead.',
969
                    __METHOD__
970
                ),
971
                E_USER_DEPRECATED
972
            );
973
        }
974
975
        if (!$this->admin->isAclEnabled()) {
976
            throw $this->createNotFoundException('ACL are not enabled for this admin');
977
        }
978
979
        $request = $this->getRequest();
980
        $id = $request->get($this->admin->getIdParameter());
981
        $object = $this->admin->getObject($id);
982
983
        if (!$object) {
984
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
985
        }
986
987
        $this->admin->checkAccess('acl', $object);
988
989
        $this->admin->setSubject($object);
990
        $aclUsers = $this->getAclUsers();
991
        $aclRoles = $this->getAclRoles();
992
993
        $adminObjectAclManipulator = $this->get('sonata.admin.object.manipulator.acl.admin');
994
        $adminObjectAclData = new AdminObjectAclData(
995
            $this->admin,
996
            $object,
997
            $aclUsers,
998
            $adminObjectAclManipulator->getMaskBuilderClass(),
999
            $aclRoles
1000
        );
1001
1002
        $aclUsersForm = $adminObjectAclManipulator->createAclUsersForm($adminObjectAclData);
1003
        $aclRolesForm = $adminObjectAclManipulator->createAclRolesForm($adminObjectAclData);
1004
1005
        if (Request::METHOD_POST === $request->getMethod()) {
1006
            if ($request->request->has(AdminObjectAclManipulator::ACL_USERS_FORM_NAME)) {
1007
                $form = $aclUsersForm;
1008
                $updateMethod = 'updateAclUsers';
1009
            } elseif ($request->request->has(AdminObjectAclManipulator::ACL_ROLES_FORM_NAME)) {
1010
                $form = $aclRolesForm;
1011
                $updateMethod = 'updateAclRoles';
1012
            }
1013
1014
            if (isset($form)) {
1015
                $form->handleRequest($request);
1016
1017
                if ($form->isValid()) {
1018
                    $adminObjectAclManipulator->$updateMethod($adminObjectAclData);
0 ignored issues
show
The variable $updateMethod does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1019
                    $this->addFlash(
1020
                        'sonata_flash_success',
1021
                        $this->trans('flash_acl_edit_success', [], 'SonataAdminBundle')
1022
                    );
1023
1024
                    return new RedirectResponse($this->admin->generateObjectUrl('acl', $object));
1025
                }
1026
            }
1027
        }
1028
1029
        // NEXT_MAJOR: Remove this line and use commented line below it instead
1030
        $template = $this->admin->getTemplate('acl');
1031
        // $template = $this->templateRegistry->getTemplate('acl');
1032
1033
        return $this->renderWithExtraParams($template, [
1034
            'action' => 'acl',
1035
            'permissions' => $adminObjectAclData->getUserPermissions(),
1036
            'object' => $object,
1037
            'users' => $aclUsers,
1038
            'roles' => $aclRoles,
1039
            'aclUsersForm' => $aclUsersForm->createView(),
1040
            'aclRolesForm' => $aclRolesForm->createView(),
1041
        ], null);
1042
    }
1043
1044
    /**
1045
     * @return Request
1046
     */
1047
    public function getRequest()
1048
    {
1049
        return $this->container->get('request_stack')->getCurrentRequest();
1050
    }
1051
1052
    /**
1053
     * @param array<string, mixed> $parameters
1054
     *
1055
     * @return array<string, mixed>
1056
     */
1057
    protected function addRenderExtraParams(array $parameters = []): array
1058
    {
1059
        if (!$this->isXmlHttpRequest()) {
1060
            $parameters['breadcrumbs_builder'] = $this->get('sonata.admin.breadcrumbs_builder');
1061
        }
1062
1063
        $parameters['admin'] = $parameters['admin'] ?? $this->admin;
1064
        $parameters['base_template'] = $parameters['base_template'] ?? $this->getBaseTemplate();
1065
        $parameters['admin_pool'] = $this->get('sonata.admin.pool');
1066
1067
        return $parameters;
1068
    }
1069
1070
    /**
1071
     * Gets a container configuration parameter by its name.
1072
     *
1073
     * @param string $name The parameter name
1074
     *
1075
     * @return mixed
1076
     */
1077
    protected function getParameter($name)
1078
    {
1079
        return $this->container->getParameter($name);
0 ignored issues
show
It seems like you code against a concrete implementation and not the interface Psr\Container\ContainerInterface as the method getParameter() does only exist in the following implementations of said interface: Symfony\Bundle\FrameworkBundle\Test\TestContainer, Symfony\Component\Depend...urationContainerBuilder, Symfony\Component\DependencyInjection\Container, Symfony\Component\Depend...ection\ContainerBuilder.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
1080
    }
1081
1082
    /**
1083
     * Render JSON.
1084
     *
1085
     * @param mixed $data
1086
     * @param int   $status
1087
     * @param array $headers
1088
     *
1089
     * @return JsonResponse with json encoded data
1090
     */
1091
    protected function renderJson($data, $status = Response::HTTP_OK, $headers = [])
1092
    {
1093
        return new JsonResponse($data, $status, $headers);
1094
    }
1095
1096
    /**
1097
     * Returns true if the request is a XMLHttpRequest.
1098
     *
1099
     * @return bool True if the request is an XMLHttpRequest, false otherwise
1100
     */
1101
    protected function isXmlHttpRequest()
1102
    {
1103
        $request = $this->getRequest();
1104
1105
        return $request->isXmlHttpRequest() || $request->get('_xml_http_request');
1106
    }
1107
1108
    /**
1109
     * Returns the correct RESTful verb, given either by the request itself or
1110
     * via the "_method" parameter.
1111
     *
1112
     * @return string HTTP method, either
1113
     */
1114
    protected function getRestMethod()
1115
    {
1116
        $request = $this->getRequest();
1117
1118
        if (Request::getHttpMethodParameterOverride() || !$request->request->has('_method')) {
1119
            return $request->getMethod();
1120
        }
1121
1122
        return $request->request->get('_method');
1123
    }
1124
1125
    /**
1126
     * Contextualize the admin class depends on the current request.
1127
     *
1128
     * @throws \RuntimeException
1129
     */
1130
    protected function configure()
1131
    {
1132
        $request = $this->getRequest();
1133
1134
        $adminCode = $request->get('_sonata_admin');
1135
1136
        if (!$adminCode) {
1137
            throw new \RuntimeException(sprintf(
1138
                'There is no `_sonata_admin` defined for the controller `%s` and the current route `%s`',
1139
                static::class,
1140
                $request->get('_route')
1141
            ));
1142
        }
1143
1144
        $this->admin = $this->container->get('sonata.admin.pool')->getAdminByAdminCode($adminCode);
1145
1146
        if (!$this->admin) {
1147
            throw new \RuntimeException(sprintf(
1148
                'Unable to find the admin class related to the current controller (%s)',
1149
                static::class
1150
            ));
1151
        }
1152
1153
        $this->templateRegistry = $this->container->get($this->admin->getCode().'.template_registry');
1154
        if (!$this->templateRegistry instanceof TemplateRegistryInterface) {
1155
            throw new \RuntimeException(sprintf(
1156
                'Unable to find the template registry related to the current admin (%s)',
1157
                $this->admin->getCode()
1158
            ));
1159
        }
1160
1161
        $rootAdmin = $this->admin;
1162
1163
        while ($rootAdmin->isChild()) {
1164
            $rootAdmin->setCurrentChild(true);
1165
            $rootAdmin = $rootAdmin->getParent();
1166
        }
1167
1168
        $rootAdmin->setRequest($request);
1169
1170
        if ($request->get('uniqid')) {
1171
            $this->admin->setUniqid($request->get('uniqid'));
1172
        }
1173
    }
1174
1175
    /**
1176
     * Proxy for the logger service of the container.
1177
     * If no such service is found, a NullLogger is returned.
1178
     *
1179
     * @return LoggerInterface
1180
     */
1181
    protected function getLogger()
1182
    {
1183
        if ($this->container->has('logger')) {
1184
            $logger = $this->container->get('logger');
1185
            \assert($logger instanceof LoggerInterface);
1186
1187
            return $logger;
1188
        }
1189
1190
        return new NullLogger();
1191
    }
1192
1193
    /**
1194
     * Returns the base template name.
1195
     *
1196
     * @return string The template name
1197
     */
1198
    protected function getBaseTemplate()
1199
    {
1200
        if ($this->isXmlHttpRequest()) {
1201
            // NEXT_MAJOR: Remove this line and use commented line below it instead
1202
            return $this->admin->getTemplate('ajax');
1203
            // return $this->templateRegistry->getTemplate('ajax');
1204
        }
1205
1206
        // NEXT_MAJOR: Remove this line and use commented line below it instead
1207
        return $this->admin->getTemplate('layout');
1208
        // return $this->templateRegistry->getTemplate('layout');
1209
    }
1210
1211
    /**
1212
     * @throws \Exception
1213
     */
1214
    protected function handleModelManagerException(\Exception $e)
1215
    {
1216
        if ($this->get('kernel')->isDebug()) {
1217
            throw $e;
1218
        }
1219
1220
        $context = ['exception' => $e];
1221
        if ($e->getPrevious()) {
1222
            $context['previous_exception_message'] = $e->getPrevious()->getMessage();
1223
        }
1224
        $this->getLogger()->error($e->getMessage(), $context);
1225
    }
1226
1227
    /**
1228
     * Redirect the user depend on this choice.
1229
     *
1230
     * @param object $object
1231
     *
1232
     * @return RedirectResponse
1233
     */
1234
    protected function redirectTo($object)
1235
    {
1236
        $request = $this->getRequest();
1237
1238
        $url = false;
1239
1240
        if (null !== $request->get('btn_update_and_list')) {
1241
            return $this->redirectToList();
1242
        }
1243
        if (null !== $request->get('btn_create_and_list')) {
1244
            return $this->redirectToList();
1245
        }
1246
1247
        if (null !== $request->get('btn_create_and_create')) {
1248
            $params = [];
1249
            if ($this->admin->hasActiveSubClass()) {
1250
                $params['subclass'] = $request->get('subclass');
1251
            }
1252
            $url = $this->admin->generateUrl('create', $params);
1253
        }
1254
1255
        if ('DELETE' === $this->getRestMethod()) {
1256
            return $this->redirectToList();
1257
        }
1258
1259
        if (!$url) {
1260
            foreach (['edit', 'show'] as $route) {
1261
                if ($this->admin->hasRoute($route) && $this->admin->hasAccess($route, $object)) {
1262
                    $url = $this->admin->generateObjectUrl(
1263
                        $route,
1264
                        $object,
1265
                        $this->getSelectedTab($request)
1266
                    );
1267
1268
                    break;
1269
                }
1270
            }
1271
        }
1272
1273
        if (!$url) {
1274
            return $this->redirectToList();
1275
        }
1276
1277
        return new RedirectResponse($url);
1278
    }
1279
1280
    /**
1281
     * Redirects the user to the list view.
1282
     *
1283
     * @return RedirectResponse
1284
     */
1285
    final protected function redirectToList()
1286
    {
1287
        $parameters = [];
1288
1289
        if ($filter = $this->admin->getFilterParameters()) {
1290
            $parameters['filter'] = $filter;
1291
        }
1292
1293
        return $this->redirect($this->admin->generateUrl('list', $parameters));
1294
    }
1295
1296
    /**
1297
     * Returns true if the preview is requested to be shown.
1298
     *
1299
     * @return bool
1300
     */
1301
    protected function isPreviewRequested()
1302
    {
1303
        $request = $this->getRequest();
1304
1305
        return null !== $request->get('btn_preview');
1306
    }
1307
1308
    /**
1309
     * Returns true if the preview has been approved.
1310
     *
1311
     * @return bool
1312
     */
1313
    protected function isPreviewApproved()
1314
    {
1315
        $request = $this->getRequest();
1316
1317
        return null !== $request->get('btn_preview_approve');
1318
    }
1319
1320
    /**
1321
     * Returns true if the request is in the preview workflow.
1322
     *
1323
     * That means either a preview is requested or the preview has already been shown
1324
     * and it got approved/declined.
1325
     *
1326
     * @return bool
1327
     */
1328
    protected function isInPreviewMode()
1329
    {
1330
        return $this->admin->supportsPreviewMode()
1331
        && ($this->isPreviewRequested()
1332
            || $this->isPreviewApproved()
1333
            || $this->isPreviewDeclined());
1334
    }
1335
1336
    /**
1337
     * Returns true if the preview has been declined.
1338
     *
1339
     * @return bool
1340
     */
1341
    protected function isPreviewDeclined()
1342
    {
1343
        $request = $this->getRequest();
1344
1345
        return null !== $request->get('btn_preview_decline');
1346
    }
1347
1348
    /**
1349
     * Gets ACL users.
1350
     *
1351
     * @return \Traversable
1352
     */
1353
    protected function getAclUsers()
1354
    {
1355
        $aclUsers = [];
1356
1357
        $userManagerServiceName = $this->container->getParameter('sonata.admin.security.acl_user_manager');
0 ignored issues
show
It seems like you code against a concrete implementation and not the interface Psr\Container\ContainerInterface as the method getParameter() does only exist in the following implementations of said interface: Symfony\Bundle\FrameworkBundle\Test\TestContainer, Symfony\Component\Depend...urationContainerBuilder, Symfony\Component\DependencyInjection\Container, Symfony\Component\Depend...ection\ContainerBuilder.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
1358
        if (null !== $userManagerServiceName && $this->has($userManagerServiceName)) {
1359
            $userManager = $this->get($userManagerServiceName);
1360
1361
            if (method_exists($userManager, 'findUsers')) {
1362
                $aclUsers = $userManager->findUsers();
1363
            }
1364
        }
1365
1366
        return \is_array($aclUsers) ? new \ArrayIterator($aclUsers) : $aclUsers;
1367
    }
1368
1369
    /**
1370
     * Gets ACL roles.
1371
     *
1372
     * @return \Traversable
1373
     */
1374
    protected function getAclRoles()
1375
    {
1376
        $aclRoles = [];
1377
        $roleHierarchy = $this->container->getParameter('security.role_hierarchy.roles');
0 ignored issues
show
It seems like you code against a concrete implementation and not the interface Psr\Container\ContainerInterface as the method getParameter() does only exist in the following implementations of said interface: Symfony\Bundle\FrameworkBundle\Test\TestContainer, Symfony\Component\Depend...urationContainerBuilder, Symfony\Component\DependencyInjection\Container, Symfony\Component\Depend...ection\ContainerBuilder.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
1378
        $pool = $this->container->get('sonata.admin.pool');
1379
1380
        foreach ($pool->getAdminServiceIds() as $id) {
1381
            try {
1382
                $admin = $pool->getInstance($id);
1383
            } catch (\Exception $e) {
1384
                continue;
1385
            }
1386
1387
            $baseRole = $admin->getSecurityHandler()->getBaseRole($admin);
1388
            foreach ($admin->getSecurityInformation() as $role => $permissions) {
1389
                $role = sprintf($baseRole, $role);
1390
                $aclRoles[] = $role;
1391
            }
1392
        }
1393
1394
        foreach ($roleHierarchy as $name => $roles) {
1395
            $aclRoles[] = $name;
1396
            $aclRoles = array_merge($aclRoles, $roles);
1397
        }
1398
1399
        $aclRoles = array_unique($aclRoles);
1400
1401
        return \is_array($aclRoles) ? new \ArrayIterator($aclRoles) : $aclRoles;
1402
    }
1403
1404
    /**
1405
     * Validate CSRF token for action without form.
1406
     *
1407
     * @param string $intention
1408
     *
1409
     * @throws HttpException
1410
     */
1411
    protected function validateCsrfToken($intention)
1412
    {
1413
        $request = $this->getRequest();
1414
        $token = $request->get('_sonata_csrf_token');
1415
1416
        if ($this->container->has('security.csrf.token_manager')) {
1417
            $valid = $this->container->get('security.csrf.token_manager')->isTokenValid(new CsrfToken($intention, $token));
1418
        } else {
1419
            return;
1420
        }
1421
1422
        if (!$valid) {
1423
            throw new HttpException(Response::HTTP_BAD_REQUEST, 'The csrf token is not valid, CSRF attack?');
1424
        }
1425
    }
1426
1427
    /**
1428
     * Escape string for html output.
1429
     *
1430
     * @param string $s
1431
     *
1432
     * @return string
1433
     */
1434
    protected function escapeHtml($s)
1435
    {
1436
        return htmlspecialchars((string) $s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
1437
    }
1438
1439
    /**
1440
     * Get CSRF token.
1441
     *
1442
     * @param string $intention
1443
     *
1444
     * @return string|false
1445
     */
1446
    protected function getCsrfToken($intention)
1447
    {
1448
        if ($this->container->has('security.csrf.token_manager')) {
1449
            return $this->container->get('security.csrf.token_manager')->getToken($intention)->getValue();
1450
        }
1451
1452
        return false;
1453
    }
1454
1455
    /**
1456
     * This method can be overloaded in your custom CRUD controller.
1457
     * It's called from createAction.
1458
     *
1459
     * @param object $object
1460
     *
1461
     * @return Response|null
1462
     */
1463
    protected function preCreate(Request $request, $object)
1464
    {
1465
        return null;
1466
    }
1467
1468
    /**
1469
     * This method can be overloaded in your custom CRUD controller.
1470
     * It's called from editAction.
1471
     *
1472
     * @param object $object
1473
     *
1474
     * @return Response|null
1475
     */
1476
    protected function preEdit(Request $request, $object)
1477
    {
1478
        return null;
1479
    }
1480
1481
    /**
1482
     * This method can be overloaded in your custom CRUD controller.
1483
     * It's called from deleteAction.
1484
     *
1485
     * @param object $object
1486
     *
1487
     * @return Response|null
1488
     */
1489
    protected function preDelete(Request $request, $object)
1490
    {
1491
        return null;
1492
    }
1493
1494
    /**
1495
     * This method can be overloaded in your custom CRUD controller.
1496
     * It's called from showAction.
1497
     *
1498
     * @param object $object
1499
     *
1500
     * @return Response|null
1501
     */
1502
    protected function preShow(Request $request, $object)
1503
    {
1504
        return null;
1505
    }
1506
1507
    /**
1508
     * This method can be overloaded in your custom CRUD controller.
1509
     * It's called from listAction.
1510
     *
1511
     * @return Response|null
1512
     */
1513
    protected function preList(Request $request)
1514
    {
1515
        return null;
1516
    }
1517
1518
    /**
1519
     * Translate a message id.
1520
     *
1521
     * @param string $id
1522
     * @param string $domain
1523
     * @param string $locale
1524
     *
1525
     * @return string translated string
1526
     */
1527
    final protected function trans($id, array $parameters = [], $domain = null, $locale = null)
1528
    {
1529
        $domain = $domain ?: $this->admin->getTranslationDomain();
1530
1531
        return $this->get('translator')->trans($id, $parameters, $domain, $locale);
1532
    }
1533
1534
    private function getSelectedTab(Request $request): array
1535
    {
1536
        return array_filter(['_tab' => $request->request->get('_tab')]);
1537
    }
1538
1539
    private function checkParentChildAssociation(Request $request, $object): void
1540
    {
1541
        if (!$this->admin->isChild()) {
1542
            return;
1543
        }
1544
1545
        // NEXT_MAJOR: remove this check
1546
        if (!$this->admin->getParentAssociationMapping()) {
1547
            return;
1548
        }
1549
1550
        $parentAdmin = $this->admin->getParent();
1551
        $parentId = $request->get($parentAdmin->getIdParameter());
1552
1553
        $propertyAccessor = PropertyAccess::createPropertyAccessor();
1554
        $propertyPath = new PropertyPath($this->admin->getParentAssociationMapping());
1555
1556
        if ($parentAdmin->getObject($parentId) !== $propertyAccessor->getValue($object, $propertyPath)) {
1557
            // NEXT_MAJOR: make this exception
1558
            @trigger_error(
1559
                "Accessing a child that isn't connected to a given parent is"
1560
                ." deprecated since sonata-project/admin-bundle 3.34 and won't be allowed in 4.0.",
1561
                E_USER_DEPRECATED
1562
            );
1563
        }
1564
    }
1565
1566
    /**
1567
     * Sets the admin form theme to form view. Used for compatibility between Symfony versions.
1568
     */
1569
    private function setFormTheme(FormView $formView, ?array $theme = null): void
1570
    {
1571
        $twig = $this->get('twig');
1572
1573
        $twig->getRuntime(FormRenderer::class)->setTheme($formView, $theme);
1574
    }
1575
1576
    private function handleXmlHttpRequestErrorResponse(Request $request, FormInterface $form): ?JsonResponse
1577
    {
1578
        if (!\in_array('application/json', $request->getAcceptableContentTypes(), true)) {
1579
            @trigger_error('In next major version response will return 406 NOT ACCEPTABLE without `Accept: application/json`', E_USER_DEPRECATED);
1580
1581
            return null;
1582
        }
1583
1584
        $errors = [];
1585
        foreach ($form->getErrors(true) as $error) {
1586
            $errors[] = $error->getMessage();
1587
        }
1588
1589
        return $this->renderJson([
1590
            'result' => 'error',
1591
            'errors' => $errors,
1592
        ], Response::HTTP_BAD_REQUEST);
1593
    }
1594
1595
    /**
1596
     * @param object $object
1597
     */
1598
    private function handleXmlHttpRequestSuccessResponse(Request $request, $object): JsonResponse
1599
    {
1600
        if (!\in_array('application/json', $request->getAcceptableContentTypes(), true)) {
1601
            @trigger_error('In next major version response will return 406 NOT ACCEPTABLE without `Accept: application/json`', E_USER_DEPRECATED);
1602
        }
1603
1604
        return $this->renderJson([
1605
            'result' => 'ok',
1606
            'objectId' => $this->admin->getNormalizedIdentifier($object),
1607
            'objectName' => $this->escapeHtml($this->admin->toString($object)),
1608
        ], Response::HTTP_OK);
1609
    }
1610
}
1611