Completed
Push — 3.x ( ed3903...bf091c )
by Grégoire
12:08
created

CRUDController::checkParentChildAssociation()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 24
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 24
rs 8.6845
c 0
b 0
f 0
cc 4
eloc 12
nc 4
nop 2
1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 54 and the first side effect is on line 48.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
2
3
/*
4
 * This file is part of the Sonata Project package.
5
 *
6
 * (c) Thomas Rabaix <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Sonata\AdminBundle\Controller;
13
14
use Doctrine\Common\Inflector\Inflector;
15
use Psr\Log\LoggerInterface;
16
use Psr\Log\NullLogger;
17
use Sonata\AdminBundle\Admin\AdminInterface;
18
use Sonata\AdminBundle\Datagrid\ProxyQueryInterface;
19
use Sonata\AdminBundle\Exception\LockException;
20
use Sonata\AdminBundle\Exception\ModelManagerException;
21
use Sonata\AdminBundle\Templating\TemplateRegistryInterface;
22
use Sonata\AdminBundle\Util\AdminObjectAclData;
23
use Sonata\AdminBundle\Util\AdminObjectAclManipulator;
24
use Symfony\Bridge\Twig\AppVariable;
25
use Symfony\Bridge\Twig\Command\DebugCommand;
26
use Symfony\Bridge\Twig\Extension\FormExtension;
27
use Symfony\Bridge\Twig\Form\TwigRenderer;
28
use Symfony\Bundle\FrameworkBundle\Controller\ControllerTrait;
29
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
30
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
31
use Symfony\Component\DependencyInjection\ContainerInterface;
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
// BC for Symfony < 3.3 where this trait does not exist
46
// NEXT_MAJOR: Remove the polyfill and inherit from \Symfony\Bundle\FrameworkBundle\Controller\Controller again
47
if (!trait_exists(ControllerTrait::class)) {
48
    require_once __DIR__.'/PolyfillControllerTrait.php';
49
}
50
51
/**
52
 * @author Thomas Rabaix <[email protected]>
53
 */
54
class CRUDController implements ContainerAwareInterface
55
{
56
    // NEXT_MAJOR: Don't use these traits anymore (inherit from Controller instead)
57
    use ControllerTrait, ContainerAwareTrait {
58
        ControllerTrait::render as originalRender;
59
    }
60
61
    /**
62
     * The related Admin class.
63
     *
64
     * @var AdminInterface
65
     */
66
    protected $admin;
67
68
    /**
69
     * The template registry of the related Admin class.
70
     *
71
     * @var TemplateRegistryInterface
72
     */
73
    private $templateRegistry;
74
75
    // BC for Symfony 3.3 where ControllerTrait exists but does not contain get() and has() methods.
76
    public function __call($method, $arguments)
77
    {
78
        if (in_array($method, ['get', 'has'])) {
79
            return call_user_func_array([$this->container, $method], $arguments);
80
        }
81
82
        if (method_exists($this, 'proxyToControllerClass')) {
83
            return $this->proxyToControllerClass($method, $arguments);
0 ignored issues
show
Documentation Bug introduced by
The method proxyToControllerClass does not exist on object<Sonata\AdminBundl...troller\CRUDController>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
84
        }
85
86
        throw new \LogicException('Call to undefined method '.__CLASS__.'::'.$method);
87
    }
88
89
    public function setContainer(ContainerInterface $container = null)
90
    {
91
        $this->container = $container;
92
93
        $this->configure();
94
    }
95
96
    /**
97
     * NEXT_MAJOR: Remove this method.
98
     *
99
     * @see renderWithExtraParams()
100
     *
101
     * @param string $view       The view name
102
     * @param array  $parameters An array of parameters to pass to the view
103
     *
104
     * @return Response A Response instance
105
     *
106
     * @deprecated since version 3.27, to be removed in 4.0. Use Sonata\AdminBundle\Controller\CRUDController::renderWithExtraParams() instead.
107
     */
108
    public function render($view, array $parameters = [], Response $response = null)
109
    {
110
        @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...
111
            'Method '.__CLASS__.'::render has been renamed to '.__CLASS__.'::renderWithExtraParams.',
112
            E_USER_DEPRECATED
113
        );
114
115
        return $this->renderWithExtraParams($view, $parameters, $response);
116
    }
117
118
    /**
119
     * Renders a view while passing mandatory parameters on to the template.
120
     *
121
     * @param string $view The view name
122
     *
123
     * @return Response A Response instance
124
     */
125
    public function renderWithExtraParams($view, array $parameters = [], Response $response = null)
126
    {
127
        if (!$this->isXmlHttpRequest()) {
128
            $parameters['breadcrumbs_builder'] = $this->get('sonata.admin.breadcrumbs_builder');
129
        }
130
        $parameters['admin'] = isset($parameters['admin']) ?
131
            $parameters['admin'] :
132
            $this->admin;
133
134
        $parameters['base_template'] = isset($parameters['base_template']) ?
135
            $parameters['base_template'] :
136
            $this->getBaseTemplate();
137
138
        $parameters['admin_pool'] = $this->get('sonata.admin.pool');
139
140
        //NEXT_MAJOR: Remove method alias and use $this->render() directly.
141
        return $this->originalRender($view, $parameters, $response);
142
    }
143
144
    /**
145
     * List action.
146
     *
147
     * @throws AccessDeniedException If access is not granted
148
     *
149
     * @return Response
150
     */
151
    public function listAction()
152
    {
153
        $request = $this->getRequest();
154
155
        $this->admin->checkAccess('list');
156
157
        $preResponse = $this->preList($request);
158
        if (null !== $preResponse) {
159
            return $preResponse;
160
        }
161
162
        if ($listMode = $request->get('_list_mode')) {
163
            $this->admin->setListMode($listMode);
164
        }
165
166
        $datagrid = $this->admin->getDatagrid();
167
        $formView = $datagrid->getForm()->createView();
168
169
        // set the theme for the current Admin Form
170
        $this->setFormTheme($formView, $this->admin->getFilterTheme());
0 ignored issues
show
Documentation introduced by
$this->admin->getFilterTheme() is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
171
172
        return $this->renderWithExtraParams($this->templateRegistry->getTemplate('list'), [
173
            'action' => 'list',
174
            'form' => $formView,
175
            'datagrid' => $datagrid,
176
            'csrf_token' => $this->getCsrfToken('sonata.batch'),
177
            'export_formats' => $this->has('sonata.admin.admin_exporter') ?
178
                $this->get('sonata.admin.admin_exporter')->getAvailableFormats($this->admin) :
179
                $this->admin->getExportFormats(),
180
        ], null);
181
    }
