captcha.module 24.5 KB
Newer Older
1
2
3
4
<?php

/**
 * @file
5
6
7
 * This module enables basic CAPTCHA functionality.
 *
 * Administrators can add a CAPTCHA to desired forms that users without
8
9
10
11
 * the 'skip CAPTCHA' permission (typically anonymous visitors) have
 * to solve.
 */

12
use Drupal\captcha\Element\Captcha;
13
use Drupal\captcha\Entity\CaptchaPoint;
14
use Drupal\Core\Database\Database;
15
use Drupal\Core\Form\BaseFormIdInterface;
16
use Drupal\Core\Form\FormStateInterface;
17
use Drupal\Core\Link;
18
use Drupal\Core\Render\Element;
19
use Drupal\Core\Render\Markup;
20
21
22
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;

23
24
/**
 * Constants for CAPTCHA persistence.
25
26
27
 *
 * TODO: change these integers to strings because the CAPTCHA settings
 * form saves them as strings in the variables table anyway?
28
 */
29

30
// @TODO: move all constants to some class.
31
32
33
34
35
36
37
38
39
// Always add a CAPTCHA (even on every page of a multipage workflow).
define('CAPTCHA_PERSISTENCE_SHOW_ALWAYS', 0);
// Only one CAPTCHA has to be solved per form instance/multi-step workflow.
define('CAPTCHA_PERSISTENCE_SKIP_ONCE_SUCCESSFUL_PER_FORM_INSTANCE', 1);
// Once the user answered correctly for a CAPTCHA on a certain form type,
// no more CAPTCHAs will be offered anymore for that form.
define('CAPTCHA_PERSISTENCE_SKIP_ONCE_SUCCESSFUL_PER_FORM_TYPE', 2);
// Once the user answered correctly for a CAPTCHA on the site,
// no more CAPTCHAs will be offered anymore.
40
41
define('CAPTCHA_PERSISTENCE_SKIP_ONCE_SUCCESSFUL', 3);

42
43
44
45
define('CAPTCHA_STATUS_UNSOLVED', 0);
define('CAPTCHA_STATUS_SOLVED', 1);
define('CAPTCHA_STATUS_EXAMPLE', 2);

46
47
48
define('CAPTCHA_DEFAULT_VALIDATION_CASE_SENSITIVE', 0);
define('CAPTCHA_DEFAULT_VALIDATION_CASE_INSENSITIVE', 1);

49
50
// Default captcha field access.
define('CAPTCHA_FIELD_DEFAULT_ACCESS', 1);
51

52
/**
53
 * Implements hook_help().
54
 */
55
56
57
58
59
60
function captcha_help($route_name, RouteMatchInterface $route_match) {
  switch ($route_name) {
    case 'help.page.captcha':
      $output = '<h3>' . t('About') . '</h3>';
      $output .= '<p>' . t('"CAPTCHA" is an acronym for "Completely Automated Public Turing test to tell Computers and Humans Apart". It is typically a challenge-response test to determine whether the user is human. The CAPTCHA module is a tool to fight automated submission by malicious users (spamming) of for example comments forms, user registration forms, guestbook forms, etc. You can extend the desired forms with an additional challenge, which should be easy for a human to solve correctly, but hard enough to keep automated scripts and spam bots out.') . '</p>';
      $output .= '<p>' . t('Note that the CAPTCHA module interacts with page caching (see <a href=":performancesettings">performance settings</a>). Because the challenge should be unique for each generated form, the caching of the page it appears on is prevented. Make sure that these forms do not appear on too many pages or you will lose much caching efficiency. For example, if you put a CAPTCHA on the user login block, which typically appears on each page for anonymous visitors, caching will practically be disabled. The comment submission forms are another example. In this case you should set the <em>Location of comment submission form</em> to <em>Display on separate page</em> in the comment settings of the relevant <a href=":contenttypes">content types</a> for better caching efficiency.', [
61
62
63
        ':performancesettings' => Url::fromRoute('system.performance_settings')->toString(),
        ':contenttypes' => Url::fromRoute('entity.node_type.collection')->toString(),
      ]) . '</p>';
64
      $output .= '<p>' . t('CAPTCHA is a trademark of Carnegie Mellon University.') . '</p>';
65
      return ['#markup' => $output];
66

67
    case 'captcha_settings':
68
      $output = '<p>' . t('A CAPTCHA can be added to virtually each Drupal form. Some default forms are already provided in the form list, but arbitrary forms can be easily added and managed when the option <em>Add CAPTCHA administration links to forms</em> is enabled.') . '</p>';
69
      $output .= '<p>' . t('Users with the <em>Skip CAPTCHA</em> <a href=":perm">permission</a> won\'t be offered a challenge. Be sure to grant this permission to the trusted users (e.g. site administrators). If you want to test a protected form, be sure to do it as a user without the <em>Skip CAPTCHA</em> permission (e.g. as anonymous user).', [
70
71
        ':perm' => Url::fromRoute('user.admin_permissions')->toString(),
      ]) . '</p>';
72
      $output .= '<p><b>' . t('Note that the CAPTCHA module disables <a href=":performancesettings">page caching</a> of pages that include a CAPTCHA challenge.', [
73
74
        ':performancesettings' => Url::fromRoute('system.performance_settings')->toString(),
      ]) . '</b></p>';
75
      return ['#markup' => $output];
76
77
78
79
  }
}

