diff --git a/commerce_vipps.services.yml b/commerce_vipps.services.yml index d46946f..f9663cb 100644 --- a/commerce_vipps.services.yml +++ b/commerce_vipps.services.yml @@ -46,3 +46,11 @@ services: class: Drupal\commerce_vipps\Access\RemoteLogAccessCheck tags: - { name: access_check, applies_to: _commerce_vipps_remote_log_access } + commerce_vipps.synchronize_payment: + class: Drupal\commerce_vipps\SynchronizePayment + arguments: ['@entity_type.manager', '@logger.channel.commerce_vipps'] + commerce_vipps.cart_token_subscriber: + class: Drupal\commerce_vipps\EventSubscriber\CartTokenSubscriber + arguments: ['@commerce_cart.cart_session', '@current_route_match', '@logger.channel.commerce_vipps'] + tags: + - { name: 'event_subscriber' } diff --git a/src/EventSubscriber/CartTokenSubscriber.php b/src/EventSubscriber/CartTokenSubscriber.php new file mode 100644 index 0000000..edd1dff --- /dev/null +++ b/src/EventSubscriber/CartTokenSubscriber.php @@ -0,0 +1,101 @@ +cartSession = $cart_session; + $this->logger = $logger; + $this->tempStore = \Drupal::service('tempstore.shared')->get(VippsLandingPageRedirectForm::QUERY_NAME); + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + $events = []; + // Run before router_listener so we execute before access checks, and before + // dynamic_page_cache so we can populate a session. The ensures proper + // access to CheckoutController. + $events[KernelEvents::REQUEST][] = ['onRequest', 100]; + return $events; + } + + /** + * Loads the token cart data and resets it to the session. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event + * The response event, which contains the current request. + */ + public function onRequest(GetResponseEvent $event) { + $cart_token = $event->getRequest()->query->get(VippsLandingPageRedirectForm::QUERY_NAME); + $order_id = $this->tempStore->get($cart_token); + if ($cart_token && $order_id) { + /** @var \Drupal\commerce_order\Entity\OrderInterface $order */ + $order = Order::load($order_id); + if ($this->cartSession->hasCartId($order->id(), CartSessionInterface::COMPLETED)) { + $this->tempStore->delete($cart_token); + return; + } + if ($order->getData('vipps_auth_key') !== $cart_token) { + // @todo: Add flood control. + $this->logger->alert('CartTokenSubscriber was called on order @order_id with incorrect vipps_auth_key', ['@order_id' => $order_id]); + return; + } + // Attach the cart to the current session. + if (!$this->cartSession->hasCartId($order->id(), CartSession::ACTIVE)) { + $this->cartSession->addCartId($order->id(), CartSession::ACTIVE); + } + if (!$this->cartSession->hasCartId($order->id(), CartSession::COMPLETED)) { + $this->cartSession->addCartId($order->id(), CartSession::COMPLETED); + } + $this->tempStore->delete($cart_token); + } + } + +} diff --git a/src/PluginForm/OffsiteRedirect/VippsLandingPageRedirectForm.php b/src/PluginForm/OffsiteRedirect/VippsLandingPageRedirectForm.php index 67a152e..c334034 100644 --- a/src/PluginForm/OffsiteRedirect/VippsLandingPageRedirectForm.php +++ b/src/PluginForm/OffsiteRedirect/VippsLandingPageRedirectForm.php @@ -10,6 +10,7 @@ use Drupal\commerce_vipps\Resolver\ChainOrderIdResolverInterface; use Drupal\commerce_vipps\VippsManagerInterface; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\TempStore\SharedTempStoreFactory; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -20,6 +21,14 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface; */ class VippsLandingPageRedirectForm extends BasePaymentOffsiteForm implements ContainerInjectionInterface { + /** + * Used for query name and tempStore collection. + * + * @internal + * For internal use only! + */ + const QUERY_NAME = 'commerce_vipps_payment_redirect_key'; + /** * The Vipps manager. * @@ -41,6 +50,13 @@ class VippsLandingPageRedirectForm extends BasePaymentOffsiteForm implements Con */ protected $eventDispatcher; + /** + * Shared temporary storage. + * + * @var \Drupal\Core\TempStore\SharedTempStore + */ + protected $tempStore; + /** * VippsLandingPageRedirectForm constructor. * @@ -50,11 +66,14 @@ class VippsLandingPageRedirectForm extends BasePaymentOffsiteForm implements Con * The chain order id resolver. * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher * The event dispatcher. + * @param \Drupal\Core\TempStore\SharedTempStoreFactory $tempStore + * The shared temporary storage factory. */ - public function __construct(VippsManagerInterface $vippsManager, ChainOrderIdResolverInterface $chainOrderIdResolver, EventDispatcherInterface $eventDispatcher) { + public function __construct(VippsManagerInterface $vippsManager, ChainOrderIdResolverInterface $chainOrderIdResolver, EventDispatcherInterface $eventDispatcher, SharedTempStoreFactory $tempStore) { $this->vippsManager = $vippsManager; $this->chainOrderIdResolver = $chainOrderIdResolver; $this->eventDispatcher = $eventDispatcher; + $this->tempStore = $tempStore->get(self::QUERY_NAME); } /** @@ -64,7 +83,8 @@ class VippsLandingPageRedirectForm extends BasePaymentOffsiteForm implements Con return new static( $container->get('commerce_vipps.manager'), $container->get('commerce_vipps.chain_order_id_resolver'), - $container->get('event_dispatcher') + $container->get('event_dispatcher'), + $container->get('tempstore.shared') ); } @@ -72,6 +92,7 @@ class VippsLandingPageRedirectForm extends BasePaymentOffsiteForm implements Con * {@inheritdoc} */ public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $form = parent::buildConfigurationForm($form, $form_state); /** @var \Drupal\commerce_payment\Entity\Payment $payment */ @@ -88,7 +109,10 @@ class VippsLandingPageRedirectForm extends BasePaymentOffsiteForm implements Con $order = $payment->getOrder(); $order_changed = FALSE; if ($order->getData('vipps_auth_key') === NULL) { - $order->setData('vipps_auth_key', $this->generateAuthToken()); + // Generate unique key, retry if key already exists. + do { + $order->setData('vipps_auth_key', $this->generateAuthToken()); + } while ($this->tempStore->get($order->getData('vipps_auth_key'))); $order_changed = TRUE; } @@ -115,7 +139,7 @@ class VippsLandingPageRedirectForm extends BasePaymentOffsiteForm implements Con $this->t('Payment for order @order_id', ['@order_id' => $payment->getOrderId()]), // Get standard payment notification callback and add. rtrim($plugin->getNotifyUrl()->toString(), '/') . '/' . $payment->getOrderId(), - $form['#return_url'], + $this->addQueryToUrl($form['#return_url'], [self::QUERY_NAME => $order->getData('vipps_auth_key')]), $options ) ->getURL(); @@ -129,6 +153,8 @@ class VippsLandingPageRedirectForm extends BasePaymentOffsiteForm implements Con if ($order_changed === TRUE) { $order->save(); } + // Add vipps_auth_key to the temp store. + $this->tempStore->set($order->getData('vipps_auth_key'), $order->id()); return $this->buildRedirectForm($form, $form_state, $url, []); } @@ -149,4 +175,24 @@ class VippsLandingPageRedirectForm extends BasePaymentOffsiteForm implements Con return bin2hex($randomStr); } + /** + * Adds custom parameter to string-formed url. + * + * @param string $url + * Absolute URL. + * @param array $query + * Array of query arguments to be added. + * + * @return string + * Abslute URL with query arguments. + */ + private function addQueryToUrl($url, array $query) { + $parsedUrl = parse_url($url); + if ($parsedUrl['path'] == NULL) { + $url .= '/'; + } + $separator = !isset($parsedUrl['query']) ? '?' : '&'; + return $url .= $separator . http_build_query($query); + } + }