182
183
    /**
184
     * Execute a batch delete.
185
     *
186
     * @throws AccessDeniedException If access is not granted
187
     *
188
     * @return RedirectResponse
189
     */
190
    public function batchActionDelete(ProxyQueryInterface $query)
191
    {
192
        $this->admin->checkAccess('batchDelete');
193
194
        $modelManager = $this->admin->getModelManager();
195
196
        try {
197
            $modelManager->batchDelete($this->admin->getClass(), $query);
198
            $this->addFlash(
199
                'sonata_flash_success',
200
                $this->trans('flash_batch_delete_success', [], 'SonataAdminBundle')
201
            );
202
        } catch (ModelManagerException $e) {
203
            $this->handleModelManagerException($e);
204
            $this->addFlash(
205
                'sonata_flash_error',
206
                $this->trans('flash_batch_delete_error', [], 'SonataAdminBundle')
207
            );
208
        }
209
210
        return $this->redirectToList();
211
    }
212
213
    /**
214
     * Delete action.
215
     *
216
     * @param int|string|null $id
217
     *
218
     * @throws NotFoundHttpException If the object does not exist
219
     * @throws AccessDeniedException If access is not granted
220
     *
221
     * @return Response|RedirectResponse
222
     */
223
    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...
224
    {
225
        $request = $this->getRequest();
226
        $id = $request->get($this->admin->getIdParameter());
227
        $object = $this->admin->getObject($id);
228
229
        if (!$object) {
230
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
231
        }
232
233
        $this->checkParentChildAssociation($request, $object);
234
235
        $this->admin->checkAccess('delete', $object);
236
237
        $preResponse = $this->preDelete($request, $object);
238
        if (null !== $preResponse) {
239
            return $preResponse;
240
        }
241
242
        if ('DELETE' == $this->getRestMethod()) {
243
            // check the csrf token
244
            $this->validateCsrfToken('sonata.delete');
245
246
            $objectName = $this->admin->toString($object);
247
248
            try {
249
                $this->admin->delete($object);
250
251
                if ($this->isXmlHttpRequest()) {
252
                    return $this->renderJson(['result' => 'ok'], 200, []);
253
                }
254
255
                $this->addFlash(
256
                    'sonata_flash_success',
257
                    $this->trans(
258
                        'flash_delete_success',
259
                        ['%name%' => $this->escapeHtml($objectName)],
260
                        'SonataAdminBundle'
261
                    )
262
                );
263
            } catch (ModelManagerException $e) {
264
                $this->handleModelManagerException($e);
265
266
                if ($this->isXmlHttpRequest()) {
267
                    return $this->renderJson(['result' => 'error'], 200, []);
268
                }
269
270
                $this->addFlash(
271
                    'sonata_flash_error',
272
                    $this->trans(
273
                        'flash_delete_error',
274
                        ['%name%' => $this->escapeHtml($objectName)],
275
                        'SonataAdminBundle'
276
                    )
277
                );
278
            }
279
280
            return $this->redirectTo($object);
281
        }
282
283
        return $this->renderWithExtraParams($this->templateRegistry->getTemplate('delete'), [
284
            'object' => $object,
285
            'action' => 'delete',
286
            'csrf_token' => $this->getCsrfToken('sonata.delete'),
287
        ], null);
288
    }
289
290
    /**
291
     * Edit action.
292
     *
293
     * @param int|string|null $id
294
     *
295
     * @throws NotFoundHttpException If the object does not exist
296
     * @throws AccessDeniedException If access is not granted
297
     *
298
     * @return Response|RedirectResponse
299
     */
300
    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...
301
    {
302
        $request = $this->getRequest();
303
        // the key used to lookup the template
304
        $templateKey = 'edit';
305
306
        $id = $request->get($this->admin->getIdParameter());
307
        $existingObject = $this->admin->getObject($id);
308
309
        if (!$existingObject) {
310
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
311
        }
312
313
        $this->checkParentChildAssociation($request, $existingObject);
314
315
        $this->admin->checkAccess('edit', $existingObject);
316
317
        $preResponse = $this->preEdit($request, $existingObject);
318
        if (null !== $preResponse) {
319
            return $preResponse;
320
        }
321
322
        $this->admin->setSubject($existingObject);
323
        $objectId = $this->admin->getNormalizedIdentifier($existingObject);
324
325
        /** @var $form Form */
326
        $form = $this->admin->getForm();
327
        $form->setData($existingObject);
328
        $form->handleRequest($request);
329
330
        if ($form->isSubmitted()) {
331
            $isFormValid = $form->isValid();
332
333
            // persist if the form was valid and if in preview mode the preview was approved
334
            if ($isFormValid && (!$this->isInPreviewMode() || $this->isPreviewApproved())) {
335
                $submittedObject = $form->getData();
336
                $this->admin->setSubject($submittedObject);
337
338
                try {
339
                    $existingObject = $this->admin->update($submittedObject);
340
341
                    if ($this->isXmlHttpRequest()) {
342
                        return $this->renderJson([
343
                            'result' => 'ok',
344
                            'objectId' => $objectId,
345
                            'objectName' => $this->escapeHtml($this->admin->toString($existingObject)),
346
                        ], 200, []);
347
                    }
348
349
                    $this->addFlash(
350
                        'sonata_flash_success',
351
                        $this->trans(
352
                            'flash_edit_success',
353
                            ['%name%' => $this->escapeHtml($this->admin->toString($existingObject))],
354
                            'SonataAdminBundle'
355
                        )
356
                    );
357
358
                    // redirect to edit mode
359
                    return $this->redirectTo($existingObject);
360
                } catch (ModelManagerException $e) {
361
                    $this->handleModelManagerException($e);
362
363
                    $isFormValid = false;
364
                } catch (LockException $e) {
365
                    $this->addFlash('sonata_flash_error', $this->trans('flash_lock_error', [
366
                        '%name%' => $this->escapeHtml($this->admin->toString($existingObject)),
367
                        '%link_start%' => '<a href="'.$this->admin->generateObjectUrl('edit', $existingObject).'">',
368
                        '%link_end%' => '</a>',
369
                    ], 'SonataAdminBundle'));
370
                }
371
            }
372
373
            // show an error message if the form failed validation
374
            if (!$isFormValid) {
375
                if (!$this->isXmlHttpRequest()) {
376
                    $this->addFlash(
377
                        'sonata_flash_error',
378
                        $this->trans(
379
                            'flash_edit_error',
380
                            ['%name%' => $this->escapeHtml($this->admin->toString($existingObject))],
381
                            'SonataAdminBundle'
382
                        )
383
                    );
384
                }
385
            } elseif ($this->isPreviewRequested()) {
386
                // enable the preview template if the form was valid and preview was requested
387
                $templateKey = 'preview';
388
                $this->admin->getShow();
389
            }
390
        }
391
392
        $formView = $form->createView();
393
        // set the theme for the current Admin Form
394
        $this->setFormTheme($formView, $this->admin->getFormTheme());
0 ignored issues
show
Documentation introduced by
$this->admin->getFormTheme() is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
395
396
        return $this->renderWithExtraParams($this->templateRegistry->getTemplate($templateKey), [
397
            'action' => 'edit',
398
            'form' => $formView,
399
            'object' => $existingObject,
400
            'objectId' => $objectId,
401
        ], null);
402
    }
