You are here

phrase_captcha.module in CAPTCHA Pack 5

File

text_captcha/phrase_captcha/phrase_captcha.module
View source
<?php

/**
 * 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.
 */
require_once drupal_get_path('module', 'phrase_captcha') . '/../text_captcha.inc';
define('PHRASE_CAPTCHA_GENERATE_NONSENSE_WORDS', 0);
define('PHRASE_CAPTCHA_USER_DEFINED_WORDS', 1);

/**
 * Implementation of hook_help().
 */
function phrase_captcha_help($section) {
  switch ($section) {
    case 'admin/user/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>';
  }
}

/**
 * Implementation of hook_menu().
 */
function phrase_captcha_menu($may_cache) {
  $items = array();
  if ($may_cache) {

    // add an administration tab for phrase_captcha
    $items[] = array(
      'path' => 'admin/user/captcha/phrase_captcha',
      'title' => t('Phrase CAPTCHA'),
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        'phrase_captcha_settings_form',
      ),
      'type' => MENU_LOCAL_TASK,
    );
  }
  return $items;
}

/**
 * Administration form
 */
function phrase_captcha_settings_form() {
  $form = array();

  // radio buttons for selecting the kind of words to use
  $form['phrase_captcha_words'] = array(
    '#type' => 'radios',
    '#title' => t('Kind of words to use in the CAPTCHA phrase'),
    '#options' => array(
      PHRASE_CAPTCHA_GENERATE_NONSENSE_WORDS => t('Generate nonsense words'),
      PHRASE_CAPTCHA_USER_DEFINED_WORDS => t('Use user defined words'),
    ),
    '#default_value' => variable_get('phrase_captcha_words', PHRASE_CAPTCHA_GENERATE_NONSENSE_WORDS),
    '#required' => TRUE,
  );

  // form elements for the word pools
  _text_captcha_word_pool_form_items($form, 'phrase_captcha_userdefined_word_pool', t('User defined word pool'), t('Enter the words to use in the CAPTCHA phrase (space separated, no punctuation).'), '');

  // select form element for the number of words in the CAPTCHA phrase
  $form['phrase_captcha_word_quantity'] = array(
    '#type' => 'select',
    '#title' => t('Number of words in the CAPTCHA phrase'),
    '#default_value' => (int) variable_get('phrase_captcha_word_quantity', 5),
    '#options' => array(
      4 => 4,
      5 => 5,
      6 => 6,
      7 => 7,
      8 => 8,
      9 => 9,
      10 => 10,
    ),
    '#required' => TRUE,
  );

  // select form element for the number of additional words
  $form['phrase_captcha_additional_word_quantity'] = array(
    '#type' => 'select',
    '#title' => t('Maximum number of additional words to let the user choose from'),
    '#default_value' => (int) variable_get('phrase_captcha_additional_word_quantity', 1),
    '#options' => array(
      0 => 0,
      1 => 1,
      2 => 2,
      3 => 3,
      4 => 4,
      5 => 5,
    ),
    '#required' => TRUE,
  );
  $form['phrase_captcha_word_selection_challenges'] = array(
    '#type' => 'checkboxes',
    '#title' => t('Word selection challenges'),
    '#options' => _phrase_captcha_available_word_challenges(),
    '#default_value' => _phrase_captcha_enabled_word_challenges(),
  );
  return system_settings_form($form);
}

/**
 * Validate function of the administration form
 */
function phrase_captcha_settings_form_validate($form_id, $form_values) {
  if ($form_id == 'phrase_captcha_settings_form') {
    if ($form_values['phrase_captcha_words'] == PHRASE_CAPTCHA_USER_DEFINED_WORDS) {
      $word_count_minimum = (int) $form_values['phrase_captcha_word_quantity'] + (int) $form_values['phrase_captcha_additional_word_quantity'] + 2;
      _text_captcha_word_pool_validate('phrase_captcha_userdefined_word_pool', $form_values, $word_count_minimum, NULL, NULL);
    }

    // check word selection
    if (count(array_filter($form_values['phrase_captcha_word_selection_challenges'])) < 1) {
      form_set_error('phrase_captcha_word_selection_challenges', t('You need to select at least one word selection criterium'));
    }
  }
}