/**
80
81
82
83
84
85
 * Loader for Captcha Point entity.
 *
 * @param string $id
 *   Form id string.
 *
 * @return \Drupal\Core\Entity\EntityInterface
86
 *   An instance of an captcha_point entity.
87
88
89
90
91
92
93
 */
function captcha_point_load($id) {
  return CaptchaPoint::load($id);
}

/**
 * Implements hook_theme().
94
95
 */
function captcha_theme() {
96
97
  return [
    'captcha' => [
98
      'render element' => 'element',
99
100
      'template' => 'captcha',
      'path' => drupal_get_path('module', 'captcha') . '/templates',
101
102
    ],
  ];
103
104
}

105
/**
106
 * Implements hook_cron().
107
 *
108
 * Remove old entries from captcha_sessions table.
109
110
 */
function captcha_cron() {
111
112
113
  // Get request time.
  $request_time = \Drupal::time()->getRequestTime();

114
  // Remove challenges older than 1 day.
115
116
  $connection = Database::getConnection();
  $connection->delete('captcha_sessions')
117
    ->condition('timestamp', $request_time - 60 * 60 * 24, '<')
118
    ->execute();
119
120
}

121
122
123
/**
 * Theme function for a CAPTCHA element.
 *
124
 * Render it in a section element if a description of the CAPTCHA
125
126
 * is available. Render it as is otherwise.
 */
127
function template_preprocess_captcha(&$variables) {
Stefaan Lippens's avatar
Stefaan Lippens committed
128
  $element = $variables['element'];
129

130
  if (!empty($element['#description']) && isset($element['captcha_widgets'])) {
131
132
133
134
135
136
137
138
    $children_keys = Element::children($element);
    $captcha_children_output = '';
    foreach ($children_keys as $key) {
      if (!empty($element[$key])) {
        $captcha_children_output .= \Drupal::service('renderer')->render($element[$key]);
      }
    }

139
    $variables['details'] = [
140
      '#type' => 'details',
141
142
      '#title' => t('CAPTCHA'),
      '#description' => $element['#description'],
143
      '#children' => Markup::create($captcha_children_output),
144
      '#attributes' => [
145
        'id' => 'captcha',
146
147
148
        'class' => ['captcha'],
        'open' => [''],
      ],
149
      '#open' => TRUE,
150
    ];
151
152
153
  }
}

154
/**
155
 * Implements hook_form_alter().
156
 *
157
158
159
 * This function adds a CAPTCHA to forms for untrusted users
 * if needed and adds. CAPTCHA administration links for site
 * administrators if this option is enabled.
160
 */