403
404
    /**
405
     * Batch action.
406
     *
407
     * @throws NotFoundHttpException If the HTTP method is not POST
408
     * @throws \RuntimeException     If the batch action is not defined
409
     *
410
     * @return Response|RedirectResponse
411
     */
412
    public function batchAction()
413
    {
414
        $request = $this->getRequest();
415
        $restMethod = $this->getRestMethod();
416
417
        if ('POST' !== $restMethod) {
418
            throw $this->createNotFoundException(sprintf('Invalid request type "%s", POST expected', $restMethod));
419
        }
420
421
        // check the csrf token
422
        $this->validateCsrfToken('sonata.batch');
423
424
        $confirmation = $request->get('confirmation', false);
425
426
        if ($data = json_decode($request->get('data'), true)) {
427
            $action = $data['action'];
428
            $idx = $data['idx'];
429
            $allElements = $data['all_elements'];
430
            $request->request->replace(array_merge($request->request->all(), $data));
431
        } else {
432
            $request->request->set('idx', $request->get('idx', []));
433
            $request->request->set('all_elements', $request->get('all_elements', false));
434
435
            $action = $request->get('action');
436
            $idx = $request->get('idx');
437
            $allElements = $request->get('all_elements');
438
            $data = $request->request->all();
439
440
            unset($data['_sonata_csrf_token']);
441
        }
442
443
        // NEXT_MAJOR: Remove reflection check.
444
        $reflector = new \ReflectionMethod($this->admin, 'getBatchActions');
445
        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...
446
            @trigger_error('Override Sonata\AdminBundle\Admin\AbstractAdmin::getBatchActions method'
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...
447
                .' is deprecated since version 3.2.'
448
                .' Use Sonata\AdminBundle\Admin\AbstractAdmin::configureBatchActions instead.'
449
                .' The method will be final in 4.0.', E_USER_DEPRECATED
450
            );
451
        }
452
        $batchActions = $this->admin->getBatchActions();
453
        if (!array_key_exists($action, $batchActions)) {
454
            throw new \RuntimeException(sprintf('The `%s` batch action is not defined', $action));
455
        }
456
457
        $camelizedAction = Inflector::classify($action);
458
        $isRelevantAction = sprintf('batchAction%sIsRelevant', $camelizedAction);
459
460
        if (method_exists($this, $isRelevantAction)) {
461
            $nonRelevantMessage = call_user_func([$this, $isRelevantAction], $idx, $allElements, $request);
462
        } else {
463
            $nonRelevantMessage = 0 != count($idx) || $allElements; // at least one item is selected
464
        }
465
466
        if (!$nonRelevantMessage) { // default non relevant message (if false of null)
467
            $nonRelevantMessage = 'flash_batch_empty';
468
        }
469
470
        $datagrid = $this->admin->getDatagrid();
471
        $datagrid->buildPager();
472
473
        if (true !== $nonRelevantMessage) {
474
            $this->addFlash(
475
                'sonata_flash_info',
476
                $this->trans($nonRelevantMessage, [], 'SonataAdminBundle')
477
            );
478
479
            return $this->redirectToList();
480
        }
481
482
        $askConfirmation = isset($batchActions[$action]['ask_confirmation']) ?
483
            $batchActions[$action]['ask_confirmation'] :
484
            true;
485
486
        if ($askConfirmation && 'ok' != $confirmation) {
487
            $actionLabel = $batchActions[$action]['label'];
488
            $batchTranslationDomain = isset($batchActions[$action]['translation_domain']) ?
489
                $batchActions[$action]['translation_domain'] :
490
                $this->admin->getTranslationDomain();
491
492
            $formView = $datagrid->getForm()->createView();
493
            $this->setFormTheme($formView, $this->admin->getFilterTheme());
0 ignored issues
show
Documentation introduced by
$this->admin->getFilterTheme() is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
494
495
            return $this->renderWithExtraParams($this->templateRegistry->getTemplate('batch_confirmation'), [
496
                'action' => 'list',
497
                'action_label' => $actionLabel,
498
                'batch_translation_domain' => $batchTranslationDomain,
499
                'datagrid' => $datagrid,
500
                'form' => $formView,
501
                'data' => $data,
502
                'csrf_token' => $this->getCsrfToken('sonata.batch'),
503
            ], null);
504
        }
505
506
        // execute the action, batchActionXxxxx
507
        $finalAction = sprintf('batchAction%s', $camelizedAction);
508
        if (!method_exists($this, $finalAction)) {
509
            throw new \RuntimeException(sprintf('A `%s::%s` method must be callable', get_class($this), $finalAction));
510
        }
511
512
        $query = $datagrid->getQuery();
513
514
        $query->setFirstResult(null);
515
        $query->setMaxResults(null);
516
517
        $this->admin->preBatchAction($action, $query, $idx, $allElements);
518
519
        if (count($idx) > 0) {
520
            $this->admin->getModelManager()->addIdentifiersToQuery($this->admin->getClass(), $query, $idx);
521
        } elseif (!$allElements) {
522
            $this->addFlash(
523
                'sonata_flash_info',
524
                $this->trans('flash_batch_no_elements_processed', [], 'SonataAdminBundle')
525
            );
526
527
            return $this->redirectToList();
528
        }
529
530
        return call_user_func([$this, $finalAction], $query, $request);
531
    }
532
533
    /**
534
     * Create action.
535
     *
536
     * @throws AccessDeniedException If access is not granted
537
     *
538
     * @return Response
539
     */
