captcha.module 24.9 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\Entity\CaptchaPoint;
13
use Drupal\Component\Utility\Unicode;
14
use Drupal\Core\Database\Database;
15
use Drupal\Core\Form\FormStateInterface;
16
use Drupal\Core\Link;
17
use Drupal\Core\Render\Markup;
18
19
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
20
use Drupal\Core\Render\Element;
21
use Drupal\Core\Form\BaseFormIdInterface;
22

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
164
165
  $config = \Drupal::config('captcha.settings');
  // Visitor does not have permission to skip CAPTCHAs.
  module_load_include('inc', 'captcha');
166
  if (!$account->hasPermission('skip CAPTCHA')) {
167

168
    /* @var CaptchaPoint $captcha_point */
169
170
171
    $captcha_point = \Drupal::entityTypeManager()
      ->getStorage('captcha_point')
      ->load($form_id);
172

173
174
175
176
177
178
179
180
181
182
183
184
185
    // If there is no CaptchaPoint for the form_id, try to use the base_form_id.
    if (!$captcha_point || !$captcha_point->status()) {
      $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);
        }
      }
    }

186
187
188
189
190
191
192
193
194
    if (!$captcha_point && $config->get('enabled_default')) {
      // Create fake captcha point without saving.
      $captcha_point = new CaptchaPoint([
        'formId' => $form_id,
        'captchaType' => $config->get('default_challenge'),
      ], 'captcha_point');
      $captcha_point->enable();
    }

195
    if ($captcha_point && $captcha_point->status()) {
196
      // Build CAPTCHA form element.
197
      $captcha_element = [
198
        '#type' => 'captcha',
199
        '#captcha_type' => $captcha_point->getCaptchaType(),
200
      ];
201

202
      // Add a CAPTCHA description if required.
203
      if ($config->get('add_captcha_description')) {
204
        $captcha_element['#description'] = _captcha_get_description();
205
      }
206
207
208
209

      // Get placement in form and insert in form.
      $captcha_placement = _captcha_get_captcha_placement($form_id, $form);
      _captcha_insert_captcha_element($form, $captcha_placement, $captcha_element);
210

211
212
    }
  }
213
  elseif ($config->get('administration_mode') && $account->hasPermission('administer CAPTCHA settings')
214
    && (!\Drupal::service('router.admin_context')
215
      ->isAdminRoute() || $config->get('allow_on_admin_pages'))
216
  ) {
217
    // Add CAPTCHA administration tools.
218
    /* @var \Drupal\captcha\Entity\CaptchaPoint $captcha_point */
219
    $captcha_point = CaptchaPoint::load($form_id);
220

221
    // For administrators: show CAPTCHA info and offer link to configure it.
222
    $captcha_element = [
223
      '#type' => 'details',
224
      '#title' => t('CAPTCHA'),
225
226
227
      '#attributes' => [
        'class' => ['captcha-admin-links'],
      ],
228
      '#open' => TRUE,
229
    ];
230
231

    if ($captcha_point !== NULL && $captcha_point->getCaptchaType()) {
232
      $captcha_element['#title'] = $captcha_point->status() ? t('CAPTCHA: challenge "@type" enabled', ['@type' => $captcha_point->getCaptchaType()]) : t('CAPTCHA: challenge "@type" disabled', ['@type' => $captcha_point->getCaptchaType()]);
233
      $captcha_point->status() ? $captcha_element['#description'] = t('Untrusted users will see a CAPTCHA here (<a href="@settings">general CAPTCHA settings</a>).',
234
235
236
237
        [
          '@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>).',
238
        ['@settings' => Url::fromRoute('captcha_settings')->toString()]
239
      );
240
      $captcha_element['challenge'] = [
241
242
        '#type' => 'item',
        '#title' => t('Enabled challenge'),
243
        '#markup' => t('<a href="@change">change</a>', [
244
245
246
247
          '@change' => $captcha_point->url('edit-form', [
            'query' => Drupal::destination()
              ->getAsArray(),
          ]),
248
249
        ]),
      ];
250
    }
251
252
    else {
      $captcha_element['#title'] = t('CAPTCHA: no challenge enabled');
253
      $captcha_element['add_captcha'] = [
254
        '#markup' => Link::fromTextAndUrl(
255
          t('Place a CAPTCHA here for untrusted users.'),
256
          Url::fromRoute('captcha_point.add', [], [
257
            'query' => Drupal::destination()
258
              ->getAsArray() + ['form_id' => $form_id],
259
          ])
260
        )->toString(),
261
      ];
262
    }
263

264
    // Get placement in form and insert in form.
265
266
267
    if ($captcha_placement = _captcha_get_captcha_placement($form_id, $form)) {
      _captcha_insert_captcha_element($form, $captcha_placement, $captcha_element);
    };
268
  }
269

270
  // Add a warning about caching on the Performance settings page.
271
  if ($form_id == 'system_performance_settings') {
272
    $form['caching']['captcha'] = [
273
274
      '#type' => 'item',
      '#title' => t('CAPTCHA'),
275
      '#markup' => '<div class="messages messages--warning">' . t('The CAPTCHA module will disable the caching of pages that contain a CAPTCHA element.') . '</div>',
276
    ];
277
  }
278
279
}