161
162
function captcha_form_alter(array &$form, FormStateInterface $form_state, $form_id) {
  $account = \Drupal::currentUser();
163
  $config = \Drupal::config('captcha.settings');
164
165
  $captchaService = \Drupal::service('captcha.helper');

166
167
  // Visitor does not have permission to skip CAPTCHAs.
  module_load_include('inc', 'captcha');
168
  if (!$account->hasPermission('skip CAPTCHA')) {
169
170
171
172
173
174
175
176
177
178
    $query = \Drupal::entityQuery('captcha_point');
    $query->condition('label', $form_id);
    $entity_ids = $query->execute();

    // If empty, see if it is a form provided by default config.
    if (empty($entity_ids)) {
      $query = \Drupal::entityQuery('captcha_point');
      $query->condition('formId', $form_id);
      $entity_ids = $query->execute();
    }
179

180
181
182
183
184
185
186
    if (!empty($entity_ids) && is_array($entity_ids)) {
      $captcha_point_id = array_pop($entity_ids);
      /* @var CaptchaPoint $captcha_point */
      $captcha_point = \Drupal::entityTypeManager()
        ->getStorage('captcha_point')
        ->load($captcha_point_id);
    }
187

188
    // If there is no CaptchaPoint for the form_id, try to use the base_form_id.
189
    if (empty($captcha_point) || !$captcha_point->status()) {
190
191
192
193
194
195
196
197
198
199
200
      $form_object = $form_state->getFormObject();
      if ($form_object instanceof BaseFormIdInterface) {
        $base_form_id = $form_object->getBaseFormId();
        if (!empty($base_form_id) && $base_form_id != $form_id) {
          $captcha_point = \Drupal::entityTypeManager()
            ->getStorage('captcha_point')
            ->load($base_form_id);
        }
      }
    }

201
    if (empty($captcha_point) && $config->get('enabled_default')) {
202
203
204
205
206
207
208
209
      // Create fake captcha point without saving.
      $captcha_point = new CaptchaPoint([
        'formId' => $form_id,
        'captchaType' => $config->get('default_challenge'),
      ], 'captcha_point');
      $captcha_point->enable();
    }

210
    if (!empty($captcha_point) && $captcha_point->status()) {
211
      // Build CAPTCHA form element.
212
      $captcha_element = [
213
        '#type' => 'captcha',
214
        '#captcha_type' => $captcha_point->getCaptchaType(),
215
      ];
216

217
      // Add a CAPTCHA description if required.
218
      if ($config->get('add_captcha_description')) {
219
        $captcha_element['#description'] = _captcha_get_description();
220
      }
221
222
223

      // Get placement in form and insert in form.
      $captcha_placement = _captcha_get_captcha_placement($form_id, $form);
224
      $captchaService->insertCaptchaElement($form, $captcha_placement, $captcha_element);
225

226
227
    }
  }
228
  elseif ($config->get('administration_mode') && $account->hasPermission('administer CAPTCHA settings')
229
    && (!\Drupal::service('router.admin_context')
230
      ->isAdminRoute() || $config->get('allow_on_admin_pages'))
231
  ) {
232
    // Add CAPTCHA administration tools.
233
    /* @var \Drupal\captcha\Entity\CaptchaPoint $captcha_point */
234
    $captcha_point = CaptchaPoint::load($form_id);
235

236
    // For administrators: show CAPTCHA info and offer link to configure it.
237
    $captcha_element = [
238
      '#type' => 'details',
239
      '#title' => t('CAPTCHA'),
240
241
242
      '#attributes' => [
        'class' => ['captcha-admin-links'],
      ],
243
      '#open' => TRUE,
244
    ];
245
246

    if ($captcha_point !== NULL && $captcha_point->getCaptchaType()) {
247
      $captcha_element['#title'] = $captcha_point->status() ? t('CAPTCHA: challenge "@type" enabled', ['@type' => $captcha_point->getCaptchaType()]) : t('CAPTCHA: challenge "@type" disabled', ['@type' => $captcha_point->getCaptchaType()]);
248
      $captcha_point->status() ? $captcha_element['#description'] = t('Untrusted users will see a CAPTCHA here (<a href="@settings">general CAPTCHA settings</a>).',
249
250
251
252
        [
          '@settings' => Url::fromRoute('captcha_settings')
            ->toString(),
        ]) : $captcha_element['#description'] = t('CAPTCHA disabled, Untrusted users won\'t see the captcha (<a href="@settings">general CAPTCHA settings</a>).',
253
        ['@settings' => Url::fromRoute('captcha_settings')->toString()]
254
      );
255
      $captcha_element['challenge'] = [
256
257
        '#type' => 'item',
        '#title' => t('Enabled challenge'),
258
259
260
261
        '#markup' => $captcha_point->toLink(t('change'), 'edit-form', [
          'query' => \Drupal::destination()
            ->getAsArray(),
        ])->toString(),
262
      ];
263
    }
264
265
    else {
      $captcha_element['#title'] = t('CAPTCHA: no challenge enabled');
266
      $captcha_element['add_captcha'] = [
267
        '#markup' => Link::fromTextAndUrl(
268
          t('Place a CAPTCHA here for untrusted users.'),
269
          Url::fromRoute('captcha_point.add', [], [
270
            'query' => \Drupal::destination()
271
              ->getAsArray() + ['form_id' => $form_id],
272
          ])
273
        )->toString(),
274
      ];
275
    }
276

277
    // Get placement in form and insert in form.
278
    if ($captcha_placement = _captcha_get_captcha_placement($form_id, $form)) {
279
      $captchaService->insertCaptchaElement($form, $captcha_placement, $captcha_element);
280
    };
281
  }
282

283
  // Add a warning about caching on the Performance settings page.
284
  if ($form_id == 'system_performance_settings') {
285
    $form['caching']['captcha'] = [
286
287
      '#type' => 'item',
      '#title' => t('CAPTCHA'),
288
      '#markup' => '<div class="messages messages--warning">' . t('Most CAPTCHA methods will disable the caching of pages that contain a CAPTCHA element. Check the different implementations to know more about how it affects caching.') . '</div>',
289
    ];
290
  }
291
292
}

