vendor/shopware/core/Framework/Webhook/WebhookDispatcher.php line 88

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