<?php
// $Id: image_captcha.module,v 1.31 2009/09/22 22:51:00 soxofaan Exp $

/**
 * @file
 * Implementation of image CAPTCHA for use with the CAPTCHA module
 */

// TODO: add hook_requirements to avoid installing when there is no GD support

define('IMAGE_CAPTCHA_ALLOWED_CHARACTERS', 'aAbBCdEeFfGHhijKLMmNPQRrSTtWXYZ23456789');

define('IMAGE_CAPTCHA_SETUP_OK', 0);
define('IMAGE_CAPTCHA_ERROR_NO_GDLIB_WITH_JPEG', 1);
define('IMAGE_CAPTCHA_ERROR_NO_TTF_SUPPORT', 2);
define('IMAGE_CAPTCHA_ERROR_TTF_FILE_READ_PROBLEM', 4);

define('IMAGE_CAPTCHA_FILE_FORMAT_JPG', 1);
define('IMAGE_CAPTCHA_FILE_FORMAT_PNG', 2);
define('IMAGE_CAPTCHA_FILE_FORMAT_TRANSPARENT_PNG', 3);


/**
 * Implementation of hook_help().
 */
function image_captcha_help($path, $arg) {
  switch ($path) {
    case 'admin/user/captcha/image_captcha':
      $output = '<p>'. t('The image CAPTCHA is a popular challenge where a random textual code is obfuscated in an image. The image is generated on the fly for each request, which is rather CPU intensive for the server. Be careful with the size and computation related settings.') .'</p>';
      return $output;
  }
}

/**
 * Implementation of hook_menu().
 */
function image_captcha_menu() {
  $items = array();
  // add an administration tab for image_captcha
  $items['admin/user/captcha/image_captcha'] = array(
    'title' => 'Image CAPTCHA',
    'file' => 'image_captcha.admin.inc',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('image_captcha_settings_form'),
    'access arguments' => array('administer CAPTCHA settings'),
    'type' => MENU_LOCAL_TASK,
  );
  // callback for generating an image
  $items['image_captcha'] = array(
    'file' => 'image_captcha.user.inc',
    'page callback' => 'image_captcha_image',
    'access callback' => TRUE,
    'type' => MENU_CALLBACK,
  );
  return $items;
}

/**
 * Helper function for getting the font setting.
 * @return array($font, $errno)
 */
function _image_captcha_get_font() {
  $font = variable_get('image_captcha_font', drupal_get_path('module', 'image_captcha') .'/fonts/Tesox/tesox.ttf');
  $errno = 0;
  if ($font != 'BUILTIN' && (!is_file($font) || !is_readable($font))) {
    $errno = IMAGE_CAPTCHA_ERROR_TTF_FILE_READ_PROBLEM;
  }
  return array($font, $errno);
}

/**
 * Helper function for splitting an utf8 string correctly in characters.
 * Assumes the given utf8 string is well formed.
 * See http://en.wikipedia.org/wiki/Utf8 for more info
 */
function _image_captcha_utf8_split($str) {
  $characters = array();
  $len = strlen($str);
  for ($i=0; $i < $len; ) {
    $chr = ord($str[$i]);
    if (($chr & 0x80) == 0x00) { // one byte character (0zzzzzzz)
      $width = 1;
    }
    else {
      if (($chr & 0xE0) == 0xC0) { // two byte character (first byte: 110yyyyy)
        $width = 2;
      }
      elseif (($chr & 0xF0) == 0xE0) { // three byte character (first byte: 1110xxxx)
        $width = 3;
      }
      elseif (($chr & 0xF8) == 0xF0) { // four byte character (first byte: 11110www)
        $width = 4;
      }
      else {
        watchdog('CAPTCHA', 'Encountered an illegal byte while splitting an utf8 string in characters.', array(), WATCHDOG_ERROR);
        return $characters;
      }
    }
    $characters[] = substr($str, $i, $width);
    $i += $width;
  }
  return $characters;
}

