turnstile

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

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 }