Passed
Push — master ( 2d425f...eb03d1 )
by Jan
04:57 queued 10s
created

BaseAdminController::_exportAll()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 3
dl 0
loc 9
rs 10
c 0
b 0
f 0
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\Base\AbstractNamedDBElement;
47
use App\Entity\Base\AbstractStructuralDBElement;
48
use App\Entity\UserSystem\User;
49
use App\Exceptions\AttachmentDownloadException;
50
use App\Form\AdminPages\ImportType;
51
use App\Form\AdminPages\MassCreationForm;
52
use App\Services\Attachments\AttachmentManager;
53
use App\Services\Attachments\AttachmentSubmitHandler;
54
use App\Services\EntityExporter;
55
use App\Services\EntityImporter;
56
use App\Services\LogSystem\EventCommentHelper;
57
use App\Services\LogSystem\HistoryHelper;
58
use App\Services\LogSystem\TimeTravel;
59
use App\Services\StructuralElementRecursionHelper;
60
use Doctrine\ORM\EntityManagerInterface;
61
use InvalidArgumentException;
62
use Omines\DataTablesBundle\DataTableFactory;
63
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
64
use Symfony\Component\Form\FormInterface;
65
use Symfony\Component\HttpFoundation\File\UploadedFile;
66
use Symfony\Component\HttpFoundation\RedirectResponse;
67
use Symfony\Component\HttpFoundation\Request;
68
use Symfony\Component\HttpFoundation\Response;
69
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
70
use Symfony\Component\Validator\ConstraintViolationList;
71
use Symfony\Contracts\Translation\TranslatorInterface;
72
73
abstract class BaseAdminController extends AbstractController
74
{
75
    protected $entity_class = '';
76
    protected $form_class = '';
77
    protected $twig_template = '';
78
    protected $route_base = '';
79
    protected $attachment_class = '';
80
81
    protected $passwordEncoder;
82
    protected $translator;
83
    protected $attachmentHelper;
84
    protected $attachmentSubmitHandler;
85
    protected $commentHelper;
86
87
    protected $historyHelper;
88
    protected $timeTravel;
89
    protected $dataTableFactory;
90
91
    public function __construct(TranslatorInterface $translator, UserPasswordEncoderInterface $passwordEncoder,
92
        AttachmentManager $attachmentHelper, AttachmentSubmitHandler $attachmentSubmitHandler,
93
        EventCommentHelper $commentHelper, HistoryHelper $historyHelper, TimeTravel $timeTravel,
94
        DataTableFactory $dataTableFactory)
95
    {
96
        if ('' === $this->entity_class || '' === $this->form_class || '' === $this->twig_template || '' === $this->route_base) {
0 ignored issues
show
introduced by
The condition '' === $this->twig_template is always false.
Loading history...
introduced by
The condition '' === $this->form_class is always false.
Loading history...
introduced by
The condition '' === $this->route_base is always false.
Loading history...
97
            throw new InvalidArgumentException('You have to override the $entity_class, $form_class, $route_base and $twig_template value in your subclasss!');
98
        }
99
100
        if ('' === $this->attachment_class) {
0 ignored issues
show
introduced by
The condition '' === $this->attachment_class is always false.
Loading history...
101
            throw new InvalidArgumentException('You have to override the $attachment_class value in your subclass!');
102
        }
103
104
        $this->translator = $translator;
105
        $this->passwordEncoder = $passwordEncoder;
106
        $this->attachmentHelper = $attachmentHelper;
107
        $this->attachmentSubmitHandler = $attachmentSubmitHandler;
108
        $this->commentHelper = $commentHelper;
109
        $this->historyHelper = $historyHelper;
110
        $this->timeTravel = $timeTravel;
111
        $this->dataTableFactory = $dataTableFactory;
112
    }
113
114
115
    protected function _edit(AbstractNamedDBElement $entity, Request $request, EntityManagerInterface $em, ?string $timestamp = null) : Response
116
    {
117
        $this->denyAccessUnlessGranted('read', $entity);
118
119
120
        $timeTravel_timestamp = null;
121
        if ($timestamp !== null) {
122
            $this->denyAccessUnlessGranted('@tools.timeTravel');
123
            $this->denyAccessUnlessGranted('show_history', $entity);
124
            //If the timestamp only contains numbers interpret it as unix timestamp
125
            if (ctype_digit($timestamp)) {
126
                $timeTravel_timestamp = new \DateTime();
127
                $timeTravel_timestamp->setTimestamp((int) $timestamp);
128
            } else { //Try to parse it via DateTime
129
                $timeTravel_timestamp = new \DateTime($timestamp);
130
            }
131
            $this->timeTravel->revertEntityToTimestamp($entity, $timeTravel_timestamp);
132
        }
133
134
        if ($this->isGranted('show_history', $entity) ) {
135
            $table = $this->dataTableFactory->createFromType(
136
                LogDataTable::class,
137
                [
138
                    'filter_elements' => $this->historyHelper->getAssociatedElements($entity),
139
                    'mode' => 'element_history'
140
                ],
141
                ['pageLength' => 10]
142
            )
143
                ->handleRequest($request);
144
145
            if ($table->isCallback()) {
146
                return $table->getResponse();
147
            }
148
        } else {
149
            $table = null;
150
        }
151
152
        $form = $this->createForm($this->form_class, $entity, [
0 ignored issues
show
Bug introduced by
$this->form_class of type mixed is incompatible with the type string expected by parameter $type of Symfony\Bundle\Framework...ontroller::createForm(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

152
        $form = $this->createForm(/** @scrutinizer ignore-type */ $this->form_class, $entity, [
Loading history...
153
            'attachment_class' => $this->attachment_class,
154
            'disabled' => $timeTravel_timestamp !== null ? true : null
155
        ]);
156
157
        $form->handleRequest($request);
158
        if ($form->isSubmitted() && $form->isValid()) {
159
            //Check if we editing a user and if we need to change the password of it
160
            if ($entity instanceof User && ! empty($form['new_password']->getData())) {
161
                $password = $this->passwordEncoder->encodePassword($entity, $form['new_password']->getData());
162
                $entity->setPassword($password);
163
                //By default the user must change the password afterwards
164
                $entity->setNeedPwChange(true);
165
            }
166
167
            //Upload passed files
168
            $attachments = $form['attachments'];
169
            foreach ($attachments as $attachment) {
170
                /** @var FormInterface $attachment */
171
                $options = [
172
                    'secure_attachment' => $attachment['secureFile']->getData(),
173
                    'download_url' => $attachment['downloadURL']->getData(),
174
                ];
175
176
                try {
177
                    $this->attachmentSubmitHandler->handleFormSubmit($attachment->getData(), $attachment['file']->getData(), $options);
178
                } catch (AttachmentDownloadException $attachmentDownloadException) {
179
                    $this->addFlash(
180
                        'error',
181
                        $this->translator->trans('attachment.download_failed').' '.$attachmentDownloadException->getMessage()
182
                    );
183
                }
184
            }
185
186
            $this->commentHelper->setMessage($form['log_comment']->getData());
187
188
            $em->persist($entity);
189
            $em->flush();
190
            $this->addFlash('success', 'entity.edit_flash');
191
192
            //Rebuild form, so it is based on the updated data. Important for the parent field!
193
            //We can not use dynamic form events here, because the parent entity list is build from database!
194
            $form = $this->createForm($this->form_class, $entity, ['attachment_class' => $this->attachment_class]);
195
        } elseif ($form->isSubmitted() && ! $form->isValid()) {
196
            $this->addFlash('error', 'entity.edit_flash.invalid');
197
        }
198
199
        return $this->render($this->twig_template, [
0 ignored issues
show
Bug introduced by
$this->twig_template of type mixed is incompatible with the type string expected by parameter $view of Symfony\Bundle\Framework...actController::render(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

199
        return $this->render(/** @scrutinizer ignore-type */ $this->twig_template, [
Loading history...
200
            'entity' => $entity,
201
            'form' => $form->createView(),
202
            'attachment_helper' => $this->attachmentHelper,
203
            'route_base' => $this->route_base,
204
            'datatable' => $table,
205
            'timeTravel' => $timeTravel_timestamp
206
        ]);
207
    }
208
209
    protected function _new(Request $request, EntityManagerInterface $em, EntityImporter $importer)
210
    {
211
        /** @var AbstractStructuralDBElement|User $new_entity */
212
        $new_entity = new $this->entity_class();
213
214
        $this->denyAccessUnlessGranted('read', $new_entity);
215
216
        //Basic edit form
217
        $form = $this->createForm($this->form_class, $new_entity, ['attachment_class' => $this->attachment_class]);
0 ignored issues
show
Bug introduced by
$this->form_class of type mixed is incompatible with the type string expected by parameter $type of Symfony\Bundle\Framework...ontroller::createForm(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

217
        $form = $this->createForm(/** @scrutinizer ignore-type */ $this->form_class, $new_entity, ['attachment_class' => $this->attachment_class]);
Loading history...
218
219
        $form->handleRequest($request);
220
221
        if ($form->isSubmitted() && $form->isValid()) {
222
            if ($new_entity instanceof User && ! empty($form['new_password']->getData())) {
223
                $password = $this->passwordEncoder->encodePassword($new_entity, $form['new_password']->getData());
224
                $new_entity->setPassword($password);
225
                //By default the user must change the password afterwards
226
                $new_entity->setNeedPwChange(true);
227
            }
228
229
            //Upload passed files
230
            $attachments = $form['attachments'];
231
            foreach ($attachments as $attachment) {
232
                /** @var FormInterface $attachment */
233
                $options = [
234
                    'secure_attachment' => $attachment['secureFile']->getData(),
235
                    'download_url' => $attachment['downloadURL']->getData(),
236
                ];
237
238
                try {
239
                    $this->attachmentSubmitHandler->handleFormSubmit($attachment->getData(), $attachment['file']->getData(), $options);
240
                } catch (AttachmentDownloadException $attachmentDownloadException) {
241
                    $this->addFlash(
242
                        'error',
243
                        $this->translator->trans('attachment.download_failed').' '.$attachmentDownloadException->getMessage()
244
                    );
245
                }
246
            }
247
248
            $this->commentHelper->setMessage($form['log_comment']->getData());
249
250
            $em->persist($new_entity);
251
            $em->flush();
252
            $this->addFlash('success', 'entity.created_flash');
253
254
            return $this->redirectToRoute($this->route_base.'_edit', ['id' => $new_entity->getID()]);
0 ignored issues
show
Bug introduced by
Are you sure $this->route_base of type mixed can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

254
            return $this->redirectToRoute(/** @scrutinizer ignore-type */ $this->route_base.'_edit', ['id' => $new_entity->getID()]);
Loading history...
255
        }
256
257
        if ($form->isSubmitted() && ! $form->isValid()) {
258
            $this->addFlash('error', 'entity.created_flash.invalid');
259
        }
260
261
        //Import form
262
        $import_form = $this->createForm(ImportType::class, ['entity_class' => $this->entity_class]);
263
        $import_form->handleRequest($request);
264
265
        if ($import_form->isSubmitted() && $import_form->isValid()) {
266
            /** @var UploadedFile $file */
267
            $file = $import_form['file']->getData();
268
            $data = $import_form->getData();
269
270
            $options = [
271
                'parent' => $data['parent'],
272
                'preserve_children' => $data['preserve_children'],
273
                'format' => $data['format'],
274
                'csv_separator' => $data['csv_separator'],
275
            ];
276
277
            $this->commentHelper->setMessage('Import ' . $file->getClientOriginalName());
278
279
280
281
            $errors = $importer->fileToDBEntities($file, $this->entity_class, $options);
0 ignored issues
show
Bug introduced by
$this->entity_class of type mixed is incompatible with the type string expected by parameter $class_name of App\Services\EntityImporter::fileToDBEntities(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

281
            $errors = $importer->fileToDBEntities($file, /** @scrutinizer ignore-type */ $this->entity_class, $options);
Loading history...
282
283
            foreach ($errors as $name => $error) {
284
                /** @var ConstraintViolationList $error */
285
                $this->addFlash('error', $name.':'.$error);
286
            }
287
        }
288
289
        //Mass creation form
290
        $mass_creation_form = $this->createForm(MassCreationForm::class, ['entity_class' => $this->entity_class]);
291
        $mass_creation_form->handleRequest($request);
292
293
        if ($mass_creation_form->isSubmitted() && $mass_creation_form->isValid()) {
294
            $data = $mass_creation_form->getData();
295
296
            //Create entries based on input
297
            $errors = [];
298
            $results = $importer->massCreation($data['lines'], $this->entity_class, $data['parent'], $errors);
0 ignored issues
show
Bug introduced by
$this->entity_class of type mixed is incompatible with the type string expected by parameter $class_name of App\Services\EntityImporter::massCreation(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

298
            $results = $importer->massCreation($data['lines'], /** @scrutinizer ignore-type */ $this->entity_class, $data['parent'], $errors);
Loading history...
299
300
            //Show errors to user:
301
            foreach ($errors as $error) {
302
                $this->addFlash('error', $error['entity']->getFullPath().':'.$error['violations']);
303
            }
304
305
            //Persist valid entities to DB
306
            foreach ($results as $result) {
307
                $em->persist($result);
308
            }
309
            $em->flush();
310
        }
311
312
        return $this->render($this->twig_template, [
0 ignored issues
show
Bug introduced by
$this->twig_template of type mixed is incompatible with the type string expected by parameter $view of Symfony\Bundle\Framework...actController::render(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

312
        return $this->render(/** @scrutinizer ignore-type */ $this->twig_template, [
Loading history...
313
            'entity' => $new_entity,
314
            'form' => $form->createView(),
315
            'import_form' => $import_form->createView(),
316
            'mass_creation_form' => $mass_creation_form->createView(),
317
            'attachment_helper' => $this->attachmentHelper,
318
            'route_base' => $this->route_base,
319
        ]);
320
    }
321
322
    protected function _delete(Request $request, AbstractNamedDBElement $entity, StructuralElementRecursionHelper $recursionHelper) : RedirectResponse
323
    {
324
        $this->denyAccessUnlessGranted('delete', $entity);
325
326
        if ($this->isCsrfTokenValid('delete'.$entity->getId(), $request->request->get('_token'))) {
327
            $entityManager = $this->getDoctrine()->getManager();
328
329
            //Check if we need to remove recursively
330
            if ($entity instanceof AbstractStructuralDBElement && $request->get('delete_recursive', false)) {
331
                $recursionHelper->delete($entity, false);
332
            } else {
333
                if ($entity instanceof AbstractStructuralDBElement) {
334
                    $parent = $entity->getParent();
335
336
                    //Move all sub entities to the current parent
337
                    foreach ($entity->getSubelements() as $subelement) {
338
                        $subelement->setParent($parent);
339
                        $entityManager->persist($subelement);
340
                    }
341
                }
342
343
                //Remove current element
344
                $entityManager->remove($entity);
345
            }
346
347
            $this->commentHelper->setMessage($request->request->get('log_comment', null));
348
349
            //Flush changes
350
            $entityManager->flush();
351
352
            $this->addFlash('success', 'attachment_type.deleted');
353
        } else {
354
            $this->addFlash('error', 'csfr_invalid');
355
        }
356
357
        return $this->redirectToRoute($this->route_base.'_new');
0 ignored issues
show
Bug introduced by
Are you sure $this->route_base of type mixed can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

357
        return $this->redirectToRoute(/** @scrutinizer ignore-type */ $this->route_base.'_new');
Loading history...
358
    }
359
360
    protected function _exportAll(EntityManagerInterface $em, EntityExporter $exporter, Request $request): Response
361
    {
362
        $entity = new $this->entity_class();
363
364
        $this->denyAccessUnlessGranted('read', $entity);
365
366
        $entities = $em->getRepository($this->entity_class)->findAll();
0 ignored issues
show
Bug introduced by
$this->entity_class of type mixed is incompatible with the type string expected by parameter $className of Doctrine\Persistence\Obj...anager::getRepository(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

366
        $entities = $em->getRepository(/** @scrutinizer ignore-type */ $this->entity_class)->findAll();
Loading history...
367
368
        return $exporter->exportEntityFromRequest($entities, $request);
369
    }
370
371
    protected function _exportEntity(AbstractNamedDBElement $entity, EntityExporter $exporter, Request $request): \Symfony\Component\HttpFoundation\Response
372
    {
373
        $this->denyAccessUnlessGranted('read', $entity);
374
375
        return $exporter->exportEntityFromRequest($entity, $request);
376
    }
377
}
378