293
294
/**
 * CAPTCHA validation function to tests strict equality.
295
296
297
298
299
300
301
302
 *
 * @param string $solution
 *   The solution of the test.
 * @param string $response
 *   The response to the test.
 *
 * @return bool
 *   TRUE when case insensitive equal, FALSE otherwise.
303
304
305
306
307
308
309
 */
function captcha_validate_strict_equality($solution, $response) {
  return $solution === $response;
}

/**
 * CAPTCHA validation function to tests case insensitive equality.
310
311
312
313
314
315
316
317
 *
 * @param string $solution
 *   The solution of the test.
 * @param string $response
 *   The response to the test.
 *
 * @return bool
 *   TRUE when case insensitive equal, FALSE otherwise.
318
319
 */
function captcha_validate_case_insensitive_equality($solution, $response) {
320
  return mb_strtolower($solution) === mb_strtolower($response);
321
322
}

323
324
/**
 * CAPTCHA validation function to tests equality while ignoring spaces.
325
326
327
328
329
330
331
332
 *
 * @param string $solution
 *   The solution of the test.
 * @param string $response
 *   The response to the test.
 *
 * @return bool
 *   TRUE when equal (ignoring spaces), FALSE otherwise.
333
334
 */
function captcha_validate_ignore_spaces($solution, $response) {
335
  return preg_replace('/\s/', '', $solution) === preg_replace('/\s/', '', $response);
336
337
338
}

/**
339
340
341
342
343
344
345
346
347
 * Validation function to tests case insensitive equality while ignoring spaces.
 *
 * @param string $solution
 *   The solution of the test.
 * @param string $response
 *   The response to the test.
 *
 * @return bool
 *   TRUE when equal (ignoring spaces), FALSE otherwise.
348
349
 */
function captcha_validate_case_insensitive_ignore_spaces($solution, $response) {
350
  return preg_replace('/\s/', '', mb_strtolower($solution)) === preg_replace('/\s/', '', mb_strtolower($response));
351
352
}

