platform/src/Core/Framework/Webhook/WebhookDispatcher.php line 93

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\Framework\Webhook;
  3. use Doctrine\DBAL\Connection;
  4. use GuzzleHttp\Client;
  5. use GuzzleHttp\Pool;
  6. use GuzzleHttp\Psr7\Request;
  7. use Shopware\Core\Framework\App\AppEntity;
  8. use Shopware\Core\Framework\App\AppLocaleProvider;
  9. use Shopware\Core\Framework\App\Event\AppChangedEvent;
  10. use Shopware\Core\Framework\App\Event\AppDeletedEvent;
  11. use Shopware\Core\Framework\App\Exception\AppUrlChangeDetectedException;
  12. use Shopware\Core\Framework\App\Hmac\Guzzle\AuthMiddleware;
  13. use Shopware\Core\Framework\App\Hmac\RequestSigner;
  14. use Shopware\Core\Framework\App\ShopId\ShopIdProvider;
  15. use Shopware\Core\Framework\Context;
  16. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
  17. use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenContainerEvent;
  18. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  19. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  20. use Shopware\Core\Framework\Event\BusinessEventInterface;
  21. use Shopware\Core\Framework\Uuid\Uuid;
  22. use Shopware\Core\Framework\Webhook\EventLog\WebhookEventLogDefinition;
  23. use Shopware\Core\Framework\Webhook\Hookable\HookableEventFactory;
  24. use Shopware\Core\Framework\Webhook\Message\WebhookEventMessage;
  25. use Symfony\Component\DependencyInjection\ContainerInterface;
  26. use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
  27. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  28. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  29. use Symfony\Component\Messenger\MessageBusInterface;
  30. class WebhookDispatcher implements EventDispatcherInterface
  31. {
  32.     private EventDispatcherInterface $dispatcher;
  33.     private Connection $connection;
  34.     private ?WebhookCollection $webhooks null;
  35.     private Client $guzzle;
  36.     private string $shopUrl;
  37.     private ContainerInterface $container;
  38.     private array $privileges = [];
  39.     private HookableEventFactory $eventFactory;
  40.     private string $shopwareVersion;
  41.     private MessageBusInterface $bus;
  42.     private bool $isAdminWorkerEnabled;
  43.     /**
  44.      * @psalm-suppress ContainerDependency
  45.      */
  46.     public function __construct(
  47.         EventDispatcherInterface $dispatcher,
  48.         Connection $connection,
  49.         Client $guzzle,
  50.         string $shopUrl,
  51.         ContainerInterface $container,
  52.         HookableEventFactory $eventFactory,
  53.         string $shopwareVersion,
  54.         MessageBusInterface $bus,
  55.         bool $isAdminWorkerEnabled
  56.     ) {
  57.         $this->dispatcher $dispatcher;
  58.         $this->connection $connection;
  59.         $this->guzzle $guzzle;
  60.         $this->shopUrl $shopUrl;
  61.         // inject container, so we can later get the ShopIdProvider and the webhook repository
  62.         // ShopIdProvider, AppLocaleProvider and webhook repository can not be injected directly as it would lead to a circular reference
  63.         $this->container $container;
  64.         $this->eventFactory $eventFactory;
  65.         $this->shopwareVersion $shopwareVersion;
  66.         $this->bus $bus;
  67.         $this->isAdminWorkerEnabled $isAdminWorkerEnabled;
  68.     }
  69.     /**
  70.      * @template TEvent of object
  71.      *
  72.      * @param TEvent $event
  73.      *
  74.      * @return TEvent
  75.      */
  76.     public function dispatch($event, ?string $eventName null): object
  77.     {
  78.         $event $this->dispatcher->dispatch($event$eventName);
  79.         foreach ($this->eventFactory->createHookablesFor($event) as $hookable) {
  80.             $context Context::createDefaultContext();
  81.             if ($event instanceof BusinessEventInterface || $event instanceof AppChangedEvent || $event instanceof EntityWrittenContainerEvent) {
  82.                 $context $event->getContext();
  83.             }
  84.             $this->callWebhooks($hookable->getName(), $hookable$context);
  85.         }
  86.         // always return the original event and never our wrapped events
  87.         // this would lead to problems in the `BusinessEventDispatcher` from core
  88.         return $event;
  89.     }
  90.     /**
  91.      * @param string   $eventName
  92.      * @param callable $listener
  93.      * @param int      $priority
  94.      */
  95.     public function addListener($eventName$listener$priority 0): void
  96.     {
  97.         $this->dispatcher->addListener($eventName$listener$priority);
  98.     }
  99.     public function addSubscriber(EventSubscriberInterface $subscriber): void
  100.     {
  101.         $this->dispatcher->addSubscriber($subscriber);
  102.     }
  103.     /**
  104.      * @param string   $eventName
  105.      * @param callable $listener
  106.      */
  107.     public function removeListener($eventName$listener): void
  108.     {
  109.         $this->dispatcher->removeListener($eventName$listener);
  110.     }
  111.     public function removeSubscriber(EventSubscriberInterface $subscriber): void
  112.     {
  113.         $this->dispatcher->removeSubscriber($subscriber);
  114.     }
  115.     /**
  116.      * @param string|null $eventName
  117.      */
  118.     public function getListeners($eventName null): array
  119.     {
  120.         return $this->dispatcher->getListeners($eventName);
  121.     }
  122.     /**
  123.      * @param string   $eventName
  124.      * @param callable $listener
  125.      */
  126.     public function getListenerPriority($eventName$listener): ?int
  127.     {
  128.         return $this->dispatcher->getListenerPriority($eventName$listener);
  129.     }
  130.     /**
  131.      * @param string|null $eventName
  132.      */
  133.     public function hasListeners($eventName null): bool
  134.     {
  135.         return $this->dispatcher->hasListeners($eventName);
  136.     }
  137.     public function clearInternalWebhookCache(): void
  138.     {
  139.         $this->webhooks null;
  140.     }
  141.     public function clearInternalPrivilegesCache(): void
  142.     {
  143.         $this->privileges = [];
  144.     }
  145.     private function callWebhooks(string $eventNameHookable $eventContext $context): void
  146.     {
  147.         /** @var WebhookCollection $webhooksForEvent */
  148.         $webhooksForEvent $this->getWebhooks()->filterForEvent($eventName);
  149.         if ($webhooksForEvent->count() === 0) {
  150.             return;
  151.         }
  152.         $payload $event->getWebhookPayload();
  153.         $affectedRoleIds $webhooksForEvent->getAclRoleIdsAsBinary();
  154.         $requests = [];
  155.         $languageId $context->getLanguageId();
  156.         $userLocale $this->getAppLocaleProvider()->getLocaleFromContext($context);
  157.         foreach ($webhooksForEvent as $webhook) {
  158.             if ($webhook->getApp() !== null && !$this->isEventDispatchingAllowed($webhook->getApp(), $event$affectedRoleIds)) {
  159.                 continue;
  160.             }
  161.             $payload = ['data' => ['payload' => $payload]];
  162.             $payload['source']['url'] = $this->shopUrl;
  163.             $payload['data']['event'] = $eventName;
  164.             if ($webhook->getApp() !== null) {
  165.                 $payload['source']['appVersion'] = $webhook->getApp()->getVersion();
  166.                 $shopIdProvider $this->getShopIdProvider();
  167.                 try {
  168.                     $shopId $shopIdProvider->getShopId();
  169.                 } catch (AppUrlChangeDetectedException $e) {
  170.                     continue;
  171.                 }
  172.                 $payload['source']['shopId'] = $shopId;
  173.             }
  174.             if ($this->isAdminWorkerEnabled) {
  175.                 /** @var string $jsonPayload */
  176.                 $jsonPayload json_encode($payload);
  177.                 $request = new Request(
  178.                     'POST',
  179.                     $webhook->getUrl(),
  180.                     [
  181.                         'Content-Type' => 'application/json',
  182.                         'sw-version' => $this->shopwareVersion,
  183.                         AuthMiddleware::SHOPWARE_CONTEXT_LANGUAGE => $languageId,
  184.                         AuthMiddleware::SHOPWARE_USER_LANGUAGE => $userLocale,
  185.                     ],
  186.                     $jsonPayload
  187.                 );
  188.                 if ($webhook->getApp() !== null && $webhook->getApp()->getAppSecret() !== null) {
  189.                     $request $request->withHeader(
  190.                         RequestSigner::SHOPWARE_SHOP_SIGNATURE,
  191.                         (new RequestSigner())->signPayload($jsonPayload$webhook->getApp()->getAppSecret())
  192.                     );
  193.                 }
  194.                 $requests[] = $request;
  195.             } else {
  196.                 $webhookEventId Uuid::randomHex();
  197.                 $appId $webhook->getApp() !== null $webhook->getApp()->getId() : null;
  198.                 $secret $webhook->getApp() !== null $webhook->getApp()->getAppSecret() : null;
  199.                 $webhookEventMessage = new WebhookEventMessage($webhookEventId$payload$appId$webhook->getId(), $this->shopwareVersion$webhook->getUrl(), $secret$languageId$userLocale);
  200.                 if (!$this->container->has('webhook_event_log.repository')) {
  201.                     throw new ServiceNotFoundException('webhook_event_log.repository');
  202.                 }
  203.                 /** @var EntityRepositoryInterface $webhookEventLogRepository */
  204.                 $webhookEventLogRepository $this->container->get('webhook_event_log.repository');
  205.                 $webhookEventLogRepository->create([
  206.                     [
  207.                         'id' => $webhookEventId,
  208.                         'appName' => $webhook->getApp() !== null $webhook->getApp()->getName() : null,
  209.                         'deliveryStatus' => WebhookEventLogDefinition::STATUS_QUEUED,
  210.                         'webhookName' => $webhook->getName(),
  211.                         'eventName' => $webhook->getEventName(),
  212.                         'appVersion' => $webhook->getApp() !== null $webhook->getApp()->getVersion() : null,
  213.                         'url' => $webhook->getUrl(),
  214.                         'serializedWebhookMessage' => serialize($webhookEventMessage),
  215.                     ],
  216.                 ], Context::createDefaultContext());
  217.                 $this->bus->dispatch($webhookEventMessage);
  218.             }
  219.         }
  220.         if ($this->isAdminWorkerEnabled) {
  221.             $pool = new Pool($this->guzzle$requests);
  222.             $pool->promise()->wait();
  223.         }
  224.     }
  225.     private function getWebhooks(): WebhookCollection
  226.     {
  227.         if ($this->webhooks) {
  228.             return $this->webhooks;
  229.         }
  230.         $criteria = new Criteria();
  231.         $criteria->addFilter(new EqualsFilter('active'true));
  232.         $criteria->addAssociation('app');
  233.         if (!$this->container->has('webhook.repository')) {
  234.             throw new ServiceNotFoundException('webhook.repository');
  235.         }
  236.         /** @var WebhookCollection $webhooks */
  237.         $webhooks $this->container->get('webhook.repository')->search($criteriaContext::createDefaultContext())->getEntities();
  238.         return $this->webhooks $webhooks;
  239.     }
  240.     private function isEventDispatchingAllowed(AppEntity $appHookable $event, array $affectedRoles): bool
  241.     {
  242.         // Only app lifecycle hooks can be received if app is deactivated
  243.         if (!$app->isActive() && !($event instanceof AppChangedEvent || $event instanceof AppDeletedEvent)) {
  244.             return false;
  245.         }
  246.         if (!($this->privileges[$event->getName()] ?? null)) {
  247.             $this->loadPrivileges($event->getName(), $affectedRoles);
  248.         }
  249.         $privileges $this->privileges[$event->getName()][$app->getAclRoleId()]
  250.             ?? new AclPrivilegeCollection([]);
  251.         if (!$event->isAllowed($app->getId(), $privileges)) {
  252.             return false;
  253.         }
  254.         return true;
  255.     }
  256.     private function loadPrivileges(string $eventName, array $affectedRoleIds): void
  257.     {
  258.         $roles $this->connection->fetchAll('
  259.             SELECT `id`, `privileges`
  260.             FROM `acl_role`
  261.             WHERE `id` IN (:aclRoleIds)
  262.         ', ['aclRoleIds' => $affectedRoleIds], ['aclRoleIds' => Connection::PARAM_STR_ARRAY]);
  263.         if (!$roles) {
  264.             $this->privileges[$eventName] = [];
  265.         }
  266.         foreach ($roles as $privilege) {
  267.             $this->privileges[$eventName][Uuid::fromBytesToHex($privilege['id'])]
  268.                 = new AclPrivilegeCollection(json_decode($privilege['privileges'], true));
  269.         }
  270.     }
  271.     private function getShopIdProvider(): ShopIdProvider
  272.     {
  273.         if (!$this->container->has(ShopIdProvider::class)) {
  274.             throw new ServiceNotFoundException(ShopIdProvider::class);
  275.         }
  276.         return $this->container->get(ShopIdProvider::class);
  277.     }
  278.     private function getAppLocaleProvider(): AppLocaleProvider
  279.     {
  280.         if (!$this->container->has(AppLocaleProvider::class)) {
  281.             throw new ServiceNotFoundException(AppLocaleProvider::class);
  282.         }
  283.         return $this->container->get(AppLocaleProvider::class);
  284.     }
  285. }