280
281
/**
 * CAPTCHA validation function to tests strict equality.
282
283
284
285
286
287
288
289
 *
 * @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.
290
291
292
293
294
295
296
 */
function captcha_validate_strict_equality($solution, $response) {
  return $solution === $response;
}

/**
 * CAPTCHA validation function to tests case insensitive equality.
297
298
299
300
301
302
303
304
 *
 * @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.
305
306
 */
function captcha_validate_case_insensitive_equality($solution, $response) {
307
  return Unicode::strtolower($solution) === Unicode::strtolower($response);
308
309
}

310
311
/**
 * CAPTCHA validation function to tests equality while ignoring spaces.
312
313
314
315
316
317
318
319
 *
 * @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.
320
321
 */
function captcha_validate_ignore_spaces($solution, $response) {
322
  return preg_replace('/\s/', '', $solution) === preg_replace('/\s/', '', $response);
323
324
325
}

/**
326
327
328
329
330
331
332
333
334
 * 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.
335
336
 */
function captcha_validate_case_insensitive_ignore_spaces($solution, $response) {
337
  return preg_replace('/\s/', '', Unicode::strtolower($solution)) === preg_replace('/\s/', '', Unicode::strtolower($response));
338
339
}

340
/**
341
 * Helper function for getting the posted CAPTCHA info.
342
343
344
345
346
347
348
349
350
351
352
353
 *
 * 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'].
 *
354
355
 * @param array $element
 *   The CAPTCHA element.
356
 * @param Drupal\Core\Form\FormStateInterface $form_state
357
358
359
360
 *   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).
361
 *
362
363
364
 * @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).
365
 */
366
function _captcha_get_posted_captcha_info(array $element, FormStateInterface $form_state, $this_form_id) {
367
  if ($form_state->isSubmitted() && $form_state->has('captcha_info')) {
368
369
370
    // 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.
371
372
    $posted_form_id = $form_state->get('captcha_info')['posted_form_id'];
    $posted_captcha_sid = $form_state->get('captcha_info')['captcha_sid'];
373
374
  }
  else {
375
376
377
378
    // 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.
379
380
381
    $input = &$form_state->getUserInput();
    $posted_form_id = isset($input['form_id']) ?
      preg_replace("/[^a-z0-9_]/", "", (string) $input['form_id'])
382
      : NULL;
383
384
    $posted_captcha_sid = isset($input['captcha_sid']) ?
      (int) $input['captcha_sid']
385
      : NULL;
386
387
    $posted_captcha_token = isset($input['captcha_token']) ?
      preg_replace("/[^a-zA-Z0-9]/", "", (string) $input['captcha_token'])
388
389
390
391
392
393
394
395
396
397
398
      : 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) {
399
400
401
402
403
404
405
        $expected_captcha_token = \Drupal::database()
          ->select('captcha_sessions', 'cs')
          ->fields('cs', ['token'])
          ->condition('csid', $posted_captcha_sid)
          ->execute()
          ->fetchField();

406
407
408
409
        // 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.');
410
          }
411
412
413
414
415
416
417
418
419
420
421
        }
        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),
            ]
          );

