Passed
Push — master ( e8f83f...f116c2 )
by Jan
05:37
created

EventLoggerSubscriber::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 8
c 0
b 0
f 0
nc 1
nop 8
dl 0
loc 13
rs 10

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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 - 2020 Jan Böhmer (https://github.com/jbtronics)
27
 *
28
 * This program is free software: you can redistribute it and/or modify
29
 * it under the terms of the GNU Affero General Public License as published
30
 * by the Free Software Foundation, either version 3 of the License, or
31
 * (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 Affero General Public License for more details.
37
 *
38
 * You should have received a copy of the GNU Affero General Public License
39
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
40
 */
41
42
namespace App\EventSubscriber\LogSystem;
43
44
use App\Entity\Attachments\Attachment;
45
use App\Entity\Base\AbstractDBElement;
46
use App\Entity\LogSystem\AbstractLogEntry;
47
use App\Entity\LogSystem\CollectionElementDeleted;
48
use App\Entity\LogSystem\ElementCreatedLogEntry;
49
use App\Entity\LogSystem\ElementDeletedLogEntry;
50
use App\Entity\LogSystem\ElementEditedLogEntry;
51
use App\Entity\Parameters\AbstractParameter;
52
use App\Entity\Parts\PartLot;
53
use App\Entity\PriceInformations\Orderdetail;
54
use App\Entity\PriceInformations\Pricedetail;
55
use App\Entity\UserSystem\User;
56
use App\Services\LogSystem\EventCommentHelper;
57
use App\Services\LogSystem\EventLogger;
58
use App\Services\LogSystem\EventUndoHelper;
59
use Doctrine\Common\EventSubscriber;
60
use Doctrine\ORM\EntityManagerInterface;
61
use Doctrine\ORM\Event\OnFlushEventArgs;
62
use Doctrine\ORM\Event\PostFlushEventArgs;
63
use Doctrine\ORM\Events;
64
use Doctrine\Persistence\Event\LifecycleEventArgs;
65
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
66
use Symfony\Component\Serializer\SerializerInterface;
67
68
/**
69
 * This event subscriber write to event log when entities are changed, removed, created.
70
 * @package App\EventSubscriber\LogSystem
71
 */
72
class EventLoggerSubscriber implements EventSubscriber
73
{
74
    /** @var array The given fields will not be saved, because they contain sensitive informations */
75
    protected const FIELD_BLACKLIST = [
76
        User::class => ['password', 'need_pw_change', 'googleAuthenticatorSecret', 'backupCodes', 'trustedDeviceCookieVersion', 'pw_reset_token', 'backupCodesGenerationDate'],
77
    ];
78
79
    /** @var array If elements of the given class are deleted, a log for the given fields will be triggered */
80
    protected const TRIGGER_ASSOCIATION_LOG_WHITELIST = [
81
        PartLot::class => ['part'],
82
        Orderdetail::class => ['part'],
83
        Pricedetail::class => ['orderdetail'],
84
        Attachment::class => ['element'],
85
        AbstractParameter::class => ['element'],
86
    ];
87
88
    protected const MAX_STRING_LENGTH = 2000;
89
90
    protected $logger;
91
    protected $serializer;
92
    protected $eventCommentHelper;
93
    protected $eventUndoHelper;
94
    protected $save_changed_fields;
95
    protected $save_changed_data;
96
    protected $save_removed_data;
97
    protected $propertyAccessor;
98
99
    public function __construct(EventLogger $logger, SerializerInterface $serializer, EventCommentHelper $commentHelper,
100
        bool $save_changed_fields, bool $save_changed_data, bool $save_removed_data, PropertyAccessorInterface $propertyAccessor,
101
        EventUndoHelper $eventUndoHelper)
102
    {
103
        $this->logger = $logger;
104
        $this->serializer = $serializer;
105
        $this->eventCommentHelper = $commentHelper;
106
        $this->propertyAccessor = $propertyAccessor;
107
        $this->eventUndoHelper = $eventUndoHelper;
108
109
        $this->save_changed_fields = $save_changed_fields;
110
        $this->save_changed_data = $save_changed_data;
111
        $this->save_removed_data = $save_removed_data;
112
    }
113
114
    public function onFlush(OnFlushEventArgs $eventArgs): void
115
    {
116
        $em = $eventArgs->getEntityManager();
117
        $uow = $em->getUnitOfWork();
118
119
        /*
120
         * Log changes and deletions of entites.
121
         * We can not log persist here, because the entities do not have IDs yet...
122
         */
123
124
        foreach ($uow->getScheduledEntityUpdates() as $entity) {
125
            if ($this->validEntity($entity)) {
126
                $this->logElementEdited($entity, $em);
127
            }
128
        }
129
130
        foreach ($uow->getScheduledEntityDeletions() as $entity) {
131
            if ($this->validEntity($entity)) {
132
                $this->logElementDeleted($entity, $em);
133
            }
134
        }
135
136
        $uow->computeChangeSets();
137
    }
138
139
    public function postPersist(LifecycleEventArgs $args): void
140
    {
141
        //Create an log entry, we have to do this post persist, cause we have to know the ID
142
143
        /** @var AbstractDBElement $entity */
144
        $entity = $args->getObject();
145
        if ($this->validEntity($entity)) {
146
            $log = new ElementCreatedLogEntry($entity);
147
            //Add user comment to log entry
148
            if ($this->eventCommentHelper->isMessageSet()) {
149
                $log->setComment($this->eventCommentHelper->getMessage());
150
            }
151
            if ($this->eventUndoHelper->isUndo()) {
152
                $undoEvent = $this->eventUndoHelper->getUndoneEvent();
153
154
                $log->setUndoneEvent($undoEvent, $this->eventUndoHelper->getMode());
155
156
                if ($undoEvent instanceof ElementDeletedLogEntry && $undoEvent->getTargetClass() === $log->getTargetClass()) {
157
                    $log->setTargetElementID($undoEvent->getTargetID());
0 ignored issues
show
Bug introduced by
It seems like $undoEvent->getTargetID() can also be of type null; however, parameter $target_id of App\Entity\LogSystem\Abs...y::setTargetElementID() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

157
                    $log->setTargetElementID(/** @scrutinizer ignore-type */ $undoEvent->getTargetID());
Loading history...
158
                }
159
                if ($undoEvent instanceof CollectionElementDeleted && $undoEvent->getDeletedElementClass() === $log->getTargetClass()) {
160
                    $log->setTargetElementID($undoEvent->getDeletedElementID());
161
                }
162
            }
163
            $this->logger->log($log);
164
        }
165
    }
166
167
    public function postFlush(PostFlushEventArgs $args): void
168
    {
169
        $em = $args->getEntityManager();
170
        $uow = $em->getUnitOfWork();
171
        // If the we have added any ElementCreatedLogEntries added in postPersist, we flush them here.
172
        if ($uow->hasPendingInsertions()) {
173
            $em->flush();
174
        }
175
176
        //Clear the message provided by user.
177
        $this->eventCommentHelper->clearMessage();
178
        $this->eventUndoHelper->clearUndoneEvent();
179
    }
180
181
    /**
182
     * Check if the given element class has restrictions to its fields.
183
     *
184
     * @return bool True if there are restrictions, and further checking is needed
185
     */
186
    public function hasFieldRestrictions(AbstractDBElement $element): bool
187
    {
188
        foreach (array_keys(static::FIELD_BLACKLIST) as $class) {
189
            if (is_a($element, $class)) {
190
                return true;
191
            }
192
        }
193
194
        return false;
195
    }
196
197
    /**
198
     * Checks if the field of the given element should be saved (if it is not blacklisted).
199
     *
200
     * @return bool
201
     */
202
    public function shouldFieldBeSaved(AbstractDBElement $element, string $field_name): bool
203
    {
204
        foreach (static::FIELD_BLACKLIST as $class => $blacklist) {
205
            if (is_a($element, $class) && in_array($field_name, $blacklist, true)) {
206
                return false;
207
            }
208
        }
209
210
        //By default allow every field.
211
        return true;
212
    }
213
214
    public function getSubscribedEvents()
215
    {
216
        return[
217
            Events::onFlush,
218
            Events::postPersist,
219
            Events::postFlush,
220
        ];
221
    }
222
223
    protected function logElementDeleted(AbstractDBElement $entity, EntityManagerInterface $em): void
224
    {
225
        $log = new ElementDeletedLogEntry($entity);
226
        //Add user comment to log entry
227
        if ($this->eventCommentHelper->isMessageSet()) {
228
            $log->setComment($this->eventCommentHelper->getMessage());
229
        }
230
        if ($this->eventUndoHelper->isUndo()) {
231
            $log->setUndoneEvent($this->eventUndoHelper->getUndoneEvent(), $this->eventUndoHelper->getMode());
232
        }
233
        if ($this->save_removed_data) {
234
            //The 4th param is important here, as we delete the element...
235
            $this->saveChangeSet($entity, $log, $em, true);
236
        }
237
        $this->logger->log($log);
238
239
        //Check if we have to log CollectionElementDeleted entries
240
        if ($this->save_changed_data) {
241
            $metadata = $em->getClassMetadata(get_class($entity));
242
            $mappings = $metadata->getAssociationMappings();
0 ignored issues
show
Bug introduced by
The method getAssociationMappings() does not exist on Doctrine\Persistence\Mapping\ClassMetadata. Did you maybe mean getAssociationNames()? ( Ignorable by Annotation )

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

242
            /** @scrutinizer ignore-call */ 
243
            $mappings = $metadata->getAssociationMappings();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
243
            //Check if class is whitelisted for CollectionElementDeleted entry
244
            foreach (static::TRIGGER_ASSOCIATION_LOG_WHITELIST as $class => $whitelist) {
245
                if (is_a($entity, $class)) {
246
                    //Check names
247
                    foreach ($mappings as $field => $mapping) {
248
                        if (in_array($field, $whitelist, true)) {
249
                            $changed = $this->propertyAccessor->getValue($entity, $field);
250
                            $log = new CollectionElementDeleted($changed, $mapping['inversedBy'], $entity);
251
                            if ($this->eventUndoHelper->isUndo()) {
252
                                $log->setUndoneEvent($this->eventUndoHelper->getUndoneEvent(), $this->eventUndoHelper->getMode());
253
                            }
254
                            $this->logger->log($log);
255
                        }
256
                    }
257
                }
258
            }
259
        }
260
    }
261
262
    protected function logElementEdited(AbstractDBElement $entity, EntityManagerInterface $em): void
263
    {
264
        $uow = $em->getUnitOfWork();
265
266
        $log = new ElementEditedLogEntry($entity);
267
        if ($this->save_changed_data) {
268
            $this->saveChangeSet($entity, $log, $em);
269
        } elseif ($this->save_changed_fields) {
270
            $changed_fields = array_keys($uow->getEntityChangeSet($entity));
271
            //Remove lastModified field, as this is always changed (gives us no additional info)
272
            $changed_fields = array_diff($changed_fields, ['lastModified']);
273
            $log->setChangedFields($changed_fields);
274
        }
275
        //Add user comment to log entry
276
        if ($this->eventCommentHelper->isMessageSet()) {
277
            $log->setComment($this->eventCommentHelper->getMessage());
278
        }
279
        if ($this->eventUndoHelper->isUndo()) {
280
            $log->setUndoneEvent($this->eventUndoHelper->getUndoneEvent(), $this->eventUndoHelper->getMode());
281
        }
282
        $this->logger->log($log);
283
    }
284
285
    /**
286
     * Filter out every forbidden field and return the cleaned array.
287
     *
288
     * @return array
289
     */
290
    protected function filterFieldRestrictions(AbstractDBElement $element, array $fields): array
291
    {
292
        unset($fields['lastModified']);
293
294
        if (! $this->hasFieldRestrictions($element)) {
295
            return $fields;
296
        }
297
298
        return array_filter($fields, function ($value, $key) use ($element) {
299
            //Associative array (save changed data) case
300
            if (is_string($key)) {
301
                return $this->shouldFieldBeSaved($element, $key);
302
            }
303
304
            return $this->shouldFieldBeSaved($element, $value);
305
        }, ARRAY_FILTER_USE_BOTH);
306
    }
307
308
    protected function saveChangeSet(AbstractDBElement $entity, AbstractLogEntry $logEntry, EntityManagerInterface $em, bool $element_deleted = false): void
309
    {
310
        $uow = $em->getUnitOfWork();
311
312
        if (! $logEntry instanceof ElementEditedLogEntry && ! $logEntry instanceof ElementDeletedLogEntry) {
313
            throw new \InvalidArgumentException('$logEntry must be ElementEditedLogEntry or ElementDeletedLogEntry!');
314
        }
315
316
        if ($element_deleted) { //If the element was deleted we can use getOriginalData to save its content
317
            $old_data = $uow->getOriginalEntityData($entity);
318
        } else { //Otherwise we have to get it from entity changeset
319
            $changeSet = $uow->getEntityChangeSet($entity);
320
            $old_data = array_combine(array_keys($changeSet), array_column($changeSet, 0));
321
        }
322
        $old_data = $this->filterFieldRestrictions($entity, $old_data);
0 ignored issues
show
Bug introduced by
It seems like $old_data can also be of type false; however, parameter $fields of App\EventSubscriber\LogS...lterFieldRestrictions() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

322
        $old_data = $this->filterFieldRestrictions($entity, /** @scrutinizer ignore-type */ $old_data);
Loading history...
323
324
        //Restrict length of string fields, to save memory...
325
        $old_data = array_map(function ($value) {
326
            if (is_string($value)) {
327
                return mb_strimwidth($value, 0, self::MAX_STRING_LENGTH, '...');
328
            }
329
330
            return $value;
331
        }, $old_data);
332
333
        $logEntry->setOldData($old_data);
334
    }
335
336
    /**
337
     * Check if the given entity can be logged.
338
     *
339
     * @return bool True, if the given entity can be logged.
340
     */
341
    protected function validEntity(object $entity): bool
342
    {
343
        //Dont log logentries itself!
344
        if ($entity instanceof AbstractDBElement && ! $entity instanceof AbstractLogEntry) {
345
            return true;
346
        }
347
348
        return false;
349
    }
350
}
351