Completed
Push — 3.x ( 1361f7...dff789 )
by Grégoire
02:58
created

CRUDController::preEdit()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
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\Common\Inflector\Inflector;
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  $parameters An array of parameters to pass to the view
83
     *
84
     * @return Response A Response instance
85
     *
86
     * @deprecated since sonata-project/admin-bundle 3.27, to be removed in 4.0. Use Sonata\AdminBundle\Controller\CRUDController::renderWithExtraParams() instead.
87
     */
88
    public function render($view, array $parameters = [], Response $response = null)
89
    {
90
        @trigger_error(
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

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

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

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
91
            'Method '.__CLASS__.'::render has been renamed to '.__CLASS__.'::renderWithExtraParams.',
92
            E_USER_DEPRECATED
93
        );
94
95
        return $this->renderWithExtraParams($view, $parameters, $response);
96
    }
97
98
    /**
99
     * Renders a view while passing mandatory parameters on to the template.
100
     *
101
     * @param string $view The view name
102
     *
103
     * @return Response A Response instance
104
     */
105
    public function renderWithExtraParams($view, array $parameters = [], Response $response = null)
106
    {
107
        if (!$this->isXmlHttpRequest()) {
108
            $parameters['breadcrumbs_builder'] = $this->get('sonata.admin.breadcrumbs_builder');
109
        }
110
        $parameters['admin'] = $parameters['admin'] ??
111
            $this->admin;
112
113
        $parameters['base_template'] = $parameters['base_template'] ??
114
            $this->getBaseTemplate();
115
116
        $parameters['admin_pool'] = $this->get('sonata.admin.pool');
117
118
        //NEXT_MAJOR: Remove method alias and use $this->render() directly.
119
        return $this->originalRender($view, $parameters, $response);
120
    }
121
122
    /**
123
     * List action.
124
     *
125
     * @throws AccessDeniedException If access is not granted
126
     *
127
     * @return Response
128
     */
129
    public function listAction()
130
    {
131
        $request = $this->getRequest();
132
133
        $this->admin->checkAccess('list');
134
135
        $preResponse = $this->preList($request);
136
        if (null !== $preResponse) {
137
            return $preResponse;
138
        }
139
140
        if ($listMode = $request->get('_list_mode')) {
141
            $this->admin->setListMode($listMode);
142
        }
143
144
        $datagrid = $this->admin->getDatagrid();
145
        $formView = $datagrid->getForm()->createView();
146
147
        // set the theme for the current Admin Form
148
        $this->setFormTheme($formView, $this->admin->getFilterTheme());
149
150
        // NEXT_MAJOR: Remove this line and use commented line below it instead
151
        $template = $this->admin->getTemplate('list');
0 ignored issues
show
Deprecated Code introduced by
The method Sonata\AdminBundle\Admin...nterface::getTemplate() has been deprecated with message: since sonata-project/admin-bundle 3.35. To be removed in 4.0. Use TemplateRegistry services instead

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

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

Loading history...
152
        // $template = $this->templateRegistry->getTemplate('list');
153
154
        return $this->renderWithExtraParams($template, [
155
            'action' => 'list',
156
            'form' => $formView,
157
            'datagrid' => $datagrid,
158
            'csrf_token' => $this->getCsrfToken('sonata.batch'),
159
            'export_formats' => $this->has('sonata.admin.admin_exporter') ?
160
                $this->get('sonata.admin.admin_exporter')->getAvailableFormats($this->admin) :
161
                $this->admin->getExportFormats(),
162
        ], null);
163
    }
164
165
    /**
166
     * Execute a batch delete.
167
     *
168
     * @throws AccessDeniedException If access is not granted
169
     *
170
     * @return RedirectResponse
171
     */
172
    public function batchActionDelete(ProxyQueryInterface $query)
173
    {
174
        $this->admin->checkAccess('batchDelete');
175
176
        $modelManager = $this->admin->getModelManager();
177
178
        try {
179
            $modelManager->batchDelete($this->admin->getClass(), $query);
180
            $this->addFlash(
181
                'sonata_flash_success',
182
                $this->trans('flash_batch_delete_success', [], 'SonataAdminBundle')
183
            );
184
        } catch (ModelManagerException $e) {
185
            $this->handleModelManagerException($e);
186
            $this->addFlash(
187
                'sonata_flash_error',
188
                $this->trans('flash_batch_delete_error', [], 'SonataAdminBundle')
189
            );
190
        }
191
192
        return $this->redirectToList();
193
    }
194
195
    /**
196
     * Delete action.
197
     *
198
     * @param int|string|null $id
199
     *
200
     * @throws NotFoundHttpException If the object does not exist
201
     * @throws AccessDeniedException If access is not granted
202
     *
203
     * @return Response|RedirectResponse
204
     */
205
    public function deleteAction($id)
0 ignored issues
show
Unused Code introduced by
The parameter $id 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...
206
    {
207
        $request = $this->getRequest();
208
        $id = $request->get($this->admin->getIdParameter());
209
        $object = $this->admin->getObject($id);
210
211
        if (!$object) {
212
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
213
        }
214
215
        $this->checkParentChildAssociation($request, $object);
216
217
        $this->admin->checkAccess('delete', $object);
218
219
        $preResponse = $this->preDelete($request, $object);
220
        if (null !== $preResponse) {
221
            return $preResponse;
222
        }
223
224
        if (Request::METHOD_DELETE === $this->getRestMethod()) {
225
            // check the csrf token
226
            $this->validateCsrfToken('sonata.delete');
227
228
            $objectName = $this->admin->toString($object);
229
230
            try {
231
                $this->admin->delete($object);
232
233
                if ($this->isXmlHttpRequest()) {
234
                    return $this->renderJson(['result' => 'ok'], Response::HTTP_OK, []);
235
                }
236
237
                $this->addFlash(
238
                    'sonata_flash_success',
239
                    $this->trans(
240
                        'flash_delete_success',
241
                        ['%name%' => $this->escapeHtml($objectName)],
242
                        'SonataAdminBundle'
243
                    )
244
                );
245
            } catch (ModelManagerException $e) {
246
                $this->handleModelManagerException($e);
247
248
                if ($this->isXmlHttpRequest()) {
249
                    return $this->renderJson(['result' => 'error'], Response::HTTP_OK, []);
250
                }
251
252
                $this->addFlash(
253
                    'sonata_flash_error',
254
                    $this->trans(
255
                        'flash_delete_error',
256
                        ['%name%' => $this->escapeHtml($objectName)],
257
                        'SonataAdminBundle'
258
                    )
259
                );
260
            }
261
262
            return $this->redirectTo($object);
263
        }
264
265
        // NEXT_MAJOR: Remove this line and use commented line below it instead
266
        $template = $this->admin->getTemplate('delete');
0 ignored issues
show
Deprecated Code introduced by
The method Sonata\AdminBundle\Admin...nterface::getTemplate() has been deprecated with message: since sonata-project/admin-bundle 3.35. To be removed in 4.0. Use TemplateRegistry services instead

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

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

Loading history...
267
        // $template = $this->templateRegistry->getTemplate('delete');
268
269
        return $this->renderWithExtraParams($template, [
270
            'object' => $object,
271
            'action' => 'delete',
272
            'csrf_token' => $this->getCsrfToken('sonata.delete'),
273
        ], null);
274
    }