/**
 * function for generating a random nonsense word of a given number of characters
 */
function _phrase_captcha_generate_nonsense_word($characters) {
  $vowels = 'bcdfghjklmnpqrstvwxyz';
  $consonants = 'aeiou';
  $vowel_max = strlen($vowels) - 1;
  $consonant_max = strlen($consonants) - 1;
  $word = '';
  $o = mt_rand(0, 1);

  // randomly start with vowel or consonant
  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 = array();
  if (variable_get('phrase_captcha_words', PHRASE_CAPTCHA_GENERATE_NONSENSE_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 represention of an ordinal
 */
function _phrase_captcha_ordinal($n) {
  $ordinalmap = array(
    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";
  }
}
function _phrase_captcha_available_word_challenges() {
  return array(
    '_phrase_captcha_word_question_word_index' => 'Word index',
    '_phrase_captcha_word_question_alphabetical_misplaced' => 'Alphabetical misplacement',
    '_phrase_captcha_word_question_double_occurence' => 'Double occurence',
  );
}
function _phrase_captcha_enabled_word_challenges() {
  $word_challenges = variable_get('phrase_captcha_word_selection_challenges', array());
  if ($word_challenges) {
    return array_keys(array_filter($word_challenges));
  }
  else {
    return array_keys(_phrase_captcha_available_word_challenges());
  }
}
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?', array(
      '@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?', array(
        '@nth' => _phrase_captcha_ordinal($n),
      ));
    }
  }
  return array(
    $words,
    $description,
    $answer,
  );
}
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 = $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);

  // build the description
  $description = t('Which word does not follow the alphabetical order in the CAPTCHA phrase above?');
  return array(
    $words,
    $description,
    $answer,
  );
}
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) {
  }
  array_splice($words, $pos, 1, $answer);
  $description = t('Which word occurs two times in the CAPTCHA phrase above?');
  return array(
    $words,
    $description,
    $answer,
  );
}

/**
 * Implementation of hook_captcha
 */
function phrase_captcha_captcha($op, $captcha_type = '') {
  switch ($op) {
    case 'list':
      return array(
        'Phrase CAPTCHA',
      );
    case 'generate':
      if ($captcha_type == 'Phrase CAPTCHA') {

        // generate words
        $words = _phrase_captcha_generate_words((int) variable_get('phrase_captcha_word_quantity', 5));

        // 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((int) variable_get('phrase_captcha_additional_word_quantity', 1)));
        shuffle($all_words);
        $options = array();
        foreach ($all_words as $word) {
          $options[$word] = $word;
        }

        // store the answer and build the form elements
        $captcha = array();
        $captcha['solution'] = $solution;
        $captcha['form']['captcha_phrase'] = array(
          '#type' => 'markup',
          '#value' => '"' . implode(' ', $phrase_words) . '"',
          '#weight' => -2,
        );
        $captcha['form']['captcha_response'] = array(
          '#type' => 'radios',
          '#title' => $question,
          '#options' => $options,
          // extra class needed for additional CSS'ing of the options
          '#attributes' => array(
            '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,
        );

        // additional text CAPTCHA CSS rules
        drupal_add_css(drupal_get_path('module', 'phrase_captcha') . '/../text_captcha.css');
        return $captcha;
      }
  }
}

Functions

Namesort descending Description
phrase_captcha_captcha Implementation of hook_captcha
phrase_captcha_help Implementation of hook_help().
phrase_captcha_menu Implementation of hook_menu().
phrase_captcha_settings_form Administration form
phrase_captcha_settings_form_validate Validate function of the administration form
_phrase_captcha_available_word_challenges
_phrase_captcha_enabled_word_challenges
_phrase_captcha_generate_nonsense_word function for generating a random nonsense word of a given number of characters
_phrase_captcha_generate_words function for generating an array of words
_phrase_captcha_ordinal function that returns a textual represention of an ordinal
_phrase_captcha_word_question_alphabetical_misplaced
_phrase_captcha_word_question_double_occurence
_phrase_captcha_word_question_word_index

Constants