altcha-1.0.0/src/Form/AltchaSettingsForm.php

src/Form/AltchaSettingsForm.php
<?php

namespace Drupal\altcha\Form;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\TypedConfigManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\File\FileUrlGeneratorInterface;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface;
use Drupal\altcha\SecretManager;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Configure ALTCHA settings for this site.
 */
class AltchaSettingsForm extends ConfigFormBase {

  /**
   * The ALTCHA SaaS API challenge path.
   */
  const string SAAS_API_CHALLENGE_PATH = '/api/v1/challenge';

  /**
   * The ALTCHA sentinel API path.
   */
  const string SENTINEL_API_CHALLENGE_PATH = '/v1/challenge';

  /**
   * Constructs an ALTCHA settings form.
   *
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   The factory for configuration objects.
   * @param \Drupal\Core\Config\TypedConfigManagerInterface $typedConfigManager
   *   The typed config manager.
   * @param \Drupal\altcha\SecretManager $secretManager
   *   The ALTCHA secret manager.
   * @param \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $streamWrapperManager
   *   The stream wrapper manager.
   * @param \Drupal\Core\File\FileUrlGeneratorInterface $fileUrlGenerator
   *   The file url generator service.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
   *   The module handler.
   */
  public function __construct(
    ConfigFactoryInterface $config_factory,
    TypedConfigManagerInterface $typedConfigManager,
    protected SecretManager $secretManager,
    protected StreamWrapperManagerInterface $streamWrapperManager,
    protected FileUrlGeneratorInterface $fileUrlGenerator,
    protected ModuleHandlerInterface $moduleHandler,
  ) {
    parent::__construct($config_factory, $typedConfigManager);
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container): static {
    return new static(
      $container->get('config.factory'),
      $container->get('config.typed'),
      $container->get('altcha.secret_manager'),
      $container->get('stream_wrapper_manager'),
      $container->get('file_url_generator'),
      $container->get('module_handler')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function getFormId(): string {
    return 'altcha_settings';
  }

  /**
   * {@inheritdoc}
   */
  protected function getEditableConfigNames(): array {
    return ['altcha.settings'];
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state): array {
    $form['integration_type'] = [
      '#type' => 'select',
      '#title' => $this->t('Integration type'),
      '#options' => [
        'self_hosted' => $this->t('Self-hosted'),
        'sentinel_api' => $this->t('Sentinel (API key needed)'),
        'saas_api' => $this->t('SaaS (API key needed - deprecated)'),
      ],
      '#config_target' => 'altcha.settings:integration_type',
    ];

    $form['self_hosted'] = [
      '#type' => 'details',
      '#title' => $this->t('Self-hosted settings'),
      '#open' => TRUE,
      '#states' => [
        'visible' => [
          ':input[name=integration_type]' => ['value' => 'self_hosted'],
        ],
      ],
    ];

    $form['self_hosted']['secret_key_status'] = [
      '#type' => 'item',
      '#markup' => $this->getSecretKeyStatusMessage(),
      '#prefix' => '<div id="altcha-secret-key-status">',
      '#suffix' => '</div>',
    ];

    $form['self_hosted']['secret_key_regenerate'] = [
      '#type' => 'button',
      '#value' => $this->t('Regenerate secret key'),
      '#limit_validation_errors' => [],
      '#ajax' => [
        'callback' => '::regenerateSecretKey',
        'wrapper' => 'altcha-secret-key-status',
        'event' => 'click',
      ],
    ];

    $form['saas_api'] = [
      '#type' => 'details',
      '#title' => $this->t('SaaS API settings'),
      '#open' => TRUE,
      '#states' => [
        'visible' => [
          ':input[name=integration_type]' => ['value' => 'saas_api'],
        ],
      ],
    ];

    $form['saas_api']['saas_api_key'] = [
      '#type' => 'textfield',
      '#title' => $this->t('API Key'),
      '#description' => $this->t('Enter an API key which has the "AntiSpam API" feature enabled in the ALTCHA UI. More info on API keys can be found <a href="https://altcha.org/docs/api/api_keys" target="_blank">here</a>.'),
      '#placeholder' => 'key_*********************',
      '#states' => [
        'required' => [
          ':input[name=integration_type]' => ['value' => 'saas_api'],
        ],
      ],
      '#config_target' => 'altcha.settings:saas_api_key',
    ];

    $form['saas_api']['saas_api_region'] = [
      '#type' => 'select',
      '#title' => $this->t('API Region'),
      '#description' => $this->t('Choose the ALTCHA API region. More info can be found <a href="https://altcha.org/docs/api/regions" target="_blank">here</a>.'),
      '#options' => static::getRegionUrlMap(),
      '#states' => [
        'required' => [
          ':input[name=integration_type]' => ['value' => 'saas_api'],
        ],
      ],
      '#config_target' => 'altcha.settings:saas_api_region',
    ];

    $form['sentinel'] = [
      '#type' => 'details',
      '#title' => $this->t('Sentinel settings'),
      '#open' => TRUE,
      '#states' => [
        'visible' => [
          ':input[name=integration_type]' => ['value' => 'sentinel_api'],
        ],
      ],
    ];

    $form['sentinel']['sentinel_api_url'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Sentinel base URL'),
      '#description' => $this->t('Configure the sentinel base url.'),
      '#placeholder' => 'https://sentinel.example.com',
      '#states' => [
        'required' => [
          ':input[name=integration_type]' => ['value' => 'sentinel_api'],
        ],
      ],
      '#config_target' => 'altcha.settings:sentinel_api_url',
    ];

    $form['sentinel']['sentinel_api_key'] = [
      '#type' => 'textfield',
      '#title' => $this->t('API key'),
      '#description' => $this->t('Configure your API key.'),
      '#placeholder' => 'key_...',
      '#states' => [
        'required' => [
          ':input[name=integration_type]' => ['value' => 'sentinel_api'],
        ],
      ],
      '#config_target' => 'altcha.settings:sentinel_api_key',
    ];

    $form['sentinel']['sentinel_api_secret'] = [
      '#type' => 'textfield',
      '#title' => $this->t('API secret'),
      '#description' => $this->t('Configure your API secret.'),
      '#placeholder' => 'sec_...',
      '#states' => [
        'required' => [
          ':input[name=integration_type]' => ['value' => 'sentinel_api'],
        ],
      ],
      '#config_target' => 'altcha.settings:sentinel_api_secret',
    ];

    $form['advanced'] = [
      '#type' => 'details',
      '#title' => $this->t('Advanced settings'),
      '#open' => TRUE,
    ];

    $form['advanced']['auto_verification'] = [
      '#type' => 'select',
      '#title' => $this->t('Auto verification'),
      '#description' => $this->t('Automatically verify without user interaction.'),
      '#options' => [
        'off' => $this->t('Off'),
        'onload' => $this->t('On page load'),
        'onfocus' => $this->t('On form focus'),
        'onsubmit' => $this->t('On form submit'),
      ],
      '#config_target' => 'altcha.settings:auto_verification',
    ];

    $form['advanced']['max_number'] = [
      '#type' => 'number',
      '#title' => $this->t('Complexity'),
      '#description' => $this->t('Tweak the complexity to make the challenge easier or harder to solve. View the <a href="https://altcha.org/docs/complexity" target="_blank">ALTCHA docs</a>  for more information.'),
      '#min' => 1000,
      '#max' => 1000000,
      '#placeholder' => 20000,
      '#config_target' => 'altcha.settings:max_number',
      '#states' => [
        'disabled' => [
          ':input[name=integration_type]' => ['value' => 'sentinel'],
        ],
      ],
    ];

    $form['advanced']['expire'] = [
      '#type' => 'number',
      '#title' => $this->t('Expire'),
      '#description' => $this->t('Challenge expiration duration in milliseconds.'),
      '#placeholder' => 0,
      '#config_target' => 'altcha.settings:expire',
      '#states' => [
        'disabled' => [
          ':input[name=integration_type]' => ['value' => 'sentinel'],
        ],
      ],
    ];

    $form['advanced']['delay'] = [
      '#type' => 'number',
      '#title' => $this->t('Delay'),
      '#description' => $this->t('Artificial delay in milliseconds before verification.'),
      '#placeholder' => 0,
      '#config_target' => 'altcha.settings:delay',
    ];

    // phpcs:disable Drupal.Strings.UnnecessaryStringConcat.Found
    $form['advanced']['library_override'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Override ALTCHA JS library'),
      '#description' => $this->t('Override the ALTCHA JavaScript library with a CDN link or custom JavaScript file.')
      . '<ul>'
      . '<li>' . $this->t('CDN example: https://cdn.jsdelivr.net/gh/altcha-org/altcha@2.1/dist/altcha.min.js') . '</li>'
      . '<li>' . $this->t('All translations example: https://cdn.jsdelivr.net/gh/altcha-org/altcha@2.1/dist/altcha.i18n.min.js') . '</li>'
      . '<li>' . $this->t('Path example: libraries/js/altcha.min.js')
      . '<br><small>' . $this->t('The path may be absolute (e.g., %abs), relative to the drupal web root (e.g., %rel), or defined using a stream wrapper (e.g., %str).', [
        '%abs' => '/var/www/html/web/libraries/js/altcha.min.js',
        '%rel' => 'libraries/js/altcha.min.js',
        '%str' => 'public://libraries/js/altcha.min.js',
      ]) . '</small></li>'
      . '</ul>'
      . $this->t('Leave empty to use the default library included with the module.'),
      '#element_validate' => ['::validateOverrideAltchaLibrary'],
      '#config_target' => 'altcha.settings:library_override',
    ];
    // phpcs:enable

    $form['widget'] = [
      '#type' => 'details',
      '#title' => $this->t('Widget settings'),
      '#description' => $this->moduleHandler->moduleExists('altcha_obfuscate') ? $this->t('These settings also apply to the ALTCHA obfuscate widget. Invisible captcha being the exception, this option is automatically enabled.') : NULL,
      '#open' => TRUE,
    ];

    $form['widget']['floating_enabled'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Enable invisible captcha'),
      '#description' => $this->t('Enables ALTCHA floating UI. This feature is currently not supported on AJAX forms.'),
      '#config_target' => 'altcha.settings:floating_enabled',
    ];

    $form['widget']['floating_mode'] = [
      '#type' => 'select',
      '#title' => $this->t('Floating mode'),
      '#options' => [
        'auto' => $this->t('Auto'),
        'top' => $this->t('Top'),
        'bottom' => $this->t('Bottom'),
      ],
      '#states' => [
        'invisible' => [
          ':input[name=floating_enabled]' => ['checked' => FALSE],
        ],
      ],
      '#config_target' => 'altcha.settings:floating_mode',
    ];

    $form['widget']['floating_anchor'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Floating anchor'),
      '#description' => $this->t('CSS selector of the “anchor” to which the floating UI will be attached.'),
      '#placeholder' => 'input[name="op"]',
      '#states' => [
        'invisible' => [
          ':input[name=floating_enabled]' => ['checked' => FALSE],
        ],
      ],
      '#config_target' => 'altcha.settings:floating_anchor',
    ];

    $form['widget']['floating_offset'] = [
      '#type' => 'number',
      '#title' => $this->t('Floating offset'),
      '#description' => $this->t('Y offset from the anchor element for the floating UI in pixels.'),
      '#placeholder' => 12,
      '#states' => [
        'invisible' => [
          ':input[name=floating_enabled]' => ['checked' => FALSE],
        ],
      ],
      '#config_target' => 'altcha.settings:floating_offset',
    ];

    $form['widget']['hide_logo'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Hide logo'),
      '#description' => $this->t('Hide the ALTCHA logo from display.'),
      '#config_target' => 'altcha.settings:hide_logo',
    ];

    $form['widget']['hide_footer'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Hide footer'),
      '#description' => $this->t('Hide the ALTCHA footer from display.'),
      '#config_target' => 'altcha.settings:hide_footer',
    ];

    foreach (static::getLabelMap() as $field_name => $altcha_info) {
      $form['widget']['labels'][$field_name] = [
        '#type' => 'textfield',
        '#title' => $altcha_info['altcha_title'],
        '#placeholder' => $altcha_info['altcha_example'],
        '#config_target' => "altcha.settings:$field_name",
      ];
    }

    if ($this->moduleHandler->moduleExists('altcha_obfuscate')) {
      $form['obfuscate'] = [
        '#type' => 'details',
        '#title' => $this->t('Obfuscate plugin'),
        '#description' => $this->t('These settings apply specifically to the ALTCHA obfuscate widget.'),
        '#open' => TRUE,
      ];

      $form['obfuscate']['obfuscate_reveal_text'] = [
        '#type' => 'textfield',
        '#title' => $this->t('Reveal text'),
        '#placeholder' => $this->t('Click to reveal'),
        '#config_target' => 'altcha.settings:obfuscate_reveal_text',
      ];

      $form['obfuscate']['obfuscate_max_number'] = [
        '#type' => 'number',
        '#title' => $this->t('Complexity'),
        '#description' => $this->t('Tweak the complexity to make the challenge easier or harder to solve. View the <a href="https://altcha.org/docs/obfuscation/#complexity" target="_blank">ALTCHA docs</a> for more information.'),
        '#min' => 1,
        '#max' => 10000,
        '#placeholder' => 10000,
        '#config_target' => 'altcha.settings:obfuscate_max_number',
      ];

      // phpcs:disable Drupal.Strings.UnnecessaryStringConcat.Found
      $form['obfuscate']['obfuscate_library_override'] = [
        '#type' => 'textfield',
        '#title' => $this->t('Override ALTCHA obfuscate JS library'),
        '#description' => $this->t('Override the ALTCHA obfuscate JavaScript library with a CDN link or custom JavaScript file.')
        . '<ul>'
        . '<li>' . $this->t('CDN example: https://cdn.jsdelivr.net/gh/altcha-org/altcha@1.x/dist_plugins/obfuscation.min.js') . '</li>'
        . '<li>' . $this->t('Path example: libraries/js/obfuscation.min.js')
        . '<br><small>' . $this->t('The path may be absolute (e.g., %abs), relative to the drupal web root (e.g., %rel), or defined using a stream wrapper (e.g., %str).', [
          '%abs' => '/var/www/html/web/libraries/js/obfuscation.min.js',
          '%rel' => 'libraries/js/obfuscation.min.js',
          '%str' => 'public://libraries/js/obfuscation.min.js',
        ]) . '</small></li>'
        . '</ul>'
        . $this->t('Leave empty to use the default library included with the module.'),
        '#element_validate' => ['::validateOverrideObfuscateLibrary'],
        '#config_target' => 'altcha.settings:obfuscate_library_override',
      ];
      // phpcs:enable
    }

    return parent::buildForm($form, $form_state);
  }

  /**
   * {@inheritdoc}
   *
   * Massage empty integer values to NULL. This is a known bug in drupal core.
   *
   * @link https://www.drupal.org/project/drupal/issues/2925445
   * @link https://www.drupal.org/project/drupal/issues/2220381
   */
  public function validateForm(array &$form, FormStateInterface $form_state): void {
    $integer_fields = ['max_number', 'expire', 'delay', 'floating_offset', 'obfuscate_max_number'];
    foreach ($integer_fields as $integer_field) {
      if (empty($form_state->getValue($integer_field))) {
        $form_state->setValue($integer_field, NULL);
      }
    }

    if ($form_state->getValue('integration_type') === 'saas_api') {
      if (empty($form_state->getValue('saas_api_key'))) {
        $form_state->setErrorByName('saas_api_key', 'API Key is required when using ALTCHA SaaS API.');
      }

      if (empty($form_state->getValue('saas_api_region'))) {
        $form_state->setErrorByName('saas_api_region', 'API Region is required when using ALTCHA SaaS API.');
      }
    }

    if ($form_state->getValue('integration_type') === 'sentinel') {
      if (empty($form_state->getValue('sentinel_api_url'))) {
        $form_state->setErrorByName('sentinel_api_url', 'Challenge URL is required when using ALTCHA sentinel.');
      }
    }

    // Obfuscate specific validation.
    if ($this->moduleHandler->moduleExists('altcha_obfuscate')) {
      if (!empty($form_state->getValue('library_override')) && empty($form_state->getValue('obfuscate_library_override'))) {
        $form_state->setErrorByName('obfuscate_library_override', 'When the main ALTCHA library is overridden, the obfuscate library should be overridden too.');
      }

      if (!empty($form_state->getValue('obfuscate_library_override')) && empty($form_state->getValue('library_override'))) {
        $form_state->setErrorByName('library_override', 'When the obfuscate library is overridden, the main ALTCHA library should be overridden too.');
      }
    }

    parent::validateForm($form, $form_state);
  }

  /**
   * Callback to validate the overridden ALTCHA JS library.
   *
   * @param array $form
   *   An associative array containing the structure of the form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   */
  public function validateOverrideAltchaLibrary(array &$form, FormStateInterface $form_state): void {
    $this->validateOverrideLibrary($form, $form_state, 'library_override');
  }

  /**
   * Callback to validate the overridden ALTCHA obfuscate JS library.
   *
   * @param array $form
   *   An associative array containing the structure of the form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   */
  public function validateOverrideObfuscateLibrary(array &$form, FormStateInterface $form_state): void {
    $this->validateOverrideLibrary($form, $form_state, 'obfuscate_library_override');
  }

  /**
   * Validate an overridden ALTCHA JS library.
   *
   * Either a CDN link or a path to a local JS file can be provided. Therefore,
   * check for either a URL or an existing file.
   *
   * @param array $form
   *   An associative array containing the structure of the form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   * @param string $field_name
   *   The name of the field to validate.
   */
  public function validateOverrideLibrary(array &$form, FormStateInterface $form_state, $field_name): void {
    $value = $form_state->getValue($field_name);
    if (empty($value)) {
      return;
    }

    // File uri: transform to relative file path before validation.
    if ($this->streamWrapperManager->isValidUri($value)) {
      $value = trim($this->fileUrlGenerator->generateString($value), '/');
    }

    // Absolute URL: validate the external url.
    if (filter_var($value, FILTER_VALIDATE_URL)) {
      // Ensure it points to a JavaScript file.
      if (!preg_match('/\.js$/', $value)) {
        $form_state->setErrorByName($field_name, $this->t('The provided URL must point to a JavaScript file.'));
      }
      return;
    }

    // File path: does the file actually exist on the path?
    if (!is_file($value)) {
      $form_state->setErrorByName($field_name, $this->t('There is no file at the specified location.'));
    }

    // File path: is the file readable?
    if ((!is_readable($value))) {
      $form_state->setErrorByName($field_name, $this->t('The file at the specified location is not readable.'));
    }
  }

  /**
   * Callback to regenerate a secret key.
   *
   * @param array $form
   *   Nested array of form elements that comprise the form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   *
   * @return array
   *   The updated secret key status form element.
   */
  public function regenerateSecretKey(array &$form, FormStateInterface $form_state): array {
    $this->secretManager->generateSecretKey();
    $form['self_hosted']['secret_key_status']['#markup'] = $this->getSecretKeyStatusMessage(TRUE);
    return $form['self_hosted']['secret_key_status'];
  }

  /**
   * Helper to get the secret key status message.
   *
   * @param bool $update
   *   Whether to fetch the 'update' status message.
   */
  protected function getSecretKeyStatusMessage(bool $update = FALSE): string {
    if ($update) {
      return $this->secretManager->getSecretKey() ? $this->t('✔ Secret key was successfully updated!') : $this->t('✗ Secret key could not be updated!');
    }

    return $this->secretManager->getSecretKey() ? $this->t('✔ Secret key is configured') : $this->t('✗ Secret key is not configured');
  }

  /**
   * Field map to create label form fields dynamically.
   *
   * @see altcha_captcha()
   */
  public static function getLabelMap(): array {
    return [
      'aria' => [
        'altcha_title' => t('Aria link label text'),
        'altcha_key' => 'ariaLinkLabel',
        'altcha_example' => t('Visit altcha.org', [], ['context' => 'altcha_placeholder']),
      ],
      'enter_code' => [
        'altcha_title' => t('Enter code text'),
        'altcha_key' => 'enterCode',
        'altcha_example' => t('Enter code', [], ['context' => 'altcha_placeholder']),
      ],
      'enter_code_aria' => [
        'altcha_title' => t('Aria enter code label text'),
        'altcha_key' => 'enterCodeAria',
        'altcha_example' => t('Enter code you hear. Press Space to play audio.', [], ['context' => 'altcha_placeholder']),
      ],
      'error' => [
        'altcha_title' => t('Error text'),
        'altcha_key' => 'error',
        'altcha_example' => t('Verification failed. Try again later.', [], ['context' => 'altcha_placeholder']),
      ],
      'expired' => [
        'altcha_title' => t('Expired text'),
        'altcha_key' => 'expired',
        'altcha_example' => t('Verification expired. Try again.', [], ['context' => 'altcha_placeholder']),
      ],
      'footer' => [
        'altcha_title' => t('Footer text'),
        'altcha_key' => 'footer',
        'altcha_example' => t('Protected by <a href="https://altcha.org" target="_blank" aria-label="Visit altcha.org">ALTCHA</a>', [], ['context' => 'altcha_placeholder']),
      ],
      'get_audio' => [
        'altcha_title' => t('Get audio challenge text'),
        'altcha_key' => 'getAudioChallenge',
        'altcha_example' => t('Get an audio challenge', [], ['context' => 'altcha_placeholder']),
      ],
      'label' => [
        'altcha_title' => t('Label text'),
        'altcha_key' => 'label',
        'altcha_example' => t("I'm not a robot", [], ['context' => 'altcha_placeholder']),
      ],
      'verify' => [
        'altcha_title' => t('Verify button text'),
        'altcha_key' => 'verify',
        'altcha_example' => t('Verify', [], ['context' => 'altcha_placeholder']),
      ],
      'verification_required' => [
        'altcha_title' => t('Verification required text'),
        'altcha_key' => 'verificationRequired',
        'altcha_example' => t('Verification required!', [], ['context' => 'altcha_placeholder']),
      ],
      'verified' => [
        'altcha_title' => t('Verified text'),
        'altcha_key' => 'verified',
        'altcha_example' => t('Verified', [], ['context' => 'altcha_placeholder']),
      ],
      'verifying' => [
        'altcha_title' => t('Verifying text'),
        'altcha_key' => 'verifying',
        'altcha_example' => t('Verifying...', [], ['context' => 'altcha_placeholder']),
      ],
      'wait' => [
        'altcha_title' => t('Wait alert text'),
        'altcha_key' => 'waitAlert',
        'altcha_example' => t('Verifying... please wait.', [], ['context' => 'altcha_placeholder']),
      ],
    ];
  }

  /**
   * SaaS API Region map.
   *
   * @see altcha_captcha()
   */
  public static function getRegionUrlMap(): array {
    return [
      'eu' => 'https://eu.altcha.org',
      'us' => 'https://us.altcha.org',
    ];
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc