billwerk_subscriptions-1.x-dev/src/Controller/WebhookListenerController.php
src/Controller/WebhookListenerController.php
<?php
declare(strict_types=1);
namespace Drupal\billwerk_subscriptions\Controller;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Controller\ControllerBase;
use Drupal\billwerk_subscriptions\Event\BillwerkWebhookEvent;
use Drupal\billwerk_subscriptions\Exception\WebhookException;
use Drupal\billwerk_subscriptions\LogHelper;
use Drupal\billwerk_subscriptions\SettingsHelper;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Returns responses for Billwerk Subscriptions routes.
*/
final class WebhookListenerController extends ControllerBase {
/**
* The constructor.
*
* @param \Drupal\billwerk_subscriptions\SettingsHelper $settingsHelper
* The Settings Helper.
* @param \Drupal\billwerk_subscriptions\LogHelper $logHelper
* The Log Helper.
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher
* The Event Dispatcher.
*/
public function __construct(
protected readonly SettingsHelper $settingsHelper,
protected readonly LogHelper $logHelper,
protected readonly EventDispatcherInterface $eventDispatcher,
) {
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('billwerk_subscriptions.settings_helper'),
$container->get('billwerk_subscriptions.log_helper'),
$container->get('event_dispatcher'),
);
}
/**
* Listens to incoming Billwerk webhook calls.
*
* @param string $secret
* The secret to authenticate the webhook call.
* @param \Symfony\Component\HttpFoundation\Request $request
* The request.
*
* @return \Symfony\Component\HttpFoundation\Response
* The response.
*/
public function listen($secret, Request $request): Response {
// Only handle webhooks that know the secret to prevent disallowed calls.
// As the path is public and hard-coded this would be risky Otherwise.
// @improve: A more secure authentication would be even better!
if ($secret === $this->settingsHelper->getWebhookSecret()) {
try {
// Extract payload.
// The given data is at a bare minimum for security reasons,
// so we have to fetch details ourselves, see
// https://docu.billwerk.plus/api/premium-enterprise/en/webhooks/general/security---reliability.html
// for explanations and
// https://docu.billwerk.plus/api/premium-enterprise/en/webhooks/customer-and-contract.html
// for details about the provided data.
$payload = $request->getContent();
if (empty($payload)) {
$payload = file_get_contents("php://input");
}
if (!empty($payload)) {
$data = Json::decode($payload);
$webhookEventName = $data["Event"];
}
if (!empty($webhookEventName)) {
$billwerkWebhookEvent = new BillwerkWebhookEvent($payload);
$this->eventDispatcher->dispatch($billwerkWebhookEvent, BillwerkWebhookEvent::EVENT_NAME_PREFIX . $webhookEventName);
}
else {
throw new WebhookException('Webhook event name could not be determined from the payload!');
}
// Billwerk will recognize this call as successful.
// @see https://docu.billwerk.plus/api/premium-enterprise/en/webhooks/general/security---reliability.html
return new Response('OK', 200);
}
catch (\Exception $e) {
$this->logHelper->logException($e);
// Billwerk will repeat failed calls a dozen times!
// @see https://docu.billwerk.plus/api/premium-enterprise/en/webhooks/general/security---reliability.html
return new Response($e->getMessage(), 500);
}
}
else {
// Return a regular Drupal page not found, if the secret does not match.
throw new NotFoundHttpException();
}
}
}