275
276
    /**
277
     * Edit action.
278
     *
279
     * @param int|string|null $id
280
     *
281
     * @throws NotFoundHttpException If the object does not exist
282
     * @throws \RuntimeException     If no editable field is defined
283
     * @throws AccessDeniedException If access is not granted
284
     *
285
     * @return Response|RedirectResponse
286
     */
287
    public function editAction($id = null)
0 ignored issues
show
Unused Code introduced by
The parameter $id 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...
288
    {
289
        $request = $this->getRequest();
290
        // the key used to lookup the template
291
        $templateKey = 'edit';
292
293
        $id = $request->get($this->admin->getIdParameter());
294
        $existingObject = $this->admin->getObject($id);
295
296
        if (!$existingObject) {
297
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
298
        }
299
300
        $this->checkParentChildAssociation($request, $existingObject);
301
302
        $this->admin->checkAccess('edit', $existingObject);
303
304
        $preResponse = $this->preEdit($request, $existingObject);
305
        if (null !== $preResponse) {
306
            return $preResponse;
307
        }
308
309
        $this->admin->setSubject($existingObject);
310
        $objectId = $this->admin->getNormalizedIdentifier($existingObject);
311
312
        $form = $this->admin->getForm();
313
314
        if (!\is_array($fields = $form->all()) || 0 === \count($fields)) {
315
            throw new \RuntimeException(
316
                'No editable field defined. Did you forget to implement the "configureFormFields" method?'
317
            );
318
        }
319
320
        $form->setData($existingObject);
321
        $form->handleRequest($request);
322
323
        if ($form->isSubmitted()) {
324
            $isFormValid = $form->isValid();
325
326
            // persist if the form was valid and if in preview mode the preview was approved
327
            if ($isFormValid && (!$this->isInPreviewMode() || $this->isPreviewApproved())) {
328
                $submittedObject = $form->getData();
329
                $this->admin->setSubject($submittedObject);
330
331
                try {
332
                    $existingObject = $this->admin->update($submittedObject);
333
334
                    if ($this->isXmlHttpRequest()) {
335
                        return $this->handleXmlHttpRequestSuccessResponse($request, $existingObject);
336
                    }
337
338
                    $this->addFlash(
339
                        'sonata_flash_success',
340
                        $this->trans(
341
                            'flash_edit_success',
342
                            ['%name%' => $this->escapeHtml($this->admin->toString($existingObject))],
343
                            'SonataAdminBundle'
344
                        )
345
                    );
346
347
                    // redirect to edit mode
348
                    return $this->redirectTo($existingObject);
349
                } catch (ModelManagerException $e) {
350
                    $this->handleModelManagerException($e);
351
352
                    $isFormValid = false;
353
                } catch (LockException $e) {
354
                    $this->addFlash('sonata_flash_error', $this->trans('flash_lock_error', [
355
                        '%name%' => $this->escapeHtml($this->admin->toString($existingObject)),
356
                        '%link_start%' => '<a href="'.$this->admin->generateObjectUrl('edit', $existingObject).'">',
357
                        '%link_end%' => '</a>',
358
                    ], 'SonataAdminBundle'));
359
                }
360
            }
361
362
            // show an error message if the form failed validation
363
            if (!$isFormValid) {
364
                if ($this->isXmlHttpRequest() && null !== ($response = $this->handleXmlHttpRequestErrorResponse($request, $form))) {
365
                    return $response;
366
                }
367
368
                $this->addFlash(
369
                    'sonata_flash_error',
370
                    $this->trans(
371
                        'flash_edit_error',
372
                        ['%name%' => $this->escapeHtml($this->admin->toString($existingObject))],
373
                        'SonataAdminBundle'
374
                    )
375
                );
376
            } elseif ($this->isPreviewRequested()) {
377
                // enable the preview template if the form was valid and preview was requested
378
                $templateKey = 'preview';
379
                $this->admin->getShow();
380
            }
381
        }
382
383
        $formView = $form->createView();
384
        // set the theme for the current Admin Form
385
        $this->setFormTheme($formView, $this->admin->getFormTheme());
386
387
        // NEXT_MAJOR: Remove this line and use commented line below it instead
388
        $template = $this->admin->getTemplate($templateKey);
0 ignored issues
show
Deprecated Code introduced by
The method Sonata\AdminBundle\Admin...nterface::getTemplate() has been deprecated with message: since sonata-project/admin-bundle 3.35. To be removed in 4.0. Use TemplateRegistry services instead

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

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

Loading history...
389
        // $template = $this->templateRegistry->getTemplate($templateKey);
390
391
        return $this->renderWithExtraParams($template, [
392
            'action' => 'edit',
393
            'form' => $formView,
394
            'object' => $existingObject,
395
            'objectId' => $objectId,
396
        ], null);
397
    }
398
399
    /**
400
     * Batch action.
401
     *
402
     * @throws NotFoundHttpException If the HTTP method is not POST
403
     * @throws \RuntimeException     If the batch action is not defined
404
     *
405
     * @return Response|RedirectResponse
406
     */
407
    public function batchAction()
