You are here

class Captcha in CAPTCHA 8

Defines the CAPTCHA form element with default properties.

Plugin annotation

@FormElement("captcha");

Hierarchy

Expanded class hierarchy of Captcha

1 file declares its use of Captcha
captcha.module in ./captcha.module
This module enables basic CAPTCHA functionality.
3 #type uses of Captcha
CaptchaExamplesForm::buildChallenge in src/Form/CaptchaExamplesForm.php
Returns a renderable array for a given CAPTCHA challenge.
captcha_form_alter in ./captcha.module
Implements hook_form_alter().
ImageCaptchaSettingsForm::buildForm in image_captcha/src/Form/ImageCaptchaSettingsForm.php
Form constructor.

File

src/Element/Captcha.php, line 17

Namespace

Drupal\captcha\Element
View source
class Captcha extends FormElement implements ContainerFactoryPluginInterface {

  /**
   * The config factory.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  protected $configFactory;

  /**
   * Captcha constructor.
   *
   * @param array $configuration
   *   A configuration array containing information about the plugin instance.
   * @param string $plugin_id
   *   The plugin ID for the plugin instance.
   * @param mixed $plugin_definition
   *   The plugin implementation definition.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
   *   The configuration factory.
   */
  public function __construct(array $configuration, $plugin_id, $plugin_definition, ConfigFactoryInterface $configFactory) {
    $this->configFactory = $configFactory;
    parent::__construct($configuration, $plugin_id, $plugin_definition);
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static($configuration, $plugin_id, $plugin_definition, $container
      ->get('config.factory'));
  }

  /**
   * {@inheritdoc}
   */
  public function getInfo() {
    $captcha_element = [
      '#input' => TRUE,
      '#process' => [
        [
          static::class,
          'processCaptchaElement',
        ],
      ],
      // The type of challenge: e.g. 'default', 'captcha/Math', etc.
      '#captcha_type' => 'default',
      '#default_value' => '',
      // CAPTCHA in admin mode: presolve the CAPTCHA and always show
      // it (despite previous successful responses).
      '#captcha_admin_mode' => FALSE,
      // The default CAPTCHA validation function.
      // TODO: should this be a single string or an array of strings?
      '#captcha_validate' => 'captcha_validate_strict_equality',
    ];

    // Override the default CAPTCHA validation function for case
    // insensitive validation.
    // TODO: shouldn't this be done somewhere else, e.g. in form_alter?
    if (CAPTCHA_DEFAULT_VALIDATION_CASE_INSENSITIVE == $this->configFactory
      ->get('captcha.settings')
      ->get('default_validation')) {
      $captcha_element['#captcha_validate'] = 'captcha_validate_case_insensitive_equality';
    }
    return $captcha_element;
  }

