Passed
Push — master ( 9cc210...167e46 )
by Jan
04:55
created

BaseAdminController::additionalActionNew()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 2
dl 0
loc 3
rs 10
1
<?php
2
/**
3
 * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
4
 *
5
 * Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
6
 *
7
 * This program is free software: you can redistribute it and/or modify
8
 * it under the terms of the GNU Affero General Public License as published
9
 * by the Free Software Foundation, either version 3 of the License, or
10
 * (at your option) any later version.
11
 *
12
 * This program is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
 * GNU Affero General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU Affero General Public License
18
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19
 */
20
21
declare(strict_types=1);
22
23
/**
24
 * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
25
 *
26
 * Copyright (C) 2019 Jan Böhmer (https://github.com/jbtronics)
27
 *
28
 * This program is free software; you can redistribute it and/or
29
 * modify it under the terms of the GNU General Public License
30
 * as published by the Free Software Foundation; either version 2
31
 * of the License, or (at your option) any later version.
32
 *
33
 * This program is distributed in the hope that it will be useful,
34
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
35
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
36
 * GNU General Public License for more details.
37
 *
38
 * You should have received a copy of the GNU General Public License
39
 * along with this program; if not, write to the Free Software
40
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
41
 */
42
43
namespace App\Controller\AdminPages;
44
45
use App\DataTables\LogDataTable;
46
use App\Entity\Attachments\AttachmentType;
47
use App\Entity\Base\AbstractDBElement;
48
use App\Entity\Base\AbstractNamedDBElement;
49
use App\Entity\Base\AbstractPartsContainingDBElement;
50
use App\Entity\Base\AbstractStructuralDBElement;
51
use App\Entity\Base\PartsContainingRepositoryInterface;
52
use App\Entity\LabelSystem\LabelProfile;
53
use App\Entity\PriceInformations\Currency;
54
use App\Entity\UserSystem\Group;
55
use App\Entity\UserSystem\User;
56
use App\Events\SecurityEvent;
57
use App\Events\SecurityEvents;
58
use App\Exceptions\AttachmentDownloadException;
59
use App\Form\AdminPages\ImportType;
60
use App\Form\AdminPages\MassCreationForm;
61
use App\Repository\AbstractPartsContainingRepository;
62
use App\Services\Attachments\AttachmentSubmitHandler;
63
use App\Services\EntityExporter;
64
use App\Services\EntityImporter;
65
use App\Services\LabelSystem\Barcodes\BarcodeExampleElementsGenerator;
66
use App\Services\LabelSystem\LabelGenerator;
67
use App\Services\LogSystem\EventCommentHelper;
68
use App\Services\LogSystem\HistoryHelper;
69
use App\Services\LogSystem\TimeTravel;
70
use App\Services\StructuralElementRecursionHelper;
71
use Doctrine\ORM\EntityManagerInterface;
72
use InvalidArgumentException;
73
use Omines\DataTablesBundle\DataTableFactory;
74
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
75
use Symfony\Component\EventDispatcher\EventDispatcher;
76
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
77
use Symfony\Component\Form\FormInterface;
78
use Symfony\Component\HttpFoundation\File\UploadedFile;
79
use Symfony\Component\HttpFoundation\RedirectResponse;
80
use Symfony\Component\HttpFoundation\Request;
81
use Symfony\Component\HttpFoundation\Response;
82
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
83
use Symfony\Component\Validator\ConstraintViolationList;
84
use Symfony\Contracts\Translation\TranslatorInterface;
85
86
abstract class BaseAdminController extends AbstractController
87
{
88
    protected $entity_class = '';
89
    protected $form_class = '';
90
    protected $twig_template = '';
91
    protected $route_base = '';
92
    protected $attachment_class = '';
93
    protected $parameter_class = '';
94
95
    protected $passwordEncoder;
96
    protected $translator;
97
    protected $attachmentSubmitHandler;
98
    protected $commentHelper;
99
100
    protected $historyHelper;
101
    protected $timeTravel;
102
    protected $dataTableFactory;
103
    /**
104
     * @var EventDispatcher
105
     */
106
    protected $eventDispatcher;
107
    protected $labelGenerator;
108
    protected $barcodeExampleGenerator;
109
110
    protected $entityManager;
111
112
    public function __construct(TranslatorInterface $translator, UserPasswordEncoderInterface $passwordEncoder,
113
        AttachmentSubmitHandler $attachmentSubmitHandler,
114
        EventCommentHelper $commentHelper, HistoryHelper $historyHelper, TimeTravel $timeTravel,
115
        DataTableFactory $dataTableFactory, EventDispatcherInterface $eventDispatcher, BarcodeExampleElementsGenerator $barcodeExampleGenerator,
116
        LabelGenerator $labelGenerator, EntityManagerInterface $entityManager)
117
    {
118
        if ('' === $this->entity_class || '' === $this->form_class || '' === $this->twig_template || '' === $this->route_base) {
0 ignored issues
show
introduced by
The condition '' === $this->form_class is always false.
Loading history...
introduced by
The condition '' === $this->twig_template is always false.
Loading history...
introduced by
The condition '' === $this->route_base is always false.
Loading history...
119
            throw new InvalidArgumentException('You have to override the $entity_class, $form_class, $route_base and $twig_template value in your subclasss!');
120
        }
121
122
        if ('' === $this->attachment_class) {
0 ignored issues
show
introduced by
The condition '' === $this->attachment_class is always false.
Loading history...
123
            throw new InvalidArgumentException('You have to override the $attachment_class value in your subclass!');
124
        }
125
126
        if ('' === $this->parameter_class) {
0 ignored issues
show
introduced by
The condition '' === $this->parameter_class is always false.
Loading history...
127
            throw new InvalidArgumentException('You have to override the $parameter_class value in your subclass!');
128
        }
129
130
        $this->translator = $translator;
131
        $this->passwordEncoder = $passwordEncoder;
132
        $this->attachmentSubmitHandler = $attachmentSubmitHandler;
133
        $this->commentHelper = $commentHelper;
134
        $this->historyHelper = $historyHelper;
135
        $this->timeTravel = $timeTravel;
136
        $this->dataTableFactory = $dataTableFactory;
137
        $this->eventDispatcher = $eventDispatcher;
138
        $this->barcodeExampleGenerator = $barcodeExampleGenerator;
139
        $this->labelGenerator = $labelGenerator;
140
        $this->entityManager = $entityManager;
141
    }
142
143
    protected function revertElementIfNeeded(AbstractDBElement $entity, ?string $timestamp): ?\DateTime
144
    {
145
        if (null !== $timestamp) {
146
            $this->denyAccessUnlessGranted('@tools.timetravel');
147
            $this->denyAccessUnlessGranted('show_history', $entity);
148
            //If the timestamp only contains numbers interpret it as unix timestamp
149
            if (ctype_digit($timestamp)) {
150
                $timeTravel_timestamp = new \DateTime();
151
                $timeTravel_timestamp->setTimestamp((int) $timestamp);
152
            } else { //Try to parse it via DateTime
153
                $timeTravel_timestamp = new \DateTime($timestamp);
154
            }
155
            $this->timeTravel->revertEntityToTimestamp($entity, $timeTravel_timestamp);
156
157
            return $timeTravel_timestamp;
158
        }
159
        return null;
160
    }
161
162
    /**
163
     * Perform some additional actions, when the form was valid, but before the entity is saved.
164
     * @return bool Return true, to save entity normally, return false, to abort saving.
165
     */
166
    protected function additionalActionEdit(FormInterface $form, AbstractNamedDBElement $entity): bool
167
    {
168
        return true;
169
    }
170
171
    protected function _edit(AbstractNamedDBElement $entity, Request $request, EntityManagerInterface $em, ?string $timestamp = null): Response
172
    {
173
        $this->denyAccessUnlessGranted('read', $entity);
174
175
        $timeTravel_timestamp = $this->revertElementIfNeeded($entity, $timestamp);
176
177
178
        if ($this->isGranted('show_history', $entity)) {
179
            $table = $this->dataTableFactory->createFromType(
180
                LogDataTable::class,
181
                [
182
                    'filter_elements' => $this->historyHelper->getAssociatedElements($entity),
183
                    'mode' => 'element_history',
184
                ],
185
                ['pageLength' => 10]
186
            )
187
                ->handleRequest($request);
188
189
            if ($table->isCallback()) {
190
                return $table->getResponse();
191
            }
192
        } else {
193
            $table = null;
194
        }
195
196
        $form_options = [
197
            'attachment_class' => $this->attachment_class,
198
            'parameter_class' => $this->parameter_class,
199
            'disabled' => null !== $timeTravel_timestamp ? true : false,
200
        ];
201
202
        //Disable editing of options, if user is not allowed to use twig...
203
        if (
204
            $entity instanceof LabelProfile
205
            && 'twig' === $entity->getOptions()->getLinesMode()
206
            && ! $this->isGranted('@labels.use_twig')
207
        ) {
208
            $form_options['disable_options'] = true;
209
        }
210
211
        $form = $this->createForm($this->form_class, $entity, $form_options);
212
213
        $form->handleRequest($request);
214
        if ($form->isSubmitted() && $form->isValid()) {
215
            if ($this->additionalActionEdit($form, $entity)) {
216
                //Upload passed files
217
                $attachments = $form['attachments'];
218
                foreach ($attachments as $attachment) {
219
                    /** @var FormInterface $attachment */
220
                    $options = [
221
                        'secure_attachment' => $attachment['secureFile']->getData(),
222
                        'download_url' => $attachment['downloadURL']->getData(),
223
                    ];
224
225
                    try {
226
                        $this->attachmentSubmitHandler->handleFormSubmit(
227
                            $attachment->getData(),
228
                            $attachment['file']->getData(),
229
                            $options
230
                        );
231
                    } catch (AttachmentDownloadException $attachmentDownloadException) {
232
                        $this->addFlash(
233
                            'error',
234
                            $this->translator->trans(
235
                                'attachment.download_failed'
236
                            ).' '.$attachmentDownloadException->getMessage()
237
                        );
238
                    }
239
                }
240
241
                $this->commentHelper->setMessage($form['log_comment']->getData());
242
243
                $em->persist($entity);
244
                $em->flush();
245
                $this->addFlash('success', 'entity.edit_flash');
246
            }
247
248
            //Rebuild form, so it is based on the updated data. Important for the parent field!
249
            //We can not use dynamic form events here, because the parent entity list is build from database!
250
            $form = $this->createForm($this->form_class, $entity, [
251
                'attachment_class' => $this->attachment_class,
252
                'parameter_class' => $this->parameter_class,
253
            ]);
254
        } elseif ($form->isSubmitted() && ! $form->isValid()) {
255
            $this->addFlash('error', 'entity.edit_flash.invalid');
256
        }
257
258
        //Show preview for LabelProfile if needed.
259
        if ($entity instanceof LabelProfile) {
260
            $example = $this->barcodeExampleGenerator->getElement($entity->getOptions()->getSupportedElement());
261
            $pdf_data = $this->labelGenerator->generateLabel($entity->getOptions(), $example);
262
        }
263
264
        /** @var AbstractPartsContainingRepository $repo */
265
        $repo = $this->entityManager->getRepository($this->entity_class);
266
267
268
        return $this->render($this->twig_template, [
269
            'entity' => $entity,
270
            'form' => $form->createView(),
271
            'route_base' => $this->route_base,
272
            'datatable' => $table,
273
            'pdf_data' => $pdf_data ?? null,
274
            'timeTravel' => $timeTravel_timestamp,
275
            'repo' => $repo,
276
            'partsContainingElement' => $repo instanceof PartsContainingRepositoryInterface,
277
        ]);
278
    }
279
280
    /**
281
     * Perform some additional actions, when the form was valid, but before the entity is saved.
282
     * @return bool Return true, to save entity normally, return false, to abort saving.
283
     */
284
    protected function additionalActionNew(FormInterface $form, AbstractNamedDBElement $entity): bool
285
    {
286
        return true;
287
    }
288
289
    protected function _new(Request $request, EntityManagerInterface $em, EntityImporter $importer, ?AbstractNamedDBElement $entity = null)
290
    {
291
        $master_picture_backup = null;
292
        if (null === $entity) {
293
            /** @var AbstractStructuralDBElement|User $new_entity */
294
            $new_entity = new $this->entity_class();
295
        } else {
296
            /** @var AbstractStructuralDBElement|User $new_entity */
297
            $new_entity = clone $entity;
298
        }
299
300
        $this->denyAccessUnlessGranted('read', $new_entity);
301
302
        //Basic edit form
303
        $form = $this->createForm($this->form_class, $new_entity, [
304
            'attachment_class' => $this->attachment_class,
305
            'parameter_class' => $this->parameter_class,
306
        ]);
307
308
        $form->handleRequest($request);
309
310
        if ($form->isSubmitted() && $form->isValid()) {
311
312
            //Perform additional actions
313
            if ($this->additionalActionNew($form, $new_entity)) {
314
                //Upload passed files
315
                $attachments = $form['attachments'];
316
                foreach ($attachments as $attachment) {
317
                    /** @var FormInterface $attachment */
318
                    $options = [
319
                        'secure_attachment' => $attachment['secureFile']->getData(),
320
                        'download_url' => $attachment['downloadURL']->getData(),
321
                    ];
322
323
                    try {
324
                        $this->attachmentSubmitHandler->handleFormSubmit(
325
                            $attachment->getData(),
326
                            $attachment['file']->getData(),
327
                            $options
328
                        );
329
                    } catch (AttachmentDownloadException $attachmentDownloadException) {
330
                        $this->addFlash(
331
                            'error',
332
                            $this->translator->trans(
333
                                'attachment.download_failed'
334
                            ).' '.$attachmentDownloadException->getMessage()
335
                        );
336
                    }
337
                }
338
339
                $this->commentHelper->setMessage($form['log_comment']->getData());
340
341
                $em->persist($new_entity);
342
                $em->flush();
343
                $this->addFlash('success', 'entity.created_flash');
344
345
                return $this->redirectToRoute($this->route_base.'_edit', ['id' => $new_entity->getID()]);
346
            }
347
        }
348
349
        if ($form->isSubmitted() && ! $form->isValid()) {
350
            $this->addFlash('error', 'entity.created_flash.invalid');
351
        }
352
353
        //Import form
354
        $import_form = $this->createForm(ImportType::class, ['entity_class' => $this->entity_class]);
355
        $import_form->handleRequest($request);
356
357
        if ($import_form->isSubmitted() && $import_form->isValid()) {
358
            /** @var UploadedFile $file */
359
            $file = $import_form['file']->getData();
360
            $data = $import_form->getData();
361
362
            $options = [
363
                'parent' => $data['parent'],
364
                'preserve_children' => $data['preserve_children'],
365
                'format' => $data['format'],
366
                'csv_separator' => $data['csv_separator'],
367
            ];
368
369
            $this->commentHelper->setMessage('Import '.$file->getClientOriginalName());
370
371
            $errors = $importer->fileToDBEntities($file, $this->entity_class, $options);
372
373
            foreach ($errors as $name => $error) {
374
                /** @var ConstraintViolationList $error */
375
                $this->addFlash('error', $name.':'.$error);
376
            }
377
        }
378
379
        //Mass creation form
380
        $mass_creation_form = $this->createForm(MassCreationForm::class, ['entity_class' => $this->entity_class]);
381
        $mass_creation_form->handleRequest($request);
382
383
        if ($mass_creation_form->isSubmitted() && $mass_creation_form->isValid()) {
384
            $data = $mass_creation_form->getData();
385
386
            //Create entries based on input
387
            $errors = [];
388
            $results = $importer->massCreation($data['lines'], $this->entity_class, $data['parent'], $errors);
389
390
            //Show errors to user:
391
            foreach ($errors as $error) {
392
                $this->addFlash('error', $error['entity']->getFullPath().':'.$error['violations']);
393
            }
394
395
            //Persist valid entities to DB
396
            foreach ($results as $result) {
397
                $em->persist($result);
398
            }
399
            $em->flush();
400
        }
401
402
403
        return $this->render($this->twig_template, [
404
            'entity' => $new_entity,
405
            'form' => $form->createView(),
406
            'import_form' => $import_form->createView(),
407
            'mass_creation_form' => $mass_creation_form->createView(),
408
            'route_base' => $this->route_base,
409
        ]);
410
    }
411
412
    /**
413
     * Performs checks if the element can be deleted safely. Otherwise an flash message is added.
414
     * @param  AbstractNamedDBElement  $entity The element that should be checked.
415
     * @return bool True if the the element can be deleted, false if not
416
     */
417
    protected function deleteCheck(AbstractNamedDBElement $entity): bool
418
    {
419
        if ($entity instanceof AbstractPartsContainingDBElement) {
420
            /** @var AbstractPartsContainingRepository $repo */
421
            $repo = $this->entityManager->getRepository($this->entity_class);
422
            if ($repo->getPartsCount($entity) > 0) {
423
                $this->addFlash('error', 'entity.delete.must_not_contain_parts');
424
                return false;
425
            }
426
        }
427
        return true;
428
    }
429
430
    protected function _delete(Request $request, AbstractNamedDBElement $entity, StructuralElementRecursionHelper $recursionHelper): RedirectResponse
431
    {
432
        $this->denyAccessUnlessGranted('delete', $entity);
433
434
        if ($this->isCsrfTokenValid('delete'.$entity->getId(), $request->request->get('_token'))) {
435
            $entityManager = $this->getDoctrine()->getManager();
436
437
            if (!$this->deleteCheck($entity)) {
438
                return $this->redirectToRoute($this->route_base.'_edit', ['id' => $entity->getID()]);
439
            }
440
441
            //Check if we need to remove recursively
442
            if ($entity instanceof AbstractStructuralDBElement && $request->get('delete_recursive', false)) {
443
                $recursionHelper->delete($entity, false);
444
            } else {
445
                if ($entity instanceof AbstractStructuralDBElement) {
446
                    $parent = $entity->getParent();
447
448
                    //Move all sub entities to the current parent
449
                    foreach ($entity->getSubelements() as $subelement) {
450
                        $subelement->setParent($parent);
451
                        $entityManager->persist($subelement);
452
                    }
453
                }
454
455
                //Remove current element
456
                $entityManager->remove($entity);
457
            }
458
459
            $this->commentHelper->setMessage($request->request->get('log_comment', null));
460
461
            //Flush changes
462
            $entityManager->flush();
463
464
            $this->addFlash('success', 'attachment_type.deleted');
465
        } else {
466
            $this->addFlash('error', 'csfr_invalid');
467
        }
468
469
        return $this->redirectToRoute($this->route_base.'_new');
470
    }
471
472
    protected function _exportAll(EntityManagerInterface $em, EntityExporter $exporter, Request $request): Response
473
    {
474
        $entity = new $this->entity_class();
475
        $this->denyAccessUnlessGranted('read', $entity);
476
        $entities = $em->getRepository($this->entity_class)->findAll();
477
        return $exporter->exportEntityFromRequest($entities, $request);
478
    }
479
480
    protected function _exportEntity(AbstractNamedDBElement $entity, EntityExporter $exporter, Request $request): \Symfony\Component\HttpFoundation\Response
481
    {
482
        $this->denyAccessUnlessGranted('read', $entity);
483
        return $exporter->exportEntityFromRequest($entity, $request);
484
    }
485
}
486