408
    {
409
        $request = $this->getRequest();
410
        $restMethod = $this->getRestMethod();
411
412
        if (Request::METHOD_POST !== $restMethod) {
413
            throw $this->createNotFoundException(sprintf('Invalid request method given "%s", %s expected', $restMethod, Request::METHOD_POST));
414
        }
415
416
        // check the csrf token
417
        $this->validateCsrfToken('sonata.batch');
418
419
        $confirmation = $request->get('confirmation', false);
420
421
        if ($data = json_decode((string) $request->get('data'), true)) {
422
            $action = $data['action'];
423
            $idx = $data['idx'];
424
            $allElements = $data['all_elements'];
425
            $request->request->replace(array_merge($request->request->all(), $data));
426
        } else {
427
            $request->request->set('idx', $request->get('idx', []));
428
            $request->request->set('all_elements', $request->get('all_elements', false));
429
430
            $action = $request->get('action');
431
            $idx = $request->get('idx');
432
            $allElements = $request->get('all_elements');
433
            $data = $request->request->all();
434
435
            unset($data['_sonata_csrf_token']);
436
        }
437
438
        // NEXT_MAJOR: Remove reflection check.
439
        $reflector = new \ReflectionMethod($this->admin, 'getBatchActions');
440
        if ($reflector->getDeclaringClass()->getName() === \get_class($this->admin)) {
0 ignored issues
show
introduced by
Consider using $reflector->class. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
441
            @trigger_error(
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

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

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

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
442
                'Override Sonata\AdminBundle\Admin\AbstractAdmin::getBatchActions method'
443
                .' is deprecated since version 3.2.'
444
                .' Use Sonata\AdminBundle\Admin\AbstractAdmin::configureBatchActions instead.'
445
                .' The method will be final in 4.0.',
446
                E_USER_DEPRECATED
447
            );
448
        }
449
        $batchActions = $this->admin->getBatchActions();
450
        if (!\array_key_exists($action, $batchActions)) {
451
            throw new \RuntimeException(sprintf('The `%s` batch action is not defined', $action));
452
        }
453
454
        $camelizedAction = Inflector::classify($action);
455
        $isRelevantAction = sprintf('batchAction%sIsRelevant', $camelizedAction);
456
457
        if (method_exists($this, $isRelevantAction)) {
458
            $nonRelevantMessage = $this->{$isRelevantAction}($idx, $allElements, $request);
459
        } else {
460
            $nonRelevantMessage = 0 !== \count($idx) || $allElements; // at least one item is selected
461
        }
462
463
        if (!$nonRelevantMessage) { // default non relevant message (if false of null)
464
            $nonRelevantMessage = 'flash_batch_empty';
465
        }
466
467
        $datagrid = $this->admin->getDatagrid();
468
        $datagrid->buildPager();
469
470
        if (true !== $nonRelevantMessage) {
471
            $this->addFlash(
472
                'sonata_flash_info',
473
                $this->trans($nonRelevantMessage, [], 'SonataAdminBundle')
474
            );
475
476
            return $this->redirectToList();
477
        }
478
479
        $askConfirmation = $batchActions[$action]['ask_confirmation'] ??
480
            true;
481
482
        if ($askConfirmation && 'ok' !== $confirmation) {
483
            $actionLabel = $batchActions[$action]['label'];
484
            $batchTranslationDomain = $batchActions[$action]['translation_domain'] ??
485
                $this->admin->getTranslationDomain();
486
487
            $formView = $datagrid->getForm()->createView();
488
            $this->setFormTheme($formView, $this->admin->getFilterTheme());
489
490
            // NEXT_MAJOR: Remove these lines and use commented lines below them instead
491
            $template = !empty($batchActions[$action]['template']) ?
492
                $batchActions[$action]['template'] :
493
                $this->admin->getTemplate('batch_confirmation');
0 ignored issues
show
Deprecated Code introduced by
The method Sonata\AdminBundle\Admin...nterface::getTemplate() has been deprecated with message: since sonata-project/admin-bundle 3.35. To be removed in 4.0. Use TemplateRegistry services instead

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

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

Loading history...
494
            // $template = !empty($batchActions[$action]['template']) ?
495
            //     $batchActions[$action]['template'] :
496
            //     $this->templateRegistry->getTemplate('batch_confirmation');
497
498
            return $this->renderWithExtraParams($template, [
499
                'action' => 'list',
500
                'action_label' => $actionLabel,
501
                'batch_translation_domain' => $batchTranslationDomain,
502
                'datagrid' => $datagrid,
503
                'form' => $formView,
504
                'data' => $data,
505
                'csrf_token' => $this->getCsrfToken('sonata.batch'),
506
            ], null);
507
        }
508
509
        // execute the action, batchActionXxxxx
510
        $finalAction = sprintf('batchAction%s', $camelizedAction);
511
        if (!method_exists($this, $finalAction)) {
512
            throw new \RuntimeException(sprintf('A `%s::%s` method must be callable', static::class, $finalAction));
513
        }
514
515
        $query = $datagrid->getQuery();
516
517
        $query->setFirstResult(null);
518
        $query->setMaxResults(null);
519
520
        $this->admin->preBatchAction($action, $query, $idx, $allElements);
521
522
        if (\count($idx) > 0) {
523
            $this->admin->getModelManager()->addIdentifiersToQuery($this->admin->getClass(), $query, $idx);
524
        } elseif (!$allElements) {
525
            $this->addFlash(
526
                'sonata_flash_info',
527
                $this->trans('flash_batch_no_elements_processed', [], 'SonataAdminBundle')
528
            );
529
530
            return $this->redirectToList();
531
        }
532
533
        return $this->{$finalAction}($query, $request);
534
    }
535
536
    /**
537
     * Create action.
538
     *
539
     * @throws AccessDeniedException If access is not granted
540
     * @throws \RuntimeException     If no editable field is defined
541
     *
542
     * @return Response
543
     */
544
    public function createAction()
545
    {
546
        $request = $this->getRequest();
547
        // the key used to lookup the template
548
        $templateKey = 'edit';
549
550
        $this->admin->checkAccess('create');
551
552
        $class = new \ReflectionClass($this->admin->hasActiveSubClass() ? $this->admin->getActiveSubClass() : $this->admin->getClass());
553
554
        if ($class->isAbstract()) {
555
            return $this->renderWithExtraParams(
556
                '@SonataAdmin/CRUD/select_subclass.html.twig',
557
                [
558
                    'base_template' => $this->getBaseTemplate(),
559
                    'admin' => $this->admin,
560
                    'action' => 'create',
561
                ],
562
                null
563
            );
564
        }
565
566
        $newObject = $this->admin->getNewInstance();
567
568
        $preResponse = $this->preCreate($request, $newObject);
569
        if (null !== $preResponse) {
570
            return $preResponse;
571
        }
572
573
        $this->admin->setSubject($newObject);
574
575
        $form = $this->admin->getForm();
576
577
        if (!\is_array($fields = $form->all()) || 0 === \count($fields)) {
578
            throw new \RuntimeException(
579
                'No editable field defined. Did you forget to implement the "configureFormFields" method?'
580
            );
581
        }
582
583
        $form->setData($newObject);
584
        $form->handleRequest($request);
585
586
        if ($form->isSubmitted()) {
587
            $isFormValid = $form->isValid();
588
589
            // persist if the form was valid and if in preview mode the preview was approved
590
            if ($isFormValid && (!$this->isInPreviewMode() || $this->isPreviewApproved())) {
591
                $submittedObject = $form->getData();
592
                $this->admin->setSubject($submittedObject);
593
                $this->admin->checkAccess('create', $submittedObject);
594
595
                try {
596
                    $newObject = $this->admin->create($submittedObject);
597
598
                    if ($this->isXmlHttpRequest()) {
599
                        return $this->handleXmlHttpRequestSuccessResponse($request, $newObject);
600
                    }
601
602
                    $this->addFlash(
603
                        'sonata_flash_success',
604
                        $this->trans(
605
                            'flash_create_success',
606
                            ['%name%' => $this->escapeHtml($this->admin->toString($newObject))],
607
                            'SonataAdminBundle'
608
                        )
609
                    );
610
611
                    // redirect to edit mode
612
                    return $this->redirectTo($newObject);
613
                } catch (ModelManagerException $e) {
614
                    $this->handleModelManagerException($e);
615
616
                    $isFormValid = false;
617
                }
618
            }
619
620
            // show an error message if the form failed validation
621
            if (!$isFormValid) {
622
                if ($this->isXmlHttpRequest() && null !== ($response = $this->handleXmlHttpRequestErrorResponse($request, $form))) {
623
                    return $response;
624
                }
625
626
                $this->addFlash(
627
                    'sonata_flash_error',
628
                    $this->trans(
629
                        'flash_create_error',
630
                        ['%name%' => $this->escapeHtml($this->admin->toString($newObject))],
631
                        'SonataAdminBundle'
632
                    )
633
                );
634
            } elseif ($this->isPreviewRequested()) {
635
                // pick the preview template if the form was valid and preview was requested
636
                $templateKey = 'preview';
637
                $this->admin->getShow();
638
            }
639
        }
640
641
        $formView = $form->createView();
642
        // set the theme for the current Admin Form
643
        $this->setFormTheme($formView, $this->admin->getFormTheme());
644
645
        // NEXT_MAJOR: Remove this line and use commented line below it instead
646
        $template = $this->admin->getTemplate($templateKey);
0 ignored issues
show
Deprecated Code introduced by
The method Sonata\AdminBundle\Admin...nterface::getTemplate() has been deprecated with message: since sonata-project/admin-bundle 3.35. To be removed in 4.0. Use TemplateRegistry services instead

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

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

Loading history...
647
        // $template = $this->templateRegistry->getTemplate($templateKey);
648
649
        return $this->renderWithExtraParams($template, [
650
            'action' => 'create',
651
            'form' => $formView,
652
            'object' => $newObject,
653
            'objectId' => null,
654
        ], null);
655
    }