  /**
   * Process callback for CAPTCHA form element.
   */
  public static function processCaptchaElement(&$element, FormStateInterface $form_state, &$complete_form) {

    // Add captcha.inc file.
    module_load_include('inc', 'captcha');

    // Add JavaScript for general CAPTCHA functionality.
    $element['#attached']['library'][] = 'captcha/base';
    if ($form_state
      ->getTriggeringElement() && isset($form_state
      ->getTriggeringElement()['#limit_validation_errors']) && is_array($form_state
      ->getTriggeringElement()['#limit_validation_errors'])) {

      // This is a partial (ajax) submission with limited validation. Do not
      // change anything about the captcha element, assume that it will not
      // update the captcha element, do not generate anything, which keeps the
      // current token intact for the real submission.
      return $element;
    }

    // Get the form ID of the form we are currently processing (which is not
    // necessary the same form that is submitted (if any).
    $this_form_id = isset($complete_form['form_id']['#value']) ? preg_replace("/[^a-z0-9_-]/", "", (string) $complete_form['form_id']['#value']) : NULL;

    // Get the CAPTCHA session ID.
    // If there is a submitted form: try to retrieve and reuse the
    // CAPTCHA session ID from the posted data.
    list($posted_form_id, $posted_captcha_sid) = _captcha_get_posted_captcha_info($element, $form_state, $this_form_id);
    if ($this_form_id == $posted_form_id && isset($posted_captcha_sid)) {
      $captcha_sid = $posted_captcha_sid;
    }
    else {

      // Generate a new CAPTCHA session if we could
      // not reuse one from a posted form.
      $captcha_sid = _captcha_generate_captcha_session($this_form_id, CAPTCHA_STATUS_UNSOLVED);
      $captcha_token = Crypt::randomBytesBase64();
      \Drupal::database()
        ->update('captcha_sessions')
        ->fields([
        'token' => $captcha_token,
      ])
        ->condition('csid', $captcha_sid)
        ->execute();
    }

    // Store CAPTCHA session ID as hidden field.
    // Strictly speaking, it is not necessary to send the CAPTCHA session id
    // with the form, as the one time CAPTCHA token (see lower) is enough.
    // However, we still send it along, because it can help debugging
    // problems on live sites with only access to the markup.
    $element['captcha_sid'] = [
      '#type' => 'hidden',
      '#value' => $captcha_sid,
    ];

    // Store CAPTCHA token as hidden field.
    $captcha_token = \Drupal::database()
      ->select('captcha_sessions', 'cs')
      ->fields('cs', [
      'token',
    ])
      ->condition('csid', $captcha_sid)
      ->execute()
      ->fetchField();
    $element['captcha_token'] = [
      '#type' => 'hidden',
      '#value' => $captcha_token,
    ];

    // Get implementing module and challenge for CAPTCHA.
    list($captcha_type_module, $captcha_type_challenge) = _captcha_parse_captcha_type($element['#captcha_type']);

    // Store CAPTCHA information for further processing in
    // - $form_state->get('captcha_info'), which survives
    //   a form rebuild (e.g. node preview),
    //   useful in _captcha_get_posted_captcha_info().
    // - $element['#captcha_info'], for post processing functions that do not
    //   receive a $form_state argument (e.g. the pre_render callback).
    // Added a new access attribute,
    // by default it will be true if access attribute
    // not defined in a custom form.
    $form_state
      ->set('captcha_info', [
      'this_form_id' => $this_form_id,
      'posted_form_id' => $posted_form_id,
      'captcha_sid' => $captcha_sid,
      'module' => $captcha_type_module,
      'captcha_type' => $captcha_type_challenge,
      'access' => isset($element['#access']) ? $element['#access'] : CAPTCHA_FIELD_DEFAULT_ACCESS,
    ]);
    $element['#captcha_info'] = [
      'form_id' => $this_form_id,
      'captcha_sid' => $captcha_sid,
    ];
    if (_captcha_required_for_user($captcha_sid, $this_form_id) || $element['#captcha_admin_mode']) {

      // Generate a CAPTCHA and its solution
      // (note that the CAPTCHA session ID is given as third argument).
      $captcha = \Drupal::moduleHandler()
        ->invoke($captcha_type_module, 'captcha', [
        'generate',
        $captcha_type_challenge,
        $captcha_sid,
      ]);

      // @todo Isn't this moment a bit late to figure out
      // that we don't need CAPTCHA?
      if (!isset($captcha)) {
        return $element;
      }
      if (!isset($captcha['form']) || !isset($captcha['solution'])) {

        // The selected module did not return what we expected:
        // log about it and quit.
        \Drupal::logger('CAPTCHA')
          ->error('CAPTCHA problem: unexpected result from hook_captcha() of module %module when trying to retrieve challenge type %type for form %form_id.', [
          '%type' => $captcha_type_challenge,
          '%module' => $captcha_type_module,
          '%form_id' => $this_form_id,
        ]);
        return $element;
      }

      // Add form elements from challenge as children to CAPTCHA form element.
      $element['captcha_widgets'] = $captcha['form'];

      // Add a validation callback for the CAPTCHA form element
      // (when not in admin mode).
      if (!$element['#captcha_admin_mode']) {
        $element['#element_validate'] = [
          'captcha_validate',
        ];
      }

      // Set a custom CAPTCHA validate function if requested.
      if (isset($captcha['captcha_validate'])) {
        $element['#captcha_validate'] = $captcha['captcha_validate'];
      }

      // Set the theme function.
      $element['#theme'] = 'captcha';

      // Add pre_render callback for additional CAPTCHA processing.
      if (!isset($element['#pre_render'])) {
        $element['#pre_render'] = [];
      }
      $element['#pre_render'][] = [
        Captcha::class,
        'preRenderProcess',
      ];

      // Store the solution in the #captcha_info array.
      $element['#captcha_info']['solution'] = $captcha['solution'];

      // Store if this captcha type is cacheable:
      // A cacheable captcha must not depend on the sid or solution, but be
      // independent - like for example recaptcha.
      $element['#captcha_info']['cacheable'] = !empty($captcha['cacheable']);
      if (!empty($element['#captcha_info']['cacheable'])) {

        // This is only used to avoid the re-use message.
        $element['captcha_cacheable'] = [
          '#type' => 'hidden',
          '#value' => 1,
        ];
      }

      // Make sure we can use a top level form value
      // $form_state->getValue('captcha_response'),
      // even if the form has #tree=true.
      $element['#tree'] = FALSE;
    }
    return $element;
  }

