Issues (655)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/Controller/CRUDController.php (5 issues)

Upgrade to new PHP Analysis Engine

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

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the Sonata Project package.
7
 *
8
 * (c) Thomas Rabaix <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Sonata\AdminBundle\Controller;
15
16
use Doctrine\Inflector\InflectorFactory;
17
use Psr\Log\LoggerInterface;
18
use Psr\Log\NullLogger;
19
use Sonata\AdminBundle\Admin\AdminInterface;
20
use Sonata\AdminBundle\Admin\FieldDescriptionCollection;
21
use Sonata\AdminBundle\Datagrid\ProxyQueryInterface;
22
use Sonata\AdminBundle\Exception\LockException;
23
use Sonata\AdminBundle\Exception\ModelManagerException;
24
use Sonata\AdminBundle\Templating\TemplateRegistryInterface;
25
use Sonata\AdminBundle\Util\AdminObjectAclData;
26
use Sonata\AdminBundle\Util\AdminObjectAclManipulator;
27
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
28
use Symfony\Component\DependencyInjection\ContainerInterface;
29
use Symfony\Component\Form\FormInterface;
30
use Symfony\Component\Form\FormRenderer;
31
use Symfony\Component\Form\FormView;
32
use Symfony\Component\HttpFoundation\JsonResponse;
33
use Symfony\Component\HttpFoundation\RedirectResponse;
34
use Symfony\Component\HttpFoundation\Request;
35
use Symfony\Component\HttpFoundation\Response;
36
use Symfony\Component\HttpKernel\Exception\HttpException;
37
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
38
use Symfony\Component\PropertyAccess\PropertyAccess;
39
use Symfony\Component\PropertyAccess\PropertyPath;
40
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
41
use Symfony\Component\Security\Csrf\CsrfToken;
42
43
/**
44
 * @author Thomas Rabaix <[email protected]>
45
 */
