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

src/Controller/CRUDController.php (1 issue)

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(
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(
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);
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');
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');
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