540
    public function createAction()
541
    {
542
        $request = $this->getRequest();
543
        // the key used to lookup the template
544
        $templateKey = 'edit';
545
546
        $this->admin->checkAccess('create');
547
548
        $class = new \ReflectionClass($this->admin->hasActiveSubClass() ? $this->admin->getActiveSubClass() : $this->admin->getClass());
549
550
        if ($class->isAbstract()) {
551
            return $this->renderWithExtraParams(
552
                '@SonataAdmin/CRUD/select_subclass.html.twig',
553
                [
554
                    'base_template' => $this->getBaseTemplate(),
555
                    'admin' => $this->admin,
556
                    'action' => 'create',
557
                ],
558
                null
559
            );
560
        }
561
562
        $newObject = $this->admin->getNewInstance();
563
564
        $preResponse = $this->preCreate($request, $newObject);
565
        if (null !== $preResponse) {
566
            return $preResponse;
567
        }
568
569
        $this->admin->setSubject($newObject);
570
571
        /** @var $form \Symfony\Component\Form\Form */
572
        $form = $this->admin->getForm();
573
        $form->setData($newObject);
574
        $form->handleRequest($request);
575
576
        if ($form->isSubmitted()) {
577
            $isFormValid = $form->isValid();
578
579
            // persist if the form was valid and if in preview mode the preview was approved
580
            if ($isFormValid && (!$this->isInPreviewMode() || $this->isPreviewApproved())) {
581
                $submittedObject = $form->getData();
582
                $this->admin->setSubject($submittedObject);
583
                $this->admin->checkAccess('create', $submittedObject);
584
585
                try {
586
                    $newObject = $this->admin->create($submittedObject);
587
588
                    if ($this->isXmlHttpRequest()) {
589
                        return $this->renderJson([
590
                            'result' => 'ok',
591
                            'objectId' => $this->admin->getNormalizedIdentifier($newObject),
592
                        ], 200, []);
593
                    }
594
595
                    $this->addFlash(
596
                        'sonata_flash_success',
597
                        $this->trans(
598
                            'flash_create_success',
599
                            ['%name%' => $this->escapeHtml($this->admin->toString($newObject))],
600
                            'SonataAdminBundle'
601
                        )
602
                    );
603
604
                    // redirect to edit mode
605
                    return $this->redirectTo($newObject);
606
                } catch (ModelManagerException $e) {
607
                    $this->handleModelManagerException($e);
608
609
                    $isFormValid = false;
610
                }
611
            }
612
613
            // show an error message if the form failed validation
614
            if (!$isFormValid) {
615
                if (!$this->isXmlHttpRequest()) {
616
                    $this->addFlash(
617
                        'sonata_flash_error',
618
                        $this->trans(
619
                            'flash_create_error',
620
                            ['%name%' => $this->escapeHtml($this->admin->toString($newObject))],
621
                            'SonataAdminBundle'
622
                        )
623
                    );
624
                }
625
            } elseif ($this->isPreviewRequested()) {
626
                // pick the preview template if the form was valid and preview was requested
627
                $templateKey = 'preview';
628
                $this->admin->getShow();
629
            }
630
        }
631
632
        $formView = $form->createView();
633
        // set the theme for the current Admin Form
634
        $this->setFormTheme($formView, $this->admin->getFormTheme());
0 ignored issues
show
Documentation introduced by
$this->admin->getFormTheme() is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
635
636
        return $this->renderWithExtraParams($this->templateRegistry->getTemplate($templateKey), [
637
            'action' => 'create',
638
            'form' => $formView,
639
            'object' => $newObject,
640
            'objectId' => null,
641
        ], null);
642
    }
643
644
    /**
645
     * Show action.
646
     *
647
     * @param int|string|null $id
648
     *
649
     * @throws NotFoundHttpException If the object does not exist
650
     * @throws AccessDeniedException If access is not granted
651
     *
652
     * @return Response
653
     */
654
    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...
655
    {
656
        $request = $this->getRequest();
657
        $id = $request->get($this->admin->getIdParameter());
658
659
        $object = $this->admin->getObject($id);
660
661
        if (!$object) {
662
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
663
        }
664
665
        $this->checkParentChildAssociation($request, $object);
666
667
        $this->admin->checkAccess('show', $object);
668
669
        $preResponse = $this->preShow($request, $object);
670
        if (null !== $preResponse) {
671
            return $preResponse;
672
        }
673
674
        $this->admin->setSubject($object);
675
676
        return $this->renderWithExtraParams($this->templateRegistry->getTemplate('show'), [
677
            'action' => 'show',
678
            'object' => $object,
679
            'elements' => $this->admin->getShow(),
680
        ], null);
681
    }
682
683
    /**
684
     * Show history revisions for object.
685
     *
686
     * @param int|string|null $id
687
     *
688
     * @throws AccessDeniedException If access is not granted
689
     * @throws NotFoundHttpException If the object does not exist or the audit reader is not available
690
     *
691
     * @return Response
692
     */
693
    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...
694
    {
695
        $request = $this->getRequest();
696
        $id = $request->get($this->admin->getIdParameter());
697
698
        $object = $this->admin->getObject($id);
699
700
        if (!$object) {
701
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
702
        }
703
704
        $this->admin->checkAccess('history', $object);
705
706
        $manager = $this->get('sonata.admin.audit.manager');
707
708
        if (!$manager->hasReader($this->admin->getClass())) {
709
            throw $this->createNotFoundException(
710
                sprintf(
711
                    'unable to find the audit reader for class : %s',
712
                    $this->admin->getClass()
713
                )
714
            );
715
        }
716
717
        $reader = $manager->getReader($this->admin->getClass());
718
719
        $revisions = $reader->findRevisions($this->admin->getClass(), $id);
720
721
        return $this->renderWithExtraParams($this->templateRegistry->getTemplate('history'), [
722
            'action' => 'history',
723
            'object' => $object,
724
            'revisions' => $revisions,
725
            'currentRevision' => $revisions ? current($revisions) : false,
726
        ], null);
727
    }
728
729
    /**
730
     * View history revision of object.
731
     *
732
     * @param int|string|null $id
733
     * @param string|null     $revision
734
     *
735
     * @throws AccessDeniedException If access is not granted
736
     * @throws NotFoundHttpException If the object or revision does not exist or the audit reader is not available
737
     *
738
     * @return Response
739
     */
740
    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...