656
657
    /**
658
     * Show action.
659
     *
660
     * @param int|string|null $id
661
     *
662
     * @throws NotFoundHttpException If the object does not exist
663
     * @throws AccessDeniedException If access is not granted
664
     *
665
     * @return Response
666
     */
667
    public function showAction($id = null)
0 ignored issues
show
Unused Code introduced by
The parameter $id 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...
668
    {
669
        $request = $this->getRequest();
670
        $id = $request->get($this->admin->getIdParameter());
671
672
        $object = $this->admin->getObject($id);
673
674
        if (!$object) {
675
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
676
        }
677
678
        $this->checkParentChildAssociation($request, $object);
679
680
        $this->admin->checkAccess('show', $object);
681
682
        $preResponse = $this->preShow($request, $object);
683
        if (null !== $preResponse) {
684
            return $preResponse;
685
        }
686
687
        $this->admin->setSubject($object);
688
689
        $fields = $this->admin->getShow();
690
        \assert($fields instanceof FieldDescriptionCollection);
691
692
        // NEXT_MAJOR: replace deprecation with exception
693
        if (!\is_array($fields->getElements()) || 0 === $fields->count()) {
694
            @trigger_error(
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

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

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

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
695
                'Calling this method without implementing "configureShowFields"'
696
                .' is not supported since sonata-project/admin-bundle 3.40.0'
697
                .' and will no longer be possible in 4.0',
698
                E_USER_DEPRECATED
699
            );
700
        }
701
702
        // NEXT_MAJOR: Remove this line and use commented line below it instead
703
        $template = $this->admin->getTemplate('show');
0 ignored issues
show
Deprecated Code introduced by
The method Sonata\AdminBundle\Admin...nterface::getTemplate() has been deprecated with message: since sonata-project/admin-bundle 3.35. To be removed in 4.0. Use TemplateRegistry services instead

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

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

Loading history...
704
        //$template = $this->templateRegistry->getTemplate('show');
705
706
        return $this->renderWithExtraParams($template, [
707
            'action' => 'show',
708
            'object' => $object,
709
            'elements' => $fields,
710
        ], null);
711
    }
712
713
    /**
714
     * Show history revisions for object.
715
     *
716
     * @param int|string|null $id
717
     *
718
     * @throws AccessDeniedException If access is not granted
719
     * @throws NotFoundHttpException If the object does not exist or the audit reader is not available
720
     *
721
     * @return Response
722
     */
723
    public function historyAction($id = null)
