turnstile

Drupal paywall plugin
Log | Files | Refs | README | LICENSE

commit 6279849d9330f686d9d1287e2255c6e190eb6490
parent 0ca7d31605a979d79d62ffcdb65a6eed1074271c
Author: Christian Grothoff <christian@grothoff.org>
Date:   Sat, 11 Oct 2025 18:01:26 +0200

centralize HTTP logic in TalerMerchantApiService

Diffstat:
MREADME.md | 2+-
Msrc/TalerMerchantApiService.php | 286++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Mturnstile.module | 263++-----------------------------------------------------------------------------
3 files changed, 280 insertions(+), 271 deletions(-)

diff --git a/README.md b/README.md @@ -47,7 +47,7 @@ Navigate to `/admin/config/content/Turnstile` to configure: ## TODO - actually *use* price categories when determining article price! -- keep or remove price field? + => keep or remove price field? - Make truncation logic work with tiles / cards diff --git a/src/TalerMerchantApiService.php b/src/TalerMerchantApiService.php @@ -12,6 +12,20 @@ namespace Drupal\turnstile; use Drupal\Core\Http\ClientFactory; use Psr\Log\LoggerInterface; + + +/** + * Taler error codes used in this module. We do not define + * the full list here as that would be excessive and could + * just slow down PHP unnecessarily. + */ +enum TalerErrorCode: int { + case TALER_EC_NONE = 0; + case TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN = 2000; + case TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN = 2005; +} + + /** * Service for fetching subscriptions and currencies from external API. */ @@ -64,7 +78,7 @@ class TalerMerchantApiService { if (empty($backend_url) || empty($access_token)) { - \Drupal::logger('turnstile')->debug('No Turnstile backend configured, returning "none" for subscriptions.'); + $this->logger->debug('No Turnstile backend configured, returning "none" for subscriptions.'); return $subscriptions; } @@ -87,7 +101,7 @@ class TalerMerchantApiService { { case 200: if (! isset($result['token_families'])) { - \Drupal::logger('turnstile')->error('Failed to obtain token family list: HTTP success response unexpectedly lacks "token_families" field.'); + $this->logger->error('Failed to obtain token family list: HTTP success response unexpectedly lacks "token_families" field.'); return $subscriptions; } /* Success, handled below */ @@ -96,15 +110,15 @@ class TalerMerchantApiService { // empty list return $subscriptions; case 403: - \Drupal::logger('turnstile')->warning('Access denied by the merchant backend. Did your credentials change or expire? Check your Turnstile configuration!'); + $this->logger->warning('Access denied by the merchant backend. Did your credentials change or expire? Check your Turnstile configuration!'); return $subscriptions; case 404: $body_log = json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); - \Drupal::logger('turnstile')->error('Failed to fetch token family list: @hint (@ec): @body', ['@hint' => $result['hint'] ?? 'N/A', '@ec' => $result['code'] ?? 'N/A', '@body' => $body_log ?? 'N/A']); + $this->logger->error('Failed to fetch token family list: @hint (@ec): @body', ['@hint' => $result['hint'] ?? 'N/A', '@ec' => $result['code'] ?? 'N/A', '@body' => $body_log ?? 'N/A']); return $subscriptions; default: $body_log = json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); - \Drupal::logger('turnstile')->error('Unexpected HTTP status code @status trying to fetch token family list: @hint (@detail, #@ec): @body', ['@status' => $http_status, '@hint' => $result['hint'] ?? 'N/A', '@ec' => $result['code'] ?? 'N/A', '@detail' => $result['detail'] ?? 'N/A', '@body' => $body_log ?? 'N/A']); + $this->logger->error('Unexpected HTTP status code @status trying to fetch token family list: @hint (@detail, #@ec): @body', ['@status' => $http_status, '@hint' => $result['hint'] ?? 'N/A', '@ec' => $result['code'] ?? 'N/A', '@detail' => $result['detail'] ?? 'N/A', '@body' => $body_log ?? 'N/A']); return $subscriptions; } // end switch on HTTP status @@ -120,7 +134,7 @@ class TalerMerchantApiService { } catch (RequestException $e) { $body_log = json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); - \Drupal::logger('turnstile')->error('Failed to obtain list of token families: @message: @body', ['@message' => $e->getMessage(), '@body' => $body_log ?? 'N/A']); + $this->logger->error('Failed to obtain list of token families: @message: @body', ['@message' => $e->getMessage(), '@body' => $body_log ?? 'N/A']); } return $subscriptions; } @@ -138,7 +152,7 @@ class TalerMerchantApiService { $payment_backend_url = $config->get('payment_backend_url'); if (empty($payment_backend_url)) { - \Drupal::logger('turnstile')->error('Taler merchant backend not configured; cannot obtain currency list'); + $this->logger->error('Taler merchant backend not configured; cannot obtain currency list'); return []; } @@ -154,20 +168,20 @@ class TalerMerchantApiService { ]); if ($response->getStatusCode() !== 200) { - \Drupal::logger('turnstile')->error('Taler merchant backend did not respond; cannot obtain currency list'); + $this->logger->error('Taler merchant backend did not respond; cannot obtain currency list'); return []; } $backend_config = json_decode($response->getBody(), TRUE); if (!$backend_config || !is_array($backend_config)) { // Invalid response, fallback to grant_access_on_error setting. - \Drupal::logger('turnstile')->error('Taler merchant backend returned invalid /config response; cannot obtain currency list'); + $this->logger->error('Taler merchant backend returned invalid /config response; cannot obtain currency list'); return []; } if (! isset($backend_config['currencies'])) { - \Drupal::logger('turnstile')->error('Backend returned malformed response for /config'); + $this->logger->error('Backend returned malformed response for /config'); return []; } @@ -188,11 +202,261 @@ class TalerMerchantApiService { } catch (\Exception $e) { // On exception, fall back to grant_access_on_error setting. - \Drupal::logger('turnstile')->error('Failed to validate obtain configuration from backend: @error', [ + $this->logger->error('Failed to validate obtain configuration from backend: @error', [ '@error' => $e->getMessage(), ]); return []; } } + + /** + * Check order status with Taler backend. + * + * @param string $order_id + * The order ID to check. + * + * @return array|FALSE + * Order status information or FALSE on failure. + */ + public function checkOrderStatus($order_id) { + $config = \Drupal::config('turnstile.settings'); + $backend_url = $config->get('payment_backend_url'); + $access_token = $config->get('access_token'); + + if (empty($backend_url) || + empty($access_token)) { + $this->logger->debug('No Turnstile backend configured, cannot check order status!'); + return FALSE; + } + + try { + $http_client = \Drupal::httpClient(); + $response = $http_client->get($backend_url . 'private/orders/' . $order_id, [ + 'headers' => [ + 'Authorization' => 'Bearer ' . $access_token, + ], + // Do not throw exceptions on 4xx/5xx status codes + 'http_errors' => false, + ]); + + $http_status = $response->getStatusCode(); + $body = $response->getBody(); + $result = json_decode($body, TRUE); + switch ($http_status) + { + case 200: + // Success, handled below + break; + case 403: + $this->logger->warning('Access denied by the merchant backend. Did your credentials change or expire? Check your Turnstile configuration!'); + return FALSE; + case 404: + // Order unknown or instance unknown + /** @var TalerErrorCode $ec */ + $ec = $result['code'] ?? TALER_EC_NONE; + switch ($ec) + { + case TALER_EC_NONE: + // Protocol violation. Could happen if the backend domain was + // taken over by someone else. + $body_log = json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + $this->logger->error('Invalid response from merchant backend when trying to obtain order status. Check your Turnstile configuration! @body', ['@body' => $body_log ?? 'N/A']); + return FALSE; + case TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN: + // This could happen if our instance was deleted after the configuration was + // checked. Very bad, log serious error. + $this->logger->error('Configured instance "@detail" unknown to merchant backend. Check your Turnstile configuration!', ['@detail' => $result['detail'] ?? 'N/A']); + return FALSE; + case TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN: + // This could happen if the instance owner manually deleted + // an order while the customer was looking at the article. + $this->logger->warning('Order "@order" disappeared in the backend.', ['@order' => $order_id]); + return FALSE; + default: + $body_log = json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + $this->logger->error('Unexpected error code @ec with HTTP status code @status from Taler merchant backend when trying to get order status: @hint (@detail, #@ec): @body', ['@status' => $http_status, '@hint' => $result['hint'] ?? 'N/A', '@ec' => $result['code'] ?? 'N/A', '@detail' => $result['detail'] ?? 'N/A', '@body' => $body_log ?? 'N/A']); + return FALSE; + } + default: + // Internal server errors and the like... + $body_log = json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + $this->logger->error('Unexpected HTTP status code @status from Taler merchant backend when trying to get order status: @hint (@detail, #@ec): @body', ['@status' => $http_status, '@hint' => $result['hint'] ?? 'N/A', '@ec' => $result['code'] ?? 'N/A', '@detail' => $result['detail'] ?? 'N/A', '@body' => $body_log ?? 'N/A']); + return FALSE; + } + + $body_log = json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + $this->logger->error('Got existing contract: @body', ['@body' => $body_log ?? 'N/A']); + + $order_status = $result['order_status'] ?? 'unknown'; + $subscription_expiration = 0; + $pay_deadline = 0; + $paid = FALSE; + switch ($order_status) + { + case 'unpaid': + // 'pay_deadline' is only available since v21 rev 1, so for now we + // fall back to creation_time + offset. + $pay_deadline = $result['pay_deadline']['t_s'] ?? + 60 * 60 * 24 + $result['creation_time']['t_s'] ?? 0; + break; + case 'claimed': + $contract_terms = $result['contract_terms']; + $pay_deadline = $contract_terms['pay_deadline']['t_s'] ?? 0; + break; + case 'paid': + $paid = TRUE; + $contract_terms = $result['contract_terms']; + $contract_version = $result['version'] ?? 0; + switch ($contract_version) { + case 0: + break; + case 1: + $choice_index = $result['choice_index'] ?? 0; + $contract_choice = $contract_terms['choices'][$choice_index]; + $outputs = $contract_choice['outputs']; + // FIXME: add logic to detect subscriptions here and + // update $subscription_expiration if one was found! + break; + default: + break; + } // switch on contract version + break; + default: + $this->logger->error('Got unexpected order status "@status"', ['@status' => $order_status]); + break; + } // switch on $order_status + return [ + 'order_id' => $order_id, + 'paid' => $paid, + 'subscription_expiration' => $subscription_expiration, + 'order_expiration' => $pay_deadline, + ]; + } + catch (RequestException $e) { + // Any kind of error that is outside of the spec. + $this->logger->error('Failed to check order status: @message', ['@message' => $e->getMessage()]); + return FALSE; + } + } + + + /** + * Create a new Taler order. + * + * @param \Drupal\node\NodeInterface $node + * The node to create an order for. + * + * @return array|FALSE + * Order information or FALSE on failure. + */ + public function createOrder(NodeInterface $node) { + $config = \Drupal::config('turnstile.settings'); + $backend_url = $config->get('payment_backend_url'); + $access_token = $config->get('access_token'); + + if (empty($backend_url) || empty($access_token)) { + $this->logger->debug('No backend, cannot setup new order'); + return FALSE; + } + + // FIXME: transition away from price to price categories... + $price = $node->get('field_price')->value; + if (empty($price)) { + $this->logger->debug('No price, cannot setup new order'); + return FALSE; + } + + // FIXME: support v1 contract terms and use it + // if we have multiple currencies in $price! + // FIXME: add support for subscriptions + $fulfillment_url = $node->toUrl('canonical', ['absolute' => TRUE])->toString(); + /* one day from now */ + // FIXME: after Merchant v1.1 we can use the returned + // the expiration time and then rely on the default already set in + // the merchant backend instead of hard-coding 1 day here! + $order_expiration = time() + 60 * 60 * 24; + $order_data = [ + 'order' => [ + 'amount' => $price, + 'summary' => 'Access to: ' . $node->getTitle(), + 'fulfillment_url' => $fulfillment_url, + 'pay_deadline' => [ + 't_s' => $order_expiration, + ], + ], + 'session_id' => session_id (), + 'create_token' => FALSE, + ]; + + try { + $http_client = \Drupal::httpClient(); + $response = $http_client->post($backend_url . 'private/orders', [ + 'json' => $order_data, + 'headers' => [ + 'Authorization' => 'Bearer ' . $access_token, + 'Content-Type' => 'application/json', + ], + // Do not throw exceptions on 4xx/5xx status codes + 'http_errors' => false, + ]); + /* Get JSON result parsed as associative array */ + $http_status = $response->getStatusCode(); + $body = $response->getBody(); + $result = json_decode($body, TRUE); + switch ($http_status) + { + case 200: + case 201: /* 201 is not in-spec, but tolerated for now */ + if (! isset($result['order_id'])) { + $this->logger->error('Failed to create order: HTTP success response unexpectedly lacks "order_id" field.'); + return FALSE; + } + /* Success, handled below */ + break; + case 403: + $this->logger->warning('Access denied by the merchant backend. Did your credentials change or expire? Check your Turnstile configuration!'); + return FALSE; + case 404: + // FIXME: go into details on why we could get 404 here... + $body_log = json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + $this->logger->error('Failed to create order: @hint (@ec): @body', ['@hint' => $result['hint'] ?? 'N/A', '@ec' => $result['code'] ?? 'N/A', '@body' => $body_log ?? 'N/A']); + return FALSE; + case 409: + case 410: + /* 409: We didn't specify an order, so this should be "wrong currency", which again Turnstile tries to prevent. So this shouldn't be possible. */ + /* 410: We didn't specify a product, so out-of-stock should also be impossible for Turnstile */ + $body_log = json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + $this->logger->error('Unexpected HTTP status code @status trying to create order: @hint (@detail, #@ec): @body', ['@status' => $http_status, '@hint' => $result['hint'] ?? 'N/A', '@ec' => $result['code'] ?? 'N/A', '@detail' => $result['detail'] ?? 'N/A', '@body' => $body_log ?? 'N/A']); + return FALSE; + case 451: + /* KYC required, can happen, warn */ + $this->logger->warning('Failed to create order as legitimization is required first. Please check legitimization status in your merchant backend.'); + return FALSE; + default: + $body_log = json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + $this->logger->error('Unexpected HTTP status code @status trying to create order: @hint (@detail, #@ec): @body', ['@status' => $http_status, '@hint' => $result['hint'] ?? 'N/A', '@ec' => $result['code'] ?? 'N/A', '@detail' => $result['detail'] ?? 'N/A', '@body' => $body_log ?? 'N/A']); + return FALSE; + } // end switch on HTTP status + + $order_id = $result['order_id']; + return [ + 'order_id' => $result['order_id'], + 'payment_url' => $backend_url . 'orders/' . $result['order_id'], + 'order_expiration' => $order_expiration, + 'paid' => FALSE, + ]; + } + catch (RequestException $e) { + $body_log = json_encode($result ?? [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + $this->logger->error('Failed to create Taler order: @message: @body', ['@message' => $e->getMessage(), '@body' => $body_log ?? 'N/A']); + } + + return FALSE; + } + + + + + } \ No newline at end of file diff --git a/turnstile.module b/turnstile.module @@ -13,17 +13,6 @@ use GuzzleHttp\Exception\RequestException; /** - * Taler error codes used in this module. We do not define - * the full list here as that would be excessive and could - * just slow down PHP unnecessarily. - */ -enum TalerErrorCode: int { - case TALER_EC_NONE = 0; - case TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN = 2000; - case TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN = 2005; -} - -/** * Implements hook_entity_bundle_field_info_alter(). * Adds the TalerPriceListFormat constraint to 'field_price' fields. * Probably can be removed entirely once we have only price categories. @@ -179,7 +168,8 @@ function turnstile_entity_view_alter(array &$build, EntityInterface $entity, Ent // Check order status with backend if not marked as paid. \Drupal::logger('turnstile')->debug('Checking order status ...'); - $status = turnstile_check_order_status($order['order_id']); + $api_service = \Drupal::service('turnstile.api_service'); + $status = $api_service->checkOrderStatus($order['order_id']); if ($status && $status['paid']) { // Update session with new status. \Drupal::logger('turnstile')->debug('Updating order status to paid.'); @@ -335,7 +325,8 @@ function turnstile_create_or_get_order(NodeInterface $node) { } // Create new order - $order_info = turnstile_create_order($node); + $api_service = \Drupal::service('turnstile.api_service'); + $order_info = $api_service->createOrder($node); if (! $order_info) { return FALSE; } @@ -347,252 +338,6 @@ function turnstile_create_or_get_order(NodeInterface $node) { /** - * Check order status with Taler backend. - * - * @param string $order_id - * The order ID to check. - * - * @return array|FALSE - * Order status information or FALSE on failure. - */ -function turnstile_check_order_status($order_id) { - $config = \Drupal::config('turnstile.settings'); - $backend_url = $config->get('payment_backend_url'); - $access_token = $config->get('access_token'); - - if (empty($backend_url) || - empty($access_token)) { - \Drupal::logger('turnstile')->debug('No Turnstile backend configured, cannot check order status!'); - return FALSE; - } - - try { - $http_client = \Drupal::httpClient(); - $response = $http_client->get($backend_url . 'private/orders/' . $order_id, [ - 'headers' => [ - 'Authorization' => 'Bearer ' . $access_token, - ], - // Do not throw exceptions on 4xx/5xx status codes - 'http_errors' => false, - ]); - - $http_status = $response->getStatusCode(); - $body = $response->getBody(); - $result = json_decode($body, TRUE); - switch ($http_status) - { - case 200: - // Success, handled below - break; - case 403: - \Drupal::logger('turnstile')->warning('Access denied by the merchant backend. Did your credentials change or expire? Check your Turnstile configuration!'); - return FALSE; - case 404: - // Order unknown or instance unknown - /** @var TalerErrorCode $ec */ - $ec = $result['code'] ?? TALER_EC_NONE; - switch ($ec) - { - case TALER_EC_NONE: - // Protocol violation. Could happen if the backend domain was - // taken over by someone else. - $body_log = json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); - \Drupal::logger('turnstile')->error('Invalid response from merchant backend when trying to obtain order status. Check your Turnstile configuration! @body', ['@body' => $body_log ?? 'N/A']); - return FALSE; - case TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN: - // This could happen if our instance was deleted after the configuration was - // checked. Very bad, log serious error. - \Drupal::logger('turnstile')->error('Configured instance "@detail" unknown to merchant backend. Check your Turnstile configuration!', ['@detail' => $result['detail'] ?? 'N/A']); - return FALSE; - case TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN: - // This could happen if the instance owner manually deleted - // an order while the customer was looking at the article. - \Drupal::logger('turnstile')->warning('Order "@order" disappeared in the backend.', ['@order' => $order_id]); - return FALSE; - default: - $body_log = json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); - \Drupal::logger('turnstile')->error('Unexpected error code @ec with HTTP status code @status from Taler merchant backend when trying to get order status: @hint (@detail, #@ec): @body', ['@status' => $http_status, '@hint' => $result['hint'] ?? 'N/A', '@ec' => $result['code'] ?? 'N/A', '@detail' => $result['detail'] ?? 'N/A', '@body' => $body_log ?? 'N/A']); - return FALSE; - } - default: - // Internal server errors and the like... - $body_log = json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); - \Drupal::logger('turnstile')->error('Unexpected HTTP status code @status from Taler merchant backend when trying to get order status: @hint (@detail, #@ec): @body', ['@status' => $http_status, '@hint' => $result['hint'] ?? 'N/A', '@ec' => $result['code'] ?? 'N/A', '@detail' => $result['detail'] ?? 'N/A', '@body' => $body_log ?? 'N/A']); - return FALSE; - } - - $body_log = json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); - \Drupal::logger('turnstile')->error('Got existing contract: @body', ['@body' => $body_log ?? 'N/A']); - - $order_status = $result['order_status'] ?? 'unknown'; - $subscription_expiration = 0; - $pay_deadline = 0; - $paid = FALSE; - switch ($order_status) - { - case 'unpaid': - // 'pay_deadline' is only available since v21 rev 1, so for now we - // fall back to creation_time + offset. - $pay_deadline = $result['pay_deadline']['t_s'] ?? - 60 * 60 * 24 + $result['creation_time']['t_s'] ?? 0; - break; - case 'claimed': - $contract_terms = $result['contract_terms']; - $pay_deadline = $contract_terms['pay_deadline']['t_s'] ?? 0; - break; - case 'paid': - $paid = TRUE; - $contract_terms = $result['contract_terms']; - $contract_version = $result['version'] ?? 0; - switch ($contract_version) { - case 0: - break; - case 1: - $choice_index = $result['choice_index'] ?? 0; - $contract_choice = $contract_terms['choices'][$choice_index]; - $outputs = $contract_choice['outputs']; - // FIXME: add logic to detect subscriptions here and - // update $subscription_expiration if one was found! - break; - default: - break; - } // switch on contract version - break; - default: - \Drupal::logger('turnstile')->error('Got unexpected order status "@status"', ['@status' => $order_status]); - break; - } // switch on $order_status - return [ - 'order_id' => $order_id, - 'paid' => $paid, - 'subscription_expiration' => $subscription_expiration, - 'order_expiration' => $pay_deadline, - ]; - } - catch (RequestException $e) { - // Any kind of error that is outside of the spec. - \Drupal::logger('turnstile')->error('Failed to check order status: @message', ['@message' => $e->getMessage()]); - return FALSE; - } -} - - -/** - * Create a new Taler order. - * - * @param \Drupal\node\NodeInterface $node - * The node to create an order for. - * - * @return array|FALSE - * Order information or FALSE on failure. - */ -function turnstile_create_order(NodeInterface $node) { - $config = \Drupal::config('turnstile.settings'); - $backend_url = $config->get('payment_backend_url'); - $access_token = $config->get('access_token'); - - if (empty($backend_url) || empty($access_token)) { - \Drupal::logger('turnstile')->debug('No backend, cannot setup new order'); - return FALSE; - } - - // FIXME: transition away from price to price categories... - $price = $node->get('field_price')->value; - if (empty($price)) { - \Drupal::logger('turnstile')->debug('No price, cannot setup new order'); - return FALSE; - } - - // FIXME: support v1 contract terms and use it - // if we have multiple currencies in $price! - // FIXME: add support for subscriptions - $fulfillment_url = $node->toUrl('canonical', ['absolute' => TRUE])->toString(); - /* one day from now */ - // FIXME: after Merchant v1.1 we can use the returned - // the expiration time and then rely on the default already set in - // the merchant backend instead of hard-coding 1 day here! - $order_expiration = time() + 60 * 60 * 24; - $order_data = [ - 'order' => [ - 'amount' => $price, - 'summary' => 'Access to: ' . $node->getTitle(), - 'fulfillment_url' => $fulfillment_url, - 'pay_deadline' => [ - 't_s' => $order_expiration, - ], - ], - 'session_id' => session_id (), - 'create_token' => FALSE, - ]; - - try { - $http_client = \Drupal::httpClient(); - $response = $http_client->post($backend_url . 'private/orders', [ - 'json' => $order_data, - 'headers' => [ - 'Authorization' => 'Bearer ' . $access_token, - 'Content-Type' => 'application/json', - ], - // Do not throw exceptions on 4xx/5xx status codes - 'http_errors' => false, - ]); - /* Get JSON result parsed as associative array */ - $http_status = $response->getStatusCode(); - $body = $response->getBody(); - $result = json_decode($body, TRUE); - switch ($http_status) - { - case 200: - case 201: /* 201 is not in-spec, but tolerated for now */ - if (! isset($result['order_id'])) { - \Drupal::logger('turnstile')->error('Failed to create order: HTTP success response unexpectedly lacks "order_id" field.'); - return FALSE; - } - /* Success, handled below */ - break; - case 403: - \Drupal::logger('turnstile')->warning('Access denied by the merchant backend. Did your credentials change or expire? Check your Turnstile configuration!'); - return FALSE; - case 404: - // FIXME: go into details on why we could get 404 here... - $body_log = json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); - \Drupal::logger('turnstile')->error('Failed to create order: @hint (@ec): @body', ['@hint' => $result['hint'] ?? 'N/A', '@ec' => $result['code'] ?? 'N/A', '@body' => $body_log ?? 'N/A']); - return FALSE; - case 409: - case 410: - /* 409: We didn't specify an order, so this should be "wrong currency", which again Turnstile tries to prevent. So this shouldn't be possible. */ - /* 410: We didn't specify a product, so out-of-stock should also be impossible for Turnstile */ - $body_log = json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); - \Drupal::logger('turnstile')->error('Unexpected HTTP status code @status trying to create order: @hint (@detail, #@ec): @body', ['@status' => $http_status, '@hint' => $result['hint'] ?? 'N/A', '@ec' => $result['code'] ?? 'N/A', '@detail' => $result['detail'] ?? 'N/A', '@body' => $body_log ?? 'N/A']); - return FALSE; - case 451: - /* KYC required, can happen, warn */ - \Drupal::logger('turnstile')->warning('Failed to create order as legitimization is required first. Please check legitimization status in your merchant backend.'); - return FALSE; - default: - $body_log = json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); - \Drupal::logger('turnstile')->error('Unexpected HTTP status code @status trying to create order: @hint (@detail, #@ec): @body', ['@status' => $http_status, '@hint' => $result['hint'] ?? 'N/A', '@ec' => $result['code'] ?? 'N/A', '@detail' => $result['detail'] ?? 'N/A', '@body' => $body_log ?? 'N/A']); - return FALSE; - } // end switch on HTTP status - - $order_id = $result['order_id']; - return [ - 'order_id' => $result['order_id'], - 'payment_url' => $backend_url . 'orders/' . $result['order_id'], - 'order_expiration' => $order_expiration, - 'paid' => FALSE, - ]; - } - catch (RequestException $e) { - $body_log = json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); - \Drupal::logger('turnstile')->error('Failed to create Taler order: @message: @body', ['@message' => $e->getMessage(), '@body' => $body_log ?? 'N/A']); - } - - return FALSE; -} - - -/** * Implements hook_theme(). */ function turnstile_theme() {