<?php declare(strict_types=1);
/*
* (c) shopware AG <info@shopware.com>
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Swag\FlowBuilderProfessional\Core\Content\Flow\Dispatching\Action;
use Doctrine\DBAL\Connection;
use GuzzleHttp\Client;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\RequestOptions;
use Psr\Log\LoggerInterface;
use Shopware\Core\Content\Flow\Dispatching\Action\FlowAction;
use Shopware\Core\Defaults;
use Shopware\Core\Framework\Adapter\Twig\StringTemplateRenderer;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\Event\FlowEvent;
use Shopware\Core\Framework\Event\FlowEventAware;
use Shopware\Core\Framework\Uuid\Uuid;
use Shopware\Core\Framework\Webhook\EventLog\WebhookEventLogDefinition;
use Swag\FlowBuilderProfessional\Core\Content\Flow\Dispatching\Exception\WebhookActionConfigurationException;
use Swag\FlowBuilderProfessional\Core\Framework\Event\WebhookAware;
class CallWebhookAction extends FlowAction
{
private const TIMEOUT = 20;
private const CONNECT_TIMEOUT = 10;
private ClientInterface $guzzleClient;
private LoggerInterface $logger;
private StringTemplateRenderer $templateRenderer;
private Connection $connection;
public function __construct(
Client $guzzleClient,
StringTemplateRenderer $templateRenderer,
LoggerInterface $logger,
Connection $connection
) {
$this->guzzleClient = $guzzleClient;
$this->templateRenderer = $templateRenderer;
$this->logger = $logger;
$this->connection = $connection;
}
public static function getName(): string
{
return 'action.call.webhook';
}
public static function getSubscribedEvents(): array
{
return [
self::getName() => 'handle',
];
}
public function requirements(): array
{
return [WebhookAware::class];
}
public function handle(FlowEvent $event): void
{
$config = $event->getConfig();
if (!$this->validateConfigData($config)) {
return;
}
$options = $config['options'] ?? [];
$options['connect_timeout'] = self::CONNECT_TIMEOUT;
$options['timeout'] = self::TIMEOUT;
if (\array_key_exists(RequestOptions::AUTH, $options) && !$config['authActive']) {
unset($options[RequestOptions::AUTH]);
}
$sequenceId = $event->getFlowState()->sequenceId;
$event = $event->getFlowState()->event;
$data = $this->getAvailableData($event);
$options = $this->buildRequestOptions($options, $data, $event->getContext());
$webhookEventId = Uuid::randomBytes();
$timestamp = \time();
$this->connection->executeStatement(
'INSERT INTO `webhook_event_log` (id, delivery_status, timestamp, webhook_name, event_name, url, request_content, created_at)
VALUES (:webhookEventId, :deliveryStatus, :timestamp, :webhookName, :eventName, :url, :requestContent, :createdAt)',
[
'webhookEventId' => $webhookEventId,
'deliveryStatus' => WebhookEventLogDefinition::STATUS_RUNNING,
'timestamp' => $timestamp,
'webhookName' => $config['method'] . ': ' . $config['baseUrl'],
'eventName' => $event->getName(),
'url' => $config['baseUrl'],
'requestContent' => \json_encode([
'method' => $config['method'],
'options' => $options,
]),
'createdAt' => (new \DateTime())->format(Defaults::STORAGE_DATE_TIME_FORMAT),
],
);
$this->connection->executeStatement(
'INSERT INTO `swag_sequence_webhook_event_log` (sequence_id, webhook_event_log_id)
VALUES (:sequenceId, :webhookEventId)',
[
'sequenceId' => Uuid::fromHexToBytes($sequenceId),
'webhookEventId' => $webhookEventId,
]
);
try {
$response = $this->guzzleClient->request($config['method'], $config['baseUrl'], $options);
$this->connection->executeStatement(
'UPDATE `webhook_event_log` SET delivery_status = :deliveryStatus, processing_time = :processingTime,
response_content = :responseContent, response_status_code = :responseStatusCode,
response_reason_phrase = :responseReasonPhrase
WHERE id = :webhookEventId',
[
'webhookEventId' => $webhookEventId,
'deliveryStatus' => WebhookEventLogDefinition::STATUS_SUCCESS,
'processingTime' => \time() - $timestamp,
'responseContent' => \json_encode([
'headers' => $response->getHeaders(),
'body' => \json_decode($response->getBody()->getContents(), true),
]),
'responseStatusCode' => $response->getStatusCode(),
'responseReasonPhrase' => $response->getReasonPhrase(),
],
);
} catch (GuzzleException $e) {
$this->logger->notice(\sprintf('Webhook execution failed to target url "%s".', $config['baseUrl']), [
'exceptionMessage' => $e->getMessage(),
'statusCode' => $e->getCode(),
]);
$payload = [
'webhookEventId' => $webhookEventId,
'deliveryStatus' => WebhookEventLogDefinition::STATUS_FAILED,
'processingTime' => \time() - $timestamp,
];
if ($e instanceof RequestException && $e->getResponse() !== null) {
$response = $e->getResponse();
$payload = \array_merge($payload, [
'responseContent' => \json_encode([
'headers' => $response->getHeaders(),
'body' => \json_decode($response->getBody()->getContents(), true),
]),
'responseStatusCode' => $response->getStatusCode(),
'responseReasonPhrase' => $response->getReasonPhrase(),
]);
}
$this->connection->executeStatement(
'UPDATE `webhook_event_log` SET delivery_status = :deliveryStatus, processing_time = :processingTime,
response_content = :responseContent, response_status_code = :responseStatusCode,
response_reason_phrase = :responseReasonPhrase
WHERE id = :webhookEventId',
$payload
);
}
}
private function buildRequestOptions(array $options, array $data, Context $context): array
{
/*
* request headers:
* $options['headers'] = [
* 'Content-Type' => 'application/json',
* 'User-Agent' => 'GuzzleHttp/7',
* ]
*/
if (\array_key_exists(RequestOptions::HEADERS, $options)) {
$options[RequestOptions::HEADERS] = $this->resolveOptionParams($options[RequestOptions::HEADERS], $data, $context);
}
/*
* request query:
* $options['query'] = [
* 'orderNumber' => '{{ order.orderNumber }}',
* 'message' => 'message test',
* ]
*/
if (\array_key_exists(RequestOptions::QUERY, $options)) {
$options[RequestOptions::QUERY] = $this->resolveOptionParams($options[RequestOptions::QUERY], $data, $context);
}
/*
* request form params:
* $options['form_params'] = [
* 'firstName' => '{{ customer.firstName }}',
* 'message' => 'Foo',
* ]
*/
if (\array_key_exists(RequestOptions::FORM_PARAMS, $options)) {
$options[RequestOptions::FORM_PARAMS] = $this->resolveOptionParams($options[RequestOptions::FORM_PARAMS], $data, $context);
}
/*
* request body:
* $options['body'] = 'Foo bar!'
* or
* $options['body'] = '{"chat_id": "332293824", "text": "Hello {{ customer.firstName }}"}'
*/
if (\array_key_exists(RequestOptions::BODY, $options)) {
$options[RequestOptions::BODY] = $this->resolveParamsData($options[RequestOptions::BODY], $data, $context);
}
return $options;
}
private function resolveOptionParams(array $params, array $data, Context $context): array
{
foreach ($params as $key => $value) {
$params[$key] = $this->resolveParamsData($value, $data, $context);
}
return $params;
}
private function resolveParamsData(string $template, array $data, Context $context): ?string
{
try {
return $this->templateRenderer->render($template, $data, $context);
} catch (\Throwable $e) {
$this->logger->error(
"Could not render template with error message:\n"
. $e->getMessage() . "\n"
. 'Error Code:' . $e->getCode() . "\n"
. 'Template source:'
. $template . "\n"
. "Template data: \n"
. \json_encode($data) . "\n"
);
return null;
}
}
private function getAvailableData(FlowEventAware $event): array
{
$data = [];
foreach (\array_keys($event::getAvailableData()->toArray()) as $key) {
$getter = 'get' . \ucfirst($key);
if (!\method_exists($event, $getter)) {
throw new WebhookActionConfigurationException('Data for ' . $key . ' not available.', \get_class($event));
}
$data[$key] = $event->$getter();
}
return $data;
}
private function validateConfigData(array $config): bool
{
if (!\array_key_exists('method', $config)) {
$this->logger->error(
"Method does not exist in config data:\n"
. \json_encode($config) . "\n"
);
return false;
}
if (!\array_key_exists('baseUrl', $config)) {
$this->logger->error(
"Base url does not exist in config data:\n"
. \json_encode($config) . "\n"
);
return false;
}
if (!\array_key_exists('authActive', $config)) {
$this->logger->error(
"Auth active does not exist in config data:\n"
. \json_encode($config) . "\n"
);
return false;
}
return true;
}
}