353
/**
354
 * Helper function for getting the posted CAPTCHA info.
355
356
357
358
359
360
361
362
363
364
365
366
 *
 * This function hides the form processing mess for several use cases an
 * browser bug workarounds.
 * For example: $element['#post'] can typically be used to get the posted
 * form_id and captcha_sid, but in the case of node preview situations
 * (with correct CAPTCHA response) that does not work and we can get them from
 * $form_state['clicked_button']['#post'].
 * However with Internet Explorer 7, the latter does not work either when
 * submitting the forms (with only one text field) with the enter key
 * (see http://drupal.org/node/534168), in which case we also have to check
 * $form_state['buttons']['button']['0']['#post'].
 *
367
368
 * @param array $element
 *   The CAPTCHA element.
369
 * @param Drupal\Core\Form\FormStateInterface $form_state
370
371
372
373
 *   The form state structure to extract the info from.
 * @param string $this_form_id
 *   The form ID of the form we are currently processing
 *   (which is not necessarily the form that was posted).
374
 *
375
376
377
 * @return array
 *   Array with $posted_form_id and $post_captcha_sid (with NULL values
 *   if the values could not be found, e.g. for a fresh form).
378
 */
379
function _captcha_get_posted_captcha_info(array $element, FormStateInterface $form_state, $this_form_id) {
380
  if ($form_state->isSubmitted() && $form_state->has('captcha_info')) {
381
382
383
    // We are handling (or rebuilding) an already submitted form,
    // so we already determined the posted form ID and CAPTCHA session ID
    // for this form (from before submitting). Reuse this info.
384
385
    $posted_form_id = $form_state->get('captcha_info')['posted_form_id'];
    $posted_captcha_sid = $form_state->get('captcha_info')['captcha_sid'];
386
387
  }
  else {
388
389
390
391
    // We have to determine the posted form ID and CAPTCHA session ID
    // from the post data.
    // Because we possibly use raw post data here,
    // we should be extra cautious and filter this data.
392
393
    $input = &$form_state->getUserInput();
    $posted_form_id = isset($input['form_id']) ?
394
      preg_replace("/[^a-z0-9_-]/", "", (string) $input['form_id'])
395
      : NULL;
396
397
    $posted_captcha_sid = isset($input['captcha_sid']) ?
      (int) $input['captcha_sid']
398
      : NULL;
399
    $posted_captcha_token = isset($input['captcha_token']) ?
400
      preg_replace("/[^a-zA-Z0-9-_]/", "", (string) $input['captcha_token'])
401
402
403
404
405
406
407
408
409
410
411
      : NULL;

    if ($posted_form_id == $this_form_id) {
      // Check if the posted CAPTCHA token is valid for the posted CAPTCHA
      // session ID. Note that we could just check the validity of the CAPTCHA
      // token and extract the CAPTCHA session ID from that (without looking at
      // the actual posted CAPTCHA session ID). However, here we check both
      // the posted CAPTCHA token and session ID: it is a bit more stringent
      // and the database query should also be more efficient (because there is
      // an index on the CAPTCHA session ID).
      if ($posted_captcha_sid != NULL) {
412
413
414
415
416
417
418
        $expected_captcha_token = \Drupal::database()
          ->select('captcha_sessions', 'cs')
          ->fields('cs', ['token'])
          ->condition('csid', $posted_captcha_sid)
          ->execute()
          ->fetchField();

419
420
421
422
        // If we do have a CAPTCHA token mismatch then log it.
        try {
          if (($expected_captcha_token !== $posted_captcha_token) && empty($input['captcha_cacheable'])) {
            throw new \UnexpectedValueException('CAPTCHA session reuse attack detected.');
423
          }
424
425
426
427
428
429
430
431
432
433
        }
        catch (\Exception $e) {
          \Drupal::logger('captcha')->debug(
            'CAPTCHA session reuse attack detected on @form_id <br/>Posted CAPTCHA token: @posted_captcha_token <br/>Expected captcha token: @expected_captcha_token',
            [
              '@form_id' => $this_form_id,
              '@expected_captcha_token' => var_export($expected_captcha_token, TRUE),
              '@posted_captcha_token' => var_export($posted_captcha_token, TRUE),
            ]
          );
434
          // Invalidate the CAPTCHA session.
435
436
437
438
439
440
441
442
443
          $posted_captcha_sid = NULL;
        }
      }
    }
    else {
      // The CAPTCHA session ID is specific to the posted form.
      // Return NULL, so a new session will be generated for this other form.
      $posted_captcha_sid = NULL;
    }
444
  }
445
  return [$posted_form_id, $posted_captcha_sid];
446
447
}

