1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony). |
4
|
|
|
* |
5
|
|
|
* Copyright (C) 2019 - 2022 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
|
|
|
namespace App\EventSubscriber\LogSystem; |
24
|
|
|
|
25
|
|
|
use App\Entity\Attachments\Attachment; |
26
|
|
|
use App\Entity\Base\AbstractDBElement; |
27
|
|
|
use App\Entity\LogSystem\AbstractLogEntry; |
28
|
|
|
use App\Entity\LogSystem\CollectionElementDeleted; |
29
|
|
|
use App\Entity\LogSystem\ElementCreatedLogEntry; |
30
|
|
|
use App\Entity\LogSystem\ElementDeletedLogEntry; |
31
|
|
|
use App\Entity\LogSystem\ElementEditedLogEntry; |
32
|
|
|
use App\Entity\Parameters\AbstractParameter; |
33
|
|
|
use App\Entity\Parts\PartLot; |
34
|
|
|
use App\Entity\PriceInformations\Orderdetail; |
35
|
|
|
use App\Entity\PriceInformations\Pricedetail; |
36
|
|
|
use App\Entity\UserSystem\User; |
37
|
|
|
use App\Services\LogSystem\EventCommentHelper; |
38
|
|
|
use App\Services\LogSystem\EventLogger; |
39
|
|
|
use App\Services\LogSystem\EventUndoHelper; |
40
|
|
|
use Doctrine\Common\EventSubscriber; |
41
|
|
|
use Doctrine\ORM\EntityManagerInterface; |
42
|
|
|
use Doctrine\ORM\Event\OnFlushEventArgs; |
43
|
|
|
use Doctrine\ORM\Event\PostFlushEventArgs; |
44
|
|
|
use Doctrine\ORM\Events; |
45
|
|
|
use Doctrine\Persistence\Event\LifecycleEventArgs; |
46
|
|
|
use Symfony\Component\PropertyAccess\PropertyAccessorInterface; |
47
|
|
|
use Symfony\Component\Serializer\SerializerInterface; |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* This event subscriber write to event log when entities are changed, removed, created. |
51
|
|
|
*/ |
52
|
|
|
class EventLoggerSubscriber implements EventSubscriber |
53
|
|
|
{ |
54
|
|
|
/** |
55
|
|
|
* @var array The given fields will not be saved, because they contain sensitive informations |
56
|
|
|
*/ |
57
|
|
|
protected const FIELD_BLACKLIST = [ |
58
|
|
|
User::class => ['password', 'need_pw_change', 'googleAuthenticatorSecret', 'backupCodes', 'trustedDeviceCookieVersion', 'pw_reset_token', 'backupCodesGenerationDate'], |
59
|
|
|
]; |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* @var array If elements of the given class are deleted, a log for the given fields will be triggered |
63
|
|
|
*/ |
64
|
|
|
protected const TRIGGER_ASSOCIATION_LOG_WHITELIST = [ |
65
|
|
|
PartLot::class => ['part'], |
66
|
|
|
Orderdetail::class => ['part'], |
67
|
|
|
Pricedetail::class => ['orderdetail'], |
68
|
|
|
Attachment::class => ['element'], |
69
|
|
|
AbstractParameter::class => ['element'], |
70
|
|
|
]; |
71
|
|
|
|
72
|
|
|
protected const MAX_STRING_LENGTH = 2000; |
73
|
|
|
|
74
|
|
|
protected EventLogger $logger; |
75
|
|
|
protected SerializerInterface $serializer; |
76
|
|
|
protected EventCommentHelper $eventCommentHelper; |
77
|
|
|
protected EventUndoHelper $eventUndoHelper; |
78
|
|
|
protected bool $save_changed_fields; |
79
|
|
|
protected bool $save_changed_data; |
80
|
|
|
protected bool $save_removed_data; |
81
|
|
|
protected PropertyAccessorInterface $propertyAccessor; |
82
|
|
|
|
83
|
|
|
public function __construct(EventLogger $logger, SerializerInterface $serializer, EventCommentHelper $commentHelper, |
84
|
|
|
bool $save_changed_fields, bool $save_changed_data, bool $save_removed_data, PropertyAccessorInterface $propertyAccessor, |
85
|
|
|
EventUndoHelper $eventUndoHelper) |
86
|
|
|
{ |
87
|
|
|
$this->logger = $logger; |
88
|
|
|
$this->serializer = $serializer; |
89
|
|
|
$this->eventCommentHelper = $commentHelper; |
90
|
|
|
$this->propertyAccessor = $propertyAccessor; |
91
|
|
|
$this->eventUndoHelper = $eventUndoHelper; |
92
|
|
|
|
93
|
|
|
$this->save_changed_fields = $save_changed_fields; |
94
|
|
|
$this->save_changed_data = $save_changed_data; |
95
|
|
|
$this->save_removed_data = $save_removed_data; |
96
|
|
|
} |
97
|
|
|
|
98
|
|
|
public function onFlush(OnFlushEventArgs $eventArgs): void |
99
|
|
|
{ |
100
|
|
|
$em = $eventArgs->getEntityManager(); |
|
|
|
|
101
|
|
|
$uow = $em->getUnitOfWork(); |
102
|
|
|
|
103
|
|
|
/* |
104
|
|
|
* Log changes and deletions of entites. |
105
|
|
|
* We can not log persist here, because the entities do not have IDs yet... |
106
|
|
|
*/ |
107
|
|
|
|
108
|
|
|
foreach ($uow->getScheduledEntityUpdates() as $entity) { |
109
|
|
|
if ($this->validEntity($entity)) { |
110
|
|
|
$this->logElementEdited($entity, $em); |
111
|
|
|
} |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
foreach ($uow->getScheduledEntityDeletions() as $entity) { |
115
|
|
|
if ($this->validEntity($entity)) { |
116
|
|
|
$this->logElementDeleted($entity, $em); |
117
|
|
|
} |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
/* Do not call $uow->computeChangeSets() in this function, only individual entities should be computed! |
121
|
|
|
* Otherwise we will run into very strange issues, that some entity changes are no longer updated! |
122
|
|
|
* This is almost impossible to debug, because it only happens in some cases, and it looks very unrelated to |
123
|
|
|
* this code (which caused the problem and which took me very long time to find out). |
124
|
|
|
* So just do not call $uow->computeChangeSets() here ever, even if it is tempting!! |
125
|
|
|
* If you need to log something from inside here, just call logFromOnFlush() instead of the normal log() function. |
126
|
|
|
*/ |
127
|
|
|
} |
128
|
|
|
|
129
|
|
|
public function postPersist(LifecycleEventArgs $args): void |
130
|
|
|
{ |
131
|
|
|
//Create an log entry, we have to do this post persist, cause we have to know the ID |
132
|
|
|
|
133
|
|
|
/** @var AbstractDBElement $entity */ |
134
|
|
|
$entity = $args->getObject(); |
135
|
|
|
if ($this->validEntity($entity)) { |
136
|
|
|
$log = new ElementCreatedLogEntry($entity); |
137
|
|
|
//Add user comment to log entry |
138
|
|
|
if ($this->eventCommentHelper->isMessageSet()) { |
139
|
|
|
$log->setComment($this->eventCommentHelper->getMessage()); |
140
|
|
|
} |
141
|
|
|
if ($this->eventUndoHelper->isUndo()) { |
142
|
|
|
$undoEvent = $this->eventUndoHelper->getUndoneEvent(); |
143
|
|
|
|
144
|
|
|
$log->setUndoneEvent($undoEvent, $this->eventUndoHelper->getMode()); |
145
|
|
|
|
146
|
|
|
if ($undoEvent instanceof ElementDeletedLogEntry && $undoEvent->getTargetClass() === $log->getTargetClass()) { |
147
|
|
|
$log->setTargetElementID($undoEvent->getTargetID()); |
|
|
|
|
148
|
|
|
} |
149
|
|
|
if ($undoEvent instanceof CollectionElementDeleted && $undoEvent->getDeletedElementClass() === $log->getTargetClass()) { |
150
|
|
|
$log->setTargetElementID($undoEvent->getDeletedElementID()); |
151
|
|
|
} |
152
|
|
|
} |
153
|
|
|
$this->logger->log($log); |
154
|
|
|
} |
155
|
|
|
} |
156
|
|
|
|
157
|
|
|
public function postFlush(PostFlushEventArgs $args): void |
158
|
|
|
{ |
159
|
|
|
$em = $args->getEntityManager(); |
|
|
|
|
160
|
|
|
$uow = $em->getUnitOfWork(); |
161
|
|
|
// If the we have added any ElementCreatedLogEntries added in postPersist, we flush them here. |
162
|
|
|
$uow->computeChangeSets(); |
163
|
|
|
if ($uow->hasPendingInsertions() || !empty($uow->getScheduledEntityUpdates())) { |
164
|
|
|
$em->flush(); |
165
|
|
|
} |
166
|
|
|
|
167
|
|
|
//Clear the message provided by user. |
168
|
|
|
$this->eventCommentHelper->clearMessage(); |
169
|
|
|
$this->eventUndoHelper->clearUndoneEvent(); |
170
|
|
|
} |
171
|
|
|
|
172
|
|
|
/** |
173
|
|
|
* Check if the given element class has restrictions to its fields. |
174
|
|
|
* |
175
|
|
|
* @return bool True if there are restrictions, and further checking is needed |
176
|
|
|
*/ |
177
|
|
|
public function hasFieldRestrictions(AbstractDBElement $element): bool |
178
|
|
|
{ |
179
|
|
|
foreach (array_keys(static::FIELD_BLACKLIST) as $class) { |
180
|
|
|
if (is_a($element, $class)) { |
181
|
|
|
return true; |
182
|
|
|
} |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
return false; |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
/** |
189
|
|
|
* Checks if the field of the given element should be saved (if it is not blacklisted). |
190
|
|
|
*/ |
191
|
|
|
public function shouldFieldBeSaved(AbstractDBElement $element, string $field_name): bool |
192
|
|
|
{ |
193
|
|
|
foreach (static::FIELD_BLACKLIST as $class => $blacklist) { |
194
|
|
|
if (is_a($element, $class) && in_array($field_name, $blacklist, true)) { |
195
|
|
|
return false; |
196
|
|
|
} |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
//By default allow every field. |
200
|
|
|
return true; |
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
public function getSubscribedEvents(): array |
204
|
|
|
{ |
205
|
|
|
return[ |
206
|
|
|
Events::onFlush, |
207
|
|
|
Events::postPersist, |
208
|
|
|
Events::postFlush, |
209
|
|
|
]; |
210
|
|
|
} |
211
|
|
|
|
212
|
|
|
protected function logElementDeleted(AbstractDBElement $entity, EntityManagerInterface $em): void |
213
|
|
|
{ |
214
|
|
|
$log = new ElementDeletedLogEntry($entity); |
215
|
|
|
//Add user comment to log entry |
216
|
|
|
if ($this->eventCommentHelper->isMessageSet()) { |
217
|
|
|
$log->setComment($this->eventCommentHelper->getMessage()); |
218
|
|
|
} |
219
|
|
|
if ($this->eventUndoHelper->isUndo()) { |
220
|
|
|
$log->setUndoneEvent($this->eventUndoHelper->getUndoneEvent(), $this->eventUndoHelper->getMode()); |
221
|
|
|
} |
222
|
|
|
if ($this->save_removed_data) { |
223
|
|
|
//The 4th param is important here, as we delete the element... |
224
|
|
|
$this->saveChangeSet($entity, $log, $em, true); |
225
|
|
|
} |
226
|
|
|
$this->logger->logFromOnFlush($log); |
227
|
|
|
|
228
|
|
|
//Check if we have to log CollectionElementDeleted entries |
229
|
|
|
if ($this->save_changed_data) { |
230
|
|
|
$metadata = $em->getClassMetadata(get_class($entity)); |
231
|
|
|
$mappings = $metadata->getAssociationMappings(); |
232
|
|
|
//Check if class is whitelisted for CollectionElementDeleted entry |
233
|
|
|
foreach (static::TRIGGER_ASSOCIATION_LOG_WHITELIST as $class => $whitelist) { |
234
|
|
|
if (is_a($entity, $class)) { |
235
|
|
|
//Check names |
236
|
|
|
foreach ($mappings as $field => $mapping) { |
237
|
|
|
if (in_array($field, $whitelist, true)) { |
238
|
|
|
$changed = $this->propertyAccessor->getValue($entity, $field); |
239
|
|
|
$log = new CollectionElementDeleted($changed, $mapping['inversedBy'], $entity); |
240
|
|
|
if ($this->eventUndoHelper->isUndo()) { |
241
|
|
|
$log->setUndoneEvent($this->eventUndoHelper->getUndoneEvent(), $this->eventUndoHelper->getMode()); |
242
|
|
|
} |
243
|
|
|
$this->logger->logFromOnFlush($log); |
244
|
|
|
} |
245
|
|
|
} |
246
|
|
|
} |
247
|
|
|
} |
248
|
|
|
} |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
protected function logElementEdited(AbstractDBElement $entity, EntityManagerInterface $em): void |
252
|
|
|
{ |
253
|
|
|
$uow = $em->getUnitOfWork(); |
254
|
|
|
|
255
|
|
|
/* We have to call that here again, so the foreign entity timestamps, that were changed in updateTimestamp |
256
|
|
|
get persisted */ |
257
|
|
|
$changeSet = $uow->getEntityChangeSet($entity); |
258
|
|
|
|
259
|
|
|
//Skip log entry, if only the lastModified field has changed... |
260
|
|
|
if (isset($changeSet['lastModified']) && count($changeSet)) { |
261
|
|
|
return; |
262
|
|
|
} |
263
|
|
|
|
264
|
|
|
$log = new ElementEditedLogEntry($entity); |
265
|
|
|
if ($this->save_changed_data) { |
266
|
|
|
$this->saveChangeSet($entity, $log, $em); |
267
|
|
|
} elseif ($this->save_changed_fields) { |
268
|
|
|
$changed_fields = array_keys($uow->getEntityChangeSet($entity)); |
269
|
|
|
//Remove lastModified field, as this is always changed (gives us no additional info) |
270
|
|
|
$changed_fields = array_diff($changed_fields, ['lastModified']); |
271
|
|
|
$log->setChangedFields($changed_fields); |
272
|
|
|
} |
273
|
|
|
//Add user comment to log entry |
274
|
|
|
if ($this->eventCommentHelper->isMessageSet()) { |
275
|
|
|
$log->setComment($this->eventCommentHelper->getMessage()); |
276
|
|
|
} |
277
|
|
|
if ($this->eventUndoHelper->isUndo()) { |
278
|
|
|
$log->setUndoneEvent($this->eventUndoHelper->getUndoneEvent(), $this->eventUndoHelper->getMode()); |
279
|
|
|
} |
280
|
|
|
$this->logger->logFromOnFlush($log); |
281
|
|
|
} |
282
|
|
|
|
283
|
|
|
/** |
284
|
|
|
* Filter out every forbidden field and return the cleaned array. |
285
|
|
|
*/ |
286
|
|
|
protected function filterFieldRestrictions(AbstractDBElement $element, array $fields): array |
287
|
|
|
{ |
288
|
|
|
unset($fields['lastModified']); |
289
|
|
|
|
290
|
|
|
if (!$this->hasFieldRestrictions($element)) { |
291
|
|
|
return $fields; |
292
|
|
|
} |
293
|
|
|
|
294
|
|
|
return array_filter($fields, function ($value, $key) use ($element) { |
295
|
|
|
//Associative array (save changed data) case |
296
|
|
|
if (is_string($key)) { |
297
|
|
|
return $this->shouldFieldBeSaved($element, $key); |
298
|
|
|
} |
299
|
|
|
|
300
|
|
|
return $this->shouldFieldBeSaved($element, $value); |
301
|
|
|
}, ARRAY_FILTER_USE_BOTH); |
302
|
|
|
} |
303
|
|
|
|
304
|
|
|
protected function saveChangeSet(AbstractDBElement $entity, AbstractLogEntry $logEntry, EntityManagerInterface $em, bool $element_deleted = false): void |
305
|
|
|
{ |
306
|
|
|
$uow = $em->getUnitOfWork(); |
307
|
|
|
|
308
|
|
|
if (!$logEntry instanceof ElementEditedLogEntry && !$logEntry instanceof ElementDeletedLogEntry) { |
309
|
|
|
throw new \InvalidArgumentException('$logEntry must be ElementEditedLogEntry or ElementDeletedLogEntry!'); |
310
|
|
|
} |
311
|
|
|
|
312
|
|
|
if ($element_deleted) { //If the element was deleted we can use getOriginalData to save its content |
313
|
|
|
$old_data = $uow->getOriginalEntityData($entity); |
314
|
|
|
} else { //Otherwise we have to get it from entity changeset |
315
|
|
|
$changeSet = $uow->getEntityChangeSet($entity); |
316
|
|
|
$old_data = array_combine(array_keys($changeSet), array_column($changeSet, 0)); |
317
|
|
|
} |
318
|
|
|
$old_data = $this->filterFieldRestrictions($entity, $old_data); |
319
|
|
|
|
320
|
|
|
//Restrict length of string fields, to save memory... |
321
|
|
|
$old_data = array_map( |
322
|
|
|
static function ($value) { |
323
|
|
|
if (is_string($value)) { |
324
|
|
|
return mb_strimwidth($value, 0, self::MAX_STRING_LENGTH, '...'); |
325
|
|
|
} |
326
|
|
|
|
327
|
|
|
return $value; |
328
|
|
|
}, $old_data); |
329
|
|
|
|
330
|
|
|
$logEntry->setOldData($old_data); |
331
|
|
|
} |
332
|
|
|
|
333
|
|
|
/** |
334
|
|
|
* Check if the given entity can be logged. |
335
|
|
|
* |
336
|
|
|
* @return bool true, if the given entity can be logged |
337
|
|
|
*/ |
338
|
|
|
protected function validEntity(object $entity): bool |
339
|
|
|
{ |
340
|
|
|
//Dont log logentries itself! |
341
|
|
|
return $entity instanceof AbstractDBElement && !$entity instanceof AbstractLogEntry; |
342
|
|
|
} |
343
|
|
|
} |
344
|
|
|
|
This function has been deprecated. The supplier of the function has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.