741
    {
742
        $request = $this->getRequest();
743
        $id = $request->get($this->admin->getIdParameter());
744
745
        $object = $this->admin->getObject($id);
746
747
        if (!$object) {
748
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
749
        }
750
751
        $this->admin->checkAccess('historyViewRevision', $object);
752
753
        $manager = $this->get('sonata.admin.audit.manager');
754
755
        if (!$manager->hasReader($this->admin->getClass())) {
756
            throw $this->createNotFoundException(
757
                sprintf(
758
                    'unable to find the audit reader for class : %s',
759
                    $this->admin->getClass()
760
                )
761
            );
762
        }
763
764
        $reader = $manager->getReader($this->admin->getClass());
765
766
        // retrieve the revisioned object
767
        $object = $reader->find($this->admin->getClass(), $id, $revision);
768
769
        if (!$object) {
770
            throw $this->createNotFoundException(
771
                sprintf(
772
                    'unable to find the targeted object `%s` from the revision `%s` with classname : `%s`',
773
                    $id,
774
                    $revision,
775
                    $this->admin->getClass()
776
                )
777
            );
778
        }
779
780
        $this->admin->setSubject($object);
781
782
        return $this->renderWithExtraParams($this->templateRegistry->getTemplate('show'), [
783
            'action' => 'show',
784
            'object' => $object,
785
            'elements' => $this->admin->getShow(),
786
        ], null);
787
    }
788
789
    /**
790
     * Compare history revisions of object.
791
     *
792
     * @param int|string|null $id
793
     * @param int|string|null $base_revision
794
     * @param int|string|null $compare_revision
795
     *
796
     * @throws AccessDeniedException If access is not granted
797
     * @throws NotFoundHttpException If the object or revision does not exist or the audit reader is not available
798
     *
799
     * @return Response
800
     */
801
    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...
802
    {
803
        $request = $this->getRequest();
804
805
        $this->admin->checkAccess('historyCompareRevisions');
806
807
        $id = $request->get($this->admin->getIdParameter());
808
809
        $object = $this->admin->getObject($id);
810
811
        if (!$object) {
812
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
813
        }
814
815
        $manager = $this->get('sonata.admin.audit.manager');
816
817
        if (!$manager->hasReader($this->admin->getClass())) {
818
            throw $this->createNotFoundException(
819
                sprintf(
820
                    'unable to find the audit reader for class : %s',
821
                    $this->admin->getClass()
822
                )
823
            );
824
        }
825
826
        $reader = $manager->getReader($this->admin->getClass());
827
828
        // retrieve the base revision
829
        $base_object = $reader->find($this->admin->getClass(), $id, $base_revision);
830
        if (!$base_object) {
831
            throw $this->createNotFoundException(
832
                sprintf(
833
                    'unable to find the targeted object `%s` from the revision `%s` with classname : `%s`',
834
                    $id,
835
                    $base_revision,
836
                    $this->admin->getClass()
837
                )
838
            );
839
        }
840
841
        // retrieve the compare revision
842
        $compare_object = $reader->find($this->admin->getClass(), $id, $compare_revision);
843
        if (!$compare_object) {
844
            throw $this->createNotFoundException(
845
                sprintf(
846
                    'unable to find the targeted object `%s` from the revision `%s` with classname : `%s`',
847
                    $id,
848
                    $compare_revision,
849
                    $this->admin->getClass()
850
                )
851
            );
852
        }
853
854
        $this->admin->setSubject($base_object);
855
856
        return $this->renderWithExtraParams($this->templateRegistry->getTemplate('show_compare'), [
857
            'action' => 'show',
858
            'object' => $base_object,
859
            'object_compare' => $compare_object,
860
            'elements' => $this->admin->getShow(),
861
        ], null);
862
    }
863
864
    /**
865
     * Export data to specified format.
866
     *
867
     * @throws AccessDeniedException If access is not granted
868
     * @throws \RuntimeException     If the export format is invalid
869
     *
870
     * @return Response
871
     */
872
    public function exportAction(Request $request)
873
    {
874
        $this->admin->checkAccess('export');
875
876
        $format = $request->get('format');
877
878
        // NEXT_MAJOR: remove the check
879
        if (!$this->has('sonata.admin.admin_exporter')) {
880
            @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...
881
                'Not registering the exporter bundle is deprecated since version 3.14.'
882
                .' You must register it to be able to use the export action in 4.0.',
883
                E_USER_DEPRECATED
884
            );
885
            $allowedExportFormats = (array) $this->admin->getExportFormats();
886
887
            $class = $this->admin->getClass();
888
            $filename = sprintf(
889
                'export_%s_%s.%s',
890
                strtolower(substr($class, strripos($class, '\\') + 1)),
891
                date('Y_m_d_H_i_s', strtotime('now')),
892
                $format
893
            );
894
            $exporter = $this->get('sonata.admin.exporter');
895
        } else {
896
            $adminExporter = $this->get('sonata.admin.admin_exporter');
897
            $allowedExportFormats = $adminExporter->getAvailableFormats($this->admin);
898
            $filename = $adminExporter->getExportFilename($this->admin, $format);
899
            $exporter = $this->get('sonata.exporter.exporter');
900
        }
901
902
        if (!in_array($format, $allowedExportFormats)) {
903
            throw new \RuntimeException(
904
                sprintf(
905
                    'Export in format `%s` is not allowed for class: `%s`. Allowed formats are: `%s`',
906
                    $format,
907
                    $this->admin->getClass(),
908
                    implode(', ', $allowedExportFormats)
909
                )
910
            );
911
        }
912
913
        return $exporter->getResponse(
914
            $format,
915
            $filename,
916
            $this->admin->getDataSourceIterator()
917
        );
918
    }
919
920
    /**
921
     * Returns the Response object associated to the acl action.
922
     *
923
     * @param int|string|null $id
924
     *
925
     * @throws AccessDeniedException If access is not granted
926
     * @throws NotFoundHttpException If the object does not exist or the ACL is not enabled
927
     *
928
     * @return Response|RedirectResponse
929
     */
930
    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...