448
449
450
451
452
453
454
455
/**
 * CAPTCHA validation handler.
 *
 * This function is placed in the main captcha.module file to make sure that
 * it is available (even for cached forms, which don't fire
 * captcha_form_alter(), and subsequently don't include additional include
 * files).
 */
456
function captcha_validate($element, FormStateInterface &$form_state) {
457

458
  $captcha_info = $form_state->get('captcha_info');
459
  $form_id = $captcha_info['this_form_id'];
460

461
  // Get CAPTCHA response.
462
  $captcha_response = $form_state->getValue('captcha_response');
463

464
  // Get CAPTCHA session from CAPTCHA info
465
  // TODO: is this correct in all cases: see comments in previous revisions?
466
  $csid = $captcha_info['captcha_sid'];
467

468
469
470
471
472
  $solution = \Drupal::database()
    ->select('captcha_sessions', 'cs')
    ->fields('cs', ['solution'])
    ->condition('csid', $csid)
    ->execute()
473
    ->fetchField();
474

475
476
  // Bypass captcha validation if access attribute value is false.
  if (empty($captcha_info['access'])) {
477
    return FALSE;
478
479
  }

480
481
  // @todo: what is the result when there is no entry for
  // the captcha_session? in D6 it was FALSE, what in D7?
482
483
  if ($solution === FALSE) {
    // Unknown challenge_id.
484
485
    // TODO: this probably never happens anymore now that there is detection
    // for CAPTCHA session reuse attacks in _captcha_get_posted_captcha_info().
tonnosf's avatar
tonnosf committed
486
    $form_state->setErrorByName('captcha', t('CAPTCHA validation error: unknown CAPTCHA session ID. Contact the site administrator if this problem persists.'));
487
    \Drupal::logger('CAPTCHA')->error(
488
      'CAPTCHA validation error: unknown CAPTCHA session ID (%csid).',
489
      ['%csid' => var_export($csid, TRUE)]);
490
491
  }
  else {
492
493
494
    // Get CAPTCHA validate function or fall back on strict equality.
    $captcha_validate = $element['#captcha_validate'];
    if (!function_exists($captcha_validate)) {
495
      $captcha_validate = 'captcha_validate_strict_equality';
496
    }
497
    // Check the response with the CAPTCHA validation function.
Stefaan Lippens's avatar
Stefaan Lippens committed
498
    // Apart from the traditional expected $solution and received $response,
499
500
    // we also provide the CAPTCHA $element and $form_state
    // arrays for more advanced use cases.
501
    if ($captcha_validate($solution, $captcha_response, $element, $form_state)) {
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517

      // Get the CAPTCHA persistence setting.
      $captcha_persistence = \Drupal::config('captcha.settings')
        ->get('persistence');

      if (in_array($captcha_persistence,
        [
          CAPTCHA_PERSISTENCE_SKIP_ONCE_SUCCESSFUL,
          CAPTCHA_PERSISTENCE_SKIP_ONCE_SUCCESSFUL_PER_FORM_TYPE,
        ])) {
        // Only save the success in $_SESSION if it is actually needed for
        // further validation in _captcha_required_for_user(). Setting
        // this kills the page cache so let's not be cavalier about it.
        $_SESSION['captcha_success_form_ids'][$form_id] = $form_id;
      }

518
      // Record success.
519
      \Drupal::database()->update('captcha_sessions')
520
        ->condition('csid', $csid)
521
        ->fields(['status' => CAPTCHA_STATUS_SOLVED])
522
523
        ->expression('attempts', 'attempts + 1')
        ->execute();
524
525
526
    }
    else {
      // Wrong answer.
527
      \Drupal::database()->update('captcha_sessions')
528
529
530
        ->condition('csid', $csid)
        ->expression('attempts', 'attempts + 1')
        ->execute();
531

tonnosf's avatar
tonnosf committed
532
      $form_state->setErrorByName('captcha_response', t('The answer you entered for the CAPTCHA was not correct.'));
533
      // Update wrong response counter.
tonnosf's avatar
tonnosf committed
534
      if (\Drupal::config('captcha.settings')->get('enable_stats', FALSE)) {
535
        Drupal::state()->set('captcha.wrong_response_counter', Drupal::state()
536
          ->get('captcha.wrong_response_counter', 0) + 1);
537
      }
538

539
540
541
      if (\Drupal::config('captcha.settings')
        ->get('log_wrong_responses', FALSE)
      ) {
542
        \Drupal::logger('CAPTCHA')->notice(
543
          '%form_id post blocked by CAPTCHA module: challenge %challenge (by module %module), user answered "@response", but the solution was "@solution".',
544
          [
545
546
547
548
549
            '%form_id' => $form_id,
            '@response' => $captcha_response,
            '@solution' => $solution,
            '%challenge' => $captcha_info['captcha_type'],
            '%module' => $captcha_info['module'],
550
          ]);
551
552
553
554
555
556
      }
    }
  }
}

