turnstile

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

commit 043b5e6d28b86825c0a1495553e572c7d5c7e87f
parent 15596dc229f08f18ec2094d4641eb1d69ae22ff9
Author: Christian Grothoff <christian@grothoff.org>
Date:   Tue, 30 Sep 2025 12:54:46 +0200

work on Turnstile settings form: validate inputs

Diffstat:
Mconfig/install/turnstile.settings.yml | 4++--
Mconfig/schema/turnstile.schema.yml | 3+++
Msrc/Form/TurnstileSettingsForm.php | 154++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
3 files changed, 155 insertions(+), 6 deletions(-)

diff --git a/config/install/turnstile.settings.yml b/config/install/turnstile.settings.yml @@ -1,4 +1,5 @@ enabled_content_types: - article payment_backend_url: '' -access_token: '' -\ No newline at end of file +access_token: '' +grant_access_on_error: true diff --git a/config/schema/turnstile.schema.yml b/config/schema/turnstile.schema.yml @@ -14,3 +14,6 @@ turnstile.settings: access_token: type: string label: 'Access token' + grant_access_on_error: + type: boolean + label: 'Disable paywall when payment backend is unavailable' diff --git a/src/Form/TurnstileSettingsForm.php b/src/Form/TurnstileSettingsForm.php @@ -98,6 +98,13 @@ class TurnstileSettingsForm extends ConfigFormBase { '#maxlength' => 255, ]; + $form['grant_access_on_error'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Disable Turnstile when payment backend is unavailable'), + '#description' => $this->t('Allows users gratis access when Turnstile is unable to communicate with the GNU Taler merchant backend. Use this setting to avoid exposing users to configuration errors.'), + '#default_value' => $config->get('grant_access_on_error') ?: '', + ]; + // FIXME: add options for subscription and discount token families + prices here! return parent::buildForm($form, $form_state); @@ -106,9 +113,148 @@ class TurnstileSettingsForm extends ConfigFormBase { /** * {@inheritdoc} */ + public function validateForm(array &$form, FormStateInterface $form_state) { + parent::validateForm($form, $form_state); + + // Test the access token and backend URL. + $payment_backend_url = $form_state->getValue('payment_backend_url'); + $access_token = $form_state->getValue('access_token'); + + if ( (!empty($payment_backend_url)) && + (! str_ends_with($payment_backend_url, '/')) ) + { + $form_state->setErrorByName('payment_backend_url', + $this->t('Payment backend URL must end with a "/".')); + $form_state->setErrorByName('access_token'); + return; + } + + if ( (!empty($access_token)) && + (! str_starts_with($access_token, 'secret-token:')) ) + { + $form_state->setErrorByName('payment_backend_url'); + $form_state->setErrorByName('access_token', + $this->t('Access token must begin with a "secret-token:".')); + return; + } + + if (!empty($payment_backend_url)) { + $parsed_url = parse_url($payment_backend_url); + $path = $parsed_url['path']; + // Remove "instances/$INSTANCE_ID/" to get the base URL (if present) + $cleaned_path = preg_replace('#^/instances/[^/]+/?#', '/', $path); + $base = $parsed_url['scheme'] . '://' . $parsed_url['host']; + $base_url = $base . $cleaned_path; + + try { + $client = \Drupal::httpClient(); + $response = $client->get( + $base_url . 'config', + [ + 'allow_redirects' => TRUE, + 'http_errors' => FALSE, + ]); + if ( ($response->getStatusCode() !== 200) || + (json_decode($response->getBody(), TRUE)['name'] + != 'taler-merchant') ) { + $form_state->setErrorByName('payment_backend_url', + $this->t('Invalid payment backend URL')); + $form_state->setErrorByName('access_token'); + return; + } + } + catch (\Exception $e) { + $form_state->setErrorByName('payment_backend_url', + $this->t('HTTP request failed:' . $e)); + $form_state->setErrorByName('access_token'); + return; + } + } + if (!empty($payment_backend_url) && !empty($access_token)) { + try { + $client = \Drupal::httpClient(); + $response = $client->get( + $payment_backend_url . 'private/orders?limit=1', + [ + 'headers' => [ + 'Authorization' => 'Bearer ' . $access_token, + ], + 'allow_redirects' => TRUE, + 'http_errors' => FALSE, + ] + ); + switch ($response->getStatusCode()) { + case 502: + $form_state->setErrorByName('payment_backend_url', + $this->t('Bad gateway (502) trying to access the merchant backend')); + $form_state->setErrorByName('access_token'); + return; + case 500: + $form_state->setErrorByName('payment_backend_url', + $this->t('Internal server error (500) of the merchant backend reported')); + $form_state->setErrorByName('access_token'); + return; + case 404: + $form_state->setErrorByName('payment_backend_url', + $this->t('The specified instance is unknown to the merchant backend')); + $form_state->setErrorByName('access_token'); + return; + case 403: + $form_state->setErrorByName('payment_backend_url'); + $form_state->setErrorByName('access_token', + $this->t('Access token not accepted by the merchant backend')); + return; + case 401: + $form_state->setErrorByName('payment_backend_url'); + $form_state->setErrorByName('access_token', + $this->t('Access token not accepted by the merchant backend')); + return; + case 204: + // Empty order list is OK + break; + case 200: + // Success is great + break; + default: + $form_state->setErrorByName('payment_backend_url', + $this->t('Unexpected response (' . $response->getStatusCode() . ') from merchant backend')); + $form_state->setErrorByName('access_token'); + return; + } + } + catch (\Exception $e) { + $form_state->setErrorByName('payment_backend_url', + $this->t('HTTP request failed:' . $e)); + $form_state->setErrorByName('access_token'); + return; + } + } + + // If the merchant backend is not configured at all, we allow the user + // to save the settings. But, we warn them if they did not set + // grant_access_on_error. + $grant_access_on_error = $form_state->getValue('grant_access_on_error'); + if ( (! $grant_access_on_error) && + (empty($payment_backend_url) || + empty($access_token)) ) { + $form_state->setErrorByName('payment_backend_url'); + $form_state->setErrorByName('access_token'); + $this->messenger()->addWarning( + $this->t('Warning: Merchant backend is not configured correctly. To keep the site working, you probably should set the "Disable Turnstile when payment backend is unavailable" option!')); + return; + } + } + + /** + * {@inheritdoc} + */ public function submitForm(array &$form, FormStateInterface $form_state) { $config = $this->config('turnstile.settings'); + // Test the access token and backend URL. + $payment_backend_url = $form_state->getValue('payment_backend_url'); + $access_token = $form_state->getValue('access_token'); + // Get old and new content types. $old_enabled_types = $config->get('enabled_content_types') ?: []; $new_enabled_types = array_filter($form_state->getValue('enabled_content_types')); @@ -128,13 +274,13 @@ class TurnstileSettingsForm extends ConfigFormBase { $this->removeFieldsFromContentTypes($types_to_remove); } - // FIXME: can we *test* the access token here and fail the - // form submission if access is not working? + $grant_access_on_error = $form_state->getValue('grant_access_on_error'); // Save configuration. $config->set('enabled_content_types', $new_enabled_types); - $config->set('payment_backend_url', $form_state->getValue('payment_backend_url')); - $config->set('access_token', $form_state->getValue('access_token')); + $config->set('payment_backend_url', $payment_backend_url); + $config->set('access_token', $access_token); + $config->set('grant_access_on_error', $grant_on_error); $config->save(); parent::submitForm($form, $form_state);