0 ignored issues
show
Unused Code introduced by
The parameter $id 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...
724
    {
725
        $request = $this->getRequest();
726
        $id = $request->get($this->admin->getIdParameter());
727
728
        $object = $this->admin->getObject($id);
729
730
        if (!$object) {
731
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
732
        }
733
734
        $this->admin->checkAccess('history', $object);
735
736
        $manager = $this->get('sonata.admin.audit.manager');
737
738
        if (!$manager->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 = $manager->getReader($this->admin->getClass());
748
749
        $revisions = $reader->findRevisions($this->admin->getClass(), $id);
750
751
        // NEXT_MAJOR: Remove this line and use commented line below it instead
752
        $template = $this->admin->getTemplate('history');
0 ignored issues
show
Deprecated Code introduced by
The method Sonata\AdminBundle\Admin...nterface::getTemplate() has been deprecated with message: since sonata-project/admin-bundle 3.35. To be removed in 4.0. Use TemplateRegistry services instead

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

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

Loading history...
753
        // $template = $this->templateRegistry->getTemplate('history');
754
755
        return $this->renderWithExtraParams($template, [
756
            'action' => 'history',
757
            'object' => $object,
758
            'revisions' => $revisions,
759
            'currentRevision' => $revisions ? current($revisions) : false,
760
        ], null);
761
    }
762
763
    /**
764
     * View history revision of object.
765
     *
766
     * @param int|string|null $id
767
     * @param string|null     $revision
768
     *
769
     * @throws AccessDeniedException If access is not granted
770
     * @throws NotFoundHttpException If the object or revision does not exist or the audit reader is not available
771
     *
772
     * @return Response
773
     */
774
    public function historyViewRevisionAction($id = null, $revision = null)
0 ignored issues
show
Unused Code introduced by
The parameter $id 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...
775
    {
776
        $request = $this->getRequest();
777
        $id = $request->get($this->admin->getIdParameter());
778
779
        $object = $this->admin->getObject($id);
780
781
        if (!$object) {
782
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
783
        }
784
785
        $this->admin->checkAccess('historyViewRevision', $object);
786
787
        $manager = $this->get('sonata.admin.audit.manager');
788
789
        if (!$manager->hasReader($this->admin->getClass())) {
790
            throw $this->createNotFoundException(
791
                sprintf(
792
                    'unable to find the audit reader for class : %s',
793
                    $this->admin->getClass()
794
                )
795
            );
796
        }
797
798
        $reader = $manager->getReader($this->admin->getClass());
799
800
        // retrieve the revisioned object
801
        $object = $reader->find($this->admin->getClass(), $id, $revision);
802
803
        if (!$object) {
804
            throw $this->createNotFoundException(
805
                sprintf(
806
                    'unable to find the targeted object `%s` from the revision `%s` with classname : `%s`',
807
                    $id,
808
                    $revision,
809
                    $this->admin->getClass()
810
                )
811
            );
812
        }
813
814
        $this->admin->setSubject($object);
815
816
        // NEXT_MAJOR: Remove this line and use commented line below it instead
817
        $template = $this->admin->getTemplate('show');
0 ignored issues
show
Deprecated Code introduced by
The method Sonata\AdminBundle\Admin...nterface::getTemplate() has been deprecated with message: since sonata-project/admin-bundle 3.35. To be removed in 4.0. Use TemplateRegistry services instead

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

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

Loading history...
818
        // $template = $this->templateRegistry->getTemplate('show');
819
820
        return $this->renderWithExtraParams($template, [
821
            'action' => 'show',
822
            'object' => $object,
823
            'elements' => $this->admin->getShow(),
824
        ], null);
825
    }
826
827
    /**
828
     * Compare history revisions of object.
829
     *
830
     * @param int|string|null $id
831
     * @param int|string|null $base_revision
832
     * @param int|string|null $compare_revision
833
     *
834
     * @throws AccessDeniedException If access is not granted
835
     * @throws NotFoundHttpException If the object or revision does not exist or the audit reader is not available
836
     *
837
     * @return Response
838
     */
839
    public function historyCompareRevisionsAction($id = null, $base_revision = null, $compare_revision = null)
0 ignored issues
show
Unused Code introduced by
The parameter $id 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...
840
    {
841
        $request = $this->getRequest();
842
843
        $this->admin->checkAccess('historyCompareRevisions');
844
845
        $id = $request->get($this->admin->getIdParameter());
846
847
        $object = $this->admin->getObject($id);
848
849
        if (!$object) {
850
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
851
        }
852
853
        $manager = $this->get('sonata.admin.audit.manager');
854
855
        if (!$manager->hasReader($this->admin->getClass())) {
856
            throw $this->createNotFoundException(
857
                sprintf(
858
                    'unable to find the audit reader for class : %s',
859
                    $this->admin->getClass()
860
                )
861
            );
862
        }
863
864
        $reader = $manager->getReader($this->admin->getClass());
865
866
        // retrieve the base revision
867
        $base_object = $reader->find($this->admin->getClass(), $id, $base_revision);
868
        if (!$base_object) {
869
            throw $this->createNotFoundException(
870
                sprintf(
871
                    'unable to find the targeted object `%s` from the revision `%s` with classname : `%s`',
872
                    $id,
873
                    $base_revision,
874
                    $this->admin->getClass()
875
                )
876
            );
877
        }
878
879
        // retrieve the compare revision
880
        $compare_object = $reader->find($this->admin->getClass(), $id, $compare_revision);
881
        if (!$compare_object) {
882
            throw $this->createNotFoundException(
883
                sprintf(
884
                    'unable to find the targeted object `%s` from the revision `%s` with classname : `%s`',
885
                    $id,
886
                    $compare_revision,
887
                    $this->admin->getClass()
888
                )
889
            );
890
        }
891
892
        $this->admin->setSubject($base_object);
893
894
        // NEXT_MAJOR: Remove this line and use commented line below it instead
895
        $template = $this->admin->getTemplate('show_compare');
0 ignored issues
show
Deprecated Code introduced by
The method Sonata\AdminBundle\Admin...nterface::getTemplate() has been deprecated with message: since sonata-project/admin-bundle 3.35. To be removed in 4.0. Use TemplateRegistry services instead

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

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

Loading history...
896
        // $template = $this->templateRegistry->getTemplate('show_compare');
897
898
        return $this->renderWithExtraParams($template, [
899
            'action' => 'show',
900
            'object' => $base_object,
901
            'object_compare' => $compare_object,
902
            'elements' => $this->admin->getShow(),
903
        ], null);
904
    }
905
906
    /**
907
     * Export data to specified format.
908
     *
909
     * @throws AccessDeniedException If access is not granted
910
     * @throws \RuntimeException     If the export format is invalid
911
     *
912
     * @return Response
913
     */
914
    public function exportAction(Request $request)
915
    {
916
        $this->admin->checkAccess('export');
917
918
        $format = $request->get('format');
919
920
        // NEXT_MAJOR: remove the check
921
        if (!$this->has('sonata.admin.admin_exporter')) {
922
            @trigger_error(
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

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

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

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
923
                'Not registering the exporter bundle is deprecated since version 3.14.'
924
                .' You must register it to be able to use the export action in 4.0.',
925
                E_USER_DEPRECATED
926
            );
927
            $allowedExportFormats = (array) $this->admin->getExportFormats();
928
929
            $class = (string) $this->admin->getClass();
930
            $filename = sprintf(
931
                'export_%s_%s.%s',
932
                strtolower((string) substr($class, strripos($class, '\\') + 1)),
933
                date('Y_m_d_H_i_s', strtotime('now')),
934
                $format
935
            );
936
            $exporter = $this->get('sonata.admin.exporter');
937
        } else {
938
            $adminExporter = $this->get('sonata.admin.admin_exporter');
939
            $allowedExportFormats = $adminExporter->getAvailableFormats($this->admin);
940
            $filename = $adminExporter->getExportFilename($this->admin, $format);
941
            $exporter = $this->get('sonata.exporter.exporter');
942
        }
943
944
        if (!\in_array($format, $allowedExportFormats, true)) {
945
            throw new \RuntimeException(
946
                sprintf(
947
                    'Export in format `%s` is not allowed for class: `%s`. Allowed formats are: `%s`',
948
                    $format,
949
                    $this->admin->getClass(),
950
                    implode(', ', $allowedExportFormats)
951
                )
952
            );
953
        }
954
955
        return $exporter->getResponse(
956
            $format,
957
            $filename,
958
            $this->admin->getDataSourceIterator()
959
        );
960
    }
961
962
    /**
963
     * Returns the Response object associated to the acl action.
964
     *
965
     * @param int|string|null $id
966
     *
967
     * @throws AccessDeniedException If access is not granted
968
     * @throws NotFoundHttpException If the object does not exist or the ACL is not enabled
969
     *
970
     * @return Response|RedirectResponse
971
     */
972
    public function aclAction($id = null)
0 ignored issues
show
Unused Code introduced by
The parameter $id 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...
973
    {
974
        $request = $this->getRequest();
975
976
        if (!$this->admin->isAclEnabled()) {
977
            throw $this->createNotFoundException('ACL are not enabled for this admin');
978
        }
979
980
        $id = $request->get($this->admin->getIdParameter());
981
982
        $object = $this->admin->getObject($id);
983
984
        if (!$object) {
985
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
986
        }
987
988
        $this->admin->checkAccess('acl', $object);
989
990
        $this->admin->setSubject($object);
991
        $aclUsers = $this->getAclUsers();
992
        $aclRoles = $this->getAclRoles();
993
994
        $adminObjectAclManipulator = $this->get('sonata.admin.object.manipulator.acl.admin');
995
        $adminObjectAclData = new AdminObjectAclData(
996
            $this->admin,
997
            $object,
998
            $aclUsers,
999
            $adminObjectAclManipulator->getMaskBuilderClass(),
1000
            $aclRoles
1001
        );
1002
1003
        $aclUsersForm = $adminObjectAclManipulator->createAclUsersForm($adminObjectAclData);
1004
        $aclRolesForm = $adminObjectAclManipulator->createAclRolesForm($adminObjectAclData);
1005
1006
        if (Request::METHOD_POST === $request->getMethod()) {
1007
            if ($request->request->has(AdminObjectAclManipulator::ACL_USERS_FORM_NAME)) {
1008
                $form = $aclUsersForm;
1009
                $updateMethod = 'updateAclUsers';
1010
            } elseif ($request->request->has(AdminObjectAclManipulator::ACL_ROLES_FORM_NAME)) {
1011
                $form = $aclRolesForm;
1012
                $updateMethod = 'updateAclRoles';
1013
            }
1014
1015
            if (isset($form)) {
1016
                $form->handleRequest($request);
1017
1018
                if ($form->isValid()) {
1019
                    $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...
1020
                    $this->addFlash(
1021
                        'sonata_flash_success',
1022
                        $this->trans('flash_acl_edit_success', [], 'SonataAdminBundle')
1023
                    );
1024
1025
                    return new RedirectResponse($this->admin->generateObjectUrl('acl', $object));
1026
                }
1027
            }
1028
        }
1029
1030
        // NEXT_MAJOR: Remove this line and use commented line below it instead
1031
        $template = $this->admin->getTemplate('acl');
0 ignored issues
show
Deprecated Code introduced by
The method Sonata\AdminBundle\Admin...nterface::getTemplate() has been deprecated with message: since sonata-project/admin-bundle 3.35. To be removed in 4.0. Use TemplateRegistry services instead

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

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

Loading history...
1032
        // $template = $this->templateRegistry->getTemplate('acl');
1033
1034
        return $this->renderWithExtraParams($template, [
1035
            'action' => 'acl',
1036
            'permissions' => $adminObjectAclData->getUserPermissions(),
1037
            'object' => $object,
1038
            'users' => $aclUsers,
1039
            'roles' => $aclRoles,
1040
            'aclUsersForm' => $aclUsersForm->createView(),
1041
            'aclRolesForm' => $aclRolesForm->createView(),
1042
        ], null);
1043
    }
1044
1045
    /**
1046
     * @return Request
1047
     */
1048
    public function getRequest()
1049
    {
1050
        return $this->container->get('request_stack')->getCurrentRequest();
1051
    }
1052
1053
    /**
1054
     * Gets a container configuration parameter by its name.
1055
     *
1056
     * @param string $name The parameter name
1057
     *
1058
     * @return mixed
1059
     */
1060
    protected function getParameter($name)
1061
    {
1062
        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...
1063
    }
1064
1065
    /**
1066
     * Render JSON.
1067
     *
1068
     * @param mixed $data
1069
     * @param int   $status
1070
     * @param array $headers
1071
     *
1072
     * @return JsonResponse with json encoded data
1073
     */
1074
    protected function renderJson($data, $status = Response::HTTP_OK, $headers = [])
1075
    {
1076
        return new JsonResponse($data, $status, $headers);
1077
    }
1078
1079
    /**
1080
     * Returns true if the request is a XMLHttpRequest.
1081
     *
1082
     * @return bool True if the request is an XMLHttpRequest, false otherwise
1083
     */
1084
    protected function isXmlHttpRequest()
1085
    {
1086
        $request = $this->getRequest();
1087
1088
        return $request->isXmlHttpRequest() || $request->get('_xml_http_request');
1089
    }
1090
1091
    /**
1092
     * Returns the correct RESTful verb, given either by the request itself or
1093
     * via the "_method" parameter.
1094
     *
1095
     * @return string HTTP method, either
1096
     */
1097
    protected function getRestMethod()
1098
    {
1099
        $request = $this->getRequest();
1100
1101
        if (Request::getHttpMethodParameterOverride() || !$request->request->has('_method')) {
1102
            return $request->getMethod();
1103
        }
1104
1105
        return $request->request->get('_method');
1106
    }
1107
1108
    /**
1109
     * Contextualize the admin class depends on the current request.
1110
     *
1111
     * @throws \RuntimeException
1112
     */
1113
    protected function configure()
1114
    {
1115
        $request = $this->getRequest();
1116
1117
        $adminCode = $request->get('_sonata_admin');
1118
1119
        if (!$adminCode) {
1120
            throw new \RuntimeException(sprintf(
1121
                'There is no `_sonata_admin` defined for the controller `%s` and the current route `%s`',
1122
                static::class,
1123
                $request->get('_route')
1124
            ));
1125
        }
1126
1127
        $this->admin = $this->container->get('sonata.admin.pool')->getAdminByAdminCode($adminCode);
1128
1129
        if (!$this->admin) {
1130
            throw new \RuntimeException(sprintf(
1131
                'Unable to find the admin class related to the current controller (%s)',
1132
                static::class
1133
            ));
1134
        }
1135
1136
        $this->templateRegistry = $this->container->get($this->admin->getCode().'.template_registry');
1137
        if (!$this->templateRegistry instanceof TemplateRegistryInterface) {
1138
            throw new \RuntimeException(sprintf(
1139
                'Unable to find the template registry related to the current admin (%s)',
1140
                $this->admin->getCode()
1141
            ));
1142
        }
1143
1144
        $rootAdmin = $this->admin;
1145
1146
        while ($rootAdmin->isChild()) {
1147
            $rootAdmin->setCurrentChild(true);
1148
            $rootAdmin = $rootAdmin->getParent();
1149
        }
1150
1151
        $rootAdmin->setRequest($request);
1152
1153
        if ($request->get('uniqid')) {
1154
            $this->admin->setUniqid($request->get('uniqid'));
1155
        }
1156
    }
1157
1158
    /**
1159
     * Proxy for the logger service of the container.
1160
     * If no such service is found, a NullLogger is returned.
1161
     *
1162
     * @return LoggerInterface
1163
     */
1164
    protected function getLogger()
1165
    {
1166
        if ($this->container->has('logger')) {
1167
            $logger = $this->container->get('logger');
1168
            \assert($logger instanceof LoggerInterface);
1169
1170
            return $logger;
1171
        }
1172
1173
        return new NullLogger();
1174
    }
1175
1176
    /**
1177
     * Returns the base template name.
1178
     *
1179
     * @return string The template name
1180
     */
1181
    protected function getBaseTemplate()
1182
    {
1183
        if ($this->isXmlHttpRequest()) {
1184
            // NEXT_MAJOR: Remove this line and use commented line below it instead
1185
            return $this->admin->getTemplate('ajax');
0 ignored issues
show
Deprecated Code introduced by
The method Sonata\AdminBundle\Admin...nterface::getTemplate() has been deprecated with message: since sonata-project/admin-bundle 3.35. To be removed in 4.0. Use TemplateRegistry services instead

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

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

Loading history...
1186
            // return $this->templateRegistry->getTemplate('ajax');
1187
        }
1188
1189
        // NEXT_MAJOR: Remove this line and use commented line below it instead
1190
        return $this->admin->getTemplate('layout');
0 ignored issues
show
Deprecated Code introduced by
The method Sonata\AdminBundle\Admin...nterface::getTemplate() has been deprecated with message: since sonata-project/admin-bundle 3.35. To be removed in 4.0. Use TemplateRegistry services instead

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

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

Loading history...
1191
        // return $this->templateRegistry->getTemplate('layout');
1192
    }
1193
1194
    /**
1195
     * @throws \Exception
1196
     */
1197
    protected function handleModelManagerException(\Exception $e)
1198
    {
1199
        if ($this->get('kernel')->isDebug()) {
1200
            throw $e;
1201
        }
1202
1203
        $context = ['exception' => $e];
1204
        if ($e->getPrevious()) {
1205
            $context['previous_exception_message'] = $e->getPrevious()->getMessage();
1206
        }
1207
        $this->getLogger()->error($e->getMessage(), $context);
1208
    }
1209
1210
    /**
1211
     * Redirect the user depend on this choice.
1212
     *
1213
     * @param object $object
1214
     *
1215
     * @return RedirectResponse
1216
     */
1217
    protected function redirectTo($object)
1218
    {
1219
        $request = $this->getRequest();
1220
1221
        $url = false;
1222
1223
        if (null !== $request->get('btn_update_and_list')) {
1224
            return $this->redirectToList();
1225
        }
1226
        if (null !== $request->get('btn_create_and_list')) {
1227
            return $this->redirectToList();
1228
        }
1229
1230
        if (null !== $request->get('btn_create_and_create')) {
1231
            $params = [];
1232
            if ($this->admin->hasActiveSubClass()) {
1233
                $params['subclass'] = $request->get('subclass');
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $params['subclass'] is correct as $request->get('subclass') (which targets Symfony\Component\HttpFoundation\Request::get()) seems to always return null.

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

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

}

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

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

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

Loading history...
1234
            }
1235
            $url = $this->admin->generateUrl('create', $params);
1236
        }
1237
1238
        if ('DELETE' === $this->getRestMethod()) {
1239
            return $this->redirectToList();
1240
        }
1241
1242
        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...
1243
            foreach (['edit', 'show'] as $route) {
1244
                if ($this->admin->hasRoute($route) && $this->admin->hasAccess($route, $object)) {
1245
                    $url = $this->admin->generateObjectUrl(
1246
                        $route,
1247
                        $object,
1248
                        $this->getSelectedTab($request)
1249
                    );
1250
1251
                    break;
1252
                }
1253
            }
1254
        }