/**
 * Helper function for checking if the requirements for the Image CAPTCHA are met.
 *
 * @return error code such as IMAGE_CAPTCHA_SETUP_OK, IMAGE_CAPTCHA_ERROR_NO_GDLIB_WITH_JPEG,
 *   IMAGE_CAPTCHA_ERROR_NO_TTF_SUPPORT, IMAGE_CAPTCHA_ERROR_TTF_FILE_READ_PROBLEM.
 */
function _image_captcha_check_setup() {
  // Check GD library and is TTF support.
  if (!function_exists('imagejpeg')) {
    return IMAGE_CAPTCHA_ERROR_NO_GDLIB_WITH_JPEG;
  }
  if (!function_exists('imagettftext')) {
    return IMAGE_CAPTCHA_ERROR_NO_TTF_SUPPORT;
  }
  // Check font settings.
  list($font, $errno) = _image_captcha_get_font();
  if ($errno && IMAGE_CAPTCHA_ERROR_TTF_FILE_READ_PROBLEM) {
    return IMAGE_CAPTCHA_ERROR_TTF_FILE_READ_PROBLEM;
  }
  return IMAGE_CAPTCHA_SETUP_OK;
}

/**
 * Implementation of hook_captcha().
 */
function image_captcha_captcha($op, $captcha_type='', $captcha_sid=NULL) {
  switch ($op) {
    case 'list':
      // only offer image CAPTCHA if possible to generate an image CAPTCHA
      if (_image_captcha_check_setup() == IMAGE_CAPTCHA_SETUP_OK) {
        return array('Image');
      }
      else {
        return array();
      }
      break;

    case 'generate':
      if ($captcha_type == 'Image') {
        // In offline mode, the image CAPTCHA does not work because the request
        // for the image itself won't succeed (only ?q=user is permitted for
        // unauthenticated users). We fall back to the Math CAPTCHA in that case.
        global $user;
        if (variable_get('site_offline', FALSE) && $user->uid == 0) {
          return captcha_captcha('generate', 'Math');
        }
        // generate a CAPTCHA code
        $allowed_chars = _image_captcha_utf8_split(variable_get('image_captcha_image_allowed_chars', IMAGE_CAPTCHA_ALLOWED_CHARACTERS));
        $code_length = (int)variable_get('image_captcha_code_length', 5);
        $code = '';
        for ($i = 0; $i < $code_length; $i++) {
          $code .= $allowed_chars[array_rand($allowed_chars)];
        }

        // build the result to return
        $result = array();

        // Handle the case insesitivity option and change the code to lower case
        // before saving it as solution.
        if (CAPTCHA_DEFAULT_VALIDATION_CASE_SENSITIVE == variable_get('captcha_default_validation', CAPTCHA_DEFAULT_VALIDATION_CASE_INSENSITIVE)) {
          $description = t('Enter the characters shown in the image. Ignore spaces and be careful about upper and lower case.');
        }
        else {
          $description = t('Enter the characters (without spaces) shown in the image.');
        }

        $result['solution'] = $code;
        // Generate image source URL (add timestamp to avoid problems with
        // client side caching: subsequent images of the same CAPTCHA session
        // have the same URL, but should display a different code).
        $img_src = check_url(url("image_captcha/$captcha_sid/". time()));
        $result['form']['captcha_image'] = array(
          '#type' => 'markup',
          '#value' => '<img src="'. $img_src .'" alt="'. t('Image CAPTCHA') .'" title="'. t('Image CAPTCHA') .'" />',
          '#weight' => -2,
        );
        $result['form']['captcha_response'] = array(
          '#type' => 'textfield',
          '#title' => t('What code is in the image?'),
          '#description' => $description,
          '#weight' => 0,
          '#required' => TRUE,
          '#size' => 15,
        );
        return $result;
      }
      break;

  }
}
