Completed
Pull Request — master (#6093)
by Mathieu
35:29
created

CRUDController::configure()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 36

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 36
rs 9.0328
c 0
b 0
f 0
cc 5
nc 6
nop 0
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\BreadcrumbsBuilder;
21
use Sonata\AdminBundle\Admin\FieldDescriptionCollection;
22
use Sonata\AdminBundle\Admin\Pool;
23
use Sonata\AdminBundle\Bridge\Exporter\AdminExporter;
24
use Sonata\AdminBundle\Datagrid\ProxyQueryInterface;
25
use Sonata\AdminBundle\Exception\LockException;
26
use Sonata\AdminBundle\Exception\ModelManagerException;
27
use Sonata\AdminBundle\Model\AuditManagerInterface;
28
use Sonata\AdminBundle\Templating\TemplateRegistryInterface;
29
use Sonata\AdminBundle\Util\AdminObjectAclData;
30
use Sonata\AdminBundle\Util\AdminObjectAclManipulator;
31
use Sonata\AdminBundle\Util\AdminObjectAclUserManager;
32
use Sonata\Exporter\Exporter;
33
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
34
use Symfony\Component\Form\FormInterface;
35
use Symfony\Component\Form\FormRenderer;
36
use Symfony\Component\Form\FormView;
37
use Symfony\Component\HttpFoundation\JsonResponse;
38
use Symfony\Component\HttpFoundation\RedirectResponse;
39
use Symfony\Component\HttpFoundation\Request;
40
use Symfony\Component\HttpFoundation\RequestStack;
41
use Symfony\Component\HttpFoundation\Response;
42
use Symfony\Component\HttpFoundation\Session\SessionInterface;
43
use Symfony\Component\HttpKernel\Exception\HttpException;
44
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
45
use Symfony\Component\HttpKernel\KernelInterface;
46
use Symfony\Component\PropertyAccess\PropertyAccess;
47
use Symfony\Component\PropertyAccess\PropertyPath;
48
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
49
use Symfony\Component\Security\Csrf\CsrfToken;
50
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
51
use Symfony\Contracts\Translation\TranslatorInterface;
52
use Twig\Environment;
53
54
/**
55
 * @author Thomas Rabaix <[email protected]>
56
 */
57
class CRUDController extends AbstractController
58
{
59
    /**
60
     * The related Admin class.
61
     *
62
     * @var AdminInterface
63
     */
64
    protected $admin;
65
66
    /**
67
     * @var RequestStack
68
     */
69
    private $requestStack;
70
71
    /**
72
     * @var Pool
73
     */
74
    private $pool;
75
76
    /**
77
     * The template registry of the related Admin class.
78
     *
79
     * @var TemplateRegistryInterface
80
     */
81
    private $templateRegistry;
82
83
    /**
84
     * @var BreadcrumbsBuilder
85
     */
86
    private $breadcrumbsBuilder;
87
88
    /**
89
     * @var Environment
90
     */
91
    private $twig;
92
93
    /**
94
     * @var TranslatorInterface
95
     */
96
    private $translator;
97
98
    /**
99
     * @var SessionInterface
100
     */
101
    private $session;
102
103
    /**
104
     * @var KernelInterface
105
     */
106
    private $kernel;
107
108
    /**
109
     * @var AuditManagerInterface
110
     */
111
    private $auditManager;
112
113
    /**
114
     * @var Exporter|null
115
     */
116
    private $exporter;
117
118
    /**
119
     * @var AdminExporter|null
120
     */
121
    private $adminExporter;
122
123
    /**
124
     * @var CsrfTokenManagerInterface|null
125
     */
126
    private $csrfTokenManager;
127
128
    /**
129
     * @var LoggerInterface
130
     */
131
    private $logger;
132
133
    /**
134
     * @var AdminObjectAclManipulator|null
135
     */
136
    private $adminObjectAclManipulator;
137
138
    /**
139
     * @var AdminObjectAclUserManager|null
140
     */
141
    private $adminObjectAclUserManager;
142
143
    public function __construct(
144
        RequestStack $requestStack,
145
        Pool $pool,
146
        TemplateRegistryInterface $templateRegistry,
147
        BreadcrumbsBuilder $breadcrumbsBuilder,
148
        Environment $twig,
149
        TranslatorInterface $translator,
150
        SessionInterface $session,
151
        KernelInterface $kernel,
152
        AuditManagerInterface $auditManager,
153
        ?Exporter $exporter = null,
154
        ?AdminExporter $adminExporter = null,
155
        ?CsrfTokenManagerInterface $csrfTokenManager = null,
156
        ?LoggerInterface $logger = null,
157
        ?AdminObjectAclManipulator $adminObjectAclManipulator = null,
158
        ?AdminObjectAclUserManager $adminObjectAclUserManager = null
159
    ) {
160
        $this->requestStack = $requestStack;
161
        $this->pool = $pool;
162
        $this->templateRegistry = $templateRegistry;
163
        $this->breadcrumbsBuilder = $breadcrumbsBuilder;
164
        $this->twig = $twig;
165
        $this->translator = $translator;
166
        $this->session = $session;
167
        $this->kernel = $kernel;
168
        $this->auditManager = $auditManager;
169
        $this->exporter = $exporter;
170
        $this->adminExporter = $adminExporter;
171
        $this->csrfTokenManager = $csrfTokenManager;
172
        $this->logger = $logger ?? new NullLogger();
173
        $this->adminObjectAclManipulator = $adminObjectAclManipulator;
174
        $this->adminObjectAclUserManager = $adminObjectAclUserManager;
175
176
        $this->configure();
177
    }
178
179
    /**
180
     * Renders a view while passing mandatory parameters on to the template.
181
     *
182
     * @param string               $view       The view name
183
     * @param array<string, mixed> $parameters An array of parameters to pass to the view
184
     */
185
    public function renderWithExtraParams($view, array $parameters = [], ?Response $response = null): Response
186
    {
187
        $response = $response ?? new Response();
188
189
        return $response->setContent($this->twig->render($view, $this->addRenderExtraParams($parameters)));
190
    }
191
192
    /**
193
     * List action.
194
     *
195
     * @throws AccessDeniedException If access is not granted
196
     *
197
     * @return Response
198
     */
199
    public function listAction(Request $request)
200
    {
201
        $this->admin->checkAccess('list');
202
203
        $preResponse = $this->preList($request);
204
        if (null !== $preResponse) {
205
            return $preResponse;
206
        }
207
208
        if ($listMode = $request->get('_list_mode')) {
209
            $this->admin->setListMode($listMode);
210
        }
211
212
        $datagrid = $this->admin->getDatagrid();
213
        $formView = $datagrid->getForm()->createView();
214
215
        // set the theme for the current Admin Form
216
        $this->setFormTheme($formView, $this->admin->getFilterTheme());
217
218
        $template = $this->templateRegistry->getTemplate('list');
219
220
        return $this->renderWithExtraParams($template, [
221
            'action' => 'list',
222
            'form' => $formView,
223
            'datagrid' => $datagrid,
224
            'csrf_token' => $this->getCsrfToken('sonata.batch'),
225
            'export_formats' => $this->adminExporter instanceof Exporter ?
226
                $this->adminExporter->getAvailableFormats($this->admin) :
227
                $this->admin->getExportFormats(),
228
        ]);
229
    }
230
231
    /**
232
     * Execute a batch delete.
233
     *
234
     * @throws AccessDeniedException If access is not granted
235
     *
236
     * @return RedirectResponse
237
     */
238
    public function batchActionDelete(ProxyQueryInterface $query)
239
    {
240
        $this->admin->checkAccess('batchDelete');
241
242
        $modelManager = $this->admin->getModelManager();
243
244
        try {
245
            $modelManager->batchDelete($this->admin->getClass(), $query);
246
            $this->addFlash(
247
                'sonata_flash_success',
248
                $this->trans('flash_batch_delete_success', [], 'SonataAdminBundle')
249
            );
250
        } catch (ModelManagerException $e) {
251
            $this->handleModelManagerException($e);
252
            $this->addFlash(
253
                'sonata_flash_error',
254
                $this->trans('flash_batch_delete_error', [], 'SonataAdminBundle')
255
            );
256
        }
257
258
        return $this->redirectToList();
259
    }
260
261
    /**
262
     * Delete action.
263
     *
264
     * @throws NotFoundHttpException If the object does not exist
265
     * @throws AccessDeniedException If access is not granted
266
     *
267
     * @return Response|RedirectResponse
268
     */
269
    public function deleteAction(Request $request)
270
    {
271
        $id = $request->get($this->admin->getIdParameter());
272
        $object = $this->admin->getObject($id);
273
274
        if (!$object) {
275
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
276
        }
277
278
        $this->checkParentChildAssociation($request, $object);
279
280
        $this->admin->checkAccess('delete', $object);
281
282
        $preResponse = $this->preDelete($request, $object);
283
        if (null !== $preResponse) {
284
            return $preResponse;
285
        }
286
287
        if (Request::METHOD_DELETE === $this->getRestMethod()) {
288
            // check the csrf token
289
            $this->validateCsrfToken('sonata.delete');
290
291
            $objectName = $this->admin->toString($object);
292
293
            try {
294
                $this->admin->delete($object);
295
296
                if ($this->isXmlHttpRequest()) {
297
                    return $this->renderJson(['result' => 'ok'], Response::HTTP_OK, []);
298
                }
299
300
                $this->addFlash(
301
                    'sonata_flash_success',
302
                    $this->trans(
303
                        'flash_delete_success',
304
                        ['%name%' => $this->escapeHtml($objectName)],
305
                        'SonataAdminBundle'
306
                    )
307
                );
308
            } catch (ModelManagerException $e) {
309
                $this->handleModelManagerException($e);
310
311
                if ($this->isXmlHttpRequest()) {
312
                    return $this->renderJson(['result' => 'error'], Response::HTTP_OK, []);
313
                }
314
315
                $this->addFlash(
316
                    'sonata_flash_error',
317
                    $this->trans(
318
                        'flash_delete_error',
319
                        ['%name%' => $this->escapeHtml($objectName)],
320
                        'SonataAdminBundle'
321
                    )
322
                );
323
            }
324
325
            return $this->redirectTo($object);
326
        }
327
328
        $template = $this->templateRegistry->getTemplate('delete');
329
330
        return $this->renderWithExtraParams($template, [
331
            'object' => $object,
332
            'action' => 'delete',
333
            'csrf_token' => $this->getCsrfToken('sonata.delete'),
334
        ]);
335
    }
336
337
    /**
338
     * Edit action.
339
     *
340
     * @throws NotFoundHttpException If the object does not exist
341
     * @throws AccessDeniedException If access is not granted
342
     *
343
     * @return Response|RedirectResponse
344
     */
345
    public function editAction(Request $request)
346
    {
347
        // the key used to lookup the template
348
        $templateKey = 'edit';
349
350
        $id = $request->get($this->admin->getIdParameter());
351
        $existingObject = $this->admin->getObject($id);
352
353
        if (!$existingObject) {
354
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
355
        }
356
357
        $this->checkParentChildAssociation($request, $existingObject);
358
359
        $this->admin->checkAccess('edit', $existingObject);
360
361
        $preResponse = $this->preEdit($request, $existingObject);
362
        if (null !== $preResponse) {
363
            return $preResponse;
364
        }
365
366
        $this->admin->setSubject($existingObject);
367
        $objectId = $this->admin->getNormalizedIdentifier($existingObject);
368
369
        $form = $this->admin->getForm();
370
371
        $form->setData($existingObject);
372
        $form->handleRequest($request);
373
374
        if ($form->isSubmitted()) {
375
            $isFormValid = $form->isValid();
376
377
            // persist if the form was valid and if in preview mode the preview was approved
378
            if ($isFormValid && (!$this->isInPreviewMode() || $this->isPreviewApproved())) {
379
                $submittedObject = $form->getData();
380
                $this->admin->setSubject($submittedObject);
381
382
                try {
383
                    $existingObject = $this->admin->update($submittedObject);
384
385
                    if ($this->isXmlHttpRequest()) {
386
                        return $this->handleXmlHttpRequestSuccessResponse($request, $existingObject);
387
                    }
388
389
                    $this->addFlash(
390
                        'sonata_flash_success',
391
                        $this->trans(
392
                            'flash_edit_success',
393
                            ['%name%' => $this->escapeHtml($this->admin->toString($existingObject))],
394
                            'SonataAdminBundle'
395
                        )
396
                    );
397
398
                    // redirect to edit mode
399
                    return $this->redirectTo($existingObject);
400
                } catch (ModelManagerException $e) {
401
                    $this->handleModelManagerException($e);
402
403
                    $isFormValid = false;
404
                } catch (LockException $e) {
405
                    $this->addFlash('sonata_flash_error', $this->trans('flash_lock_error', [
406
                        '%name%' => $this->escapeHtml($this->admin->toString($existingObject)),
407
                        '%link_start%' => '<a href="'.$this->admin->generateObjectUrl('edit', $existingObject).'">',
408
                        '%link_end%' => '</a>',
409
                    ], 'SonataAdminBundle'));
410
                }
411
            }
412
413
            // show an error message if the form failed validation
414
            if (!$isFormValid) {
415
                if ($this->isXmlHttpRequest() && null !== ($response = $this->handleXmlHttpRequestErrorResponse($request, $form))) {
416
                    return $response;
417
                }
418
419
                $this->addFlash(
420
                    'sonata_flash_error',
421
                    $this->trans(
422
                        'flash_edit_error',
423
                        ['%name%' => $this->escapeHtml($this->admin->toString($existingObject))],
424
                        'SonataAdminBundle'
425
                    )
426
                );
427
            } elseif ($this->isPreviewRequested()) {
428
                // enable the preview template if the form was valid and preview was requested
429
                $templateKey = 'preview';
430
                $this->admin->getShow();
431
            }
432
        }
433
434
        $formView = $form->createView();
435
        // set the theme for the current Admin Form
436
        $this->setFormTheme($formView, $this->admin->getFormTheme());
437
438
        $template = $this->templateRegistry->getTemplate($templateKey);
439
440
        return $this->renderWithExtraParams($template, [
441
            'action' => 'edit',
442
            'form' => $formView,
443
            'object' => $existingObject,
444
            'objectId' => $objectId,
445
        ]);
446
    }
447
448
    /**
449
     * Batch action.
450
     *
451
     * @throws NotFoundHttpException If the HTTP method is not POST
452
     * @throws \RuntimeException     If the batch action is not defined
453
     *
454
     * @return Response|RedirectResponse
455
     */
456
    public function batchAction(Request $request)
457
    {
458
        $restMethod = $this->getRestMethod();
459
460
        if (Request::METHOD_POST !== $restMethod) {
461
            throw $this->createNotFoundException(sprintf('Invalid request method given "%s", %s expected', $restMethod, Request::METHOD_POST));
462
        }
463
464
        // check the csrf token
465
        $this->validateCsrfToken('sonata.batch');
466
467
        $confirmation = $request->get('confirmation', false);
468
469
        if ($data = json_decode($request->get('data', ''), true)) {
470
            $action = $data['action'];
471
            $idx = $data['idx'];
472
            $allElements = $data['all_elements'];
473
            $request->request->replace(array_merge($request->request->all(), $data));
474
        } else {
475
            $request->request->set('idx', $request->get('idx', []));
476
            $request->request->set('all_elements', $request->get('all_elements', false));
477
478
            $action = $request->get('action');
479
            $idx = $request->get('idx');
480
            $allElements = $request->get('all_elements');
481
            $data = $request->request->all();
482
483
            unset($data['_sonata_csrf_token']);
484
        }
485
486
        $batchActions = $this->admin->getBatchActions();
487
        if (!\array_key_exists($action, $batchActions)) {
488
            throw new \RuntimeException(sprintf('The `%s` batch action is not defined', $action));
489
        }
490
491
        $camelizedAction = InflectorFactory::create()->build()->classify($action);
492
        $isRelevantAction = sprintf('batchAction%sIsRelevant', $camelizedAction);
493
494
        if (method_exists($this, $isRelevantAction)) {
495
            $nonRelevantMessage = $this->$isRelevantAction($idx, $allElements, $request);
496
        } else {
497
            $nonRelevantMessage = 0 !== \count($idx) || $allElements; // at least one item is selected
498
        }
499
500
        if (!$nonRelevantMessage) { // default non relevant message (if false of null)
501
            $nonRelevantMessage = 'flash_batch_empty';
502
        }
503
504
        $datagrid = $this->admin->getDatagrid();
505
        $datagrid->buildPager();
506
507
        if (true !== $nonRelevantMessage) {
508
            $this->addFlash(
509
                'sonata_flash_info',
510
                $this->trans($nonRelevantMessage, [], 'SonataAdminBundle')
511
            );
512
513
            return $this->redirectToList();
514
        }
515
516
        $askConfirmation = $batchActions[$action]['ask_confirmation'] ??
517
            true;
518
519
        if ($askConfirmation && 'ok' !== $confirmation) {
520
            $actionLabel = $batchActions[$action]['label'];
521
            $batchTranslationDomain = $batchActions[$action]['translation_domain'] ??
522
                $this->admin->getTranslationDomain();
523
524
            $formView = $datagrid->getForm()->createView();
525
            $this->setFormTheme($formView, $this->admin->getFilterTheme());
526
527
            $template = !empty($batchActions[$action]['template']) ?
528
                $batchActions[$action]['template'] :
529
                $this->templateRegistry->getTemplate('batch_confirmation');
530
531
            return $this->renderWithExtraParams($template, [
532
                'action' => 'list',
533
                'action_label' => $actionLabel,
534
                'batch_translation_domain' => $batchTranslationDomain,
535
                'datagrid' => $datagrid,
536
                'form' => $formView,
537
                'data' => $data,
538
                'csrf_token' => $this->getCsrfToken('sonata.batch'),
539
            ]);
540
        }
541
542
        // execute the action, batchActionXxxxx
543
        $finalAction = sprintf('batchAction%s', $camelizedAction);
544
        if (!method_exists($this, $finalAction)) {
545
            throw new \RuntimeException(sprintf('A `%s::%s` method must be callable', static::class, $finalAction));
546
        }
547
548
        $query = $datagrid->getQuery();
549
550
        $query->setFirstResult(null);
551
        $query->setMaxResults(null);
552
553
        $this->admin->preBatchAction($action, $query, $idx, $allElements);
554
555
        if (\count($idx) > 0) {
556
            $this->admin->getModelManager()->addIdentifiersToQuery($this->admin->getClass(), $query, $idx);
557
        } elseif (!$allElements) {
558
            $this->addFlash(
559
                'sonata_flash_info',
560
                $this->trans('flash_batch_no_elements_processed', [], 'SonataAdminBundle')
561
            );
562
563
            return $this->redirectToList();
564
        }
565
566
        return $this->$finalAction($query, $request);
567
    }
568
569
    /**
570
     * Create action.
571
     *
572
     * @throws AccessDeniedException If access is not granted
573
     *
574
     * @return Response
575
     */
576
    public function createAction(Request $request)
577
    {
578
        // the key used to lookup the template
579
        $templateKey = 'edit';
580
581
        $this->admin->checkAccess('create');
582
583
        $class = new \ReflectionClass($this->admin->hasActiveSubClass() ? $this->admin->getActiveSubClass() : $this->admin->getClass());
584
585
        if ($class->isAbstract()) {
586
            return $this->renderWithExtraParams(
587
                '@SonataAdmin/CRUD/select_subclass.html.twig',
588
                [
589
                    'base_template' => $this->getBaseTemplate(),
590
                    'admin' => $this->admin,
591
                    'action' => 'create',
592
                ]
593
            );
594
        }
595
596
        $newObject = $this->admin->getNewInstance();
597
598
        $preResponse = $this->preCreate($request, $newObject);
599
        if (null !== $preResponse) {
600
            return $preResponse;
601
        }
602
603
        $this->admin->setSubject($newObject);
604
605
        $form = $this->admin->getForm();
606
607
        $form->setData($newObject);
608
        $form->handleRequest($request);
609
610
        if ($form->isSubmitted()) {
611
            $isFormValid = $form->isValid();
612
613
            // persist if the form was valid and if in preview mode the preview was approved
614
            if ($isFormValid && (!$this->isInPreviewMode() || $this->isPreviewApproved())) {
615
                $submittedObject = $form->getData();
616
                $this->admin->setSubject($submittedObject);
617
                $this->admin->checkAccess('create', $submittedObject);
618
619
                try {
620
                    $newObject = $this->admin->create($submittedObject);
621
622
                    if ($this->isXmlHttpRequest()) {
623
                        return $this->handleXmlHttpRequestSuccessResponse($request, $newObject);
624
                    }
625
626
                    $this->addFlash(
627
                        'sonata_flash_success',
628
                        $this->trans(
629
                            'flash_create_success',
630
                            ['%name%' => $this->escapeHtml($this->admin->toString($newObject))],
631
                            'SonataAdminBundle'
632
                        )
633
                    );
634
635
                    // redirect to edit mode
636
                    return $this->redirectTo($newObject);
637
                } catch (ModelManagerException $e) {
638
                    $this->handleModelManagerException($e);
639
640
                    $isFormValid = false;
641
                }
642
            }
643
644
            // show an error message if the form failed validation
645
            if (!$isFormValid) {
646
                if ($this->isXmlHttpRequest() && null !== ($response = $this->handleXmlHttpRequestErrorResponse($request, $form))) {
647
                    return $response;
648
                }
649
650
                $this->addFlash(
651
                    'sonata_flash_error',
652
                    $this->trans(
653
                        'flash_create_error',
654
                        ['%name%' => $this->escapeHtml($this->admin->toString($newObject))],
655
                        'SonataAdminBundle'
656
                    )
657
                );
658
            } elseif ($this->isPreviewRequested()) {
659
                // pick the preview template if the form was valid and preview was requested
660
                $templateKey = 'preview';
661
                $this->admin->getShow();
662
            }
663
        }
664
665
        $formView = $form->createView();
666
        // set the theme for the current Admin Form
667
        $this->setFormTheme($formView, $this->admin->getFormTheme());
668
669
        $template = $this->templateRegistry->getTemplate($templateKey);
670
671
        return $this->renderWithExtraParams($template, [
672
            'action' => 'create',
673
            'form' => $formView,
674
            'object' => $newObject,
675
            'objectId' => null,
676
        ]);
677
    }
678
679
    /**
680
     * Show action.
681
     *
682
     * @throws NotFoundHttpException If the object does not exist
683
     * @throws AccessDeniedException If access is not granted
684
     *
685
     * @return Response
686
     */
687
    public function showAction(Request $request)
688
    {
689
        $id = $request->get($this->admin->getIdParameter());
690
        $object = $this->admin->getObject($id);
691
692
        if (!$object) {
693
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
694
        }
695
696
        $this->checkParentChildAssociation($request, $object);
697
698
        $this->admin->checkAccess('show', $object);
699
700
        $preResponse = $this->preShow($request, $object);
701
        if (null !== $preResponse) {
702
            return $preResponse;
703
        }
704
705
        $this->admin->setSubject($object);
706
707
        $fields = $this->admin->getShow();
708
        \assert($fields instanceof FieldDescriptionCollection);
709
710
        $template = $this->templateRegistry->getTemplate('show');
711
712
        return $this->renderWithExtraParams($template, [
713
            'action' => 'show',
714
            'object' => $object,
715
            'elements' => $fields,
716
        ]);
717
    }
718
719
    /**
720
     * Show history revisions for object.
721
     *
722
     * @throws AccessDeniedException If access is not granted
723
     * @throws NotFoundHttpException If the object does not exist or the audit reader is not available
724
     *
725
     * @return Response
726
     */
727
    public function historyAction(Request $request)
728
    {
729
        $id = $request->get($this->admin->getIdParameter());
730
        $object = $this->admin->getObject($id);
731
732
        if (!$object) {
733
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
734
        }
735
736
        $this->admin->checkAccess('history', $object);
737
738
        if (!$this->auditManager->hasReader($this->admin->getClass())) {
739
            throw $this->createNotFoundException(
740
                sprintf(
741
                    'unable to find the audit reader for class : %s',
742
                    $this->admin->getClass()
743
                )
744
            );
745
        }
746
747
        $reader = $this->auditManager->getReader($this->admin->getClass());
748
749
        $revisions = $reader->findRevisions($this->admin->getClass(), $id);
750
751
        $template = $this->templateRegistry->getTemplate('history');
752
753
        return $this->renderWithExtraParams($template, [
754
            'action' => 'history',
755
            'object' => $object,
756
            'revisions' => $revisions,
757
            'currentRevision' => $revisions ? current($revisions) : false,
758
        ]);
759
    }
760
761
    /**
762
     * View history revision of object.
763
     *
764
     * @param string|null $revision
765
     *
766
     * @throws AccessDeniedException If access is not granted
767
     * @throws NotFoundHttpException If the object or revision does not exist or the audit reader is not available
768
     *
769
     * @return Response
770
     */
771
    public function historyViewRevisionAction(Request $request, $revision = null)
772
    {
773
        $id = $request->get($this->admin->getIdParameter());
774
        $object = $this->admin->getObject($id);
775
776
        if (!$object) {
777
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
778
        }
779
780
        $this->admin->checkAccess('historyViewRevision', $object);
781
782
        if (!$this->auditManager->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 = $this->auditManager->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
        $template = $this->templateRegistry->getTemplate('show');
810
811
        return $this->renderWithExtraParams($template, [
812
            'action' => 'show',
813
            'object' => $object,
814
            'elements' => $this->admin->getShow(),
815
        ]);
816
    }
817
818
    /**
819
     * Compare history revisions of object.
820
     *
821
     * @param int|string|null $baseRevision
822
     * @param int|string|null $compareRevision
823
     *
824
     * @throws AccessDeniedException If access is not granted
825
     * @throws NotFoundHttpException If the object or revision does not exist or the audit reader is not available
826
     *
827
     * @return Response
828
     */
829
    public function historyCompareRevisionsAction(Request $request, $baseRevision = null, $compareRevision = null)
830
    {
831
        $this->admin->checkAccess('historyCompareRevisions');
832
833
        $id = $request->get($this->admin->getIdParameter());
834
        $object = $this->admin->getObject($id);
835
836
        if (!$object) {
837
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
838
        }
839
840
        if (!$this->auditManager->hasReader($this->admin->getClass())) {
841
            throw $this->createNotFoundException(
842
                sprintf(
843
                    'unable to find the audit reader for class : %s',
844
                    $this->admin->getClass()
845
                )
846
            );
847
        }
848
849
        $reader = $this->auditManager->getReader($this->admin->getClass());
850
851
        // retrieve the base revision
852
        $baseObject = $reader->find($this->admin->getClass(), $id, $baseRevision);
853
        if (!$baseObject) {
854
            throw $this->createNotFoundException(
855
                sprintf(
856
                    'unable to find the targeted object `%s` from the revision `%s` with classname : `%s`',
857
                    $id,
858
                    $baseRevision,
859
                    $this->admin->getClass()
860
                )
861
            );
862
        }
863
864
        // retrieve the compare revision
865
        $compareObject = $reader->find($this->admin->getClass(), $id, $compareRevision);
866
        if (!$compareObject) {
867
            throw $this->createNotFoundException(
868
                sprintf(
869
                    'unable to find the targeted object `%s` from the revision `%s` with classname : `%s`',
870
                    $id,
871
                    $compareRevision,
872
                    $this->admin->getClass()
873
                )
874
            );
875
        }
876
877
        $this->admin->setSubject($baseObject);
878
879
        $template = $this->templateRegistry->getTemplate('show_compare');
880
881
        return $this->renderWithExtraParams($template, [
882
            'action' => 'show',
883
            'object' => $baseObject,
884
            'object_compare' => $compareObject,
885
            'elements' => $this->admin->getShow(),
886
        ]);
887
    }
888
889
    /**
890
     * Export data to specified format.
891
     *
892
     * @throws AccessDeniedException If access is not granted
893
     * @throws \RuntimeException     If the export format is invalid
894
     *
895
     * @return Response
896
     */
897
    public function exportAction(Request $request)
898
    {
899
        $this->admin->checkAccess('export');
900
901
        $format = $request->get('format');
902
903
        $allowedExportFormats = $this->adminExporter->getAvailableFormats($this->admin);
904
        $filename = $this->adminExporter->getExportFilename($this->admin, $format);
905
906
        if (!\in_array($format, $allowedExportFormats, true)) {
907
            throw new \RuntimeException(
908
                sprintf(
909
                    'Export in format `%s` is not allowed for class: `%s`. Allowed formats are: `%s`',
910
                    $format,
911
                    $this->admin->getClass(),
912
                    implode(', ', $allowedExportFormats)
913
                )
914
            );
915
        }
916
917
        return $this->exporter->getResponse(
918
            $format,
919
            $filename,
920
            $this->admin->getDataSourceIterator()
921
        );
922
    }
923
924
    /**
925
     * Returns the Response object associated to the acl action.
926
     *
927
     * @throws AccessDeniedException If access is not granted
928
     * @throws NotFoundHttpException If the object does not exist or the ACL is not enabled
929
     *
930
     * @return Response|RedirectResponse
931
     */
932
    public function aclAction(Request $request)
933
    {
934
        if (!$this->admin->isAclEnabled()) {
935
            throw $this->createNotFoundException('ACL are not enabled for this admin');
936
        }
937
938
        $id = $request->get($this->admin->getIdParameter());
939
        $object = $this->admin->getObject($id);
940
941
        if (!$object) {
942
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
943
        }
944
945
        $this->admin->checkAccess('acl', $object);
946
947
        $this->admin->setSubject($object);
948
        $aclUsers = $this->getAclUsers();
949
        $aclRoles = $this->getAclRoles();
950
951
        $adminObjectAclData = new AdminObjectAclData(
952
            $this->admin,
953
            $object,
954
            $aclUsers,
955
            $this->adminObjectAclManipulator->getMaskBuilderClass(),
956
            $aclRoles
957
        );
958
959
        $aclUsersForm = $this->adminObjectAclManipulator->createAclUsersForm($adminObjectAclData);
960
        $aclRolesForm = $this->adminObjectAclManipulator->createAclRolesForm($adminObjectAclData);
961
962
        if (Request::METHOD_POST === $request->getMethod()) {
963
            if ($request->request->has(AdminObjectAclManipulator::ACL_USERS_FORM_NAME)) {
964
                $form = $aclUsersForm;
965
                $updateMethod = 'updateAclUsers';
966
            } elseif ($request->request->has(AdminObjectAclManipulator::ACL_ROLES_FORM_NAME)) {
967
                $form = $aclRolesForm;
968
                $updateMethod = 'updateAclRoles';
969
            }
970
971
            if (isset($form)) {
972
                $form->handleRequest($request);
973
974
                if ($form->isValid()) {
975
                    $this->adminObjectAclManipulator->$updateMethod($adminObjectAclData);
0 ignored issues
show
Bug introduced by
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...
976
                    $this->addFlash(
977
                        'sonata_flash_success',
978
                        $this->trans('flash_acl_edit_success', [], 'SonataAdminBundle')
979
                    );
980
981
                    return new RedirectResponse($this->admin->generateObjectUrl('acl', $object));
982
                }
983
            }
984
        }
985
986
        $template = $this->templateRegistry->getTemplate('acl');
987
988
        return $this->renderWithExtraParams($template, [
989
            'action' => 'acl',
990
            'permissions' => $adminObjectAclData->getUserPermissions(),
991
            'object' => $object,
992
            'users' => $aclUsers,
993
            'roles' => $aclRoles,
994
            'aclUsersForm' => $aclUsersForm->createView(),
995
            'aclRolesForm' => $aclRolesForm->createView(),
996
        ]);
997
    }
998
999
    public function getRequest(): ?Request
1000
    {
1001
        return $this->requestStack->getCurrentRequest();
1002
    }
1003
1004
    protected function addFlash(string $type, string $message): void
1005
    {
1006
        $this->session->getFlashBag()->add($type, $message);
1007
    }
1008
1009
    /**
1010
     * @param array<string, mixed> $parameters
1011
     *
1012
     * @return array<string, mixed>
0 ignored issues
show
Documentation introduced by
The doc-type array<string, could not be parsed: Expected ">" at position 5, but found "end of type". (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
1013
     */
1014
    protected function addRenderExtraParams(array $parameters = []): array
1015
    {
1016
        if (!$this->isXmlHttpRequest()) {
1017
            $parameters['breadcrumbs_builder'] = $this->breadcrumbsBuilder;
1018
        }
1019
1020
        $parameters['admin'] = $parameters['admin'] ?? $this->admin;
1021
        $parameters['base_template'] = $parameters['base_template'] ?? $this->getBaseTemplate();
1022
        $parameters['admin_pool'] = $this->pool;
1023
1024
        return $parameters;
1025
    }
1026
1027
    /**
1028
     * Gets a container configuration parameter by its name.
1029
     *
1030
     * @param string $name The parameter name
1031
     *
1032
     * @return mixed
1033
     */
1034
    protected function getParameter($name)
1035
    {
1036
        return $this->container->getParameter($name);
0 ignored issues
show
Bug introduced by
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...
1037
    }
1038
1039
    /**
1040
     * Render JSON.
1041
     *
1042
     * @param mixed $data
1043
     * @param int   $status
1044
     * @param array $headers
1045
     *
1046
     * @return JsonResponse with json encoded data
1047
     */
1048
    protected function renderJson($data, $status = Response::HTTP_OK, $headers = [])
1049
    {
1050
        return new JsonResponse($data, $status, $headers);
1051
    }
1052
1053
    /**
1054
     * Returns true if the request is a XMLHttpRequest.
1055
     *
1056
     * @return bool True if the request is an XMLHttpRequest, false otherwise
1057
     */
1058
    protected function isXmlHttpRequest()
1059
    {
1060
        $request = $this->getRequest();
1061
1062
        return $request->isXmlHttpRequest() || $request->get('_xml_http_request');
1063
    }
1064
1065
    /**
1066
     * Returns the correct RESTful verb, given either by the request itself or
1067
     * via the "_method" parameter.
1068
     *
1069
     * @return string HTTP method, either
1070
     */
1071
    protected function getRestMethod()
1072
    {
1073
        $request = $this->getRequest();
1074
1075
        if (Request::getHttpMethodParameterOverride() || !$request->request->has('_method')) {
1076
            return $request->getMethod();
1077
        }
1078
1079
        return $request->request->get('_method');
1080
    }
1081
1082
    /**
1083
     * Contextualize the admin class depends on the current request.
1084
     *
1085
     * @throws \RuntimeException
1086
     */
1087
    protected function configure(): void
1088
    {
1089
        $request = $this->getRequest();
1090
1091
        $adminCode = $request->get('_sonata_admin');
1092
1093
        if (!$adminCode) {
1094
            throw new \RuntimeException(sprintf(
1095
                'There is no `_sonata_admin` defined for the controller `%s` and the current route `%s`',
1096
                static::class,
1097
                $request->get('_route')
1098
            ));
1099
        }
1100
1101
        try {
1102
            $this->admin = $this->pool->getAdminByAdminCode($adminCode);
1103
        } catch (\InvalidArgumentException $e) {
1104
            throw new \RuntimeException(sprintf(
1105
                'Unable to find the admin class related to the current controller (%s)',
1106
                static::class
1107
            ));
1108
        }
1109
1110
        $rootAdmin = $this->admin;
1111
1112
        while ($rootAdmin->isChild()) {
1113
            $rootAdmin->setCurrentChild(true);
1114
            $rootAdmin = $rootAdmin->getParent();
1115
        }
1116
1117
        $rootAdmin->setRequest($request);
1118
1119
        if ($request->get('uniqid')) {
1120
            $this->admin->setUniqid($request->get('uniqid'));
1121
        }
1122
    }
1123
1124
    /**
1125
     * Proxy for the logger service of the container.
1126
     * If no such service is found, a NullLogger is returned.
1127
     */
1128
    protected function getLogger(): LoggerInterface
1129
    {
1130
        return $this->logger;
1131
    }
1132
1133
    /**
1134
     * Returns the base template name.
1135
     *
1136
     * @return string The template name
1137
     */
1138
    protected function getBaseTemplate()
1139
    {
1140
        if ($this->isXmlHttpRequest()) {
1141
            return $this->templateRegistry->getTemplate('ajax');
1142
        }
1143
1144
        return $this->templateRegistry->getTemplate('layout');
1145
    }
1146
1147
    /**
1148
     * @throws \Exception
1149
     */
1150
    protected function handleModelManagerException(\Exception $e): void
1151
    {
1152
        if ($this->kernel->isDebug()) {
1153
            throw $e;
1154
        }
1155
1156
        $context = ['exception' => $e];
1157
        if ($e->getPrevious()) {
1158
            $context['previous_exception_message'] = $e->getPrevious()->getMessage();
1159
        }
1160
        $this->getLogger()->error($e->getMessage(), $context);
1161
    }
1162
1163
    /**
1164
     * Redirect the user depend on this choice.
1165
     *
1166
     * @param object $object
1167
     *
1168
     * @return RedirectResponse
1169
     */
1170
    protected function redirectTo($object)
1171
    {
1172
        $request = $this->getRequest();
1173
1174
        $url = false;
1175
1176
        if (null !== $request->get('btn_update_and_list')) {
1177
            return $this->redirectToList();
1178
        }
1179
        if (null !== $request->get('btn_create_and_list')) {
1180
            return $this->redirectToList();
1181
        }
1182
1183
        if (null !== $request->get('btn_create_and_create')) {
1184
            $params = [];
1185
            if ($this->admin->hasActiveSubClass()) {
1186
                $params['subclass'] = $request->get('subclass');
1187
            }
1188
            $url = $this->admin->generateUrl('create', $params);
1189
        }
1190
1191
        if ('DELETE' === $this->getRestMethod()) {
1192
            return $this->redirectToList();
1193
        }
1194
1195
        if (!$url) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $url of type string|false is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1196
            foreach (['edit', 'show'] as $route) {
1197
                if ($this->admin->hasRoute($route) && $this->admin->hasAccess($route, $object)) {
1198
                    $url = $this->admin->generateObjectUrl(
1199
                        $route,
1200
                        $object,
1201
                        $this->getSelectedTab($request)
0 ignored issues
show
Bug introduced by
It seems like $request defined by $this->getRequest() on line 1172 can be null; however, Sonata\AdminBundle\Contr...oller::getSelectedTab() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1202
                    );
1203
1204
                    break;
1205
                }
1206
            }
1207
        }
1208
1209
        if (!$url) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $url of type string|false is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1210
            return $this->redirectToList();
1211
        }
1212
1213
        return new RedirectResponse($url);
1214
    }
1215
1216
    /**
1217
     * Redirects the user to the list view.
1218
     *
1219
     * @return RedirectResponse
1220
     */
1221
    final protected function redirectToList()
1222
    {
1223
        $parameters = [];
1224
1225
        if ($filter = $this->admin->getFilterParameters()) {
1226
            $parameters['filter'] = $filter;
1227
        }
1228
1229
        return $this->redirect($this->admin->generateUrl('list', $parameters));
1230
    }
1231
1232
    /**
1233
     * Returns true if the preview is requested to be shown.
1234
     *
1235
     * @return bool
1236
     */
1237
    protected function isPreviewRequested()
1238
    {
1239
        $request = $this->getRequest();
1240
1241
        return null !== $request->get('btn_preview');
1242
    }
1243
1244
    /**
1245
     * Returns true if the preview has been approved.
1246
     *
1247
     * @return bool
1248
     */
1249
    protected function isPreviewApproved()
1250
    {
1251
        $request = $this->getRequest();
1252
1253
        return null !== $request->get('btn_preview_approve');
1254
    }
1255
1256
    /**
1257
     * Returns true if the request is in the preview workflow.
1258
     *
1259
     * That means either a preview is requested or the preview has already been shown
1260
     * and it got approved/declined.
1261
     *
1262
     * @return bool
1263
     */
1264
    protected function isInPreviewMode()
1265
    {
1266
        return $this->admin->supportsPreviewMode()
1267
        && ($this->isPreviewRequested()
1268
            || $this->isPreviewApproved()
1269
            || $this->isPreviewDeclined());
1270
    }
1271
1272
    /**
1273
     * Returns true if the preview has been declined.
1274
     *
1275
     * @return bool
1276
     */
1277
    protected function isPreviewDeclined()
1278
    {
1279
        $request = $this->getRequest();
1280
1281
        return null !== $request->get('btn_preview_decline');
1282
    }
1283
1284
    /**
1285
     * Gets ACL users.
1286
     */
1287
    protected function getAclUsers(): \Traversable
1288
    {
1289
        $aclUsers = $this->adminObjectAclUserManager instanceof AdminObjectAclUserManager ?
1290
            $this->adminObjectAclUserManager->findUsers() :
1291
            [];
1292
1293
        return \is_array($aclUsers) ? new \ArrayIterator($aclUsers) : $aclUsers;
1294
    }
1295
1296
    /**
1297
     * Gets ACL roles.
1298
     */
1299
    protected function getAclRoles(): \Traversable
1300
    {
1301
        $aclRoles = [];
1302
        $roleHierarchy = $this->pool->getContainer()->getParameter('security.role_hierarchy.roles');
1303
1304
        foreach ($this->pool->getAdminServiceIds() as $id) {
1305
            try {
1306
                $admin = $this->pool->getInstance($id);
1307
            } catch (\Exception $e) {
1308
                continue;
1309
            }
1310
1311
            $baseRole = $admin->getSecurityHandler()->getBaseRole($admin);
1312
            foreach ($admin->getSecurityInformation() as $role => $permissions) {
1313
                $role = sprintf($baseRole, $role);
1314
                $aclRoles[] = $role;
1315
            }
1316
        }
1317
1318
        foreach ($roleHierarchy as $name => $roles) {
1319
            $aclRoles[] = $name;
1320
            $aclRoles = array_merge($aclRoles, $roles);
1321
        }
1322
1323
        $aclRoles = array_unique($aclRoles);
1324
1325
        return \is_array($aclRoles) ? new \ArrayIterator($aclRoles) : $aclRoles;
1326
    }
1327
1328
    /**
1329
     * Validate CSRF token for action without form.
1330
     *
1331
     * @param string $intention
1332
     *
1333
     * @throws HttpException
1334
     */
1335
    protected function validateCsrfToken($intention): void
1336
    {
1337
        $request = $this->getRequest();
1338
        $token = $request->get('_sonata_csrf_token');
1339
1340
        if ($this->csrfTokenManager instanceof CsrfTokenManagerInterface) {
1341
            $valid = $this->csrfTokenManager->isTokenValid(new CsrfToken($intention, $token));
1342
        } else {
1343
            return;
1344
        }
1345
1346
        if (!$valid) {
1347
            throw new HttpException(Response::HTTP_BAD_REQUEST, 'The csrf token is not valid, CSRF attack?');
1348
        }
1349
    }
1350
1351
    /**
1352
     * Escape string for html output.
1353
     *
1354
     * @param string $s
1355
     *
1356
     * @return string
1357
     */
1358
    protected function escapeHtml($s)
1359
    {
1360
        return htmlspecialchars((string) $s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
1361
    }
1362
1363
    /**
1364
     * Get CSRF token.
1365
     *
1366
     * @param string $intention
1367
     *
1368
     * @return string|false
1369
     */
1370
    protected function getCsrfToken($intention)
1371
    {
1372
        if ($this->csrfTokenManager instanceof CsrfTokenManagerInterface) {
1373
            return $this->csrfTokenManager->getToken($intention)->getValue();
1374
        }
1375
1376
        return false;
1377
    }
1378
1379
    /**
1380
     * This method can be overloaded in your custom CRUD controller.
1381
     * It's called from createAction.
1382
     *
1383
     * @param object $object
1384
     *
1385
     * @return Response|null
1386
     */
1387
    protected function preCreate(Request $request, $object)
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1388
    {
1389
        return null;
1390
    }
1391
1392
    /**
1393
     * This method can be overloaded in your custom CRUD controller.
1394
     * It's called from editAction.
1395
     *
1396
     * @param object $object
1397
     *
1398
     * @return Response|null
1399
     */
1400
    protected function preEdit(Request $request, $object)
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1401
    {
1402
        return null;
1403
    }
1404
1405
    /**
1406
     * This method can be overloaded in your custom CRUD controller.
1407
     * It's called from deleteAction.
1408
     *
1409
     * @param object $object
1410
     *
1411
     * @return Response|null
1412
     */
1413
    protected function preDelete(Request $request, $object)
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1414
    {
1415
        return null;
1416
    }
1417
1418
    /**
1419
     * This method can be overloaded in your custom CRUD controller.
1420
     * It's called from showAction.
1421
     *
1422
     * @param object $object
1423
     *
1424
     * @return Response|null
1425
     */
1426
    protected function preShow(Request $request, $object)
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1427
    {
1428
        return null;
1429
    }
1430
1431
    /**
1432
     * This method can be overloaded in your custom CRUD controller.
1433
     * It's called from listAction.
1434
     *
1435
     * @return Response|null
1436
     */
1437
    protected function preList(Request $request)
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1438
    {
1439
        return null;
1440
    }
1441
1442
    /**
1443
     * Translate a message id.
1444
     *
1445
     * @param string $id
1446
     * @param string $domain
1447
     * @param string $locale
1448
     *
1449
     * @return string translated string
1450
     */
1451
    final protected function trans($id, array $parameters = [], $domain = null, $locale = null)
1452
    {
1453
        $domain = $domain ?: $this->admin->getTranslationDomain();
1454
1455
        return $this->translator->trans($id, $parameters, $domain, $locale);
1456
    }
1457
1458
    private function getSelectedTab(Request $request): array
1459
    {
1460
        return array_filter(['_tab' => $request->request->get('_tab')]);
1461
    }
1462
1463
    private function checkParentChildAssociation(Request $request, $object): void
1464
    {
1465
        if (!$this->admin->isChild()) {
1466
            return;
1467
        }
1468
1469
        $parentAdmin = $this->admin->getParent();
1470
        $parentId = $request->get($parentAdmin->getIdParameter());
1471
1472
        $propertyAccessor = PropertyAccess::createPropertyAccessor();
1473
        $propertyPath = new PropertyPath($this->admin->getParentAssociationMapping());
0 ignored issues
show
Bug introduced by
The method getParentAssociationMapping() does not exist on Sonata\AdminBundle\Admin\AdminInterface. Did you maybe mean getParent()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
1474
1475
        if ($parentAdmin->getObject($parentId) !== $propertyAccessor->getValue($object, $propertyPath)) {
1476
            throw new \RuntimeException(sprintf(
1477
                'There is no association between "%s" and "%s"',
1478
                $parentAdmin->toString($parentAdmin->getObject($parentId)),
1479
                $this->admin->toString($object)
1480
            ));
1481
        }
1482
    }
1483
1484
    /**
1485
     * Sets the admin form theme to form view. Used for compatibility between Symfony versions.
1486
     */
1487
    private function setFormTheme(FormView $formView, ?array $theme = null): void
1488
    {
1489
        $this->twig->getRuntime(FormRenderer::class)->setTheme($formView, $theme);
1490
    }
1491
1492
    private function handleXmlHttpRequestErrorResponse(Request $request, FormInterface $form): JsonResponse
1493
    {
1494
        if (!\in_array('application/json', $request->getAcceptableContentTypes(), true)) {
1495
            return $this->renderJson([], Response::HTTP_NOT_ACCEPTABLE);
1496
        }
1497
1498
        $errors = [];
1499
        foreach ($form->getErrors(true) as $error) {
1500
            $errors[] = $error->getMessage();
1501
        }
1502
1503
        return $this->renderJson([
1504
            'result' => 'error',
1505
            'errors' => $errors,
1506
        ], Response::HTTP_BAD_REQUEST);
1507
    }
1508
1509
    /**
1510
     * @param object $object
1511
     */
1512
    private function handleXmlHttpRequestSuccessResponse(Request $request, $object): JsonResponse
1513
    {
1514
        if (!\in_array('application/json', $request->getAcceptableContentTypes(), true)) {
1515
            return $this->renderJson([], Response::HTTP_NOT_ACCEPTABLE);
1516
        }
1517
1518
        return $this->renderJson([
1519
            'result' => 'ok',
1520
            'objectId' => $this->admin->getNormalizedIdentifier($object),
1521
            'objectName' => $this->escapeHtml($this->admin->toString($object)),
1522
        ], Response::HTTP_OK);
1523
    }
1524
}
1525