1255
1256
        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...
1257
            return $this->redirectToList();
1258
        }
1259
1260
        return new RedirectResponse($url);
1261
    }
1262
1263
    /**
1264
     * Redirects the user to the list view.
1265
     *
1266
     * @return RedirectResponse
1267
     */
1268
    final protected function redirectToList()
1269
    {
1270
        $parameters = [];
1271
1272
        if ($filter = $this->admin->getFilterParameters()) {
1273
            $parameters['filter'] = $filter;
1274
        }
1275
1276
        return $this->redirect($this->admin->generateUrl('list', $parameters));
1277
    }
1278
1279
    /**
1280
     * Returns true if the preview is requested to be shown.
1281
     *
1282
     * @return bool
1283
     */
1284
    protected function isPreviewRequested()
1285
    {
1286
        $request = $this->getRequest();
1287
1288
        return null !== $request->get('btn_preview');
1289
    }
1290
1291
    /**
1292
     * Returns true if the preview has been approved.
1293
     *
1294
     * @return bool
1295
     */
1296
    protected function isPreviewApproved()
1297
    {
1298
        $request = $this->getRequest();
1299
1300
        return null !== $request->get('btn_preview_approve');
1301
    }
1302
1303
    /**
1304
     * Returns true if the request is in the preview workflow.
1305
     *
1306
     * That means either a preview is requested or the preview has already been shown
1307
     * and it got approved/declined.
1308
     *
1309
     * @return bool
1310
     */
