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:
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);