vendor/wns/security-compliance-suite/src/Logging/Subscriber/CrudSubscriber.php line 76

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace WnsSecurityComplianceSuite\Logging\Subscriber;
  4. use Psr\Log\LoggerInterface;
  5. use Shopware\Core\Defaults;
  6. use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityDeletedEvent;
  7. use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenContainerEvent;
  8. use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenEvent;
  9. use Shopware\Core\Framework\DataAbstractionLayer\Write\EntityExistence;
  10. use Shopware\Core\Framework\Event\NestedEventCollection;
  11. use Shopware\Core\PlatformRequest;
  12. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  13. use Symfony\Component\HttpFoundation\Request;
  14. use Symfony\Component\HttpFoundation\RequestStack;
  15. use Symfony\Component\HttpKernel\Event\RequestEvent;
  16. use Symfony\Component\HttpKernel\KernelEvents;
  17. use Symfony\Contracts\EventDispatcher\Event;
  18. use WnsSecurityComplianceSuite\Logging\DTO\Log;
  19. use WnsSecurityComplianceSuite\Logging\DTO\LogSource;
  20. use WnsSecurityComplianceSuite\Logging\Provider\EntityInformationProvider;
  21. use WnsSecurityComplianceSuite\Logging\Provider\MonitoredEntityProvider;
  22. use WnsSecurityComplianceSuite\Logging\Provider\SourceInfoProvider;
  23. use WnsSecurityComplianceSuite\Logging\Service\ActionLogger;
  24. use WnsSecurityComplianceSuite\Logging\Service\DataSanitizer;
  25. use WnsSecurityComplianceSuite\Logging\Service\RequestInterpreter;
  26. final class CrudSubscriber implements EventSubscriberInterface
  27. {
  28. private RequestStack $requestStack;
  29. private EntityInformationProvider $entityInformationProvider;
  30. private MonitoredEntityProvider $monitoredEntityProvider;
  31. private ActionLogger $actionLogger;
  32. private SourceInfoProvider $sourceInfoProvider;
  33. private DataSanitizer $dataSanitizer;
  34. private LoggerInterface $wscsLogger;
  35. private RequestInterpreter $requestInterpreter;
  36. public function __construct(
  37. RequestStack $requestStack,
  38. EntityInformationProvider $entityInformationProvider,
  39. MonitoredEntityProvider $monitoredEntityProvider,
  40. ActionLogger $actionLogger,
  41. SourceInfoProvider $sourceInfoProvider,
  42. DataSanitizer $dataSanitizer,
  43. LoggerInterface $wscsLogger,
  44. RequestInterpreter $requestInterpreter
  45. ) {
  46. $this->requestStack = $requestStack;
  47. $this->entityInformationProvider = $entityInformationProvider;
  48. $this->monitoredEntityProvider = $monitoredEntityProvider;
  49. $this->actionLogger = $actionLogger;
  50. $this->sourceInfoProvider = $sourceInfoProvider;
  51. $this->dataSanitizer = $dataSanitizer;
  52. $this->wscsLogger = $wscsLogger;
  53. $this->requestInterpreter = $requestInterpreter;
  54. }
  55. public static function getSubscribedEvents()
  56. {
  57. return [
  58. EntityWrittenContainerEvent::class => 'onEntityWritten',
  59. KernelEvents::REQUEST => 'checkForDeleteRequest',
  60. ];
  61. }
  62. public function checkForDeleteRequest(RequestEvent $event): void
  63. {
  64. $request = $event->getRequest();
  65. try {
  66. $entities = $this->requestInterpreter->getRequestedEntityDeletions($request);
  67. if (!empty($entities)) {
  68. $this->entityInformationProvider->preloadEntityData($entities);
  69. }
  70. } catch (\Throwable $e) {
  71. $this->wscsLogger->error('Error getting entity data for request: ' . $e->getMessage(), ['request' => $request->attributes->all()]);
  72. }
  73. }
  74. public function onEntityWritten(EntityWrittenContainerEvent $event): void
  75. {
  76. $salesChannelId = null;
  77. $source = $this->getSource($event);
  78. $eventCollection = $event->getEvents() ?? new NestedEventCollection();
  79. if ($event->getEvents() === null) {
  80. return;
  81. }
  82. /** @var EntityWrittenEvent $innerEvent */
  83. foreach ($eventCollection as $innerEvent) {
  84. $payloads = $innerEvent->getPayloads();
  85. if (empty($payloads) || [[]] === $payloads) {
  86. continue;
  87. }
  88. $entityName = $innerEvent->getEntityName();
  89. if ($this->entityInformationProvider->isTranslationEntity($entityName) === true) {
  90. // Do not log translation deletions
  91. continue;
  92. }
  93. $eventType = $innerEvent->getName();
  94. if (!$this->monitoredEntityProvider->entityMonitored($entityName)) {
  95. if (!$this->monitoredEntityProvider->entityIgnored($entityName)) {
  96. $this->actionLogger->logEntityDebug($entityName, $innerEvent->getPayloads());
  97. }
  98. continue;
  99. }
  100. if ($innerEvent instanceof EntityDeletedEvent) {
  101. // Assume there is only one entity per event. It is not an unreasonable assumption, because Shopware by default creates 1 event per 1 entity deleted, even if many are removed by a single request
  102. $writeResult = $innerEvent->getWriteResults()[0];
  103. $primaryKeys = $writeResult->getExistence() instanceof EntityExistence ? $writeResult->getExistence()->getPrimaryKey() : [];
  104. $entityData = $this->entityInformationProvider->retrieveEntityData($entityName, $primaryKeys);
  105. $detail = [
  106. 'data' => $entityData ?? $writeResult->getPayload(),
  107. ];
  108. } else {
  109. // Regular upsert
  110. $payloads = $this->filterMeaningfulData($entityName, $payloads);
  111. if (empty($payloads)) {
  112. continue;
  113. }
  114. $detail = [
  115. 'data' => $payloads,
  116. ];
  117. }
  118. $log = new Log(
  119. $eventType,
  120. $source,
  121. $this->dataSanitizer->sanitize($detail),
  122. new \DateTimeImmutable()
  123. );
  124. $this->actionLogger->log($log, $salesChannelId);
  125. }
  126. }
  127. /**
  128. * This function filters out data entries that consist only of primary keys and/or createdAt/updatedAt fields
  129. * This is a relatively frequent occurrence, particularly with translations. We do not need to log these if no data is modified.
  130. *
  131. * @param array<mixed> $entities
  132. *
  133. * @return array<mixed>
  134. */
  135. private function filterMeaningfulData(string $entityName, array $entities): array
  136. {
  137. $primaryKeys = $this->entityInformationProvider->getEntityPrimaryKeys($entityName);
  138. $ignoredKeys = array_merge(['createdAt', 'updatedAt'], $primaryKeys);
  139. $filtered = [];
  140. foreach ($entities as $key => $entity) {
  141. if (isset($entity['versionId']) && $entity['versionId'] !== Defaults::LIVE_VERSION) {
  142. // Do not do anything with non-live versions
  143. continue;
  144. }
  145. $entityKeys = array_keys($entity);
  146. $meaningfulKeys = array_diff($entityKeys, $ignoredKeys);
  147. if (\count($meaningfulKeys) > 0) {
  148. $filtered[$key] = $entity;
  149. }
  150. }
  151. return $filtered;
  152. }
  153. private function getSource(EntityWrittenContainerEvent $event): LogSource
  154. {
  155. /** @var Request|null $request */
  156. $request = $this->requestStack->getMainRequest();
  157. try {
  158. $context = isset($request) && $request->get(PlatformRequest::ATTRIBUTE_CONTEXT_OBJECT) !== null ?
  159. $request->get(PlatformRequest::ATTRIBUTE_CONTEXT_OBJECT)
  160. : $event->getContext()
  161. ;
  162. $source = $this->sourceInfoProvider->getSourceDTO($context->getSource());
  163. } catch (\Throwable $e) {
  164. $this->wscsLogger->error('Error getting scope: ' . $e->getMessage(), ['request' => $request]);
  165. throw $e;
  166. }
  167. return $source;
  168. }
  169. }