taler_turnstile.module (10054B)
1 <?php 2 3 /** 4 * @file 5 * Main module file for Turnstile. 6 */ 7 8 use Drupal\Core\Entity\EntityInterface; 9 use Drupal\Core\Entity\EntityTypeInterface; 10 use Drupal\Core\Entity\Display\EntityViewDisplayInterface; 11 use Drupal\node\NodeInterface; 12 13 14 /** 15 * Implements hook_form_FORM_ID_alter() for node forms. Adds a 16 * description for the Turnstile price category field. 17 */ 18 function taler_turnstile_form_node_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) { 19 $node = $form_state->getFormObject()->getEntity(); 20 $config = \Drupal::config('taler_turnstile.settings'); 21 $enabled_types = $config->get('enabled_content_types') ?: []; 22 23 // Only show price field on enabled content types. 24 if (! in_array($node->bundle(), $enabled_types)) { 25 return; 26 } 27 if (! isset($form['field_taler_turnstile_prcat'])) { 28 return; 29 } 30 $form['field_taler_turnstile_prcat']['#group'] = 'meta'; 31 $form['field_taler_turnstile_prcat']['widget'][0]['value']['#description'] = t('Set a price category to enable paywall protection for this content.'); 32 33 // Load all price categories for the description. 34 $price_categories = \Drupal::entityTypeManager() 35 ->getStorage('taler_turnstile_price_category') 36 ->loadMultiple(); 37 38 $category_list = []; 39 foreach ($price_categories as $category) { 40 $category_list[] = $category->label() . ': ' . $category->getDescription(); 41 } 42 43 $description = t('Select a price category to enable paywall protection for this content.'); 44 if (!empty($category_list)) { 45 $description .= '<br><br><strong>' . t('Available categories:') . '</strong><ul><li>' 46 . implode('</li><li>', $category_list) . '</li></ul>'; 47 } 48 49 $form['field_taler_turnstile_prcat']['widget']['#description'] = $description; 50 } 51 52 53 /** 54 * Implements hook_entity_view_alter(). Transforms the body of an entity to 55 * show the Turnstile dialog instead of the full body if the user needs 56 * to pay to see the full article. 57 */ 58 function taler_turnstile_entity_view_alter(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display) { 59 // Only process nodes with turnstile enabled 60 if ($entity->getEntityTypeId() !== 'node') { 61 return; 62 } 63 /** @var \Drupal\node\NodeInterface $node */ 64 $node = $entity; 65 66 if (!$node->hasField('field_taler_turnstile_prcat')) { 67 return; 68 } 69 70 /** @var \Drupal\Core\Field\EntityReferenceFieldItemList $field */ 71 $field = $node->get('field_taler_turnstile_prcat'); 72 if ($field->isEmpty()) { 73 \Drupal::logger('taler_turnstile')->debug('No price category selected'); 74 return FALSE; 75 } 76 77 /** @var TurnstilePriceCategory $price_category */ 78 $price_category = $field->entity; 79 if (! $price_category) { 80 \Drupal::logger('taler_turnstile')->debug('Node has no price category, skipping payment.'); 81 return; 82 } 83 84 $view_mode = $display->getMode(); 85 if ($view_mode !== 'full') { 86 \Drupal::logger('taler_turnstile')->debug('Turnstile only active for "Full" view mode.'); 87 return; 88 } 89 90 $subscriptions = $price_category->getFullSubscriptions(); 91 foreach ($subscriptions as $subscription_id) { 92 if (_taler_turnstile_is_subscriber ($subscription_id)) { 93 \Drupal::logger('taler_turnstile')->debug('Subscriber detected, granting access.'); 94 return; 95 } 96 } 97 98 // Disable page cache, this page is personalized! 99 \Drupal::service('page_cache_kill_switch')->trigger(); 100 101 $node_id = $node->id(); 102 if (_taler_turnstile_has_session_access($node_id)) { 103 \Drupal::logger('taler_turnstile')->debug('Session has access to this node.'); 104 return; 105 } 106 107 /** @var \Drupal\taler_turnstile\TalerMerchantApiService $api_service */ 108 $api_service = \Drupal::service('taler_turnstile.api_service'); 109 110 $order_info = _taler_turnstile_get_node_order_info ($node_id); 111 if ($order_info) { 112 \Drupal::logger('taler_turnstile')->debug('Found existing order @ORDER_ID for this session.', [ '@ORDER_ID' => $order_info['order_id'] ]); 113 // We have an existing order, check if it was paid 114 $order_id = $order_info['order_id']; 115 $order_status = $api_service->checkOrderStatus($order_info['order_id']); 116 if ($order_status && $order_status['paid']) { 117 \Drupal::logger('taler_turnstile')->debug('Order was paid, granting session access.'); 118 _taler_turnstile_grant_session_access($node_id); 119 if ($order_status['subscription_slug'] ?? FALSE) { 120 \Drupal::logger('taler_turnstile')->debug('Subscription was purchased, granting subscription access.'); 121 $subscription_slug = $order_status['subscription_slug']; 122 $expiration = $order_status['subscription_expiration']; 123 _taler_turnstile_grant_subscriber_access ($subscription_slug, $expiration); 124 } 125 return; 126 } 127 if ($order_status && 128 ($order_status['order_expiration'] ?? 0) < time() + 60) { 129 // If order expired (or would expire in less than one minute, 130 // so too soon for the user to still pay it), then ignore it! 131 $order_info = NULL; 132 } 133 if (!$order_status) 134 { 135 $order_info = NULL; 136 } 137 else 138 { 139 \Drupal::logger('taler_turnstile')->debug('Order expires in @future seconds, not creating new one.', ['@future' => ($order_status['order_expiration'] ?? 0) - time ()] ); 140 } 141 } 142 if (!$order_info) { 143 // Need to try to create a new order 144 $order_info = $api_service->createOrder($node); 145 } 146 if (!$order_info) { 147 \Drupal::logger('taler_turnstile')->warning('Failed to setup order with Taler merchant backend!'); 148 $config = \Drupal::config('taler_turnstile.settings'); 149 $grant_access_on_error = $config->get('grant_access_on_error') ?? TRUE; 150 if ($grant_access_on_error) { 151 \Drupal::logger('taler_turnstile')->debug('Could not setup order, disabling Turnstile.'); 152 return; 153 } 154 $pay_button = [ 155 '#markup' => '<div class="taler-turnstile-error">' . t('Payment system temporarily unavailable. Please try again later.') . '</div>', 156 ]; 157 } 158 else 159 { 160 _taler_turnstile_store_order_node_mapping($node_id, $order_info); 161 $pay_button = [ 162 '#theme' => 'taler_turnstile_payment_button', 163 '#order_id' => $order_info['order_id'], 164 '#session_id' => $order_info['session_id'], 165 '#payment_url' => $order_info['payment_url'], 166 '#node_title' => $node->getTitle(), 167 '#price_hint' => $price_category->getPriceHint(), 168 '#subscription_hint' => $price_category->getSubscriptionHint(), 169 '#attached' => [ 170 'library' => ['taler_turnstile/payment_button'], 171 ], 172 ]; 173 } 174 // User needs to pay - replace full content with teaser + payment button 175 // Generate teaser view mode 176 $view_builder = \Drupal::entityTypeManager()->getViewBuilder('node'); 177 $teaser_build = $view_builder->view($entity, 'teaser'); 178 179 // Replace the build array with teaser content 180 // Keep important metadata from original build (?) 181 $build = [ 182 '#cache' => ['contexts' => ['url']], 183 '#weight' => $build['#weight'] ?? 0, 184 ]; 185 186 // Add teaser content 187 $build['teaser'] = [ 188 '#type' => 'container', 189 '#attributes' => ['class' => ['taler-turnstile-teaser-wrapper']], 190 'content' => $teaser_build, 191 '#weight' => 0, 192 ]; 193 194 // Add payment button 195 $build['payment_button'] = [ 196 '#type' => 'container', 197 '#attributes' => ['class' => ['taler-turnstile-payment-wrapper']], 198 'button' => $pay_button, 199 '#weight' => 10, 200 ]; 201 } 202 203 204 /** 205 * Helper function to grant subscription access for this 206 * visitor to the given node ID until the given expiration time. 207 */ 208 function _taler_turnstile_grant_subscriber_access($subscription_slug, $expiration) { 209 $session = \Drupal::request()->getSession(); 210 $access_data = $session->get('taler_turnstile_subscriptions', []); 211 $access_data[$subscription_slug] = $expiration; 212 $session->set('taler_turnstile_subscriptions', $access_data); 213 } 214 215 216 /** 217 * Helper function to check if this session is currently 218 * subscribed on the given type of subscription. 219 */ 220 function _taler_turnstile_is_subscriber($subscription_slug) { 221 $session = \Drupal::request()->getSession(); 222 $access_data = $session->get('taler_turnstile_subscriptions', []); 223 return ($access_data[$subscription_slug] ?? 0) >= time(); 224 } 225 226 227 /** 228 * Helper function to grant session access for this 229 * visitor to the given node ID. 230 */ 231 function _taler_turnstile_grant_session_access($node_id) { 232 $session = \Drupal::request()->getSession(); 233 $access_data = $session->get('taler_turnstile_access', []); 234 $access_data[$node_id] = TRUE; 235 $session->set('taler_turnstile_access', $access_data); 236 } 237 238 239 /** 240 * Helper function to check session access. Checks if this 241 * visitor has been granted access to the given $node_id. 242 */ 243 function _taler_turnstile_has_session_access($node_id) { 244 $session = \Drupal::request()->getSession(); 245 $access_data = $session->get('taler_turnstile_access', []); 246 return $access_data[$node_id] ?? FALSE; 247 } 248 249 250 /** 251 * Store the mapping between order_id and node_id. 252 * Uses session to track which orders belong to which nodes. 253 */ 254 function _taler_turnstile_store_order_node_mapping($node_id, $order_info) { 255 $session = \Drupal::request()->getSession(); 256 $node_orders = $session->get('taler_turnstile_node_orders', []); 257 $node_orders[$node_id] = $order_info; 258 $session->set('taler_turnstile_node_orders', $node_orders); 259 } 260 261 262 /** 263 * Get the order_info associated with a node_id. 264 */ 265 function _taler_turnstile_get_node_order_info($node_id) { 266 $session = \Drupal::request()->getSession(); 267 $node_orders = $session->get('taler_turnstile_node_orders', []); 268 return $node_orders[$node_id] ?? NULL; 269 } 270 271 272 /** 273 * Implements hook_theme(). 274 */ 275 function taler_turnstile_theme() { 276 return [ 277 'taler_turnstile_payment_button' => [ 278 'variables' => [ 279 'order_id' => NULL, 280 'session_id' => NULL, 281 'payment_url' => NULL, 282 'node_title' => NULL, 283 'price_hint' => NULL, 284 'subscription_hint' => NULL, 285 ], 286 'template' => 'taler-turnstile-payment-button', 287 ], 288 'taler_turnstile_settings' => [ 289 'variables' => [ 290 'config' => NULL, 291 ], 292 ], 293 ]; 294 }