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 }