931
    {
932
        $request = $this->getRequest();
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
940
        $object = $this->admin->getObject($id);
941
942
        if (!$object) {
943
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
944
        }
945
946
        $this->admin->checkAccess('acl', $object);
947
948
        $this->admin->setSubject($object);
949
        $aclUsers = $this->getAclUsers();
950
        $aclRoles = $this->getAclRoles();
951
952
        $adminObjectAclManipulator = $this->get('sonata.admin.object.manipulator.acl.admin');
953
        $adminObjectAclData = new AdminObjectAclData(
954
            $this->admin,
955
            $object,
956
            $aclUsers,
957
            $adminObjectAclManipulator->getMaskBuilderClass(),
958
            $aclRoles
959
        );
960
961
        $aclUsersForm = $adminObjectAclManipulator->createAclUsersForm($adminObjectAclData);
962
        $aclRolesForm = $adminObjectAclManipulator->createAclRolesForm($adminObjectAclData);
963
964
        if ('POST' === $request->getMethod()) {
965
            if ($request->request->has(AdminObjectAclManipulator::ACL_USERS_FORM_NAME)) {
966
                $form = $aclUsersForm;
967
                $updateMethod = 'updateAclUsers';
968
            } elseif ($request->request->has(AdminObjectAclManipulator::ACL_ROLES_FORM_NAME)) {
969
                $form = $aclRolesForm;
970
                $updateMethod = 'updateAclRoles';
971
            }
972
973
            if (isset($form)) {
974
                $form->handleRequest($request);
975
976
                if ($form->isValid()) {
977
                    $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...
978
                    $this->addFlash(
979
                        'sonata_flash_success',
980
                        $this->trans('flash_acl_edit_success', [], 'SonataAdminBundle')
981
                    );
982
983
                    return new RedirectResponse($this->admin->generateObjectUrl('acl', $object));
984
                }
985
            }
986
        }
987
988
        return $this->renderWithExtraParams($this->templateRegistry->getTemplate('acl'), [
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
        ], null);
997
    }
998
999
    /**
1000
     * @return Request
1001
     */
1002
    public function getRequest()
1003
    {
1004
        return $this->container->get('request_stack')->getCurrentRequest();
1005
    }
1006
1007
    /**
1008
     * Gets a container configuration parameter by its name.
1009
     *
1010
     * @param string $name The parameter name
1011
     *
1012
     * @return mixed
1013
     */
1014
    protected function getParameter($name)
1015
    {
1016
        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: Container14\ProjectServiceContainer, ProjectServiceContainer, Symfony\Component\Depend...urationContainerBuilder, Symfony\Component\DependencyInjection\Container, Symfony\Component\Depend...ection\ContainerBuilder, Symfony\Component\Depend...tainers\CustomContainer, Symfony\Component\Depend...ProjectServiceContainer, Symfony\Component\Depend...ProjectServiceContainer, Symfony_DI_PhpDumper_Test_Almost_Circular_Private, Symfony_DI_PhpDumper_Test_Almost_Circular_Public, Symfony_DI_PhpDumper_Test_Base64Parameters, Symfony_DI_PhpDumper_Test_EnvParameters, Symfony_DI_PhpDumper_Test_Legacy_Privates, Symfony_DI_PhpDumper_Test_Rot13Parameters, Symfony_DI_PhpDumper_Test_Uninitialized_Reference.

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...
1017
    }
1018
1019
    /**
1020
     * Render JSON.
1021
     *
1022
     * @param mixed $data
1023
     * @param int   $status
1024
     * @param array $headers
1025
     *
1026
     * @return Response with json encoded data
1027
     */
1028
    protected function renderJson($data, $status = 200, $headers = [])
1029
    {
1030
        return new JsonResponse($data, $status, $headers);
1031
    }
1032
1033
    /**
1034
     * Returns true if the request is a XMLHttpRequest.
1035
     *
1036
     * @return bool True if the request is an XMLHttpRequest, false otherwise
1037
     */
1038
    protected function isXmlHttpRequest()
1039
    {
1040
        $request = $this->getRequest();
1041
1042
        return $request->isXmlHttpRequest() || $request->get('_xml_http_request');
1043
    }
1044
1045
    /**
1046
     * Returns the correct RESTful verb, given either by the request itself or
1047
     * via the "_method" parameter.
1048
     *
1049
     * @return string HTTP method, either
1050
     */
1051
    protected function getRestMethod()
1052
    {
1053
        $request = $this->getRequest();
1054
1055
        if (Request::getHttpMethodParameterOverride() || !$request->request->has('_method')) {
1056
            return $request->getMethod();
1057
        }
1058
1059
        return $request->request->get('_method');
1060
    }
1061
1062
    /**
1063
     * Contextualize the admin class depends on the current request.
1064
     *
1065
     * @throws \RuntimeException
1066
     */
1067
    protected function configure()
1068
    {
1069
        $request = $this->getRequest();
1070
1071
        $adminCode = $request->get('_sonata_admin');
1072
1073
        if (!$adminCode) {
1074
            throw new \RuntimeException(sprintf(
1075
                'There is no `_sonata_admin` defined for the controller `%s` and the current route `%s`',
1076
                get_class($this),
1077
                $request->get('_route')
1078
            ));
1079
        }
1080
1081
        $this->admin = $this->container->get('sonata.admin.pool')->getAdminByAdminCode($adminCode);
1082
1083
        if (!$this->admin) {
1084
            throw new \RuntimeException(sprintf(
1085
                'Unable to find the admin class related to the current controller (%s)',
1086
                get_class($this)
1087
            ));
1088
        }
1089
1090
        $this->templateRegistry = $this->container->get($this->admin->getCode().'.template_registry');
1091
        if (!$this->templateRegistry instanceof TemplateRegistryInterface) {
1092
            throw new \RuntimeException(sprintf(
1093
                'Unable to find the template registry related to the current admin (%s)',
1094
                $this->admin->getCode()
1095
            ));
1096
        }
1097
1098
        $rootAdmin = $this->admin;
1099
1100
        while ($rootAdmin->isChild()) {
1101
            $rootAdmin->setCurrentChild(true);
1102
            $rootAdmin = $rootAdmin->getParent();
1103
        }
1104
1105
        $rootAdmin->setRequest($request);
1106
1107
        if ($request->get('uniqid')) {
1108
            $this->admin->setUniqid($request->get('uniqid'));
1109
        }
1110
    }
1111
1112
    /**
1113
     * Proxy for the logger service of the container.
1114
     * If no such service is found, a NullLogger is returned.
1115
     *
1116
     * @return LoggerInterface
1117
     */
1118
    protected function getLogger()
1119
    {
1120
        if ($this->container->has('logger')) {
1121
            return $this->container->get('logger');
1122
        }
1123
1124
        return new NullLogger();
1125
    }
1126
1127
    /**
1128
     * Returns the base template name.
1129
     *
1130
     * @return string The template name
1131
     */
