wordpress-turnstile

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

class-content-filter.php (12823B)


      1 <?php
      2 /**
      3  * Content Filter
      4  *
      5  * Handles content filtering and paywall logic for protected posts.
      6  */
      7 
      8 if (!defined('ABSPATH')) {
      9     exit;
     10 }
     11 
     12 class Taler_Content_Filter {
     13 
     14     /**
     15      * Initialize the content filter
     16      */
     17     public static function init() {
     18         add_filter('the_content', array(__CLASS__, 'filter_content'), 10, 1);
     19         add_action('wp', array(__CLASS__, 'disable_cache_for_protected_content'));
     20     }
     21 
     22     /**
     23      * Disable caching for protected content
     24      */
     25     public static function disable_cache_for_protected_content() {
     26         if (!is_singular()) {
     27             return;
     28         }
     29 
     30         $post_id = get_the_ID();
     31         $price_category_id = get_post_meta($post_id, '_taler_price_category', true);
     32 
     33         if (!empty($price_category_id)) {
     34             // Disable page caching for protected content
     35             if (!defined('DONOTCACHEPAGE')) {
     36                 define('DONOTCACHEPAGE', true);
     37             }
     38             nocache_headers();
     39         }
     40     }
     41 
     42     /**
     43      * Filter post content to show paywall if needed
     44      *
     45      * @param string $content The post content
     46      * @return string Modified content or original content
     47      */
     48     public static function filter_content($content) {
     49         // Only apply on singular post views (not archives, search, etc.)
     50         if (!is_singular()) {
     51             return $content;
     52         }
     53 
     54         // Don't apply in admin or feeds
     55         if (is_admin() || is_feed()) {
     56             return $content;
     57         }
     58 
     59         $post = get_post();
     60         if (!$post) {
     61             return $content;
     62         }
     63 
     64         // Check if this post type is enabled for Turnstile
     65         $enabled_types = get_option('taler_turnstile_enabled_post_types', array('post'));
     66         if (!in_array($post->post_type, $enabled_types)) {
     67             return $content;
     68         }
     69 
     70         // Check if a price category is set
     71         $price_category_id = get_post_meta($post->ID, '_taler_price_category', true);
     72         if (empty($price_category_id)) {
     73             return $content;
     74         }
     75 
     76         $price_category = Taler_Price_Category::get($price_category_id);
     77         if (!$price_category) {
     78             error_log('Taler Turnstile: Post has invalid price category');
     79             return $content;
     80         }
     81 
     82         // Check if user has subscription that grants full access
     83         $full_subscriptions = Taler_Price_Category::get_full_subscriptions($price_category['prices']);
     84         foreach ($full_subscriptions as $subscription_id) {
     85             if (self::is_subscriber($subscription_id)) {
     86                 self::debug_log('Subscriber access granted');
     87                 return $content;
     88             }
     89         }
     90 
     91         // Check if this session already has access
     92         if (self::has_session_access($post->ID)) {
     93             self::debug_log('Session access already granted');
     94             return $content;
     95         }
     96 
     97         // Check if there's an existing order and if it's been paid
     98         $order_info = self::get_node_order_info($post->ID);
     99         if ($order_info) {
    100             $order_status = Taler_Merchant_API::check_order_status($order_info['order_id']);
    101 
    102             if ($order_status && $order_status['paid']) {
    103                 self::debug_log('Taler Turnstile: Order was paid, granting session access');
    104                 self::grant_session_access($post->ID);
    105 
    106                 if (!empty($order_status['subscription_slug'])) {
    107                     self::debug_log('Taler Turnstile: Subscription was purchased, granting subscription access');
    108                     self::grant_subscriber_access(
    109                         $order_status['subscription_slug'],
    110                         $order_status['subscription_expiration']
    111                     );
    112                 }
    113 
    114                 return $content;
    115             }
    116 
    117             // Check if order expired
    118             if ($order_status &&
    119                 isset($order_status['order_expiration']) &&
    120                 $order_status['order_expiration'] < time() + 60) {
    121                 // Order expired or will expire soon, ignore it
    122                 $order_info = null;
    123             }
    124 
    125             if (!$order_status) {
    126                 $order_info = null;
    127             }
    128         }
    129 
    130         // Need to create a new order if we don't have a valid one
    131         if (!$order_info) {
    132             self::debug_log('Creating new order for ' . $post->ID);
    133             $order_info = Taler_Merchant_API::create_order($post->ID);
    134         }
    135 
    136         if (!$order_info) {
    137             $grant_access_on_error = get_option('taler_turnstile_grant_access_on_error', false);
    138 
    139             if ($grant_access_on_error) {
    140                 error_log('Taler Turnstile: Could not setup order, disabling Turnstile');
    141                 return $content;
    142             }
    143             error_log('Taler Turnstile: Failed to setup order with Taler merchant backend, returning error page');
    144 
    145             return self::render_error_message();
    146         }
    147 
    148         // Store order info in session
    149         self::store_order_node_mapping($post->ID, $order_info);
    150 
    151         self::debug_log('Showing paywall page for ' . $post->ID);
    152         // User needs to pay - show teaser + payment button
    153         return self::render_paywall($post, $order_info);
    154     }
    155 
    156     /**
    157      * Render the paywall with excerpt and payment button
    158      *
    159      * @param WP_Post $post The post object
    160      * @param array $order_info Order information
    161      * @return string HTML for paywall
    162      */
    163     private static function render_paywall($post, $order_info) {
    164         $excerpt = $post->post_excerpt;
    165 
    166         // If no excerpt, generate one from content
    167         if (empty($excerpt)) {
    168             $excerpt = wp_trim_words(strip_shortcodes($post->post_content), 55, '...');
    169         }
    170 
    171         ob_start();
    172         ?>
    173         <div class="taler-turnstile-paywall">
    174             <div class="taler-turnstile-excerpt">
    175                 <?php echo wpautop($excerpt); ?>
    176             </div>
    177 
    178             <div class="taler-turnstile-payment-wrapper">
    179                 <?php echo self::render_payment_button($order_info, $post->post_title); ?>
    180             </div>
    181         </div>
    182         <?php
    183         return ob_get_clean();
    184     }
    185 
    186     /**
    187      * Render the payment button with QR code
    188      *
    189      * @param array $order_info Order information
    190      * @param string $title Post title
    191      * @return string HTML for payment button
    192      */
    193     private static function render_payment_button($order_info, $title) {
    194         $order_id = $order_info['order_id'];
    195         $session_id = $order_info['session_id'];
    196         $payment_url = $order_info['payment_url'];
    197         $price_hint = $order_info['price_hint'];
    198         $subscription_hint = $order_info['subscription_hint'];
    199 
    200         ob_start();
    201         ?>
    202         <div class="taler-payment-container">
    203             <h3><?php esc_html_e('Payment Required', 'taler-turnstile'); ?></h3>
    204             <p><?php esc_html_e('To access the full content, please complete the payment using GNU Taler.', 'taler-turnstile'); ?></p>
    205 
    206             <div class="taler-payment-methods">
    207                 <div class="taler-qr-section">
    208                     <h4><?php esc_html_e('Scan QR Code', 'taler-turnstile'); ?></h4>
    209                     <div class="taler-turnstile-qr-code-container"
    210                          data-payment-url="<?php echo esc_attr($payment_url); ?>"
    211                          data-session-id="<?php echo esc_attr($session_id); ?>">
    212                     </div>
    213                     <p class="description"><?php esc_html_e('Scan this QR code with your Taler wallet app', 'taler-turnstile'); ?></p>
    214                 </div>
    215 
    216                 <div class="taler-button-section">
    217                     <h4><?php esc_html_e('Or Click to Pay', 'taler-turnstile'); ?></h4>
    218                     <a href="<?php echo esc_url($payment_url); ?>"
    219                        class="button button-primary taler-pay-button">
    220                         <?php esc_html_e('Pay with GNU Taler', 'taler-turnstile'); ?>
    221                     </a>
    222                 </div>
    223             </div>
    224 
    225             <h4 class="price_hint_title"><?php esc_html_e('Price per article', 'taler-turnstile'); ?></h3>
    226             <div class="taler-price-per-article-hint">
    227                <?php esc_html_e($price_hint); ?>
    228             </div>
    229             <h4 class="subscription_hint_title"><?php esc_html_e('Acceptable subscriptions', 'taler-turnstile'); ?></h3>
    230             <div class="taler-available-subscription-hint">
    231                <?php esc_html_e($subscription_hint); ?>
    232             </div>
    233             <!-- div class="taler-payment-info">
    234                 <p class="taler-order-id">
    235                     /* translators: placeholder is the order ID of the merchant backend */
    236                     <small><?php printf(esc_html__('Order ID: %s', 'taler-turnstile'), esc_html($order_id)); ?></small>
    237                 </p>
    238             </div -->
    239         </div>
    240         <?php
    241         return ob_get_clean();
    242     }
    243 
    244     /**
    245      * Render error message when payment system is unavailable
    246      *
    247      * @return string HTML for error message
    248      */
    249     private static function render_error_message() {
    250         ob_start();
    251         ?>
    252         <div class="taler-turnstile-error">
    253             <p><strong><?php esc_html_e('Payment System Temporarily Unavailable', 'taler-turnstile'); ?></strong></p>
    254             <p><?php esc_html_e('We are experiencing technical difficulties with our payment system. Please try again later.', 'taler-turnstile'); ?></p>
    255         </div>
    256         <?php
    257         return ob_get_clean();
    258     }
    259 
    260     /**
    261      * Grant subscription access for this visitor
    262      *
    263      * @param string $subscription_slug The subscription slug
    264      * @param int $expiration Unix timestamp of expiration
    265      */
    266     private static function grant_subscriber_access($subscription_slug, $expiration) {
    267         if (session_status() === PHP_SESSION_NONE) {
    268             session_start();
    269         }
    270 
    271         if (!isset($_SESSION['taler_turnstile_subscriptions'])) {
    272             $_SESSION['taler_turnstile_subscriptions'] = array();
    273         }
    274 
    275         $_SESSION['taler_turnstile_subscriptions'][$subscription_slug] = $expiration;
    276     }
    277 
    278     /**
    279      * Check if visitor has active subscription
    280      *
    281      * @param string $subscription_slug The subscription slug
    282      * @return bool True if subscribed and not expired
    283      */
    284     private static function is_subscriber($subscription_slug) {
    285         if (session_status() === PHP_SESSION_NONE) {
    286             session_start();
    287         }
    288 
    289         $expiration = $_SESSION['taler_turnstile_subscriptions'][$subscription_slug] ?? 0;
    290         return $expiration >= time();
    291     }
    292 
    293     /**
    294      * Grant session access to a specific post
    295      *
    296      * @param int $post_id The post ID
    297      */
    298     private static function grant_session_access($post_id) {
    299         if (session_status() === PHP_SESSION_NONE) {
    300             session_start();
    301         }
    302 
    303         if (!isset($_SESSION['taler_turnstile_access'])) {
    304             $_SESSION['taler_turnstile_access'] = array();
    305         }
    306 
    307         $_SESSION['taler_turnstile_access'][$post_id] = true;
    308     }
    309 
    310     /**
    311      * Check if session has access to a post
    312      *
    313      * @param int $post_id The post ID
    314      * @return bool True if session has access
    315      */
    316     private static function has_session_access($post_id) {
    317         if (session_status() === PHP_SESSION_NONE) {
    318             session_start();
    319         }
    320 
    321         return isset($_SESSION['taler_turnstile_access'][$post_id]) &&
    322                $_SESSION['taler_turnstile_access'][$post_id] === true;
    323     }
    324 
    325     /**
    326      * Store order-to-node mapping in session
    327      *
    328      * @param int $post_id The post ID
    329      * @param array $order_info Order information
    330      */
    331     private static function store_order_node_mapping($post_id, $order_info) {
    332         if (session_status() === PHP_SESSION_NONE) {
    333             session_start();
    334         }
    335 
    336         if (!isset($_SESSION['taler_turnstile_node_orders'])) {
    337             $_SESSION['taler_turnstile_node_orders'] = array();
    338         }
    339 
    340         $_SESSION['taler_turnstile_node_orders'][$post_id] = $order_info;
    341     }
    342 
    343     /**
    344      * Get order info for a post
    345      *
    346      * @param int $post_id The post ID
    347      * @return array|null Order information or null
    348      */
    349     private static function get_node_order_info($post_id) {
    350         if (session_status() === PHP_SESSION_NONE) {
    351             session_start();
    352         }
    353 
    354         // FIXME: reviewers say sanitization/validation is needed here
    355         // FIXME: reviewers claim this data can be
    356         // **manipulated** by the sender of the request!??!??
    357         return $_SESSION['taler_turnstile_node_orders'][$post_id] ?? NULL;
    358     }
    359 
    360     /**
    361      * Helper function for logging that is easily turned off.
    362      *
    363      * @param string $msg The log message
    364      */
    365     private static function debug_log($msg) {
    366         // error_log($msg);
    367     }
    368 }