commit 55dc15bccb7dbf97cae8ee45b237ecbe9018f8f7
parent 8b3a41b9ba801823fa3909605725c2afd0dbcc14
Author: Christian Grothoff <christian@grothoff.org>
Date: Thu, 24 Jul 2025 22:14:01 +0200
fix up logic
Diffstat:
| M | src/Service/Turnstile.php | | | 113 | ++----------------------------------------------------------------------------- |
| M | turnstile.module | | | 231 | ++++++++++++++++++++++++++++++++++++++++++++++--------------------------------- |
2 files changed, 137 insertions(+), 207 deletions(-)
diff --git a/src/Service/Turnstile.php b/src/Service/Turnstile.php
@@ -9,6 +9,8 @@ use Psr\Log\LoggerInterface;
/**
* Service for handling paywall functionality.
+ *
+ * FIXME: is this class needed at all?
*/
class Turnstile {
@@ -39,115 +41,4 @@ class Turnstile {
$this->httpClient = $http_client;
}
-
-
- /**
- * 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 = $this->configFactory->get('turnstile.settings');
- $backend_url = $config->get('payment_backend_url');
- $access_token = $config->get('access_token');
-
- if (empty($backend_url) || empty($access_token)) {
- return FALSE;
- }
-
- try {
- $response = $this->httpClient->get($backend_url . '/private/orders/' . $order_id, [
- 'headers' => [
- 'Authorization' => 'Bearer ' . $access_token,
- ],
- ]);
-
- // FIXME: check HTTP status first...
- $result = json_decode($response->getBody(), TRUE);
-
- // FIXME: check $result['contract_terms'] to see if this is
- // a valid *subscription*, and if so, return subscription status!
-
- return [
- 'order_id' => $order_id,
- 'paid' => isset($result['order_status']) && $result['order_status'] === 'paid',
- 'refunded' => isset($result['refunded']) && $result['refunded'] === TRUE,
- 'order_status' => $result['order_status'] ?? 'unknown',
- ];
- }
- catch (RequestException $e) {
- \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.
- */
- public function createOrder($node) {
- $config = $this->configFactory->get('turnstile.settings');
- $backend_url = $config->get('payment_backend_url');
- $access_token = $config->get('access_token');
-
- if (empty($backend_url) || empty($access_token)) {
- return FALSE;
- }
-
- $price = $node->get('field_price')->value;
- if (empty($price)) {
- return FALSE;
- }
-
- $fulfillment_url = $node->toUrl('canonical', ['absolute' => TRUE])->toString();
- $order_data = [
- 'order' => [
- 'amount' => $price,
- 'summary' => 'Access to: ' . $node->getTitle(),
- 'fulfillment_url' => $fulfillment_url,
- ],
- ],
- 'create_token' => FALSE,
- ];
-
- try {
- $response = $this->httpClient->post($backend_url . '/private/orders', [
- 'json' => $order_data,
- 'headers' => [
- 'Authorization' => 'Bearer ' . $access_token,
- 'Content-Type' => 'application/json',
- ],
- ]);
-
- // FIXME: check HTTP status first...
- $result = json_decode($response->getBody(), TRUE);
-
- if (isset($result['order_id'])) {
- // FIXME: Someone must set
- // $_SESSION['turnstile_orders'][$node_id]['order_id'] = $result['order_id'];
- return [
- 'order_id' => $result['order_id'],
- 'payment_url' => $backend_url . '/orders/' . $result['order_id'],
- 'paid' => FALSE,
- 'refunded' => FALSE,
- 'created' => time(),
- ];
- }
- }
- catch (RequestException $e) {
- \Drupal::logger('turnstile')->error('Failed to create Taler order: @message', ['@message' => $e->getMessage()]);
- }
-
- return FALSE;
- }
-
}
\ No newline at end of file
diff --git a/turnstile.module b/turnstile.module
@@ -8,6 +8,7 @@
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\node\NodeInterface;
+use GuzzleHttp\Exception\RequestException;
/**
* Implements hook_entity_view_alter().
@@ -33,7 +34,7 @@ function turnstile_entity_view_alter(array &$build, EntityInterface $entity, Ent
}
// Get the original body content.
- if (! $entity->hasField('body')) {
+ if (!$entity->hasField('body')) {
return;
}
if ($entity->get('body')->isEmpty()) {
@@ -48,7 +49,6 @@ function turnstile_entity_view_alter(array &$build, EntityInterface $entity, Ent
if (isset($build['body'][0]['#text'])) {
$build['body'][0]['#text'] = $transformed_body;
}
-
}
@@ -57,11 +57,13 @@ function turnstile_entity_view_alter(array &$build, EntityInterface $entity, Ent
*
* @param string $body
* The original body content.
+ * @param \Drupal\node\NodeInterface $node
+ * The node entity.
*
* @return string
* The transformed body content.
*/
-function turnstile_check_paywall_cookie($body) {
+function turnstile_check_paywall_cookie($body, NodeInterface $node) {
// Start session if not already started.
if (session_status() == PHP_SESSION_NONE) {
session_start();
@@ -78,10 +80,11 @@ function turnstile_check_paywall_cookie($body) {
// Check if there's an existing order for this article.
$node_id = $node ? $node->id() : NULL;
- if (! $node_id) {
+ if (!$node_id) {
// Strange, but not a case where we can do a payment.
return $body;
}
+
if (isset($_SESSION['turnstile_orders'][$node_id])) {
$order = $_SESSION['turnstile_orders'][$node_id];
@@ -95,27 +98,55 @@ function turnstile_check_paywall_cookie($body) {
// Check order status with backend if not marked as paid.
if (!$order['paid']) {
- $status = turnstile_checkOrderStatus($order['order_id']);
+ $status = turnstile_check_order_status($order['order_id']);
if ($status && $status['paid']) {
// Update session with new status.
// FIXME: if we keep refunds above, we should check
// for refunds here as well!
$_SESSION['turnstile_orders'][$node_id]['paid'] = TRUE;
- return TRUE;
+ return $body;
}
}
}
-
-
// No valid payment found, need to create order or show payment option.
$order_info = turnstile_create_or_get_order($node);
+ if (! $order_info) {
+ // Fallback: just return body, not good to have unhappy readers.
+ return $body;
+ }
+
+ return turnstile_render_preview_with_payment_button($body, $order_info);
+}
+
+
+/**
+ * Create or get existing order for a node.
+ *
+ * @param \Drupal\node\NodeInterface $node
+ * The node to create an order for.
+ *
+ * @return array|FALSE
+ * Order information or FALSE on failure.
+ */
+function turnstile_create_or_get_order(NodeInterface $node) {
+ $node_id = $node->id();
+
+ // Check if we already have an order for this node in session
+ if (isset($_SESSION['turnstile_orders'][$node_id])) {
+ // FIXME: check of order expired, not useful if payment deadline expired!
+ return $_SESSION['turnstile_orders'][$node_id];
+ }
+
+ // Create new order
+ $order_info = turnstile_create_order($node);
if ($order_info) {
- return turnstile_render_preview_with_payment_button($body, $order_info);
+ // Store order in session
+ $_SESSION['turnstile_orders'][$node_id] = $order_info;
+ return $order_info;
}
- // Fallback - truncate content with safe HTML handling.
- return turnstile_truncate_content_safely($body);
+ return FALSE;
}
@@ -130,7 +161,7 @@ function turnstile_check_paywall_cookie($body) {
* @return string
* Safely truncated HTML.
*/
-function paywall_guard_truncate_content_safely($content, $length = 200) {
+function turnstile_truncate_content_safely($content, $length = 200) {
// Remove HTML tags for length calculation.
$text_only = strip_tags($content);
@@ -140,7 +171,9 @@ function paywall_guard_truncate_content_safely($content, $length = 200) {
// Use DOMDocument to safely truncate HTML.
$dom = new \DOMDocument();
+ libxml_use_internal_errors(true);
$dom->loadHTML('<?xml encoding="UTF-8">' . $content, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
+ libxml_clear_errors();
$truncated = '';
$current_length = 0;
@@ -186,23 +219,22 @@ function paywall_guard_truncate_content_safely($content, $length = 200) {
* @return string
* Rendered preview with payment button.
*/
-function paywall_guard_render_preview_with_payment_button($body, $order_info) {
+function turnstile_render_preview_with_payment_button($body, $order_info) {
// Safely truncate content to first few lines.
- $preview = paywall_guard_truncate_content_safely($body, 200);
+ $preview = turnstile_truncate_content_safely($body, 200);
+ // FIXME: eventually, we may want to at least give the option to
+ // inline the payment dialog / QR code and long-poll ourselves,
+ // and/or to send the Taler:-HTTP header with the response to the client!
// Add payment button.
$payment_button = '<div class="paywall-payment-section" style="margin-top: 20px; padding: 20px; border: 1px solid #ddd; background: #f9f9f9;">';
- $payment_button .= '<p><strong>This content is protected by a paywall.</strong></p>';
- $payment_button .= '<p>Continue reading by completing your payment.</p>';
- $payment_button .= '<a href="' . htmlspecialchars($order_info['payment_url']) . '" class="button btn btn-primary" style="display: inline-block; padding: 10px 20px; background: #007cba; color: white; text-decoration: none; border-radius: 4px;">Pay to Read More</a>';
+ $payment_button .= '<p><strong>Please pay with GNU Taler to continue reading.</strong></p>';
+ $payment_button .= '<a href="' . htmlspecialchars($order_info['payment_url']) . '" class="button btn btn-primary" style="display: inline-block; padding: 10px 20px; background: #007cba; color: white; text-decoration: none; border-radius: 4px;">Click here to open payment dialog</a>';
$payment_button .= '</div>';
return $preview . $payment_button;
}
-
-
-
/**
* Implements hook_form_FORM_ID_alter() for node forms.
*/
@@ -230,39 +262,46 @@ function turnstile_form_node_form_alter(&$form, \Drupal\Core\Form\FormStateInter
* @return array|FALSE
* Order status information or FALSE on failure.
*/
-function turnstile_checkOrderStatus($order_id) {
- $config = $this->configFactory->get('turnstile.settings');
- $backend_url = $config->get('payment_backend_url');
- $access_token = $config->get('access_token');
+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)) {
+ return FALSE;
+ }
+
+ try {
+ $http_client = \Drupal::httpClient();
+ $response = $http_client->get($backend_url . '/private/orders/' . $order_id, [
+ 'headers' => [
+ 'Authorization' => 'Bearer ' . $access_token,
+ ],
+ ]);
- if (empty($backend_url) || empty($access_token)) {
+ // Check HTTP status
+ if ($response->getStatusCode() !== 200) {
return FALSE;
}
- try {
- $response = $this->httpClient->get($backend_url . '/private/orders/' . $order_id, [
- 'headers' => [
- 'Authorization' => 'Bearer ' . $access_token,
- ],
- ]);
-
- // FIXME: check HTTP status first...
- $result = json_decode($response->getBody(), TRUE);
+ $result = json_decode($response->getBody(), TRUE);
- // FIXME: check $result['contract_terms'] to see if this is
- // a valid *subscription*, and if so, return subscription status!
+ // FIXME: check $result['contract_terms'] to see if this is
+ // a valid *subscription*, and if so, return subscription status!
- return [
- 'order_id' => $order_id,
- 'paid' => isset($result['order_status']) && $result['order_status'] === 'paid',
- 'refunded' => isset($result['refunded']) && $result['refunded'] === TRUE,
- 'order_status' => $result['order_status'] ?? 'unknown',
- ];
- }
- catch (RequestException $e) {
- \Drupal::logger('turnstile')->error('Failed to check order status: @message', ['@message' => $e->getMessage()]);
- return FALSE;
- }
+ return [
+ 'order_id' => $order_id,
+ 'paid' => isset($result['order_status']) && $result['order_status'] === 'paid',
+ 'refunded' => isset($result['refunded']) && $result['refunded'] === TRUE,
+ 'order_status' => $result['order_status'] ?? 'unknown',
+ // FIXME: initialize 'created'?
+ // FIXME: probably better keep when offer expires!
+ ];
+ }
+ catch (RequestException $e) {
+ \Drupal::logger('turnstile')->error('Failed to check order status: @message', ['@message' => $e->getMessage()]);
+ return FALSE;
+ }
}
@@ -275,64 +314,65 @@ function turnstile_checkOrderStatus($order_id) {
* @return array|FALSE
* Order information or FALSE on failure.
*/
-public function turnstile_createOrder($node) {
- $config = $this->configFactory->get('turnstile.settings');
- $backend_url = $config->get('payment_backend_url');
- $access_token = $config->get('access_token');
+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)) {
- return FALSE;
- }
+ if (empty($backend_url) || empty($access_token)) {
+ return FALSE;
+ }
- $price = $node->get('field_price')->value;
- if (empty($price)) {
- return FALSE;
- }
+ $price = $node->get('field_price')->value;
+ if (empty($price)) {
+ return FALSE;
+ }
+
+ $fulfillment_url = $node->toUrl('canonical', ['absolute' => TRUE])->toString();
+ $order_data = [
+ 'order' => [
+ 'amount' => $price,
+ 'summary' => 'Access to: ' . $node->getTitle(),
+ 'fulfillment_url' => $fulfillment_url,
+ ],
+ 'create_token' => FALSE,
+ ];
- $fulfillment_url = $node->toUrl('canonical', ['absolute' => TRUE])->toString();
- $order_data = [
- 'order' => [
- 'amount' => $price,
- 'summary' => 'Access to: ' . $node->getTitle(),
- 'fulfillment_url' => $fulfillment_url,
- ],
+ 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',
],
- 'create_token' => FALSE,
- ];
+ ]);
- try {
- $response = $this->httpClient->post($backend_url . '/private/orders', [
- 'json' => $order_data,
- 'headers' => [
- 'Authorization' => 'Bearer ' . $access_token,
- 'Content-Type' => 'application/json',
- ],
- ]);
-
- // FIXME: check HTTP status first...
- $result = json_decode($response->getBody(), TRUE);
-
- if (isset($result['order_id'])) {
- // FIXME: Someone must set
- // $_SESSION['turnstile_orders'][$node_id]['order_id'] = $result['order_id'];
- return [
- 'order_id' => $result['order_id'],
- 'payment_url' => $backend_url . '/orders/' . $result['order_id'],
- 'paid' => FALSE,
- 'refunded' => FALSE,
- 'created' => time(),
- ];
- }
+ // Check HTTP status
+ if ($response->getStatusCode() !== 200 && $response->getStatusCode() !== 201) {
+ return FALSE;
}
- catch (RequestException $e) {
- \Drupal::logger('turnstile')->error('Failed to create Taler order: @message', ['@message' => $e->getMessage()]);
+
+ $result = json_decode($response->getBody(), TRUE);
+
+ if (isset($result['order_id'])) {
+ return [
+ 'order_id' => $result['order_id'],
+ 'payment_url' => $backend_url . '/orders/' . $result['order_id'],
+ 'paid' => FALSE,
+ 'refunded' => FALSE,
+ 'created' => time(), // FIXME: probably better keep when offer expires!
+ ];
}
+ }
+ catch (RequestException $e) {
+ \Drupal::logger('turnstile')->error('Failed to create Taler order: @message', ['@message' => $e->getMessage()]);
+ }
- return FALSE;
+ return FALSE;
}
-
/**
* Implements hook_theme().
*/
@@ -344,4 +384,4 @@ function turnstile_theme() {
],
],
];
-}
-\ No newline at end of file
+}