vendor/shopware/platform/src/Core/Framework/Webhook/WebhookDispatcher.php line 91

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\Event\AppChangedEvent;
  9. use Shopware\Core\Framework\App\Event\AppDeletedEvent;
  10. use Shopware\Core\Framework\App\Exception\AppUrlChangeDetectedException;
  11. use Shopware\Core\Framework\App\ShopId\ShopIdProvider;
  12. use Shopware\Core\Framework\Context;
  13. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  14. use Shopware\Core\Framework\Uuid\Uuid;
  15. use Shopware\Core\Framework\Webhook\Hookable\HookableEventFactory;
  16. use Symfony\Component\DependencyInjection\ContainerInterface;
  17. use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
  18. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  19. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  20. class WebhookDispatcher implements EventDispatcherInterface
  21. {
  22.     /**
  23.      * @var EventDispatcherInterface
  24.      */
  25.     private $dispatcher;
  26.     /**
  27.      * @var Connection
  28.      */
  29.     private $connection;
  30.     /**
  31.      * @var WebhookCollection|null
  32.      */
  33.     private $webhooks;
  34.     /**
  35.      * @var Client
  36.      */
  37.     private $guzzle;
  38.     /**
  39.      * @var string
  40.      */
  41.     private $shopUrl;
  42.     /**
  43.      * @var ContainerInterface
  44.      */
  45.     private $container;
  46.     /**
  47.      * @var array
  48.      */
  49.     private $privileges = [];
  50.     /**
  51.      * @var HookableEventFactory
  52.      */
  53.     private $eventFactory;
  54.     /**
  55.      * @psalm-suppress ContainerDependency
  56.      */
  57.     public function __construct(
  58.         EventDispatcherInterface $dispatcher,
  59.         Connection $connection,
  60.         Client $guzzle,
  61.         string $shopUrl,
  62.         ContainerInterface $container,
  63.         HookableEventFactory $eventFactory
  64.     ) {
  65.         $this->dispatcher $dispatcher;
  66.         $this->connection $connection;
  67.         $this->guzzle $guzzle;
  68.         $this->shopUrl $shopUrl;
  69.         // inject container, so we can later get the ShopIdProvider and the webhook repository
  70.         // ShopIdProvider and webhook repository can not be injected directly as it would lead to a circular reference
  71.         $this->container $container;
  72.         $this->eventFactory $eventFactory;
  73.     }
  74.     /**
  75.      * @param object $event
  76.      */
  77.     public function dispatch($event, ?string $eventName null): object
  78.     {
  79.         $event $this->dispatcher->dispatch($event$eventName);
  80.         foreach ($this->eventFactory->createHookablesFor($event) as $hookable) {
  81.             $this->callWebhooks($hookable->getName(), $hookable);
  82.         }
  83.         // always return the original event and never our wrapped events
  84.         // this would lead to problems in the `BusinessEventDispatcher` from core
  85.         return $event;
  86.     }
  87.     /**
  88.      * @param string   $eventName
  89.      * @param callable $listener
  90.      * @param int      $priority
  91.      */
  92.     public function addListener($eventName$listener$priority 0): void
  93.     {
  94.         $this->dispatcher->addListener($eventName$listener$priority);
  95.     }
  96.     public function addSubscriber(EventSubscriberInterface $subscriber): void
  97.     {
  98.         $this->dispatcher->addSubscriber($subscriber);
  99.     }
  100.     /**
  101.      * @param string   $eventName
  102.      * @param callable $listener
  103.      */
  104.     public function removeListener($eventName$listener): void
  105.     {
  106.         $this->dispatcher->removeListener($eventName$listener);
  107.     }
  108.     public function removeSubscriber(EventSubscriberInterface $subscriber): void
  109.     {
  110.         $this->dispatcher->removeSubscriber($subscriber);
  111.     }
  112.     /**
  113.      * @param string|null $eventName
  114.      */
  115.     public function getListeners($eventName null): array
  116.     {
  117.         return $this->dispatcher->getListeners($eventName);
  118.     }
  119.     /**
  120.      * @param string   $eventName
  121.      * @param callable $listener
  122.      */
  123.     public function getListenerPriority($eventName$listener): ?int
  124.     {
  125.         return $this->dispatcher->getListenerPriority($eventName$listener);
  126.     }
  127.     /**
  128.      * @param string|null $eventName
  129.      */
  130.     public function hasListeners($eventName null): bool
  131.     {
  132.         return $this->dispatcher->hasListeners($eventName);
  133.     }
  134.     public function clearInternalWebhookCache(): void
  135.     {
  136.         $this->webhooks null;
  137.     }
  138.     public function clearInternalPrivilegesCache(): void
  139.     {
  140.         $this->privileges = [];
  141.     }
  142.     private function callWebhooks(string $eventNameHookable $event): void
  143.     {
  144.         /** @var WebhookCollection $webhooksForEvent */
  145.         $webhooksForEvent $this->getWebhooks()->filterForEvent($eventName);
  146.         if ($webhooksForEvent->count() === 0) {
  147.             return;
  148.         }
  149.         $payload $event->getWebhookPayload();
  150.         $affectedRoleIds $webhooksForEvent->getAclRoleIdsAsBinary();
  151.         $requests = [];
  152.         foreach ($webhooksForEvent as $webhook) {
  153.             if ($webhook->getApp()) {
  154.                 if (!$this->isEventDispatchingAllowed($webhook->getApp(), $event$affectedRoleIds)) {
  155.                     continue;
  156.                 }
  157.             }
  158.             $payload = ['data' => ['payload' => $payload]];
  159.             $payload['source']['url'] = $this->shopUrl;
  160.             $payload['data']['event'] = $eventName;
  161.             if ($webhook->getApp()) {
  162.                 $payload['source']['appVersion'] = $webhook->getApp()->getVersion();
  163.                 $shopIdProvider $this->getShopIdProvider();
  164.                 try {
  165.                     $shopId $shopIdProvider->getShopId();
  166.                 } catch (AppUrlChangeDetectedException $e) {
  167.                     continue;
  168.                 }
  169.                 $payload['source']['shopId'] = $shopId;
  170.             }
  171.             /** @var string $jsonPayload */
  172.             $jsonPayload json_encode($payload);
  173.             $request = new Request(
  174.                 'POST',
  175.                 $webhook->getUrl(),
  176.                 [
  177.                     'Content-Type' => 'application/json',
  178.                 ],
  179.                 $jsonPayload
  180.             );
  181.             if ($webhook->getApp() && $webhook->getApp()->getAppSecret()) {
  182.                 $request $request->withHeader(
  183.                     'shopware-shop-signature',
  184.                     hash_hmac('sha256'$jsonPayload$webhook->getApp()->getAppSecret())
  185.                 );
  186.             }
  187.             $requests[] = $request;
  188.         }
  189.         $pool = new Pool($this->guzzle$requests);
  190.         $pool->promise()->wait();
  191.     }
  192.     private function getWebhooks(): WebhookCollection
  193.     {
  194.         if ($this->webhooks) {
  195.             return $this->webhooks;
  196.         }
  197.         $criteria = new Criteria();
  198.         $criteria->addAssociation('app');
  199.         if (!$this->container->has('webhook.repository')) {
  200.             throw new ServiceNotFoundException('webhook.repository');
  201.         }
  202.         /** @var WebhookCollection $webhooks */
  203.         $webhooks $this->container->get('webhook.repository')->search($criteriaContext::createDefaultContext())->getEntities();
  204.         return $this->webhooks $webhooks;
  205.     }
  206.     private function isEventDispatchingAllowed(AppEntity $appHookable $event, array $affectedRoles): bool
  207.     {
  208.         // Only app lifecycle hooks can be received if app is deactivated
  209.         if (!$app->isActive() && !($event instanceof AppChangedEvent || $event instanceof AppDeletedEvent)) {
  210.             return false;
  211.         }
  212.         if (!($this->privileges[$event->getName()] ?? null)) {
  213.             $this->loadPrivileges($event->getName(), $affectedRoles);
  214.         }
  215.         $privileges $this->privileges[$event->getName()][$app->getAclRoleId()]
  216.             ?? new AclPrivilegeCollection([]);
  217.         if (!$event->isAllowed($app->getId(), $privileges)) {
  218.             return false;
  219.         }
  220.         return true;
  221.     }
  222.     private function loadPrivileges(string $eventName, array $affectedRoleIds): void
  223.     {
  224.         $roles $this->connection->fetchAll('
  225.             SELECT `id`, `privileges`
  226.             FROM `acl_role`
  227.             WHERE `id` IN (:aclRoleIds)
  228.         ', ['aclRoleIds' => $affectedRoleIds], ['aclRoleIds' => Connection::PARAM_STR_ARRAY]);
  229.         if (!$roles) {
  230.             $this->privileges[$eventName] = [];
  231.         }
  232.         foreach ($roles as $privilege) {
  233.             $this->privileges[$eventName][Uuid::fromBytesToHex($privilege['id'])]
  234.                 = new AclPrivilegeCollection(json_decode($privilege['privileges'], true));
  235.         }
  236.     }
  237.     private function getShopIdProvider(): ShopIdProvider
  238.     {
  239.         if (!$this->container->has(ShopIdProvider::class)) {
  240.             throw new ServiceNotFoundException(ShopIdProvider::class);
  241.         }
  242.         return $this->container->get(ShopIdProvider::class);
  243.     }
  244. }