uc_gc_client-8.x-1.x-dev/src/Controller/WebhookHandler.php
src/Controller/WebhookHandler.php
<?php
namespace Drupal\uc_gc_client\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\uc_order\Entity\Order;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Database\Driver\mysql\Connection;
use Drupal\Core\Extension\ModuleHandler;
/**
* Controller for handling webhooks from GoCardless.com.
*/
class WebhookHandler extends ControllerBase {
/**
* The database driver connection.
*
* @var \Drupal\Core\Database\Driver\mysql\Connection
*/
protected $db;
/**
* The logger.
*
* @var \Psr\Log\LoggerInterface
*/
private $logger;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandler
*/
protected $moduleHandler;
/**
* Constructs the WebhookHandler object.
*
* @param \Drupal\Core\Database\Driver\mysql\Connection $connection
* The database driver connection.
* @param \Drupal\Core\Extension\ModuleHandler $moduleHandler
* The module handler.
*/
public function __construct(Connection $connection, ModuleHandler $moduleHandler) {
$this->db = $connection;
$this->moduleHandler = $moduleHandler;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('database'),
$container->get('module_handler')
);
}
/**
* Handles webhooks from GoCardless.com.
*/
public function webhook() {
$settings = GoCardlessPartner::getSettings();
$secret = GoCardlessPartner::getPartnerWebhook();
$webhook = file_get_contents('php://input');
$headers = function_exists('getallheaders') ? getallheaders() : $this->getAllHeaders();
$provided_signature = $headers["Webhook-Signature"];
$calculated_signature = hash_hmac("sha256", $webhook, $secret);
$hash_equals = function_exists('hash_equals') ? hash_equals($provided_signature, $calculated_signature) : $this->hashEquals($provided_signature, $calculated_signature);
if ($hash_equals) {
$data = json_decode($webhook, TRUE);
// Optionally write webhook to log.
if ($settings['log_webhook']) {
if (!isset($this->logger)) {
$this->logger = $this->getLogger('uc_gc_client');
}
$this->logger->notice('<pre>GoCardless webhook: <br />' . print_r($data, TRUE) . '</pre>');
}
// Process the events.
foreach ($data['events'] as $event) {
switch ($event['resource_type']) {
case 'mandates':
if ($order_id = $this->getOrderId($event['links']['mandate'])) {
if ($order = Order::load($order_id)) {
$resource = [];
$this->mandate($order, $event['action'], $event);
}
}
break;
case 'subscriptions':
if ($order_id = $this->getOrderId($event['links']['mandate'])) {
if ($order = Order::load($order_id)) {
$resource = [];
$this->subscription($order, $event['action'], $event);
}
}
break;
case 'payments':
$payment_id = $event['links']['payment'];
$partner = new GoCardlessPartner();
$result = $partner->api([
'endpoint' => 'payments',
'action' => 'get',
'id' => $payment_id,
]);
if ($result->response->status_code == 200) {
$resource = $result->response->body->payments;
if ($order_id = $this->getOrderId($resource->links->mandate)) {
if ($order = Order::load($order_id)) {
$this->payment($order, $event['action'], $event, $resource);
}
}
}
break;
}
// Provide hook so other modules can respond to GoCardless webhooks.
$params = [
'event' => $event,
'resource' => $resource,
'order_id' => isset($order_id) ? $order_id : NULL,
];
$this->moduleHandler->invokeAll('gc_client_webhook', [$params]);
}
// Send a success header.
return new Response();
}
else {
$this->getLogger('uc_gc_client')
->warning('Webhook cannot be processed because the webhook secret is not set or is invalid.');
return new Response('Invalid Token.', 498, ['Content-Type' => 'text/html']);
}
}
/**
* Processes 'mandate' webhooks.
*
* @param object $order
* Ubercart order entity.
* @param string $action
* The webhook event type, as provided by GoCardless.
* @param array $event
* The webhook event, provided by GoCardless.
*/
private function mandate($order, $action, array $event) {
switch ($action) {
case 'submitted':
if ($order->order_status->getString() <> 'processing') {
$comment = $this->t('Your direct debit mandate @mandate has been submitted to your bank by GoCardless and will be processed soon.', ['@mandate' => $event['links']['mandate']]);
uc_order_comment_save($order->id(), $order->uid->getString(), $comment, 'order', 'processing', FALSE);
$order->setStatusId('processing')->save();
$this->db->update('uc_gc_client')
->fields([
'status' => 'pending',
'updated' => REQUEST_TIME,
])
->condition('ucid', $order->id())
->execute();
}
break;
case 'failed':
if ($order->order_status->getString() <> 'mandate_failed') {
$comment = $this->t('Your direct debit mandate @mandate creation has failed.', ['@mandate' => $event['links']['mandate']]);
uc_order_comment_save($order->id(), $order->uid->getString(), $comment, 'order', 'processing', TRUE);
$order->setStatusId('mandate_failed')->save();
}
break;
case 'active':
if ($order->order_status->getString() <> 'mandate_active') {
$comment = $this->t('Your direct debit mandate @mandate has been activated successfully with your bank.', ['@mandate' => $event['links']['mandate']]);
uc_order_comment_save($order->id(), $order->uid->getString(), $comment, 'order', 'completed', TRUE);
$order->setStatusId('mandate_active')->save();
$this->db->update('uc_gc_client')
->fields([
'status' => 'completed',
'updated' => REQUEST_TIME,
])
->condition('ucid', $order->id())
->execute();
}
break;
case 'cancelled':
if ($order->order_status->getString() <> 'canceled') {
$comment = $this->t('Your direct debit mandate @mandate has been cancelled with your bank by GoCardless.', ['@mandate' => $event['links']['mandate']]);
uc_order_comment_save($order->id(), $order->uid->getString(), $comment, 'order', 'canceled', TRUE);
$order->setStatusId('canceled')->save();
$this->db->update('uc_gc_client')
->fields([
'status' => 'canceled',
'updated' => REQUEST_TIME,
])
->condition('ucid', $order->id())
->execute();
}
break;
case 'reinstated':
if ($order->order_status->getString() <> 'processing') {
$comment = $this->t('Your direct debit mandate @mandate has been reinstated at GoCardless.', ['@mandate' => $event['links']['mandate']]);
uc_order_comment_save($order->id(), $order->uid->getString(), $comment, 'order', 'processing', FALSE);
$order->setStatusId('pending')->save();
$this->db->update('uc_gc_client')
->fields([
'status' => 'pending',
'updated' => REQUEST_TIME,
])
->condition('ucid', $order->id())
->execute();
}
break;
}
}
/**
* Processes 'payment' webhooks.
*
* @param object $order
* Ubercart order entity.
* @param string $action
* The webhook event type, as provided by GoCardless.
* @param array $event
* The webhook event, provided by GoCardless.
* @param object $resource
* The GoCardless payment resource relating to the webhook event. This is
* retreived from GoCardless after receiving the webhook.
*/
private function payment($order, $action, array $event, $resource) {
$amount = $resource->amount / 100;
!empty($order->billing_country->getString()) ? $country_code = $order->billing_country->getString() : $country_code = $order->delivery_country->getString();
$currency_sign = uc_gc_client_currency($country_code)['sign'];
switch ($action) {
case 'confirmed':
uc_payment_enter($order->id(), 'gc_client', $amount, 0, NULL, $this->t('Direct debit has been taken by GoCardless'));
$comment = $this->t('Your payment of @amount has been confirmed by GoCardless and will be paid from your bank account.', ['@amount' => uc_currency_format($amount, $currency_sign)]);
uc_order_comment_save($order->id(), 0, $comment, 'order', $order->order_status->getString(), TRUE);
// Update status to payment_received if it is the first one.
if ($order->order_status->getString() == 'mandate_active') {
$order->setStatusId('payment_received')->save();
}
break;
case 'cancelled':
$comment = $this->t("Your direct debit payment '@id' for @amount has been cancelled at GoCardless.", [
'@id' => $event['id'],
'@amount' => uc_currency_format($amount, $currency_sign),
]);
uc_order_comment_save($order->id, 0, $comment, 'order', $order->order_status->getString(), TRUE);
break;
}
}
/**
* Processes 'subscription' webhooks.
*
* @param object $order
* Ubercart order entity.
* @param string $action
* The webhook event type, as provided by GoCardless.
* @param array $event
* The webhook event, provided by GoCardless.
*/
private function subscription($order, $action, array $event) {
/*
switch ($action) {
case 'cancelled' :
foreach ($items as $item) {
isset($item['source_id']) ? $gc_order_id = $item['source_id'] : $gc_order_id = $item['id'];
$order_id = uc_gc_client_id($gc_order_id);
$order = uc_order_load($order_id);
uc_order_update_status($order_id, 'canceled');
uc_order_comment_save($order_id, $order->uid, $this->t('This direct debit Subscription has been cancelled with GoCardless.com.'), 'order', 'canceled', TRUE);
// update the status on the database
$update = $this->db->update('uc_gcsubs')
->fields(array(
'status' => 'canceled',
'updated' => time(),
))
->condition('ucid', $order_id, '=')
->execute();
}
// Invoke Rules event
//if (module_exists('rules')) {
// $items_string = json_encode($items);
// rules_invoke_event('uc_gcsubs_subs_cancellation', $items_string);
//}
break;
}
*/
}
/**
* Return the Ubercart order ID from database for specified GC mandate ID.
*
* @param string $gcid
* The GoCardless mandate ID.
*
* @return int
* The Ubercart order ID for the mandate.
*/
private function getOrderId($gcid) {
return(
$this->db->select('uc_gc_client', 'c')
->fields('c', ['ucid'])
->condition('gcid', $gcid)
->execute()->fetchField()
);
}
/**
* Timing attack safe string comparison.
*
* Compares two strings using the same time whether they're equal or not.
* Required when hash_equals() function is not present (PHP < 5.6.0).
*
* @param string $known_string
* The string of known length to compare against.
* @param string $user_string
* The user-supplied string.
*
* @return bool
* Returns TRUE when the two strings are equal, FALSE otherwise.
*/
private function hashEquals($known_string, $user_string) {
$ret = 0;
if (strlen($known_string) !== strlen($user_string)) {
$user_string = $known_string;
$ret = 1;
}
$res = $known_string ^ $user_string;
for ($i = strlen($res) - 1; $i >= 0; --$i) {
$ret |= ord($res[$i]);
}
return !$ret;
}
/**
* Fetch all HTTP request headers.
*
* Required for Nginx servers that do not support getallheaders().
*
* @return mixed
* An associative array of all the HTTP headers in the current request, or
* FALSE on failure.
*/
private function getAllHeaders() {
$headers = [];
foreach ($_SERVER as $name => $value) {
if (substr($name, 0, 5) == 'HTTP_') {
$headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
}
}
return $headers;
}
}