46
class CRUDController extends Controller
47
{
48
    /**
49
     * @var ContainerInterface
50
     */
51
    protected $container;
52
53
    /**
54
     * The related Admin class.
55
     *
56
     * @var AdminInterface
57
     */
58
    protected $admin;
59
60
    /**
61
     * The template registry of the related Admin class.
62
     *
63
     * @var TemplateRegistryInterface
64
     */
65
    private $templateRegistry;
66
67
    public function setContainer(?ContainerInterface $container = null): void
68
    {
69
        $this->container = $container;
70
71
        $this->configure();
72
    }
73
74
    /**
75
     * Renders a view while passing mandatory parameters on to the template.
76
     *
77
     * @param string               $view       The view name
78
     * @param array<string, mixed> $parameters An array of parameters to pass to the view
79
     *
80
     * @return Response A Response instance
81
     */
82
    public function renderWithExtraParams($view, array $parameters = [], ?Response $response = null)
83
    {
84
        return $this->render($view, $this->addRenderExtraParams($parameters), $response);
85
    }
86
87
    /**
88
     * List action.
89
     *
90
     * @throws AccessDeniedException If access is not granted
91
     *
92
     * @return Response
93
     */
94
    public function listAction(Request $request)
95
    {
96
        $this->admin->checkAccess('list');
97
98
        $preResponse = $this->preList($request);
99
        if (null !== $preResponse) {
100
            return $preResponse;
101
        }
102
103
        if ($listMode = $request->get('_list_mode')) {
104
            $this->admin->setListMode($listMode);
105
        }
106
107
        $datagrid = $this->admin->getDatagrid();
108
        $formView = $datagrid->getForm()->createView();
109
110
        // set the theme for the current Admin Form
111
        $this->setFormTheme($formView, $this->admin->getFilterTheme());
112
113
        $template = $this->templateRegistry->getTemplate('list');
114
115
        return $this->renderWithExtraParams($template, [
116
            'action' => 'list',
117
            'form' => $formView,
118
            'datagrid' => $datagrid,
119
            'csrf_token' => $this->getCsrfToken('sonata.batch'),
120
            'export_formats' => $this->has('sonata.admin.admin_exporter') ?
121
                $this->get('sonata.admin.admin_exporter')->getAvailableFormats($this->admin) :
122
                $this->admin->getExportFormats(),
123
        ], null);
124
    }
125
126
    /**
127
     * Execute a batch delete.
128
     *
129
     * @throws AccessDeniedException If access is not granted
130
     *
131
     * @return RedirectResponse
132
     */
133
    public function batchActionDelete(ProxyQueryInterface $query)
134
    {
135
        $this->admin->checkAccess('batchDelete');
136
137
        $modelManager = $this->admin->getModelManager();
138
139
        try {
140
            $modelManager->batchDelete($this->admin->getClass(), $query);
141
            $this->addFlash(
142
                'sonata_flash_success',
143
                $this->trans('flash_batch_delete_success', [], 'SonataAdminBundle')
144
            );
145
        } catch (ModelManagerException $e) {
146
            $this->handleModelManagerException($e);
147
            $this->addFlash(
148
                'sonata_flash_error',
149
                $this->trans('flash_batch_delete_error', [], 'SonataAdminBundle')
150
            );
151
        }
152
153
        return $this->redirectToList();
154
    }
155
156
    /**
157
     * Delete action.
158
     *
159
     * @throws NotFoundHttpException If the object does not exist
160
     * @throws AccessDeniedException If access is not granted
161
     *
162
     * @return Response|RedirectResponse
163
     */
164
    public function deleteAction(Request $request)
165
    {
166
        $id = $request->get($this->admin->getIdParameter());
167
        $object = $this->admin->getObject($id);
168
169
        if (!$object) {
170
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
171
        }
172
173
        $this->checkParentChildAssociation($request, $object);
174
175
        $this->admin->checkAccess('delete', $object);
176
177
        $preResponse = $this->preDelete($request, $object);
178
        if (null !== $preResponse) {
179
            return $preResponse;
180
        }
181
182
        if (Request::METHOD_DELETE === $this->getRestMethod()) {
183
            // check the csrf token
184
            $this->validateCsrfToken('sonata.delete');
185
186
            $objectName = $this->admin->toString($object);
187
188
            try {
189
                $this->admin->delete($object);
190
191
                if ($this->isXmlHttpRequest()) {
192
                    return $this->renderJson(['result' => 'ok'], Response::HTTP_OK, []);
193
                }
194
195
                $this->addFlash(
196
                    'sonata_flash_success',
197
                    $this->trans(
198
                        'flash_delete_success',
199
                        ['%name%' => $this->escapeHtml($objectName)],
200
                        'SonataAdminBundle'
201
                    )
202
                );
203
            } catch (ModelManagerException $e) {
204
                $this->handleModelManagerException($e);
205
206
                if ($this->isXmlHttpRequest()) {
207
                    return $this->renderJson(['result' => 'error'], Response::HTTP_OK, []);
208
                }
209
210
                $this->addFlash(
211
                    'sonata_flash_error',
212
                    $this->trans(
213
                        'flash_delete_error',
214
                        ['%name%' => $this->escapeHtml($objectName)],
215
                        'SonataAdminBundle'
216
                    )
217
                );
218
            }
219
220
            return $this->redirectTo($object);
221
        }
222
223
        $template = $this->templateRegistry->getTemplate('delete');
224
225
        return $this->renderWithExtraParams($template, [
226
            'object' => $object,
227
            'action' => 'delete',
228
            'csrf_token' => $this->getCsrfToken('sonata.delete'),
229
        ], null);
230
    }
231
232
    /**
233
     * Edit action.
234
     *
235
     * @throws NotFoundHttpException If the object does not exist
236
     * @throws AccessDeniedException If access is not granted
237
     *
238
     * @return Response|RedirectResponse
239
     */
240
    public function editAction(Request $request)
241
    {
242
        // the key used to lookup the template
243
        $templateKey = 'edit';
244
245
        $id = $request->get($this->admin->getIdParameter());
246
        $existingObject = $this->admin->getObject($id);
247
248
        if (!$existingObject) {
249
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
250
        }
251
252
        $this->checkParentChildAssociation($request, $existingObject);
253
254
        $this->admin->checkAccess('edit', $existingObject);
255
256
        $preResponse = $this->preEdit($request, $existingObject);
257
        if (null !== $preResponse) {
258
            return $preResponse;
259
        }
260
261
        $this->admin->setSubject($existingObject);
262
        $objectId = $this->admin->getNormalizedIdentifier($existingObject);
263
264
        $form = $this->admin->getForm();
265
266
        $form->setData($existingObject);
267
        $form->handleRequest($request);
268
269
        if ($form->isSubmitted()) {
270
            $isFormValid = $form->isValid();
271
272
            // persist if the form was valid and if in preview mode the preview was approved
273
            if ($isFormValid && (!$this->isInPreviewMode() || $this->isPreviewApproved())) {
274
                $submittedObject = $form->getData();
275
                $this->admin->setSubject($submittedObject);
276
277
                try {
278
                    $existingObject = $this->admin->update($submittedObject);
279
280
                    if ($this->isXmlHttpRequest()) {
281
                        return $this->handleXmlHttpRequestSuccessResponse($request, $existingObject);
282
                    }
283
284
                    $this->addFlash(
285
                        'sonata_flash_success',
286
                        $this->trans(
287
                            'flash_edit_success',
288
                            ['%name%' => $this->escapeHtml($this->admin->toString($existingObject))],
289
                            'SonataAdminBundle'
290
                        )
291
                    );
292
293
                    // redirect to edit mode
294
                    return $this->redirectTo($existingObject);
295
                } catch (ModelManagerException $e) {
296
                    $this->handleModelManagerException($e);
297
298
                    $isFormValid = false;
299
                } catch (LockException $e) {
300
                    $this->addFlash('sonata_flash_error', $this->trans('flash_lock_error', [
301
                        '%name%' => $this->escapeHtml($this->admin->toString($existingObject)),
302
                        '%link_start%' => sprintf('<a href="%s">', $this->admin->generateObjectUrl('edit', $existingObject)),
303
                        '%link_end%' => '</a>',
304
                    ], 'SonataAdminBundle'));
305
                }
306
            }
307
308
            // show an error message if the form failed validation
309
            if (!$isFormValid) {
310
                if ($this->isXmlHttpRequest() && null !== ($response = $this->handleXmlHttpRequestErrorResponse($request, $form))) {
311
                    return $response;
312
                }
313
314
                $this->addFlash(
315
                    'sonata_flash_error',
316
                    $this->trans(
317
                        'flash_edit_error',
318
                        ['%name%' => $this->escapeHtml($this->admin->toString($existingObject))],
319
                        'SonataAdminBundle'
320
                    )
321
                );
322
            } elseif ($this->isPreviewRequested()) {
323
                // enable the preview template if the form was valid and preview was requested
324
                $templateKey = 'preview';
325
                $this->admin->getShow();
326
            }
327
        }
328
329
        $formView = $form->createView();
330
        // set the theme for the current Admin Form
331
        $this->setFormTheme($formView, $this->admin->getFormTheme());
332
333
        $template = $this->templateRegistry->getTemplate($templateKey);
334
335
        return $this->renderWithExtraParams($template, [
336
            'action' => 'edit',
337
            'form' => $formView,
338
            'object' => $existingObject,
339
            'objectId' => $objectId,
340
        ], null);
341
    }
342
343
    /**
344
     * Batch action.
345
     *
346
     * @throws NotFoundHttpException If the HTTP method is not POST
347
     * @throws \RuntimeException     If the batch action is not defined
348
     *
349
     * @return Response|RedirectResponse
350
     */
351
    public function batchAction(Request $request)
352
    {
353
        $restMethod = $this->getRestMethod();
354
355
        if (Request::METHOD_POST !== $restMethod) {
356
            throw $this->createNotFoundException(sprintf(
357
                'Invalid request method given "%s", %s expected',
358
                $restMethod,
359
                Request::METHOD_POST
360
            ));
361
        }
362
363
        // check the csrf token
364
        $this->validateCsrfToken('sonata.batch');
365
366
        $confirmation = $request->get('confirmation', false);
367
368
        if ($data = json_decode($request->get('data', ''), true)) {
369
            $action = $data['action'];
370
            $idx = $data['idx'];
371
            $allElements = (bool) $data['all_elements'];
372
            $request->request->replace(array_merge($request->request->all(), $data));
373
        } else {
374
            $action = $request->request->getAlnum('action');
375
            $idx = $request->request->get('idx', []);
376
            $allElements = $request->request->getBoolean('all_elements');
377
            $data = $request->request->all();
378
379
            unset($data['_sonata_csrf_token']);
380
        }
381
382
        $batchActions = $this->admin->getBatchActions();
383
        if (!\array_key_exists($action, $batchActions)) {
384
            throw new \RuntimeException(sprintf('The `%s` batch action is not defined', $action));
385
        }
386
387
        $camelizedAction = InflectorFactory::create()->build()->classify($action);
388
        $isRelevantAction = sprintf('batchAction%sIsRelevant', $camelizedAction);
389
390
        if (method_exists($this, $isRelevantAction)) {
391
            $nonRelevantMessage = $this->$isRelevantAction($idx, $allElements, $request);
392
        } else {
393
            $nonRelevantMessage = 0 !== \count($idx) || $allElements; // at least one item is selected
394
        }
395
396
        if (!$nonRelevantMessage) { // default non relevant message (if false of null)
397
            $nonRelevantMessage = 'flash_batch_empty';
398
        }
399
400
        $datagrid = $this->admin->getDatagrid();
401
        $datagrid->buildPager();
402
403
        if (true !== $nonRelevantMessage) {
404
            $this->addFlash(
405
                'sonata_flash_info',
406
                $this->trans($nonRelevantMessage, [], 'SonataAdminBundle')
407
            );
408
409
            return $this->redirectToList();
410
        }
411
412
        $askConfirmation = $batchActions[$action]['ask_confirmation'] ??
413
            true;
414
415
        if ($askConfirmation && 'ok' !== $confirmation) {
416
            $actionLabel = $batchActions[$action]['label'];
417
            $batchTranslationDomain = $batchActions[$action]['translation_domain'] ??
418
                $this->admin->getTranslationDomain();
419
420
            $formView = $datagrid->getForm()->createView();
421
            $this->setFormTheme($formView, $this->admin->getFilterTheme());
422
423
            $template = !empty($batchActions[$action]['template']) ?
424
                $batchActions[$action]['template'] :
425
                $this->templateRegistry->getTemplate('batch_confirmation');
426
427
            return $this->renderWithExtraParams($template, [
428
                'action' => 'list',
429
                'action_label' => $actionLabel,
430
                'batch_translation_domain' => $batchTranslationDomain,
431
                'datagrid' => $datagrid,
432
                'form' => $formView,
433
                'data' => $data,
434
                'csrf_token' => $this->getCsrfToken('sonata.batch'),
435
            ], null);
436
        }
437
438
        // execute the action, batchActionXxxxx
439
        $finalAction = sprintf('batchAction%s', $camelizedAction);
440
        if (!method_exists($this, $finalAction)) {
441
            throw new \RuntimeException(sprintf('A `%s::%s` method must be callable', static::class, $finalAction));
442
        }
443
444
        $query = $datagrid->getQuery();
445
446
        $query->setFirstResult(null);
447
        $query->setMaxResults(null);
448
449
        $this->admin->preBatchAction($action, $query, $idx, $allElements);
450
451
        if (\count($idx) > 0) {
452
            $this->admin->getModelManager()->addIdentifiersToQuery($this->admin->getClass(), $query, $idx);
453
        } elseif (!$allElements) {
454
            $this->addFlash(
455
                'sonata_flash_info',
456
                $this->trans('flash_batch_no_elements_processed', [], 'SonataAdminBundle')
457
            );
458
459
            return $this->redirectToList();
460
        }
461
462
        return $this->$finalAction($query, $request);
463
    }
464
465
    /**
466
     * Create action.
467
     *
468
     * @throws AccessDeniedException If access is not granted
469
     *
470
     * @return Response
471
     */
472
    public function createAction(Request $request)
473
    {
474
        // the key used to lookup the template
475
        $templateKey = 'edit';
476
477
        $this->admin->checkAccess('create');
478
479
        $class = new \ReflectionClass($this->admin->hasActiveSubClass() ? $this->admin->getActiveSubClass() : $this->admin->getClass());
480
481
        if ($class->isAbstract()) {
482
            return $this->renderWithExtraParams(
483
                '@SonataAdmin/CRUD/select_subclass.html.twig',
484
                [
485
                    'base_template' => $this->getBaseTemplate(),
486
                    'admin' => $this->admin,
487
                    'action' => 'create',
488
                ],
489
                null
490
            );
491
        }
492
493
        $newObject = $this->admin->getNewInstance();
494
495
        $preResponse = $this->preCreate($request, $newObject);
496
        if (null !== $preResponse) {
497
            return $preResponse;
498
        }
499
500
        $this->admin->setSubject($newObject);
501
502
        $form = $this->admin->getForm();
503
504
        $form->setData($newObject);
505
        $form->handleRequest($request);
506
507
        if ($form->isSubmitted()) {
508
            $isFormValid = $form->isValid();
509
510
            // persist if the form was valid and if in preview mode the preview was approved
511
            if ($isFormValid && (!$this->isInPreviewMode() || $this->isPreviewApproved())) {
512
                $submittedObject = $form->getData();
513
                $this->admin->setSubject($submittedObject);
514
                $this->admin->checkAccess('create', $submittedObject);
515
516
                try {
517
                    $newObject = $this->admin->create($submittedObject);
518
519
                    if ($this->isXmlHttpRequest()) {
520
                        return $this->handleXmlHttpRequestSuccessResponse($request, $newObject);
521
                    }
522
523
                    $this->addFlash(
524
                        'sonata_flash_success',
525
                        $this->trans(
526
                            'flash_create_success',
527
                            ['%name%' => $this->escapeHtml($this->admin->toString($newObject))],
528
                            'SonataAdminBundle'
529
                        )
530
                    );
531
532
                    // redirect to edit mode
533
                    return $this->redirectTo($newObject);
534
                } catch (ModelManagerException $e) {
535
                    $this->handleModelManagerException($e);
536
537
                    $isFormValid = false;
538
                }
539
            }
540
541
            // show an error message if the form failed validation
542
            if (!$isFormValid) {
543
                if ($this->isXmlHttpRequest() && null !== ($response = $this->handleXmlHttpRequestErrorResponse($request, $form))) {
544
                    return $response;
545
                }
546
547
                $this->addFlash(
548
                    'sonata_flash_error',
549
                    $this->trans(
550
                        'flash_create_error',
551
                        ['%name%' => $this->escapeHtml($this->admin->toString($newObject))],
552
                        'SonataAdminBundle'
553
                    )
554
                );
555
            } elseif ($this->isPreviewRequested()) {
556
                // pick the preview template if the form was valid and preview was requested
557
                $templateKey = 'preview';
558
                $this->admin->getShow();
559
            }
560
        }
561
562
        $formView = $form->createView();
563
        // set the theme for the current Admin Form
564
        $this->setFormTheme($formView, $this->admin->getFormTheme());
565
566
        $template = $this->templateRegistry->getTemplate($templateKey);
567
568
        return $this->renderWithExtraParams($template, [
569
            'action' => 'create',
570
            'form' => $formView,
571
            'object' => $newObject,
572
            'objectId' => null,
573
        ], null);
574
    }
575
576
    /**
577
     * Show action.
578
     *
579
     * @throws NotFoundHttpException If the object does not exist
580
     * @throws AccessDeniedException If access is not granted
581
     *
582
     * @return Response
583
     */
584
    public function showAction(Request $request)
585
    {
586
        $id = $request->get($this->admin->getIdParameter());
587
        $object = $this->admin->getObject($id);
588
589
        if (!$object) {
590
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
591
        }
592
593
        $this->checkParentChildAssociation($request, $object);
594
595
        $this->admin->checkAccess('show', $object);
596
597
        $preResponse = $this->preShow($request, $object);
598
        if (null !== $preResponse) {
599
            return $preResponse;
600
        }
601
602
        $this->admin->setSubject($object);
603
604
        $fields = $this->admin->getShow();
605
        \assert($fields instanceof FieldDescriptionCollection);
606
607
        $template = $this->templateRegistry->getTemplate('show');
608
609
        return $this->renderWithExtraParams($template, [
610
            'action' => 'show',
611
            'object' => $object,
612
            'elements' => $fields,
613
        ]);
614
    }
615
616
    /**
617
     * Show history revisions for object.
618
     *
619
     * @throws AccessDeniedException If access is not granted
620
     * @throws NotFoundHttpException If the object does not exist or the audit reader is not available
621
     *
622
     * @return Response
623
     */
624
    public function historyAction(Request $request)
625
    {
626
        $id = $request->get($this->admin->getIdParameter());
627
        $object = $this->admin->getObject($id);
628
629
        if (!$object) {
630
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
631
        }
632
633
        $this->admin->checkAccess('history', $object);
634
635
        $manager = $this->get('sonata.admin.audit.manager');
636
637
        if (!$manager->hasReader($this->admin->getClass())) {
638
            throw $this->createNotFoundException(sprintf(
639
                'unable to find the audit reader for class : %s',
640
                $this->admin->getClass()
641
            ));
642
        }
643
644
        $reader = $manager->getReader($this->admin->getClass());
645
646
        $revisions = $reader->findRevisions($this->admin->getClass(), $id);
647
648
        $template = $this->templateRegistry->getTemplate('history');
649
650
        return $this->renderWithExtraParams($template, [
651
            'action' => 'history',
652
            'object' => $object,
653
            'revisions' => $revisions,
654
            'currentRevision' => $revisions ? current($revisions) : false,
655
        ], null);
656
    }
657
658
    /**
659
     * View history revision of object.
660
     *
661
     * @param string|null $revision
662
     *
663
     * @throws AccessDeniedException If access is not granted
664
     * @throws NotFoundHttpException If the object or revision does not exist or the audit reader is not available
665
     *
666
     * @return Response
667
     */
668
    public function historyViewRevisionAction(Request $request, $revision = null)
669
    {
670
        $id = $request->get($this->admin->getIdParameter());
671
        $object = $this->admin->getObject($id);
672
673
        if (!$object) {
674
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
675
        }
676
677
        $this->admin->checkAccess('historyViewRevision', $object);
678
679
        $manager = $this->get('sonata.admin.audit.manager');
680
681
        if (!$manager->hasReader($this->admin->getClass())) {
682
            throw $this->createNotFoundException(sprintf(
683
                'unable to find the audit reader for class : %s',
684
                $this->admin->getClass()
685
            ));
686
        }
687
688
        $reader = $manager->getReader($this->admin->getClass());
689
690
        // retrieve the revisioned object
691
        $object = $reader->find($this->admin->getClass(), $id, $revision);
692
693
        if (!$object) {
694
            throw $this->createNotFoundException(sprintf(
695
                'unable to find the targeted object `%s` from the revision `%s` with classname : `%s`',
696
                $id,
697
                $revision,
698
                $this->admin->getClass()
699
            ));
700
        }
701
702
        $this->admin->setSubject($object);
703
704
        $template = $this->templateRegistry->getTemplate('show');
705
706
        return $this->renderWithExtraParams($template, [
707
            'action' => 'show',
708
            'object' => $object,
709
            'elements' => $this->admin->getShow(),
710
        ], null);
711
    }
712
713
    /**
714
     * Compare history revisions of object.
715
     *
716
     * @param int|string|null $baseRevision
717
     * @param int|string|null $compareRevision
718
     *
719
     * @throws AccessDeniedException If access is not granted
720
     * @throws NotFoundHttpException If the object or revision does not exist or the audit reader is not available
721
     *
722
     * @return Response
723
     */
724
    public function historyCompareRevisionsAction(Request $request, $baseRevision = null, $compareRevision = null)
725
    {
726
        $this->admin->checkAccess('historyCompareRevisions');
727
728
        $id = $request->get($this->admin->getIdParameter());
729
        $object = $this->admin->getObject($id);
730
731
        if (!$object) {
732
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
733
        }
734
735
        $manager = $this->get('sonata.admin.audit.manager');
736
737
        if (!$manager->hasReader($this->admin->getClass())) {
738
            throw $this->createNotFoundException(sprintf(
739
                'unable to find the audit reader for class : %s',
740
                $this->admin->getClass()
741
            ));
742
        }
743
744
        $reader = $manager->getReader($this->admin->getClass());
745
746
        // retrieve the base revision
747
        $baseObject = $reader->find($this->admin->getClass(), $id, $baseRevision);
748
        if (!$baseObject) {
749
            throw $this->createNotFoundException(
750
                sprintf(
751
                    'unable to find the targeted object `%s` from the revision `%s` with classname : `%s`',
752
                    $id,
753
                    $baseRevision,
754
                    $this->admin->getClass()
755
                )
756
            );
757
        }
758
759
        // retrieve the compare revision
760
        $compareObject = $reader->find($this->admin->getClass(), $id, $compareRevision);
761
        if (!$compareObject) {
762
            throw $this->createNotFoundException(
763
                sprintf(
764
                    'unable to find the targeted object `%s` from the revision `%s` with classname : `%s`',
765
                    $id,
766
                    $compareRevision,
767
                    $this->admin->getClass()
768
                )
769
            );
770
        }
771
772
        $this->admin->setSubject($baseObject);
773
774
        $template = $this->templateRegistry->getTemplate('show_compare');
775
776
        return $this->renderWithExtraParams($template, [
777
            'action' => 'show',
778
            'object' => $baseObject,
779
            'object_compare' => $compareObject,
780
            'elements' => $this->admin->getShow(),
781
        ], null);
782
    }
783
784
    /**
785
     * Export data to specified format.
786
     *
787
     * @throws AccessDeniedException If access is not granted
788
     * @throws \RuntimeException     If the export format is invalid
789
     *
790
     * @return Response
791
     */
792
    public function exportAction(Request $request)
793
    {
794
        $this->admin->checkAccess('export');
795
796
        $format = $request->get('format');
797
798
        $adminExporter = $this->get('sonata.admin.admin_exporter');
799
        $allowedExportFormats = $adminExporter->getAvailableFormats($this->admin);
800
        $filename = $adminExporter->getExportFilename($this->admin, $format);
801
        $exporter = $this->get('sonata.exporter.exporter');
802
803
        if (!\in_array($format, $allowedExportFormats, true)) {
804
            throw new \RuntimeException(sprintf(
805
                'Export in format `%s` is not allowed for class: `%s`. Allowed formats are: `%s`',
806
                $format,
807
                $this->admin->getClass(),
808
                implode(', ', $allowedExportFormats)
809
            ));
810
        }
811
812
        return $exporter->getResponse(
813
            $format,
814
            $filename,
815
            $this->admin->getDataSourceIterator()
816
        );
817
    }
818
819
    /**
820
     * Returns the Response object associated to the acl action.
821
     *
822
     * @throws AccessDeniedException If access is not granted
823
     * @throws NotFoundHttpException If the object does not exist or the ACL is not enabled
824
     *
825
     * @return Response|RedirectResponse
826
     */
827
    public function aclAction(Request $request)
828
    {
829
        if (!$this->admin->isAclEnabled()) {
830
            throw $this->createNotFoundException('ACL are not enabled for this admin');
831
        }
832
833
        $id = $request->get($this->admin->getIdParameter());
834
        $object = $this->admin->getObject($id);
835
836
        if (!$object) {
837
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
838
        }
839
840
        $this->admin->checkAccess('acl', $object);
841
842
        $this->admin->setSubject($object);
843
        $aclUsers = $this->getAclUsers();
844
        $aclRoles = $this->getAclRoles();
845
846
        $adminObjectAclManipulator = $this->get('sonata.admin.object.manipulator.acl.admin');
847
        $adminObjectAclData = new AdminObjectAclData(
848
            $this->admin,
849
            $object,
850
            $aclUsers,
851
            $adminObjectAclManipulator->getMaskBuilderClass(),
852
            $aclRoles
853
        );
854
855
        $aclUsersForm = $adminObjectAclManipulator->createAclUsersForm($adminObjectAclData);
856
        $aclRolesForm = $adminObjectAclManipulator->createAclRolesForm($adminObjectAclData);
857
858
        if (Request::METHOD_POST === $request->getMethod()) {
859
            if ($request->request->has(AdminObjectAclManipulator::ACL_USERS_FORM_NAME)) {
860
                $form = $aclUsersForm;
861
                $updateMethod = 'updateAclUsers';
862
            } elseif ($request->request->has(AdminObjectAclManipulator::ACL_ROLES_FORM_NAME)) {
863
                $form = $aclRolesForm;
864
                $updateMethod = 'updateAclRoles';
865
            }
866
867
            if (isset($form, $updateMethod)) {
868
                $form->handleRequest($request);
869
870
                if ($form->isValid()) {
871
                    $adminObjectAclManipulator->$updateMethod($adminObjectAclData);
872
                    $this->addFlash(
873
                        'sonata_flash_success',
874
                        $this->trans('flash_acl_edit_success', [], 'SonataAdminBundle')
875
                    );
876
877
                    return new RedirectResponse($this->admin->generateObjectUrl('acl', $object));
878
                }
879
            }
880
        }
881
882
        $template = $this->templateRegistry->getTemplate('acl');
883
884
        return $this->renderWithExtraParams($template, [
885
            'action' => 'acl',
886
            'permissions' => $adminObjectAclData->getUserPermissions(),
887
            'object' => $object,
888
            'users' => $aclUsers,
889
            'roles' => $aclRoles,
890
            'aclUsersForm' => $aclUsersForm->createView(),
891
            'aclRolesForm' => $aclRolesForm->createView(),
892
        ], null);
893
    }
894
895
    /**
896
     * @return Request
897
     */
898
    public function getRequest()
899
    {
900
        return $this->container->get('request_stack')->getCurrentRequest();
901
    }
902
903
    /**
904
     * @param array<string, mixed> $parameters
905
     *
906
     * @return array<string, mixed>
907
     */
908
    protected function addRenderExtraParams(array $parameters = []): array
909
    {
910
        if (!$this->isXmlHttpRequest()) {
911
            $parameters['breadcrumbs_builder'] = $this->get('sonata.admin.breadcrumbs_builder');
912
        }
913
914
        $parameters['admin'] = $parameters['admin'] ?? $this->admin;
915
        $parameters['base_template'] = $parameters['base_template'] ?? $this->getBaseTemplate();
916
        $parameters['admin_pool'] = $this->get('sonata.admin.pool');
917
918
        return $parameters;
919
    }
920
921
    /**
922
     * Gets a container configuration parameter by its name.
923
     *
924
     * @param string $name The parameter name
925
     *
926
     * @return mixed
927
     */
928
    protected function getParameter($name)
929
    {
930
        return $this->container->getParameter($name);
931
    }
932
933
    /**
934
     * Render JSON.
935
     *
936
     * @param mixed $data
937
     * @param int   $status
938
     * @param array $headers
939
     *
940
     * @return JsonResponse with json encoded data
941
     */
942
    protected function renderJson($data, $status = Response::HTTP_OK, $headers = [])
943
    {
944
        return new JsonResponse($data, $status, $headers);
945
    }
946
947
    /**
948
     * Returns true if the request is a XMLHttpRequest.
949
     *
950
     * @return bool True if the request is an XMLHttpRequest, false otherwise
951
     */
952
    protected function isXmlHttpRequest()
953
    {
954
        $request = $this->getRequest();
955
956
        return $request->isXmlHttpRequest() || $request->get('_xml_http_request');
957
    }
958
959
    /**
960
     * Returns the correct RESTful verb, given either by the request itself or
961
     * via the "_method" parameter.
962
     *
963
     * @return string HTTP method, either
964
     */
965
    protected function getRestMethod()
966
    {
967
        $request = $this->getRequest();
968
969
        if (Request::getHttpMethodParameterOverride() || !$request->request->has('_method')) {
970
            return $request->getMethod();
971
        }
972
973
        return $request->request->get('_method');
974
    }
975
976
    /**
977
     * Contextualize the admin class depends on the current request.
978
     *
979
     * @throws \RuntimeException
980
     */
981
    protected function configure(): void
982
    {
983
        $request = $this->getRequest();
984
985
        $adminCode = $request->get('_sonata_admin');
986
987
        if (!$adminCode) {
988
            throw new \RuntimeException(sprintf(
989
                'There is no `_sonata_admin` defined for the controller `%s` and the current route `%s`',
990
                static::class,
991
                $request->get('_route')
992
            ));
993
        }
994
995
        try {
996
            $this->admin = $this->container->get('sonata.admin.pool')->getAdminByAdminCode($adminCode);
997
        } catch (\InvalidArgumentException $e) {
998
            throw new \RuntimeException(sprintf(
999
                'Unable to find the admin class related to the current controller (%s)',
1000
                static::class
1001
            ));
1002
        }
1003
1004
        $this->templateRegistry = $this->container->get(sprintf('%s.template_registry', $this->admin->getCode()));
1005
        if (!$this->templateRegistry instanceof TemplateRegistryInterface) {
1006
            throw new \RuntimeException(sprintf(
1007
                'Unable to find the template registry related to the current admin (%s)',
1008
                $this->admin->getCode()
1009
            ));
1010
        }
1011
1012
        $rootAdmin = $this->admin;
1013
1014
        while ($rootAdmin->isChild()) {
1015
            $rootAdmin->setCurrentChild(true);
1016
            $rootAdmin = $rootAdmin->getParent();
1017
        }
1018
1019
        $rootAdmin->setRequest($request);
1020
1021
        if ($request->get('uniqid')) {
1022
            $this->admin->setUniqid($request->get('uniqid'));
1023
        }
1024
    }
1025
1026
    /**
1027
     * Proxy for the logger service of the container.
1028
     * If no such service is found, a NullLogger is returned.
1029
     *
1030
     * @return LoggerInterface
1031
     */
1032
    protected function getLogger()
1033
    {
1034
        if ($this->container->has('logger')) {
1035
            $logger = $this->container->get('logger');
1036
            \assert($logger instanceof LoggerInterface);
1037
1038
            return $logger;
1039
        }
1040
1041
        return new NullLogger();
1042
    }
1043
1044
    /**
1045
     * Returns the base template name.
1046
     *
1047
     * @return string The template name
1048
     */
1049
    protected function getBaseTemplate()
1050
    {
1051
        if ($this->isXmlHttpRequest()) {
1052
            return $this->templateRegistry->getTemplate('ajax');
1053
        }
1054
1055
        return $this->templateRegistry->getTemplate('layout');
1056
    }
1057
1058
    /**
1059
     * @throws \Exception
1060
     */
1061
    protected function handleModelManagerException(\Exception $e): void
1062
    {
1063
        if ($this->get('kernel')->isDebug()) {
1064
            throw $e;
1065
        }
1066
1067
        $context = ['exception' => $e];
1068
        if ($e->getPrevious()) {
1069
            $context['previous_exception_message'] = $e->getPrevious()->getMessage();
1070
        }
1071
        $this->getLogger()->error($e->getMessage(), $context);
1072
    }
1073
1074
    /**
1075
     * Redirect the user depend on this choice.
1076
     *
1077
     * @param object $object
1078
     *
1079
     * @return RedirectResponse
1080
     */
1081
    protected function redirectTo($object)
1082
    {
1083
        $request = $this->getRequest();
1084
1085
        $url = false;
1086
1087
        if (null !== $request->get('btn_update_and_list')) {
1088
            return $this->redirectToList();
1089
        }
1090
        if (null !== $request->get('btn_create_and_list')) {
1091
            return $this->redirectToList();
1092
        }
1093
1094
        if (null !== $request->get('btn_create_and_create')) {
1095
            $params = [];
1096
            if ($this->admin->hasActiveSubClass()) {
1097
                $params['subclass'] = $request->get('subclass');
1098
            }
1099
            $url = $this->admin->generateUrl('create', $params);
1100
        }
1101
1102
        if ('DELETE' === $this->getRestMethod()) {
1103
            return $this->redirectToList();
1104
        }
1105
1106
        if (!$url) {
1107
            foreach (['edit', 'show'] as $route) {
1108
                if ($this->admin->hasRoute($route) && $this->admin->hasAccess($route, $object)) {
1109
                    $url = $this->admin->generateObjectUrl(
1110
                        $route,
1111
                        $object,
1112
                        $this->getSelectedTab($request)
1113
                    );
1114
1115
                    break;
1116
                }
1117
            }
1118
        }
1119
1120
        if (!$url) {
1121
            return $this->redirectToList();
1122
        }
1123
1124
        return new RedirectResponse($url);
1125
    }
1126
1127
    /**
1128
     * Redirects the user to the list view.
1129
     *
1130
     * @return RedirectResponse
1131
     */
1132
    final protected function redirectToList()
1133
    {
1134
        $parameters = [];
1135
1136
        if ($filter = $this->admin->getFilterParameters()) {
1137
            $parameters['filter'] = $filter;
1138
        }
1139
1140
        return $this->redirect($this->admin->generateUrl('list', $parameters));
1141
    }
1142
1143
    /**
1144
     * Returns true if the preview is requested to be shown.
1145
     *
1146
     * @return bool
1147
     */
1148
    protected function isPreviewRequested()
1149
    {
1150
        $request = $this->getRequest();
1151
1152
        return null !== $request->get('btn_preview');
1153
    }
1154
1155
    /**
1156
     * Returns true if the preview has been approved.
1157
     *
1158
     * @return bool
1159
     */
1160
    protected function isPreviewApproved()
1161
    {
1162
        $request = $this->getRequest();
1163
1164
        return null !== $request->get('btn_preview_approve');
1165
    }
1166
1167
    /**
1168
     * Returns true if the request is in the preview workflow.
1169
     *
1170
     * That means either a preview is requested or the preview has already been shown
1171
     * and it got approved/declined.
1172
     *
1173
     * @return bool
1174
     */
1175
    protected function isInPreviewMode()
1176
    {
1177
        return $this->admin->supportsPreviewMode()
1178
        && ($this->isPreviewRequested()
1179
            || $this->isPreviewApproved()
1180
            || $this->isPreviewDeclined());
1181
    }
1182
1183
    /**
1184
     * Returns true if the preview has been declined.
1185
     *
1186
     * @return bool
1187
     */
1188
    protected function isPreviewDeclined()
1189
    {
1190
        $request = $this->getRequest();
1191
1192
        return null !== $request->get('btn_preview_decline');
1193
    }
1194
1195
    /**
1196
     * Gets ACL users.
1197
     *
1198
     * @return \Traversable
1199
     */
1200
    protected function getAclUsers()
1201
    {
1202
        $aclUsers = [];
1203
1204
        $userManagerServiceName = $this->container->getParameter('sonata.admin.security.acl_user_manager');
1205
        if (null !== $userManagerServiceName && $this->has($userManagerServiceName)) {
1206
            $userManager = $this->get($userManagerServiceName);
1207
1208
            if (method_exists($userManager, 'findUsers')) {
1209
                $aclUsers = $userManager->findUsers();
1210
            }
1211
        }
1212
1213
        return \is_array($aclUsers) ? new \ArrayIterator($aclUsers) : $aclUsers;
1214
    }
1215
1216
    /**
1217
     * Gets ACL roles.
1218
     *
1219
     * @return \Traversable
1220
     */
1221
    protected function getAclRoles()
1222
    {
1223
        $aclRoles = [];
1224
        $roleHierarchy = $this->container->getParameter('security.role_hierarchy.roles');
1225
        $pool = $this->container->get('sonata.admin.pool');
1226
1227
        foreach ($pool->getAdminServiceIds() as $id) {
1228
            try {
1229
                $admin = $pool->getInstance($id);
1230
            } catch (\Exception $e) {
1231
                continue;
1232
            }
1233
1234
            $baseRole = $admin->getSecurityHandler()->getBaseRole($admin);
1235
            foreach ($admin->getSecurityInformation() as $role => $permissions) {
1236
                $role = sprintf($baseRole, $role);
1237
                $aclRoles[] = $role;
1238
            }
1239
        }
1240
1241
        foreach ($roleHierarchy as $name => $roles) {
1242
            $aclRoles[] = $name;
1243
            $aclRoles = array_merge($aclRoles, $roles);
1244
        }
1245
1246
        $aclRoles = array_unique($aclRoles);
1247
1248
        return new \ArrayIterator($aclRoles);
1249
    }
1250
1251
    /**
1252
     * Validate CSRF token for action without form.
1253
     *
1254
     * @param string $intention
1255
     *
1256
     * @throws HttpException
1257
     */
1258
    protected function validateCsrfToken($intention): void
1259
    {
1260
        $request = $this->getRequest();
1261
        $token = $request->get('_sonata_csrf_token');
1262
1263
        if ($this->container->has('security.csrf.token_manager')) {
1264
            $valid = $this->container->get('security.csrf.token_manager')->isTokenValid(new CsrfToken($intention, $token));
1265
        } else {
1266
            return;
1267
        }
1268
1269
        if (!$valid) {
1270
            throw new HttpException(Response::HTTP_BAD_REQUEST, 'The csrf token is not valid, CSRF attack?');
1271
        }
1272
    }
1273
1274
    /**
1275
     * Escape string for html output.
1276
     *
1277
     * @param string $s
1278
     *
1279
     * @return string
1280
     */
1281
    protected function escapeHtml($s)
1282
    {
1283
        return htmlspecialchars((string) $s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
1284
    }
1285
1286
    /**
1287
     * Get CSRF token.
1288
     *
1289
     * @param string $intention
1290
     *
1291
     * @return string|false
1292
     */
1293
    protected function getCsrfToken($intention)
1294
    {
1295
        if ($this->container->has('security.csrf.token_manager')) {
1296
            return $this->container->get('security.csrf.token_manager')->getToken($intention)->getValue();
1297
        }
1298
1299
        return false;
1300
    }
1301
1302
    /**
1303
     * This method can be overloaded in your custom CRUD controller.
1304
     * It's called from createAction.
1305
     *
1306
     * @param object $object
1307
     *
1308
     * @return Response|null
1309
     */
1310
    protected function preCreate(Request $request, $object)
0 ignored issues
show
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...
1311
    {
1312
        return null;
1313
    }
1314
1315
    /**
1316
     * This method can be overloaded in your custom CRUD controller.
1317
     * It's called from editAction.
1318
     *
1319
     * @param object $object
1320
     *
1321
     * @return Response|null
1322
     */
1323
    protected function preEdit(Request $request, $object)
0 ignored issues
show
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...
1324
    {
1325
        return null;
1326
    }
1327
1328
    /**
1329
     * This method can be overloaded in your custom CRUD controller.
1330
     * It's called from deleteAction.
1331
     *
1332
     * @param object $object
1333
     *
1334
     * @return Response|null
1335
     */
1336
    protected function preDelete(Request $request, $object)
0 ignored issues
show
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...
1337
    {
1338
        return null;
1339
    }
1340
1341
    /**
1342
     * This method can be overloaded in your custom CRUD controller.
1343
     * It's called from showAction.
1344
     *
1345
     * @param object $object
1346
     *
1347
     * @return Response|null
1348
     */
1349
    protected function preShow(Request $request, $object)
0 ignored issues
show
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...
1350
    {
1351
        return null;
1352
    }
1353
1354
    /**
1355
     * This method can be overloaded in your custom CRUD controller.
1356
     * It's called from listAction.
1357
     *
1358
     * @return Response|null
1359
     */
1360
    protected function preList(Request $request)
0 ignored issues
show
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...
1361
    {
1362
        return null;
1363
    }
1364
1365
    /**
1366
     * Translate a message id.
1367
     *
1368
     * @param string $id
1369
     * @param string $domain
1370
     * @param string $locale
1371
     *
1372
     * @return string translated string
1373
     */
1374
    final protected function trans($id, array $parameters = [], $domain = null, $locale = null)
1375
    {
1376
        $domain = $domain ?: $this->admin->getTranslationDomain();
1377
1378
        return $this->get('translator')->trans($id, $parameters, $domain, $locale);
1379
    }
1380
1381
    private function getSelectedTab(Request $request): array
1382
    {
1383
        return array_filter(['_tab' => $request->request->get('_tab')]);
1384
    }
1385
1386
    private function checkParentChildAssociation(Request $request, object $object): void
1387
    {
1388
        if (!$this->admin->isChild()) {
1389
            return;
1390
        }
1391
1392
        $parentAdmin = $this->admin->getParent();
1393
        $parentId = $request->get($parentAdmin->getIdParameter());
1394
1395
        $propertyAccessor = PropertyAccess::createPropertyAccessor();
1396
        $propertyPath = new PropertyPath($this->admin->getParentAssociationMapping());
1397
1398
        if ($parentAdmin->getObject($parentId) !== $propertyAccessor->getValue($object, $propertyPath)) {
1399
            throw new \RuntimeException(sprintf(
1400
                'There is no association between "%s" and "%s"',
1401
                $parentAdmin->toString($parentAdmin->getObject($parentId)),
1402
                $this->admin->toString($object)
1403
            ));
1404
        }
1405
    }
1406
1407
    /**
1408
     * Sets the admin form theme to form view. Used for compatibility between Symfony versions.
1409
     */
1410
    private function setFormTheme(FormView $formView, ?array $theme = null): void
1411
    {
1412
        $twig = $this->get('twig');
1413
1414
        $twig->getRuntime(FormRenderer::class)->setTheme($formView, $theme);
1415
    }
1416
1417
    private function handleXmlHttpRequestErrorResponse(Request $request, FormInterface $form): JsonResponse
1418
    {
1419
        if (empty(array_intersect(['application/json', '*/*'], $request->getAcceptableContentTypes()))) {
1420
            return $this->renderJson([], Response::HTTP_NOT_ACCEPTABLE);
1421
        }
1422
1423
        $errors = [];
1424
        foreach ($form->getErrors(true) as $error) {
1425
            $errors[] = $error->getMessage();
1426
        }
1427
1428
        return $this->renderJson([
1429
            'result' => 'error',
1430
            'errors' => $errors,
1431
        ], Response::HTTP_BAD_REQUEST);
1432
    }
1433
1434
    /**
1435
     * @param object $object
1436
     */
1437
    private function handleXmlHttpRequestSuccessResponse(Request $request, $object): JsonResponse
1438
    {
1439
        if (empty(array_intersect(['application/json', '*/*'], $request->getAcceptableContentTypes()))) {
1440
            return $this->renderJson([], Response::HTTP_NOT_ACCEPTABLE);
1441
        }
1442
1443
        return $this->renderJson([
1444
            'result' => 'ok',
1445
            'objectId' => $this->admin->getNormalizedIdentifier($object),
1446
            'objectName' => $this->escapeHtml($this->admin->toString($object)),
1447
        ], Response::HTTP_OK);
1448
    }
1449
}
1450