turnstile

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

commit 33f75c04c03b343655f3b695acdae770fea4ff47
parent 489bb0a98a869961dd9803dd54f52c364ce74e61
Author: Christian Grothoff <christian@grothoff.org>
Date:   Wed,  8 Oct 2025 20:08:36 +0200

content transformation works for articles

Diffstat:
Mturnstile.module | 154++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
1 file changed, 98 insertions(+), 56 deletions(-)

diff --git a/turnstile.module b/turnstile.module @@ -13,7 +13,6 @@ use GuzzleHttp\Exception\RequestException; - /** * Implements hook_entity_bundle_field_info_alter(). */ @@ -54,9 +53,11 @@ function turnstile_entity_view_alter(array &$build, EntityInterface $entity, Ent \Drupal::logger('turnstile')->debug('Not a node, skipping Turnstile checks.'); return; } + /** @var \Drupal\node\NodeInterface $node */ + $node = $entity; // Check if the node has a price field and it's not empty. - if (!$entity->hasField('field_price') || $entity->get('field_price')->isEmpty()) { + if (!$node->hasField('field_price') || $node->get('field_price')->isEmpty()) { \Drupal::logger('turnstile')->debug('Node has no price, skipping Turnstile checks.'); return; } @@ -66,73 +67,50 @@ function turnstile_entity_view_alter(array &$build, EntityInterface $entity, Ent $enabled_types = $config->get('enabled_content_types') ?: []; // Check if this node type is enabled for paywall. - if (!in_array($entity->bundle(), $enabled_types)) { + if (!in_array($node->bundle(), $enabled_types)) { + // NOTE: This case is very strange: how can we have a field_price if + // turnstile is not enabled? \Drupal::logger('turnstile')->debug('Bundle not enabled with Turnstile, skipping payment.'); return; } // Get the original body content. - if (!$entity->hasField('body')) { + if (!$node->hasField('body')) { \Drupal::logger('turnstile')->debug('Note has no body, skipping payment.'); return; } - if ($entity->get('body')->isEmpty()) { + if ($node->get('body')->isEmpty()) { \Drupal::logger('turnstile')->debug('Note has empty body, skipping payment.'); return; } + $body_value = $node->get('body')->value; - $body_value = $entity->get('body')->value; - // Call the paywall check function with the node. - \Drupal::logger('turnstile')->debug('Checking for payment...'); - $transformed_body = turnstile_check_paywall_cookie($body_value, $entity); - - // Update the body in the build array. - // FIXME: this somehow does not work, the body is not there! - if (isset($build['body'][0]['#text'])) { - \Drupal::logger('turnstile')->debug('Transformed body for Turnstile.'); - $build['body'][0]['#text'] = $transformed_body; - } - else - { - \Drupal::logger('turnstile')->debug('Failed to inject transformed body.'); - } -} - - -/** - * Custom function to check paywall cookie and transform content. - * - * @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, NodeInterface $node) { - // Start session if not already started. + // Start session if not already started, we use it to persist payment status data. if (session_status() == PHP_SESSION_NONE) { session_start(); } + \Drupal::logger('turnstile')->debug('Checking for payment...'); // Check if user is a subscriber. + // NOTE: setting subscriber status is not yet supported! if (isset($_SESSION['turnstile_subscriber']) && $_SESSION['turnstile_subscriber'] === TRUE) { - \Drupal::logger('turnstile')->debug('Subscriber cookie detected, passing turnstile'); // FIXME: should set turnstile_subscriber to *expiration* time // of the subscription and check not for TRUE but for "greater // current time". - // Subscribers have full access to everything - return $body; + // FIXME: For now, subscribers have full access to everything + // in the future, we may want to have 50%-off subscriptions + // and others where we need to check the exact type of subscription. + \Drupal::logger('turnstile')->debug('Subscriber cookie detected, granting access.'); + return; } // Check if there's an existing order for this article. - $node_id = $node ? $node->id() : NULL; + $node_id = $node->id(); if (!$node_id) { // Strange, but not a case where we can do a payment. \Drupal::logger('turnstile')->debug('No payment due to lack of node ID...'); - return $body; + return; } if (isset($_SESSION['turnstile_orders'][$node_id])) { @@ -141,10 +119,12 @@ function turnstile_check_paywall_cookie($body, NodeInterface $node) { // Check if order is paid and not refunded. // FIXME: refunds are not really supported, do we care to // keep support for them here? - if (isset($order['paid']) && $order['paid'] === TRUE && - (!isset($order['refunded']) || $order['refunded'] === FALSE)) { + if (isset($order['paid']) && + (TRUE === $order['paid']) && + ( (! isset($order['refunded'])) || + (FALSE === $order['refunded'])) ) { \Drupal::logger('turnstile')->debug('Paid order detected, passing Turnstile.'); - return $body; + return; } // Check order status with backend if not marked as paid. @@ -157,7 +137,7 @@ function turnstile_check_paywall_cookie($body, NodeInterface $node) { // for refunds here as well! \Drupal::logger('turnstile')->debug('Updating order status to paid.'); $_SESSION['turnstile_orders'][$node_id]['paid'] = TRUE; - return $body; + return; } } } @@ -170,13 +150,78 @@ function turnstile_check_paywall_cookie($body, NodeInterface $node) { $config = \Drupal::config('turnstile.settings'); $grant_access_on_error = $config->get('grant_access_on_error') ?: true; if ($grant_access_on_error) { - return $body; + return; } - return 'Error: failed to setup order with payment backend'; + // FIXME: transform body into error message! + // 'Error: failed to setup order with payment backend' + return; } \Drupal::logger('turnstile')->debug('Rendering page with payment request.'); - return turnstile_render_preview_with_payment_button($body, $order_info); + + _turnstile_transform_body_recursive ($build, $node, $order_info); + + // FIXME: we probably need to invalidate caches using + // something like this (untested!) + // $build['body']['#cache']['contexts'][] = 'user.permissions'; + +} + + +/** + * Recursively traverses a render array to trim full body text to summary + * and append payment option. + * + * @param array &$build + * The render array. + * @param \Drupal\Core\node\NodeInterface $node + * The node being rendered. + * @param $order + * Order data + * @param int $depth + * Recursion depth (internal use). + * @param int $max_depth + * Max recursion depth. + */ +function _turnstile_transform_body_recursive(array &$build, NodeInterface $node, array $order, $depth = 0, $max_depth = 20) { + if ($depth > $max_depth) { + return; + } + + foreach ($build as $key => &$element) { + if (is_array($element)) { + // Check if this render array appears to be for the 'body' field. + if ( + (isset($element['#field_name']) && $element['#field_name'] === 'body') || + (isset($element['#type']) && $element['#type'] === 'processed_text' && isset($element['#text'])) || + $key === 'body' + ) { + if ($node->hasField('body') && !$node->get('body')->isEmpty()) { + $body_field = $node->get('body')->first(); + + $summary = $body_field->summary; + $full = $body_field->value; + $format = $body_field->format; + + // FIXME: use text_summary, or + // maybe better turnstile_truncate_content_safely? Not sure! + $replacement_text = $summary ?: text_summary($full, $format, 300); + + // Replace with processed summary or trimmed text. + $element = [ + '#type' => 'processed_text', + '#text' => $replacement_text . turnstile_render_payment_button ($order), + '#format' => $format, + '#cache' => ['contexts' => ['url.path']], // Optional caching metadata + ]; + } + } + else { + // Recurse into the element. + _turnstile_transform_body_recursive($element, $node, $order, $depth + 1, $max_depth); + } + } + } } @@ -273,19 +318,15 @@ function turnstile_truncate_content_safely($content, $length = 200) { /** - * Render content preview with payment button. + * Render payment button. * - * @param string $body - * The original body content. * @param array $order_info * Order information. * * @return string - * Rendered preview with payment button. + * Rendered payment button. */ -function turnstile_render_preview_with_payment_button($body, $order_info) { - // Safely truncate content to first few lines. - $preview = turnstile_truncate_content_safely($body, 200); +function turnstile_render_payment_button($order_info) { \Drupal::logger('turnstile')->debug('Generating payment request body'); // FIXME: eventually, we may want to at least give the option to @@ -297,9 +338,10 @@ function turnstile_render_preview_with_payment_button($body, $order_info) { $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; + return $payment_button; } + /** * Implements hook_form_FORM_ID_alter() for node forms. */