/**
557
 * Pre-render callback for additional processing of a CAPTCHA form element.
558
 *
559
 * This encompasses tasks that should happen after the general FAPI processing
560
561
 * (building, submission and validation) but before rendering
 * (e.g. storing the solution).
562
 *
563
564
565
566
567
 * @param array $element
 *   The CAPTCHA form element.
 *
 * @return array
 *   The manipulated element.
568
569
 *
 * @deprecated Use \Drupal\captcha\Element\Captcha::preRenderProcess() instead.
570
 */
571
function captcha_pre_render_process(array $element) {
572
  return Captcha::preRenderProcess($element);
573
574
}

575
/**
576
 * Default implementation of hook_captcha().
577
578
579
580
 */
function captcha_captcha($op, $captcha_type = '') {
  switch ($op) {
    case 'list':
581
      return ['Math'];
Stefaan Lippens's avatar
Stefaan Lippens committed
582

583
584
    case 'generate':
      if ($captcha_type == 'Math') {
585
        $result = [];
586
587
588
589
        $answer = mt_rand(1, 20);
        $x = mt_rand(1, $answer);
        $y = $answer - $x;
        $result['solution'] = "$answer";
590
591
592
593
        // Build challenge widget.
        // Note that we also use t() for the math challenge itself. This makes
        // it possible to 'rephrase' the challenge a bit through localization
        // or string overrides.
594
        $result['form']['captcha_response'] = [
595
          '#type' => 'textfield',
Stefaan Lippens's avatar
typo    
Stefaan Lippens committed
596
          '#title' => t('Math question'),
597
          '#description' => t('Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.'),
598
          '#field_prefix' => t('@x + @y =', ['@x' => $x, '@y' => $y]),
599
600
601
          '#size' => 4,
          '#maxlength' => 2,
          '#required' => TRUE,
602
          '#attributes' => [
603
            'autocomplete' => 'off',
604
          ],
605
          '#cache' => ['max-age' => 0],
606
        ];
607
        \Drupal::service('page_cache_kill_switch')->trigger();
608

609
610
        return $result;
      }
611
612
613
614
615
616
      elseif ($captcha_type == 'Test') {
        // This challenge is not visible through the administrative interface
        // as it is not listed in captcha_captcha('list'),
        // but it is meant for debugging and testing purposes.
        // TODO for Drupal 7 version: This should be done with a mock module,
        // but Drupal 6 does not support this (mock modules can not be hidden).
617
        $result = [
618
          'solution' => 'Test 123',
619
620
621
          'form' => [],
        ];
        $result['form']['captcha_response'] = [
622
          '#type' => 'textfield',
623
          '#title' => t('Test one two three'),
624
          '#required' => TRUE,
625
          '#cache' => ['max-age' => 0],
626
        ];
627
628
        \Drupal::service('page_cache_kill_switch')->trigger();

629
630
        return $result;
      }
Stefaan Lippens's avatar
Stefaan Lippens committed
631
      break;
632
633
  }
}