422
423
          $posted_captcha_sid = NULL;
        }
424
425
        // Invalidate CAPTCHA token to avoid reuse.
        if (!empty($posted_captcha_sid)) {
426
427
428
429
          \Drupal::database()->update('captcha_sessions')
            ->fields(['token' => NULL])
            ->condition('csid', $posted_captcha_sid)
            ->execute();
430
        }
431
432
433
434
435
436
437
      }
    }
    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;
    }
438
  }
439
  return [$posted_form_id, $posted_captcha_sid];
440
441
}

442
443
444
445
446
447
448
449
/**
 * 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).
 */
450
function captcha_validate($element, FormStateInterface &$form_state) {
451

452
  $captcha_info = $form_state->get('captcha_info');
453
  $form_id = $captcha_info['this_form_id'];
454

455
  // Get CAPTCHA response.
456
  $captcha_response = $form_state->getValue('captcha_response');
457

458
  // Get CAPTCHA session from CAPTCHA info
459
  // TODO: is this correct in all cases: see comments in previous revisions?
460
  $csid = $captcha_info['captcha_sid'];
461

462
463
464
465
466
  $solution = \Drupal::database()
    ->select('captcha_sessions', 'cs')
    ->fields('cs', ['solution'])
    ->condition('csid', $csid)
    ->execute()
467
    ->fetchField();
468

469
470
  // Bypass captcha validation if access attribute value is false.
  if (empty($captcha_info['access'])) {
471
    return FALSE;
472
473
  }

474
475
  // @todo: what is the result when there is no entry for
  // the captcha_session? in D6 it was FALSE, what in D7?
476
477
  if ($solution === FALSE) {
    // Unknown challenge_id.
478
479
    // 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
480
    $form_state->setErrorByName('captcha', t('CAPTCHA validation error: unknown CAPTCHA session ID. Contact the site administrator if this problem persists.'));
481
    \Drupal::logger('CAPTCHA')->error(
482
      'CAPTCHA validation error: unknown CAPTCHA session ID (%csid).',
483
      ['%csid' => var_export($csid, TRUE)]);
484
485
  }
  else {
486
487
488
    // Get CAPTCHA validate function or fall back on strict equality.
    $captcha_validate = $element['#captcha_validate'];
    if (!function_exists($captcha_validate)) {
489
      $captcha_validate = 'captcha_validate_strict_equality';
490
    }
491
    // Check the response with the CAPTCHA validation function.
Stefaan Lippens's avatar
Stefaan Lippens committed
492
    // Apart from the traditional expected $solution and received $response,
493
494
    // we also provide the CAPTCHA $element and $form_state
    // arrays for more advanced use cases.
495
    if ($captcha_validate($solution, $captcha_response, $element, $form_state)) {
496
497
498
      // Correct answer.
      $_SESSION['captcha_success_form_ids'][$form_id] = $form_id;
      // Record success.
499
      \Drupal::database()->update('captcha_sessions')
500
        ->condition('csid', $csid)
501
        ->fields(['status' => CAPTCHA_STATUS_SOLVED])
502
503
        ->expression('attempts', 'attempts + 1')
        ->execute();
504
505
506
    }
    else {
      // Wrong answer.
507
      \Drupal::database()->update('captcha_sessions')
508
509
510
        ->condition('csid', $csid)
        ->expression('attempts', 'attempts + 1')
        ->execute();
511

tonnosf's avatar
tonnosf committed
512
      $form_state->setErrorByName('captcha_response', t('The answer you entered for the CAPTCHA was not correct.'));
513
      // Update wrong response counter.
tonnosf's avatar
tonnosf committed
514
      if (\Drupal::config('captcha.settings')->get('enable_stats', FALSE)) {
515
        Drupal::state()->set('captcha.wrong_response_counter', Drupal::state()
516
          ->get('captcha.wrong_response_counter', 0) + 1);
517
      }
518

519
520
521
      if (\Drupal::config('captcha.settings')
        ->get('log_wrong_responses', FALSE)
      ) {
522
        \Drupal::logger('CAPTCHA')->notice(
523
          '%form_id post blocked by CAPTCHA module: challenge %challenge (by module %module), user answered "@response", but the solution was "@solution".',
524
          [
525
526
527
528
529
            '%form_id' => $form_id,
            '@response' => $captcha_response,
            '@solution' => $solution,
            '%challenge' => $captcha_info['captcha_type'],
            '%module' => $captcha_info['module'],
530
          ]);
531
532
533
534
535
536
      }
    }
  }
}