1311
    protected function isInPreviewMode()
1312
    {
1313
        return $this->admin->supportsPreviewMode()
1314
        && ($this->isPreviewRequested()
1315
            || $this->isPreviewApproved()
1316
            || $this->isPreviewDeclined());
1317
    }
1318
1319
    /**
1320
     * Returns true if the preview has been declined.
1321
     *
1322
     * @return bool
1323
     */
1324
    protected function isPreviewDeclined()
1325
    {
1326
        $request = $this->getRequest();
1327
1328
        return null !== $request->get('btn_preview_decline');
1329
    }
1330
1331
    /**
1332
     * Gets ACL users.
1333
     *
1334
     * @return \Traversable
1335
     */
1336
    protected function getAclUsers()
1337
    {
1338
        $aclUsers = [];
1339
1340
        $userManagerServiceName = $this->container->getParameter('sonata.admin.security.acl_user_manager');
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...
1341
        if (null !== $userManagerServiceName && $this->has($userManagerServiceName)) {
1342
            $userManager = $this->get($userManagerServiceName);
1343
1344
            if (method_exists($userManager, 'findUsers')) {
1345
                $aclUsers = $userManager->findUsers();
1346
            }
1347
        }
1348
1349
        return \is_array($aclUsers) ? new \ArrayIterator($aclUsers) : $aclUsers;
1350
    }
1351
1352
    /**
1353
     * Gets ACL roles.
1354
     *
1355
     * @return \Traversable
1356
     */
1357
    protected function getAclRoles()
1358
    {
1359
        $aclRoles = [];
1360
        $roleHierarchy = $this->container->getParameter('security.role_hierarchy.roles');
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...
1361
        $pool = $this->container->get('sonata.admin.pool');
1362
1363
        foreach ($pool->getAdminServiceIds() as $id) {
1364
            try {
1365
                $admin = $pool->getInstance($id);
1366
            } catch (\Exception $e) {
1367
                continue;
1368
            }
1369
1370
            $baseRole = $admin->getSecurityHandler()->getBaseRole($admin);
1371
            foreach ($admin->getSecurityInformation() as $role => $permissions) {
1372
                $role = sprintf($baseRole, $role);
1373
                $aclRoles[] = $role;
1374
            }
1375
        }
1376
1377
        foreach ($roleHierarchy as $name => $roles) {
1378
            $aclRoles[] = $name;
1379
            $aclRoles = array_merge($aclRoles, $roles);
1380
        }
1381
1382
        $aclRoles = array_unique($aclRoles);
1383
1384
        return \is_array($aclRoles) ? new \ArrayIterator($aclRoles) : $aclRoles;
1385
    }