1132
    protected function getBaseTemplate()
1133
    {
1134
        if ($this->isXmlHttpRequest()) {
1135
            return $this->templateRegistry->getTemplate('ajax');
1136
        }
1137
1138
        return $this->templateRegistry->getTemplate('layout');
1139
    }
1140
1141
    /**
1142
     * @throws \Exception
1143
     */
1144
    protected function handleModelManagerException(\Exception $e)
1145
    {
1146
        if ($this->get('kernel')->isDebug()) {
1147
            throw $e;
1148
        }
1149
1150
        $context = ['exception' => $e];
1151
        if ($e->getPrevious()) {
1152
            $context['previous_exception_message'] = $e->getPrevious()->getMessage();
1153
        }
1154
        $this->getLogger()->error($e->getMessage(), $context);
1155
    }
1156
1157
    /**
1158
     * Redirect the user depend on this choice.
1159
     *
1160
     * @param object $object
1161
     *
1162
     * @return RedirectResponse
1163
     */
1164
    protected function redirectTo($object)
1165
    {
1166
        $request = $this->getRequest();
1167
1168
        $url = false;
1169
1170
        if (null !== $request->get('btn_update_and_list')) {
1171
            return $this->redirectToList();
1172
        }
1173
        if (null !== $request->get('btn_create_and_list')) {
1174
            return $this->redirectToList();
1175
        }
1176
1177
        if (null !== $request->get('btn_create_and_create')) {
1178
            $params = [];
1179
            if ($this->admin->hasActiveSubClass()) {
1180
                $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...
1181
            }
1182
            $url = $this->admin->generateUrl('create', $params);
1183
        }
1184
1185
        if ('DELETE' === $this->getRestMethod()) {
1186
            return $this->redirectToList();
1187
        }
1188
1189
        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...
1190
            foreach (['edit', 'show'] as $route) {
1191
                if ($this->admin->hasRoute($route) && $this->admin->hasAccess($route, $object)) {
1192
                    $url = $this->admin->generateObjectUrl($route, $object);
1193
1194
                    break;
1195
                }
1196
            }
1197
        }
1198
1199
        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...
1200
            return $this->redirectToList();
1201
        }
1202
1203
        return new RedirectResponse($url);
1204
    }
1205
1206
    /**
1207
     * Redirects the user to the list view.
1208
     *
1209
     * @return RedirectResponse
1210
     */
1211
    final protected function redirectToList()
1212
    {
1213
        $parameters = [];
1214
1215
        if ($filter = $this->admin->getFilterParameters()) {
1216
            $parameters['filter'] = $filter;
1217
        }
1218
1219
        return $this->redirect($this->admin->generateUrl('list', $parameters));
1220
    }
1221
1222
    /**
1223
     * Returns true if the preview is requested to be shown.
1224
     *
1225
     * @return bool
1226
     */
1227
    protected function isPreviewRequested()
1228
    {
1229
        $request = $this->getRequest();
1230
1231
        return null !== $request->get('btn_preview');
1232
    }
1233
1234
    /**
1235
     * Returns true if the preview has been approved.
1236
     *
1237
     * @return bool
1238
     */
1239
    protected function isPreviewApproved()
1240
    {
1241
        $request = $this->getRequest();
1242
1243
        return null !== $request->get('btn_preview_approve');
1244
    }
1245
1246
    /**
1247
     * Returns true if the request is in the preview workflow.
1248
     *
1249
     * That means either a preview is requested or the preview has already been shown
1250
     * and it got approved/declined.
1251
     *
1252
     * @return bool
1253
     */
1254
    protected function isInPreviewMode()
1255
    {
1256
        return $this->admin->supportsPreviewMode()
1257
        && ($this->isPreviewRequested()
1258
            || $this->isPreviewApproved()
1259
            || $this->isPreviewDeclined());
1260
    }
1261
1262
    /**
1263
     * Returns true if the preview has been declined.
1264
     *
1265
     * @return bool
1266
     */
1267
    protected function isPreviewDeclined()
1268
    {
1269
        $request = $this->getRequest();
1270
1271
        return null !== $request->get('btn_preview_decline');
1272
    }
1273
1274
    /**
1275
     * Gets ACL users.
1276
     *
1277
     * @return \Traversable
1278
     */
1279
    protected function getAclUsers()
1280
    {
1281
        $aclUsers = [];
1282
1283
        $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: Container14\ProjectServiceContainer, ProjectServiceContainer, Symfony\Component\Depend...urationContainerBuilder, Symfony\Component\DependencyInjection\Container, Symfony\Component\Depend...ection\ContainerBuilder, Symfony\Component\Depend...tainers\CustomContainer, Symfony\Component\Depend...ProjectServiceContainer, Symfony\Component\Depend...ProjectServiceContainer, Symfony_DI_PhpDumper_Test_Almost_Circular_Private, Symfony_DI_PhpDumper_Test_Almost_Circular_Public, Symfony_DI_PhpDumper_Test_Base64Parameters, Symfony_DI_PhpDumper_Test_EnvParameters, Symfony_DI_PhpDumper_Test_Legacy_Privates, Symfony_DI_PhpDumper_Test_Rot13Parameters, Symfony_DI_PhpDumper_Test_Uninitialized_Reference.

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...
1284
        if (null !== $userManagerServiceName && $this->has($userManagerServiceName)) {
1285
            $userManager = $this->get($userManagerServiceName);
1286
1287
            if (method_exists($userManager, 'findUsers')) {
1288
                $aclUsers = $userManager->findUsers();
1289
            }
1290
        }
1291
1292
        return is_array($aclUsers) ? new \ArrayIterator($aclUsers) : $aclUsers;
1293
    }
1294
1295
    /**
1296
     * Gets ACL roles.
1297
     *
1298
     * @return \Traversable
1299
     */
1300
    protected function getAclRoles()