/**
537
 * Pre-render callback for additional processing of a CAPTCHA form element.
538
 *
539
 * This encompasses tasks that should happen after the general FAPI processing
540
541
 * (building, submission and validation) but before rendering
 * (e.g. storing the solution).
542
 *
543
544
545
546
547
 * @param array $element
 *   The CAPTCHA form element.
 *
 * @return array
 *   The manipulated element.
548
 */
549
function captcha_pre_render_process(array $element) {
550
551
  module_load_include('inc', 'captcha');

552
  // Get form and CAPTCHA information.
553
554
  $captcha_info = $element['#captcha_info'];
  $form_id = $captcha_info['form_id'];
555
  $captcha_sid = (int) ($captcha_info['captcha_sid']);
556
557
558
559
560
561
  // 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.
562
  if (_captcha_required_for_user($captcha_sid, $form_id) || $element['#captcha_admin_mode']) {
563
564
    // Update captcha_sessions table: store the solution
    // of the generated CAPTCHA.
565
566
    _captcha_update_captcha_session($captcha_sid, $captcha_info['solution']);

567
568
569
570
571
572
    // 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;
573
    }
574
575
576
  }
  else {
    // Remove CAPTCHA widgets from form.
577
    unset($element['captcha_widgets']);
578
579
  }

580
  return $element;
581
582
}

583
/**
584
 * Default implementation of hook_captcha().
585
586
587
588
 */
function captcha_captcha($op, $captcha_type = '') {
  switch ($op) {
    case 'list':
589
      return ['Math'];
Stefaan Lippens's avatar
Stefaan Lippens committed
590

591
592
    case 'generate':
      if ($captcha_type == 'Math') {
593
        $result = [];
594
595
596
597
        $answer = mt_rand(1, 20);
        $x = mt_rand(1, $answer);
        $y = $answer - $x;
        $result['solution'] = "$answer";
598
599
600
601
        // 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.
602
        $result['form']['captcha_response'] = [
603
          '#type' => 'textfield',
Stefaan Lippens's avatar
typo    
Stefaan Lippens committed
604
          '#title' => t('Math question'),
605
          '#description' => t('Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.'),
606
          '#field_prefix' => t('@x + @y =', ['@x' => $x, '@y' => $y]),
607
608
609
          '#size' => 4,
          '#maxlength' => 2,
          '#required' => TRUE,
610
          '#attributes' => [
611
            'autocomplete' => 'off',
612
          ],
613
          '#cache' => ['max-age' => 0],
614
        ];
615
        \Drupal::service('page_cache_kill_switch')->trigger();
616

617
618
        return $result;
      }
619
620
621
622
623
624
      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).
625
        $result = [
626
          'solution' => 'Test 123',
627
628
629
          'form' => [],
        ];
        $result['form']['captcha_response'] = [
630
          '#type' => 'textfield',
631
          '#title' => t('Test one two three'),
632
          '#required' => TRUE,
633
          '#cache' => ['max-age' => 0],
634
        ];
635
636
        \Drupal::service('page_cache_kill_switch')->trigger();

637
638
        return $result;
      }
Stefaan Lippens's avatar
Stefaan Lippens committed
639
      break;
640
641
  }
}