phrase_captcha.module in CAPTCHA Pack 8
Implementation of a phrase based CAPTCHA, for use with the CAPTCHA module.
@todo: add character picking of current word, which is harder for spam bots than plain word guessing.
File
text_captcha/modules/phrase_captcha/phrase_captcha.moduleView source
<?php
/**
* @file
* Implementation of a phrase based CAPTCHA, for use with the CAPTCHA module.
*
* @todo: add character picking of current word, which is harder for spam bots
* than plain word guessing.
*/
define('PHRASE_CAPTCHA_GENERATE_NONSENSE_WORDS', 0);
define('PHRASE_CAPTCHA_USER_DEFINED_WORDS', 1);
/**
* Implements hook_help().
*/
function phrase_captcha_help($path, $arg) {
switch ($path) {
case 'admin/config/people/captcha/phrase_captcha':
return '<p>' . t('This phrase based CAPTCHA presents a CAPTCHA phrase of a given number of words and asks to pick the right word (based on counting, alphabetical order, etc).') . '</p>';
}
}
/**
* Implements hook_captcha().
*/
function phrase_captcha_captcha($op, $captcha_type = '') {
$config = \Drupal::config('phrase_captcha.settings');
switch ($op) {
case 'list':
return [
'Phrase CAPTCHA',
];
case 'generate':
if ($captcha_type == 'Phrase CAPTCHA') {
// Generate words.
$words = _phrase_captcha_generate_words($config
->get('phrase_captcha_word_quantity'));
// Pick a random word selection challenge.
$word_challenges = _phrase_captcha_enabled_word_challenges();
$key = array_rand($word_challenges);
$function = $word_challenges[$key];
list($phrase_words, $question, $solution) = call_user_func($function, $words);
// Build options list.
$all_words = array_merge($words, _phrase_captcha_generate_words($config
->get('phrase_captcha_additional_word_quantity')));
shuffle($all_words);
$options = [];
foreach ($all_words as $word) {
$options[$word] = $word;
}
// Store the answer and build the form elements.
$captcha = [];
$captcha['solution'] = $solution;
$captcha['form']['captcha_phrase'] = [
'#type' => 'markup',
'#value' => '"' . implode(' ', $phrase_words) . '"',
'#weight' => -2,
];
$captcha['form']['captcha_response'] = [
'#type' => 'radios',
'#title' => $question,
'#options' => $options,
// Extra class needed for additional CSS'ing of the options.
'#attributes' => [
'class' => [
'text-captcha-word-list-radios',
],
],
// The following entry '#DANGEROUS_SKIP_CHECK' is needed to prevent
// that Drupal checks during validation phase if a submitted option
// is in the list of possible options. (see includes/form.inc)
// The options are randomly generated on each call and consequently
// almost never the same during the generate phase and the validation
// phase.
//
'#DANGEROUS_SKIP_CHECK' => TRUE,
'#required' => TRUE,
'#cache' => [
'max-age' => 0,
],
];
\Drupal::service('page_cache_kill_switch')
->trigger();
// Add css to form.
$captcha['form']['captcha_response']['#attached']['library'][] = 'text_captcha/base';
return $captcha;
}
}
}
/**
* Function for generating a random nonsense word of a given number of chars.
*/
function _phrase_captcha_generate_nonsense_word($characters) {
$vowels = 'bcdfghjklmnpqrstvwxyz';
$consonants = 'aeiou';
$vowel_max = strlen($vowels) - 1;
$consonant_max = strlen($consonants) - 1;
$word = '';
// Randomly start with vowel or consonant.
$o = mt_rand(0, 1);
for ($i = 0; $i < $characters; ++$i) {
if (($i + $o) % 2) {
$word .= $consonants[mt_rand(0, $consonant_max)];
}
else {
$word .= $vowels[mt_rand(0, $vowel_max)];
}
}
return $word;
}
/**
* Function for generating an array of words.
*/
function _phrase_captcha_generate_words($num) {
$words = [];
if (\Drupal::config('phrase_captcha.settings')
->get('phrase_captcha_words') == PHRASE_CAPTCHA_USER_DEFINED_WORDS) {
// Use user defined words.
$uwords = _text_captcha_word_pool_get_content('phrase_captcha_userdefined_word_pool', NULL, '', TRUE);
switch ($num) {
case 0:
break;
case 1:
$words[] = $uwords[array_rand($uwords, $num)];
break;
default:
$keys = array_rand($uwords, $num);
foreach ($keys as $key) {
$words[] = $uwords[$key];
}
break;
}
}
else {
// Generate nonsense words.
for ($w = 0; $w < $num; ++$w) {
$words[] = _phrase_captcha_generate_nonsense_word(mt_rand(3, 7));
}
}
return $words;
}
/**
* Function that returns a textual representation of an ordinal.
*/
function _phrase_captcha_ordinal($n) {
$ordinalmap = [
1 => t('first'),
2 => t('second'),
3 => t('third'),
4 => t('fourth'),
5 => t('fifth'),
6 => t('sixth'),
7 => t('seventh'),
8 => t('eighth'),
9 => t('ninth'),
10 => t('tenth'),
];
if (array_key_exists($n, $ordinalmap)) {
return $ordinalmap[$n];
}
else {
return "{$n}th";
}
}
/**
* Return array with available word challenges.
*/
function _phrase_captcha_available_word_challenges() {
return [
'_phrase_captcha_word_question_word_index' => 'Word index',
'_phrase_captcha_word_question_alphabetical_misplaced' => 'Alphabetical misplacement',
'_phrase_captcha_word_question_double_occurence' => 'Double occurence',
];
}
/**
* Return enabled challenges.
*/
function _phrase_captcha_enabled_word_challenges() {
$word_challenges = \Drupal::config('phrase_captcha.settings')
->get('phrase_captcha_word_selection_challenges');
if ($word_challenges) {
return array_keys(array_filter($word_challenges));
}
else {
return array_keys(_phrase_captcha_available_word_challenges());
}
}
/**
* Return details of 'word index' challenge.
*/
function _phrase_captcha_word_question_word_index($words) {
$key = array_rand($words, 1);
$answer = $words[$key];
if (mt_rand(0, 1)) {
$description = t('What is the @nth word in the CAPTCHA phrase above?', [
'@nth' => _phrase_captcha_ordinal($key + 1),
]);
}
else {
$n = count($words) - $key;
if ($n == 1) {
$description = t('What is the last word in the CAPTCHA phrase above?');
}
else {
$description = t('What is the @nth last word in the CAPTCHA phrase above?', [
'@nth' => _phrase_captcha_ordinal($n),
]);
}
}
return [
$words,
$description,
$answer,
];
}
/**
* Return details of 'alphabetical misplaced' challenge.
*/
function _phrase_captcha_word_question_alphabetical_misplaced($words) {
// Sort the words.
mt_rand(0, 1) ? sort($words) : rsort($words);
// Pick a word and its new destination
// New destination has to be at least 2 places from the original place,
// otherwise it could lead to something like swapping two neighbours,
// in which case there is no unique answer.
$from = 0;
$to = 0;
while (abs($from - $to) < 2) {
$from = array_rand($words, 1);
$to = array_rand($words, 1);
}
// Get the word.
$answer = $words[$from];
// Move the word from $from to $to.
unset($words[$from]);
array_splice($words, $to, 0, $answer);
// bBuild the description.
$description = t('Which word does not follow the alphabetical order in the CAPTCHA phrase above?');
return [
$words,
$description,
$answer,
];
}
/**
* Return details of 'double occurrence' challenge.
*/
function _phrase_captcha_word_question_double_occurence($words) {
// Assure single occurence of each word.
$words = array_unique($words);
// Pick a word.
$key = array_rand($words, 1);
$answer = $words[$key];
// Replace another word with it.
while (($pos = array_rand($words, 1)) == $key) {
// NOP aka NOOP aka pass.
}
array_splice($words, $pos, 1, $answer);
$description = t('Which word occurs two times in the CAPTCHA phrase above?');
return [
$words,
$description,
$answer,
];
}
Functions
Name | Description |
---|---|
phrase_captcha_captcha | Implements hook_captcha(). |
phrase_captcha_help | Implements hook_help(). |
_phrase_captcha_available_word_challenges | Return array with available word challenges. |
_phrase_captcha_enabled_word_challenges | Return enabled challenges. |
_phrase_captcha_generate_nonsense_word | Function for generating a random nonsense word of a given number of chars. |
_phrase_captcha_generate_words | Function for generating an array of words. |
_phrase_captcha_ordinal | Function that returns a textual representation of an ordinal. |
_phrase_captcha_word_question_alphabetical_misplaced | Return details of 'alphabetical misplaced' challenge. |
_phrase_captcha_word_question_double_occurence | Return details of 'double occurrence' challenge. |
_phrase_captcha_word_question_word_index | Return details of 'word index' challenge. |
Constants
Name | Description |
---|---|
PHRASE_CAPTCHA_GENERATE_NONSENSE_WORDS | @file Implementation of a phrase based CAPTCHA, for use with the CAPTCHA module. |
PHRASE_CAPTCHA_USER_DEFINED_WORDS |