1301
    {
1302
        $aclRoles = [];
1303
        $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: Container14\ProjectServiceContainer, ProjectServiceContainer, Symfony\Component\Depend...urationContainerBuilder, Symfony\Component\DependencyInjection\Container, Symfony\Component\Depend...ection\ContainerBuilder, Symfony\Component\Depend...tainers\CustomContainer, Symfony\Component\Depend...ProjectServiceContainer, Symfony\Component\Depend...ProjectServiceContainer, Symfony_DI_PhpDumper_Test_Almost_Circular_Private, Symfony_DI_PhpDumper_Test_Almost_Circular_Public, Symfony_DI_PhpDumper_Test_Base64Parameters, Symfony_DI_PhpDumper_Test_EnvParameters, Symfony_DI_PhpDumper_Test_Legacy_Privates, Symfony_DI_PhpDumper_Test_Rot13Parameters, Symfony_DI_PhpDumper_Test_Uninitialized_Reference.

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...
1304
        $pool = $this->container->get('sonata.admin.pool');
1305
1306
        foreach ($pool->getAdminServiceIds() as $id) {
1307
            try {
1308
                $admin = $pool->getInstance($id);
1309
            } catch (\Exception $e) {
1310
                continue;
1311
            }
1312
1313
            $baseRole = $admin->getSecurityHandler()->getBaseRole($admin);
1314
            foreach ($admin->getSecurityInformation() as $role => $permissions) {
1315
                $role = sprintf($baseRole, $role);
1316
                $aclRoles[] = $role;
1317
            }
1318
        }
1319
1320
        foreach ($roleHierarchy as $name => $roles) {
1321
            $aclRoles[] = $name;
1322
            $aclRoles = array_merge($aclRoles, $roles);
1323
        }
1324
1325
        $aclRoles = array_unique($aclRoles);
1326
1327
        return is_array($aclRoles) ? new \ArrayIterator($aclRoles) : $aclRoles;
1328
    }
1329
1330
    /**
1331
     * Validate CSRF token for action without form.
1332
     *
1333
     * @param string $intention
1334
     *
1335
     * @throws HttpException
1336
     */
1337
    protected function validateCsrfToken($intention)
1338
    {
1339
        $request = $this->getRequest();
1340
        $token = $request->request->get('_sonata_csrf_token', false);
1341
1342
        if ($this->container->has('security.csrf.token_manager')) {
1343
            $valid = $this->container->get('security.csrf.token_manager')->isTokenValid(new CsrfToken($intention, $token));
1344
        } else {
1345
            return;
1346
        }
1347
1348
        if (!$valid) {
1349
            throw new HttpException(400, 'The csrf token is not valid, CSRF attack?');
1350
        }
1351
    }
1352
1353
    /**
1354
     * Escape string for html output.
1355
     *
1356
     * @param string $s
1357
     *
1358
     * @return string
1359
     */
1360
    protected function escapeHtml($s)
1361
    {
1362
        return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
1363
    }
1364
1365
    /**
1366
     * Get CSRF token.
1367
     *
1368
     * @param string $intention
1369
     *
1370
     * @return string|false
1371
     */
1372
    protected function getCsrfToken($intention)
1373
    {
1374
        if ($this->container->has('security.csrf.token_manager')) {
1375
            return $this->container->get('security.csrf.token_manager')->getToken($intention)->getValue();
1376
        }
1377
1378
        return false;
1379
    }
1380
1381
    /**
1382
     * This method can be overloaded in your custom CRUD controller.
1383
     * It's called from createAction.
1384
     *
1385
     * @param mixed $object
1386
     *
1387
     * @return Response|null
1388
     */
1389
    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...
1390
    {
1391
    }
1392
1393
    /**
1394
     * This method can be overloaded in your custom CRUD controller.
1395
     * It's called from editAction.
1396
     *
1397
     * @param mixed $object
1398
     *
1399
     * @return Response|null
1400
     */
1401
    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...
1402
    {
1403
    }
1404
1405
    /**
1406
     * This method can be overloaded in your custom CRUD controller.
1407
     * It's called from deleteAction.
1408
     *
1409
     * @param mixed $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
    }
1416
1417
    /**
1418
     * This method can be overloaded in your custom CRUD controller.
1419
     * It's called from showAction.
1420
     *
1421
     * @param mixed $object
1422
     *
1423
     * @return Response|null
1424
     */
1425
    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...
1426
    {
1427
    }
1428
1429
    /**
1430
     * This method can be overloaded in your custom CRUD controller.
1431
     * It's called from listAction.
1432
     *
1433
     * @return Response|null
1434
     */
1435
    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...
1436
    {
1437
    }
1438
1439
    /**
1440
     * Translate a message id.
1441
     *
1442
     * @param string $id
1443
     * @param string $domain
1444
     * @param string $locale
1445
     *
1446
     * @return string translated string
1447
     */
1448
    final protected function trans($id, array $parameters = [], $domain = null, $locale = null)
1449
    {
1450
        $domain = $domain ?: $this->admin->getTranslationDomain();
1451
1452
        return $this->get('translator')->trans($id, $parameters, $domain, $locale);
1453
    }
1454
1455
    private function checkParentChildAssociation(Request $request, $object)
1456
    {
1457
        if (!($parentAdmin = $this->admin->getParent())) {
1458
            return;
1459
        }
1460
1461
        // NEXT_MAJOR: remove this check
1462
        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...
1463
            return;
1464
        }
1465
1466
        $parentId = $request->get($parentAdmin->getIdParameter());
1467
1468
        $propertyAccessor = PropertyAccess::createPropertyAccessor();
1469
        $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...
1470
1471
        if ($parentAdmin->getObject($parentId) !== $propertyAccessor->getValue($object, $propertyPath)) {
1472
            // NEXT_MAJOR: make this exception
1473
            @trigger_error("Accessing a child that isn't connected to a given parent is deprecated since 3.x"
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...
1474
                ." and won't be allowed in 4.0.",
1475
                E_USER_DEPRECATED
1476
            );
1477
        }
1478
    }
1479
1480
    /**
1481
     * Sets the admin form theme to form view. Used for compatibility between Symfony versions.
1482
     *
1483
     * @param string $theme
1484
     */
1485
    private function setFormTheme(FormView $formView, $theme)
1486
    {
1487
        $twig = $this->get('twig');
1488
1489
        // BC for Symfony < 3.2 where this runtime does not exists
1490
        if (!method_exists(AppVariable::class, 'getToken')) {
1491
            $twig->getExtension(FormExtension::class)->renderer->setTheme($formView, $theme);
1492
1493
            return;
1494
        }
1495
1496
        // BC for Symfony < 3.4 where runtime should be TwigRenderer
1497
        if (!method_exists(DebugCommand::class, 'getLoaderPaths')) {
1498
            $twig->getRuntime(TwigRenderer::class)->setTheme($formView, $theme);
1499
1500
            return;
1501
        }
1502
1503
        $twig->getRuntime(FormRenderer::class)->setTheme($formView, $theme);
1504
    }
1505
}
1506