commit 8e3a77bd2c1a9982692b42091ebf6d022a8f1c4e
parent 043b5e6d28b86825c0a1495553e572c7d5c7e87f
Author: Christian Grothoff <christian@grothoff.org>
Date: Thu, 2 Oct 2025 23:29:30 +0200
add debug controller and TalerPriceListFormat validator (not yet working nicely)
Diffstat:
4 files changed, 250 insertions(+), 2 deletions(-)
diff --git a/src/Controller/DebugController.php b/src/Controller/DebugController.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Drupal\turnstile\Controller;
+use Drupal\Core\Controller\ControllerBase;
+
+use Drupal\Core\Form\ConfigFormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\Core\Entity\Entity\EntityFormDisplay;
+use Drupal\Core\Entity\Entity\EntityViewDisplay;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+
+/**
+ * Turnstile debug controller.
+ */
+class DebugController extends ControllerBase {
+
+
+ /**
+ * Returns a renderable array for a test page.
+ *
+ * return []
+ */
+ public function content() {
+
+
+$config = \Drupal::config('turnstile.settings');
+$enabled_types = $config->get('enabled_content_types') ?: [];
+
+foreach ($enabled_types as $bundle) {
+ $field_config = \Drupal\field\Entity\FieldConfig::loadByName('node', $bundle, 'field_price');
+ if ($field_config) {
+ $field_config->setConstraints(['TalerPriceListFormat' => []]);
+ $field_config->save();
+ }
+}
+
+ // Temporary debugging code - add to a hook or controller
+ $constraint_manager = \Drupal::service('validation.constraint');
+ $definitions = $constraint_manager->getDefinitions();
+ $build = [
+ '#markup' => $this->t(isset($definitions['TalerPriceListFormat']) ? 'ok' : 'bad'),
+ ];
+
+ $field_config = FieldConfig::loadByName('node', 'article', 'field_price');
+ if ($field_config) {
+ $constraints = $field_config->getConstraints();
+ $build = [
+ '#markup' => $this->t('constrained' . implode (",", $constraints)),
+ ];
+
+ }
+
+ return $build;
+ }
+
+}
+\ No newline at end of file
diff --git a/src/Plugin/Validation/Constraint/TalerPriceListFormatConstraint.php b/src/Plugin/Validation/Constraint/TalerPriceListFormatConstraint.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Drupal\turnstile\Plugin\Validation\Constraint;
+
+use Symfony\Component\Validator\Constraint;
+
+/**
+ * Checks that the price format is valid.
+ *
+ * @Constraint(
+ * id = "TalerPriceListFormat",
+ * label = @Translation("Taler Price List Format", context = "Validation"),
+ * type = "string",
+ * )
+ */
+class TalerPriceListFormatConstraint extends Constraint {
+
+ public $invalidFormat = 'The price list "@value" is not valid. Please enter a valid price list (e.g., "EUR:1" or "USD:3,CHF:2" to support payment in multiple currencies).';
+
+ public $invalidCurrency = 'Price uses currency "@currency" that is not supported by the Taler merchant backend. Please only use supported currencies.';
+
+ public $backendTrouble = 'Failed to fetch configuration data from Taler merchant backend.';
+
+ public $internalError = 'Failed to validate price list: @error';
+
+}
+\ No newline at end of file
diff --git a/src/Plugin/Validation/Constraint/TalerPriceListFormatConstraintValidator.php b/src/Plugin/Validation/Constraint/TalerPriceListFormatConstraintValidator.php
@@ -0,0 +1,150 @@
+<?php
+
+namespace Drupal\turnstile\Plugin\Validation\Constraint;
+
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\ConstraintValidator;
+
+/**
+ * Validates the TalerPriceListFormat constraint.
+ */
+class TalerPriceListFormatConstraintValidator extends ConstraintValidator {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validate($item, Constraint $constraint) {
+ \Drupal::logger('turnstile')->debug('Validating price list');
+
+ if (!isset($item)) {
+ return;
+ }
+
+ $pricelist = $item->value;
+
+ // Empty is OK, simply gratis.
+ if (empty($pricelist)) {
+ return;
+ }
+
+
+ $pattern = '/^(([A-Za-z]{1,11}:\d+(\.\d{1,8})?)(\s*,\s*[A-Za-z]{1,11}:\d+(\.\d{1,8})?)*$)?/';
+
+ if (preg_match($pattern, $pricelist) !== 1) {
+ $this->context->addViolation($constraint->invalidFormat, [
+ '@value' => $pricelist,
+ ]);
+ return;
+ }
+
+ $config = \Drupal::config('turnstile.settings');
+ $payment_backend_url = $config->get('payment_backend_url');
+ $grant_access_on_error = $config->get('grant_access_on_error');
+
+ if (empty($payment_backend_url)) {
+ // If backend is not configured, allow the pricelist if
+ // grant_access_on_error is set.
+ \Drupal::logger('turnstile')->error('Taler merchant backend not configured; cannot validate pricelist');
+ if ($grant_access_on_error) {
+ return;
+ }
+ $this->context->addViolation($constraint->backendTrouble);
+ return;
+ }
+
+ try {
+ // Fetch backend configuration.
+ $client = \Drupal::httpClient();
+
+ $config_url = $payment_backend_url . 'config';
+ $response = $client->get($config_url, [
+ 'allow_redirects' => TRUE,
+ 'http_errors' => FALSE,
+ 'timeout' => 5,
+ ]);
+
+ if ($response->getStatusCode() !== 200) {
+ // If backend is unavailable, allow the pricelist if
+ // grant_access_on_error is set.
+ \Drupal::logger('turnstile')->error('Taler merchant backend unavailable, cannot validate pricelist');
+ $this->backendTrouble = TRUE;
+ if ($grant_access_on_error) {
+ return;
+ }
+ $this->context->addViolation($constraint->backendTrouble);
+ return;
+ }
+
+ $backend_config = json_decode($response->getBody(), TRUE);
+ if (!$backend_config || !is_array($backend_config)) {
+ // Invalid response, fallback to grant_access_on_error setting.
+ \Drupal::logger('turnstile')->error('Taler merchant backend returned invalid /config response; cannot validate pricelist');
+ $this->backendTrouble = TRUE;
+ if ($grant_access_on_error) {
+ return;
+ }
+ $this->context->addViolation($constraint->backendTrouble);
+ return;
+ }
+
+ if (! isset($backend_config['currencies']))
+ {
+ \Drupal::logger('turnstile')->error('Backend returned malformed response for /config');
+ $this->context->addViolation($constraint->backendTrouble);
+ return;
+ }
+
+ // Parse and validate each amount in the comma-separated list.
+ $amounts = preg_split('/\s*,\s*/', trim($pricelist));
+
+ foreach ($amounts as $amount) {
+ // Extract currency from each amount (format: CURRENCY:NUMBER[.FRACTION]).
+ if (preg_match('/^([A-Za-z]{1,11}):/', $amount, $matches)) {
+ $currency = $matches[1];
+
+ if (!$this->checkCurrency($currency, $backend_config)) {
+ $this->context->addViolation($constraint->invalidCurrency, [
+ '@currency' => $currency
+ ]);
+ return;
+ }
+ }
+ }
+ return;
+
+ } catch (\Exception $e) {
+
+ // On exception, fall back to grant_access_on_error setting.
+ \Drupal::logger('turnstile')->error('Failed to validate pricelist with backend: @error', [
+ '@error' => $e->getMessage(),
+ ]);
+ if ($grant_access_on_error) {
+ return;
+ }
+ $this->context->addViolation($constraint->invalidCurrency, [
+ '@error' => $e->getMessage(),
+ ]);
+ return;
+ }
+ }
+
+
+ /**
+ * Check if a currency is supported by the merchant backend.
+ *
+ * @param string $currency
+ * The currency code to check.
+ * @param array $backend_config
+ * The decoded JSON configuration from the payment backend.
+ *
+ * @return bool
+ * TRUE if the currency is supported, FALSE otherwise.
+ */
+ protected function checkCurrency($currency, array $backend_config) {
+ $currencies = $backend_config['currencies'];
+ if (! isset($currencies[strtoupper($currency)]))
+ return FALSE;
+ return TRUE;
+ }
+
+}
+\ No newline at end of file
diff --git a/turnstile.routing.yml b/turnstile.routing.yml
@@ -6,4 +6,12 @@ turnstile.settings:
requirements:
_permission: 'administer Turnstile'
options:
- _admin_route: TRUE
-\ No newline at end of file
+ _admin_route: TRUE
+
+turnstile.debughook:
+ path: '/turndebug'
+ defaults:
+ _controller: '\Drupal\turnstile\Controller\DebugController::content'
+ _title: 'Turnstile Debugger'
+ requirements:
+ _permission: 'access content'
+\ No newline at end of file