  /**
   * Pre-render callback for additional processing of a CAPTCHA form element.
   *
   * This encompasses tasks that should happen after the general FAPI processing
   * (building, submission and validation) but before rendering
   * (e.g. storing the solution).
   *
   * @param array $element
   *   The CAPTCHA form element.
   *
   * @return array
   *   The manipulated element.
   */
  public static function preRenderProcess(array $element) {
    module_load_include('inc', 'captcha');

    // Get form and CAPTCHA information.
    $captcha_info = $element['#captcha_info'];
    $form_id = $captcha_info['form_id'];
    $captcha_sid = (int) $captcha_info['captcha_sid'];

    // Check if CAPTCHA is still required.
    // This check is done in a first phase during the element processing
    // (@see captcha_process), but it is also done here for better support
    // of multi-page forms. Take previewing a node submission for example:
    // when the challenge is solved correctely on preview, the form is still
    // not completely submitted, but the CAPTCHA can be skipped.
    if (_captcha_required_for_user($captcha_sid, $form_id) || $element['#captcha_admin_mode']) {

      // Update captcha_sessions table: store the solution
      // of the generated CAPTCHA.
      _captcha_update_captcha_session($captcha_sid, $captcha_info['solution']);

      // Handle the response field if it is available and if it is a textfield.
      if (isset($element['captcha_widgets']['captcha_response']['#type']) && $element['captcha_widgets']['captcha_response']['#type'] == 'textfield') {

        // Before rendering: presolve an admin mode challenge or
        // empty the value of the captcha_response form item.
        $value = $element['#captcha_admin_mode'] ? $captcha_info['solution'] : '';
        $element['captcha_widgets']['captcha_response']['#value'] = $value;
      }
    }
    else {

      // Remove CAPTCHA widgets from form.
      unset($element['captcha_widgets']);
    }
    return $element;
  }

}

Members

Namesort descending Modifiers Type Description Overrides
Captcha::$configFactory protected property The config factory.
Captcha::create public static function Creates an instance of the plugin. Overrides ContainerFactoryPluginInterface::create
Captcha::getInfo public function Returns the element properties for this element. Overrides ElementInterface::getInfo
Captcha::preRenderProcess public static function Pre-render callback for additional processing of a CAPTCHA form element.
Captcha::processCaptchaElement public static function Process callback for CAPTCHA form element.
Captcha::__construct public function Captcha constructor. Overrides PluginBase::__construct
DependencySerializationTrait::$_entityStorages protected property An array of entity type IDs keyed by the property name of their storages.
DependencySerializationTrait::$_serviceIds protected property An array of service IDs keyed by property name used for serialization.
DependencySerializationTrait::__sleep public function 1
DependencySerializationTrait::__wakeup public function 2
FormElement::processAutocomplete public static function Adds autocomplete functionality to elements.
FormElement::processPattern public static function #process callback for #pattern form element property.
FormElement::validatePattern public static function #element_validate callback for #pattern form element property.
FormElement::valueCallback public static function Determines how user input is mapped to an element's #value property. Overrides FormElementInterface::valueCallback 15
MessengerTrait::$messenger protected property The messenger. 29
MessengerTrait::messenger public function Gets the messenger. 29
MessengerTrait::setMessenger public function Sets the messenger.
PluginBase::$configuration protected property Configuration information passed into the plugin. 1
PluginBase::$pluginDefinition protected property The plugin implementation definition. 1
PluginBase::$pluginId protected property The plugin_id.
PluginBase::DERIVATIVE_SEPARATOR constant A string which is used to separate base plugin IDs from the derivative ID.
PluginBase::getBaseId public function Gets the base_plugin_id of the plugin instance. Overrides DerivativeInspectionInterface::getBaseId
PluginBase::getDerivativeId public function Gets the derivative_id of the plugin instance. Overrides DerivativeInspectionInterface::getDerivativeId
PluginBase::getPluginDefinition public function Gets the definition of the plugin implementation. Overrides PluginInspectionInterface::getPluginDefinition 3
PluginBase::getPluginId public function Gets the plugin_id of the plugin instance. Overrides PluginInspectionInterface::getPluginId
PluginBase::isConfigurable public function Determines if the plugin is configurable.
RenderElement::preRenderAjaxForm public static function Adds Ajax information about an element to communicate with JavaScript.
RenderElement::preRenderGroup public static function Adds members of this group as actual elements for rendering.
RenderElement::processAjaxForm public static function Form element processing handler for the #ajax form property. 1
RenderElement::processGroup public static function Arranges elements into groups.
RenderElement::setAttributes public static function Sets a form element's class attribute. Overrides ElementInterface::setAttributes
StringTranslationTrait::$stringTranslation protected property The string translation service. 1
StringTranslationTrait::formatPlural protected function Formats a string containing a count of items.
StringTranslationTrait::getNumberOfPlurals protected function Returns the number of plurals supported by a given language.
StringTranslationTrait::getStringTranslation protected function Gets the string translation service.
StringTranslationTrait::setStringTranslation public function Sets the string translation service to use. 2
StringTranslationTrait::t protected function Translates a string to the current language or to a given language.