1386
1387
    /**
1388
     * Validate CSRF token for action without form.
1389
     *
1390
     * @param string $intention
1391
     *
1392
     * @throws HttpException
1393
     */
1394
    protected function validateCsrfToken($intention)
1395
    {
1396
        if (false === $this->admin->getFormBuilder()->getOption('csrf_protection')) {
1397
            return;
1398
        }
1399
1400
        $request = $this->getRequest();
1401
        $token = $request->get('_sonata_csrf_token');
1402
1403
        if ($this->container->has('security.csrf.token_manager')) {
1404
            $valid = $this->container->get('security.csrf.token_manager')->isTokenValid(new CsrfToken($intention, $token));
1405
        } else {
1406
            return;
1407
        }
1408
1409
        if (!$valid) {
1410
            throw new HttpException(Response::HTTP_BAD_REQUEST, 'The csrf token is not valid, CSRF attack?');
1411
        }
1412
    }
1413
1414
    /**
1415
     * Escape string for html output.
1416
     *
1417
     * @param string $s
1418
     *
1419
     * @return string
1420
     */
1421
    protected function escapeHtml($s)
1422
    {
1423
        return htmlspecialchars((string) $s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
1424
    }
1425
1426
    /**
1427
     * Get CSRF token.
1428
     *
1429
     * @param string $intention
1430
     *
1431
     * @return string|false
1432
     */
1433
    protected function getCsrfToken($intention)
1434
    {
1435
        if ($this->container->has('security.csrf.token_manager')) {
1436
            return $this->container->get('security.csrf.token_manager')->getToken($intention)->getValue();
1437
        }
1438
1439
        return false;
1440
    }
1441
1442
    /**
1443
     * This method can be overloaded in your custom CRUD controller.
1444
     * It's called from createAction.
1445
     *
1446
     * @param object $object
1447
     *
1448
     * @return Response|null
1449
     */
1450
    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...
1451
    {
1452
        return null;
1453
    }
1454
1455
    /**
1456
     * This method can be overloaded in your custom CRUD controller.
1457
     * It's called from editAction.
1458
     *
1459
     * @param object $object
1460
     *
1461
     * @return Response|null
1462
     */
1463
    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...
1464
    {
1465
        return null;
1466
    }
1467
1468
    /**
1469
     * This method can be overloaded in your custom CRUD controller.
1470
     * It's called from deleteAction.
1471
     *
1472
     * @param object $object
1473
     *
1474
     * @return Response|null
1475
     */
1476
    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...
1477
    {
1478
        return null;
1479
    }
1480
1481
    /**
1482
     * This method can be overloaded in your custom CRUD controller.
1483
     * It's called from showAction.
1484
     *
1485
     * @param object $object
1486
     *
1487
     * @return Response|null
1488
     */
1489
    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...
1490
    {
1491
        return null;
1492
    }
1493
1494
    /**
1495
     * This method can be overloaded in your custom CRUD controller.
1496
     * It's called from listAction.
1497
     *
1498
     * @return Response|null
1499
     */
1500
    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...
1501
    {
1502
        return null;
1503
    }
1504
1505
    /**
1506
     * Translate a message id.
1507
     *
1508
     * @param string $id
1509
     * @param string $domain
1510
     * @param string $locale
1511
     *
1512
     * @return string translated string
1513
     */
1514
    final protected function trans($id, array $parameters = [], $domain = null, $locale = null)
1515
    {
1516
        $domain = $domain ?: $this->admin->getTranslationDomain();
1517
1518
        return $this->get('translator')->trans($id, $parameters, $domain, $locale);
1519
    }
1520
1521
    private function getSelectedTab(Request $request): array
1522
    {
1523
        return array_filter(['_tab' => $request->request->get('_tab')]);
1524
    }
1525
1526
    private function checkParentChildAssociation(Request $request, $object): void
1527
    {
1528
        if (!($parentAdmin = $this->admin->getParent())) {
1529
            return;
1530
        }
1531
1532
        // NEXT_MAJOR: remove this check
1533
        if (!$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...
1534
            return;
1535
        }
1536
1537
        $parentId = $request->get($parentAdmin->getIdParameter());
1538
1539
        $propertyAccessor = PropertyAccess::createPropertyAccessor();
1540
        $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...
1541
1542
        if ($parentAdmin->getObject($parentId) !== $propertyAccessor->getValue($object, $propertyPath)) {
1543
            // NEXT_MAJOR: make this exception
1544
            @trigger_error(
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

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

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

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
1545
                "Accessing a child that isn't connected to a given parent is"
1546
                ." deprecated since sonata-project/admin-bundle 3.34 and won't be allowed in 4.0.",
1547
                E_USER_DEPRECATED
1548
            );
1549
        }
1550
    }
1551
1552
    /**
1553
     * Sets the admin form theme to form view. Used for compatibility between Symfony versions.
1554
     */
1555
    private function setFormTheme(FormView $formView, array $theme = null): void
1556
    {
1557
        $twig = $this->get('twig');
1558
1559
        $twig->getRuntime(FormRenderer::class)->setTheme($formView, $theme);
1560
    }
1561
1562
    private function handleXmlHttpRequestErrorResponse(Request $request, FormInterface $form): ?JsonResponse
1563
    {
1564
        if ('application/json' !== $request->headers->get('Accept')) {
1565
            @trigger_error('In next major version response will return 406 NOT ACCEPTABLE without `Accept: application/json`', E_USER_DEPRECATED);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

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

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

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
1566
1567
            return null;
1568
        }
1569
1570
        $errors = [];
1571
        foreach ($form->getErrors(true) as $error) {
1572
            $errors[] = $error->getMessage();
1573
        }
1574
1575
        return $this->renderJson([
1576
            'result' => 'error',
1577
            'errors' => $errors,
1578
        ], Response::HTTP_BAD_REQUEST);
1579
    }
1580
1581
    /**
1582
     * @param object $object
1583
     */
1584
    private function handleXmlHttpRequestSuccessResponse(Request $request, $object): JsonResponse
1585
    {
1586
        if ('application/json' !== $request->headers->get('Accept')) {
1587
            @trigger_error('In next major version response will return 406 NOT ACCEPTABLE without `Accept: application/json`', E_USER_DEPRECATED);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

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

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

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
1588
        }
1589
1590
        return $this->renderJson([
1591
            'result' => 'ok',
1592
            'objectId' => $this->admin->getNormalizedIdentifier($object),
1593
            'objectName' => $this->escapeHtml($this->admin->toString($object)),
1594
        ], Response::HTTP_OK);
1595
    }
1596
}
1597